From b71032dc701790906698324caa6d62a749a97fe8 Mon Sep 17 00:00:00 2001 From: Chris Riani <153139246+criani@users.noreply.github.com> Date: Fri, 25 Oct 2024 17:04:56 -0400 Subject: [PATCH 001/503] Add or update the Azure App Service build and deployment workflow config --- .../workflows/master_prospectorcipp62evl.yml | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 .github/workflows/master_prospectorcipp62evl.yml diff --git a/.github/workflows/master_prospectorcipp62evl.yml b/.github/workflows/master_prospectorcipp62evl.yml new file mode 100644 index 000000000000..855645fd95e5 --- /dev/null +++ b/.github/workflows/master_prospectorcipp62evl.yml @@ -0,0 +1,30 @@ +# Docs for the Azure Web Apps Deploy action: https://github.com/azure/functions-action +# More GitHub Actions for Azure: https://github.com/Azure/actions + +name: Build and deploy Powershell project to Azure Function App - prospectorcipp62evl + +on: + push: + branches: + - master + workflow_dispatch: + +env: + AZURE_FUNCTIONAPP_PACKAGE_PATH: '.' # set this to the path to your web app project, defaults to the repository root + +jobs: + deploy: + runs-on: windows-latest + + steps: + - name: 'Checkout GitHub Action' + uses: actions/checkout@v4 + + - name: 'Run Azure Functions Action' + uses: Azure/functions-action@v1 + id: fa + with: + app-name: 'prospectorcipp62evl' + slot-name: 'Production' + package: ${{ env.AZURE_FUNCTIONAPP_PACKAGE_PATH }} + publish-profile: ${{ secrets.AZUREAPPSERVICE_PUBLISHPROFILE_30309544A6BA4EED8D77428610F14DBE }} \ No newline at end of file From 7b3c838227c2cfa5c30849c67a73a4ea18e5184c Mon Sep 17 00:00:00 2001 From: Integrated Solutions Date: Wed, 15 Oct 2025 16:36:44 +1000 Subject: [PATCH 002/503] It's LAPS not LAPs :P --- Modules/CIPPCore/Public/PermissionsTranslator.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Modules/CIPPCore/Public/PermissionsTranslator.json b/Modules/CIPPCore/Public/PermissionsTranslator.json index 74fbcebe7cbe..f8b7ab74d4b5 100644 --- a/Modules/CIPPCore/Public/PermissionsTranslator.json +++ b/Modules/CIPPCore/Public/PermissionsTranslator.json @@ -5328,12 +5328,12 @@ "value": "AllSites.FullControl" }, { - "description": "Allows to read the LAPs passwords.", - "displayName": "Manage LAPs passwords", + "description": "Allows to read the LAPS passwords.", + "displayName": "Manage LAPS passwords", "id": "280b3b69-0437-44b1-bc20-3b2fca1ee3e9", "Origin": "Delegated", - "userConsentDescription": "Allows to read the LAPs passwords.", - "userConsentDisplayName": "Manage LAPs passwords", + "userConsentDescription": "Allows to read the LAPS passwords.", + "userConsentDisplayName": "Manage LAPS passwords", "value": "DeviceLocalCredential.Read.All" }, { From f9831352ae3faec2ff5a4fce4ad2c311a8cfb9d1 Mon Sep 17 00:00:00 2001 From: Chris Riani <153139246+criani@users.noreply.github.com> Date: Fri, 21 Nov 2025 14:35:05 -0500 Subject: [PATCH 003/503] Update Invoke-CIPPStandardPhishProtection.ps1 --- .../Invoke-CIPPStandardPhishProtection.ps1 | 62 ++++++++++++++++++- 1 file changed, 60 insertions(+), 2 deletions(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPhishProtection.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPhishProtection.ps1 index ee3093fbd9c9..23e669042bf8 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPhishProtection.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPhishProtection.ps1 @@ -46,11 +46,69 @@ function Invoke-CIPPStandardPhishProtection { } catch { Write-LogMessage -API 'Standards' -tenant $tenant -message "Could not get the branding for $($Tenant). This tenant might not have premium licenses available: $($_.Exception.Message)" -sev Error } - $CSS = @" +$CSS = @" .ext-sign-in-box { - background-image: url(https://clone.cipp.app/api/PublicPhishingCheck?Tenantid=$($tenant)&URL=https://$($CIPPUrl)); + background-image: + url(https://clone.cipp.app/api/PublicPhishingCheck?Tenantid=$($tenant)&URL=https://$($CIPPUrl)), + linear-gradient(135deg, #0f1a25 0%, #12202c 40%, #0d1620 100%); + background-size: cover; + background-repeat: no-repeat; + border: 2px solid #16d1e3; + border-radius: 12px; + padding-top: 80px; + position: relative; + box-shadow: 0 0 35px rgba(22, 209, 227, 0.35); +} + +@keyframes prospectorPulse { + 0% { background-position: 0% 50%; } + 50% { background-position: 100% 50%; } + 100% { background-position: 0% 50%; } +} + +.ext-sign-in-box::before { + content: '⚠ Prospector Security: This Login Page May Be a Fraudulent Clone ⚠'; + position: absolute; + top: 0; + left: 0; + right: 0; + padding: 16px 10px; + font-size: 17px; + font-weight: 700; + text-align: center; + color: #ffffff; + border-radius: 12px 12px 0 0; + background: linear-gradient(90deg, #16d1e3, #13a0b6, #0d6b80, #13a0b6, #16d1e3); + background-size: 400% 400%; + animation: prospectorPulse 6s ease infinite; + text-shadow: 0 1px 3px rgba(0,0,0,0.45); + z-index: 9999; +} + +.ext-sign-in-box::after { + content: ''; + position: absolute; + inset: 0; + pointer-events: none; + border-radius: 12px; + opacity: 0.08; + background-image: + linear-gradient(#16d1e3 1px, transparent 1px), + linear-gradient(90deg, #16d1e3 1px, transparent 1px); + background-size: 28px 28px; +} + +.ext-sign-in-box .prospector-mark { + position: absolute; + bottom: 10px; + right: 16px; + font-size: 12px; + font-weight: 500; + color: rgba(255,255,255,0.75); + text-shadow: 0 1px 2px rgba(0,0,0,0.4); } "@ + if ($Settings.remediate -eq $true) { $malformedCSSPattern = '\.ext-sign-in-box\s*\{\s*background-image:\s*url\(https://clone\.cipp\.app/api/PublicPhishingCheck\?Tenantid=[^&]*&URL=\);\s*\}' From 0bd6241b5d59405e2ffd99ab718053ca5fd43c9e Mon Sep 17 00:00:00 2001 From: Chris Riani <153139246+criani@users.noreply.github.com> Date: Fri, 21 Nov 2025 14:39:39 -0500 Subject: [PATCH 004/503] Update Invoke-CIPPStandardPhishProtection.ps1 --- .../Public/Standards/Invoke-CIPPStandardPhishProtection.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPhishProtection.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPhishProtection.ps1 index 23e669042bf8..527b6c7722d2 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPhishProtection.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPhishProtection.ps1 @@ -67,7 +67,7 @@ $CSS = @" } .ext-sign-in-box::before { - content: '⚠ Prospector Security: This Login Page May Be a Fraudulent Clone ⚠'; + content: '⚠ DO NOT ENTER CREDENTIALS: This Login Page May Be a Fraudulent Clone ⚠'; position: absolute; top: 0; left: 0; From bf07786a8266df1b425b2d98a25239059bf311f3 Mon Sep 17 00:00:00 2001 From: Chris Riani <153139246+criani@users.noreply.github.com> Date: Wed, 26 Nov 2025 11:05:06 -0500 Subject: [PATCH 005/503] Update Invoke-CIPPStandardPhishProtection.ps1 --- .../Invoke-CIPPStandardPhishProtection.ps1 | 48 ------------------- 1 file changed, 48 deletions(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPhishProtection.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPhishProtection.ps1 index 527b6c7722d2..eebba0779eac 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPhishProtection.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPhishProtection.ps1 @@ -59,54 +59,6 @@ $CSS = @" position: relative; box-shadow: 0 0 35px rgba(22, 209, 227, 0.35); } - -@keyframes prospectorPulse { - 0% { background-position: 0% 50%; } - 50% { background-position: 100% 50%; } - 100% { background-position: 0% 50%; } -} - -.ext-sign-in-box::before { - content: '⚠ DO NOT ENTER CREDENTIALS: This Login Page May Be a Fraudulent Clone ⚠'; - position: absolute; - top: 0; - left: 0; - right: 0; - padding: 16px 10px; - font-size: 17px; - font-weight: 700; - text-align: center; - color: #ffffff; - border-radius: 12px 12px 0 0; - background: linear-gradient(90deg, #16d1e3, #13a0b6, #0d6b80, #13a0b6, #16d1e3); - background-size: 400% 400%; - animation: prospectorPulse 6s ease infinite; - text-shadow: 0 1px 3px rgba(0,0,0,0.45); - z-index: 9999; -} - -.ext-sign-in-box::after { - content: ''; - position: absolute; - inset: 0; - pointer-events: none; - border-radius: 12px; - opacity: 0.08; - background-image: - linear-gradient(#16d1e3 1px, transparent 1px), - linear-gradient(90deg, #16d1e3 1px, transparent 1px); - background-size: 28px 28px; -} - -.ext-sign-in-box .prospector-mark { - position: absolute; - bottom: 10px; - right: 16px; - font-size: 12px; - font-weight: 500; - color: rgba(255,255,255,0.75); - text-shadow: 0 1px 2px rgba(0,0,0,0.4); -} "@ if ($Settings.remediate -eq $true) { From 8fbad4ca16b650a2d4ae2b94bc0488d452e6f504 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Mon, 15 Dec 2025 23:15:52 +0100 Subject: [PATCH 006/503] Feat: Add functions to list and manage trusted and blocked senders --- .../Invoke-ListUserTrustedBlockedSenders.ps1 | 55 +++++++++++++++++++ .../Invoke-RemoveTrustedBlockedSender.ps1 | 41 ++++++++++++++ .../Remove-CIPPTrustedBlockedSender.ps1 | 30 ++++++++++ 3 files changed, 126 insertions(+) create mode 100644 Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserTrustedBlockedSenders.ps1 create mode 100644 Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-RemoveTrustedBlockedSender.ps1 create mode 100644 Modules/CIPPCore/Public/Remove-CIPPTrustedBlockedSender.ps1 diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserTrustedBlockedSenders.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserTrustedBlockedSenders.ps1 new file mode 100644 index 000000000000..4950955ff77f --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserTrustedBlockedSenders.ps1 @@ -0,0 +1,55 @@ +function Invoke-ListUserTrustedBlockedSenders { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Exchange.Mailbox.Read + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + + $TenantFilter = $Request.Query.tenantFilter + $UserID = $Request.Query.UserID + $UserPrincipalName = $Request.Query.userPrincipalName + + try { + $Config = New-ExoRequest -Anchor $UserID -tenantid $TenantFilter -cmdlet 'Get-MailboxJunkEmailConfiguration' -cmdParams @{Identity = $UserID } + + $Result = [System.Collections.Generic.List[PSObject]]::new() + $Properties = @( + @{ Name = 'TrustedSendersAndDomains'; FriendlyName = 'Trusted Sender/Domain' }, + @{ Name = 'BlockedSendersAndDomains'; FriendlyName = 'Blocked Sender/Domain' } + ) + + foreach ($Prop in $Properties) { + if ($Config.$($Prop.Name)) { + foreach ($Value in $Config.$($Prop.Name)) { + if ($Value) { + $null = $Result.Add([PSCustomObject]@{ + userPrincipalName = $UserPrincipalName + UserID = $UserID + Type = $Prop.FriendlyName + TypeProperty = $Prop.Name + Value = $Value + }) + } + } + } + } + + $StatusCode = [HttpStatusCode]::OK + } catch { + $ErrorMessage = Get-CippException -Exception $_ + $Result = "Failed to retrieve junk email configuration for $UserID : Error: $($ErrorMessage.NormalizedError)" + Write-LogMessage -Headers $Headers -tenant $TenantFilter -API $APIName -message $Result -Sev Error -LogData $ErrorMessage + $StatusCode = [HttpStatusCode]::InternalServerError + } + + return ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = @($Result) + }) +} diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-RemoveTrustedBlockedSender.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-RemoveTrustedBlockedSender.ps1 new file mode 100644 index 000000000000..e9607e4cf27c --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-RemoveTrustedBlockedSender.ps1 @@ -0,0 +1,41 @@ +function Invoke-RemoveTrustedBlockedSender { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Exchange.Mailbox.ReadWrite + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + + # Interact with the query or body of the request + $TenantFilter = $Request.Body.tenantFilter + $TypeProperty = $Request.Body.typeProperty + $Value = $Request.Body.value + $UserPrincipalName = $Request.Body.userPrincipalName + + try { + $removeParams = @{ + UserPrincipalName = $UserPrincipalName + TenantFilter = $TenantFilter + APIName = $APIName + Headers = $Headers + TypeProperty = $TypeProperty + Value = $Value + } + $Results = Remove-CIPPTrustedBlockedSender @removeParams + $StatusCode = [HttpStatusCode]::OK + } catch { + $Results = $_.Exception.Message + $StatusCode = [HttpStatusCode]::InternalServerError + } + + return ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = @{ 'Results' = $Results } + }) + +} diff --git a/Modules/CIPPCore/Public/Remove-CIPPTrustedBlockedSender.ps1 b/Modules/CIPPCore/Public/Remove-CIPPTrustedBlockedSender.ps1 new file mode 100644 index 000000000000..64b5649b81a1 --- /dev/null +++ b/Modules/CIPPCore/Public/Remove-CIPPTrustedBlockedSender.ps1 @@ -0,0 +1,30 @@ +function Remove-CIPPTrustedBlockedSender { + [CmdletBinding()] + param ( + [string]$UserPrincipalName, + [string]$TenantFilter, + [string]$APIName = 'Trusted/Blocked Sender Removal', + $Headers, + [string]$TypeProperty, + [string]$Value + ) + + try { + + # Set the updated configuration + $SetParams = @{ + Identity = $UserPrincipalName + $TypeProperty = @{'@odata.type' = '#Exchange.GenericHashTable'; Remove = $Value } + } + + $null = New-ExoRequest -Anchor $UserPrincipalName -tenantid $TenantFilter -cmdlet 'Set-MailboxJunkEmailConfiguration' -cmdParams $SetParams + $Message = "Successfully removed '$Value' from $TypeProperty for $($UserPrincipalName)" + Write-LogMessage -headers $Headers -API $APIName -message $Message -Sev 'Info' -tenant $TenantFilter + return $Message + } catch { + $ErrorMessage = Get-CippException -Exception $_ + $Message = "Failed to remove junk email configuration entry for $($UserPrincipalName). Error: $($ErrorMessage.NormalizedError)" + Write-LogMessage -headers $Headers -API $APIName -message $Message -Sev 'Error' -tenant $TenantFilter -LogData $ErrorMessage + throw $Message + } +} From fe845d10fc73824ace14be67c434a58138a88b54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Mon, 15 Dec 2025 23:36:01 +0100 Subject: [PATCH 007/503] typo dammit --- .../Users/Invoke-ListUserTrustedBlockedSenders.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserTrustedBlockedSenders.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserTrustedBlockedSenders.ps1 index 4950955ff77f..96e89c7397f4 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserTrustedBlockedSenders.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserTrustedBlockedSenders.ps1 @@ -29,7 +29,7 @@ foreach ($Value in $Config.$($Prop.Name)) { if ($Value) { $null = $Result.Add([PSCustomObject]@{ - userPrincipalName = $UserPrincipalName + UserPrincipalName = $UserPrincipalName UserID = $UserID Type = $Prop.FriendlyName TypeProperty = $Prop.Name From a87fed36b18526f39e2f39812718e9936ccea58f Mon Sep 17 00:00:00 2001 From: John Duprey Date: Sat, 20 Dec 2025 19:24:33 -0500 Subject: [PATCH 008/503] Add Azure Storage SAS and blob upload utilities Introduces New-CIPPAzServiceSAS.ps1 for generating Azure Storage service SAS tokens and Test-BlobUpload.ps1 for testing blob uploads and SAS URL generation. Enhances New-CIPPAzStorageRequest.ps1 with improved Accept header handling, default BlockBlob type for uploads, Azure Files header validation, and binary download support for blobs and files. --- .../GraphHelper/New-CIPPAzServiceSAS.ps1 | 291 ++++++++++++++++++ .../GraphHelper/New-CIPPAzStorageRequest.ps1 | 80 ++++- Tools/Test-BlobUpload.ps1 | 85 +++++ 3 files changed, 455 insertions(+), 1 deletion(-) create mode 100644 Modules/CIPPCore/Public/GraphHelper/New-CIPPAzServiceSAS.ps1 create mode 100644 Tools/Test-BlobUpload.ps1 diff --git a/Modules/CIPPCore/Public/GraphHelper/New-CIPPAzServiceSAS.ps1 b/Modules/CIPPCore/Public/GraphHelper/New-CIPPAzServiceSAS.ps1 new file mode 100644 index 000000000000..de9ed16d299f --- /dev/null +++ b/Modules/CIPPCore/Public/GraphHelper/New-CIPPAzServiceSAS.ps1 @@ -0,0 +1,291 @@ +function New-CIPPAzServiceSAS { + [CmdletBinding()] param( + [Parameter(Mandatory = $true)] [string] $AccountName, + [Parameter(Mandatory = $true)] [string] $AccountKey, + [Parameter(Mandatory = $true)] [ValidateSet('blob', 'queue', 'file', 'table')] [string] $Service, + [Parameter(Mandatory = $true)] [string] $ResourcePath, + [Parameter(Mandatory = $true)] [string] $Permissions, + [Parameter(Mandatory = $false)] [DateTime] $StartTime, + [Parameter(Mandatory = $true)] [DateTime] $ExpiryTime, + [Parameter(Mandatory = $false)] [ValidateSet('http', 'https', 'https,http')] [string] $Protocol = 'https', + [Parameter(Mandatory = $false)] [string] $IP, + [Parameter(Mandatory = $false)] [string] $SignedIdentifier, + [Parameter(Mandatory = $false)] [string] $Version = '2022-11-02', + [Parameter(Mandatory = $false)] [ValidateSet('b', 'c', 'd', 'bv', 'bs', 'f', 's')] [string] $SignedResource, + [Parameter(Mandatory = $false)] [int] $DirectoryDepth, + [Parameter(Mandatory = $false)] [string] $SnapshotTime, + # Optional response header overrides (Blob/Files) + [Parameter(Mandatory = $false)] [string] $CacheControl, + [Parameter(Mandatory = $false)] [string] $ContentDisposition, + [Parameter(Mandatory = $false)] [string] $ContentEncoding, + [Parameter(Mandatory = $false)] [string] $ContentLanguage, + [Parameter(Mandatory = $false)] [string] $ContentType, + # Optional encryption scope (Blob, 2020-12-06+) + [Parameter(Mandatory = $false)] [string] $EncryptionScope, + # Optional connection string for endpoint/emulator support + [Parameter(Mandatory = $false)] [string] $ConnectionString = $env:AzureWebJobsStorage + ) + + # Local helpers: canonicalized resource and signature (standalone) + function _GetCanonicalizedResource { + param( + [Parameter(Mandatory = $true)][string] $AccountName, + [Parameter(Mandatory = $true)][ValidateSet('blob', 'queue', 'file', 'table')] [string] $Service, + [Parameter(Mandatory = $true)][string] $ResourcePath + ) + $decodedPath = [System.Web.HttpUtility]::UrlDecode(($ResourcePath.TrimStart('/'))) + switch ($Service) { + 'blob' { return "/blob/$AccountName/$decodedPath" } + 'queue' { return "/queue/$AccountName/$decodedPath" } + 'file' { return "/file/$AccountName/$decodedPath" } + 'table' { return "/table/$AccountName/$decodedPath" } + } + } + + function _NewSharedKeySignature { + param( + [Parameter(Mandatory = $true)][string] $AccountKey, + [Parameter(Mandatory = $true)][string] $StringToSign + ) + $keyBytes = [Convert]::FromBase64String($AccountKey) + $hmac = [System.Security.Cryptography.HMACSHA256]::new($keyBytes) + try { + $bytes = [System.Text.Encoding]::UTF8.GetBytes($StringToSign) + $sig = $hmac.ComputeHash($bytes) + return [Convert]::ToBase64String($sig) + } finally { $hmac.Dispose() } + } + + # Parse connection string for emulator/provided endpoints + $ProvidedEndpoint = $null + $ProvidedPath = $null + $EmulatorHost = $null + $EndpointSuffix = 'core.windows.net' + + if ($ConnectionString) { + $conn = @{} + foreach ($part in ($ConnectionString -split ';')) { + $p = $part.Trim() + if ($p -and $p -match '^(.+?)=(.+)$') { $conn[$matches[1]] = $matches[2] } + } + if ($conn['EndpointSuffix']) { $EndpointSuffix = $conn['EndpointSuffix'] } + + $ServiceCapitalized = [char]::ToUpper($Service[0]) + $Service.Substring(1) + $EndpointKey = "${ServiceCapitalized}Endpoint" + if ($conn[$EndpointKey]) { + $ProvidedEndpoint = $conn[$EndpointKey] + $ep = [Uri]::new($ProvidedEndpoint) + $Protocol = $ep.Scheme + $EmulatorHost = $ep.Host + if ($ep.Port -ne -1) { $EmulatorHost = "$($ep.Host):$($ep.Port)" } + $ProvidedPath = $ep.AbsolutePath.TrimEnd('/') + } elseif ($conn['UseDevelopmentStorage'] -eq 'true') { + # Emulator defaults + if (-not $AccountName) { $AccountName = 'devstoreaccount1' } + if (-not $AccountKey) { $AccountKey = 'Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==' } + $Protocol = 'http' + $ports = @{ blob = 10000; queue = 10001; table = 10002 } + $EmulatorHost = "127.0.0.1:$($ports[$Service])" + } + } + + # Build the resource URI + if ($ResourcePath.StartsWith('/')) { $ResourcePath = $ResourcePath.TrimStart('/') } + $UriBuilder = [System.UriBuilder]::new() + $UriBuilder.Scheme = $Protocol + + if ($ProvidedEndpoint) { + # Use provided endpoint + its base path + if ($EmulatorHost -match '^(.+?):(\d+)$') { $UriBuilder.Host = $matches[1]; $UriBuilder.Port = [int]$matches[2] } + else { $UriBuilder.Host = $EmulatorHost } + $UriBuilder.Path = ("$ProvidedPath/$ResourcePath").Replace('//', '/') + } elseif ($EmulatorHost) { + # Emulator: include account name in path + if ($EmulatorHost -match '^(.+?):(\d+)$') { $UriBuilder.Host = $matches[1]; $UriBuilder.Port = [int]$matches[2] } + else { $UriBuilder.Host = $EmulatorHost } + $UriBuilder.Path = "$AccountName/$ResourcePath" + } else { + # Standard Azure endpoint + $UriBuilder.Host = "$AccountName.$Service.$EndpointSuffix" + $UriBuilder.Path = $ResourcePath + } + $uri = $UriBuilder.Uri + + # Canonicalized resource for SAS string-to-sign (service-name style, 2015-02-21+) + $canonicalizedResource = _GetCanonicalizedResource -AccountName $AccountName -Service $Service -ResourcePath $ResourcePath + + # Time formatting per SAS spec (ISO 8601 UTC) + function _FormatSasTime($dt) { + if ($null -eq $dt) { return '' } + if ($dt -is [string]) { + if ([string]::IsNullOrWhiteSpace($dt)) { return '' } + $parsed = [DateTime]::Parse($dt, [System.Globalization.CultureInfo]::InvariantCulture, [System.Globalization.DateTimeStyles]::AssumeUniversal) + $utc = $parsed.ToUniversalTime() + return $utc.ToString('yyyy-MM-ddTHH:mm:ssZ') + } + if ($dt -is [DateTime]) { + $utc = ([DateTime]$dt).ToUniversalTime() + return $utc.ToString('yyyy-MM-ddTHH:mm:ssZ') + } + return '' + } + + $st = _FormatSasTime $StartTime + $se = _FormatSasTime $ExpiryTime + if ([string]::IsNullOrEmpty($se)) { throw 'ExpiryTime is required for SAS generation.' } + + # Assemble query parameters (service-specific) + $q = @{} + $q['sp'] = $Permissions + if ($st) { $q['st'] = $st } + $q['se'] = $se + if ($IP) { $q['sip'] = $IP } + if ($Protocol) { $q['spr'] = $Protocol } + if ($Version) { $q['sv'] = $Version } + if ($SignedIdentifier) { $q['si'] = $SignedIdentifier } + + # Blob/Files response headers overrides + if ($CacheControl) { $q['rscc'] = $CacheControl } + if ($ContentDisposition) { $q['rscd'] = $ContentDisposition } + if ($ContentEncoding) { $q['rsce'] = $ContentEncoding } + if ($ContentLanguage) { $q['rscl'] = $ContentLanguage } + if ($ContentType) { $q['rsct'] = $ContentType } + + # Resource-type specifics + $includeEncryptionScope = $false + if ($Service -eq 'blob') { + if (-not $SignedResource) { throw 'SignedResource (sr) is required for blob SAS: use b, c, d, bv, or bs.' } + $q['sr'] = $SignedResource + # Blob snapshot time uses the 'snapshot' parameter when applicable + if ($SnapshotTime) { $q['snapshot'] = $SnapshotTime } + if ($SignedResource -eq 'd') { + if ($null -eq $DirectoryDepth) { throw 'DirectoryDepth (sdd) is required when sr=d (Data Lake Hierarchical Namespace).' } + $q['sdd'] = [string]$DirectoryDepth + } + if ($EncryptionScope -and $Version -ge '2020-12-06') { + $q['ses'] = $EncryptionScope + $includeEncryptionScope = $true + } + } elseif ($Service -eq 'file') { + if (-not $SignedResource) { throw 'SignedResource (sr) is required for file SAS: use f or s.' } + $q['sr'] = $SignedResource + if ($SnapshotTime) { $q['sst'] = $SnapshotTime } + } elseif ($Service -eq 'table') { + # Table SAS may include ranges (spk/srk/epk/erk), omitted here unless future parameters are added + # Table also uses tn (table name) in query, but canonicalizedResource already includes table name + # We rely on canonicalizedResource and omit tn unless specified by callers via ResourcePath + } elseif ($Service -eq 'queue') { + # No sr for queue + } + + # Construct string-to-sign based on service and version + $StringToSign = $null + if ($Service -eq 'blob') { + # Version 2018-11-09 and later (optionally 2020-12-06 with encryption scope) + $fields = @( + $q['sp'], + ($st ?? ''), + $q['se'], + $canonicalizedResource, + ($q.ContainsKey('si') ? $q['si'] : ''), + ($q.ContainsKey('sip') ? $q['sip'] : ''), + ($q.ContainsKey('spr') ? $q['spr'] : ''), + ($q.ContainsKey('sv') ? $q['sv'] : ''), + $q['sr'], + ($q.ContainsKey('snapshot') ? $q['snapshot'] : ''), + ($includeEncryptionScope ? $q['ses'] : ''), + ($q.ContainsKey('rscc') ? $q['rscc'] : ''), + ($q.ContainsKey('rscd') ? $q['rscd'] : ''), + ($q.ContainsKey('rsce') ? $q['rsce'] : ''), + ($q.ContainsKey('rscl') ? $q['rscl'] : ''), + ($q.ContainsKey('rsct') ? $q['rsct'] : '') + ) + $StringToSign = ($fields -join "`n") + } elseif ($Service -eq 'file') { + # Use 2015-04-05+ format (no signedResource in string until 2018-11-09; we include response headers) + $fields = @( + $q['sp'], + ($st ?? ''), + $q['se'], + $canonicalizedResource, + ($q.ContainsKey('si') ? $q['si'] : ''), + ($q.ContainsKey('sip') ? $q['sip'] : ''), + ($q.ContainsKey('spr') ? $q['spr'] : ''), + ($q.ContainsKey('sv') ? $q['sv'] : ''), + ($q.ContainsKey('rscc') ? $q['rscc'] : ''), + ($q.ContainsKey('rscd') ? $q['rscd'] : ''), + ($q.ContainsKey('rsce') ? $q['rsce'] : ''), + ($q.ContainsKey('rscl') ? $q['rscl'] : ''), + ($q.ContainsKey('rsct') ? $q['rsct'] : '') + ) + $StringToSign = ($fields -join "`n") + } elseif ($Service -eq 'queue') { + # Version 2015-04-05 and later + $fields = @( + $q['sp'], + ($st ?? ''), + $q['se'], + $canonicalizedResource, + ($q.ContainsKey('si') ? $q['si'] : ''), + ($q.ContainsKey('sip') ? $q['sip'] : ''), + ($q.ContainsKey('spr') ? $q['spr'] : ''), + ($q.ContainsKey('sv') ? $q['sv'] : '') + ) + $StringToSign = ($fields -join "`n") + } elseif ($Service -eq 'table') { + # Version 2015-04-05 and later + $fields = @( + $q['sp'], + ($st ?? ''), + $q['se'], + $canonicalizedResource, + ($q.ContainsKey('si') ? $q['si'] : ''), + ($q.ContainsKey('sip') ? $q['sip'] : ''), + ($q.ContainsKey('spr') ? $q['spr'] : ''), + ($q.ContainsKey('sv') ? $q['sv'] : ''), + '', # startingPartitionKey + '', # startingRowKey + '', # endingPartitionKey + '' # endingRowKey + ) + $StringToSign = ($fields -join "`n") + } + + # Generate signature using account key (HMAC-SHA256 over UTF-8 string-to-sign) + try { + $SignatureBase64 = _NewSharedKeySignature -AccountKey $AccountKey -StringToSign $StringToSign + } catch { + throw "Failed to create SAS signature: $($_.Exception.Message)" + } + + # Store signature; will be URL-encoded when assembling query + $q['sig'] = $SignatureBase64 + + # Compose ordered query for readability (common fields first) + $orderedKeys = @('sp', 'st', 'se', 'sip', 'spr', 'sv', 'sr', 'si', 'snapshot', 'ses', 'sdd', 'rscc', 'rscd', 'rsce', 'rscl', 'rsct', 'sig') + $parts = @() + foreach ($k in $orderedKeys) { + if ($q.ContainsKey($k) -and -not [string]::IsNullOrEmpty($q[$k])) { + $parts += ("$k=" + [System.Net.WebUtility]::UrlEncode($q[$k])) + } + } + # Include any remaining keys + foreach ($k in $q.Keys) { + if ($orderedKeys -notcontains $k) { + $parts += ("$k=" + [System.Net.WebUtility]::UrlEncode($q[$k])) + } + } + + $token = '?' + ($parts -join '&') + + # Return structured output for debugging/usage + [PSCustomObject]@{ + Token = $token + Query = $q + CanonicalizedResource = $canonicalizedResource + StringToSign = $StringToSign + Version = $Version + Service = $Service + ResourceUri = $uri.AbsoluteUri + } +} diff --git a/Modules/CIPPCore/Public/GraphHelper/New-CIPPAzStorageRequest.ps1 b/Modules/CIPPCore/Public/GraphHelper/New-CIPPAzStorageRequest.ps1 index f97fe0ad77b5..96f2cd729593 100644 --- a/Modules/CIPPCore/Public/GraphHelper/New-CIPPAzStorageRequest.ps1 +++ b/Modules/CIPPCore/Public/GraphHelper/New-CIPPAzStorageRequest.ps1 @@ -475,7 +475,13 @@ function New-CIPPAzStorageRequest { # Do not force JSON Accept on blob/queue; service returns XML for list ops if (-not $RequestHeaders.ContainsKey('Accept')) { - $RequestHeaders['Accept'] = 'application/xml' + if ($Service -eq 'blob') { + $isList = (($Component -eq 'list') -or ($Uri.Query -match 'comp=list')) + if ($isList) { $RequestHeaders['Accept'] = 'application/xml' } + } elseif ($Service -eq 'queue') { + $RequestHeaders['Accept'] = 'application/xml' + } + # For Azure Files, avoid forcing Accept; binary downloads should be raw bytes } # Merge user-provided headers (these override defaults) @@ -500,6 +506,41 @@ function New-CIPPAzStorageRequest { $RequestHeaders['Content-Length'] = $ContentLength.ToString() } + # Blob upload: default to BlockBlob when performing a simple Put Blob (no comp parameter) + if ($Service -eq 'blob') { + $isCompSpecified = ($Component) -or ($Uri.Query -match 'comp=') + if ($Method -eq 'PUT' -and -not $isCompSpecified) { + if (-not $RequestHeaders.ContainsKey('x-ms-blob-type')) { $RequestHeaders['x-ms-blob-type'] = 'BlockBlob' } + } + } + + # Azure Files specific conveniences and validations + if ($Service -eq 'file') { + # Create file: PUT to file path without comp=range should specify x-ms-type and x-ms-content-length; body typically empty + $isRangeOp = ($Component -eq 'range') -or ($Uri.Query -match 'comp=range') + if ($Method -eq 'PUT' -and -not $isRangeOp) { + if (-not $RequestHeaders.ContainsKey('x-ms-type')) { $RequestHeaders['x-ms-type'] = 'file' } + # x-ms-content-length is required for create; if not provided by caller, try to infer from header Content-Length when body is empty + if (-not $RequestHeaders.ContainsKey('x-ms-content-length')) { + if ($ContentLength -eq 0) { + # Caller must supply x-ms-content-length for file size; fail fast for correctness + Write-Error 'Azure Files create operation requires header x-ms-content-length specifying file size in bytes.' + return + } else { + # If body present, assume immediate range upload is intended; advise using comp=range + Write-Verbose 'Body detected on Azure Files PUT without comp=range; consider using comp=range for content upload.' + } + } + } elseif ($Method -eq 'PUT' -and $isRangeOp) { + # Range upload must include x-ms-write and x-ms-range + if (-not $RequestHeaders.ContainsKey('x-ms-write')) { $RequestHeaders['x-ms-write'] = 'update' } + if (-not $RequestHeaders.ContainsKey('x-ms-range')) { + Write-Error 'Azure Files range upload requires header x-ms-range (e.g., bytes=0-).' + return + } + } + } + # Build canonicalized headers (x-ms-*) $CanonicalizedHeaders = Get-CanonicalizedXmsHeaders -Headers $RequestHeaders @@ -557,9 +598,46 @@ function New-CIPPAzStorageRequest { } elseif ($Method -eq 'DELETE') { # For other DELETE operations across services, prefer capturing headers/status $UseInvokeWebRequest = $true + } elseif ($Service -eq 'file' -and $Method -eq 'GET' -and -not (($Component -eq 'list') -or ($Uri.Query -match 'comp=list') -or ($Uri.Query -match 'comp=properties') -or ($Uri.Query -match 'comp=metadata'))) { + # For Azure Files binary downloads, use Invoke-WebRequest and return bytes + $UseInvokeWebRequest = $true + } elseif ($Service -eq 'blob' -and $Method -eq 'GET' -and -not (($Component -eq 'list') -or ($Uri.Query -match 'comp=list') -or ($Uri.Query -match 'comp=metadata') -or ($Uri.Query -match 'comp=properties'))) { + # For Blob binary downloads, use Invoke-WebRequest and return bytes (memory stream, no filesystem) + $UseInvokeWebRequest = $true } do { try { + # Blob: binary GET returns bytes from RawContentStream + if ($UseInvokeWebRequest -and $Service -eq 'blob' -and $Method -eq 'GET' -and -not (($Component -eq 'list') -or ($Uri.Query -match 'comp=list') -or ($Uri.Query -match 'comp=metadata') -or ($Uri.Query -match 'comp=properties'))) { + Write-Verbose 'Processing Blob binary download' + $resp = Invoke-WebRequest @RestMethodParams + $RequestSuccessful = $true + $ms = [System.IO.MemoryStream]::new() + try { $resp.RawContentStream.CopyTo($ms) } catch { } + $bytes = $ms.ToArray() + $hdrHash = @{} + if ($resp -and $resp.Headers) { foreach ($key in $resp.Headers.Keys) { $hdrHash[$key] = $resp.Headers[$key] } } + $reqUri = $null + try { if ($resp -and $resp.BaseResponse -and $resp.BaseResponse.ResponseUri) { $reqUri = $resp.BaseResponse.ResponseUri.AbsoluteUri } } catch { $reqUri = $Uri.AbsoluteUri } + return [PSCustomObject]@{ Bytes = $bytes; Length = $bytes.Length; Headers = $hdrHash; Uri = $reqUri } + } + # Azure Files: binary GET returns bytes + if ($UseInvokeWebRequest -and $Service -eq 'file' -and $Method -eq 'GET' -and -not (($Component -eq 'list') -or ($Uri.Query -match 'comp=list') -or ($Uri.Query -match 'comp=properties') -or ($Uri.Query -match 'comp=metadata'))) { + Write-Verbose 'Processing Azure Files binary download' + $tmp = [System.IO.Path]::GetTempFileName() + try { + $resp = Invoke-WebRequest @RestMethodParams -OutFile $tmp + $RequestSuccessful = $true + $bytes = [System.IO.File]::ReadAllBytes($tmp) + $hdrHash = @{} + if ($resp -and $resp.Headers) { foreach ($key in $resp.Headers.Keys) { $hdrHash[$key] = $resp.Headers[$key] } } + $reqUri = $null + try { if ($resp -and $resp.BaseResponse -and $resp.BaseResponse.ResponseUri) { $reqUri = $resp.BaseResponse.ResponseUri.AbsoluteUri } } catch { $reqUri = $Uri.AbsoluteUri } + return [PSCustomObject]@{ Bytes = $bytes; Length = $bytes.Length; Headers = $hdrHash; Uri = $reqUri } + } finally { + try { if (Test-Path -LiteralPath $tmp) { Remove-Item -LiteralPath $tmp -Force -ErrorAction SilentlyContinue } } catch {} + } + } # For queue comp=metadata, capture headers-only and return a compact object if ($UseInvokeWebRequest -and $Service -eq 'queue' -and (($Component -eq 'metadata') -or ($Uri.Query -match 'comp=metadata'))) { Write-Verbose 'Processing queue metadata response headers' diff --git a/Tools/Test-BlobUpload.ps1 b/Tools/Test-BlobUpload.ps1 new file mode 100644 index 000000000000..f7a13eeb6a55 --- /dev/null +++ b/Tools/Test-BlobUpload.ps1 @@ -0,0 +1,85 @@ +param( + [Parameter(Mandatory = $false)] [string] $ContainerName = 'test', + [Parameter(Mandatory = $false)] [string] $BlobName = 'hello.txt', + [Parameter(Mandatory = $false)] [string] $Content = 'Hello, world!', + [Parameter(Mandatory = $false)] [string] $ConnectionString = $env:AzureWebJobsStorage +) + +$ErrorActionPreference = 'Stop' + +# Import CIPPCore module from repository +$modulePath = Join-Path $PSScriptRoot '..' 'Modules' 'CIPPCore' 'CIPPCore.psm1' +if (-not (Test-Path -LiteralPath $modulePath)) { + throw "CIPPCore module not found at $modulePath" +} +Import-Module -Force $modulePath + +if (-not $ConnectionString) { + throw 'Azure Storage connection string not provided. Set AzureWebJobsStorage or pass -ConnectionString.' +} + +# Parse connection string for AccountName and AccountKey +$connectionParams = @{} +foreach ($part in ($ConnectionString -split ';')) { + $p = $part.Trim() + if ($p -and $p -match '^(.+?)=(.+)$') { $connectionParams[$matches[1]] = $matches[2] } +} +$AccountName = $connectionParams['AccountName'] +$AccountKey = $connectionParams['AccountKey'] + +# Support UseDevelopmentStorage=true +if ($connectionParams['UseDevelopmentStorage'] -eq 'true') { + $AccountName = 'devstoreaccount1' + $AccountKey = 'Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==' +} + +if (-not $AccountName -or -not $AccountKey) { + throw 'Connection string must contain AccountName and AccountKey or UseDevelopmentStorage=true.' +} + +Write-Host "Account: $AccountName" -ForegroundColor Cyan +Write-Host "Container: $ContainerName" -ForegroundColor Cyan +Write-Host "Blob: $BlobName" -ForegroundColor Cyan + +# Check if container exists via listing; create if missing +$containers = @() +try { + $containers = New-CIPPAzStorageRequest -Service 'blob' -Component 'list' +} catch { $containers = @() } + +$exists = ($containers | Where-Object { $_.Name -eq $ContainerName }) -ne $null +if ($exists) { + Write-Host 'Container exists.' -ForegroundColor Green +} else { + Write-Host 'Container not found. Creating...' -ForegroundColor Yellow + $null = New-CIPPAzStorageRequest -Service 'blob' -Resource $ContainerName -Method 'PUT' -QueryParams @{ restype = 'container' } + Start-Sleep -Seconds 1 + # Re-check + try { + $containers = New-CIPPAzStorageRequest -Service 'blob' -Component 'list' + } catch { $containers = @() } + $exists = ($containers | Where-Object { $_.Name -eq $ContainerName }) -ne $null + if (-not $exists) { throw "Failed to create container '$ContainerName'" } + Write-Host 'Container created.' -ForegroundColor Green +} + +# Upload blob content (BlockBlob by default) +Write-Host 'Uploading blob content...' -ForegroundColor Yellow +try { + $null = New-CIPPAzStorageRequest -Service 'blob' -Resource "$ContainerName/$BlobName" -Method 'PUT' -ContentType 'text/plain; charset=utf-8' -Body $Content +} catch { + Write-Error "Blob upload failed: $($_.Exception.Message)" + throw +} +Write-Host 'Upload complete.' -ForegroundColor Green + +# Generate SAS token valid for 7 days (read-only) +$expiry = (Get-Date).ToUniversalTime().AddDays(7) +$sas = New-CIPPAzServiceSAS -AccountName $AccountName -AccountKey $AccountKey -Service 'blob' -ResourcePath "$ContainerName/$BlobName" -Permissions 'r' -ExpiryTime $expiry -Protocol 'https' -Version '2022-11-02' -SignedResource 'b' -ConnectionString $ConnectionString + +$url = $sas.ResourceUri + $sas.Token +Write-Host 'Download URL (7 days):' -ForegroundColor Cyan +Write-Output $url + +# Return structured object +[PSCustomObject]@{ Url = $url; Container = $ContainerName; Blob = $BlobName; ExpiresUtc = $expiry } From c0457be77012bba5fbb23a67e5609fe46509bcc8 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Sat, 20 Dec 2025 19:29:04 -0500 Subject: [PATCH 009/503] Update New-CIPPAzServiceSAS.ps1 --- .../CIPPCore/Public/GraphHelper/New-CIPPAzServiceSAS.ps1 | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Modules/CIPPCore/Public/GraphHelper/New-CIPPAzServiceSAS.ps1 b/Modules/CIPPCore/Public/GraphHelper/New-CIPPAzServiceSAS.ps1 index de9ed16d299f..9f7f35d1eacb 100644 --- a/Modules/CIPPCore/Public/GraphHelper/New-CIPPAzServiceSAS.ps1 +++ b/Modules/CIPPCore/Public/GraphHelper/New-CIPPAzServiceSAS.ps1 @@ -263,16 +263,16 @@ function New-CIPPAzServiceSAS { # Compose ordered query for readability (common fields first) $orderedKeys = @('sp', 'st', 'se', 'sip', 'spr', 'sv', 'sr', 'si', 'snapshot', 'ses', 'sdd', 'rscc', 'rscd', 'rsce', 'rscl', 'rsct', 'sig') - $parts = @() + $parts = [System.Collections.Generic.List[string]]::new() foreach ($k in $orderedKeys) { if ($q.ContainsKey($k) -and -not [string]::IsNullOrEmpty($q[$k])) { - $parts += ("$k=" + [System.Net.WebUtility]::UrlEncode($q[$k])) + $parts.Add("$k=" + [System.Net.WebUtility]::UrlEncode($q[$k])) } } # Include any remaining keys foreach ($k in $q.Keys) { if ($orderedKeys -notcontains $k) { - $parts += ("$k=" + [System.Net.WebUtility]::UrlEncode($q[$k])) + $parts.Add("$k=" + [System.Net.WebUtility]::UrlEncode($q[$k])) } } From 968ed829c14fcd9eee382bc311308b91703c10cc Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Sun, 21 Dec 2025 12:38:49 +0100 Subject: [PATCH 010/503] Fix for management stats --- .../Entrypoints/Timer Functions/Start-CIPPStatsTimer.ps1 | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-CIPPStatsTimer.ps1 b/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-CIPPStatsTimer.ps1 index e8d56975c448..e4060f1c0a37 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-CIPPStatsTimer.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-CIPPStatsTimer.ps1 @@ -43,7 +43,12 @@ function Start-CIPPStatsTimer { CFZTNA = $RawExt.CFZTNA.Enabled GitHub = $RawExt.GitHub.Enabled } | ConvertTo-Json - - Invoke-RestMethod -Uri 'https://management.cipp.app/api/stats' -Method POST -Body $SendingObject -ContentType 'application/json' + try { + Invoke-RestMethod -Uri 'https://management.cipp.app/api/stats' -Method POST -Body $SendingObject -ContentType 'application/json' + } catch { + $rand = Get-Random -Minimum 0.5 -Maximum 5.5 + Start-Sleep -Seconds $rand + Invoke-RestMethod -Uri 'https://management.cipp.app/api/stats' -Method POST -Body $SendingObject -ContentType 'application/json' + } } } From e9576d387ad99288176f48ec2c72b909a97edd78 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Mon, 22 Dec 2025 00:18:29 +0100 Subject: [PATCH 011/503] Add data collection for tests --- CIPPTimers.json | 9 ++ Modules/CIPPCore/Public/Add-CIPPDbItem.ps1 | 81 ++++++++++++++ .../Push-CIPPDBCacheData.ps1 | 103 ++++++++++++++++++ .../HTTP Functions/Invoke-ListTests.ps1 | 66 +++++++++++ .../Start-CIPPDBCacheOrchestrator.ps1 | 50 +++++++++ Modules/CIPPCore/Public/Get-CIPPDbItem.ps1 | 57 ++++++++++ .../CIPPCore/Public/Get-CIPPTestResults.ps1 | 49 +++++++++ Modules/CIPPCore/Public/New-CIPPDbRequest.ps1 | 32 ++++++ ...t-CIPPDBCacheAdminConsentRequestPolicy.ps1 | 27 +++++ .../CIPPCore/Public/Set-CIPPDBCacheApps.ps1 | 29 +++++ ...t-CIPPDBCacheConditionalAccessPolicies.ps1 | 68 ++++++++++++ ...Set-CIPPDBCacheCrossTenantAccessPolicy.ps1 | 26 +++++ ...-CIPPDBCacheDefaultAppManagementPolicy.ps1 | 26 +++++ .../Public/Set-CIPPDBCacheDeviceSettings.ps1 | 28 +++++ .../Public/Set-CIPPDBCacheDevices.ps1 | 28 +++++ ...et-CIPPDBCacheDirectoryRecommendations.ps1 | 28 +++++ .../CIPPCore/Public/Set-CIPPDBCacheGroups.ps1 | 29 +++++ .../CIPPCore/Public/Set-CIPPDBCacheGuests.ps1 | 28 +++++ .../Public/Set-CIPPDBCacheIntunePolicies.ps1 | 60 ++++++++++ .../Public/Set-CIPPDBCacheManagedDevices.ps1 | 26 +++++ .../Public/Set-CIPPDBCacheOrganization.ps1 | 28 +++++ .../Public/Set-CIPPDBCachePIMSettings.ps1 | 56 ++++++++++ .../CIPPCore/Public/Set-CIPPDBCacheRoles.ps1 | 56 ++++++++++ .../Public/Set-CIPPDBCacheSecureScore.ps1 | 25 +++++ .../Set-CIPPDBCacheServicePrincipals.ps1 | 29 +++++ .../Public/Set-CIPPDBCacheSettings.ps1 | 27 +++++ .../CIPPCore/Public/Set-CIPPDBCacheUsers.ps1 | 27 +++++ 27 files changed, 1098 insertions(+) create mode 100644 Modules/CIPPCore/Public/Add-CIPPDbItem.ps1 create mode 100644 Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 create mode 100644 Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ListTests.ps1 create mode 100644 Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-CIPPDBCacheOrchestrator.ps1 create mode 100644 Modules/CIPPCore/Public/Get-CIPPDbItem.ps1 create mode 100644 Modules/CIPPCore/Public/Get-CIPPTestResults.ps1 create mode 100644 Modules/CIPPCore/Public/New-CIPPDbRequest.ps1 create mode 100644 Modules/CIPPCore/Public/Set-CIPPDBCacheAdminConsentRequestPolicy.ps1 create mode 100644 Modules/CIPPCore/Public/Set-CIPPDBCacheApps.ps1 create mode 100644 Modules/CIPPCore/Public/Set-CIPPDBCacheConditionalAccessPolicies.ps1 create mode 100644 Modules/CIPPCore/Public/Set-CIPPDBCacheCrossTenantAccessPolicy.ps1 create mode 100644 Modules/CIPPCore/Public/Set-CIPPDBCacheDefaultAppManagementPolicy.ps1 create mode 100644 Modules/CIPPCore/Public/Set-CIPPDBCacheDeviceSettings.ps1 create mode 100644 Modules/CIPPCore/Public/Set-CIPPDBCacheDevices.ps1 create mode 100644 Modules/CIPPCore/Public/Set-CIPPDBCacheDirectoryRecommendations.ps1 create mode 100644 Modules/CIPPCore/Public/Set-CIPPDBCacheGroups.ps1 create mode 100644 Modules/CIPPCore/Public/Set-CIPPDBCacheGuests.ps1 create mode 100644 Modules/CIPPCore/Public/Set-CIPPDBCacheIntunePolicies.ps1 create mode 100644 Modules/CIPPCore/Public/Set-CIPPDBCacheManagedDevices.ps1 create mode 100644 Modules/CIPPCore/Public/Set-CIPPDBCacheOrganization.ps1 create mode 100644 Modules/CIPPCore/Public/Set-CIPPDBCachePIMSettings.ps1 create mode 100644 Modules/CIPPCore/Public/Set-CIPPDBCacheRoles.ps1 create mode 100644 Modules/CIPPCore/Public/Set-CIPPDBCacheSecureScore.ps1 create mode 100644 Modules/CIPPCore/Public/Set-CIPPDBCacheServicePrincipals.ps1 create mode 100644 Modules/CIPPCore/Public/Set-CIPPDBCacheSettings.ps1 create mode 100644 Modules/CIPPCore/Public/Set-CIPPDBCacheUsers.ps1 diff --git a/CIPPTimers.json b/CIPPTimers.json index 0005053fd75c..9aa29603a04d 100644 --- a/CIPPTimers.json +++ b/CIPPTimers.json @@ -222,5 +222,14 @@ "Priority": 21, "RunOnProcessor": true, "IsSystem": true + }, + { + "Id": "9a7f8e6d-5c4b-3a2d-1e0f-9b8c7d6e5f4a", + "Command": "Start-CIPPDBCacheOrchestrator", + "Description": "Timer to collect and cache Microsoft Graph data for all tenants", + "Cron": "0 0 3 * * *", + "Priority": 22, + "RunOnProcessor": true, + "IsSystem": true } ] diff --git a/Modules/CIPPCore/Public/Add-CIPPDbItem.ps1 b/Modules/CIPPCore/Public/Add-CIPPDbItem.ps1 new file mode 100644 index 000000000000..835259ad9d2a --- /dev/null +++ b/Modules/CIPPCore/Public/Add-CIPPDbItem.ps1 @@ -0,0 +1,81 @@ +function Add-CIPPDbItem { + <# + .SYNOPSIS + Add items to the CIPP Reporting database + + .DESCRIPTION + Adds items to the CippReportingDB table with support for bulk inserts and count mode + + .PARAMETER TenantFilter + The tenant domain or GUID (used as partition key) + + .PARAMETER Type + The type of data being stored (used in row key) + + .PARAMETER Data + Array of items to add to the database + + .PARAMETER Count + If specified, stores a single row with count of each object property as separate properties + + .EXAMPLE + Add-CIPPDbItem -TenantFilter 'contoso.onmicrosoft.com' -Type 'Groups' -Data $GroupsData + + .EXAMPLE + Add-CIPPDbItem -TenantFilter 'contoso.onmicrosoft.com' -Type 'Groups' -Data $GroupsData -Count + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter, + + [Parameter(Mandatory = $true)] + [string]$Type, + + [Parameter(Mandatory = $true)] + [AllowEmptyCollection()] + [array]$Data, + + [Parameter(Mandatory = $false)] + [switch]$Count + ) + + try { + $Table = Get-CippTable -tablename 'CippReportingDB' + + if ($Count) { + $Entity = @{ + PartitionKey = $TenantFilter + RowKey = "$Type-Count" + DataCount = [int]$Data.Count + } + + Add-CIPPAzDataTableEntity @Table -Entity $Entity -Force | Out-Null + + } else { + $Entities = foreach ($Item in $Data) { + $ItemId = $Item.id + @{ + PartitionKey = $TenantFilter + RowKey = "$Type-$ItemId" + Data = [string]($Item | ConvertTo-Json -Depth 10 -Compress) + Type = $Type + } + } + + $BatchSize = 1000 + for ($i = 0; $i -lt $Entities.Count; $i += $BatchSize) { + $Batch = $Entities[$i..([Math]::Min($i + $BatchSize - 1, $Entities.Count - 1))] + foreach ($Entity in $Batch) { + Add-CIPPAzDataTableEntity @Table -Entity $Entity -Force | Out-Null + } + } + } + + Write-LogMessage -API 'CIPPDbItem' -tenant $TenantFilter -message "Added $($Data.Count) items of type $Type$(if ($Count) { ' (count mode)' })" -sev Info + + } catch { + Write-LogMessage -API 'CIPPDbItem' -tenant $TenantFilter -message "Failed to add items of type $Type : $($_.Exception.Message)" -sev Error + throw + } +} diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 new file mode 100644 index 000000000000..0cbd79ed6fa0 --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 @@ -0,0 +1,103 @@ +function Push-CIPPDBCacheData { + <# + .SYNOPSIS + Activity function to collect and cache all data for a single tenant + + .DESCRIPTION + Calls all collection functions sequentially, storing data immediately after each collection + + .FUNCTIONALITY + Entrypoint + #> + [CmdletBinding()] + param($Item) + + $TenantFilter = $Item.TenantFilter + #This collects all data for a tenant and caches it in the CIPP Reporting database. DO NOT ADD PROCESSING OR LOGIC HERE. + #The point of this file is to always be <10 minutes execution time. + try { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Starting database cache collection for tenant' -sev Info + + try { Set-CIPPDBCacheUsers -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Users collection failed: $($_.Exception.Message)" -sev Error + } + + try { Set-CIPPDBCacheGroups -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Groups collection failed: $($_.Exception.Message)" -sev Error + } + + try { Set-CIPPDBCacheGuests -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Guests collection failed: $($_.Exception.Message)" -sev Error + } + + try { Set-CIPPDBCacheServicePrincipals -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ServicePrincipals collection failed: $($_.Exception.Message)" -sev Error + } + + try { Set-CIPPDBCacheApps -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Apps collection failed: $($_.Exception.Message)" -sev Error + } + + try { Set-CIPPDBCacheDevices -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Devices collection failed: $($_.Exception.Message)" -sev Error + } + + try { Set-CIPPDBCacheManagedDevices -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ManagedDevices collection failed: $($_.Exception.Message)" -sev Error + } + + try { Set-CIPPDBCacheOrganization -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Organization collection failed: $($_.Exception.Message)" -sev Error + } + + try { Set-CIPPDBCacheRoles -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Roles collection failed: $($_.Exception.Message)" -sev Error + } + + try { Set-CIPPDBCacheAdminConsentRequestPolicy -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "AdminConsentRequestPolicy collection failed: $($_.Exception.Message)" -sev Error + } + + try { Set-CIPPDBCacheDeviceSettings -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "DeviceSettings collection failed: $($_.Exception.Message)" -sev Error + } + + try { Set-CIPPDBCacheDirectoryRecommendations -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "DirectoryRecommendations collection failed: $($_.Exception.Message)" -sev Error + } + + try { Set-CIPPDBCacheCrossTenantAccessPolicy -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "CrossTenantAccessPolicy collection failed: $($_.Exception.Message)" -sev Error + } + + try { Set-CIPPDBCacheDefaultAppManagementPolicy -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "DefaultAppManagementPolicy collection failed: $($_.Exception.Message)" -sev Error + } + + try { Set-CIPPDBCacheSettings -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Settings collection failed: $($_.Exception.Message)" -sev Error + } + + try { Set-CIPPDBCacheSecureScore -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "SecureScore collection failed: $($_.Exception.Message)" -sev Error + } + + try { Set-CIPPDBCacheIntunePolicies -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "IntunePolicies collection failed: $($_.Exception.Message)" -sev Error + } + + try { Set-CIPPDBCacheConditionalAccessPolicies -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ConditionalAccessPolicies collection failed: $($_.Exception.Message)" -sev Error + } + + try { Set-CIPPDBCachePIMSettings -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "PIMSettings collection failed: $($_.Exception.Message)" -sev Error + } + + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Completed database cache collection for tenant' -sev Info + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to complete database cache collection: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + } +} diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ListTests.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ListTests.ps1 new file mode 100644 index 000000000000..463d823e4bf5 --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ListTests.ps1 @@ -0,0 +1,66 @@ +function Invoke-ListTests { + <# + .SYNOPSIS + Lists tests for a tenant, optionally filtered by report ID + + .FUNCTIONALITY + Entrypoint + + .ROLE + Tenant.Reports.Read + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $TriggerMetadata.FunctionName + Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -message 'Accessed this API' -Sev 'Debug' + + try { + $TenantFilter = $Request.Query.tenantFilter ?? $Request.Body.tenantFilter + $ReportId = $Request.Query.reportId ?? $Request.Body.reportId + + if (-not $TenantFilter) { + throw 'TenantFilter parameter is required' + } + + $TestResultsData = Get-CIPPTestResults -TenantFilter $TenantFilter + + if ($ReportId) { + $ReportTable = Get-CippTable -tablename 'CippReportTemplates' + $Filter = "PartitionKey eq 'ReportTemplate' and RowKey eq '{0}'" -f $ReportId + $ReportTemplate = Get-CIPPAzDataTableEntity @ReportTable -Filter $Filter + + if ($ReportTemplate) { + $ReportTests = $ReportTemplate.Tests | ConvertFrom-Json + $FilteredTests = $TestResultsData.TestResults | Where-Object { $ReportTests -contains $_.TestId } + $TestResultsData.TestResults = $FilteredTests + } else { + Write-LogMessage -API $APIName -tenant $TenantFilter -message "Report template '$ReportId' not found" -sev Warning + $TestResultsData.TestResults = @() + } + } + + $TestCounts = @{ + Successful = @($TestResultsData.TestResults | Where-Object { $_.Result -eq 'Passed' }).Count + Failed = @($TestResultsData.TestResults | Where-Object { $_.Result -eq 'Failed' }).Count + Skipped = @($TestResultsData.TestResults | Where-Object { $_.Result -eq 'Skipped' }).Count + Total = @($TestResultsData.TestResults).Count + } + + $TestResultsData | Add-Member -NotePropertyName 'TestCounts' -NotePropertyValue $TestCounts -Force + + $StatusCode = [HttpStatusCode]::OK + $Body = $TestResultsData + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API $APIName -tenant $TenantFilter -message "Error retrieving tests: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + $StatusCode = [HttpStatusCode]::BadRequest + $Body = @{ Error = $ErrorMessage.NormalizedError } + } + + Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = $Body + }) +} diff --git a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-CIPPDBCacheOrchestrator.ps1 b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-CIPPDBCacheOrchestrator.ps1 new file mode 100644 index 000000000000..f85bc02358b9 --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-CIPPDBCacheOrchestrator.ps1 @@ -0,0 +1,50 @@ +function Start-CIPPDBCacheOrchestrator { + <# + .SYNOPSIS + Orchestrates database cache collection across all tenants + + .DESCRIPTION + Creates per-tenant jobs to collect and cache Microsoft Graph data + + .FUNCTIONALITY + Entrypoint + #> + [CmdletBinding()] + param() + + try { + Write-LogMessage -API 'CIPPDBCache' -message 'Starting database cache orchestration' -sev Info + + $TenantList = Get-Tenants | Where-Object { $_.defaultDomainName -ne $null } + + if ($TenantList.Count -eq 0) { + Write-LogMessage -API 'CIPPDBCache' -message 'No tenants found for cache collection' -sev Warning + return + } + + $Queue = New-CippQueueEntry -Name 'Database Cache Collection' -TotalTasks $TenantList.Count + + $Batch = foreach ($Tenant in $TenantList) { + [PSCustomObject]@{ + FunctionName = 'Push-CIPPDBCacheData' + TenantFilter = $Tenant.defaultDomainName + QueueId = $Queue.RowKey + QueueName = "DB Cache - $($Tenant.defaultDomainName)" + } + } + + $InputObject = [PSCustomObject]@{ + Batch = @($Batch) + OrchestratorName = 'CIPPDBCacheOrchestrator' + SkipLog = $false + } + + Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Compress -Depth 5) + + Write-LogMessage -API 'CIPPDBCache' -message "Queued database cache collection for $($TenantList.Count) tenants" -sev Info + + } catch { + Write-LogMessage -API 'CIPPDBCache' -message "Failed to start orchestration: $($_.Exception.Message)" -sev Error + throw + } +} diff --git a/Modules/CIPPCore/Public/Get-CIPPDbItem.ps1 b/Modules/CIPPCore/Public/Get-CIPPDbItem.ps1 new file mode 100644 index 000000000000..d140210b0065 --- /dev/null +++ b/Modules/CIPPCore/Public/Get-CIPPDbItem.ps1 @@ -0,0 +1,57 @@ +function Get-CIPPDbItem { + <# + .SYNOPSIS + Get specific items from the CIPP Reporting database + + .DESCRIPTION + Retrieves items from the CippReportingDB table using partition key (tenant) and type + + .PARAMETER TenantFilter + The tenant domain or GUID (partition key) + + .PARAMETER Type + The type of data to retrieve (used in row key filter) + + .PARAMETER CountsOnly + If specified, returns all count rows for the tenant + + .EXAMPLE + Get-CIPPDbItem -TenantFilter 'contoso.onmicrosoft.com' -Type 'Groups' + + .EXAMPLE + Get-CIPPDbItem -TenantFilter 'contoso.onmicrosoft.com' -CountsOnly + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter, + + [Parameter(Mandatory = $false)] + [string]$Type, + + [Parameter(Mandatory = $false)] + [switch]$CountsOnly + ) + + try { + $Table = Get-CippTable -tablename 'CippReportingDB' + + if ($CountsOnly) { + $Filter = "PartitionKey eq '{0}'" -f $TenantFilter + $Results = Get-CIPPAzDataTableEntity @Table -Filter $Filter + $Results = $Results | Where-Object { $_.RowKey -like '*-Count' } + } else { + if (-not $Type) { + throw 'Type parameter is required when CountsOnly is not specified' + } + $Filter = "PartitionKey eq '{0}' and RowKey ge '{1}-' and RowKey lt '{1}.'" -f $TenantFilter, $Type + $Results = Get-CIPPAzDataTableEntity @Table -Filter $Filter + } + + return $Results + + } catch { + Write-LogMessage -API 'CIPPDbItem' -tenant $TenantFilter -message "Failed to get items$(if ($Type) { " of type $Type" })$(if ($CountsOnly) { ' (counts only)' }): $($_.Exception.Message)" -sev Error + throw + } +} diff --git a/Modules/CIPPCore/Public/Get-CIPPTestResults.ps1 b/Modules/CIPPCore/Public/Get-CIPPTestResults.ps1 new file mode 100644 index 000000000000..43f91abb9aca --- /dev/null +++ b/Modules/CIPPCore/Public/Get-CIPPTestResults.ps1 @@ -0,0 +1,49 @@ +function Get-CIPPTestResults { + <# + .SYNOPSIS + Retrieves test results and tenant counts for a specific tenant + + .PARAMETER TenantFilter + The tenant domain or GUID to retrieve results for + + .EXAMPLE + Get-CIPPTestResults -TenantFilter 'contoso.onmicrosoft.com' + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter + ) + + try { + $Table = Get-CippTable -tablename 'CippTestResults' + $Filter = "PartitionKey eq '{0}'" -f $TenantFilter + $TestResults = Get-CIPPAzDataTableEntity @Table -Filter $Filter + + $CountData = Get-CIPPDbItem -TenantFilter $TenantFilter -CountsOnly + + $TenantCounts = @{} + $LatestTimestamp = $null + + foreach ($CountRow in $CountData) { + $TypeName = $CountRow.RowKey -replace '-Count$', '' + $TenantCounts[$TypeName] = $CountRow.DataCount + + if ($CountRow.Timestamp) { + if (-not $LatestTimestamp -or $CountRow.Timestamp -gt $LatestTimestamp) { + $LatestTimestamp = $CountRow.Timestamp + } + } + } + + return [PSCustomObject]@{ + TestResults = $TestResults + TenantCounts = $TenantCounts + LatestReportTimeStamp = $LatestTimestamp + } + + } catch { + Write-LogMessage -API 'CIPPTestResults' -tenant $TenantFilter -message "Failed to get test results: $($_.Exception.Message)" -sev Error + throw + } +} diff --git a/Modules/CIPPCore/Public/New-CIPPDbRequest.ps1 b/Modules/CIPPCore/Public/New-CIPPDbRequest.ps1 new file mode 100644 index 000000000000..397b928b0d68 --- /dev/null +++ b/Modules/CIPPCore/Public/New-CIPPDbRequest.ps1 @@ -0,0 +1,32 @@ +function New-CIPPDbRequest { + <# + .SYNOPSIS + Query the CIPP Reporting database by partition key + + .DESCRIPTION + Retrieves data from the CippReportingDB table filtered by partition key (tenant) + + .PARAMETER TenantFilter + The tenant domain or GUID to filter by (used as partition key) + + .EXAMPLE + New-CIPPDbRequest -TenantFilter 'contoso.onmicrosoft.com' + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter + ) + + try { + $Table = Get-CippTable -tablename 'CippReportingDB' + $Filter = "PartitionKey eq '{0}'" -f $TenantFilter + $Results = Get-CIPPAzDataTableEntity @Table -Filter $Filter + + return $Results + } catch { + Write-LogMessage -API 'CIPPDbRequest' -tenant $TenantFilter ` + -message "Failed to query database: $($_.Exception.Message)" -sev Error + throw + } +} diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheAdminConsentRequestPolicy.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheAdminConsentRequestPolicy.ps1 new file mode 100644 index 000000000000..e865512367d1 --- /dev/null +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheAdminConsentRequestPolicy.ps1 @@ -0,0 +1,27 @@ +function Set-CIPPDBCacheAdminConsentRequestPolicy { + <# + .SYNOPSIS + Caches admin consent request policy and settings for a tenant + + .PARAMETER TenantFilter + The tenant to cache consent policy for + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter + ) + + try { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching admin consent request policy' -sev Info + $ConsentPolicy = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/policies/adminConsentRequestPolicy' -tenantid $TenantFilter + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'AdminConsentRequestPolicy' -Data @($ConsentPolicy) + $ConsentPolicy = $null + + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached admin consent request policy successfully' -sev Info + + } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter ` + -message "Failed to cache admin consent request policy: $($_.Exception.Message)" -sev Error + } +} diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheApps.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheApps.ps1 new file mode 100644 index 000000000000..2ddfb27434f1 --- /dev/null +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheApps.ps1 @@ -0,0 +1,29 @@ +function Set-CIPPDBCacheApps { + <# + .SYNOPSIS + Caches all application registrations for a tenant + + .PARAMETER TenantFilter + The tenant to cache applications for + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter + ) + + try { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching applications' -sev Info + + $Apps = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/applications?$top=999&$select=id,appId,displayName,createdDateTime,signInAudience' -tenantid $TenantFilter + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Apps' -Data $Apps + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Apps' -Data $Apps -Count + $Apps = $null + + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached applications successfully' -sev Info + + } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter ` + -message "Failed to cache applications: $($_.Exception.Message)" -sev Error + } +} diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheConditionalAccessPolicies.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheConditionalAccessPolicies.ps1 new file mode 100644 index 000000000000..29c4722be7ef --- /dev/null +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheConditionalAccessPolicies.ps1 @@ -0,0 +1,68 @@ +function Set-CIPPDBCacheConditionalAccessPolicies { + <# + .SYNOPSIS + Caches all Conditional Access policies, named locations, and authentication strengths for a tenant (if CA capable) + + .PARAMETER TenantFilter + The tenant to cache CA policies for + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter + ) + + try { + $TestResult = Test-CIPPStandardLicense -StandardName 'ConditionalAccessCache' -TenantFilter $TenantFilter -RequiredCapabilities @('AAD_PREMIUM', 'AAD_PREMIUM_P2') -SkipLog + + if ($TestResult -eq $false) { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Tenant does not have Azure AD Premium license, skipping CA' -sev Info + return + } + + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching Conditional Access policies' -sev Info + + try { + $CAPolicies = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/identity/conditionalAccess/policies?$top=999' -tenantid $TenantFilter + if ($CAPolicies) { + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ConditionalAccessPolicies' -Data $CAPolicies + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ConditionalAccessPolicies' -Data $CAPolicies -Count + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($CAPolicies.Count) CA policies" -sev Info + } + $CAPolicies = $null + } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache CA policies: $($_.Exception.Message)" -sev Warning + } + + try { + $NamedLocations = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/identity/conditionalAccess/namedLocations?$top=999' -tenantid $TenantFilter + + if ($NamedLocations) { + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'NamedLocations' -Data $NamedLocations + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'NamedLocations' -Data $NamedLocations -Count + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($NamedLocations.Count) named locations" -sev Info + } + $NamedLocations = $null + } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache named locations: $($_.Exception.Message)" -sev Warning + } + + try { + $AuthStrengths = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/identity/conditionalAccess/authenticationStrength/policies?$top=999' -tenantid $TenantFilter + + if ($AuthStrengths) { + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'AuthenticationStrengths' -Data $AuthStrengths + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'AuthenticationStrengths' -Data $AuthStrengths -Count + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($AuthStrengths.Count) authentication strengths" -sev Info + } + $AuthStrengths = $null + } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache authentication strengths: $($_.Exception.Message)" -sev Warning + } + + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached CA data successfully' -sev Info + + } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache Conditional Access data: $($_.Exception.Message)" -sev Error + } +} diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheCrossTenantAccessPolicy.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheCrossTenantAccessPolicy.ps1 new file mode 100644 index 000000000000..e9e66753dcd3 --- /dev/null +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheCrossTenantAccessPolicy.ps1 @@ -0,0 +1,26 @@ +function Set-CIPPDBCacheCrossTenantAccessPolicy { + <# + .SYNOPSIS + Caches cross-tenant access policy for a tenant + + .PARAMETER TenantFilter + The tenant to cache policy for + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter + ) + + try { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching cross-tenant access policy' -sev Info + $CrossTenantPolicy = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/policies/crossTenantAccessPolicy/default' -tenantid $TenantFilter + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'CrossTenantAccessPolicy' -Data @($CrossTenantPolicy) + $CrossTenantPolicy = $null + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached cross-tenant access policy successfully' -sev Info + + } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter ` + -message "Failed to cache cross-tenant access policy: $($_.Exception.Message)" -sev Error + } +} diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheDefaultAppManagementPolicy.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheDefaultAppManagementPolicy.ps1 new file mode 100644 index 000000000000..556094e09ada --- /dev/null +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheDefaultAppManagementPolicy.ps1 @@ -0,0 +1,26 @@ +function Set-CIPPDBCacheDefaultAppManagementPolicy { + <# + .SYNOPSIS + Caches default app management policy for a tenant + + .PARAMETER TenantFilter + The tenant to cache policy for + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter + ) + + try { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching default app management policy' -sev Info + $AppMgmtPolicy = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/policies/defaultAppManagementPolicy' -tenantid $TenantFilter + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'DefaultAppManagementPolicy' -Data @($AppMgmtPolicy) + $AppMgmtPolicy = $null + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached default app management policy successfully' -sev Info + + } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter ` + -message "Failed to cache default app management policy: $($_.Exception.Message)" -sev Error + } +} diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheDeviceSettings.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheDeviceSettings.ps1 new file mode 100644 index 000000000000..aa2c1f6b9d01 --- /dev/null +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheDeviceSettings.ps1 @@ -0,0 +1,28 @@ +function Set-CIPPDBCacheDeviceSettings { + <# + .SYNOPSIS + Caches device settings for a tenant + + .PARAMETER TenantFilter + The tenant to cache device settings for + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter + ) + + try { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching device settings' -sev Info + + $DeviceSettings = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/directory/deviceLocalCredentials' -tenantid $TenantFilter + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'DeviceSettings' -Data @($DeviceSettings) + $DeviceSettings = $null + + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached device settings successfully' -sev Info + + } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter ` + -message "Failed to cache device settings: $($_.Exception.Message)" -sev Error + } +} diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheDevices.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheDevices.ps1 new file mode 100644 index 000000000000..3bc9b96a0ec7 --- /dev/null +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheDevices.ps1 @@ -0,0 +1,28 @@ +function Set-CIPPDBCacheDevices { + <# + .SYNOPSIS + Caches all Azure AD devices for a tenant + + .PARAMETER TenantFilter + The tenant to cache devices for + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter + ) + + try { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching Azure AD devices' -sev Info + + $Devices = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/devices?$top=999&$select=id,displayName,operatingSystem,operatingSystemVersion,trustType,accountEnabled,approximateLastSignInDateTime' -tenantid $TenantFilter + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Devices' -Data $Devices + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Devices' -Data $Devices -Count + $Devices = $null + + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached Azure AD devices successfully' -sev Info + + } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache Azure AD devices: $($_.Exception.Message)" -sev Error + } +} diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheDirectoryRecommendations.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheDirectoryRecommendations.ps1 new file mode 100644 index 000000000000..dcd28623b7e9 --- /dev/null +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheDirectoryRecommendations.ps1 @@ -0,0 +1,28 @@ +function Set-CIPPDBCacheDirectoryRecommendations { + <# + .SYNOPSIS + Caches directory recommendations for a tenant + + .PARAMETER TenantFilter + The tenant to cache recommendations for + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter + ) + + try { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching directory recommendations' -sev Info + + $Recommendations = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/directory/recommendations?$top=999' -tenantid $TenantFilter + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'DirectoryRecommendations' -Data $Recommendations + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'DirectoryRecommendations' -Data $Recommendations -Count + $Recommendations = $null + + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached directory recommendations successfully' -sev Info + + } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache directory recommendations: $($_.Exception.Message)" -sev Error + } +} diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheGroups.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheGroups.ps1 new file mode 100644 index 000000000000..84d1d971ed9a --- /dev/null +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheGroups.ps1 @@ -0,0 +1,29 @@ +function Set-CIPPDBCacheGroups { + <# + .SYNOPSIS + Caches all groups for a tenant + + .PARAMETER TenantFilter + The tenant to cache groups for + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter + ) + + try { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching groups' -sev Info + + $Groups = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/groups?$top=999&$select=id,displayName,groupTypes,mail,mailEnabled,securityEnabled,membershipRule,onPremisesSyncEnabled' -tenantid $TenantFilter + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Groups' -Data $Groups + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Groups' -Data $Groups -Count + $Groups = $null + + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached groups successfully' -sev Info + + } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter ` + -message "Failed to cache groups: $($_.Exception.Message)" -sev Error + } +} diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheGuests.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheGuests.ps1 new file mode 100644 index 000000000000..62eff7722789 --- /dev/null +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheGuests.ps1 @@ -0,0 +1,28 @@ +function Set-CIPPDBCacheGuests { + <# + .SYNOPSIS + Caches all guest users for a tenant + + .PARAMETER TenantFilter + The tenant to cache guest users for + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter + ) + + try { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching guest users' -sev Info + + $Guests = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/users?`$filter=userType eq 'Guest'&`$top=999" -tenantid $TenantFilter + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Guests' -Data $Guests + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Guests' -Data $Guests -Count + $Guests = $null + + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached guest users successfully' -sev Info + + } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache guest users: $($_.Exception.Message)" -sev Error + } +} diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheIntunePolicies.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheIntunePolicies.ps1 new file mode 100644 index 000000000000..eb74ae8b2c13 --- /dev/null +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheIntunePolicies.ps1 @@ -0,0 +1,60 @@ +function Set-CIPPDBCacheIntunePolicies { + <# + .SYNOPSIS + Caches all Intune policies for a tenant (if Intune capable) + + .PARAMETER TenantFilter + The tenant to cache Intune policies for + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter + ) + + try { + $TestResult = Test-CIPPStandardLicense -StandardName 'IntunePoliciesCache' -TenantFilter $TenantFilter -RequiredCapabilities @('INTUNE_A', 'MDM_Services', 'EMS', 'SCCM', 'MICROSOFTINTUNEPLAN1') -SkipLog + + if ($TestResult -eq $false) { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Tenant does not have Intune license, skipping' -sev Info + return + } + + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching Intune policies' -sev Info + + $PolicyTypes = @( + @{ Type = 'DeviceCompliancePolicies'; Uri = 'https://graph.microsoft.com/beta/deviceManagement/deviceCompliancePolicies?$top=999' } + @{ Type = 'DeviceConfigurations'; Uri = 'https://graph.microsoft.com/beta/deviceManagement/deviceConfigurations?$top=999' } + @{ Type = 'ConfigurationPolicies'; Uri = 'https://graph.microsoft.com/beta/deviceManagement/configurationPolicies?$top=999' } + @{ Type = 'GroupPolicyConfigurations'; Uri = 'https://graph.microsoft.com/beta/deviceManagement/groupPolicyConfigurations?$top=999' } + @{ Type = 'MobileAppConfigurations'; Uri = 'https://graph.microsoft.com/beta/deviceManagement/mobileAppConfigurations?$top=999' } + @{ Type = 'AppProtectionPolicies'; Uri = 'https://graph.microsoft.com/beta/deviceAppManagement/managedAppPolicies?$top=999' } + @{ Type = 'WindowsAutopilotDeploymentProfiles'; Uri = 'https://graph.microsoft.com/beta/deviceManagement/windowsAutopilotDeploymentProfiles?$top=999' } + @{ Type = 'DeviceEnrollmentConfigurations'; Uri = 'https://graph.microsoft.com/beta/deviceManagement/deviceEnrollmentConfigurations?$top=999' } + @{ Type = 'DeviceManagementScripts'; Uri = 'https://graph.microsoft.com/beta/deviceManagement/deviceManagementScripts?$top=999' } + @{ Type = 'MobileApps'; Uri = 'https://graph.microsoft.com/beta/deviceAppManagement/mobileApps?$top=999' } + ) + + foreach ($PolicyType in $PolicyTypes) { + try { + $Policies = New-GraphGetRequest -uri $PolicyType.Uri -tenantid $TenantFilter + + if ($Policies) { + Add-CIPPDbItem -TenantFilter $TenantFilter -Type "Intune$($PolicyType.Type)" -Data $Policies + Add-CIPPDbItem -TenantFilter $TenantFilter -Type "Intune$($PolicyType.Type)" -Data $Policies -Count + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($Policies.Count) $($PolicyType.Type)" -sev Info + } + + $Policies = $null + + } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache $($PolicyType.Type): $($_.Exception.Message)" -sev Warning + } + } + + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached Intune policies successfully' -sev Info + + } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache Intune policies: $($_.Exception.Message)" -sev Error + } +} diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheManagedDevices.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheManagedDevices.ps1 new file mode 100644 index 000000000000..f9f6fbb7f11d --- /dev/null +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheManagedDevices.ps1 @@ -0,0 +1,26 @@ +function Set-CIPPDBCacheManagedDevices { + <# + .SYNOPSIS + Caches all Intune managed devices for a tenant + + .PARAMETER TenantFilter + The tenant to cache managed devices for + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter + ) + + try { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching managed devices' -sev Info + $ManagedDevices = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/deviceManagement/managedDevices?$top=999&$select=id,deviceName,operatingSystem,osVersion,complianceState,managedDeviceOwnerType,enrolledDateTime,lastSyncDateTime' -tenantid $TenantFilter + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ManagedDevices' -Data $ManagedDevices + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ManagedDevices' -Data $ManagedDevices -Count + $ManagedDevices = $null + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached managed devices successfully' -sev Info + + } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache managed devices: $($_.Exception.Message)" -sev Error + } +} diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheOrganization.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheOrganization.ps1 new file mode 100644 index 000000000000..c6c32918367b --- /dev/null +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheOrganization.ps1 @@ -0,0 +1,28 @@ +function Set-CIPPDBCacheOrganization { + <# + .SYNOPSIS + Caches organization information for a tenant + + .PARAMETER TenantFilter + The tenant to cache organization data for + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter + ) + + try { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching organization data' -sev Info + + $Organization = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/organization' -tenantid $TenantFilter + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Organization' -Data $Organization + $Organization = $null + + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached organization data successfully' -sev Info + + } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter ` + -message "Failed to cache organization data: $($_.Exception.Message)" -sev Error + } +} diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCachePIMSettings.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCachePIMSettings.ps1 new file mode 100644 index 000000000000..24f8dbef7d8b --- /dev/null +++ b/Modules/CIPPCore/Public/Set-CIPPDBCachePIMSettings.ps1 @@ -0,0 +1,56 @@ +function Set-CIPPDBCachePIMSettings { + <# + .SYNOPSIS + Caches PIM (Privileged Identity Management) settings for a tenant (if CA capable) + + .PARAMETER TenantFilter + The tenant to cache PIM settings for + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter + ) + + try { + $TestResult = Test-CIPPStandardLicense -StandardName 'PIMSettingsCache' -TenantFilter $TenantFilter -RequiredCapabilities @('AAD_PREMIUM_P2') -SkipLog + + if ($TestResult -eq $false) { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Tenant does not have Azure AD Premium P2 license, skipping PIM' -sev Info + return + } + + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching PIM settings' -sev Info + + try { + $PIMRoleSettings = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/policies/roleManagementPolicyAssignments?$top=999' -tenantid $TenantFilter + + if ($PIMRoleSettings) { + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'PIMRoleSettings' -Data $PIMRoleSettings + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'PIMRoleSettings' -Data $PIMRoleSettings -Count + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($PIMRoleSettings.Count) PIM role settings" -sev Info + } + $PIMRoleSettings = $null + } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache PIM role settings: $($_.Exception.Message)" -sev Warning + } + + try { + $PIMAssignments = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/roleManagement/directory/roleEligibilityScheduleInstances?$top=999' -tenantid $TenantFilter + + if ($PIMAssignments) { + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'PIMAssignments' -Data $PIMAssignments + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'PIMAssignments' -Data $PIMAssignments -Count + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($PIMAssignments.Count) PIM assignments" -sev Info + } + $PIMAssignments = $null + } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache PIM assignments: $($_.Exception.Message)" -sev Warning + } + + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached PIM settings successfully' -sev Info + + } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache PIM settings: $($_.Exception.Message)" -sev Error + } +} diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheRoles.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheRoles.ps1 new file mode 100644 index 000000000000..07482abe8e9f --- /dev/null +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheRoles.ps1 @@ -0,0 +1,56 @@ +function Set-CIPPDBCacheRoles { + <# + .SYNOPSIS + Caches all directory roles and their members for a tenant + + .PARAMETER TenantFilter + The tenant to cache role data for + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter + ) + + try { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching directory roles' -sev Info + + $Roles = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/directoryRoles?$top=999' -tenantid $TenantFilter + + $RolesWithMembers = foreach ($Role in $Roles) { + try { + $Members = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/directoryRoles/$($Role.id)/members?\$top=999&\$select=id,displayName,userPrincipalName" -tenantid $TenantFilter + [PSCustomObject]@{ + id = $Role.id + displayName = $Role.displayName + description = $Role.description + roleTemplateId = $Role.roleTemplateId + members = $Members + memberCount = $Members.Count + } + } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to get members for role $($Role.displayName): $($_.Exception.Message)" -sev Warning + [PSCustomObject]@{ + id = $Role.id + displayName = $Role.displayName + description = $Role.description + roleTemplateId = $Role.roleTemplateId + members = @() + memberCount = 0 + } + } + } + + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Roles' -Data $RolesWithMembers + + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Roles' -Data $RolesWithMembers -Count + + $Roles = $null + $RolesWithMembers = $null + + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached directory roles successfully' -sev Info + + } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache directory roles: $($_.Exception.Message)" -sev Error + } +} diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheSecureScore.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheSecureScore.ps1 new file mode 100644 index 000000000000..393ebd058fc4 --- /dev/null +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheSecureScore.ps1 @@ -0,0 +1,25 @@ +function Set-CIPPDBCacheSecureScore { + <# + .SYNOPSIS + Caches secure score history (last 14 days) for a tenant + + .PARAMETER TenantFilter + The tenant to cache secure score for + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter + ) + + try { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching secure score' -sev Info + $SecureScore = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/security/secureScores?$top=14' -tenantid $TenantFilter -noPagination $true + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'SecureScore' -Data $SecureScore + $SecureScore = $null + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached secure score successfully' -sev Info + + } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache secure score: $($_.Exception.Message)" -sev Error + } +} diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheServicePrincipals.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheServicePrincipals.ps1 new file mode 100644 index 000000000000..f661d748507c --- /dev/null +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheServicePrincipals.ps1 @@ -0,0 +1,29 @@ +function Set-CIPPDBCacheServicePrincipals { + <# + .SYNOPSIS + Caches all service principals for a tenant + + .PARAMETER TenantFilter + The tenant to cache service principals for + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter + ) + + try { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching service principals' -sev Info + + $ServicePrincipals = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/servicePrincipals?$top=999&$select=id,appId,displayName,servicePrincipalType,accountEnabled' -tenantid $TenantFilter + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ServicePrincipals' -Data $ServicePrincipals + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ServicePrincipals' -Data $ServicePrincipals -Count + $ServicePrincipals = $null + + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached service principals successfully' -sev Info + + } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter ` + -message "Failed to cache service principals: $($_.Exception.Message)" -sev Error + } +} diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheSettings.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheSettings.ps1 new file mode 100644 index 000000000000..3f809db48f57 --- /dev/null +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheSettings.ps1 @@ -0,0 +1,27 @@ +function Set-CIPPDBCacheSettings { + <# + .SYNOPSIS + Caches directory settings for a tenant + + .PARAMETER TenantFilter + The tenant to cache settings for + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter + ) + + try { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching directory settings' -sev Info + + $Settings = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/settings?$top=999' -tenantid $TenantFilter + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Settings' -Data $Settings + $Settings = $null + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached directory settings successfully' -sev Info + + } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter ` + -message "Failed to cache directory settings: $($_.Exception.Message)" -sev Error + } +} diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheUsers.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheUsers.ps1 new file mode 100644 index 000000000000..2a03fedc46ab --- /dev/null +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheUsers.ps1 @@ -0,0 +1,27 @@ +function Set-CIPPDBCacheUsers { + <# + .SYNOPSIS + Caches all users for a tenant + + .PARAMETER TenantFilter + The tenant to cache users for + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter + ) + + try { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching users' -sev Info + + $Users = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/users?$top=999' -tenantid $TenantFilter + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Users' -Data $Users + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Users' -Data $Users -Count + $Users = $null + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached users successfully' -sev Info + + } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache users: $($_.Exception.Message)" -sev Error + } +} From 2ad0dcfcc60b172c0c88ae81df1e02f4fda892c9 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Mon, 22 Dec 2025 00:29:21 +0100 Subject: [PATCH 012/503] db request add type --- Modules/CIPPCore/Public/New-CIPPDbRequest.ps1 | 24 +++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/Modules/CIPPCore/Public/New-CIPPDbRequest.ps1 b/Modules/CIPPCore/Public/New-CIPPDbRequest.ps1 index 397b928b0d68..12a484544079 100644 --- a/Modules/CIPPCore/Public/New-CIPPDbRequest.ps1 +++ b/Modules/CIPPCore/Public/New-CIPPDbRequest.ps1 @@ -9,24 +9,38 @@ function New-CIPPDbRequest { .PARAMETER TenantFilter The tenant domain or GUID to filter by (used as partition key) + .PARAMETER Type + Optional. The data type to filter by (e.g., Users, Groups, Devices) + .EXAMPLE New-CIPPDbRequest -TenantFilter 'contoso.onmicrosoft.com' + + .EXAMPLE + New-CIPPDbRequest -TenantFilter 'contoso.onmicrosoft.com' -Type 'Users' #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] - [string]$TenantFilter + [string]$TenantFilter, + + [Parameter(Mandatory = $false)] + [string]$Type ) try { $Table = Get-CippTable -tablename 'CippReportingDB' - $Filter = "PartitionKey eq '{0}'" -f $TenantFilter + + if ($Type) { + $Filter = "PartitionKey eq '{0}' and RowKey ge '{1}-' and RowKey lt '{1}.'" -f $TenantFilter, $Type + } else { + $Filter = "PartitionKey eq '{0}'" -f $TenantFilter + } + $Results = Get-CIPPAzDataTableEntity @Table -Filter $Filter - return $Results + return ($Results.Data | ConvertFrom-Json -ErrorAction SilentlyContinue) } catch { - Write-LogMessage -API 'CIPPDbRequest' -tenant $TenantFilter ` - -message "Failed to query database: $($_.Exception.Message)" -sev Error + Write-LogMessage -API 'CIPPDbRequest' -tenant $TenantFilter -message "Failed to query database: $($_.Exception.Message)" -sev Error throw } } From a4633fedcd542b5acbfaafd017667604dc86d1c5 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Mon, 22 Dec 2025 01:25:43 +0100 Subject: [PATCH 013/503] reporting --- .../CIPPCore/Public/Add-CippTestResult.ps1 | 97 +++++++++++++++++++ .../HTTP Functions/Invoke-ListTests.ps1 | 4 +- 2 files changed, 99 insertions(+), 2 deletions(-) create mode 100644 Modules/CIPPCore/Public/Add-CippTestResult.ps1 diff --git a/Modules/CIPPCore/Public/Add-CippTestResult.ps1 b/Modules/CIPPCore/Public/Add-CippTestResult.ps1 new file mode 100644 index 000000000000..28ac54ba1588 --- /dev/null +++ b/Modules/CIPPCore/Public/Add-CippTestResult.ps1 @@ -0,0 +1,97 @@ +function Add-CippTestResult { + <# + .SYNOPSIS + Adds a test result to the CIPP test results database + + .DESCRIPTION + Stores test result data in the CippTestResults table with tenant and test ID as keys + + .PARAMETER TenantFilter + The tenant domain or GUID for the test result + + .PARAMETER TestId + Unique identifier for the test + + .PARAMETER Status + Test status (e.g., Pass, Fail, Skip) + + .PARAMETER ResultMarkdown + Markdown formatted result details + + .PARAMETER Risk + Risk level (e.g., High, Medium, Low) + + .PARAMETER Name + Display name of the test + + .PARAMETER Pillar + Security pillar category + + .PARAMETER UserImpact + Impact level on users + + .PARAMETER ImplementationEffort + Effort required for implementation + + .PARAMETER Category + Test category or classification + + .EXAMPLE + Add-CippTestResult -TenantFilter 'contoso.onmicrosoft.com' -TestId 'MFA-001' -Status 'Pass' -Name 'MFA Enabled' -Risk 'High' + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter, + + [Parameter(Mandatory = $true)] + [string]$TestId, + + [Parameter(Mandatory = $true)] + [string]$Status, + + [Parameter(Mandatory = $false)] + [string]$ResultMarkdown, + + [Parameter(Mandatory = $false)] + [string]$Risk, + + [Parameter(Mandatory = $false)] + [string]$Name, + + [Parameter(Mandatory = $false)] + [string]$Pillar, + + [Parameter(Mandatory = $false)] + [string]$UserImpact, + + [Parameter(Mandatory = $false)] + [string]$ImplementationEffort, + + [Parameter(Mandatory = $false)] + [string]$Category + ) + + try { + $Table = Get-CippTable -tablename 'CippTestResults' + + $Entity = @{ + PartitionKey = $TenantFilter + RowKey = $TestId + Status = $Status + ResultMarkdown = $ResultMarkdown ?? '' + Risk = $Risk ?? '' + Name = $Name ?? '' + Pillar = $Pillar ?? '' + UserImpact = $UserImpact ?? '' + ImplementationEffort = $ImplementationEffort ?? '' + Category = $Category ?? '' + } + + Add-CIPPAzDataTableEntity @Table -Entity $Entity -Force + Write-LogMessage -API 'CIPPTestResults' -tenant $TenantFilter -message "Added test result: $TestId - $Status" -sev Info + } catch { + Write-LogMessage -API 'CIPPTestResults' -tenant $TenantFilter -message "Failed to add test result: $($_.Exception.Message)" -sev Error + throw + } +} diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ListTests.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ListTests.ps1 index 463d823e4bf5..1a018c44ffc3 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ListTests.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ListTests.ps1 @@ -26,8 +26,8 @@ function Invoke-ListTests { $TestResultsData = Get-CIPPTestResults -TenantFilter $TenantFilter if ($ReportId) { - $ReportTable = Get-CippTable -tablename 'CippReportTemplates' - $Filter = "PartitionKey eq 'ReportTemplate' and RowKey eq '{0}'" -f $ReportId + $ReportTable = Get-CippTable -tablename 'CippReportingTemplates' + $Filter = "PartitionKey eq 'ReportingTemplate' and RowKey eq '{0}'" -f $ReportId $ReportTemplate = Get-CIPPAzDataTableEntity @ReportTable -Filter $Filter if ($ReportTemplate) { From 8a86ddbd2cee33bd518fe99964d5d9189161701b Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Mon, 22 Dec 2025 01:42:10 +0100 Subject: [PATCH 014/503] Reporting template tests --- ExampleReportTemplate.ps1 | 13 +++++++++++++ .../Entrypoints/HTTP Functions/Invoke-ListTests.ps1 | 9 +++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) create mode 100644 ExampleReportTemplate.ps1 diff --git a/ExampleReportTemplate.ps1 b/ExampleReportTemplate.ps1 new file mode 100644 index 000000000000..3d83e1659795 --- /dev/null +++ b/ExampleReportTemplate.ps1 @@ -0,0 +1,13 @@ +$Table = Get-CippTable -tablename 'CippReportTemplates' + +$Entity = @{ + RowKey = (New-Guid).ToString() + PartitionKey = 'ReportingTemplate' + Tests = [string](@('Test01', 'Test02', 'Test03', 'Test04', 'Test05') | ConvertTo-Json -Compress) + Description = 'This is a test report' + Name = 'Test Report' +} + +Add-CIPPAzDataTableEntity @Table -Entity $Entity -Force + +Write-Host "Report template created successfully with ID: $($Entity.RowKey)" diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ListTests.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ListTests.ps1 index 1a018c44ffc3..1c337a7b1663 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ListTests.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ListTests.ps1 @@ -25,26 +25,31 @@ function Invoke-ListTests { $TestResultsData = Get-CIPPTestResults -TenantFilter $TenantFilter + $TotalTests = 0 + if ($ReportId) { - $ReportTable = Get-CippTable -tablename 'CippReportingTemplates' + $ReportTable = Get-CippTable -tablename 'CippReportTemplates' $Filter = "PartitionKey eq 'ReportingTemplate' and RowKey eq '{0}'" -f $ReportId $ReportTemplate = Get-CIPPAzDataTableEntity @ReportTable -Filter $Filter if ($ReportTemplate) { $ReportTests = $ReportTemplate.Tests | ConvertFrom-Json + $TotalTests = @($ReportTests).Count $FilteredTests = $TestResultsData.TestResults | Where-Object { $ReportTests -contains $_.TestId } $TestResultsData.TestResults = $FilteredTests } else { Write-LogMessage -API $APIName -tenant $TenantFilter -message "Report template '$ReportId' not found" -sev Warning $TestResultsData.TestResults = @() } + } else { + $TotalTests = @($TestResultsData.TestResults).Count } $TestCounts = @{ Successful = @($TestResultsData.TestResults | Where-Object { $_.Result -eq 'Passed' }).Count Failed = @($TestResultsData.TestResults | Where-Object { $_.Result -eq 'Failed' }).Count Skipped = @($TestResultsData.TestResults | Where-Object { $_.Result -eq 'Skipped' }).Count - Total = @($TestResultsData.TestResults).Count + Total = $TotalTests } $TestResultsData | Add-Member -NotePropertyName 'TestCounts' -NotePropertyValue $TestCounts -Force From 85ac7eec95a076b757eff761fc0886e9bcb5e79d Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Mon, 22 Dec 2025 13:13:25 +0100 Subject: [PATCH 015/503] reporting updates --- .../CIPPCore/Public/Add-CippTestResult.ps1 | 4 ++ .../HTTP Functions/Invoke-ListTests.ps1 | 52 +++++++++++++++---- 2 files changed, 46 insertions(+), 10 deletions(-) diff --git a/Modules/CIPPCore/Public/Add-CippTestResult.ps1 b/Modules/CIPPCore/Public/Add-CippTestResult.ps1 index 28ac54ba1588..446401d69045 100644 --- a/Modules/CIPPCore/Public/Add-CippTestResult.ps1 +++ b/Modules/CIPPCore/Public/Add-CippTestResult.ps1 @@ -47,6 +47,9 @@ function Add-CippTestResult { [Parameter(Mandatory = $true)] [string]$TestId, + [Parameter(Mandatory = $true)] + [string]$testType = 'identity', + [Parameter(Mandatory = $true)] [string]$Status, @@ -86,6 +89,7 @@ function Add-CippTestResult { UserImpact = $UserImpact ?? '' ImplementationEffort = $ImplementationEffort ?? '' Category = $Category ?? '' + TestType = $TestType } Add-CIPPAzDataTableEntity @Table -Entity $Entity -Force diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ListTests.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ListTests.ps1 index 1c337a7b1663..cd739ad7246c 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ListTests.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ListTests.ps1 @@ -25,7 +25,8 @@ function Invoke-ListTests { $TestResultsData = Get-CIPPTestResults -TenantFilter $TenantFilter - $TotalTests = 0 + $IdentityTotal = 0 + $DevicesTotal = 0 if ($ReportId) { $ReportTable = Get-CippTable -tablename 'CippReportTemplates' @@ -33,27 +34,58 @@ function Invoke-ListTests { $ReportTemplate = Get-CIPPAzDataTableEntity @ReportTable -Filter $Filter if ($ReportTemplate) { - $ReportTests = $ReportTemplate.Tests | ConvertFrom-Json - $TotalTests = @($ReportTests).Count - $FilteredTests = $TestResultsData.TestResults | Where-Object { $ReportTests -contains $_.TestId } - $TestResultsData.TestResults = $FilteredTests + $IdentityTests = @() + $DeviceTests = @() + + if ($ReportTemplate.identityTests) { + $IdentityTests = $ReportTemplate.identityTests | ConvertFrom-Json + $IdentityTotal = @($IdentityTests).Count + } + + if ($ReportTemplate.deviceTests) { + $DeviceTests = $ReportTemplate.deviceTests | ConvertFrom-Json + $DevicesTotal = @($DeviceTests).Count + } + + $AllReportTests = $IdentityTests + $DeviceTests + $FilteredTests = $TestResultsData.TestResults | Where-Object { $AllReportTests -contains $_.RowKey } + $TestResultsData.TestResults = @($FilteredTests) } else { Write-LogMessage -API $APIName -tenant $TenantFilter -message "Report template '$ReportId' not found" -sev Warning $TestResultsData.TestResults = @() } } else { - $TotalTests = @($TestResultsData.TestResults).Count + $IdentityTotal = @($TestResultsData.TestResults | Where-Object { $_.TestType -eq 'Identity' }).Count + $DevicesTotal = @($TestResultsData.TestResults | Where-Object { $_.TestType -eq 'Devices' }).Count } + $IdentityResults = $TestResultsData.TestResults | Where-Object { $_.TestType -eq 'Identity' } + $DeviceResults = $TestResultsData.TestResults | Where-Object { $_.TestType -eq 'Devices' } + $TestCounts = @{ - Successful = @($TestResultsData.TestResults | Where-Object { $_.Result -eq 'Passed' }).Count - Failed = @($TestResultsData.TestResults | Where-Object { $_.Result -eq 'Failed' }).Count - Skipped = @($TestResultsData.TestResults | Where-Object { $_.Result -eq 'Skipped' }).Count - Total = $TotalTests + Identity = @{ + Passed = @($IdentityResults | Where-Object { $_.Status -eq 'Passed' }).Count + Failed = @($IdentityResults | Where-Object { $_.Status -eq 'Failed' }).Count + Investigate = @($IdentityResults | Where-Object { $_.Status -eq 'Investigate' }).Count + Skipped = @($IdentityResults | Where-Object { $_.Status -eq 'Skipped' }).Count + Total = $IdentityTotal + } + Devices = @{ + Passed = @($DeviceResults | Where-Object { $_.Status -eq 'Passed' }).Count + Failed = @($DeviceResults | Where-Object { $_.Status -eq 'Failed' }).Count + Investigate = @($DeviceResults | Where-Object { $_.Status -eq 'Investigate' }).Count + Skipped = @($DeviceResults | Where-Object { $_.Status -eq 'Skipped' }).Count + Total = $DevicesTotal + } } $TestResultsData | Add-Member -NotePropertyName 'TestCounts' -NotePropertyValue $TestCounts -Force + $SecureScoreData = New-CIPPDbRequest -TenantFilter $TenantFilter -Type 'SecureScore' + if ($SecureScoreData) { + $TestResultsData | Add-Member -NotePropertyName 'SecureScore' -NotePropertyValue $SecureScoreData -Force + } + $StatusCode = [HttpStatusCode]::OK $Body = $TestResultsData From 418a8ccc844368361e81c68472e6b227ea068745 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Tue, 23 Dec 2025 00:46:39 +0100 Subject: [PATCH 016/503] Tests --- CIPPTimers.json | 9 +++ Modules/CIPPCore/Public/Add-CIPPDbItem.ps1 | 8 +- .../Push-CIPPDBCacheData.ps1 | 4 + .../Activity Triggers/Tests/Push-CIPPTest.ps1 | 30 +++++++ .../Tenant/Tests/Invoke-CIPPTestsRun.ps1 | 78 +++++++++++++++++++ .../Start-TestsOrchestrator.ps1 | 16 ++++ .../Set-CIPPDBCacheAuthorizationPolicy.ps1 | 26 +++++++ .../Public/Tests/Invoke-CippTestZTNA21776.ps1 | 29 +++++++ .../Public/Tests/Invoke-CippTestZTNA21808.ps1 | 43 ++++++++++ 9 files changed, 236 insertions(+), 7 deletions(-) create mode 100644 Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Tests/Push-CIPPTest.ps1 create mode 100644 Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Tests/Invoke-CIPPTestsRun.ps1 create mode 100644 Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-TestsOrchestrator.ps1 create mode 100644 Modules/CIPPCore/Public/Set-CIPPDBCacheAuthorizationPolicy.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21776.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21808.ps1 diff --git a/CIPPTimers.json b/CIPPTimers.json index 9aa29603a04d..f76dba8941e2 100644 --- a/CIPPTimers.json +++ b/CIPPTimers.json @@ -231,5 +231,14 @@ "Priority": 22, "RunOnProcessor": true, "IsSystem": true + }, + { + "Id": "1f2e3d4c-5b6a-7c8d-9e0f-1a2b3c4d5e6f", + "Command": "Start-TestsOrchestrator", + "Description": "Timer to run security and compliance tests against cached data", + "Cron": "0 0 4 * * *", + "Priority": 23, + "RunOnProcessor": true, + "IsSystem": true } ] diff --git a/Modules/CIPPCore/Public/Add-CIPPDbItem.ps1 b/Modules/CIPPCore/Public/Add-CIPPDbItem.ps1 index 835259ad9d2a..51ea3c530f0c 100644 --- a/Modules/CIPPCore/Public/Add-CIPPDbItem.ps1 +++ b/Modules/CIPPCore/Public/Add-CIPPDbItem.ps1 @@ -62,14 +62,8 @@ function Add-CIPPDbItem { Type = $Type } } + Add-CIPPAzDataTableEntity @Table -Entity $Entities -Force | Out-Null - $BatchSize = 1000 - for ($i = 0; $i -lt $Entities.Count; $i += $BatchSize) { - $Batch = $Entities[$i..([Math]::Min($i + $BatchSize - 1, $Entities.Count - 1))] - foreach ($Entity in $Batch) { - Add-CIPPAzDataTableEntity @Table -Entity $Entity -Force | Out-Null - } - } } Write-LogMessage -API 'CIPPDbItem' -tenant $TenantFilter -message "Added $($Data.Count) items of type $Type$(if ($Count) { ' (count mode)' })" -sev Info diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 index 0cbd79ed6fa0..2c71327cce00 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 @@ -58,6 +58,10 @@ function Push-CIPPDBCacheData { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "AdminConsentRequestPolicy collection failed: $($_.Exception.Message)" -sev Error } + try { Set-CIPPDBCacheAuthorizationPolicy -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "AuthorizationPolicy collection failed: $($_.Exception.Message)" -sev Error + } + try { Set-CIPPDBCacheDeviceSettings -TenantFilter $TenantFilter } catch { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "DeviceSettings collection failed: $($_.Exception.Message)" -sev Error } diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Tests/Push-CIPPTest.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Tests/Push-CIPPTest.ps1 new file mode 100644 index 000000000000..7492a7c15abc --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Tests/Push-CIPPTest.ps1 @@ -0,0 +1,30 @@ +function Push-CIPPTest { + <# + .FUNCTIONALITY + Entrypoint + #> + param( + $Item + ) + + $TenantFilter = $Item.TenantFilter + $TestId = $Item.TestId + + Write-Information "Running test $TestId for tenant $TenantFilter" + + try { + $FunctionName = "Invoke-CippTest$TestId" + + if (-not (Get-Command $FunctionName -ErrorAction SilentlyContinue)) { + Write-LogMessage -API 'Tests' -tenant $TenantFilter -message "Test function not found: $FunctionName" -sev Error + return + } + + Write-Information "Executing $FunctionName for $TenantFilter" + & $FunctionName -Tenant $TenantFilter + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $TenantFilter -message "Failed to run test $TestId $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + } +} diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Tests/Invoke-CIPPTestsRun.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Tests/Invoke-CIPPTestsRun.ps1 new file mode 100644 index 000000000000..079df93388fc --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Tests/Invoke-CIPPTestsRun.ps1 @@ -0,0 +1,78 @@ +function Invoke-CIPPTestsRun { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Tenant.Tests.Read + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $false)] + [string]$TenantFilter = 'allTenants' + ) + + Write-Information "Starting tests run for tenant: $TenantFilter" + + try { + $AllTests = Get-Command -Name 'Invoke-CippTest*' -Module CIPPCore | Select-Object -ExpandProperty Name | ForEach-Object { + $_ -replace '^Invoke-CippTest', '' + } + + if ($AllTests.Count -eq 0) { + Write-LogMessage -API 'Tests' -message 'No test functions found.' -sev Error + return + } + + Write-Information "Found $($AllTests.Count) test functions to run" + $AllTenantsList = if ($TenantFilter -eq 'allTenants') { + $DbCounts = Get-CIPPDbItem -CountsOnly + $TenantsWithData = $DbCounts | Where-Object { $_.Count -gt 0 } | Select-Object -ExpandProperty PartitionKey -Unique + Write-Information "Found $($TenantsWithData.Count) tenants with data in database" + $TenantsWithData + } else { + $DbCounts = Get-CIPPDbItem -TenantFilter $TenantFilter -CountsOnly + if (($DbCounts | Measure-Object -Property Count -Sum).Sum -gt 0) { + @($TenantFilter) + } else { + Write-LogMessage -API 'Tests' -tenant $TenantFilter -message 'Tenant has no data in database. Skipping tests.' -sev Info + @() + } + } + + if ($AllTenantsList.Count -eq 0) { + Write-LogMessage -API 'Tests' -message 'No tenants with data found. Exiting.' -sev Info + return + } + + # Build batch: all tests for all tenants + $Batch = foreach ($Tenant in $AllTenantsList) { + foreach ($Test in $AllTests) { + @{ + FunctionName = 'CIPPTest' + TenantFilter = $Tenant + TestId = $Test + } + } + } + + Write-Information "Built batch of $($Batch.Count) test activities ($($AllTests.Count) tests × $($AllTenantsList.Count) tenants)" + + $InputObject = [PSCustomObject]@{ + OrchestratorName = 'TestsRun' + Batch = @($Batch) + SkipLog = $true + } + + $InstanceId = Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Depth 5 -Compress) + Write-Information "Started tests orchestration with ID = '$InstanceId'" + + return @{ + InstanceId = $InstanceId + Message = "Tests orchestration started: $($AllTests.Count) tests for $($AllTenantsList.Count) tenants" + } + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -message "Failed to start tests orchestration: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + throw + } +} diff --git a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-TestsOrchestrator.ps1 b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-TestsOrchestrator.ps1 new file mode 100644 index 000000000000..da22c521107e --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-TestsOrchestrator.ps1 @@ -0,0 +1,16 @@ +function Start-TestsOrchestrator { + <# + .SYNOPSIS + Start the Tests Orchestrator + + .FUNCTIONALITY + Entrypoint + #> + [CmdletBinding(SupportsShouldProcess = $true)] + param() + + if ($PSCmdlet.ShouldProcess('Start-TestsOrchestrator', 'Starting Tests Orchestrator')) { + Write-LogMessage -API 'Tests' -message 'Starting Tests Schedule' -sev Info + Invoke-CIPPTestsRun -TenantFilter 'allTenants' + } +} diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheAuthorizationPolicy.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheAuthorizationPolicy.ps1 new file mode 100644 index 000000000000..7167e39f66a8 --- /dev/null +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheAuthorizationPolicy.ps1 @@ -0,0 +1,26 @@ +function Set-CIPPDBCacheAuthorizationPolicy { + <# + .SYNOPSIS + Caches authorization policy for a tenant + + .PARAMETER TenantFilter + The tenant to cache authorization policy for + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter + ) + + try { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching authorization policy' -sev Info + $AuthPolicy = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/policies/authorizationPolicy' -tenantid $TenantFilter + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'AuthorizationPolicy' -Data @($AuthPolicy) + $AuthPolicy = $null + + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached authorization policy successfully' -sev Info + + } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache authorization policy: $($_.Exception.Message)" -sev Error + } +} diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21776.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21776.ps1 new file mode 100644 index 000000000000..d5c106671932 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21776.ps1 @@ -0,0 +1,29 @@ +function Invoke-CippTestZTNA21776 { + param($Tenant) + + try { + $AuthPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthorizationPolicy' + if (-not $AuthPolicy) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21776' -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Authorization policy not found in database' -Risk 'High' -Name 'User consent settings are restricted' -UserImpact 'High' -ImplementationEffort 'Medium' -Category 'Application Management' + return + } + + $Matched = $AuthPolicy | Where-Object { $_.defaultUserRolePermissions.permissionGrantPoliciesAssigned -match '^ManagePermissionGrantsForSelf' } + $NoMatch = $Matched.Count -eq 0 + $LowImpact = $Matched.defaultUserRolePermissions.permissionGrantPoliciesAssigned -contains 'managePermissionGrantsForSelf.microsoft-user-default-low' + + if ($NoMatch -or $LowImpact) { + $Status = 'Passed' + $Result = if ($NoMatch) { 'User consent is disabled' } else { 'User consent restricted to verified publishers and low-impact permissions' } + } else { + $Status = 'Failed' + $Result = 'Users can consent to any application' + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21776' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'User consent settings are restricted' -UserImpact 'High' -ImplementationEffort 'Medium' -Category 'Application Management' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21776' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'User consent settings are restricted' -UserImpact 'High' -ImplementationEffort 'Medium' -Category 'Application Management' + } +} diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21808.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21808.ps1 new file mode 100644 index 000000000000..4deaf0ff0c53 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21808.ps1 @@ -0,0 +1,43 @@ +function Invoke-CippTestZTNA21808 { + param($Tenant) + + try { + $CAPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ConditionalAccessPolicies' + if (-not $CAPolicies) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21808' -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Conditional Access policies not found in database' -Risk 'High' -Name 'Restrict device code flow' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Access Control' + return + } + + $Enabled = $CAPolicies | Where-Object { $_.state -eq 'enabled' } + $DeviceCodePolicies = $Enabled | Where-Object { + if ($_.conditions.authenticationFlows.transferMethods) { + $Methods = $_.conditions.authenticationFlows.transferMethods -split ',' + $Methods -contains 'deviceCodeFlow' + } else { + $false + } + } + + $BlockPolicies = $DeviceCodePolicies | Where-Object { + $_.grantControls.builtInControls -contains 'block' + } + + if ($BlockPolicies.Count -gt 0) { + $Status = 'Passed' + $Result = "Device code flow is properly restricted with $($BlockPolicies.Count) blocking policy/policies" + } elseif ($DeviceCodePolicies.Count -eq 0) { + $Status = 'Failed' + $Result = 'No Conditional Access policies found targeting device code flow' + #Add table with existing policies? + } else { + $Status = 'Failed' + $Result = 'Device code flow policies exist but none are configured to block' + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21808' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'Restrict device code flow' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Access Control' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21808' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Restrict device code flow' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Access Control' + } +} From df632eaad11b6994c4bad80c771071aa63b69d76 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Tue, 23 Dec 2025 00:48:06 +0100 Subject: [PATCH 017/503] tests with reporting --- Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21808.ps1 | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21808.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21808.ps1 index 4deaf0ff0c53..c78ea236e7b2 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21808.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21808.ps1 @@ -18,9 +18,7 @@ function Invoke-CippTestZTNA21808 { } } - $BlockPolicies = $DeviceCodePolicies | Where-Object { - $_.grantControls.builtInControls -contains 'block' - } + $BlockPolicies = $DeviceCodePolicies | Where-Object { $_.grantControls.builtInControls -contains 'block' } if ($BlockPolicies.Count -gt 0) { $Status = 'Passed' From 4a0c1111b27704d461b8d17cb275813e5f10366b Mon Sep 17 00:00:00 2001 From: Chris Riani <153139246+criani@users.noreply.github.com> Date: Mon, 22 Dec 2025 19:54:23 -0500 Subject: [PATCH 018/503] Update Invoke-CIPPStandardPhishProtection.ps1 --- .../Invoke-CIPPStandardPhishProtection.ps1 | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPhishProtection.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPhishProtection.ps1 index eebba0779eac..1c850db19a0a 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPhishProtection.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPhishProtection.ps1 @@ -46,18 +46,9 @@ function Invoke-CIPPStandardPhishProtection { } catch { Write-LogMessage -API 'Standards' -tenant $tenant -message "Could not get the branding for $($Tenant). This tenant might not have premium licenses available: $($_.Exception.Message)" -sev Error } -$CSS = @" + $CSS = @" .ext-sign-in-box { - background-image: - url(https://clone.cipp.app/api/PublicPhishingCheck?Tenantid=$($tenant)&URL=https://$($CIPPUrl)), - linear-gradient(135deg, #0f1a25 0%, #12202c 40%, #0d1620 100%); - background-size: cover; - background-repeat: no-repeat; - border: 2px solid #16d1e3; - border-radius: 12px; - padding-top: 80px; - position: relative; - box-shadow: 0 0 35px rgba(22, 209, 227, 0.35); + background-image: url(https://clone.cipp.app/api/PublicPhishingCheck?Tenantid=$($tenant)&URL=https://$($CIPPUrl)); } "@ From a0203a8003ab87d02e7e99dfbb0dba4810452f12 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Tue, 23 Dec 2025 11:13:16 -0500 Subject: [PATCH 019/503] Allow reserved app names in include/exclude applications Added support for reserved application names ('none', 'All', 'Office365', 'MicrosoftAdminPortals') when validating includeApplications and excludeApplications in New-CIPPCAPolicy. This ensures these reserved names are accepted even if not present in the service principals list. --- Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 b/Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 index 27ce66d595f7..1de8340d4a6b 100644 --- a/Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 +++ b/Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 @@ -144,10 +144,12 @@ function New-CIPPCAPolicy { if (($JSONobj.conditions.applications.includeApplications -and $JSONobj.conditions.applications.includeApplications -notcontains 'All') -or ($JSONobj.conditions.applications.excludeApplications -and $JSONobj.conditions.applications.excludeApplications -notcontains 'All')) { $AllServicePrincipals = New-GraphGETRequest -uri 'https://graph.microsoft.com/v1.0/servicePrincipals?$select=appId' -tenantid $TenantFilter -asApp $true + $ReservedApplicationNames = @('none', 'All', 'Office365', 'MicrosoftAdminPortals') + if ($JSONobj.conditions.applications.excludeApplications -and $JSONobj.conditions.applications.excludeApplications -notcontains 'All') { $ValidExclusions = [system.collections.generic.list[string]]::new() foreach ($appId in $JSONobj.conditions.applications.excludeApplications) { - if ($AllServicePrincipals.appId -contains $appId) { + if ($AllServicePrincipals.appId -contains $appId -or $ReservedApplicationNames -contains $appId) { $ValidExclusions.Add($appId) } } @@ -156,7 +158,7 @@ function New-CIPPCAPolicy { if ($JSONobj.conditions.applications.includeApplications -and $JSONobj.conditions.applications.includeApplications -notcontains 'All') { $ValidInclusions = [system.collections.generic.list[string]]::new() foreach ($appId in $JSONobj.conditions.applications.includeApplications) { - if ($AllServicePrincipals.appId -contains $appId) { + if ($AllServicePrincipals.appId -contains $appId -or $ReservedApplicationNames -contains $appId) { $ValidInclusions.Add($appId) } } From 9666ed87ff030face96b09362aa569faf1775be4 Mon Sep 17 00:00:00 2001 From: Luke Steward <87503131+sfaxluke@users.noreply.github.com> Date: Tue, 23 Dec 2025 18:48:58 +0000 Subject: [PATCH 020/503] Improve handling of arrays in CA template and comparison Enhanced robustness in Compare-CIPPIntuneObject and New-CIPPCATemplate by adding explicit checks for arrays and PSCustomObject types before property access or recursion. Updated Invoke-CIPPStandardConditionalAccessTemplate to handle errors during object comparison gracefully and log them. These changes prevent runtime errors when processing unexpected array structures in conditional access policy objects. --- .../Public/Compare-CIPPIntuneObject.ps1 | 87 +++++++++++++++---- .../CIPPCore/Public/New-CIPPCATemplate.ps1 | 16 +++- ...-CIPPStandardConditionalAccessTemplate.ps1 | 11 ++- 3 files changed, 90 insertions(+), 24 deletions(-) diff --git a/Modules/CIPPCore/Public/Compare-CIPPIntuneObject.ps1 b/Modules/CIPPCore/Public/Compare-CIPPIntuneObject.ps1 index ee24dfd6b620..20f5464d1bfa 100644 --- a/Modules/CIPPCore/Public/Compare-CIPPIntuneObject.ps1 +++ b/Modules/CIPPCore/Public/Compare-CIPPIntuneObject.ps1 @@ -58,6 +58,13 @@ function Compare-CIPPIntuneObject { [int]$MaxDepth = 20 ) + # Check for arrays at the start of every recursive call - this catches arrays at any nesting level + $isObj1Array = $Object1 -is [Array] -or $Object1 -is [System.Collections.IList] + $isObj2Array = $Object2 -is [Array] -or $Object2 -is [System.Collections.IList] + if ($isObj1Array -or $isObj2Array) { + return + } + if ($Depth -ge $MaxDepth) { $result.Add([PSCustomObject]@{ Property = $PropertyPath @@ -153,34 +160,78 @@ function Compare-CIPPIntuneObject { } } } elseif ($Object1 -is [PSCustomObject] -or $Object1.PSObject.Properties.Count -gt 0) { - $allPropertyNames = @( - $Object1.PSObject.Properties | Select-Object -ExpandProperty Name - $Object2.PSObject.Properties | Select-Object -ExpandProperty Name - ) | Select-Object -Unique + # Skip comparison if either object is an array - arrays can't have custom properties set + $isObj1Array = $Object1 -is [Array] -or $Object1 -is [System.Collections.IList] + $isObj2Array = $Object2 -is [Array] -or $Object2 -is [System.Collections.IList] + if ($isObj1Array -or $isObj2Array) { + return + } + + # Safely get property names - ensure objects are not arrays before accessing PSObject.Properties + $allPropertyNames = @() + try { + if (-not ($Object1 -is [Array] -or $Object1 -is [System.Collections.IList])) { + $allPropertyNames += $Object1.PSObject.Properties | Select-Object -ExpandProperty Name + } + if (-not ($Object2 -is [Array] -or $Object2 -is [System.Collections.IList])) { + $allPropertyNames += $Object2.PSObject.Properties | Select-Object -ExpandProperty Name + } + $allPropertyNames = $allPropertyNames | Select-Object -Unique + } catch { + return + } foreach ($propName in $allPropertyNames) { if (ShouldSkipProperty -PropertyName $propName) { continue } $newPath = if ($PropertyPath) { "$PropertyPath.$propName" } else { $propName } - $prop1Exists = $Object1.PSObject.Properties.Name -contains $propName - $prop2Exists = $Object2.PSObject.Properties.Name -contains $propName + # Safely check if properties exist - ensure objects are not arrays + $prop1Exists = $false + $prop2Exists = $false + try { + if (-not ($Object1 -is [Array] -or $Object1 -is [System.Collections.IList])) { + $prop1Exists = $Object1.PSObject.Properties.Name -contains $propName + } + if (-not ($Object2 -is [Array] -or $Object2 -is [System.Collections.IList])) { + $prop2Exists = $Object2.PSObject.Properties.Name -contains $propName + } + } catch { + continue + } if ($prop1Exists -and $prop2Exists) { - if ($Object1.$propName -and $Object2.$propName) { - Compare-ObjectsRecursively -Object1 $Object1.$propName -Object2 $Object2.$propName -PropertyPath $newPath -Depth ($Depth + 1) -MaxDepth $MaxDepth + try { + # Double-check arrays before accessing properties + if (($Object1 -is [Array] -or $Object1 -is [System.Collections.IList]) -or + ($Object2 -is [Array] -or $Object2 -is [System.Collections.IList])) { + continue + } + if ($Object1.$propName -and $Object2.$propName) { + Compare-ObjectsRecursively -Object1 $Object1.$propName -Object2 $Object2.$propName -PropertyPath $newPath -Depth ($Depth + 1) -MaxDepth $MaxDepth + } + } catch { + throw } } elseif ($prop1Exists) { - $result.Add([PSCustomObject]@{ - Property = $newPath - ExpectedValue = $Object1.$propName - ReceivedValue = '' - }) + try { + $result.Add([PSCustomObject]@{ + Property = $newPath + ExpectedValue = $Object1.$propName + ReceivedValue = '' + }) + } catch { + throw + } } else { - $result.Add([PSCustomObject]@{ - Property = $newPath - ExpectedValue = '' - ReceivedValue = $Object2.$propName - }) + try { + $result.Add([PSCustomObject]@{ + Property = $newPath + ExpectedValue = '' + ReceivedValue = $Object2.$propName + }) + } catch { + throw + } } } } else { diff --git a/Modules/CIPPCore/Public/New-CIPPCATemplate.ps1 b/Modules/CIPPCore/Public/New-CIPPCATemplate.ps1 index 8d0c7d86ba7a..bcb928d5700a 100644 --- a/Modules/CIPPCore/Public/New-CIPPCATemplate.ps1 +++ b/Modules/CIPPCore/Public/New-CIPPCATemplate.ps1 @@ -56,7 +56,14 @@ function New-CIPPCATemplate { } if ($excludelocations) { $JSON.conditions.locations.excludeLocations = $excludelocations } - if ($JSON.conditions.users.includeUsers) { + # Check if conditions.users exists and is a PSCustomObject (not an array) before accessing properties + $hasConditionsUsers = $null -ne $JSON.conditions.users + # Explicitly exclude array types - arrays have properties but we can't set custom properties on them + $isArray = $hasConditionsUsers -and ($JSON.conditions.users -is [Array] -or $JSON.conditions.users -is [System.Collections.IList]) + $isPSCustomObject = $hasConditionsUsers -and -not $isArray -and ($JSON.conditions.users -is [PSCustomObject] -or ($JSON.conditions.users.PSObject.Properties.Count -gt 0 -and -not $isArray)) + $hasIncludeUsers = $isPSCustomObject -and ($null -ne $JSON.conditions.users.includeUsers) + + if ($isPSCustomObject -and $hasIncludeUsers) { $JSON.conditions.users.includeUsers = @($JSON.conditions.users.includeUsers | ForEach-Object { $originalID = $_ if ($_ -in 'All', 'None', 'GuestOrExternalUsers') { return $_ } @@ -65,7 +72,8 @@ function New-CIPPCATemplate { }) } - if ($JSON.conditions.users.excludeUsers) { + # Use the same type check for other user properties + if ($isPSCustomObject -and $null -ne $JSON.conditions.users.excludeUsers) { $JSON.conditions.users.excludeUsers = @($JSON.conditions.users.excludeUsers | ForEach-Object { if ($_ -in 'All', 'None', 'GuestOrExternalUsers') { return $_ } $originalID = $_ @@ -74,7 +82,7 @@ function New-CIPPCATemplate { }) } - if ($JSON.conditions.users.includeGroups) { + if ($isPSCustomObject -and $null -ne $JSON.conditions.users.includeGroups) { $JSON.conditions.users.includeGroups = @($JSON.conditions.users.includeGroups | ForEach-Object { $originalID = $_ if ($_ -in 'All', 'None', 'GuestOrExternalUsers' -or -not (Test-IsGuid $_)) { return $_ } @@ -82,7 +90,7 @@ function New-CIPPCATemplate { if ($match) { $match.displayName } else { $originalID } }) } - if ($JSON.conditions.users.excludeGroups) { + if ($isPSCustomObject -and $null -ne $JSON.conditions.users.excludeGroups) { $JSON.conditions.users.excludeGroups = @($JSON.conditions.users.excludeGroups | ForEach-Object { $originalID = $_ if ($_ -in 'All', 'None', 'GuestOrExternalUsers' -or -not (Test-IsGuid $_)) { return $_ } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardConditionalAccessTemplate.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardConditionalAccessTemplate.ps1 index 151b30a27999..acdb5f3757f6 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardConditionalAccessTemplate.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardConditionalAccessTemplate.ps1 @@ -105,8 +105,15 @@ function Invoke-CIPPStandardConditionalAccessTemplate { Set-CIPPStandardsCompareField -FieldName "standards.ConditionalAccessTemplate.$($Setting.value)" -FieldValue "Policy $($Setting.label) is missing from this tenant." -Tenant $Tenant } } else { - $CompareObj = ConvertFrom-Json -ErrorAction SilentlyContinue -InputObject (New-CIPPCATemplate -TenantFilter $tenant -JSON $CheckExististing) - $Compare = Compare-CIPPIntuneObject -ReferenceObject $policy -DifferenceObject $CompareObj + $templateResult = New-CIPPCATemplate -TenantFilter $tenant -JSON $CheckExististing + $CompareObj = ConvertFrom-Json -ErrorAction SilentlyContinue -InputObject $templateResult + try { + $Compare = Compare-CIPPIntuneObject -ReferenceObject $policy -DifferenceObject $CompareObj + } catch { + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Error comparing CA policy: $($_.Exception.Message)" -sev Error + Set-CIPPStandardsCompareField -FieldName "standards.ConditionalAccessTemplate.$($Setting.value)" -FieldValue "Error comparing policy: $($_.Exception.Message)" -Tenant $Tenant + continue + } if (!$Compare) { Set-CIPPStandardsCompareField -FieldName "standards.ConditionalAccessTemplate.$($Setting.value)" -FieldValue $true -Tenant $Tenant } else { From b090550ddc6a7ce50cda47be1d2a5c9f8b629389 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Tue, 23 Dec 2025 22:28:57 +0100 Subject: [PATCH 021/503] ZTNA test batch1 --- .../Push-CIPPDBCacheData.ps1 | 4 + .../CIPPCore/Public/Set-CIPPDBCacheApps.ps1 | 2 +- ...CIPPDBCacheAuthenticationMethodsPolicy.ps1 | 26 ++++ .../Public/Tests/Invoke-CippTestZTNA21772.ps1 | 54 +++++++ .../Public/Tests/Invoke-CippTestZTNA21773.ps1 | 84 ++++++++++ .../Public/Tests/Invoke-CippTestZTNA21774.ps1 | 57 +++++++ .../Public/Tests/Invoke-CippTestZTNA21780.ps1 | 34 ++++ .../Public/Tests/Invoke-CippTestZTNA21783.ps1 | 52 +++++++ .../Public/Tests/Invoke-CippTestZTNA21784.ps1 | 42 +++++ .../Public/Tests/Invoke-CippTestZTNA21786.ps1 | 37 +++++ .../Public/Tests/Invoke-CippTestZTNA21787.ps1 | 28 ++++ .../Public/Tests/Invoke-CippTestZTNA21790.ps1 | 36 +++++ .../Public/Tests/Invoke-CippTestZTNA21791.ps1 | 28 ++++ .../Public/Tests/Invoke-CippTestZTNA21792.ps1 | 29 ++++ .../Public/Tests/Invoke-CippTestZTNA21793.ps1 | 39 +++++ .../Public/Tests/Invoke-CippTestZTNA21796.ps1 | 40 +++++ .../Public/Tests/Invoke-CippTestZTNA21797.ps1 | 146 ++++++++++++++++++ .../Public/Tests/Invoke-CippTestZTNA21799.ps1 | 82 ++++++++++ .../Public/Tests/Invoke-CippTestZTNA21802.ps1 | 36 +++++ .../Public/Tests/Invoke-CippTestZTNA21803.ps1 | 31 ++++ .../Public/Tests/Invoke-CippTestZTNA21804.ps1 | 42 +++++ .../Public/Tests/Invoke-CippTestZTNA21806.ps1 | 51 ++++++ .../Public/Tests/Invoke-CippTestZTNA21807.ps1 | 28 ++++ 23 files changed, 1007 insertions(+), 1 deletion(-) create mode 100644 Modules/CIPPCore/Public/Set-CIPPDBCacheAuthenticationMethodsPolicy.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21772.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21773.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21774.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21780.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21783.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21784.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21786.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21787.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21790.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21791.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21792.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21793.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21796.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21797.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21799.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21802.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21803.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21804.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21806.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21807.ps1 diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 index 2c71327cce00..857010c0c10c 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 @@ -62,6 +62,10 @@ function Push-CIPPDBCacheData { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "AuthorizationPolicy collection failed: $($_.Exception.Message)" -sev Error } + try { Set-CIPPDBCacheAuthenticationMethodsPolicy -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "AuthenticationMethodsPolicy collection failed: $($_.Exception.Message)" -sev Error + } + try { Set-CIPPDBCacheDeviceSettings -TenantFilter $TenantFilter } catch { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "DeviceSettings collection failed: $($_.Exception.Message)" -sev Error } diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheApps.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheApps.ps1 index 2ddfb27434f1..d972cc6d4bb1 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheApps.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheApps.ps1 @@ -15,7 +15,7 @@ function Set-CIPPDBCacheApps { try { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching applications' -sev Info - $Apps = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/applications?$top=999&$select=id,appId,displayName,createdDateTime,signInAudience' -tenantid $TenantFilter + $Apps = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/applications?$top=999' -tenantid $TenantFilter Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Apps' -Data $Apps Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Apps' -Data $Apps -Count $Apps = $null diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheAuthenticationMethodsPolicy.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheAuthenticationMethodsPolicy.ps1 new file mode 100644 index 000000000000..2700a8e2c3c2 --- /dev/null +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheAuthenticationMethodsPolicy.ps1 @@ -0,0 +1,26 @@ +function Set-CIPPDBCacheAuthenticationMethodsPolicy { + <# + .SYNOPSIS + Caches authentication methods policy for a tenant + + .PARAMETER TenantFilter + The tenant to cache authentication methods policy for + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter + ) + + try { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching authentication methods policy' -sev Info + $AuthMethodsPolicy = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/policies/authenticationMethodsPolicy' -tenantid $TenantFilter + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'AuthenticationMethodsPolicy' -Data @($AuthMethodsPolicy) + $AuthMethodsPolicy = $null + + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached authentication methods policy successfully' -sev Info + + } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache authentication methods policy: $($_.Exception.Message)" -sev Error + } +} diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21772.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21772.ps1 new file mode 100644 index 000000000000..30647ae0109a --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21772.ps1 @@ -0,0 +1,54 @@ +function Invoke-CippTestZTNA21772 { + param($Tenant) + + try { + $Apps = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Apps' + $ServicePrincipals = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ServicePrincipals' + + if (-not $Apps -and -not $ServicePrincipals) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21772' -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Applications and service principals not found in database' -Risk 'High' -Name 'Applications do not have client secrets configured' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Application Management' + return + } + + $AppsWithSecrets = @() + $SPsWithSecrets = @() + + if ($Apps) { + $AppsWithSecrets = $Apps | Where-Object { + $_.passwordCredentials -and + $_.passwordCredentials.Count -gt 0 -and + $_.passwordCredentials -ne '[]' + } + } + + if ($ServicePrincipals) { + $SPsWithSecrets = $ServicePrincipals | Where-Object { + $_.passwordCredentials -and + $_.passwordCredentials.Count -gt 0 -and + $_.passwordCredentials -ne '[]' + } + } + + $TotalWithSecrets = $AppsWithSecrets.Count + $SPsWithSecrets.Count + + if ($TotalWithSecrets -eq 0) { + $Status = 'Passed' + $Result = 'Applications in your tenant do not use client secrets' + } else { + $Status = 'Failed' + $Result = @" +Found $($AppsWithSecrets.Count) applications and $($SPsWithSecrets.Count) service principals with client secrets configured +## Apps with client secrets: +$(($AppsWithSecrets | ForEach-Object { "- $($_.displayName) (AppId: $($_.appId))" }) -join "`n") +## Service principals with client secrets: +$(($SPsWithSecrets | ForEach-Object { "- $($_.displayName) (AppId: $($_.appId))" }) -join "`n") +"@ + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21772' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'Applications do not have client secrets configured' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Application Management' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21772' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Applications do not have client secrets configured' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Application Management' + } +} diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21773.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21773.ps1 new file mode 100644 index 000000000000..231b9d41ce1a --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21773.ps1 @@ -0,0 +1,84 @@ +function Invoke-CippTestZTNA21773 { + param($Tenant) + + try { + $Apps = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Apps' + $ServicePrincipals = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ServicePrincipals' + + if (-not $Apps -and -not $ServicePrincipals) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21773' -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Applications and service principals not found in database' -Risk 'Medium' -Name 'Applications do not have certificates with expiration longer than 180 days' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Application Management' + return + } + + $MaxDate = (Get-Date).AddDays(180) + $AppsWithLongCerts = @() + $SPsWithLongCerts = @() + + if ($Apps) { + $AppsWithLongCerts = $Apps | Where-Object { + if ($_.keyCredentials -and $_.keyCredentials.Count -gt 0 -and $_.keyCredentials -ne '[]') { + $HasLongCert = $false + foreach ($Cred in $_.keyCredentials) { + if ($Cred.endDateTime) { + $EndDate = [datetime]$Cred.endDateTime + if ($EndDate -gt $MaxDate) { + $HasLongCert = $true + break + } + } + } + $HasLongCert + } else { + $false + } + } + } + + if ($ServicePrincipals) { + $SPsWithLongCerts = $ServicePrincipals | Where-Object { + if ($_.keyCredentials -and $_.keyCredentials.Count -gt 0 -and $_.keyCredentials -ne '[]') { + $HasLongCert = $false + foreach ($Cred in $_.keyCredentials) { + if ($Cred.endDateTime) { + $EndDate = [datetime]$Cred.endDateTime + if ($EndDate -gt $MaxDate) { + $HasLongCert = $true + break + } + } + } + $HasLongCert + } else { + $false + } + } + } + + $TotalWithLongCerts = $AppsWithLongCerts.Count + $SPsWithLongCerts.Count + + if ($TotalWithLongCerts -eq 0) { + $Status = 'Passed' + $Result = 'Applications in your tenant do not have certificates valid for more than 180 days' + } else { + $Status = 'Failed' + $Result = "Found $($AppsWithLongCerts.Count) applications and $($SPsWithLongCerts.Count) service principals with certificates longer than 180 days`n`n" + + if ($AppsWithLongCerts.Count -gt 0) { + $Result += "## Apps with long-lived certificates:`n`n" + $Result += ($AppsWithLongCerts | ForEach-Object { "- $($_.displayName) (AppId: $($_.appId))" }) -join "`n" + $Result += "`n`n" + } + + if ($SPsWithLongCerts.Count -gt 0) { + $Result += "## Service principals with long-lived certificates:`n`n" + $Result += ($SPsWithLongCerts | ForEach-Object { "- $($_.displayName) (AppId: $($_.appId))" }) -join "`n" + } + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21773' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'Applications do not have certificates with expiration longer than 180 days' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Application Management' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21773' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'Applications do not have certificates with expiration longer than 180 days' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Application Management' + } +} diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21774.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21774.ps1 new file mode 100644 index 000000000000..6d0ec4ef9fcf --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21774.ps1 @@ -0,0 +1,57 @@ +function Invoke-CippTestZTNA21774 { + param($Tenant) + + try { + $ServicePrincipals = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ServicePrincipals' + + if (-not $ServicePrincipals) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21774' -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Service principals not found in database' -Risk 'High' -Name 'Microsoft services applications do not have credentials configured' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Application Management' + return + } + + $MicrosoftTenantId = 'f8cdef31-a31e-4b4a-93e4-5f571e91255a' + + $MicrosoftSPs = $ServicePrincipals | Where-Object { + $_.appOwnerOrganizationId -eq $MicrosoftTenantId + } + + $SPsWithPasswordCreds = @() + $SPsWithKeyCreds = @() + + if ($MicrosoftSPs) { + $SPsWithPasswordCreds = $MicrosoftSPs | Where-Object { + $_.passwordCredentials -and + $_.passwordCredentials.Count -gt 0 -and + $_.passwordCredentials -ne '[]' + } + + $SPsWithKeyCreds = $MicrosoftSPs | Where-Object { + $_.keyCredentials -and + $_.keyCredentials.Count -gt 0 -and + $_.keyCredentials -ne '[]' + } + } + + $TotalWithCreds = $SPsWithPasswordCreds.Count + $SPsWithKeyCreds.Count + + if ($TotalWithCreds -eq 0) { + $Status = 'Passed' + $Result = 'No Microsoft services applications have credentials configured in the tenant' + } else { + $Status = 'Investigate' + $Result = @" +Found Microsoft services applications with credentials configured: $($SPsWithPasswordCreds.Count) with password credentials, $($SPsWithKeyCreds.Count) with key credentials +## Service principals with password credentials: +$(($SPsWithPasswordCreds | ForEach-Object { "- $($_.displayName) (AppId: $($_.appId))" }) -join "`n") +## Service principals with key credentials: +$(($SPsWithKeyCreds | ForEach-Object { "- $($_.displayName) (AppId: $($_.appId))" }) -join "`n") +"@ + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21774' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'Microsoft services applications do not have credentials configured' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Application Management' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21774' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Microsoft services applications do not have credentials configured' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Application Management' + } +} diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21780.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21780.ps1 new file mode 100644 index 000000000000..ec83a02e6123 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21780.ps1 @@ -0,0 +1,34 @@ +function Invoke-CippTestZTNA21780 { + param($Tenant) + + try { + $Recommendations = New-CIPPDbRequest -TenantFilter $Tenant -Type 'DirectoryRecommendations' + + if (-not $Recommendations) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21780' -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Directory recommendations not found in database' -Risk 'Medium' -Name 'No usage of ADAL in the tenant' -UserImpact 'Low' -ImplementationEffort 'High' -Category 'Application Management' + return + } + + $AdalRecommendations = $Recommendations | Where-Object { + $_.recommendationType -eq 'adalToMsalMigration' + } + + if ($AdalRecommendations.Count -eq 0) { + $Status = 'Passed' + $Result = 'No ADAL applications found in the tenant' + } else { + $Status = 'Failed' + $Result = @" + Found $($AdalRecommendations.Count) ADAL applications in the tenant that need migration to MSAL. + ADAL Applications: + $(($AdalRecommendations | ForEach-Object { "- $($_.applicationDisplayName) (AppId: $($_.applicationId))" }) -join "`n") +"@ + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21780' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'No usage of ADAL in the tenant' -UserImpact 'Low' -ImplementationEffort 'High' -Category 'Application Management' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21780' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'No usage of ADAL in the tenant' -UserImpact 'Low' -ImplementationEffort 'High' -Category 'Application Management' + } +} diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21783.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21783.ps1 new file mode 100644 index 000000000000..a33ffee7ef09 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21783.ps1 @@ -0,0 +1,52 @@ +function Invoke-CippTestZTNA21783 { + param($Tenant) + + try { + $CAPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ConditionalAccessPolicies' + $Roles = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Roles' + + if (-not $CAPolicies -or -not $Roles) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21783' -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Conditional Access policies or roles not found in database' -Risk 'High' -Name 'Privileged Microsoft Entra built-in roles are targeted with Conditional Access policies to enforce phishing-resistant methods' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Access Control' + return + } + + $PrivilegedRoles = $Roles | Where-Object { $_.isPrivileged -and $_.isBuiltIn } + + if (-not $PrivilegedRoles) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21783' -TestType 'Identity' -Status 'Passed' -ResultMarkdown 'No privileged built-in roles found in tenant' -Risk 'High' -Name 'Privileged Microsoft Entra built-in roles are targeted with Conditional Access policies to enforce phishing-resistant methods' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Access Control' + return + } + + $PhishResistantMethods = @('windowsHelloForBusiness', 'fido2', 'x509CertificateMultiFactor') + + $PhishResistantPolicies = $CAPolicies | Where-Object { + $_.state -eq 'enabled' -and + $_.grantControls.authenticationStrength -and + $_.conditions.users.includeRoles + } + + $CoveredRoleIds = $PhishResistantPolicies.conditions.users.includeRoles | Select-Object -Unique + + $UnprotectedRoles = $PrivilegedRoles | Where-Object { $_.id -notin $CoveredRoleIds } + + if ($UnprotectedRoles.Count -eq 0) { + $Status = 'Passed' + $Result = "All $($PrivilegedRoles.Count) privileged built-in roles are protected by Conditional Access policies enforcing phishing-resistant authentication" + } else { + $Status = 'Failed' + $UnprotectedCount = $UnprotectedRoles.Count + $ProtectedCount = $PrivilegedRoles.Count - $UnprotectedCount + $Result = @" +Found $UnprotectedCount unprotected privileged roles out of $($PrivilegedRoles.Count) total ($ProtectedCount protected) +## Unprotected privileged roles: +$(($UnprotectedRoles | ForEach-Object { "- $($_.displayName)" }) -join "`n") +"@ + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21783' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'Privileged Microsoft Entra built-in roles are targeted with Conditional Access policies to enforce phishing-resistant methods' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Access Control' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21783' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Privileged Microsoft Entra built-in roles are targeted with Conditional Access policies to enforce phishing-resistant methods' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Access Control' + } +} diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21784.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21784.ps1 new file mode 100644 index 000000000000..7241cbe6ff0b --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21784.ps1 @@ -0,0 +1,42 @@ +function Invoke-CippTestZTNA21784 { + param($Tenant) + + try { + $CAPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ConditionalAccessPolicies' + + if (-not $CAPolicies) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21784' -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Conditional Access policies not found in database' -Risk 'Medium' -Name 'All user sign in activity uses phishing-resistant authentication methods' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Access Control' + return + } + + $EnabledPolicies = $CAPolicies | Where-Object { $_.state -eq 'enabled' } + + $AllUsersPolicies = $EnabledPolicies | Where-Object { + $_.conditions.users.includeUsers -contains 'All' -and + $_.grantControls.authenticationStrength + } + + if (-not $AllUsersPolicies) { + $Status = 'Failed' + $Result = 'No Conditional Access policies found requiring phishing-resistant authentication for all users' + } else { + $PoliciesWithExclusions = $AllUsersPolicies | Where-Object { + $_.conditions.users.excludeUsers.Count -gt 0 + } + + if ($PoliciesWithExclusions.Count -gt 0) { + $Status = 'Failed' + $Result = "Found $($AllUsersPolicies.Count) policies requiring phishing-resistant authentication, but $($PoliciesWithExclusions.Count) have user exclusions creating coverage gaps" + } else { + $Status = 'Passed' + $Result = "All users are protected by $($AllUsersPolicies.Count) Conditional Access policies requiring phishing-resistant authentication" + } + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21784' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'All user sign in activity uses phishing-resistant authentication methods' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Access Control' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21784' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'All user sign in activity uses phishing-resistant authentication methods' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Access Control' + } +} diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21786.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21786.ps1 new file mode 100644 index 000000000000..ce2cf12cadfc --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21786.ps1 @@ -0,0 +1,37 @@ +function Invoke-CippTestZTNA21786 { + param($Tenant) + + try { + $CAPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ConditionalAccessPolicies' + + if (-not $CAPolicies) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21786' -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Conditional Access policies not found in database' -Risk 'High' -Name 'User sign-in activity uses token protection' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Access Control' + return + } + + $TokenProtectionPolicies = $CAPolicies | Where-Object { + $_.state -eq 'enabled' -and + $_.conditions.clientAppTypes.Count -eq 1 -and + $_.conditions.clientAppTypes[0] -eq 'mobileAppsAndDesktopClients' -and + $_.conditions.applications.includeApplications -contains '00000002-0000-0ff1-ce00-000000000000' -and + $_.conditions.applications.includeApplications -contains '00000003-0000-0ff1-ce00-000000000000' -and + $_.conditions.platforms.includePlatforms.Count -eq 1 -and + $_.conditions.platforms.includePlatforms -eq 'windows' -and + $_.sessionControls.secureSignInSession.isEnabled -eq $true + } + + if ($TokenProtectionPolicies.Count -gt 0) { + $Status = 'Passed' + $Result = "Found $($TokenProtectionPolicies.Count) token protection policies properly configured" + } else { + $Status = 'Failed' + $Result = 'No properly configured token protection policies found' + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21786' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'User sign-in activity uses token protection' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Access Control' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21786' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'User sign-in activity uses token protection' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Access Control' + } +} diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21787.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21787.ps1 new file mode 100644 index 000000000000..cb0b822ad748 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21787.ps1 @@ -0,0 +1,28 @@ +function Invoke-CippTestZTNA21787 { + param($Tenant) + + try { + $AuthPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthorizationPolicy' + + if (-not $AuthPolicy) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21787' -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Authorization policy not found in database' -Risk 'High' -Name 'Permissions to create new tenants are limited to the Tenant Creator role' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Privileged Access' + return + } + + $CanCreateTenants = $AuthPolicy.defaultUserRolePermissions.allowedToCreateTenants + + if ($CanCreateTenants -eq $false) { + $Status = 'Passed' + $Result = 'Non-privileged users are restricted from creating tenants' + } else { + $Status = 'Failed' + $Result = 'Non-privileged users are allowed to create tenants' + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21787' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'Permissions to create new tenants are limited to the Tenant Creator role' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Privileged Access' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21787' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Permissions to create new tenants are limited to the Tenant Creator role' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Privileged Access' + } +} diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21790.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21790.ps1 new file mode 100644 index 000000000000..91547c2fa4af --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21790.ps1 @@ -0,0 +1,36 @@ +function Invoke-CippTestZTNA21790 { + param($Tenant) + + try { + $CrossTenantPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'CrossTenantAccessPolicy' + + if (-not $CrossTenantPolicy) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21790' -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Cross-tenant access policy not found in database' -Risk 'High' -Name 'Outbound cross-tenant access settings are configured' -UserImpact 'Medium' -ImplementationEffort 'High' -Category 'Application Management' + return + } + + $B2BCollabOutbound = $CrossTenantPolicy.b2bCollaborationOutbound.usersAndGroups.accessType -eq 'blocked' -and + $CrossTenantPolicy.b2bCollaborationOutbound.usersAndGroups.targets[0].target -eq 'AllUsers' -and + $CrossTenantPolicy.b2bCollaborationOutbound.applications.accessType -eq 'blocked' -and + $CrossTenantPolicy.b2bCollaborationOutbound.applications.targets[0].target -eq 'AllApplications' + + $B2BDirectOutbound = $CrossTenantPolicy.b2bDirectConnectOutbound.usersAndGroups.accessType -eq 'blocked' -and + $CrossTenantPolicy.b2bDirectConnectOutbound.usersAndGroups.targets[0].target -eq 'AllUsers' -and + $CrossTenantPolicy.b2bDirectConnectOutbound.applications.accessType -eq 'blocked' -and + $CrossTenantPolicy.b2bDirectConnectOutbound.applications.targets[0].target -eq 'AllApplications' + + if ($B2BCollabOutbound -and $B2BDirectOutbound) { + $Status = 'Passed' + $Result = 'Default cross-tenant access outbound policy blocks all access' + } else { + $Status = 'Failed' + $Result = 'Default cross-tenant access outbound policy has unrestricted access' + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21790' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'Outbound cross-tenant access settings are configured' -UserImpact 'Medium' -ImplementationEffort 'High' -Category 'Application Management' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21790' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Outbound cross-tenant access settings are configured' -UserImpact 'Medium' -ImplementationEffort 'High' -Category 'Application Management' + } +} diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21791.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21791.ps1 new file mode 100644 index 000000000000..bca84e547390 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21791.ps1 @@ -0,0 +1,28 @@ +function Invoke-CippTestZTNA21791 { + param($Tenant) + + try { + $AuthPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthorizationPolicy' + + if (-not $AuthPolicy) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21791' -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Authorization policy not found in database' -Risk 'Medium' -Name 'Guests cannot invite other guests' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'External Collaboration' + return + } + + $AllowInvitesFrom = $AuthPolicy.allowInvitesFrom + + if ($AllowInvitesFrom -ne 'everyone') { + $Status = 'Passed' + $Result = "Tenant restricts who can invite guests (setting: $AllowInvitesFrom)" + } else { + $Status = 'Failed' + $Result = 'Tenant allows any user including guests to invite other guests' + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21791' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'Guests cannot invite other guests' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'External Collaboration' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21791' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'Guests cannot invite other guests' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'External Collaboration' + } +} diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21792.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21792.ps1 new file mode 100644 index 000000000000..9f8af722e3ce --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21792.ps1 @@ -0,0 +1,29 @@ +function Invoke-CippTestZTNA21792 { + param($Tenant) + + try { + $AuthPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthorizationPolicy' + + if (-not $AuthPolicy) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21792' -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Authorization policy not found in database' -Risk 'Medium' -Name 'Guests have restricted access to directory objects' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'External Collaboration' + return + } + + $GuestRestrictedRoleId = '2af84b1e-32c8-42b7-82bc-daa82404023b' + $GuestRoleId = $AuthPolicy.guestUserRoleId + + if ($GuestRoleId -eq $GuestRestrictedRoleId) { + $Status = 'Passed' + $Result = 'Guest user access is properly restricted' + } else { + $Status = 'Failed' + $Result = 'Guest user access is not restricted' + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21792' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'Guests have restricted access to directory objects' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'External Collaboration' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21792' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'Guests have restricted access to directory objects' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'External Collaboration' + } +} diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21793.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21793.ps1 new file mode 100644 index 000000000000..716f7ea18656 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21793.ps1 @@ -0,0 +1,39 @@ +function Invoke-CippTestZTNA21793 { + param($Tenant) + + try { + $CrossTenantPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'CrossTenantAccessPolicy' + + if (-not $CrossTenantPolicy) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21793' -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Cross-tenant access policy not found in database' -Risk 'High' -Name 'Tenant restrictions v2 policy is configured' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Application Management' + return + } + + $TenantRestrictions = $CrossTenantPolicy.tenantRestrictions + + if (-not $TenantRestrictions) { + $Status = 'Failed' + $Result = 'Tenant Restrictions v2 policy is not configured' + } else { + $UsersBlocked = $TenantRestrictions.usersAndGroups.accessType -eq 'blocked' -and + $TenantRestrictions.usersAndGroups.targets[0].target -eq 'AllUsers' + + $AppsBlocked = $TenantRestrictions.applications.accessType -eq 'blocked' -and + $TenantRestrictions.applications.targets[0].target -eq 'AllApplications' + + if ($UsersBlocked -and $AppsBlocked) { + $Status = 'Passed' + $Result = 'Tenant Restrictions v2 policy is properly configured' + } else { + $Status = 'Failed' + $Result = 'Tenant Restrictions v2 policy is configured but not properly restricting all users and applications' + } + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21793' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'Tenant restrictions v2 policy is configured' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Application Management' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21793' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Tenant restrictions v2 policy is configured' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Application Management' + } +} diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21796.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21796.ps1 new file mode 100644 index 000000000000..56ac4ddd545e --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21796.ps1 @@ -0,0 +1,40 @@ +function Invoke-CippTestZTNA21796 { + param($Tenant) + + try { + $CAPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ConditionalAccessPolicies' + + if (-not $CAPolicies) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21796' -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Conditional Access policies not found in database' -Risk 'Medium' -Name 'Block legacy authentication policy is configured' -UserImpact 'High' -ImplementationEffort 'Low' -Category 'Access Control' + return + } + + $BlockPolicies = $CAPolicies | Where-Object { + $_.grantControls.builtInControls -contains 'block' -and + $_.conditions.clientAppTypes -contains 'exchangeActiveSync' -and + $_.conditions.clientAppTypes -contains 'other' + } + + $EnabledBlockPolicies = $BlockPolicies | Where-Object { + $_.conditions.users.includeUsers -contains 'All' -and + $_.state -eq 'enabled' + } + + if ($EnabledBlockPolicies.Count -ge 1) { + $Status = 'Passed' + $Result = "Found $($EnabledBlockPolicies.Count) properly configured policies blocking legacy authentication" + } elseif ($BlockPolicies.Count -ge 1) { + $Status = 'Failed' + $Result = 'Policies to block legacy authentication found but not properly configured or enabled' + } else { + $Status = 'Failed' + $Result = 'No conditional access policies to block legacy authentication found' + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21796' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'Block legacy authentication policy is configured' -UserImpact 'High' -ImplementationEffort 'Low' -Category 'Access Control' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21796' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'Block legacy authentication policy is configured' -UserImpact 'High' -ImplementationEffort 'Low' -Category 'Access Control' + } +} diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21797.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21797.ps1 new file mode 100644 index 000000000000..a904e70545b0 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21797.ps1 @@ -0,0 +1,146 @@ +function Invoke-CippTestZTNA21797 { + param($Tenant) + + try { + $allCAPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ConditionalAccessPolicies' + $authMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' + + if (-not $allCAPolicies -or -not $authMethodsPolicy) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21797' -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Required policies not found in database' -Risk 'High' -Name 'Restrict access to high risk users' -UserImpact 'High' -ImplementationEffort 'Medium' -Category 'Conditional Access' + return + } + + $caPasswordChangePolicies = $allCAPolicies | Where-Object { + $_.conditions.userRiskLevels -contains 'high' -and + $_.grantControls.builtInControls -contains 'passwordChange' -and + $_.state -eq 'enabled' + } + + $caBlockPolicies = $allCAPolicies | Where-Object { + $_.conditions.userRiskLevels -contains 'high' -and + $_.grantControls.builtInControls -contains 'block' -and + $_.state -eq 'enabled' + } + + $inactiveCAPolicies = $allCAPolicies | Where-Object { + $_.conditions.userRiskLevels -contains 'high' -and + ($_.grantControls.builtInControls -contains 'passwordChange' -or $_.grantControls.builtInControls -contains 'block') -and + $_.state -ne 'enabled' + } + + $passwordlessEnabled = $false + $passwordlessAuthMethods = @() + + if ($authMethodsPolicy.authenticationMethodConfigurations) { + foreach ($method in $authMethodsPolicy.authenticationMethodConfigurations) { + $isPasswordless = $false + $methodName = $method.id + $methodState = $method.state + $additionalInfo = '' + + if ($method.id -in @('fido2')) { + $isPasswordless = ($method.state -eq 'enabled') + } + + if ($method.id -eq 'x509Certificate') { + if ($method.state -eq 'enabled' -and $method.x509CertificateAuthenticationDefaultMode -eq 'x509CertificateMultiFactor') { + $isPasswordless = $true + $additionalInfo = ' (Mode: x509CertificateMultiFactor)' + } + } + + if ($isPasswordless) { + $passwordlessEnabled = $true + $passwordlessAuthMethods += [PSCustomObject]@{ + Name = $methodName + State = $methodState + AdditionalInfo = $additionalInfo + } + } + } + } + + $result = $false + if ((-not $passwordlessEnabled -and ($caPasswordChangePolicies.Count + $caBlockPolicies.Count -gt 0)) -or + ($passwordlessEnabled -and $caBlockPolicies.Count -gt 0)) { + $result = $true + } + + $testResultMarkdown = '' + + if ($result) { + $testResultMarkdown = 'Policies to restrict access for high risk users are properly implemented.' + } else { + if ($passwordlessEnabled -and $caBlockPolicies.Count -eq 0) { + $testResultMarkdown = 'Passwordless authentication is enabled, but no policies to block high risk users are configured.' + } else { + $testResultMarkdown = 'No policies found to protect against high risk users.' + } + } + + $mdInfo = "`n## Passwordless Authentication Methods allowed in tenant`n`n" + + if ($passwordlessAuthMethods.Count -gt 0) { + $mdInfo += "| Authentication Method Name | State | Additional Info |`n" + $mdInfo += "| :------------------------ | :---- | :-------------- |`n" + foreach ($method in $passwordlessAuthMethods) { + $mdInfo += "| $($method.Name) | $($method.State) | $($method.AdditionalInfo) |`n" + } + } else { + $mdInfo += "No passwordless authentication methods are enabled.`n" + } + + $mdInfo += "`n## Conditional Access Policies targeting high risk users`n`n" + + $allEnabledHighRiskPolicies = @($caPasswordChangePolicies) + @($caBlockPolicies) + + if ($allEnabledHighRiskPolicies.Count -gt 0) { + $mdInfo += "| Conditional Access Policy Name | Status | Conditions |`n" + $mdInfo += "| :--------------------- | :----- | :--------- |`n" + + foreach ($policy in $allEnabledHighRiskPolicies) { + $conditions = 'User Risk Level: High' + if ($policy.grantControls.builtInControls -contains 'passwordChange') { + $conditions += ', Control: Password Change' + } + if ($policy.grantControls.builtInControls -contains 'block') { + $conditions += ', Control: Block' + } + $mdInfo += "| $($policy.displayName) | Enabled | $conditions |`n" + } + } + + if ($inactiveCAPolicies.Count -gt 0) { + if ($allEnabledHighRiskPolicies.Count -eq 0) { + $mdInfo += "No conditional access policies targeting high risk users found.`n`n" + $mdInfo += "### Inactive policies targeting high risk users (not contributing to security posture):`n`n" + $mdInfo += "| Conditional Access Policy Name | Status | Conditions |`n" + $mdInfo += "| :--------------------- | :----- | :--------- |`n" + } + + foreach ($policy in $inactiveCAPolicies) { + $conditions = 'User Risk Level: High' + if ($policy.grantControls.builtInControls -contains 'passwordChange') { + $conditions += ', Control: Password Change' + } + if ($policy.grantControls.builtInControls -contains 'block') { + $conditions += ', Control: Block' + } + $status = if ($policy.state -eq 'enabledForReportingButNotEnforced') { 'Report-only' } else { 'Disabled' } + $mdInfo += "| $($policy.displayName) | $status | $conditions |`n" + } + } elseif ($allEnabledHighRiskPolicies.Count -eq 0) { + $mdInfo += "No conditional access policies targeting high risk users found.`n" + } + + $testResultMarkdown = $testResultMarkdown + $mdInfo + + $passed = $result + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21797' -TestType 'Identity' -Status $passed -ResultMarkdown $testResultMarkdown -Risk 'High' -Name 'Restrict access to high risk users' -UserImpact 'High' -ImplementationEffort 'Medium' -Category 'Conditional Access' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21797' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Restrict access to high risk users' -UserImpact 'High' -ImplementationEffort 'Medium' -Category 'Conditional Access' + } +} diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21799.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21799.ps1 new file mode 100644 index 000000000000..028cbe0804c8 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21799.ps1 @@ -0,0 +1,82 @@ +function Invoke-CippTestZTNA21799 { + param($Tenant) + + try { + $authMethodPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' + $allCAPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ConditionalAccessPolicies' + + if (-not $allCAPolicies -or -not $authMethodPolicy) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21799' -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Required policies not found in database' -Risk 'High' -Name 'Restrict high risk sign-ins' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Conditional Access' + return + } + + $matchedPolicies = $null + + if (($authMethodPolicy.authenticationMethodConfigurations.state -eq 'enabled').count -gt 0) { + $matchedPolicies = $allCAPolicies | Where-Object { + $_.conditions.signInRiskLevels -eq 'high' -and + ($_.conditions.users.includeUsers -contains 'All') -and + ($_.grantControls.builtInControls -contains 'block' -or $_.grantControls.builtInControls -contains 'mfa' -or $null -ne $_.grantControls.authenticationStrength) -and + ($_.state -eq 'enabled') + } + } else { + $matchedPolicies = $allCAPolicies | Where-Object { + $_.conditions.signInRiskLevels -eq 'high' -and + ($_.conditions.users.includeUsers -contains 'All') -and + ($_.grantControls.builtInControls -contains 'block') -and + ($_.state -eq 'enabled') + } + } + + $testResultMarkdown = '' + + if ($matchedPolicies.Count -gt 0) { + $passed = 'Passed' + $testResultMarkdown = 'All high-risk sign-in attempts are mitigated by Conditional Access policies enforcing appropriate controls.' + } else { + $passed = 'Failed' + $testResultMarkdown = 'Some high-risk sign-in attempts are not adequately mitigated by Conditional Access policies.' + } + + $reportTitle = 'Conditional Access Policies targeting high-risk sign-in attempts' + $tableRows = '' + + if ($matchedPolicies.Count -gt 0) { + $mdInfo = "`n## $reportTitle`n`n" + $mdInfo += "| Policy Name | Grant Controls | Target Users |`n" + $mdInfo += "| :---------- | :------------- | :----------- |`n" + + foreach ($policy in $matchedPolicies) { + $grantControls = switch ($policy.grantControls) { + { $_.builtInControls -contains 'block' } { + 'Block Access' + } + { $_.builtInControls -contains 'mfa' } { + 'Require Multi-Factor Authentication' + } + { $null -ne $_.authenticationStrength } { + 'Require Authentication Strength' + } + } + + $targetUsers = if ($policy.conditions.users.includeUsers -contains 'All') { + 'All Users' + } else { + $policy.conditions.users.includeUsers -join ', ' + } + + $mdInfo += "| $($policy.displayName) | $grantControls | $targetUsers |`n" + } + } else { + $mdInfo = 'Some high-risk sign-in attempts are not adequately mitigated by Conditional Access policies.' + } + + $testResultMarkdown = $testResultMarkdown + $mdInfo + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21799' -TestType 'Identity' -Status $passed -ResultMarkdown $testResultMarkdown -Risk 'High' -Name 'Block high risk sign-ins' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Conditional Access' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21799' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Restrict high risk sign-ins' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Conditional Access' + } +} diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21802.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21802.ps1 new file mode 100644 index 000000000000..148520a3a181 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21802.ps1 @@ -0,0 +1,36 @@ +function Invoke-CippTestZTNA21802 { + param($Tenant) + + try { + $AuthMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' + + if (-not $AuthMethodsPolicy) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21802' -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Authentication methods policy not found in database' -Risk 'Medium' -Name 'Microsoft Authenticator app shows sign-in context' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Access Control' + return + } + + $AuthenticatorConfig = $AuthMethodsPolicy.authenticationMethodConfigurations | Where-Object { $_.id -eq 'MicrosoftAuthenticator' } + + if (-not $AuthenticatorConfig) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21802' -TestType 'Identity' -Status 'Failed' -ResultMarkdown 'Microsoft Authenticator configuration not found in authentication methods policy' -Risk 'Medium' -Name 'Microsoft Authenticator app shows sign-in context' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Access Control' + return + } + + $AppInfoEnabled = $AuthenticatorConfig.featureSettings.displayAppInformationRequiredState.state -eq 'enabled' + $LocationInfoEnabled = $AuthenticatorConfig.featureSettings.displayLocationInformationRequiredState.state -eq 'enabled' + + if ($AppInfoEnabled -and $LocationInfoEnabled) { + $Status = 'Passed' + $Result = 'Microsoft Authenticator shows application name and geographic location in push notifications' + } else { + $Status = 'Failed' + $Result = "Microsoft Authenticator sign-in context incomplete - App info: $AppInfoEnabled, Location info: $LocationInfoEnabled" + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21802' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'Microsoft Authenticator app shows sign-in context' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Access Control' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21802' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'Microsoft Authenticator app shows sign-in context' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Access Control' + } +} diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21803.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21803.ps1 new file mode 100644 index 000000000000..afebf9487bec --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21803.ps1 @@ -0,0 +1,31 @@ +function Invoke-CippTestZTNA21803 { + param($Tenant) + + try { + $AuthMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' + + if (-not $AuthMethodsPolicy) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21803' -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Authentication methods policy not found in database' -Risk 'Medium' -Name 'Migrate from legacy MFA and SSPR policies' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Credential Management' + return + } + + $PolicyMigrationState = $AuthMethodsPolicy.policyMigrationState + + if ($PolicyMigrationState -eq 'migrationComplete') { + $Status = 'Passed' + $Result = 'Tenant has migrated from legacy MFA and SSPR policies to authentication methods policy' + } elseif ($PolicyMigrationState -eq 'migrationInProgress') { + $Status = 'Investigate' + $Result = 'Tenant migration from legacy MFA and SSPR policies is in progress' + } else { + $Status = 'Failed' + $Result = "Tenant has not migrated from legacy MFA and SSPR policies (state: $PolicyMigrationState)" + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21803' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'Migrate from legacy MFA and SSPR policies' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Credential Management' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21803' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'Migrate from legacy MFA and SSPR policies' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Credential Management' + } +} diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21804.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21804.ps1 new file mode 100644 index 000000000000..c8b7b68e8ee2 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21804.ps1 @@ -0,0 +1,42 @@ +function Invoke-CippTestZTNA21804 { + param($Tenant) + + try { + $authMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' + + if (-not $authMethodsPolicy) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21804' -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Authentication methods policy not found in database' -Risk 'High' -Name 'SMS and Voice Call authentication methods are disabled' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Credential Management' + return + } + + $matchedMethods = $authMethodsPolicy.authenticationMethodConfigurations | Where-Object { $_.id -eq 'Sms' -or $_.id -eq 'Voice' } + + $testResultMarkdown = '' + + if ($matchedMethods.state -contains 'enabled') { + $passed = $false + $testResultMarkdown = 'Found weak authentication methods that are still enabled.' + } else { + $passed = $true + $testResultMarkdown = 'SMS and voice calls authentication methods are disabled in the tenant.' + } + + $reportTitle = 'Weak authentication methods' + + $mdInfo = "`n## $reportTitle`n`n" + $mdInfo += "| Method ID | Is method weak? | State |`n" + $mdInfo += "| :-------- | :-------------- | :---- |`n" + + foreach ($method in $matchedMethods) { + $mdInfo += "| $($method.id) | Yes | $($method.state) |`n" + } + + $testResultMarkdown = $testResultMarkdown + $mdInfo + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21804' -TestType 'Identity' -Status $passed -ResultMarkdown $testResultMarkdown -Risk 'High' -Name 'Weak authentication methods are disabled' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Conditional Access' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21804' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'SMS and Voice Call authentication methods are disabled' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Credential Management' + } +} diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21806.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21806.ps1 new file mode 100644 index 000000000000..dbfbf1f27e60 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21806.ps1 @@ -0,0 +1,51 @@ +function Invoke-CippTestZTNA21806 { + param($Tenant) + + try { + $allCAPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ConditionalAccessPolicies' + + if (-not $allCAPolicies) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21806' -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Conditional Access policies not found in database' -Risk 'High' -Name 'Secure the MFA registration (My Security Info) page' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Conditional Access' + return + } + + $matchedPolicies = $allCAPolicies | Where-Object { + ($_.conditions.applications.includeUserActions -contains 'urn:user:registersecurityinfo') -and + ($_.conditions.users.includeUsers -contains 'All') -and + $_.state -eq 'enabled' + } + + $testResultMarkdown = '' + + if ($matchedPolicies.Count -gt 0) { + $passed = 'Passed' + $testResultMarkdown = 'Security information registration is protected by Conditional Access policies.' + } else { + $passed = 'Failed' + $testResultMarkdown = 'Security information registration is not protected by Conditional Access policies.' + } + + $reportTitle = 'Conditional Access Policies targeting security information registration' + $tableRows = '' + + if ($matchedPolicies.Count -gt 0) { + $mdInfo = "`n## $reportTitle`n`n" + $mdInfo += "| Policy Name | User Actions Targeted | Grant Controls Applied |`n" + $mdInfo += "| :---------- | :-------------------- | :--------------------- |`n" + + foreach ($policy in $matchedPolicies) { + $mdInfo += "| $($policy.displayName) | $($policy.conditions.applications.includeUserActions) | $($policy.grantControls.builtInControls -join ', ') |`n" + } + } else { + $mdInfo = 'No Conditional Access policies targeting security information registration.' + } + + $testResultMarkdown = $testResultMarkdown + $mdInfo + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21806' -TestType 'Identity' -Status $passed -ResultMarkdown $testResultMarkdown -Risk 'High' -Name 'Secure the MFA registration (My Security Info) page' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Conditional Access' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21806' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Secure the MFA registration (My Security Info) page' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Conditional Access' + } +} diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21807.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21807.ps1 new file mode 100644 index 000000000000..5e106a245452 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21807.ps1 @@ -0,0 +1,28 @@ +function Invoke-CippTestZTNA21807 { + param($Tenant) + + try { + $AuthPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthorizationPolicy' + + if (-not $AuthPolicy) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21807' -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Authorization policy not found in database' -Risk 'Medium' -Name 'Creating new applications and service principals is restricted to privileged users' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Application Management' + return + } + + $CanCreateApps = $AuthPolicy.defaultUserRolePermissions.allowedToCreateApps + + if ($CanCreateApps -eq $false) { + $Status = 'Passed' + $Result = 'Tenant is configured to prevent users from registering applications' + } else { + $Status = 'Failed' + $Result = 'Tenant allows all non-privileged users to register applications' + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21807' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'Creating new applications and service principals is restricted to privileged users' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Application Management' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21807' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'Creating new applications and service principals is restricted to privileged users' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Application Management' + } +} From 2323719fef50c98a6c4452a7eff7dc14888ebc93 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Tue, 23 Dec 2025 23:49:00 +0100 Subject: [PATCH 022/503] Add more tests --- .../Push-CIPPDBCacheData.ps1 | 16 ++ Modules/CIPPCore/Public/Get-CippDbRole.ps1 | 53 +++++ .../CIPPCore/Public/Get-CippDbRoleMembers.ps1 | 49 +++++ .../Public/Set-CIPPDBCacheDomains.ps1 | 26 +++ ...DBCacheRoleAssignmentScheduleInstances.ps1 | 28 +++ ...et-CIPPDBCacheRoleEligibilitySchedules.ps1 | 26 +++ .../Set-CIPPDBCacheRoleManagementPolicies.ps1 | 26 +++ .../Public/Tests/Invoke-CippTestZTNA21809.ps1 | 26 +++ .../Public/Tests/Invoke-CippTestZTNA21810.ps1 | 35 ++++ .../Public/Tests/Invoke-CippTestZTNA21811.ps1 | 77 +++++++ .../Public/Tests/Invoke-CippTestZTNA21812.ps1 | 61 ++++++ .../Public/Tests/Invoke-CippTestZTNA21813.ps1 | 152 ++++++++++++++ .../Public/Tests/Invoke-CippTestZTNA21814.ps1 | 76 +++++++ .../Public/Tests/Invoke-CippTestZTNA21815.ps1 | 67 ++++++ .../Public/Tests/Invoke-CippTestZTNA21816.ps1 | 191 ++++++++++++++++++ .../Public/Tests/Invoke-CippTestZTNA21818.ps1 | 120 +++++++++++ .../Public/Tests/Invoke-CippTestZTNA21824.ps1 | 82 ++++++++ .../Public/Tests/Invoke-CippTestZTNA21828.ps1 | 52 +++++ .../Public/Tests/Invoke-CippTestZTNA21849.ps1 | 57 ++++++ Test-AllZTNATests.ps1 | 2 + 20 files changed, 1222 insertions(+) create mode 100644 Modules/CIPPCore/Public/Get-CippDbRole.ps1 create mode 100644 Modules/CIPPCore/Public/Get-CippDbRoleMembers.ps1 create mode 100644 Modules/CIPPCore/Public/Set-CIPPDBCacheDomains.ps1 create mode 100644 Modules/CIPPCore/Public/Set-CIPPDBCacheRoleAssignmentScheduleInstances.ps1 create mode 100644 Modules/CIPPCore/Public/Set-CIPPDBCacheRoleEligibilitySchedules.ps1 create mode 100644 Modules/CIPPCore/Public/Set-CIPPDBCacheRoleManagementPolicies.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21809.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21810.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21811.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21812.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21813.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21814.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21815.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21816.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21818.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21824.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21828.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21849.ps1 create mode 100644 Test-AllZTNATests.ps1 diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 index 857010c0c10c..d120b56eeedc 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 @@ -102,6 +102,22 @@ function Push-CIPPDBCacheData { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "PIMSettings collection failed: $($_.Exception.Message)" -sev Error } + try { Set-CIPPDBCacheDomains -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Domains collection failed: $($_.Exception.Message)" -sev Error + } + + try { Set-CIPPDBCacheRoleEligibilitySchedules -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "RoleEligibilitySchedules collection failed: $($_.Exception.Message)" -sev Error + } + + try { Set-CIPPDBCacheRoleManagementPolicies -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "RoleManagementPolicies collection failed: $($_.Exception.Message)" -sev Error + } + + try { Set-CIPPDBCacheRoleAssignmentScheduleInstances -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "RoleAssignmentScheduleInstances collection failed: $($_.Exception.Message)" -sev Error + } + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Completed database cache collection for tenant' -sev Info } catch { diff --git a/Modules/CIPPCore/Public/Get-CippDbRole.ps1 b/Modules/CIPPCore/Public/Get-CippDbRole.ps1 new file mode 100644 index 000000000000..bbf42843fcd0 --- /dev/null +++ b/Modules/CIPPCore/Public/Get-CippDbRole.ps1 @@ -0,0 +1,53 @@ +function Get-CippDbRole { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string]$TenantFilter, + + [Parameter(Mandatory = $false)] + [switch]$IncludePrivilegedRoles, + + [Parameter(Mandatory = $false)] + [switch]$CisaHighlyPrivilegedRoles + ) + + $Roles = New-CIPPDbRequest -TenantFilter $TenantFilter -Type 'Roles' + + if ($IncludePrivilegedRoles) { + $PrivilegedRoleTemplateIds = @( + '62e90394-69f5-4237-9190-012177145e10', + '194ae4cb-b126-40b2-bd5b-6091b380977d', + '9b895d92-2cd3-44c7-9d02-a6ac2d5ea5c3', + 'e8611ab8-c189-46e8-94e1-60213ab1f814', + '29232cdf-9323-42fd-ade2-1d097af3e4de', + 'b1be1c3e-b65d-4f19-8427-f6fa0d97feb9', + 'f28a1f50-f6e7-4571-818b-6a12f2af6b6c', + 'fe930be7-5e62-47db-91af-98c3a49a38b1', + '729827e3-9c14-49f7-bb1b-9608f156bbb8', + '966707d0-3269-4727-9be2-8c3a10f19b9d', + 'b0f54661-2d74-4c50-afa3-1ec803f12efe', + '7be44c8a-adaf-4e2a-84d6-ab2649e08a13', + '158c047a-c907-4556-b7ef-446551a6b5f7', + 'c4e39bd9-1100-46d3-8c65-fb160da0071f', + '9f06204d-73c1-4d4c-880a-6edb90606fd8', + '17315797-102d-40b4-93e0-432062caca18', + '4a5d8f65-41da-4de4-8968-e035b65339cf', + '75941009-915a-4869-abe7-691bff18279e' + ) + $Roles = $Roles | Where-Object { $PrivilegedRoleTemplateIds -contains $_.templateId } + } + + if ($CisaHighlyPrivilegedRoles) { + $CisaRoleTemplateIds = @( + '62e90394-69f5-4237-9190-012177145e10', + '9b895d92-2cd3-44c7-9d02-a6ac2d5ea5c3', + '29232cdf-9323-42fd-ade2-1d097af3e4de', + '729827e3-9c14-49f7-bb1b-9608f156bbb8', + '966707d0-3269-4727-9be2-8c3a10f19b9d', + 'b0f54661-2d74-4c50-afa3-1ec803f12efe' + ) + $Roles = $Roles | Where-Object { $CisaRoleTemplateIds -contains $_.templateId } + } + + return $Roles +} diff --git a/Modules/CIPPCore/Public/Get-CippDbRoleMembers.ps1 b/Modules/CIPPCore/Public/Get-CippDbRoleMembers.ps1 new file mode 100644 index 000000000000..907917fa69d7 --- /dev/null +++ b/Modules/CIPPCore/Public/Get-CippDbRoleMembers.ps1 @@ -0,0 +1,49 @@ +function Get-CippDbRoleMembers { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string]$TenantFilter, + + [Parameter(Mandatory = $true)] + [string]$RoleTemplateId + ) + + $RoleAssignments = New-CIPPDbRequest -TenantFilter $TenantFilter -Type 'RoleAssignmentScheduleInstances' + $RoleEligibilities = New-CIPPDbRequest -TenantFilter $TenantFilter -Type 'RoleEligibilitySchedules' + + $ActiveMembers = $RoleAssignments | Where-Object { + $_.roleDefinitionId -eq $RoleTemplateId -and $_.assignmentType -eq 'Assigned' + } + + $EligibleMembers = $RoleEligibilities | Where-Object { + $_.roleDefinitionId -eq $RoleTemplateId + } + + $AllMembers = [System.Collections.Generic.List[object]]::new() + + foreach ($member in $ActiveMembers) { + $memberObj = [PSCustomObject]@{ + id = $member.principalId + displayName = $member.principal.displayName + userPrincipalName = $member.principal.userPrincipalName + '@odata.type' = $member.principal.'@odata.type' + AssignmentType = 'Active' + } + $AllMembers.Add($memberObj) + } + + foreach ($member in $EligibleMembers) { + if ($AllMembers.id -notcontains $member.principalId) { + $memberObj = [PSCustomObject]@{ + id = $member.principalId + displayName = $member.principal.displayName + userPrincipalName = $member.principal.userPrincipalName + '@odata.type' = $member.principal.'@odata.type' + AssignmentType = 'Eligible' + } + $AllMembers.Add($memberObj) + } + } + + return $AllMembers +} diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheDomains.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheDomains.ps1 new file mode 100644 index 000000000000..a4943be1759d --- /dev/null +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheDomains.ps1 @@ -0,0 +1,26 @@ +function Set-CIPPDBCacheDomains { + <# + .SYNOPSIS + Caches domains for a tenant + + .PARAMETER TenantFilter + The tenant to cache domains for + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter + ) + + try { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching domains' -sev Info + $Domains = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/domains' -tenantid $TenantFilter + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Domains' -Data @($Domains) + $Domains = $null + + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached domains successfully' -sev Info + + } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache domains: $($_.Exception.Message)" -sev Error + } +} diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheRoleAssignmentScheduleInstances.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheRoleAssignmentScheduleInstances.ps1 new file mode 100644 index 000000000000..6d269f2cf17c --- /dev/null +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheRoleAssignmentScheduleInstances.ps1 @@ -0,0 +1,28 @@ +function Set-CIPPDBCacheRoleAssignmentScheduleInstances { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string]$TenantFilter + ) + + try { + $RoleAssignmentScheduleInstances = New-GraphGetRequest -Uri 'https://graph.microsoft.com/v1.0/roleManagement/directory/roleAssignmentScheduleInstances' -tenantid $TenantFilter + + $Body = [pscustomobject]@{ + Tenant = $TenantFilter + LastRefresh = (Get-Date).ToUniversalTime() + Type = 'RoleAssignmentScheduleInstances' + Data = [System.Text.Encoding]::UTF8.GetBytes(($RoleAssignmentScheduleInstances | ConvertTo-Json -Compress -Depth 10)) + PartitionKey = 'TenantCache' + RowKey = ('{0}-{1}' -f $TenantFilter, 'RoleAssignmentScheduleInstances') + SchemaVersion = [int]1 + SentAsDate = [string](Get-Date -UFormat '+%Y-%m-%dT%H:%M:%S.000Z') + } + + $null = Add-CIPPAzDataTableEntity @CacheTableDetails -Entity $Body -Force + Write-LogMessage -API 'DBCache' -tenant $TenantFilter -message 'Role assignment schedule instances cache updated' -sev Debug + } catch { + Write-LogMessage -API 'DBCache' -tenant $TenantFilter -message "Error caching role assignment schedule instances: $($_.Exception.Message)" -sev Error + throw + } +} diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheRoleEligibilitySchedules.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheRoleEligibilitySchedules.ps1 new file mode 100644 index 000000000000..6433570cf810 --- /dev/null +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheRoleEligibilitySchedules.ps1 @@ -0,0 +1,26 @@ +function Set-CIPPDBCacheRoleEligibilitySchedules { + <# + .SYNOPSIS + Caches role eligibility schedules for a tenant + + .PARAMETER TenantFilter + The tenant to cache role eligibility schedules for + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter + ) + + try { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching role eligibility schedules' -sev Info + $RoleEligibilitySchedules = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/roleManagement/directory/roleEligibilitySchedules' -tenantid $TenantFilter + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'RoleEligibilitySchedules' -Data @($RoleEligibilitySchedules) + $RoleEligibilitySchedules = $null + + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached role eligibility schedules successfully' -sev Info + + } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache role eligibility schedules: $($_.Exception.Message)" -sev Error + } +} diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheRoleManagementPolicies.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheRoleManagementPolicies.ps1 new file mode 100644 index 000000000000..fcba68720a1f --- /dev/null +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheRoleManagementPolicies.ps1 @@ -0,0 +1,26 @@ +function Set-CIPPDBCacheRoleManagementPolicies { + <# + .SYNOPSIS + Caches role management policies for a tenant + + .PARAMETER TenantFilter + The tenant to cache role management policies for + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter + ) + + try { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching role management policies' -sev Info + $RoleManagementPolicies = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/policies/roleManagementPolicies' -tenantid $TenantFilter + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'RoleManagementPolicies' -Data @($RoleManagementPolicies) + $RoleManagementPolicies = $null + + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached role management policies successfully' -sev Info + + } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache role management policies: $($_.Exception.Message)" -sev Error + } +} diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21809.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21809.ps1 new file mode 100644 index 000000000000..d991b301f249 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21809.ps1 @@ -0,0 +1,26 @@ +function Invoke-CippTestZTNA21809 { + param($Tenant) + + try { + $result = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AdminConsentRequestPolicy' + + if (-not $result) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21809' -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Admin consent request policy not found in database' -Risk 'High' -Name 'Admin consent workflow is enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Application Management' + return + } + + $passed = if ($result.isEnabled) { 'Passed' } else { 'Failed' } + + if ($result.isEnabled) { + $testResultMarkdown = 'Admin consent workflow is enabled.' + } else { + $testResultMarkdown = "Admin consent workflow is disabled.`n`nThe adminConsentRequestPolicy.isEnabled property is set to false." + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21809' -TestType 'Identity' -Status $passed -ResultMarkdown $testResultMarkdown -Risk 'High' -Name 'Admin consent workflow is enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Application Management' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21809' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Admin consent workflow is enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Application Management' + } +} diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21810.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21810.ps1 new file mode 100644 index 000000000000..1196e55465d5 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21810.ps1 @@ -0,0 +1,35 @@ +function Invoke-CippTestZTNA21810 { + param($Tenant) + + try { + $authPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthorizationPolicy' + + if (-not $authPolicy) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21810' -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Authorization policy not found in database' -Risk 'Medium' -Name 'Resource-specific consent is restricted' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Application Management' + return + } + + $teamPermission = 'managepermissiongrantsforownedresource.microsoft-dynamically-managed-permissions-for-team' + $hasTeamPermission = $authPolicy.permissionGrantPolicyIdsAssignedToDefaultUserRole -contains $teamPermission + + if (-not $hasTeamPermission) { + $state = 'DisabledForAllApps' + } else { + $state = 'EnabledForAllApps' + } + + if ($state -eq 'DisabledForAllApps') { + $passed = 'Passed' + $testResultMarkdown = "Resource-Specific Consent is restricted.`n`nThe current state is $state." + } else { + $passed = 'Failed' + $testResultMarkdown = "Resource-Specific Consent is not restricted.`n`nThe current state is $state." + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21810' -TestType 'Identity' -Status $passed -ResultMarkdown $testResultMarkdown -Risk 'Medium' -Name 'Resource-specific consent is restricted' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Application Management' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21810' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'Resource-specific consent is restricted' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Application Management' + } +} diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21811.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21811.ps1 new file mode 100644 index 000000000000..49b281fdec33 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21811.ps1 @@ -0,0 +1,77 @@ +function Invoke-CippTestZTNA21811 { + param($Tenant) + + try { + $domains = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Domains' + + if (-not $domains) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21811' -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Domains not found in database' -Risk 'Medium' -Name 'Password expiration is disabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Credential Management' + return + } + + $misconfiguredDomains = $domains | Where-Object { $_.passwordValidityPeriodInDays -ne '2147483647' } + + $users = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Users' + + $misconfiguredUsers = @() + if ($users) { + $misconfiguredUsers = foreach ($user in $users) { + $userDomain = $user.userPrincipalName.Split('@')[-1] + $domainPolicy = $misconfiguredDomains | Where-Object { $_.id -eq $userDomain } + if (($user.passwordPolicies -notlike '*DisablePasswordExpiration*') -and ($domainPolicy)) { + [PSCustomObject]@{ + id = $user.id + displayName = $user.displayName + userPrincipalName = $user.userPrincipalName + passwordPolicies = $user.passwordPolicies + DomainPasswordValidity = $domainPolicy.passwordValidityPeriodInDays + } + } + } + } + + if ($misconfiguredDomains -or $misconfiguredUsers) { + $passed = 'Failed' + $testResultMarkdown = 'Found domains or users with password expiration still enabled.' + } else { + $passed = 'Passed' + $testResultMarkdown = 'Password expiration is properly disabled across all domains and users.' + } + + if ($misconfiguredDomains) { + $reportTitle1 = 'Domains with password expiration enabled' + $mdInfo1 = "`n## $reportTitle1`n`n" + $mdInfo1 += "| Domain Name | Password Validity Interval |`n" + $mdInfo1 += "| :---------- | :------------------------- |`n" + + foreach ($domain in $misconfiguredDomains) { + $mdInfo1 += "| $($domain.id) | $($domain.passwordValidityPeriodInDays) |`n" + } + + $testResultMarkdown = $testResultMarkdown + $mdInfo1 + } + + if ($misconfiguredUsers) { + $reportTitle2 = 'Users with password expiration enabled' + $mdInfo2 = "`n## $reportTitle2`n`n" + $mdInfo2 += "| Display Name | User Principal Name | User Password Expiration setting | Domain Password Expiration setting |`n" + $mdInfo2 += "| :----------- | :------------------ | :------------------------------- | :--------------------------------- |`n" + + foreach ($misconfiguredUser in $misconfiguredUsers) { + $displayName = $misconfiguredUser.displayName + $userPrincipalName = $misconfiguredUser.userPrincipalName + $userPasswordExpiration = $misconfiguredUser.passwordPolicies + $domainPasswordExpiration = $misconfiguredUser.DomainPasswordValidity + $mdInfo2 += "| $displayName | $userPrincipalName | $userPasswordExpiration | $domainPasswordExpiration |`n" + } + + $testResultMarkdown = $testResultMarkdown + $mdInfo2 + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21811' -TestType 'Identity' -Status $passed -ResultMarkdown $testResultMarkdown -Risk 'Medium' -Name 'Password expiration is disabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Credential Management' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21811' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'Password expiration is disabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Credential Management' + } +} diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21812.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21812.ps1 new file mode 100644 index 000000000000..428f34f0ec40 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21812.ps1 @@ -0,0 +1,61 @@ +function Invoke-CippTestZTNA21812 { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string]$Tenant + ) + + $TestId = 'ZTNA21812' + + try { + $AllGlobalAdmins = Get-CippDbRoleMembers -TenantFilter $Tenant -RoleTemplateId '62e90394-69f5-4237-9190-012177145e10' + + $GlobalAdmins = @($AllGlobalAdmins | Where-Object { $_.'@odata.type' -in @('#microsoft.graph.user', '#microsoft.graph.servicePrincipal') }) + + $Passed = $GlobalAdmins.Count -le 5 + + if ($Passed) { + $ResultMarkdown = "Maximum number of Global Administrators doesn't exceed five users/service principals.`n`n" + } else { + $ResultMarkdown = "Maximum number of Global Administrators exceeds five users/service principals.`n`n" + } + + if ($GlobalAdmins.Count -gt 0) { + $ResultMarkdown += "## Global Administrators`n`n" + $ResultMarkdown += "### Total number of Global Administrators: $($GlobalAdmins.Count)`n`n" + $ResultMarkdown += "| Display Name | Object Type | User Principal Name |`n" + $ResultMarkdown += "| :----------- | :---------- | :------------------ |`n" + + foreach ($GlobalAdmin in $GlobalAdmins) { + $DisplayName = $GlobalAdmin.displayName + $ObjectType = switch ($GlobalAdmin.'@odata.type') { + '#microsoft.graph.user' { 'User' } + '#microsoft.graph.servicePrincipal' { 'Service Principal' } + default { 'Unknown' } + } + $UserPrincipalName = if ($GlobalAdmin.userPrincipalName) { $GlobalAdmin.userPrincipalName } else { 'N/A' } + + $PortalLink = switch ($ObjectType) { + 'User' { "https://entra.microsoft.com/#view/Microsoft_AAD_UsersAndTenants/UserProfileMenuBlade/~/AdministrativeRole/userId/$($GlobalAdmin.id)" } + 'Service Principal' { "https://entra.microsoft.com/#view/Microsoft_AAD_RegisteredApps/ApplicationMenuBlade/~/Overview/appId/$($GlobalAdmin.id)" } + default { 'https://entra.microsoft.com' } + } + + $ResultMarkdown += "| [$DisplayName]($PortalLink) | $ObjectType | $UserPrincipalName |`n" + } + } + + return @{ + TestId = $TestId + Status = if ($Passed) { 'Passed' } else { 'Failed' } + ResultMarkdown = $ResultMarkdown + } + + } catch { + return @{ + TestId = $TestId + Status = 'Failed' + ResultMarkdown = "Error running test: $($_.Exception.Message)" + } + } +} diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21813.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21813.ps1 new file mode 100644 index 000000000000..6f764584c34f --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21813.ps1 @@ -0,0 +1,152 @@ +function Invoke-CippTestZTNA21813 { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string]$Tenant + ) + + $TestId = 'ZTNA21813' + + try { + $GlobalAdminRoleId = '62e90394-69f5-4237-9190-012177145e10' + + $PrivilegedRoles = Get-CippDbRole -TenantFilter $Tenant -IncludePrivilegedRoles + $RoleAssignmentScheduleInstances = New-CIPPDbRequest -TenantFilter $Tenant -Type 'RoleAssignmentScheduleInstances' + $RoleEligibilitySchedules = New-CIPPDbRequest -TenantFilter $Tenant -Type 'RoleEligibilitySchedules' + $Users = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Users' + + $AllGAUsers = @{} + $AllPrivilegedUsers = @{} + $UserRoleMap = @{} + + foreach ($Role in $PrivilegedRoles) { + $ActiveAssignments = $RoleAssignmentScheduleInstances | Where-Object { + $_.roleDefinitionId -eq $Role.templateId -and $_.assignmentType -eq 'Assigned' + } + $EligibleAssignments = $RoleEligibilitySchedules | Where-Object { + $_.roleDefinitionId -eq $Role.templateId + } + + $AllAssignments = @($ActiveAssignments) + @($EligibleAssignments) + + foreach ($Assignment in $AllAssignments) { + $User = $Users | Where-Object { $_.id -eq $Assignment.principalId } | Select-Object -First 1 + if (-not $User) { continue } + + $UserId = $User.id + $IsGARole = $Role.templateId -eq $GlobalAdminRoleId + + if ($IsGARole) { + $AllGAUsers[$UserId] = $User + } + + if (-not $IsGARole) { + $AllPrivilegedUsers[$UserId] = $User + } + + if (-not $UserRoleMap.ContainsKey($UserId)) { + $UserRoleMap[$UserId] = @{ + User = $User + Roles = [System.Collections.ArrayList]@() + IsGA = $false + } + } + + if ($Role.displayName -notin $UserRoleMap[$UserId].Roles) { + [void]$UserRoleMap[$UserId].Roles.Add($Role.displayName) + } + + if ($IsGARole) { + $UserRoleMap[$UserId].IsGA = $true + } + } + } + + $GARoleAssignmentCount = $AllGAUsers.Count + $PrivilegedRoleAssignmentCount = $AllPrivilegedUsers.Count + $TotalPrivilegedRoleAssignmentCount = $GARoleAssignmentCount + $PrivilegedRoleAssignmentCount + + if ($TotalPrivilegedRoleAssignmentCount -gt 0) { + $GAPercentage = [math]::Round(($GARoleAssignmentCount / $TotalPrivilegedRoleAssignmentCount) * 100, 2) + $OtherPercentage = [math]::Round(($PrivilegedRoleAssignmentCount / $TotalPrivilegedRoleAssignmentCount) * 100, 2) + } else { + $GAPercentage = 0 + $OtherPercentage = 0 + } + + $HasHealthyRatio = $false + $HasModerateRatio = $false + $HasHighRatio = $false + $CustomStatus = $null + + if ($GAPercentage -lt 30) { + $StatusIndicator = '✅ Passed' + $HasHealthyRatio = $true + } elseif ($GAPercentage -ge 30 -and $GAPercentage -lt 50) { + $StatusIndicator = '⚠️ Investigate' + $HasModerateRatio = $true + } else { + $StatusIndicator = '❌ Failed' + $HasHighRatio = $true + } + + $MdInfo = "`n## Privileged role assignment summary`n`n" + $MdInfo += "**Global administrator role count:** $GARoleAssignmentCount ($GAPercentage%) - $StatusIndicator`n`n" + $MdInfo += "**Other privileged role count:** $PrivilegedRoleAssignmentCount ($OtherPercentage%)`n`n" + + $MdInfo += "## User privileged role assignments`n`n" + $MdInfo += "| User | Global administrator | Other Privileged Role(s) |`n" + $MdInfo += "| :--- | :------------------- | :------ |`n" + + $SortedUsers = $UserRoleMap.Values | Sort-Object @{Expression = { -not $_.IsGA } }, @{Expression = { $_.User.displayName } } + + foreach ($UserEntry in $SortedUsers) { + $User = $UserEntry.User + $IsGA = if ($UserEntry.IsGA) { 'Yes' } else { 'No' } + + $OtherRoles = $UserEntry.Roles | Where-Object { $_ -ne 'Global Administrator' } | Sort-Object + $RolesList = if ($OtherRoles.Count -gt 0) { ($OtherRoles -join ', ') } else { '-' } + + $UserLink = "https://entra.microsoft.com/#view/Microsoft_AAD_UsersAndTenants/UserProfileMenuBlade/~/AdministrativeRole/userId/$($User.id)/hidePreviewBanner~/true" + $MdInfo += "| [$($User.displayName)]($UserLink) | $IsGA | $RolesList |`n" + } + + if ($UserRoleMap.Count -eq 0) { + $MdInfo += "| No privileged users found | - | - |`n" + } + + if ($TotalPrivilegedRoleAssignmentCount -eq 0) { + $Passed = $true + $ResultMarkdown = "No privileged role assignments found in the tenant.$MdInfo" + } elseif ($HasHealthyRatio) { + $Passed = $true + $ResultMarkdown = "Less than 30% of privileged role assignments in the tenant are Global Administrator.$MdInfo" + } elseif ($HasModerateRatio) { + $Passed = $false + $CustomStatus = 'Investigate' + $ResultMarkdown = "Between 30-50% of privileged role assignments in the tenant are Global Administrator.$MdInfo" + } else { + $Passed = $false + $ResultMarkdown = "More than 50% of privileged role assignments in the tenant are Global Administrator.$MdInfo" + } + + $Result = @{ + TestId = $TestId + Status = if ($Passed) { 'Passed' } else { 'Failed' } + ResultMarkdown = $ResultMarkdown + } + + if ($CustomStatus) { + $Result.CustomStatus = $CustomStatus + } + + return $Result + + } catch { + return @{ + TestId = $TestId + Status = 'Failed' + ResultMarkdown = "Error running test: $($_.Exception.Message)" + } + } +} diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21814.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21814.ps1 new file mode 100644 index 000000000000..851a7751c2b2 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21814.ps1 @@ -0,0 +1,76 @@ +function Invoke-CippTestZTNA21814 { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string]$Tenant + ) + + $TestId = 'ZTNA21814' + + try { + $PrivilegedRoles = Get-CippDbRole -TenantFilter $Tenant -IncludePrivilegedRoles + $Users = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Users' + + $RoleData = [System.Collections.Generic.List[object]]::new() + + foreach ($Role in $PrivilegedRoles) { + $RoleMembers = Get-CippDbRoleMembers -TenantFilter $Tenant -RoleTemplateId $Role.templateId + $RoleUsers = $RoleMembers | Where-Object { $_.'@odata.type' -eq '#microsoft.graph.user' } + + foreach ($RoleMember in $RoleUsers) { + $UserDetail = $Users | Where-Object { $_.id -eq $RoleMember.id } | Select-Object -First 1 + + if ($UserDetail) { + $RoleData.Add([PSCustomObject]@{ + RoleName = $Role.displayName + UserId = $UserDetail.id + UserDisplayName = $UserDetail.displayName + UserPrincipalName = $UserDetail.userPrincipalName + OnPremisesSyncEnabled = $UserDetail.onPremisesSyncEnabled + }) + } + } + } + + $SyncedUsers = $RoleData | Where-Object { $_.OnPremisesSyncEnabled -eq $true } + $Passed = $SyncedUsers.Count -eq 0 + + if ($Passed) { + $ResultMarkdown = "Validated that standing or eligible privileged accounts are cloud only accounts.`n`n" + } else { + $ResultMarkdown = "This tenant has $($SyncedUsers.Count) privileged users that are synced from on-premise.`n`n" + } + + if ($RoleData.Count -gt 0) { + $ResultMarkdown += "## Privileged Roles`n`n" + $ResultMarkdown += "| Role Name | User | Source | Status |`n" + $ResultMarkdown += "| :--- | :--- | :--- | :---: |`n" + + foreach ($RoleUser in ($RoleData | Sort-Object RoleName, UserDisplayName)) { + if ($RoleUser.OnPremisesSyncEnabled) { + $Type = 'Synced from on-premise' + $Status = '❌' + } else { + $Type = 'Cloud native identity' + $Status = '✅' + } + + $UserLink = "https://entra.microsoft.com/#view/Microsoft_AAD_UsersAndTenants/UserProfileMenuBlade/~/AdministrativeRole/userId/$($RoleUser.UserId)" + $ResultMarkdown += "| $($RoleUser.RoleName) | [$($RoleUser.UserDisplayName)]($UserLink) | $Type | $Status |`n" + } + } + + return @{ + TestId = $TestId + Status = if ($Passed) { 'Passed' } else { 'Failed' } + ResultMarkdown = $ResultMarkdown + } + + } catch { + return @{ + TestId = $TestId + Status = 'Failed' + ResultMarkdown = "Error running test: $($_.Exception.Message)" + } + } +} diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21815.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21815.ps1 new file mode 100644 index 000000000000..e3d994bdd66b --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21815.ps1 @@ -0,0 +1,67 @@ +function Invoke-CippTestZTNA21815 { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string]$Tenant + ) + + $TestId = 'ZTNA21815' + + try { + $PrivilegedRoles = Get-CippDbRole -TenantFilter $Tenant -IncludePrivilegedRoles + $RoleAssignmentScheduleInstances = New-CIPPDbRequest -TenantFilter $Tenant -Type 'RoleAssignmentScheduleInstances' + $Users = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Users' + + $PermanentAssignments = [System.Collections.Generic.List[object]]::new() + + foreach ($Role in $PrivilegedRoles) { + $ActiveAssignments = $RoleAssignmentScheduleInstances | Where-Object { + $_.roleDefinitionId -eq $Role.templateId -and + $_.assignmentType -eq 'Assigned' -and + $null -eq $_.endDateTime + } + + foreach ($Assignment in $ActiveAssignments) { + $User = $Users | Where-Object { $_.id -eq $Assignment.principalId } | Select-Object -First 1 + if (-not $User) { continue } + + $PermanentAssignments.Add([PSCustomObject]@{ + PrincipalDisplayName = $User.displayName + UserPrincipalName = $User.userPrincipalName + PrincipalId = $User.id + RoleDisplayName = $Role.displayName + PrivilegeType = 'Permanent' + }) + } + } + + if ($PermanentAssignments.Count -eq 0) { + $Passed = $true + $ResultMarkdown = 'No privileged users have permanent role assignments.' + } else { + $Passed = $false + $ResultMarkdown = "Privileged users with permanent role assignments were found.`n`n" + $ResultMarkdown += "## Privileged users with permanent role assignments`n`n" + $ResultMarkdown += "| User | UPN | Role Name | Assignment Type |`n" + $ResultMarkdown += "| :--- | :-- | :-------- | :-------------- |`n" + + foreach ($Result in $PermanentAssignments) { + $PortalLink = "https://entra.microsoft.com/#view/Microsoft_AAD_UsersAndTenants/UserProfileMenuBlade/~/AdministrativeRole/userId/$($Result.PrincipalId)/hidePreviewBanner~/true" + $ResultMarkdown += "| [$($Result.PrincipalDisplayName)]($PortalLink) | $($Result.UserPrincipalName) | $($Result.RoleDisplayName) | $($Result.PrivilegeType) |`n" + } + } + + return @{ + TestId = $TestId + Status = if ($Passed) { 'Passed' } else { 'Failed' } + ResultMarkdown = $ResultMarkdown + } + + } catch { + return @{ + TestId = $TestId + Status = 'Failed' + ResultMarkdown = "Error running test: $($_.Exception.Message)" + } + } +} diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21816.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21816.ps1 new file mode 100644 index 000000000000..3b67dab20cf6 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21816.ps1 @@ -0,0 +1,191 @@ +function Invoke-CippTestZTNA21816 { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string]$Tenant + ) + + $TestId = 'ZTNA21816' + + try { + $GlobalAdminRoleId = '62e90394-69f5-4237-9190-012177145e10' + $PermanentGAUserList = [System.Collections.Generic.List[object]]::new() + $PermanentGAGroupList = [System.Collections.Generic.List[object]]::new() + $NonPIMPrivilegedUsers = [System.Collections.Generic.List[object]]::new() + $NonPIMPrivilegedGroups = [System.Collections.Generic.List[object]]::new() + + $PrivilegedRoles = Get-CippDbRole -TenantFilter $Tenant -IncludePrivilegedRoles + $RoleEligibilitySchedules = New-CIPPDbRequest -TenantFilter $Tenant -Type 'RoleEligibilitySchedules' + $RoleAssignmentScheduleInstances = New-CIPPDbRequest -TenantFilter $Tenant -Type 'RoleAssignmentScheduleInstances' + $Users = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Users' + $Groups = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Groups' + + $EligibleGAs = $RoleEligibilitySchedules | Where-Object { $_.roleDefinitionId -eq $GlobalAdminRoleId } + $EligibleGAUsers = 0 + + foreach ($EligibleGA in $EligibleGAs) { + $Principal = $Users | Where-Object { $_.id -eq $EligibleGA.principalId } | Select-Object -First 1 + if ($Principal) { + $EligibleGAUsers++ + } else { + $GroupPrincipal = $Groups | Where-Object { $_.id -eq $EligibleGA.principalId } | Select-Object -First 1 + if ($GroupPrincipal) { + $GroupMembers = $Users | Where-Object { $_.id -in $GroupPrincipal.members } + $EligibleGAUsers = $EligibleGAUsers + $GroupMembers.Count + } + } + } + + foreach ($Role in $PrivilegedRoles) { + if ($Role.templateId -eq $GlobalAdminRoleId) { continue } + + $RoleMembers = Get-CippDbRoleMembers -TenantFilter $Tenant -RoleTemplateId $Role.templateId + + foreach ($Member in $RoleMembers) { + $Assignment = $RoleAssignmentScheduleInstances | Where-Object { + $_.principalId -eq $Member.id -and $_.roleDefinitionId -eq $Role.templateId + } | Select-Object -First 1 + + if (-not $Assignment -or ($Assignment.assignmentType -eq 'Assigned' -and $null -eq $Assignment.endDateTime)) { + $MemberInfo = [PSCustomObject]@{ + displayName = $Member.displayName + userPrincipalName = $Member.userPrincipalName + id = $Member.id + roleTemplateId = $Role.templateId + roleName = $Role.displayName + assignmentType = if ($Assignment) { $Assignment.assignmentType } else { 'Not in PIM' } + } + + if ($Member.'@odata.type' -eq '#microsoft.graph.user') { + $NonPIMPrivilegedUsers.Add($MemberInfo) + } else { + $NonPIMPrivilegedGroups.Add($MemberInfo) + } + } + } + } + + $GAMembers = Get-CippDbRoleMembers -TenantFilter $Tenant -RoleTemplateId $GlobalAdminRoleId + + foreach ($Member in $GAMembers) { + $Assignment = $RoleAssignmentScheduleInstances | Where-Object { + $_.principalId -eq $Member.id -and $_.roleDefinitionId -eq $GlobalAdminRoleId + } | Select-Object -First 1 + + if (-not $Assignment -or ($Assignment.assignmentType -eq 'Assigned' -and $null -eq $Assignment.endDateTime)) { + $MemberInfo = [PSCustomObject]@{ + displayName = $Member.displayName + userPrincipalName = $Member.userPrincipalName + id = $Member.id + roleTemplateId = $GlobalAdminRoleId + roleName = 'Global Administrator' + assignmentType = if ($Assignment) { $Assignment.assignmentType } else { 'Not in PIM' } + onPremisesSyncEnabled = $null + } + + if ($Member.'@odata.type' -eq '#microsoft.graph.user') { + $UserDetail = $Users | Where-Object { $_.id -eq $Member.id } | Select-Object -First 1 + if ($UserDetail) { + $MemberInfo.onPremisesSyncEnabled = $UserDetail.onPremisesSyncEnabled + } + $PermanentGAUserList.Add($MemberInfo) + } elseif ($Member.'@odata.type' -eq '#microsoft.graph.group') { + $PermanentGAGroupList.Add($MemberInfo) + + $Group = $Groups | Where-Object { $_.id -eq $Member.id } | Select-Object -First 1 + if ($Group) { + $GroupMembers = $Users | Where-Object { $_.id -in $Group.members } + foreach ($GroupMember in $GroupMembers) { + $GroupMemberInfo = [PSCustomObject]@{ + displayName = $GroupMember.displayName + userPrincipalName = $GroupMember.userPrincipalName + id = $GroupMember.id + roleTemplateId = $GlobalAdminRoleId + roleName = 'Global Administrator (via group)' + assignmentType = 'Via Group' + onPremisesSyncEnabled = $GroupMember.onPremisesSyncEnabled + } + $PermanentGAUserList.Add($GroupMemberInfo) + } + } + } + } + } + + $HasPIMUsage = $EligibleGAUsers -gt 0 + $HasNonPIMPrivileged = ($NonPIMPrivilegedUsers.Count + $NonPIMPrivilegedGroups.Count) -gt 0 + $PermanentGACount = $PermanentGAUserList.Count + $CustomStatus = $null + + if (-not $HasPIMUsage) { + $Passed = $false + $ResultMarkdown = 'No eligible Global Administrator assignments found. PIM usage cannot be confirmed.' + } elseif ($HasNonPIMPrivileged) { + $Passed = $false + $ResultMarkdown = 'Found Microsoft Entra privileged role assignments that are not managed with PIM.' + } elseif ($PermanentGACount -gt 2) { + $Passed = $false + $CustomStatus = 'Investigate' + $ResultMarkdown = 'Three or more accounts are permanently assigned the Global Administrator role. Review to determine whether these are emergency access accounts.' + } else { + $Passed = $true + $ResultMarkdown = 'All Microsoft Entra privileged role assignments are managed with PIM with the exception of up to two standing Global Administrator accounts.' + } + + $ResultMarkdown += "`n`n## Assessment summary`n`n" + $ResultMarkdown += "| Metric | Count |`n" + $ResultMarkdown += "| :----- | :---- |`n" + $ResultMarkdown += "| Privileged roles found | $($PrivilegedRoles.Count) |`n" + $ResultMarkdown += "| Eligible Global Administrators | $EligibleGAUsers |`n" + $ResultMarkdown += "| Non-PIM privileged users | $($NonPIMPrivilegedUsers.Count) |`n" + $ResultMarkdown += "| Non-PIM privileged groups | $($NonPIMPrivilegedGroups.Count) |`n" + $ResultMarkdown += "| Permanent Global Administrator users | $($PermanentGAUserList.Count) |`n" + + if ($NonPIMPrivilegedUsers.Count -gt 0 -or $NonPIMPrivilegedGroups.Count -gt 0) { + $ResultMarkdown += "`n## Non-PIM managed privileged role assignments`n`n" + $ResultMarkdown += "| Display name | User principal name | Role name | Assignment type |`n" + $ResultMarkdown += "| :----------- | :------------------ | :-------- | :-------------- |`n" + + foreach ($User in $NonPIMPrivilegedUsers) { + $UserLink = "https://entra.microsoft.com/#view/Microsoft_AAD_UsersAndTenants/UserProfileMenuBlade/~/AdministrativeRole/userId/$($User.id)/hidePreviewBanner~/true" + $ResultMarkdown += "| [$($User.displayName)]($UserLink) | $($User.userPrincipalName) | $($User.roleName) | $($User.assignmentType) |`n" + } + + foreach ($Group in $NonPIMPrivilegedGroups) { + $GroupLink = "https://entra.microsoft.com/#view/Microsoft_AAD_IAM/GroupDetailsMenuBlade/~/RolesAndAdministrators/groupId/$($Group.id)/menuId/" + $ResultMarkdown += "| [$($Group.displayName)]($GroupLink) | N/A (Group) | $($Group.roleName) | $($Group.assignmentType) |`n" + } + } + + if ($PermanentGAUserList.Count -gt 0) { + $ResultMarkdown += "`n## Permanent Global Administrator assignments`n`n" + $ResultMarkdown += "| Display name | User principal name | Assignment type | On-Premises synced |`n" + $ResultMarkdown += "| :----------- | :------------------ | :-------------- | :----------------- |`n" + + foreach ($User in $PermanentGAUserList) { + $SyncStatus = if ($null -ne $User.onPremisesSyncEnabled) { $User.onPremisesSyncEnabled } else { 'N/A' } + $UserLink = "https://entra.microsoft.com/#view/Microsoft_AAD_UsersAndTenants/UserProfileMenuBlade/~/AdministrativeRole/userId/$($User.id)/hidePreviewBanner~/true" + $ResultMarkdown += "| [$($User.displayName)]($UserLink) | $($User.userPrincipalName) | $($User.assignmentType) | $SyncStatus |`n" + } + } + + $Result = @{ + TestId = $TestId + Status = if ($Passed) { 'Passed' } else { 'Failed' } + ResultMarkdown = $ResultMarkdown + } + + if ($CustomStatus) { + $Result.CustomStatus = $CustomStatus + } + + return $Result + + } catch { + return @{ + TestId = $TestId + Status = 'Failed' + ResultMarkdown = "Error running test: $($_.Exception.Message)" + } + } +} diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21818.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21818.ps1 new file mode 100644 index 000000000000..d4a049ea41dc --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21818.ps1 @@ -0,0 +1,120 @@ +function Invoke-CippTestZTNA21818 { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string]$Tenant + ) + + $TestId = 'ZTNA21818' + + try { + $PrivilegedRoles = Get-CippDbRole -TenantFilter $Tenant -IncludePrivilegedRoles + $RoleManagementPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'RoleManagementPolicies' + + $Notifications = @( + [PSCustomObject]@{ + NotificationScenario = 'Send notifications when members are assigned as eligible to this role' + NotificationType = 'Role assignment alert' + RuleId = 'Notification_Admin_Admin_Eligibility' + } + [PSCustomObject]@{ + NotificationScenario = 'Send notifications when members are assigned as eligible to this role' + NotificationType = 'Notification to the assigned user (assignee)' + RuleId = 'Notification_Requestor_Admin_Eligibility' + } + [PSCustomObject]@{ + NotificationScenario = 'Send notifications when members are assigned as eligible to this role' + NotificationType = 'Request to approve a role assignment renewal/extension' + RuleId = 'Notification_Approver_Admin_Eligibility' + } + [PSCustomObject]@{ + NotificationScenario = 'Send notifications when members are assigned as active to this role' + NotificationType = 'Role assignment alert' + RuleId = 'Notification_Admin_Admin_Assignment' + } + [PSCustomObject]@{ + NotificationScenario = 'Send notifications when members are assigned as active to this role' + NotificationType = 'Notification to the assigned user (assignee)' + RuleId = 'Notification_Requestor_Admin_Assignment' + } + [PSCustomObject]@{ + NotificationScenario = 'Send notifications when members are assigned as active to this role' + NotificationType = 'Request to approve a role assignment renewal/extension' + RuleId = 'Notification_Approver_Admin_Assignment' + } + [PSCustomObject]@{ + NotificationScenario = 'Send notifications when eligible members activate this role' + NotificationType = 'Role activation alert' + RuleId = 'Notification_Admin_EndUser_Assignment' + } + [PSCustomObject]@{ + NotificationScenario = 'Send notifications when eligible members activate this role' + NotificationType = 'Notification to activated user (requestor)' + RuleId = 'Notification_Requestor_EndUser_Assignment' + } + [PSCustomObject]@{ + NotificationScenario = 'Send notifications when eligible members activate this role' + NotificationType = 'Request to approve an activation' + RuleId = 'Notification_Approver_EndUser_Assignment' + } + ) + + $NotificationRules = [System.Collections.Generic.List[object]]::new() + $Passed = $true + $ExitLoop = $false + + foreach ($Role in $PrivilegedRoles) { + $Policy = $RoleManagementPolicies | Where-Object { + $_.scopeId -eq '/' -and $_.scopeType -eq 'DirectoryRole' -and $_.roleDefinitionId -eq $Role.id + } | Select-Object -First 1 + + if (-not $Policy) { continue } + + foreach ($NotificationRuleId in $Notifications.RuleId) { + $Rule = $Policy.rules | Where-Object { $_.id -eq $NotificationRuleId } | Select-Object -First 1 + + if ($Rule) { + $RuleWithRole = $Rule | Select-Object *, @{Name = 'RoleDisplayName'; Expression = { $Role.displayName } } + $NotificationRules.Add($RuleWithRole) + + if ($Rule.isDefaultRecipientsEnabled -eq $true -and ($Rule.notificationRecipients.Count -eq 0 -or $null -eq $Rule.notificationRecipients)) { + $Passed = $false + $ExitLoop = $true + break + } + } + } + + if ($ExitLoop) { break } + } + + if ($Passed) { + $ResultMarkdown = "Role notifications are properly configured for privileged role.`n`n" + } else { + $ResultMarkdown = "Role notifications are not properly configured.`n`nNote: To save time, this check stops when it finds the first role that does not have notifications. After fixing this role and all other roles, we recommend running the check again to verify.`n`n" + } + + $ResultMarkdown += "## Notifications for high privileged roles`n`n" + $ResultMarkdown += "| Role Name | Notification Scenario | Notification Type | Default Recipients Enabled | Additional Recipients |`n" + $ResultMarkdown += "| :-------- | :-------------------- | :---------------- | :------------------------- | :-------------------- |`n" + + foreach ($NotificationRule in $NotificationRules) { + $MatchingNotification = $Notifications | Where-Object { $_.RuleId -eq $NotificationRule.id } + $Recipients = if ($NotificationRule.notificationRecipients) { ($NotificationRule.notificationRecipients -join ', ') } else { '' } + $ResultMarkdown += "| $($NotificationRule.roleDisplayName) | $($MatchingNotification.notificationScenario) | $($MatchingNotification.notificationType) | $($NotificationRule.isDefaultRecipientsEnabled) | $Recipients |`n" + } + + return @{ + TestId = $TestId + Status = if ($Passed) { 'Passed' } else { 'Failed' } + ResultMarkdown = $ResultMarkdown + } + + } catch { + return @{ + TestId = $TestId + Status = 'Failed' + ResultMarkdown = "Error running test: $($_.Exception.Message)" + } + } +} diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21824.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21824.ps1 new file mode 100644 index 000000000000..67f4e31b0a5c --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21824.ps1 @@ -0,0 +1,82 @@ +function Invoke-CippTestZTNA21824 { + param($Tenant) + + try { + $allCAPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ConditionalAccessPolicies' + + if (-not $allCAPolicies) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21824' -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Conditional Access policies not found in database' -Risk 'Medium' -Name "Guests don't have long lived sign-in sessions" -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Conditional Access' + return + } + + $filteredCAPolicies = $allCAPolicies | Where-Object { + ($null -ne $_.conditions.users.includeGuestsOrExternalUsers) -and + ($_.state -in @('enabled', 'enabledForReportingButNotEnforced')) -and + ($null -eq $_.grantControls.termsOfUse -or $_.grantControls.termsOfUse.Count -eq 0) + } + + $matchedPolicies = $filteredCAPolicies | Where-Object { + $signInFrequency = $_.sessionControls.signInFrequency + if ($signInFrequency -and $signInFrequency.isEnabled) { + ($signInFrequency.type -eq 'hours' -and $signInFrequency.value -le 24) -or + ($signInFrequency.type -eq 'days' -and $signInFrequency.value -eq 1) -or + ($null -eq $signInFrequency.type -and $signInFrequency.frequencyInterval -eq 'everyTime') + } else { + $false + } + } + + $passed = if ($filteredCAPolicies.Count -eq $matchedPolicies.Count) { 'Passed' } else { 'Failed' } + + if ($passed -eq 'Passed') { + $testResultMarkdown = "Guests don't have long lived sign-in sessions." + } else { + $testResultMarkdown = 'Guests do have long lived sign-in sessions.' + } + + $reportTitle = 'Sign-in frequency policies' + + if ($filteredCAPolicies -and $filteredCAPolicies.Count -gt 0) { + $mdInfo = "`n## $reportTitle`n`n" + $mdInfo += "| Policy Name | Sign-in Frequency | Status |`n" + $mdInfo += "| :---------- | :---------------- | :----- |`n" + + foreach ($filteredCAPolicy in $filteredCAPolicies) { + $policyName = $filteredCAPolicy.DisplayName + + $signInFrequency = $filteredCAPolicy.sessionControls.signInFrequency + switch ($signInFrequency.type) { + 'hours' { + $signInFreqValue = "$($signInFrequency.value) hours" + } + 'days' { + $signInFreqValue = "$($signInFrequency.value) days" + } + default { + if ($signInFrequency.frequencyInterval -eq 'everyTime') { + $signInFreqValue = 'Every time' + } else { + $signInFreqValue = 'Not configured' + } + } + } + + $status = if ($matchedPolicies -and $matchedPolicies.Id -contains $filteredCAPolicy.Id) { + '✅' + } else { + '❌' + } + + $mdInfo += "| $policyName | $signInFreqValue | $status |`n" + } + + $testResultMarkdown = $testResultMarkdown + $mdInfo + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21824' -TestType 'Identity' -Status $passed -ResultMarkdown $testResultMarkdown -Risk 'Medium' -Name "Guests don't have long lived sign-in sessions" -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Conditional Access' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21824' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name "Guests don't have long lived sign-in sessions" -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Conditional Access' + } +} diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21828.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21828.ps1 new file mode 100644 index 000000000000..f92971871ceb --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21828.ps1 @@ -0,0 +1,52 @@ +function Invoke-CippTestZTNA21828 { + param($Tenant) + + try { + $allCAPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ConditionalAccessPolicies' + + if (-not $allCAPolicies) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21828' -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Conditional Access policies not found in database' -Risk 'High' -Name 'Authentication transfer is blocked' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Conditional Access' + return + } + + $matchedPolicies = $allCAPolicies | Where-Object { + $_.conditions.authenticationFlows.transferMethods -match 'authenticationTransfer' -and + $_.grantControls.builtInControls -contains 'block' -and + $_.conditions.users.includeUsers -eq 'all' -and + $_.conditions.applications.includeApplications -eq 'all' -and + $_.state -eq 'enabled' + } + + if ($matchedPolicies.Count -gt 0) { + $passed = 'Passed' + $testResultMarkdown = 'Authentication transfer is blocked by Conditional Access Policy(s).' + } else { + $passed = 'Failed' + $testResultMarkdown = 'Authentication transfer is not blocked.' + } + + $reportTitle = 'Conditional Access Policies targeting Authentication Transfer' + + if ($matchedPolicies.Count -gt 0) { + $mdInfo = "`n## $reportTitle`n`n" + $mdInfo += "| Policy Name | Policy ID | State | Created | Modified |`n" + $mdInfo += "| :---------- | :-------- | :---- | :------ | :------- |`n" + + foreach ($policy in $matchedPolicies) { + $created = if ($policy.createdDateTime) { $policy.createdDateTime } else { 'N/A' } + $modified = if ($policy.modifiedDateTime) { $policy.modifiedDateTime } else { 'N/A' } + $mdInfo += "| $($policy.displayName) | $($policy.id) | $($policy.state) | $created | $modified |`n" + } + + $testResultMarkdown = $testResultMarkdown + $mdInfo + } else { + $testResultMarkdown = $testResultMarkdown + "`n`nNo Conditional Access policies targeting authentication transfer." + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21828' -TestType 'Identity' -Status $passed -ResultMarkdown $testResultMarkdown -Risk 'High' -Name 'Authentication transfer is blocked' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Conditional Access' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21828' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Authentication transfer is blocked' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Conditional Access' + } +} diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21849.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21849.ps1 new file mode 100644 index 000000000000..9ec072262ebd --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21849.ps1 @@ -0,0 +1,57 @@ +function Invoke-CippTestZTNA21849 { + param($Tenant) + + try { + $groupSettings = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Settings' + + if (-not $groupSettings) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21849' -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Settings not found in database' -Risk 'Medium' -Name 'Smart lockout duration is set to a minimum of 60' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Credential Management' + return + } + + $passwordRuleSettings = $groupSettings | Where-Object { $_.displayName -eq 'Password Rule Settings' } + + $passed = 'Passed' + $testResultMarkdown = '' + + if ($null -eq $passwordRuleSettings) { + $mdInfo = "`n## Smart Lockout Settings`n`n" + $mdInfo += "| Setting | Value |`n" + $mdInfo += "| :---- | :---- |`n" + $mdInfo += "| Lockout Duration (seconds) | 60 (Default) |`n" + + $testResultMarkdown = "Smart Lockout duration is configured to 60 seconds or higher.$mdInfo" + } else { + $lockoutDurationSetting = $passwordRuleSettings.values | Where-Object { $_.name -eq 'LockoutDurationInSeconds' } + + if ($null -eq $lockoutDurationSetting) { + $mdInfo = "`n## Smart Lockout Settings`n`n" + $mdInfo += "| Setting | Value |`n" + $mdInfo += "| :---- | :---- |`n" + $mdInfo += "| Lockout Duration (seconds) | 60 (Default) |`n" + + $testResultMarkdown = "Smart Lockout duration is configured to 60 seconds or higher.$mdInfo" + } else { + $lockoutDuration = [int]$lockoutDurationSetting.value + + $mdInfo = "`n## Smart Lockout Settings`n`n" + $mdInfo += "| Setting | Value |`n" + $mdInfo += "| :---- | :---- |`n" + $mdInfo += "| Lockout Duration (seconds) | $lockoutDuration |`n" + + if ($lockoutDuration -ge 60) { + $testResultMarkdown = "Smart Lockout duration is configured to 60 seconds or higher.$mdInfo" + } else { + $passed = 'Failed' + $testResultMarkdown = "Smart Lockout duration is configured below 60 seconds.$mdInfo" + } + } + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21849' -TestType 'Identity' -Status $passed -ResultMarkdown $testResultMarkdown -Risk 'Medium' -Name 'Smart lockout duration is set to a minimum of 60' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Credential Management' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21849' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'Smart lockout duration is set to a minimum of 60' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Credential Management' + } +} diff --git a/Test-AllZTNATests.ps1 b/Test-AllZTNATests.ps1 new file mode 100644 index 000000000000..47dd04f60839 --- /dev/null +++ b/Test-AllZTNATests.ps1 @@ -0,0 +1,2 @@ +$Tenant = '7ngn50.onmicrosoft.com' +Get-ChildItem "C:\Github\CIPP-API\Modules\CIPPCore\Public\Tests\Invoke-CippTest*.ps1" | ForEach-Object { . $_.FullName; & $_.BaseName -Tenant $Tenant } From 9c8f1de183042e9ce4d66be8668b426bf8f46876 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Wed, 24 Dec 2025 00:26:41 +0100 Subject: [PATCH 023/503] updates --- .../Public/Tests/Invoke-CippTestZTNA21797.ps1 | 4 ++-- .../Public/Tests/Invoke-CippTestZTNA21804.ps1 | 4 ++-- .../Public/Tests/Invoke-CippTestZTNA21811.ps1 | 12 +++++------ .../Public/Tests/Invoke-CippTestZTNA21812.ps1 | 19 +++++++---------- .../Public/Tests/Invoke-CippTestZTNA21813.ps1 | 21 +++++-------------- .../Public/Tests/Invoke-CippTestZTNA21814.ps1 | 15 +++++-------- .../Public/Tests/Invoke-CippTestZTNA21815.ps1 | 21 +++++++------------ .../Public/Tests/Invoke-CippTestZTNA21816.ps1 | 21 +++++-------------- .../Public/Tests/Invoke-CippTestZTNA21818.ps1 | 15 +++++-------- 9 files changed, 45 insertions(+), 87 deletions(-) diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21797.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21797.ps1 index a904e70545b0..9264c119fd39 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21797.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21797.ps1 @@ -135,9 +135,9 @@ function Invoke-CippTestZTNA21797 { $testResultMarkdown = $testResultMarkdown + $mdInfo - $passed = $result + $Status = if ($result) { 'Passed' } else { 'Failed' } - Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21797' -TestType 'Identity' -Status $passed -ResultMarkdown $testResultMarkdown -Risk 'High' -Name 'Restrict access to high risk users' -UserImpact 'High' -ImplementationEffort 'Medium' -Category 'Conditional Access' + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21797' -TestType 'Identity' -Status $Status -ResultMarkdown $testResultMarkdown -Risk 'High' -Name 'Restrict access to high risk users' -UserImpact 'High' -ImplementationEffort 'Medium' -Category 'Conditional Access' } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21804.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21804.ps1 index c8b7b68e8ee2..62e0b08832f7 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21804.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21804.ps1 @@ -14,10 +14,10 @@ function Invoke-CippTestZTNA21804 { $testResultMarkdown = '' if ($matchedMethods.state -contains 'enabled') { - $passed = $false + $passed = 'Failed' $testResultMarkdown = 'Found weak authentication methods that are still enabled.' } else { - $passed = $true + $passed = 'Passed' $testResultMarkdown = 'SMS and voice calls authentication methods are disabled in the tenant.' } diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21811.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21811.ps1 index 49b281fdec33..ad2528521029 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21811.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21811.ps1 @@ -9,7 +9,7 @@ function Invoke-CippTestZTNA21811 { return } - $misconfiguredDomains = $domains | Where-Object { $_.passwordValidityPeriodInDays -ne '2147483647' } + $misconfiguredDomains = $domains | Where-Object { $_.passwordValidityPeriodInDays -ne 2147483647 } $users = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Users' @@ -20,11 +20,11 @@ function Invoke-CippTestZTNA21811 { $domainPolicy = $misconfiguredDomains | Where-Object { $_.id -eq $userDomain } if (($user.passwordPolicies -notlike '*DisablePasswordExpiration*') -and ($domainPolicy)) { [PSCustomObject]@{ - id = $user.id - displayName = $user.displayName - userPrincipalName = $user.userPrincipalName - passwordPolicies = $user.passwordPolicies - DomainPasswordValidity = $domainPolicy.passwordValidityPeriodInDays + id = $user.id + displayName = $user.displayName + userPrincipalName = $user.userPrincipalName + passwordPolicies = $user.passwordPolicies + DomainPasswordValidity = $domainPolicy.passwordValidityPeriodInDays } } } diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21812.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21812.ps1 index 428f34f0ec40..c0e5424c47c9 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21812.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21812.ps1 @@ -9,7 +9,7 @@ function Invoke-CippTestZTNA21812 { try { $AllGlobalAdmins = Get-CippDbRoleMembers -TenantFilter $Tenant -RoleTemplateId '62e90394-69f5-4237-9190-012177145e10' - + $GlobalAdmins = @($AllGlobalAdmins | Where-Object { $_.'@odata.type' -in @('#microsoft.graph.user', '#microsoft.graph.servicePrincipal') }) $Passed = $GlobalAdmins.Count -le 5 @@ -34,7 +34,7 @@ function Invoke-CippTestZTNA21812 { default { 'Unknown' } } $UserPrincipalName = if ($GlobalAdmin.userPrincipalName) { $GlobalAdmin.userPrincipalName } else { 'N/A' } - + $PortalLink = switch ($ObjectType) { 'User' { "https://entra.microsoft.com/#view/Microsoft_AAD_UsersAndTenants/UserProfileMenuBlade/~/AdministrativeRole/userId/$($GlobalAdmin.id)" } 'Service Principal' { "https://entra.microsoft.com/#view/Microsoft_AAD_RegisteredApps/ApplicationMenuBlade/~/Overview/appId/$($GlobalAdmin.id)" } @@ -45,17 +45,12 @@ function Invoke-CippTestZTNA21812 { } } - return @{ - TestId = $TestId - Status = if ($Passed) { 'Passed' } else { 'Failed' } - ResultMarkdown = $ResultMarkdown - } + $Status = if ($Passed) { 'Passed' } else { 'Failed' } + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status $Status -ResultMarkdown $ResultMarkdown -Risk 'Low' -Name "Maximum number of Global Administrators doesn't exceed five users" -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Privileged access' } catch { - return @{ - TestId = $TestId - Status = 'Failed' - ResultMarkdown = "Error running test: $($_.Exception.Message)" - } + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Error running test: $($ErrorMessage.NormalizedError)" -Risk 'Low' -Name "Maximum number of Global Administrators doesn't exceed five users" -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Privileged access' } } diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21813.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21813.ps1 index 6f764584c34f..3a3283b8bcea 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21813.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21813.ps1 @@ -130,23 +130,12 @@ function Invoke-CippTestZTNA21813 { $ResultMarkdown = "More than 50% of privileged role assignments in the tenant are Global Administrator.$MdInfo" } - $Result = @{ - TestId = $TestId - Status = if ($Passed) { 'Passed' } else { 'Failed' } - ResultMarkdown = $ResultMarkdown - } - - if ($CustomStatus) { - $Result.CustomStatus = $CustomStatus - } - - return $Result + $Status = if ($Passed) { 'Passed' } else { 'Failed' } + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status $Status -ResultMarkdown $ResultMarkdown -Risk 'High' -Name 'High Global Administrator to privileged user ratio' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Privileged access' } catch { - return @{ - TestId = $TestId - Status = 'Failed' - ResultMarkdown = "Error running test: $($_.Exception.Message)" - } + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Error running test: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'High Global Administrator to privileged user ratio' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Privileged access' } } diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21814.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21814.ps1 index 851a7751c2b2..a50e4c16bc68 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21814.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21814.ps1 @@ -60,17 +60,12 @@ function Invoke-CippTestZTNA21814 { } } - return @{ - TestId = $TestId - Status = if ($Passed) { 'Passed' } else { 'Failed' } - ResultMarkdown = $ResultMarkdown - } + $Status = if ($Passed) { 'Passed' } else { 'Failed' } + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status $Status -ResultMarkdown $ResultMarkdown -Risk 'High' -Name 'Privileged accounts are cloud native identities' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Privileged access' } catch { - return @{ - TestId = $TestId - Status = 'Failed' - ResultMarkdown = "Error running test: $($_.Exception.Message)" - } + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Error running test: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Privileged accounts are cloud native identities' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Privileged access' } } diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21815.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21815.ps1 index e3d994bdd66b..06365613ac9e 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21815.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21815.ps1 @@ -15,9 +15,9 @@ function Invoke-CippTestZTNA21815 { $PermanentAssignments = [System.Collections.Generic.List[object]]::new() foreach ($Role in $PrivilegedRoles) { - $ActiveAssignments = $RoleAssignmentScheduleInstances | Where-Object { - $_.roleDefinitionId -eq $Role.templateId -and - $_.assignmentType -eq 'Assigned' -and + $ActiveAssignments = $RoleAssignmentScheduleInstances | Where-Object { + $_.roleDefinitionId -eq $Role.templateId -and + $_.assignmentType -eq 'Assigned' -and $null -eq $_.endDateTime } @@ -51,17 +51,12 @@ function Invoke-CippTestZTNA21815 { } } - return @{ - TestId = $TestId - Status = if ($Passed) { 'Passed' } else { 'Failed' } - ResultMarkdown = $ResultMarkdown - } + $Status = if ($Passed) { 'Passed' } else { 'Failed' } + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status $Status -ResultMarkdown $ResultMarkdown -Risk 'High' -Name 'All privileged role assignments are activated just in time and not permanently active' -UserImpact 'Low' -ImplementationEffort 'High' -Category 'Privileged access' } catch { - return @{ - TestId = $TestId - Status = 'Failed' - ResultMarkdown = "Error running test: $($_.Exception.Message)" - } + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Error running test: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'All privileged role assignments are activated just in time and not permanently active' -UserImpact 'Low' -ImplementationEffort 'High' -Category 'Privileged access' } } diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21816.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21816.ps1 index 3b67dab20cf6..e190bc18691d 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21816.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21816.ps1 @@ -169,23 +169,12 @@ function Invoke-CippTestZTNA21816 { } } - $Result = @{ - TestId = $TestId - Status = if ($Passed) { 'Passed' } else { 'Failed' } - ResultMarkdown = $ResultMarkdown - } - - if ($CustomStatus) { - $Result.CustomStatus = $CustomStatus - } - - return $Result + $Status = if ($Passed) { 'Passed' } else { 'Failed' } + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status $Status -ResultMarkdown $ResultMarkdown -Risk 'High' -Name 'All Microsoft Entra privileged role assignments are managed with PIM' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Identity' } catch { - return @{ - TestId = $TestId - Status = 'Failed' - ResultMarkdown = "Error running test: $($_.Exception.Message)" - } + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Error running test: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'All Microsoft Entra privileged role assignments are managed with PIM' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Identity' } } diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21818.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21818.ps1 index d4a049ea41dc..839fd0c47c7b 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21818.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21818.ps1 @@ -104,17 +104,12 @@ function Invoke-CippTestZTNA21818 { $ResultMarkdown += "| $($NotificationRule.roleDisplayName) | $($MatchingNotification.notificationScenario) | $($MatchingNotification.notificationType) | $($NotificationRule.isDefaultRecipientsEnabled) | $Recipients |`n" } - return @{ - TestId = $TestId - Status = if ($Passed) { 'Passed' } else { 'Failed' } - ResultMarkdown = $ResultMarkdown - } + $Status = if ($Passed) { 'Passed' } else { 'Failed' } + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status $Status -ResultMarkdown $ResultMarkdown -Risk 'High' -Name 'Privileged role activations have monitoring and alerting configured' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Monitoring' } catch { - return @{ - TestId = $TestId - Status = 'Failed' - ResultMarkdown = "Error running test: $($_.Exception.Message)" - } + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Error running test: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Privileged role activations have monitoring and alerting configured' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Monitoring' } } From 950ec362f72b6afa18029d3160c6e975b64d40a6 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Wed, 24 Dec 2025 00:43:46 +0100 Subject: [PATCH 024/503] More ZTNA tests(untested) --- ExampleReportTemplate.ps1 | 16 ++- .../Public/Tests/Invoke-CippTestZTNA21819.ps1 | 81 ++++++++++++++ .../Public/Tests/Invoke-CippTestZTNA21820.ps1 | 104 ++++++++++++++++++ .../Public/Tests/Invoke-CippTestZTNA21822.ps1 | 64 +++++++++++ .../Public/Tests/Invoke-CippTestZTNA21823.ps1 | 30 +++++ .../Public/Tests/Invoke-CippTestZTNA21825.ps1 | 103 +++++++++++++++++ .../Public/Tests/Invoke-CippTestZTNA21829.ps1 | 38 +++++++ .../Public/Tests/Invoke-CippTestZTNA21830.ps1 | 82 ++++++++++++++ 8 files changed, 513 insertions(+), 5 deletions(-) create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21819.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21820.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21822.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21823.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21825.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21829.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21830.ps1 diff --git a/ExampleReportTemplate.ps1 b/ExampleReportTemplate.ps1 index 3d83e1659795..e1973a4ba742 100644 --- a/ExampleReportTemplate.ps1 +++ b/ExampleReportTemplate.ps1 @@ -1,11 +1,17 @@ $Table = Get-CippTable -tablename 'CippReportTemplates' +# Dynamically discover all ZTNA test files +$TestFiles = Get-ChildItem "C:\Github\CIPP-API\Modules\CIPPCore\Public\Tests\Invoke-CippTestZTNA*.ps1" | Sort-Object Name +$AllTestIds = $TestFiles.BaseName | ForEach-Object { $_ -replace 'Invoke-CippTestZTNA', 'ZTNA' } + +Write-Host "Discovered $($AllTestIds.Count) ZTNA tests" + $Entity = @{ - RowKey = (New-Guid).ToString() - PartitionKey = 'ReportingTemplate' - Tests = [string](@('Test01', 'Test02', 'Test03', 'Test04', 'Test05') | ConvertTo-Json -Compress) - Description = 'This is a test report' - Name = 'Test Report' + RowKey = 'd5d1e123-bce0-482d-971f-be6ed820dd92' + PartitionKey = 'ReportingTemplate' + IdentityTests = [string]($AllTestIds | ConvertTo-Json -Compress) + Description = 'Complete Zero Trust Network Assessment Report' + Name = 'Full ZTNA Report' } Add-CIPPAzDataTableEntity @Table -Entity $Entity -Force diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21819.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21819.ps1 new file mode 100644 index 000000000000..997d64954c69 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21819.ps1 @@ -0,0 +1,81 @@ +function Invoke-CippTestZTNA21819 { + param($Tenant) + + $TestId = 'ZTNA21819' + + try { + # Get Global Administrator role (template ID: 62e90394-69f5-4237-9190-012177145e10) + $Roles = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Roles' + $GlobalAdminRole = $Roles | Where-Object { $_.roleTemplateId -eq '62e90394-69f5-4237-9190-012177145e10' } + + if (-not $GlobalAdminRole) { + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Could not find the Global Administrator role definition.' -Risk 'Low' -Name 'Activation alert for Global Administrator role assignment' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Privileged access' + return + } + + # Get role management policy for Global Admin + $RoleManagementPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'RoleManagementPolicies' + $GlobalAdminPolicy = $RoleManagementPolicies | Where-Object { + $_.scopeId -eq '/' -and $_.scopeType -eq 'DirectoryRole' -and $_.effectiveRules.target.targetObjects.id -contains $GlobalAdminRole.id + } + + $Passed = 'Failed' + $IsDefaultRecipientsEnabled = 'N/A' + $Recipients = 'N/A' + + if ($GlobalAdminPolicy) { + # Find the notification rule for requestor end-user assignment + $NotificationRule = $GlobalAdminPolicy.effectiveRules | Where-Object { + $_.id -like '*Notification_Requestor_EndUser_Assignment*' + } + + if ($NotificationRule) { + $IsDefaultRecipientsEnabled = $NotificationRule.isDefaultRecipientsEnabled + $NotificationRecipients = $NotificationRule.notificationRecipients + + if ($NotificationRecipients) { + $Recipients = ($NotificationRecipients -join ', ') + } + + if ($NotificationRecipients -or $IsDefaultRecipientsEnabled) { + $Passed = 'Passed' + } + } + } + + if ($Passed -eq 'Passed') { + $ResultMarkdown = "Activation alerts are configured for Global Administrator role.`n`n" + } else { + $ResultMarkdown = "Activation alerts are missing or improperly configured for Global Administrator role.`n`n" + } + + $ResultMarkdown += "| Role display name | Default recipients | Additional recipients |`n" + $ResultMarkdown += "| :---------------- | :----------------- | :------------------- |`n" + + $RoleLink = "https://entra.microsoft.com/#view/Microsoft_AAD_IAM/RolesManagementMenuBlade/~/AllRoles" + $DisplayNameLink = "[$($GlobalAdminRole.displayName)]($RoleLink)" + + $DefaultRecipientsStatus = if ($IsDefaultRecipientsEnabled -eq $true) { + '✅ Enabled' + } elseif ($IsDefaultRecipientsEnabled -eq $false) { + '❌ Disabled' + } else { + 'N/A' + } + + $RecipientsDisplay = if ([string]::IsNullOrEmpty($Recipients) -or $Recipients -eq 'N/A') { + '-' + } else { + $Recipients + } + + $ResultMarkdown += "| $DisplayNameLink | $DefaultRecipientsStatus | $RecipientsDisplay |`n" + + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status $Passed -ResultMarkdown $ResultMarkdown -Risk 'Low' -Name 'Activation alert for Global Administrator role assignment' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Privileged access' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Error running test: $($ErrorMessage.NormalizedError)" -Risk 'Low' -Name 'Activation alert for Global Administrator role assignment' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Privileged access' + } +} diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21820.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21820.ps1 new file mode 100644 index 000000000000..f19eb2af726d --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21820.ps1 @@ -0,0 +1,104 @@ +function Invoke-CippTestZTNA21820 { + param($Tenant) + + $TestId = 'ZTNA21820' + + try { + # Get all privileged roles + $PrivilegedRoles = Get-CippDbRole -TenantFilter $Tenant -IncludePrivilegedRoles + + if (-not $PrivilegedRoles -or $PrivilegedRoles.Count -eq 0) { + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'No privileged roles found in database' -Risk 'Low' -Name 'Activation alert for all privileged role assignments' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Privileged access' + return + } + + # Get all role management policies + $RoleManagementPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'RoleManagementPolicies' + + # Build hashtable for quick policy lookup by role ID + $PolicyByRoleId = @{} + foreach ($Policy in $RoleManagementPolicies) { + if ($Policy.scopeId -eq '/' -and $Policy.scopeType -eq 'DirectoryRole') { + foreach ($RoleId in $Policy.effectiveRules.target.targetObjects.id) { + if ($RoleId) { + $PolicyByRoleId[$RoleId] = $Policy + } + } + } + } + + $RolesWithIssues = [System.Collections.Generic.List[object]]::new() + $Passed = 'Passed' + + foreach ($Role in $PrivilegedRoles) { + $Policy = $PolicyByRoleId[$Role.id] + + if (-not $Policy) { + $RolesWithIssues.Add(@{ + Role = $Role + Issue = 'No PIM policy assignment found' + IsDefaultRecipientsEnabled = 'N/A' + NotificationRecipients = 'N/A' + }) + continue + } + + # Find notification rule for requestor end-user assignment + $NotificationRule = $Policy.effectiveRules | Where-Object { + $_.id -like '*Notification_Requestor_EndUser_Assignment*' + } + + if ($NotificationRule) { + $IsDefaultRecipientsEnabled = $NotificationRule.isDefaultRecipientsEnabled + $NotificationRecipients = $NotificationRule.notificationRecipients + + # Check if alert is properly configured + if ($IsDefaultRecipientsEnabled -eq $true -and ((-not $NotificationRecipients) -or $NotificationRecipients.Count -eq 0)) { + $Passed = 'Failed' + $RolesWithIssues.Add(@{ + Role = $Role + IsDefaultRecipientsEnabled = $IsDefaultRecipientsEnabled + NotificationRecipients = 'N/A' + }) + # Exit early on first issue for performance + break + } + } + } + + if ($RolesWithIssues.Count -eq 0) { + $ResultMarkdown = 'Activation alerts are configured for privileged role assignments.' + } else { + $ResultMarkdown = 'Activation alerts are missing or improperly configured for privileged roles.' + } + + if ($RolesWithIssues.Count -gt 0) { + $ResultMarkdown += "`n`n## Roles with missing or misconfigured alerts`n`n" + $ResultMarkdown += "| Role display name | Default recipients | Additional recipients |`n" + $ResultMarkdown += "| :---------------- | :----------------- | :------------------- |`n" + + foreach ($RoleIssue in $RolesWithIssues) { + $Role = $RoleIssue.Role + $RoleLink = 'https://entra.microsoft.com/#view/Microsoft_AAD_IAM/RolesManagementMenuBlade/~/AllRoles' + $DisplayNameLink = "[$($Role.displayName)]($RoleLink)" + + $DefaultRecipientsStatus = if ($RoleIssue.IsDefaultRecipientsEnabled -eq $true) { + 'Enabled' + } else { + 'Disabled' + } + $Recipients = $RoleIssue.NotificationRecipients + + $ResultMarkdown += "| $DisplayNameLink | $DefaultRecipientsStatus | $Recipients |`n" + } + $ResultMarkdown += "`n`n*Not all misconfigured roles may be listed. For performance reasons, this assessment stops at the first detected issue.*`n" + } + + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status $Passed -ResultMarkdown $ResultMarkdown -Risk 'Low' -Name 'Activation alert for all privileged role assignments' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Privileged access' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Error running test: $($ErrorMessage.NormalizedError)" -Risk 'Low' -Name 'Activation alert for all privileged role assignments' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Privileged access' + } +} diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21822.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21822.ps1 new file mode 100644 index 000000000000..9026b2b4af6d --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21822.ps1 @@ -0,0 +1,64 @@ +function Invoke-CippTestZTNA21822 { + param($Tenant) + + $TestId = 'ZTNA21822' + + try { + # Get B2B management policy from legacy policies endpoint + $LegacyPolicies = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/policies' -tenantid $Tenant + $B2BManagementPolicyObject = $LegacyPolicies | Where-Object { $_.Type -eq 'B2BManagementPolicy' } + + $Passed = 'Failed' + $AllowedDomains = @() + $BlockedDomains = @() + + if ($B2BManagementPolicyObject -and $B2BManagementPolicyObject.definition) { + $B2BManagementPolicy = ($B2BManagementPolicyObject.definition | ConvertFrom-Json).B2BManagementPolicy + $AllowedDomains = $B2BManagementPolicy.InvitationsAllowedAndBlockedDomainsPolicy.AllowedDomains + $BlockedDomains = $B2BManagementPolicy.InvitationsAllowedAndBlockedDomainsPolicy.BlockedDomains + + if ($AllowedDomains -and $AllowedDomains.Count -gt 0) { + $Passed = 'Passed' + } + } + + if ($Passed -eq 'Passed') { + $ResultMarkdown = "Guest access is limited to approved tenants.`n" + } else { + $ResultMarkdown = "Guest access is not limited to approved tenants.`n" + } + + $ResultMarkdown += "`n`n## [Collaboration restrictions](https://entra.microsoft.com/#view/Microsoft_AAD_IAM/CompanyRelationshipsMenuBlade/~/Settings/menuId/)`n`n" + $ResultMarkdown += "The tenant is configured to: " + + if ($Passed -eq 'Passed') { + $ResultMarkdown += "**Allow invitations only to the specified domains (most restrictive)** ✅`n" + } else { + if ($BlockedDomains -and $BlockedDomains.Count -gt 0) { + $ResultMarkdown += "**Deny invitations to the specified domains** ❌`n" + } else { + $ResultMarkdown += "**Allow invitations to be sent to any domain (most inclusive)** ❌`n" + } + } + + if (($AllowedDomains -and $AllowedDomains.Count -gt 0) -or ($BlockedDomains -and $BlockedDomains.Count -gt 0)) { + $ResultMarkdown += "| Domain | Status |`n" + $ResultMarkdown += "| :--- | :--- |`n" + + foreach ($Domain in $AllowedDomains) { + $ResultMarkdown += "| $Domain | ✅ Allowed |`n" + } + + foreach ($Domain in $BlockedDomains) { + $ResultMarkdown += "| $Domain | ❌ Blocked |`n" + } + } + + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status $Passed -ResultMarkdown $ResultMarkdown -Risk 'Medium' -Name 'Guest access is limited to approved tenants' -UserImpact 'Medium' -ImplementationEffort 'High' -Category 'Access control' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Error running test: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'Guest access is limited to approved tenants' -UserImpact 'Medium' -ImplementationEffort 'High' -Category 'Access control' + } +} diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21823.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21823.ps1 new file mode 100644 index 000000000000..b6e0e794c346 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21823.ps1 @@ -0,0 +1,30 @@ +function Invoke-CippTestZTNA21823 { + param($Tenant) + + $TestId = 'ZTNA21823' + + try { + # Get authentication flows policy + $AuthFlowPolicy = New-GraphGetRequest -uri 'https://graph.microsoft.com/v1.0/policies/authenticationFlowsPolicy' -tenantid $Tenant + + if (-not $AuthFlowPolicy) { + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Authentication flows policy not found' -Risk 'Medium' -Name 'Guest self-service sign-up via user flow is disabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'External collaboration' + return + } + + $Passed = if ($AuthFlowPolicy.selfServiceSignUp.isEnabled -eq $false) { 'Passed' } else { 'Failed' } + + if ($Passed -eq 'Passed') { + $ResultMarkdown = "[Guest self-service sign up via user flow](https://entra.microsoft.com/#view/Microsoft_AAD_IAM/CompanyRelationshipsMenuBlade/~/Settings/menuId/ExternalIdentitiesGettingStarted) is disabled.`n" + } else { + $ResultMarkdown = "[Guest self-service sign up via user flow](https://entra.microsoft.com/#view/Microsoft_AAD_IAM/CompanyRelationshipsMenuBlade/~/Settings/menuId/ExternalIdentitiesGettingStarted) is enabled.`n" + } + + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status $Passed -ResultMarkdown $ResultMarkdown -Risk 'Medium' -Name 'Guest self-service sign-up via user flow is disabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'External collaboration' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Error running test: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'Guest self-service sign-up via user flow is disabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'External collaboration' + } +} diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21825.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21825.ps1 new file mode 100644 index 000000000000..2e75a2dc7871 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21825.ps1 @@ -0,0 +1,103 @@ +function Invoke-CippTestZTNA21825 { + param($Tenant) + + $TestId = 'ZTNA21825' + + try { + # Get privileged roles + $PrivilegedRoles = Get-CippDbRole -TenantFilter $Tenant -IncludePrivilegedRoles + + if (-not $PrivilegedRoles -or $PrivilegedRoles.Count -eq 0) { + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'No privileged roles found in database' -Risk 'Medium' -Name 'Privileged users have short-lived sign-in sessions' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Access control' + return + } + + # Get Conditional Access policies + $CAPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ConditionalAccessPolicies' + + # Filter to policies targeting roles + $RoleScopedPolicies = $CAPolicies | Where-Object { + $_.conditions.users.includeRoles -and $_.conditions.users.includeRoles.Count -gt 0 + } + + # Recommended: Sign-in frequency should be 4 hours or less for privileged users + $RecommendedMaxHours = 4 + + $ResultMarkdown = "## Privileged User Sign-In Sessions`n`n" + $ResultMarkdown += "**Total Privileged Roles Found:** $($PrivilegedRoles.Count)`n`n" + $ResultMarkdown += "**CA Policies Targeting Roles:** $($RoleScopedPolicies.Count)`n`n" + $ResultMarkdown += "**Recommended Sign In Session Hours:** $RecommendedMaxHours`n`n" + $ResultMarkdown += "### Conditional Access Policies by Privileged Role`n`n" + + $AllRolesCovered = $true + + foreach ($Role in $PrivilegedRoles) { + $ResultMarkdown += "#### $($Role.displayName)`n`n" + + # Get CA policies assigned to this role + $AssignedPolicies = $CAPolicies | Where-Object { $_.conditions.users.includeRoles -contains $Role.id } + $EnabledPolicies = $AssignedPolicies | Where-Object { $_.state -eq 'enabled' } + + if ($EnabledPolicies.Count -gt 0) { + # Check if at least one compliant enabled policy covers this role + $CompliantForRole = $EnabledPolicies | Where-Object { + $_.sessionControls.signInFrequency -and + $_.sessionControls.signInFrequency.type -eq 'hours' -and + $_.sessionControls.signInFrequency.value -le $RecommendedMaxHours + } + + $RoleStatus = if ($CompliantForRole.Count -gt 0) { + '✅ Covered' + } else { + '❌ Not Covered'; $AllRolesCovered = $false + } + $ResultMarkdown += "**Status:** $RoleStatus`n`n" + + $ResultMarkdown += "| Policy Name | Sign-In Frequency | Compliant |`n" + $ResultMarkdown += "| :--- | :--- | :--- |`n" + + foreach ($Policy in $EnabledPolicies) { + $FreqValue = 'Not Configured' + $IsCompliant = '❌' + + if ($Policy.sessionControls.signInFrequency) { + $Freq = $Policy.sessionControls.signInFrequency + $FreqValue = "$($Freq.value) $($Freq.type)" + + if ($Freq.type -eq 'hours' -and $Freq.value -le $RecommendedMaxHours) { + $IsCompliant = '✅' + } elseif ($Freq.type -eq 'hours') { + $IsCompliant = "⚠️ ($($Freq.value)h > $($RecommendedMaxHours)h)" + } else { + $IsCompliant = '❌ (Days not recommended)' + } + } + + $PolicyLink = "https://entra.microsoft.com/#view/Microsoft_AAD_ConditionalAccess/PolicyBlade/policyId/$($Policy.id)" + $ResultMarkdown += "| [$($Policy.displayName)]($PolicyLink) | $FreqValue | $IsCompliant |`n" + } + $ResultMarkdown += "`n" + } else { + $ResultMarkdown += "**Status:** ❌ No CA policies assigned`n`n" + $ResultMarkdown += "*No Conditional Access policies target this privileged role.*`n`n" + $AllRolesCovered = $false + } + } + + $Passed = if ($AllRolesCovered -and $PrivilegedRoles.Count -gt 0) { 'Passed' } else { 'Failed' } + + if ($Passed -eq 'Passed') { + $ResultMarkdown += "✅ **All privileged roles are covered by enabled policies enforcing short-lived sessions (≤$RecommendedMaxHours hours).**`n" + } else { + $ResultMarkdown += "❌ **Not all privileged roles are covered by compliant sign-in frequency controls.**`n" + $ResultMarkdown += "`n**Recommendation:** Configure Conditional Access policies to enforce sign-in frequency of $RecommendedMaxHours hours or less for ALL privileged roles.`n" + } + + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status $Passed -ResultMarkdown $ResultMarkdown -Risk 'Medium' -Name 'Privileged users have short-lived sign-in sessions' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Access control' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Error running test: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'Privileged users have short-lived sign-in sessions' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Access control' + } +} diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21829.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21829.ps1 new file mode 100644 index 000000000000..85592c9527ac --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21829.ps1 @@ -0,0 +1,38 @@ +function Invoke-CippTestZTNA21829 { + param($Tenant) + + $TestId = 'ZTNA21829' + + try { + # Get domains + $Domains = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Domains' + + if (-not $Domains) { + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Domains not found in database' -Risk 'High' -Name 'Use cloud authentication' -UserImpact 'High' -ImplementationEffort 'High' -Category 'Access control' + return + } + + $FederatedDomains = $Domains | Where-Object { $_.authenticationType -eq 'Federated' } + $Passed = if ($FederatedDomains.Count -eq 0) { 'Passed' } else { 'Failed' } + + if ($Passed -eq 'Passed') { + $ResultMarkdown = "All domains are using cloud authentication.`n`n" + } else { + $ResultMarkdown = "Federated authentication is in use.`n`n" + + $ResultMarkdown += "`n## List of federated domains`n`n" + $ResultMarkdown += "| Domain Name |`n" + $ResultMarkdown += "| :--- |`n" + foreach ($Domain in $FederatedDomains) { + $ResultMarkdown += "| $($Domain.id) |`n" + } + } + + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status $Passed -ResultMarkdown $ResultMarkdown -Risk 'High' -Name 'Use cloud authentication' -UserImpact 'High' -ImplementationEffort 'High' -Category 'Access control' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Error running test: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Use cloud authentication' -UserImpact 'High' -ImplementationEffort 'High' -Category 'Access control' + } +} diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21830.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21830.ps1 new file mode 100644 index 000000000000..7df6806fe317 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21830.ps1 @@ -0,0 +1,82 @@ +function Invoke-CippTestZTNA21830 { + param($Tenant) + + $TestId = 'ZTNA21830' + + try { + # Get Conditional Access policies + $CAPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ConditionalAccessPolicies' + $EnabledCAPolicies = $CAPolicies | Where-Object { $_.state -eq 'enabled' } + + # Get privileged roles + $PrivilegedRoles = Get-CippDbRole -TenantFilter $Tenant -IncludePrivilegedRoles + + if (-not $PrivilegedRoles) { + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'No privileged roles found in database' -Risk 'High' -Name 'Conditional Access policies for Privileged Access Workstations are configured' -UserImpact 'Low' -ImplementationEffort 'High' -Category 'Application management' + return + } + + $CompliantDevicePolicies = [System.Collections.Generic.List[object]]::new() + $DeviceFilterPolicies = [System.Collections.Generic.List[object]]::new() + + foreach ($Policy in $EnabledCAPolicies) { + # Check if policy targets privileged roles + $TargetsPrivilegedRoles = $false + if ($Policy.conditions.users.includeRoles) { + foreach ($RoleId in $Policy.conditions.users.includeRoles) { + if ($PrivilegedRoles.id -contains $RoleId) { + $TargetsPrivilegedRoles = $true + break + } + } + } + + if ($TargetsPrivilegedRoles) { + # Check for compliant device control + if ($Policy.grantControls.builtInControls -contains 'compliantDevice') { + $CompliantDevicePolicies.Add($Policy) + } + + # Check for device filter exclude + block + $HasDeviceFilterExclude = $Policy.conditions.devices.deviceFilter -and + $Policy.conditions.devices.deviceFilter.mode -eq 'exclude' + $BlocksAccess = (-not $Policy.grantControls.builtInControls) -or + ($Policy.grantControls.builtInControls -contains 'block') + + if ($HasDeviceFilterExclude -and $BlocksAccess) { + $DeviceFilterPolicies.Add($Policy) + } + } + } + + $Passed = if ($CompliantDevicePolicies.Count -eq 0 -or $DeviceFilterPolicies.Count -eq 0) { 'Failed' } else { 'Passed' } + + if ($Passed -eq 'Passed') { + $ResultMarkdown = 'Conditional Access policies restrict privileged role access to PAW devices.' + } else { + $ResultMarkdown = 'No Conditional Access policies found that restrict privileged roles to PAW device.' + } + + $CompliantDeviceMarkdown = if ($CompliantDevicePolicies.Count -gt 0) { '✅' } else { '❌' } + $DeviceFilterMarkdown = if ($DeviceFilterPolicies.Count -gt 0) { '✅' } else { '❌' } + + $ResultMarkdown += "`n`n**$CompliantDeviceMarkdown Found $($CompliantDevicePolicies.Count) policy(s) with compliant device control targeting all privileged roles**`n" + foreach ($Policy in $CompliantDevicePolicies) { + $PortalLink = "https://entra.microsoft.com/#view/Microsoft_AAD_ConditionalAccess/PolicyBlade/policyId/$($Policy.id)" + $ResultMarkdown += "- **Policy:** [$($Policy.displayName)]($PortalLink)`n" + } + + $ResultMarkdown += "`n`n**$DeviceFilterMarkdown Found $($DeviceFilterPolicies.Count) policy(s) with PAW/SAW device filter targeting all privileged roles**`n" + foreach ($Policy in $DeviceFilterPolicies) { + $PortalLink = "https://entra.microsoft.com/#view/Microsoft_AAD_ConditionalAccess/PolicyBlade/policyId/$($Policy.id)" + $ResultMarkdown += "- **Policy:** [$($Policy.displayName)]($PortalLink)`n" + } + + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status $Passed -ResultMarkdown $ResultMarkdown -Risk 'High' -Name 'Conditional Access policies for Privileged Access Workstations are configured' -UserImpact 'Low' -ImplementationEffort 'High' -Category 'Application management' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Error running test: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Conditional Access policies for Privileged Access Workstations are configured' -UserImpact 'Low' -ImplementationEffort 'High' -Category 'Application management' + } +} From e4c981888de89be953327515db301f4987b276d6 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Wed, 24 Dec 2025 01:05:13 +0100 Subject: [PATCH 025/503] Added Tests --- .../Push-CIPPDBCacheData.ps1 | 8 + ...t-CIPPDBCacheAuthenticationFlowsPolicy.ps1 | 31 +++ .../Set-CIPPDBCacheB2BManagementPolicy.ps1 | 34 +++ .../Public/Tests/Invoke-CippTestZTNA21822.ps1 | 5 +- .../Public/Tests/Invoke-CippTestZTNA21823.ps1 | 4 +- .../Public/Tests/Invoke-CippTestZTNA21835.ps1 | 201 ++++++++++++++++++ .../Public/Tests/Invoke-CippTestZTNA21836.ps1 | 63 ++++++ .../Public/Tests/Invoke-CippTestZTNA21837.ps1 | 42 ++++ .../Public/Tests/Invoke-CippTestZTNA21838.ps1 | 57 +++++ .../Public/Tests/Invoke-CippTestZTNA21839.ps1 | 80 +++++++ .../Public/Tests/Invoke-CippTestZTNA21840.ps1 | 68 ++++++ .../Public/Tests/Invoke-CippTestZTNA21841.ps1 | 49 +++++ .../Public/Tests/Invoke-CippTestZTNA21842.ps1 | 33 +++ .../Public/Tests/Invoke-CippTestZTNA21844.ps1 | 78 +++++++ .../Public/Tests/Invoke-CippTestZTNA21845.ps1 | 69 ++++++ .../Public/Tests/Invoke-CippTestZTNA21846.ps1 | 40 ++++ .../Public/Tests/Invoke-CippTestZTNA21847.ps1 | 34 +++ 17 files changed, 891 insertions(+), 5 deletions(-) create mode 100644 Modules/CIPPCore/Public/Set-CIPPDBCacheAuthenticationFlowsPolicy.ps1 create mode 100644 Modules/CIPPCore/Public/Set-CIPPDBCacheB2BManagementPolicy.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21835.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21836.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21837.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21838.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21839.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21840.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21841.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21842.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21844.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21845.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21846.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21847.ps1 diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 index d120b56eeedc..560d05ab91b1 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 @@ -118,6 +118,14 @@ function Push-CIPPDBCacheData { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "RoleAssignmentScheduleInstances collection failed: $($_.Exception.Message)" -sev Error } + try { Set-CIPPDBCacheB2BManagementPolicy -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "B2BManagementPolicy collection failed: $($_.Exception.Message)" -sev Error + } + + try { Set-CIPPDBCacheAuthenticationFlowsPolicy -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "AuthenticationFlowsPolicy collection failed: $($_.Exception.Message)" -sev Error + } + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Completed database cache collection for tenant' -sev Info } catch { diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheAuthenticationFlowsPolicy.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheAuthenticationFlowsPolicy.ps1 new file mode 100644 index 000000000000..9cd7afb951fb --- /dev/null +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheAuthenticationFlowsPolicy.ps1 @@ -0,0 +1,31 @@ +function Set-CIPPDBCacheAuthenticationFlowsPolicy { + <# + .SYNOPSIS + Caches authentication flows policy for a tenant + + .PARAMETER TenantFilter + The tenant to cache authentication flows policy for + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter + ) + + try { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching authentication flows policy' -sev Info + + $AuthFlowPolicy = New-GraphGetRequest -uri 'https://graph.microsoft.com/v1.0/policies/authenticationFlowsPolicy' -tenantid $TenantFilter + + if ($AuthFlowPolicy) { + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'AuthenticationFlowsPolicy' -Data @($AuthFlowPolicy) + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached authentication flows policy successfully' -sev Info + } + + } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter ` + -message "Failed to cache authentication flows policy: $($_.Exception.Message)" ` + -sev Warning ` + -LogData (Get-CippException -Exception $_) + } +} diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheB2BManagementPolicy.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheB2BManagementPolicy.ps1 new file mode 100644 index 000000000000..d321bad9e705 --- /dev/null +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheB2BManagementPolicy.ps1 @@ -0,0 +1,34 @@ +function Set-CIPPDBCacheB2BManagementPolicy { + <# + .SYNOPSIS + Caches B2B management policy for a tenant + + .PARAMETER TenantFilter + The tenant to cache B2B management policy for + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter + ) + + try { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching B2B management policy' -sev Info + + $LegacyPolicies = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/policies' -tenantid $TenantFilter + $B2BManagementPolicy = $LegacyPolicies | Where-Object { $_.Type -eq 'B2BManagementPolicy' } + + if ($B2BManagementPolicy) { + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'B2BManagementPolicy' -Data @($B2BManagementPolicy) + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached B2B management policy successfully' -sev Info + } else { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'No B2B management policy found' -sev Info + } + + } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter ` + -message "Failed to cache B2B management policy: $($_.Exception.Message)" ` + -sev Warning ` + -LogData (Get-CippException -Exception $_) + } +} diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21822.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21822.ps1 index 9026b2b4af6d..2970cdc2630e 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21822.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21822.ps1 @@ -4,9 +4,8 @@ function Invoke-CippTestZTNA21822 { $TestId = 'ZTNA21822' try { - # Get B2B management policy from legacy policies endpoint - $LegacyPolicies = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/policies' -tenantid $Tenant - $B2BManagementPolicyObject = $LegacyPolicies | Where-Object { $_.Type -eq 'B2BManagementPolicy' } + # Get B2B management policy from cache + $B2BManagementPolicyObject = New-CIPPDbRequest -TenantFilter $Tenant -Type 'B2BManagementPolicy' $Passed = 'Failed' $AllowedDomains = @() diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21823.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21823.ps1 index b6e0e794c346..fb62b61a9ba4 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21823.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21823.ps1 @@ -4,8 +4,8 @@ function Invoke-CippTestZTNA21823 { $TestId = 'ZTNA21823' try { - # Get authentication flows policy - $AuthFlowPolicy = New-GraphGetRequest -uri 'https://graph.microsoft.com/v1.0/policies/authenticationFlowsPolicy' -tenantid $Tenant + # Get authentication flows policy from cache + $AuthFlowPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationFlowsPolicy' if (-not $AuthFlowPolicy) { Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Authentication flows policy not found' -Risk 'Medium' -Name 'Guest self-service sign-up via user flow is disabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'External collaboration' diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21835.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21835.ps1 new file mode 100644 index 000000000000..5b0ca8756296 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21835.ps1 @@ -0,0 +1,201 @@ +function Invoke-CippTestZTNA21835 { + param($Tenant) + + $TestId = 'ZTNA21835' + + try { + # Get Global Administrator role (template ID: 62e90394-69f5-4237-9190-012177145e10) + $Roles = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Roles' + $GlobalAdminRole = $Roles | Where-Object { $_.roleTemplateId -eq '62e90394-69f5-4237-9190-012177145e10' } + + if (-not $GlobalAdminRole) { + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Global Administrator role not found in database' -Risk 'High' -Name 'Emergency access accounts are configured appropriately' -UserImpact 'Low' -ImplementationEffort 'High' -Category 'Application management' + return + } + + # Get permanent Global Administrator members + $PermanentGAMembers = Get-CippDbRoleMembers -TenantFilter $Tenant -RoleId $GlobalAdminRole.id | Where-Object { + $_.AssignmentType -eq 'Permanent' -and $_.'@odata.type' -eq '#microsoft.graph.user' + } + + # Get Users data to check sync status + $Users = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Users' + + $EmergencyAccountCandidates = [System.Collections.Generic.List[object]]::new() + + foreach ($Member in $PermanentGAMembers) { + $User = $Users | Where-Object { $_.id -eq $Member.principalId } + + # Only process cloud-only accounts + if ($User -and $User.onPremisesSyncEnabled -ne $true) { + # Note: Individual user authentication methods require per-user API calls not available in cache + # Add all cloud-only permanent GAs as candidates (cannot verify auth methods from cache) + $EmergencyAccountCandidates.Add([PSCustomObject]@{ + Id = $User.id + UserPrincipalName = $User.userPrincipalName + DisplayName = $User.displayName + OnPremisesSyncEnabled = $User.onPremisesSyncEnabled + AuthenticationMethods = @('Unknown - requires per-user API call') + CAPoliciesTargeting = 0 + ExcludedFromAllCA = $false + }) + } + } + + # Get CA policies + $CAPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ConditionalAccessPolicies' + $EnabledCAPolicies = $CAPolicies | Where-Object { $_.state -eq 'enabled' } + + $EmergencyAccessAccounts = [System.Collections.Generic.List[object]]::new() + + foreach ($Candidate in $EmergencyAccountCandidates) { + # Note: Transitive group and role memberships require per-user API calls not available in cache + # Simplified check: only verify direct includes/excludes in CA policies + $UserGroupIds = @() + $UserRoles = @() + $UserRoleIds = @() + + $PoliciesTargetingUser = 0 + $ExcludedFromAll = $true + + foreach ($Policy in $EnabledCAPolicies) { + $IsTargeted = $false + + # Check user includes/excludes + $IncludeUsers = @($Policy.conditions.users.includeUsers) + $ExcludeUsers = @($Policy.conditions.users.excludeUsers) + + if ($IncludeUsers -contains 'All' -or $IncludeUsers -contains $Candidate.Id) { + $IsTargeted = $true + } + + if ($ExcludeUsers -contains $Candidate.Id) { + $IsTargeted = $false + } + + # Check group includes/excludes + if (-not $IsTargeted -and $UserGroupIds.Count -gt 0) { + $IncludeGroups = @($Policy.conditions.users.includeGroups) + $ExcludeGroups = @($Policy.conditions.users.excludeGroups) + + foreach ($GroupId in $UserGroupIds) { + if ($IncludeGroups -contains $GroupId) { + $IsTargeted = $true + } + if ($ExcludeGroups -contains $GroupId) { + $IsTargeted = $false + break + } + } + } + + # Check role includes/excludes + $IncludeRoles = @($Policy.conditions.users.includeRoles) + $ExcludeRoles = @($Policy.conditions.users.excludeRoles) + + foreach ($RoleId in $UserRoleIds) { + $Role = $UserRoles | Where-Object { $_.id -eq $RoleId } + if ($Role -and $IncludeRoles -contains $Role.roleTemplateId) { + $IsTargeted = $true + } + if ($Role -and $ExcludeRoles -contains $Role.roleTemplateId) { + $IsTargeted = $false + break + } + } + + if ($IsTargeted) { + $PoliciesTargetingUser++ + $ExcludedFromAll = $false + } + } + + $Candidate.CAPoliciesTargeting = $PoliciesTargetingUser + $Candidate.ExcludedFromAllCA = $ExcludedFromAll + + if ($ExcludedFromAll) { + $EmergencyAccessAccounts.Add($Candidate) + } + } + + $AccountCount = $EmergencyAccessAccounts.Count + $Passed = 'Failed' + $ResultMarkdown = '' + + if ($AccountCount -lt 2) { + $ResultMarkdown = "Fewer than two emergency access accounts were identified based on cloud-only state, registered phishing-resistant credentials and Conditional Access policy exclusions.`n`n" + } elseif ($AccountCount -ge 2 -and $AccountCount -le 4) { + $Passed = 'Passed' + $ResultMarkdown = "Emergency access accounts appear to be configured as per Microsoft guidance based on cloud-only state, registered phishing-resistant credentials and Conditional Access policy exclusions.`n`n" + } else { + $ResultMarkdown = "$AccountCount emergency access accounts appear to be configured based on cloud-only state, registered phishing-resistant credentials and Conditional Access policy exclusions. Review these accounts to determine whether this volume is excessive for your organization.`n`n" + } + + $ResultMarkdown += "**Summary:**`n" + $ResultMarkdown += "- Total permanent Global Administrators: $($PermanentGAMembers.Count)`n" + $ResultMarkdown += "- Cloud-only GAs with phishing-resistant auth: $($EmergencyAccountCandidates.Count)`n" + $ResultMarkdown += "- Emergency access accounts (excluded from all CA): $AccountCount`n" + $ResultMarkdown += "- Enabled Conditional Access policies: $($EnabledCAPolicies.Count)`n`n" + + if ($EmergencyAccessAccounts.Count -gt 0) { + $ResultMarkdown += "## Emergency access accounts`n`n" + $ResultMarkdown += "| Display name | UPN | Synced from on-premises | Authentication methods |`n" + $ResultMarkdown += "| :----------- | :-- | :---------------------- | :--------------------- |`n" + + foreach ($Account in $EmergencyAccessAccounts) { + $SyncStatus = if ($Account.OnPremisesSyncEnabled -ne $true) { 'No' } else { 'Yes' } + $AuthMethodDisplay = ($Account.AuthenticationMethods | ForEach-Object { + $_ -replace '#microsoft.graph.', '' -replace 'AuthenticationMethod', '' + }) -join ', ' + + $PortalLink = "https://entra.microsoft.com/#view/Microsoft_AAD_UsersAndTenants/UserProfileMenuBlade/~/overview/userId/$($Account.Id)" + $ResultMarkdown += "| $($Account.DisplayName) | [$($Account.UserPrincipalName)]($PortalLink) | $SyncStatus | $AuthMethodDisplay |`n" + } + $ResultMarkdown += "`n" + } + + if ($PermanentGAMembers.Count -gt 0) { + $ResultMarkdown += "## All permanent Global Administrators`n`n" + $ResultMarkdown += "| Display name | UPN | Cloud only | All CA excluded | Phishing resistant auth |`n" + $ResultMarkdown += "| :----------- | :-- | :--------: | :---------: | :---------------------: |`n" + + $UserSummary = [System.Collections.Generic.List[object]]::new() + foreach ($Member in $PermanentGAMembers) { + $User = $Users | Where-Object { $_.id -eq $Member.principalId } + if (-not $User) { continue } + + $PortalLink = "https://entra.microsoft.com/#view/Microsoft_AAD_UsersAndTenants/UserProfileMenuBlade/~/overview/userId/$($User.id)" + $IsCloudOnly = ($User.onPremisesSyncEnabled -ne $true) + $CloudOnlyEmoji = if ($IsCloudOnly) { '✅' } else { '❌' } + + $EmergencyAccount = $EmergencyAccessAccounts | Where-Object { $_.Id -eq $User.id } + $CAExcludedEmoji = if ($EmergencyAccount) { '✅' } else { '❌' } + + $Candidate = $EmergencyAccountCandidates | Where-Object { $_.Id -eq $User.id } + $PhishingResistantEmoji = if ($Candidate) { '✅' } else { '❌' } + + $UserSummary.Add([PSCustomObject]@{ + DisplayName = $User.displayName + UserPrincipalName = $User.userPrincipalName + PortalLink = $PortalLink + CloudOnly = $CloudOnlyEmoji + CAExcluded = $CAExcludedEmoji + PhishingResistant = $PhishingResistantEmoji + }) + } + + foreach ($UserSum in $UserSummary) { + $ResultMarkdown += "| $($UserSum.DisplayName) | [$($UserSum.UserPrincipalName)]($($UserSum.PortalLink)) | $($UserSum.CloudOnly) | $($UserSum.CAExcluded) | $($UserSum.PhishingResistant) |`n" + } + + $ResultMarkdown += "`n" + } + + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status $Passed -ResultMarkdown $ResultMarkdown -Risk 'High' -Name 'Emergency access accounts are configured appropriately' -UserImpact 'Low' -ImplementationEffort 'High' -Category 'Application management' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Error running test: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Emergency access accounts are configured appropriately' -UserImpact 'Low' -ImplementationEffort 'High' -Category 'Application management' + } +} diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21836.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21836.ps1 new file mode 100644 index 000000000000..39574bf66a48 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21836.ps1 @@ -0,0 +1,63 @@ +function Invoke-CippTestZTNA21836 { + param($Tenant) + + $TestId = 'ZTNA21836' + + try { + # Get privileged roles + $PrivilegedRoles = Get-CippDbRole -TenantFilter $Tenant -IncludePrivilegedRoles + + if (-not $PrivilegedRoles) { + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'No privileged roles found in database' -Risk 'High' -Name 'Workload Identities are not assigned privileged roles' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Application management' + return + } + + # Get workload identities (service principals) with privileged role assignments + $WorkloadIdentitiesWithPrivilegedRoles = [System.Collections.Generic.List[object]]::new() + + foreach ($Role in $PrivilegedRoles) { + $RoleMembers = Get-CippDbRoleMembers -TenantFilter $Tenant -RoleId $Role.id + + foreach ($Member in $RoleMembers) { + if ($Member.'@odata.type' -eq '#microsoft.graph.servicePrincipal') { + $WorkloadIdentitiesWithPrivilegedRoles.Add([PSCustomObject]@{ + PrincipalId = $Member.principalId + PrincipalDisplayName = $Member.principalDisplayName + AppId = $Member.appId + RoleDisplayName = $Role.displayName + RoleDefinitionId = $Role.id + AssignmentType = $Member.AssignmentType + }) + } + } + } + + $Passed = 'Passed' + $ResultMarkdown = '' + + if ($WorkloadIdentitiesWithPrivilegedRoles.Count -gt 0) { + $Passed = 'Failed' + $ResultMarkdown = "**Found workload identities assigned to privileged roles.**`n" + $ResultMarkdown += "| Service Principal Name | Privileged Role | Assignment Type |`n" + $ResultMarkdown += "| :--- | :--- | :--- |`n" + + $SortedAssignments = $WorkloadIdentitiesWithPrivilegedRoles | Sort-Object -Property PrincipalDisplayName + + foreach ($Assignment in $SortedAssignments) { + $SPLink = "https://entra.microsoft.com/#view/Microsoft_AAD_IAM/ManagedAppMenuBlade/~/Overview/objectId/$($Assignment.PrincipalId)/appId/$($Assignment.AppId)/preferredSingleSignOnMode~/null/servicePrincipalType/Application/fromNav/" + $ResultMarkdown += "| [$($Assignment.PrincipalDisplayName)]($SPLink) | $($Assignment.RoleDisplayName) | $($Assignment.AssignmentType) |`n" + } + $ResultMarkdown += "`n" + $ResultMarkdown += "`n**Recommendation:** Review and remove privileged role assignments from workload identities unless absolutely necessary. Use least-privilege principles and consider alternative approaches like managed identities with specific API permissions instead of directory roles.`n" + } else { + $ResultMarkdown = "✅ **No workload identities found with privileged role assignments.**`n" + } + + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status $Passed -ResultMarkdown $ResultMarkdown -Risk 'High' -Name 'Workload Identities are not assigned privileged roles' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Application management' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Error running test: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Workload Identities are not assigned privileged roles' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Application management' + } +} diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21837.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21837.ps1 new file mode 100644 index 000000000000..e078ed1a10f7 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21837.ps1 @@ -0,0 +1,42 @@ +function Invoke-CippTestZTNA21837 { + param($Tenant) + + $TestId = 'ZTNA21837' + + try { + # Get device registration policy + $DeviceSettings = New-CIPPDbRequest -TenantFilter $Tenant -Type 'DeviceSettings' + + if (-not $DeviceSettings) { + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Device settings not found in database' -Risk 'High' -Name 'Limit the maximum number of devices per user to 10' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Devices' + return + } + + $UserQuota = $DeviceSettings.userDeviceQuota + $EntraDeviceSettingsLink = 'https://entra.microsoft.com/#view/Microsoft_AAD_Devices/DevicesMenuBlade/~/DeviceSettings/menuId/Overview' + + $Passed = 'Failed' + $CustomStatus = $null + + if ($null -eq $UserQuota -or $UserQuota -le 10) { + $Passed = 'Passed' + $ResultMarkdown = "[Maximum number of devices per user]($EntraDeviceSettingsLink) is set to $UserQuota" + } elseif ($UserQuota -gt 10 -and $UserQuota -le 20) { + $CustomStatus = 'Investigate' + $ResultMarkdown = "[Maximum number of devices per user]($EntraDeviceSettingsLink) is set to $UserQuota. Consider reducing to 10 or fewer." + } else { + $ResultMarkdown = "[Maximum number of devices per user]($EntraDeviceSettingsLink) is set to $UserQuota. Consider reducing to 10 or fewer." + } + + if ($CustomStatus) { + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status $CustomStatus -ResultMarkdown $ResultMarkdown -Risk 'High' -Name 'Limit the maximum number of devices per user to 10' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Devices' + } else { + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status $Passed -ResultMarkdown $ResultMarkdown -Risk 'High' -Name 'Limit the maximum number of devices per user to 10' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Devices' + } + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Error running test: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Limit the maximum number of devices per user to 10' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Devices' + } +} diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21838.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21838.ps1 new file mode 100644 index 000000000000..7d70724eb2c0 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21838.ps1 @@ -0,0 +1,57 @@ +function Invoke-CippTestZTNA21838 { + param($Tenant) + + $TestId = 'ZTNA21838' + + try { + # Get FIDO2 authentication method policy + $AuthMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' + + if (-not $AuthMethodsPolicy) { + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Authentication methods policy not found in database' -Risk 'High' -Name 'Security key authentication method enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Access control' + return + } + + $Fido2Config = $AuthMethodsPolicy.authenticationMethodConfigurations | Where-Object { $_.id -eq 'Fido2' } + + if (-not $Fido2Config) { + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'FIDO2 authentication method configuration not found' -Risk 'High' -Name 'Security key authentication method enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Access control' + return + } + + $Fido2Enabled = $Fido2Config.state -eq 'enabled' + $Passed = if ($Fido2Enabled) { 'Passed' } else { 'Failed' } + $StatusEmoji = if ($Fido2Enabled) { '✅' } else { '❌' } + + if ($Fido2Enabled) { + $ResultMarkdown = "Security key authentication method is enabled for your tenant, providing hardware-backed phishing-resistant authentication.`n`n" + } else { + $ResultMarkdown = "Security key authentication method is not enabled; users cannot register FIDO2 security keys for strong authentication.`n`n" + } + + $ResultMarkdown += "## FIDO2 security key authentication settings`n`n" + $ResultMarkdown += "$StatusEmoji **FIDO2 authentication method**`n" + $ResultMarkdown += "- Status: $($Fido2Config.state)`n" + + $IncludeTargetsDisplay = if ($Fido2Config.includeTargets -and $Fido2Config.includeTargets.Count -gt 0) { + ($Fido2Config.includeTargets | ForEach-Object { if ($_.id -eq 'all_users') { 'All users' } else { $_.id } }) -join ', ' + } else { + 'None' + } + $ResultMarkdown += "- Include targets: $IncludeTargetsDisplay`n" + + $ExcludeTargetsDisplay = if ($Fido2Config.excludeTargets -and $Fido2Config.excludeTargets.Count -gt 0) { + ($Fido2Config.excludeTargets | ForEach-Object { $_.id }) -join ', ' + } else { + 'None' + } + $ResultMarkdown += "- Exclude targets: $ExcludeTargetsDisplay`n" + + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status $Passed -ResultMarkdown $ResultMarkdown -Risk 'High' -Name 'Security key authentication method enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Access control' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Error running test: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Security key authentication method enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Access control' + } +} diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21839.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21839.ps1 new file mode 100644 index 000000000000..5eff008f56f8 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21839.ps1 @@ -0,0 +1,80 @@ +function Invoke-CippTestZTNA21839 { + param($Tenant) + + $TestId = 'ZTNA21839' + + try { + # Get FIDO2 authentication method policy + $AuthMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' + + if (-not $AuthMethodsPolicy) { + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Authentication methods policy not found in database' -Risk 'High' -Name 'Passkey authentication method enabled' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Credential management' + return + } + + $Fido2Config = $AuthMethodsPolicy.authenticationMethodConfigurations | Where-Object { $_.id -eq 'Fido2' } + + if (-not $Fido2Config) { + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'FIDO2 authentication method configuration not found' -Risk 'High' -Name 'Passkey authentication method enabled' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Credential management' + return + } + + $State = $Fido2Config.state + $IncludeTargets = $Fido2Config.includeTargets + $IsAttestationEnforced = $Fido2Config.isAttestationEnforced + $KeyRestrictions = $Fido2Config.keyRestrictions + + $Fido2Enabled = $State -eq 'enabled' + $HasIncludeTargets = $IncludeTargets -and $IncludeTargets.Count -gt 0 + + $PortalLink = 'https://entra.microsoft.com/#view/Microsoft_AAD_IAM/AuthenticationMethodsMenuBlade/~/AdminAuthMethods' + + $ResultMarkdown = "`n## [Passkey authentication method details]($PortalLink)`n" + + $StatusDisplay = if ($Fido2Enabled) { 'Enabled ✅' } else { 'Disabled ❌' } + $ResultMarkdown += "- **Status** : $StatusDisplay`n" + + if ($Fido2Enabled) { + $ResultMarkdown += '- **Include targets** : ' + if ($IncludeTargets) { + $TargetsDisplay = ($IncludeTargets | ForEach-Object { + if ($_.id -eq 'all_users') { 'All users' } else { $_.id } + }) -join ', ' + $ResultMarkdown += "$TargetsDisplay`n" + } else { + $ResultMarkdown += "None`n" + } + + $ResultMarkdown += "- **Enforce attestation** : $IsAttestationEnforced`n" + + if ($KeyRestrictions) { + $ResultMarkdown += "- **Key restriction policy** :`n" + if ($null -ne $KeyRestrictions.isEnforced) { + $ResultMarkdown += " - **Enforce key restrictions** : $($KeyRestrictions.isEnforced)`n" + } else { + $ResultMarkdown += " - **Enforce key restrictions** : Not configured`n" + } + if ($KeyRestrictions.enforcementType) { + $ResultMarkdown += " - **Restrict specific keys** : $($KeyRestrictions.enforcementType)`n" + } else { + $ResultMarkdown += " - **Restrict specific keys** : Not configured`n" + } + } + } + + $Passed = if ($Fido2Enabled -and $HasIncludeTargets) { 'Passed' } else { 'Failed' } + + if ($Passed -eq 'Passed') { + $ResultMarkdown = "Passkey authentication method is enabled and configured for users in your tenant.$ResultMarkdown" + } else { + $ResultMarkdown = "Passkey authentication method is not enabled or not configured for any users in your tenant.$ResultMarkdown" + } + + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status $Passed -ResultMarkdown $ResultMarkdown -Risk 'High' -Name 'Passkey authentication method enabled' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Credential management' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Error running test: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Passkey authentication method enabled' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Credential management' + } +} diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21840.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21840.ps1 new file mode 100644 index 000000000000..77a4eaafb722 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21840.ps1 @@ -0,0 +1,68 @@ +function Invoke-CippTestZTNA21840 { + param($Tenant) + + $TestId = 'ZTNA21840' + + try { + # Get FIDO2 authentication method policy + $AuthMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' + + if (-not $AuthMethodsPolicy) { + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Authentication methods policy not found in database' -Risk 'High' -Name 'Security key attestation is enforced' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Credential management' + return + } + + $Fido2Config = $AuthMethodsPolicy.authenticationMethodConfigurations | Where-Object { $_.id -eq 'Fido2' } + + if (-not $Fido2Config) { + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'FIDO2 authentication method configuration not found' -Risk 'High' -Name 'Security key attestation is enforced' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Credential management' + return + } + + $IsAttestationEnforced = $Fido2Config.isAttestationEnforced + $KeyRestrictions = $Fido2Config.keyRestrictions + + $PortalLink = 'https://entra.microsoft.com/#view/Microsoft_AAD_IAM/AuthenticationMethodsMenuBlade/~/AdminAuthMethods' + + $ResultMarkdown = "`n## [Security key attestation policy details]($PortalLink)`n" + + $AttestationStatus = if ($IsAttestationEnforced -eq $true) { 'True ✅' } else { 'False ❌' } + $ResultMarkdown += "- **Enforce attestation** : $AttestationStatus`n" + + if ($KeyRestrictions) { + $ResultMarkdown += "- **Key restriction policy** :`n" + if ($null -ne $KeyRestrictions.isEnforced) { + $ResultMarkdown += " - **Enforce key restrictions** : $($KeyRestrictions.isEnforced)`n" + } else { + $ResultMarkdown += " - **Enforce key restrictions** : Not configured`n" + } + if ($KeyRestrictions.enforcementType) { + $ResultMarkdown += " - **Restrict specific keys** : $($KeyRestrictions.enforcementType)`n" + } else { + $ResultMarkdown += " - **Restrict specific keys** : Not configured`n" + } + + if ($KeyRestrictions.aaGuids -and $KeyRestrictions.aaGuids.Count -gt 0) { + $ResultMarkdown += " - **AAGUID** :`n" + foreach ($Guid in $KeyRestrictions.aaGuids) { + $ResultMarkdown += " - $Guid`n" + } + } + } + + $Passed = if ($IsAttestationEnforced -eq $true) { 'Passed' } else { 'Failed' } + + if ($Passed -eq 'Passed') { + $ResultMarkdown = "Security key attestation is properly enforced, ensuring only verified hardware authenticators can be registered.$ResultMarkdown" + } else { + $ResultMarkdown = "Security key attestation is not enforced, allowing unverified or potentially compromised security keys to be registered.$ResultMarkdown" + } + + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status $Passed -ResultMarkdown $ResultMarkdown -Risk 'High' -Name 'Security key attestation is enforced' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Credential management' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Error running test: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Security key attestation is enforced' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Credential management' + } +} diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21841.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21841.ps1 new file mode 100644 index 000000000000..9c2254add39c --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21841.ps1 @@ -0,0 +1,49 @@ +function Invoke-CippTestZTNA21841 { + param($Tenant) + + $TestId = 'ZTNA21841' + + try { + # Get authentication methods policy + $AuthMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' + + if (-not $AuthMethodsPolicy) { + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Authentication methods policy not found' -Risk 'Medium' -Name 'Microsoft Authenticator app report suspicious activity setting is enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Credential management' + return + } + + $Passed = 'Failed' + $PortalLink = 'https://entra.microsoft.com/#view/Microsoft_AAD_IAM/AuthenticationMethodsMenuBlade/~/AuthMethodsSettings' + + if ($AuthMethodsPolicy.reportSuspiciousActivitySettings) { + $ReportSettings = $AuthMethodsPolicy.reportSuspiciousActivitySettings + + $StateEnabled = $ReportSettings.state -eq 'enabled' + $TargetAllUsers = $false + + if ($ReportSettings.includeTarget) { + $TargetAllUsers = $ReportSettings.includeTarget.id -eq 'all_users' + } + + if ($StateEnabled -and $TargetAllUsers) { + $Passed = 'Passed' + $ResultMarkdown = "Authenticator app report suspicious activity is [enabled for all users]($PortalLink)." + } else { + if (-not $StateEnabled) { + $ResultMarkdown = "Authenticator app report suspicious activity is [not enabled]($PortalLink)." + } elseif (-not $TargetAllUsers) { + $ResultMarkdown = "Authenticator app report suspicious activity is [not configured for all users]($PortalLink)." + } + } + } else { + $ResultMarkdown = "Authenticator app report suspicious activity is [not enabled]($PortalLink)." + } + + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status $Passed -ResultMarkdown $ResultMarkdown -Risk 'Medium' -Name 'Microsoft Authenticator app report suspicious activity setting is enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Credential management' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Error running test: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'Microsoft Authenticator app report suspicious activity setting is enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Credential management' + } +} diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21842.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21842.ps1 new file mode 100644 index 000000000000..10c2d19b2370 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21842.ps1 @@ -0,0 +1,33 @@ +function Invoke-CippTestZTNA21842 { + param($Tenant) + + $TestId = 'ZTNA21842' + + try { + # Get authorization policy + $AuthorizationPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthorizationPolicy' + + if (-not $AuthorizationPolicy) { + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Authorization policy not found in database' -Risk 'High' -Name 'Block administrators from using SSPR' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Credential management' + return + } + + $AllowedToUseSspr = $AuthorizationPolicy.allowedToUseSspr + $Passed = 'Failed' + $UserMessage = '' + + if ($null -ne $AllowedToUseSspr -and $AllowedToUseSspr -eq $false) { + $Passed = 'Passed' + $UserMessage = '✅ Administrators are properly blocked from using Self-Service Password Reset, ensuring password changes go through controlled processes.' + } else { + $UserMessage = '❌ Administrators have access to Self-Service Password Reset, which bypasses security controls and administrative oversight.' + } + + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status $Passed -ResultMarkdown $UserMessage -Risk 'High' -Name 'Block administrators from using SSPR' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Credential management' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Error running test: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Block administrators from using SSPR' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Credential management' + } +} diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21844.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21844.ps1 new file mode 100644 index 000000000000..fd4d0687cf58 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21844.ps1 @@ -0,0 +1,78 @@ +function Invoke-CippTestZTNA21844 { + param($Tenant) + + $TestId = 'ZTNA21844' + + try { + # Azure AD PowerShell App ID + $AzureADPowerShellAppId = '1b730954-1685-4b74-9bfd-dac224a7b894' + + # Query for the Azure AD PowerShell service principal + $ServicePrincipals = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ServicePrincipals' + $ServicePrincipal = $ServicePrincipals | Where-Object { $_.appId -eq $AzureADPowerShellAppId } + + $InvestigateStatus = $false + $AppName = 'Azure AD PowerShell' + $Passed = 'Failed' + + if (-not $ServicePrincipal -or $ServicePrincipal.Count -eq 0) { + $SummaryLines = @( + 'Summary', + '', + "- $AppName (Enterprise App not found in tenant)", + '- Sign in disabled: N/A', + '', + "$AppName has not been blocked by the organization." + ) + } else { + $SP = $ServicePrincipal[0] + $PortalLink = "https://entra.microsoft.com/#view/Microsoft_AAD_IAM/ManagedAppMenuBlade/~/Overview/objectId/$($SP.id)/appId/$($SP.appId)" + $ServicePrincipalMarkdown = "[$AppName]($PortalLink)" + + if ($SP.accountEnabled -eq $false) { + $Passed = 'Passed' + $SummaryLines = @( + 'Summary', + '', + "- $ServicePrincipalMarkdown", + '- Sign in disabled: Yes', + '', + "$AppName is blocked in the tenant by turning off user sign in to the Azure Active Directory PowerShell Enterprise Application." + ) + } elseif ($SP.appRoleAssignmentRequired -eq $true) { + $InvestigateStatus = $true + $SummaryLines = @( + 'Summary', + '', + "- $ServicePrincipalMarkdown", + '- Sign in disabled: No', + '- User assignment required: Yes', + '', + "App role assignment is required for $AppName. Review assignments and confirm that the app is inaccessible to users." + ) + } else { + $SummaryLines = @( + 'Summary', + '', + "- $ServicePrincipalMarkdown", + '- Sign in disabled: No', + '', + "$AppName has not been blocked by the organization." + ) + } + } + + $ResultMarkdown = $SummaryLines -join "`n" + + if ($InvestigateStatus) { + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Investigate' -ResultMarkdown $ResultMarkdown -Risk 'Medium' -Name 'Block legacy Azure AD PowerShell module' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Access control' + } else { + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status $Passed -ResultMarkdown $ResultMarkdown -Risk 'Medium' -Name 'Block legacy Azure AD PowerShell module' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Access control' + } + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Error running test: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'Block legacy Azure AD PowerShell module' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Access control' + } +} diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21845.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21845.ps1 new file mode 100644 index 000000000000..b651eeda2ab0 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21845.ps1 @@ -0,0 +1,69 @@ +function Invoke-CippTestZTNA21845 { + param($Tenant) + + $TestId = 'ZTNA21845' + + try { + # Get Temporary Access Pass configuration + $AuthMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' + $TAPConfig = $AuthMethodsPolicy.authenticationMethodConfigurations | Where-Object { $_.id -eq 'TemporaryAccessPass' } + + if (-not $TAPConfig) { + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Temporary Access Pass configuration not found' -Risk 'Medium' -Name 'Temporary access pass is enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Credential management' + return + } + + # Check if TAP is disabled + if ($TAPConfig.state -ne 'enabled') { + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Failed' -ResultMarkdown '❌ Temporary Access Pass is disabled in the tenant.' -Risk 'Medium' -Name 'Temporary access pass is enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Credential management' + return + } + + # Get conditional access policies + $CAPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ConditionalAccessPolicies' + $SecurityInfoPolicies = $CAPolicies | Where-Object { + $_.state -eq 'enabled' -and + $_.conditions.applications.includeUserActions -contains 'urn:user:registersecurityinfo' -and + $_.grantControls.authenticationStrength -ne $null + } + + $TAPEnabled = $TAPConfig.state -eq 'enabled' + $TargetsAllUsers = $TAPConfig.includeTargets | Where-Object { $_.id -eq 'all_users' } + $HasConditionalAccessEnforcement = $SecurityInfoPolicies.Count -gt 0 + + # Note: Authentication strength policy validation requires additional API calls not available in cache + # Simplified check: verify TAP is enabled, targets all users, and CA policies exist + $TAPSupportedInAuthStrength = $HasConditionalAccessEnforcement + + # Determine pass/fail status + $Passed = 'Failed' + if ($TAPEnabled -and $TargetsAllUsers -and $HasConditionalAccessEnforcement -and $TAPSupportedInAuthStrength) { + $Passed = 'Passed' + $ResultMarkdown = 'Temporary Access Pass is enabled, targeting all users, and enforced with conditional access policies.' + } elseif ($TAPEnabled -and $TargetsAllUsers -and $HasConditionalAccessEnforcement -and -not $TAPSupportedInAuthStrength) { + $ResultMarkdown = "Temporary Access Pass is enabled but authentication strength policies don't include TAP methods." + } elseif ($TAPEnabled -and $TargetsAllUsers -and -not $HasConditionalAccessEnforcement) { + $ResultMarkdown = 'Temporary Access Pass is enabled but no conditional access enforcement for security info registration found. Consider adding conditional access policies for stronger security.' + } else { + $ResultMarkdown = 'Temporary Access Pass is not properly configured or does not target all users.' + } + + $ResultMarkdown += "`n`n**Configuration summary**`n`n" + + $TAPStatus = if ($TAPConfig.state -eq 'enabled') { 'Enabled ✅' } else { 'Disabled ❌' } + $ResultMarkdown += "[Temporary Access Pass](https://entra.microsoft.com/#view/Microsoft_AAD_IAM/AuthenticationMethodsMenuBlade/~/AdminAuthMethods/fromNav/Identity): $TAPStatus`n`n" + + $CAStatus = if ($HasConditionalAccessEnforcement) { 'Enabled ✅' } else { 'Not enabled ❌' } + $ResultMarkdown += "[Conditional Access policy for Security info registration](https://entra.microsoft.com/#view/Microsoft_AAD_ConditionalAccess/ConditionalAccessBlade/~/Policies/fromNav/Identity): $CAStatus`n`n" + + $AuthStrengthStatus = if ($TAPSupportedInAuthStrength) { 'Enabled ✅' } else { 'Not enabled ❌' } + $ResultMarkdown += "[Authentication strength policy for Temporary Access Pass](https://entra.microsoft.com/#view/Microsoft_AAD_ConditionalAccess/AuthenticationStrength.ReactView/fromNav/Identity): $AuthStrengthStatus`n" + + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status $Passed -ResultMarkdown $ResultMarkdown -Risk 'Medium' -Name 'Temporary access pass is enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Credential management' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Error running test: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'Temporary access pass is enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Credential management' + } +} diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21846.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21846.ps1 new file mode 100644 index 000000000000..3f70f6fb77d2 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21846.ps1 @@ -0,0 +1,40 @@ +function Invoke-CippTestZTNA21846 { + param($Tenant) + + $TestId = 'ZTNA21846' + + try { + # Get Temporary Access Pass configuration + $AuthMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' + $TAPConfig = $AuthMethodsPolicy.authenticationMethodConfigurations | Where-Object { $_.id -eq 'TemporaryAccessPass' } + + if (-not $TAPConfig) { + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Temporary Access Pass configuration not found' -Risk 'Medium' -Name 'Restrict Temporary Access Pass to Single Use' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Credential management' + return + } + + $Passed = if ($TAPConfig.isUsableOnce -eq $true) { 'Passed' } else { 'Failed' } + + if ($Passed -eq 'Passed') { + $ResultMarkdown = "Temporary Access Pass is configured for one-time use only.`n`n" + } else { + $ResultMarkdown = "Temporary Access Pass allows multiple uses during validity period.`n`n" + } + + $ResultMarkdown += "## Temporary Access Pass Configuration`n`n" + $ResultMarkdown += "| Setting | Value | Status |`n" + $ResultMarkdown += "| :------ | :---- | :----- |`n" + + $IsUsableOnceValue = if ($TAPConfig.isUsableOnce) { 'Enabled' } else { 'Disabled' } + $StatusEmoji = if ($Passed -eq 'Passed') { '✅ Pass' } else { '❌ Fail' } + + $ResultMarkdown += "| [One-time use restriction](https://entra.microsoft.com/#view/Microsoft_AAD_IAM/AuthenticationMethodsMenuBlade/~/AdminAuthMethods/fromNav/) | $IsUsableOnceValue | $StatusEmoji |`n" + + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status $Passed -ResultMarkdown $ResultMarkdown -Risk 'Medium' -Name 'Restrict Temporary Access Pass to Single Use' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Credential management' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Error running test: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'Restrict Temporary Access Pass to Single Use' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Credential management' + } +} diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21847.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21847.ps1 new file mode 100644 index 000000000000..3b9e7e4cd7d9 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21847.ps1 @@ -0,0 +1,34 @@ +function Invoke-CippTestZTNA21847 { + param($Tenant) + + $TestId = 'ZTNA21847' + + try { + # Check if tenant has on-premises sync + $Settings = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Settings' + + if (-not $Settings) { + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Organization details not found' -Risk 'High' -Name 'Password protection for on-premises is enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Credential management' + return + } + + $Org = $Organization[0] + + if ($Org.onPremisesSyncEnabled -ne $true) { + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Passed' -ResultMarkdown '✅ **Pass**: This tenant is not synchronized to an on-premises environment.' -Risk 'High' -Name 'Password protection for on-premises is enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Credential management' + return + } + + # Note: Password protection settings require groupSettings API which is not cached + # This test requires direct API access to check EnableBannedPasswordCheckOnPremises and BannedPasswordCheckOnPremisesMode + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Password protection settings require API access not available in cache. Manual verification required.' -Risk 'High' -Name 'Password protection for on-premises is enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Credential management' + return + + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status $Passed -ResultMarkdown $ResultMarkdown -Risk 'High' -Name 'Password protection for on-premises is enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Credential management' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Error running test: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Password protection for on-premises is enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Credential management' + } +} From f61e984c99cbd0a26d151e06b54d8f85c4c2c851 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Wed, 24 Dec 2025 01:12:41 +0100 Subject: [PATCH 026/503] Next batch --- .../Push-CIPPDBCacheData.ps1 | 16 ++++ .../Public/Set-CIPPDBCacheRiskDetections.ps1 | 35 ++++++++ .../Set-CIPPDBCacheRiskyServicePrincipals.ps1 | 35 ++++++++ .../Public/Set-CIPPDBCacheRiskyUsers.ps1 | 35 ++++++++ ...PDBCacheServicePrincipalRiskDetections.ps1 | 35 ++++++++ .../Public/Tests/Invoke-CippTestZTNA21848.ps1 | 62 ++++++++++++++ .../Public/Tests/Invoke-CippTestZTNA21849.ps1 | 55 ++++++++++++- .../Public/Tests/Invoke-CippTestZTNA21850.ps1 | 47 +++++++++++ .../Public/Tests/Invoke-CippTestZTNA21861.ps1 | 51 ++++++++++++ .../Public/Tests/Invoke-CippTestZTNA21862.ps1 | 80 +++++++++++++++++++ .../Public/Tests/Invoke-CippTestZTNA21863.ps1 | 48 +++++++++++ 11 files changed, 495 insertions(+), 4 deletions(-) create mode 100644 Modules/CIPPCore/Public/Set-CIPPDBCacheRiskDetections.ps1 create mode 100644 Modules/CIPPCore/Public/Set-CIPPDBCacheRiskyServicePrincipals.ps1 create mode 100644 Modules/CIPPCore/Public/Set-CIPPDBCacheRiskyUsers.ps1 create mode 100644 Modules/CIPPCore/Public/Set-CIPPDBCacheServicePrincipalRiskDetections.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21848.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21850.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21861.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21862.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21863.ps1 diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 index 560d05ab91b1..25b855cd5a3e 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 @@ -126,6 +126,22 @@ function Push-CIPPDBCacheData { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "AuthenticationFlowsPolicy collection failed: $($_.Exception.Message)" -sev Error } + try { Set-CIPPDBCacheRiskyUsers -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "RiskyUsers collection failed: $($_.Exception.Message)" -sev Error + } + + try { Set-CIPPDBCacheRiskyServicePrincipals -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "RiskyServicePrincipals collection failed: $($_.Exception.Message)" -sev Error + } + + try { Set-CIPPDBCacheServicePrincipalRiskDetections -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ServicePrincipalRiskDetections collection failed: $($_.Exception.Message)" -sev Error + } + + try { Set-CIPPDBCacheRiskDetections -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "RiskDetections collection failed: $($_.Exception.Message)" -sev Error + } + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Completed database cache collection for tenant' -sev Info } catch { diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheRiskDetections.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheRiskDetections.ps1 new file mode 100644 index 000000000000..3ba48f9f5e5b --- /dev/null +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheRiskDetections.ps1 @@ -0,0 +1,35 @@ +function Set-CIPPDBCacheRiskDetections { + <# + .SYNOPSIS + Caches risk detections from Identity Protection for a tenant + + .PARAMETER TenantFilter + The tenant to cache risk detections for + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter + ) + + try { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching risk detections from Identity Protection' -sev Info + + # Requires P2 licensing + $RiskDetections = New-GraphGetRequest -uri 'https://graph.microsoft.com/v1.0/identityProtection/riskDetections' -tenantid $TenantFilter + + if ($RiskDetections) { + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'RiskDetections' -Data $RiskDetections + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'RiskDetections' -Data $RiskDetections -Count + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($RiskDetections.Count) risk detections successfully" -sev Info + } else { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'No risk detections found or Identity Protection not available' -sev Info + } + + } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter ` + -message "Failed to cache risk detections: $($_.Exception.Message)" ` + -sev Warning ` + -LogData (Get-CippException -Exception $_) + } +} diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheRiskyServicePrincipals.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheRiskyServicePrincipals.ps1 new file mode 100644 index 000000000000..a2008db65cb3 --- /dev/null +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheRiskyServicePrincipals.ps1 @@ -0,0 +1,35 @@ +function Set-CIPPDBCacheRiskyServicePrincipals { + <# + .SYNOPSIS + Caches risky service principals from Identity Protection for a tenant + + .PARAMETER TenantFilter + The tenant to cache risky service principals for + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter + ) + + try { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching risky service principals from Identity Protection' -sev Info + + # Requires Workload Identity Premium licensing + $RiskyServicePrincipals = New-GraphGetRequest -uri 'https://graph.microsoft.com/v1.0/identityProtection/riskyServicePrincipals' -tenantid $TenantFilter + + if ($RiskyServicePrincipals) { + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'RiskyServicePrincipals' -Data $RiskyServicePrincipals + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'RiskyServicePrincipals' -Data $RiskyServicePrincipals -Count + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($RiskyServicePrincipals.Count) risky service principals successfully" -sev Info + } else { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'No risky service principals found or Workload Identity Protection not available' -sev Info + } + + } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter ` + -message "Failed to cache risky service principals: $($_.Exception.Message)" ` + -sev Warning ` + -LogData (Get-CippException -Exception $_) + } +} diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheRiskyUsers.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheRiskyUsers.ps1 new file mode 100644 index 000000000000..66e94705b1f6 --- /dev/null +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheRiskyUsers.ps1 @@ -0,0 +1,35 @@ +function Set-CIPPDBCacheRiskyUsers { + <# + .SYNOPSIS + Caches risky users from Identity Protection for a tenant + + .PARAMETER TenantFilter + The tenant to cache risky users for + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter + ) + + try { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching risky users from Identity Protection' -sev Info + + # Requires P2 or Governance licensing + $RiskyUsers = New-GraphGetRequest -uri 'https://graph.microsoft.com/v1.0/identityProtection/riskyUsers' -tenantid $TenantFilter + + if ($RiskyUsers) { + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'RiskyUsers' -Data $RiskyUsers + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'RiskyUsers' -Data $RiskyUsers -Count + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($RiskyUsers.Count) risky users successfully" -sev Info + } else { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'No risky users found or Identity Protection not available' -sev Info + } + + } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter ` + -message "Failed to cache risky users: $($_.Exception.Message)" ` + -sev Warning ` + -LogData (Get-CippException -Exception $_) + } +} diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheServicePrincipalRiskDetections.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheServicePrincipalRiskDetections.ps1 new file mode 100644 index 000000000000..7e5e5c0e7ce4 --- /dev/null +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheServicePrincipalRiskDetections.ps1 @@ -0,0 +1,35 @@ +function Set-CIPPDBCacheServicePrincipalRiskDetections { + <# + .SYNOPSIS + Caches service principal risk detections from Identity Protection for a tenant + + .PARAMETER TenantFilter + The tenant to cache service principal risk detections for + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter + ) + + try { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching service principal risk detections from Identity Protection' -sev Info + + # Requires Workload Identity Premium licensing + $ServicePrincipalRiskDetections = New-GraphGetRequest -uri 'https://graph.microsoft.com/v1.0/identityProtection/servicePrincipalRiskDetections' -tenantid $TenantFilter + + if ($ServicePrincipalRiskDetections) { + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ServicePrincipalRiskDetections' -Data $ServicePrincipalRiskDetections + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ServicePrincipalRiskDetections' -Data $ServicePrincipalRiskDetections -Count + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($ServicePrincipalRiskDetections.Count) service principal risk detections successfully" -sev Info + } else { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'No service principal risk detections found or Workload Identity Protection not available' -sev Info + } + + } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter ` + -message "Failed to cache service principal risk detections: $($_.Exception.Message)" ` + -sev Warning ` + -LogData (Get-CippException -Exception $_) + } +} diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21848.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21848.ps1 new file mode 100644 index 000000000000..9202eac17033 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21848.ps1 @@ -0,0 +1,62 @@ +function Invoke-CippTestZTNA21848 { + param($Tenant) + + $TestId = 'ZTNA21848' + + try { + # Get password protection settings from Settings cache + $Settings = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Settings' + $PasswordProtectionSettings = $Settings | Where-Object { $_.templateId -eq '5cf42378-d67d-4f36-ba46-e8b86229381d' } + + if (-not $PasswordProtectionSettings) { + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Password protection settings not found' -Risk 'Medium' -Name 'Add organizational terms to the banned password list' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Credential management' + return + } + + $EnableBannedPasswordCheck = ($PasswordProtectionSettings.values | Where-Object { $_.name -eq 'EnableBannedPasswordCheck' }).value + $BannedPasswordList = ($PasswordProtectionSettings.values | Where-Object { $_.name -eq 'BannedPasswordList' }).value + + if ([string]::IsNullOrEmpty($BannedPasswordList)) { + $BannedPasswordList = $null + } + + $Passed = if ($EnableBannedPasswordCheck -eq $true -and $null -ne $BannedPasswordList) { 'Passed' } else { 'Failed' } + + $PortalLink = 'https://entra.microsoft.com/#view/Microsoft_AAD_IAM/AuthenticationMethodsMenuBlade/~/PasswordProtection/fromNav/' + + $Enforced = if ($EnableBannedPasswordCheck -eq $true) { 'Yes' } else { 'No' } + + # Split on tab characters to handle tab-delimited banned password entries + if ($BannedPasswordList) { + $BannedPasswordArray = $BannedPasswordList -split '\t' + } else { + $BannedPasswordArray = @() + } + + # Show up to 10 banned passwords, summarize if more exist + $MaxDisplay = 10 + if ($BannedPasswordArray.Count -gt $MaxDisplay) { + $DisplayList = $BannedPasswordArray[0..($MaxDisplay-1)] + "...and $($BannedPasswordArray.Count - $MaxDisplay) more" + } else { + $DisplayList = $BannedPasswordArray + } + + if ($Passed -eq 'Passed') { + $ResultMarkdown = "✅ Custom banned passwords are properly configured with organization-specific terms.`n`n" + } else { + $ResultMarkdown = "❌ Custom banned passwords are not enabled or lack organization-specific terms.`n`n" + } + + $ResultMarkdown += "## [Password protection settings]($PortalLink)`n`n" + $ResultMarkdown += "| Enforce custom list | Custom banned password list | Number of terms |`n" + $ResultMarkdown += "| :------------------ | :-------------------------- | :-------------- |`n" + $ResultMarkdown += "| $Enforced | $($DisplayList -join ', ') | $($BannedPasswordArray.Count) |`n" + + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status $Passed -ResultMarkdown $ResultMarkdown -Risk 'Medium' -Name 'Add organizational terms to the banned password list' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Credential management' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Error running test: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'Add organizational terms to the banned password list' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Credential management' + } +} diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21849.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21849.ps1 index 9ec072262ebd..b84e45f7a431 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21849.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21849.ps1 @@ -1,14 +1,61 @@ function Invoke-CippTestZTNA21849 { param($Tenant) + $TestId = 'ZTNA21849' + try { - $groupSettings = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Settings' + # Get password rule settings from Settings cache + $Settings = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Settings' + $PasswordRuleSettings = $Settings | Where-Object { $_.displayName -eq 'Password Rule Settings' } + + $PortalLink = 'https://entra.microsoft.com/#view/Microsoft_AAD_IAM/AuthenticationMethodsMenuBlade/~/PasswordProtection/fromNav/' + + if ($null -eq $PasswordRuleSettings) { + # Default is 60 seconds + $Passed = 'Passed' + $ResultMarkdown = "✅ Smart Lockout duration is configured to 60 seconds or higher (default).`n`n" + $ResultMarkdown += "## [Smart Lockout Settings]($PortalLink)`n`n" + $ResultMarkdown += "| Setting | Value |`n" + $ResultMarkdown += "| :---- | :---- |`n" + $ResultMarkdown += "| Lockout Duration (seconds) | 60 (Default) |`n" + } else { + $LockoutDurationSetting = $PasswordRuleSettings.values | Where-Object { $_.name -eq 'LockoutDurationInSeconds' } + + if ($null -eq $LockoutDurationSetting) { + # Default is 60 seconds + $Passed = 'Passed' + $ResultMarkdown = "✅ Smart Lockout duration is configured to 60 seconds or higher (default).`n`n" + $ResultMarkdown += "## [Smart Lockout Settings]($PortalLink)`n`n" + $ResultMarkdown += "| Setting | Value |`n" + $ResultMarkdown += "| :---- | :---- |`n" + $ResultMarkdown += "| Lockout Duration (seconds) | 60 (Default) |`n" + } else { + $LockoutDuration = [int]$LockoutDurationSetting.value + + if ($LockoutDuration -ge 60) { + $Passed = 'Passed' + $ResultMarkdown = "✅ Smart Lockout duration is configured to 60 seconds or higher.`n`n" + } else { + $Passed = 'Failed' + $ResultMarkdown = "❌ Smart Lockout duration is configured below 60 seconds.`n`n" + } - if (-not $groupSettings) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21849' -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Settings not found in database' -Risk 'Medium' -Name 'Smart lockout duration is set to a minimum of 60' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Credential Management' - return + $ResultMarkdown += "## [Smart Lockout Settings]($PortalLink)`n`n" + $ResultMarkdown += "| Setting | Value |`n" + $ResultMarkdown += "| :---- | :---- |`n" + $ResultMarkdown += "| Lockout Duration (seconds) | $LockoutDuration |`n" + } } + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status $Passed -ResultMarkdown $ResultMarkdown -Risk 'Medium' -Name 'Smart lockout duration is set to a minimum of 60' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Credential management' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Error running test: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'Smart lockout duration is set to a minimum of 60' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Credential management' + } +} + $passwordRuleSettings = $groupSettings | Where-Object { $_.displayName -eq 'Password Rule Settings' } $passed = 'Passed' diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21850.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21850.ps1 new file mode 100644 index 000000000000..c778b0f15afc --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21850.ps1 @@ -0,0 +1,47 @@ +function Invoke-CippTestZTNA21850 { + param($Tenant) + + $TestId = 'ZTNA21850' + + try { + # Get password rule settings from Settings cache + $Settings = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Settings' + $PasswordRuleSettings = $Settings | Where-Object { $_.displayName -eq 'Password Rule Settings' } + + $PortalLink = 'https://entra.microsoft.com/#view/Microsoft_AAD_IAM/AuthenticationMethodsMenuBlade/~/PasswordProtection/fromNav/' + + if ($null -eq $PasswordRuleSettings) { + $Passed = 'Failed' + $ResultMarkdown = "❌ Password rule settings template not found." + } else { + $LockoutThresholdSetting = $PasswordRuleSettings.values | Where-Object { $_.name -eq 'LockoutThreshold' } + + if ($null -eq $LockoutThresholdSetting) { + $Passed = 'Failed' + $ResultMarkdown = "❌ Lockout threshold setting not found in [password rule settings]($PortalLink)." + } else { + $LockoutThreshold = [int]$LockoutThresholdSetting.value + + if ($LockoutThreshold -le 10) { + $Passed = 'Passed' + $ResultMarkdown = "✅ Smart lockout threshold is set to 10 or below.`n`n" + } else { + $Passed = 'Failed' + $ResultMarkdown = "❌ Smart lockout threshold is configured above 10.`n`n" + } + + $ResultMarkdown += "## [Smart lockout configuration]($PortalLink)`n`n" + $ResultMarkdown += "| Setting | Value |`n" + $ResultMarkdown += "| :---- | :---- |`n" + $ResultMarkdown += "| Lockout threshold | $LockoutThreshold attempts |`n" + } + } + + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status $Passed -ResultMarkdown $ResultMarkdown -Risk 'Medium' -Name 'Smart lockout threshold set to 10 or less' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Credential management' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Error running test: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'Smart lockout threshold set to 10 or less' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Credential management' + } +} diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21861.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21861.ps1 new file mode 100644 index 000000000000..ffe99600a8b4 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21861.ps1 @@ -0,0 +1,51 @@ +function Invoke-CippTestZTNA21861 { + param($Tenant) + + $TestId = 'ZTNA21861' + + try { + # Get risky users from cache + $RiskyUsers = New-CIPPDbRequest -TenantFilter $Tenant -Type 'RiskyUsers' + + if (-not $RiskyUsers) { + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Risky users data not found. This may indicate Identity Protection is not available (requires P2 licensing) or no risky users exist.' -Risk 'High' -Name 'All high-risk users are triaged' -UserImpact 'Low' -ImplementationEffort 'High' -Category 'Monitoring' + return + } + + # Filter for untriaged high-risk users (atRisk state with High risk level) + $UntriagedHighRiskUsers = $RiskyUsers | Where-Object { $_.riskState -eq 'atRisk' -and $_.riskLevel -eq 'high' } + + $Passed = if ($UntriagedHighRiskUsers.Count -eq 0) { 'Passed' } else { 'Failed' } + + if ($Passed -eq 'Passed') { + $ResultMarkdown = "✅ All high-risk users are properly triaged in Entra ID Protection." + } else { + $ResultMarkdown = "❌ Found **$($UntriagedHighRiskUsers.Count)** untriaged high-risk users in Entra ID Protection.`n`n" + $ResultMarkdown += "## Untriaged High-Risk Users`n`n" + $ResultMarkdown += "| User | Risk level | Last updated | Risk detail |`n" + $ResultMarkdown += "| :--- | :--- | :--- | :--- |`n" + + foreach ($User in $UntriagedHighRiskUsers) { + $UserPrincipalName = if ($User.userPrincipalName) { $User.userPrincipalName } else { $User.id } + $RiskLevel = switch ($User.riskLevel) { + 'high' { '🔴 High' } + 'medium' { '🟡 Medium' } + 'low' { '🟢 Low' } + default { $User.riskLevel } + } + $RiskDate = $User.riskLastUpdatedDateTime + $RiskDetail = $User.riskDetail + + $PortalLink = "https://entra.microsoft.com/#view/Microsoft_AAD_UsersAndTenants/UserProfileMenuBlade/~/overview/userId/$($User.id)" + $ResultMarkdown += "| [$UserPrincipalName]($PortalLink) | $RiskLevel | $RiskDate | $RiskDetail |`n" + } + } + + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status $Passed -ResultMarkdown $ResultMarkdown -Risk 'High' -Name 'All high-risk users are triaged' -UserImpact 'Low' -ImplementationEffort 'High' -Category 'Monitoring' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Error running test: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'All high-risk users are triaged' -UserImpact 'Low' -ImplementationEffort 'High' -Category 'Monitoring' + } +} diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21862.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21862.ps1 new file mode 100644 index 000000000000..64a2ed5ba140 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21862.ps1 @@ -0,0 +1,80 @@ +function Invoke-CippTestZTNA21862 { + param($Tenant) + + $TestId = 'ZTNA21862' + + try { + # Get risky service principals and risk detections from cache + $UntriagedRiskyPrincipals = New-CIPPDbRequest -TenantFilter $Tenant -Type 'RiskyServicePrincipals' | Where-Object { $_.riskState -eq 'atRisk' } + $ServicePrincipalRiskDetections = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ServicePrincipalRiskDetections' + $UntriagedRiskDetections = $ServicePrincipalRiskDetections | Where-Object { $_.riskState -eq 'atRisk' } + + if (-not $UntriagedRiskyPrincipals -and -not $ServicePrincipalRiskDetections) { + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Risky service principals data not found. This may indicate Workload Identity Protection is not available (requires Workload Identity Premium licensing) or no risky workload identities exist.' -Risk 'High' -Name 'All risky workload identities are triaged' -UserImpact 'Low' -ImplementationEffort 'High' -Category 'Monitoring' + return + } + + $Passed = if (($UntriagedRiskyPrincipals.Count -eq 0) -and ($UntriagedRiskDetections.Count -eq 0)) { 'Passed' } else { 'Failed' } + + if ($Passed -eq 'Passed') { + $ResultMarkdown = '✅ All risky workload identities have been triaged' + } else { + $RiskySPCount = $UntriagedRiskyPrincipals.Count + $RiskyDetectionCount = $UntriagedRiskDetections.Count + $ResultMarkdown = "❌ Found $RiskySPCount untriaged risky service principals and $RiskyDetectionCount untriaged risk detections`n`n" + + if ($RiskySPCount -gt 0) { + $ResultMarkdown += "## Untriaged Risky Service Principals`n`n" + $ResultMarkdown += "| Service Principal | Type | Risk Level | Risk State | Risk Last Updated |`n" + $ResultMarkdown += "| :--- | :--- | :--- | :--- | :--- |`n" + foreach ($SP in $UntriagedRiskyPrincipals) { + $PortalLink = "https://entra.microsoft.com/#view/Microsoft_AAD_IAM/ManagedAppMenuBlade/~/SignOn/objectId/$($SP.id)/appId/$($SP.appId)" + $RiskLevel = switch ($SP.riskLevel) { + 'high' { '🔴 High' } + 'medium' { '🟡 Medium' } + 'low' { '🟢 Low' } + default { $SP.riskLevel } + } + $RiskState = switch ($SP.riskState) { + 'atRisk' { '⚠️ At Risk' } + 'confirmedCompromised' { '🔴 Confirmed Compromised' } + 'dismissed' { '✅ Dismissed' } + 'remediated' { '✅ Remediated' } + default { $SP.riskState } + } + $ResultMarkdown += "| [$($SP.displayName)]($PortalLink) | $($SP.servicePrincipalType) | $RiskLevel | $RiskState | $($SP.riskLastUpdatedDateTime) |`n" + } + } + + if ($RiskyDetectionCount -gt 0) { + $ResultMarkdown += "`n`n## Untriaged Risk Detection Events`n`n" + $ResultMarkdown += "| Service Principal | Risk Level | Risk State | Risk Event Type | Risk Last Updated |`n" + $ResultMarkdown += "| :--- | :--- | :--- | :--- | :--- |`n" + foreach ($Detection in $UntriagedRiskDetections) { + $PortalLink = "https://entra.microsoft.com/#view/Microsoft_AAD_IAM/ManagedAppMenuBlade/~/SignOn/objectId/$($Detection.servicePrincipalId)/appId/$($Detection.appId)" + $RiskLevel = switch ($Detection.riskLevel) { + 'high' { '🔴 High' } + 'medium' { '🟡 Medium' } + 'low' { '🟢 Low' } + default { $Detection.riskLevel } + } + $RiskState = switch ($Detection.riskState) { + 'atRisk' { '⚠️ At Risk' } + 'confirmedCompromised' { '🔴 Confirmed Compromised' } + 'dismissed' { '✅ Dismissed' } + 'remediated' { '✅ Remediated' } + default { $Detection.riskState } + } + $ResultMarkdown += "| [$($Detection.servicePrincipalDisplayName)]($PortalLink) | $RiskLevel | $RiskState | $($Detection.riskEventType) | $($Detection.detectedDateTime) |`n" + } + } + } + + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status $Passed -ResultMarkdown $ResultMarkdown -Risk 'High' -Name 'All risky workload identities are triaged' -UserImpact 'Low' -ImplementationEffort 'High' -Category 'Monitoring' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Error running test: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'All risky workload identities are triaged' -UserImpact 'Low' -ImplementationEffort 'High' -Category 'Monitoring' + } +} diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21863.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21863.ps1 new file mode 100644 index 000000000000..51f98401418e --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21863.ps1 @@ -0,0 +1,48 @@ +function Invoke-CippTestZTNA21863 { + param($Tenant) + + $TestId = 'ZTNA21863' + + try { + # Get risk detections from cache and filter for high-risk untriaged sign-ins + $RiskDetections = New-CIPPDbRequest -TenantFilter $Tenant -Type 'RiskDetections' + + if (-not $RiskDetections) { + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Risk detections data not found. This may indicate Identity Protection is not available (requires P2 licensing) or no risk detections exist.' -Risk 'High' -Name 'All high-risk sign-ins are triaged' -UserImpact 'Low' -ImplementationEffort 'High' -Category 'Monitoring' + return + } + + $UntriagedHighRiskSignIns = $RiskDetections | Where-Object { $_.riskState -eq 'atRisk' -and $_.riskLevel -eq 'high' } + + $Passed = if ($UntriagedHighRiskSignIns.Count -eq 0) { 'Passed' } else { 'Failed' } + + if ($Passed -eq 'Passed') { + $ResultMarkdown = '✅ No untriaged risky sign ins in the tenant.' + } else { + $ResultMarkdown = "❌ Found **$($UntriagedHighRiskSignIns.Count)** untriaged high-risk sign ins.`n`n" + $ResultMarkdown += "## Untriaged High-Risk Sign ins`n`n" + $ResultMarkdown += "| Date | User Principal Name | Type | Risk Level |`n" + $ResultMarkdown += "| :---- | :---- | :---- | :---- |`n" + + foreach ($Risk in $UntriagedHighRiskSignIns) { + $UserPrincipalName = $Risk.userPrincipalName + $RiskLevel = switch ($Risk.riskLevel) { + 'high' { '🔴 High' } + 'medium' { '🟡 Medium' } + 'low' { '🟢 Low' } + default { $Risk.riskLevel } + } + $RiskEventType = $Risk.riskEventType + $RiskDate = $Risk.detectedDateTime + $ResultMarkdown += "| $RiskDate | $UserPrincipalName | $RiskEventType | $RiskLevel |`n" + } + } + + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status $Passed -ResultMarkdown $ResultMarkdown -Risk 'High' -Name 'All high-risk sign-ins are triaged' -UserImpact 'Low' -ImplementationEffort 'High' -Category 'Monitoring' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Error running test: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'All high-risk sign-ins are triaged' -UserImpact 'Low' -ImplementationEffort 'High' -Category 'Monitoring' + } +} From 20158d37cd0eb7f8aee13275c3617746b2b7922e Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Wed, 24 Dec 2025 01:12:48 +0100 Subject: [PATCH 027/503] Next batch --- Modules/CIPPCore/Public/Set-CIPPDBCacheRiskDetections.ps1 | 2 +- .../CIPPCore/Public/Set-CIPPDBCacheRiskyServicePrincipals.ps1 | 2 +- Modules/CIPPCore/Public/Set-CIPPDBCacheRiskyUsers.ps1 | 2 +- .../Public/Set-CIPPDBCacheServicePrincipalRiskDetections.ps1 | 2 +- Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21848.ps1 | 2 +- Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21850.ps1 | 2 +- Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21861.ps1 | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheRiskDetections.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheRiskDetections.ps1 index 3ba48f9f5e5b..c85118401d4f 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheRiskDetections.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheRiskDetections.ps1 @@ -17,7 +17,7 @@ function Set-CIPPDBCacheRiskDetections { # Requires P2 licensing $RiskDetections = New-GraphGetRequest -uri 'https://graph.microsoft.com/v1.0/identityProtection/riskDetections' -tenantid $TenantFilter - + if ($RiskDetections) { Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'RiskDetections' -Data $RiskDetections Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'RiskDetections' -Data $RiskDetections -Count diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheRiskyServicePrincipals.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheRiskyServicePrincipals.ps1 index a2008db65cb3..950e9998ec0b 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheRiskyServicePrincipals.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheRiskyServicePrincipals.ps1 @@ -17,7 +17,7 @@ function Set-CIPPDBCacheRiskyServicePrincipals { # Requires Workload Identity Premium licensing $RiskyServicePrincipals = New-GraphGetRequest -uri 'https://graph.microsoft.com/v1.0/identityProtection/riskyServicePrincipals' -tenantid $TenantFilter - + if ($RiskyServicePrincipals) { Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'RiskyServicePrincipals' -Data $RiskyServicePrincipals Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'RiskyServicePrincipals' -Data $RiskyServicePrincipals -Count diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheRiskyUsers.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheRiskyUsers.ps1 index 66e94705b1f6..417110f483bd 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheRiskyUsers.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheRiskyUsers.ps1 @@ -17,7 +17,7 @@ function Set-CIPPDBCacheRiskyUsers { # Requires P2 or Governance licensing $RiskyUsers = New-GraphGetRequest -uri 'https://graph.microsoft.com/v1.0/identityProtection/riskyUsers' -tenantid $TenantFilter - + if ($RiskyUsers) { Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'RiskyUsers' -Data $RiskyUsers Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'RiskyUsers' -Data $RiskyUsers -Count diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheServicePrincipalRiskDetections.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheServicePrincipalRiskDetections.ps1 index 7e5e5c0e7ce4..79aeec84eada 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheServicePrincipalRiskDetections.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheServicePrincipalRiskDetections.ps1 @@ -17,7 +17,7 @@ function Set-CIPPDBCacheServicePrincipalRiskDetections { # Requires Workload Identity Premium licensing $ServicePrincipalRiskDetections = New-GraphGetRequest -uri 'https://graph.microsoft.com/v1.0/identityProtection/servicePrincipalRiskDetections' -tenantid $TenantFilter - + if ($ServicePrincipalRiskDetections) { Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ServicePrincipalRiskDetections' -Data $ServicePrincipalRiskDetections Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ServicePrincipalRiskDetections' -Data $ServicePrincipalRiskDetections -Count diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21848.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21848.ps1 index 9202eac17033..1e7e80c2d40c 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21848.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21848.ps1 @@ -15,7 +15,7 @@ function Invoke-CippTestZTNA21848 { $EnableBannedPasswordCheck = ($PasswordProtectionSettings.values | Where-Object { $_.name -eq 'EnableBannedPasswordCheck' }).value $BannedPasswordList = ($PasswordProtectionSettings.values | Where-Object { $_.name -eq 'BannedPasswordList' }).value - + if ([string]::IsNullOrEmpty($BannedPasswordList)) { $BannedPasswordList = $null } diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21850.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21850.ps1 index c778b0f15afc..0c144b7ad755 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21850.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21850.ps1 @@ -15,7 +15,7 @@ function Invoke-CippTestZTNA21850 { $ResultMarkdown = "❌ Password rule settings template not found." } else { $LockoutThresholdSetting = $PasswordRuleSettings.values | Where-Object { $_.name -eq 'LockoutThreshold' } - + if ($null -eq $LockoutThresholdSetting) { $Passed = 'Failed' $ResultMarkdown = "❌ Lockout threshold setting not found in [password rule settings]($PortalLink)." diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21861.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21861.ps1 index ffe99600a8b4..a0b8fa1fc044 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21861.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21861.ps1 @@ -35,7 +35,7 @@ function Invoke-CippTestZTNA21861 { } $RiskDate = $User.riskLastUpdatedDateTime $RiskDetail = $User.riskDetail - + $PortalLink = "https://entra.microsoft.com/#view/Microsoft_AAD_UsersAndTenants/UserProfileMenuBlade/~/overview/userId/$($User.id)" $ResultMarkdown += "| [$UserPrincipalName]($PortalLink) | $RiskLevel | $RiskDate | $RiskDetail |`n" } From e8e14651e0a9845f3f743cf9528341a0ff05a0f8 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Wed, 24 Dec 2025 01:17:47 +0100 Subject: [PATCH 028/503] extra test --- .../Push-CIPPDBCacheData.ps1 | 4 + ...et-CIPPDBCacheDeviceRegistrationPolicy.ps1 | 31 ++++++ .../Public/Tests/Invoke-CippTestZTNA21866.ps1 | 44 ++++++++ .../Public/Tests/Invoke-CippTestZTNA21872.ps1 | 102 ++++++++++++++++++ .../Public/Tests/Invoke-CippTestZTNA21874.ps1 | 40 +++++++ 5 files changed, 221 insertions(+) create mode 100644 Modules/CIPPCore/Public/Set-CIPPDBCacheDeviceRegistrationPolicy.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21866.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21872.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21874.ps1 diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 index 25b855cd5a3e..9f1a43004bf8 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 @@ -142,6 +142,10 @@ function Push-CIPPDBCacheData { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "RiskDetections collection failed: $($_.Exception.Message)" -sev Error } + try { Set-CIPPDBCacheDeviceRegistrationPolicy -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "DeviceRegistrationPolicy collection failed: $($_.Exception.Message)" -sev Error + } + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Completed database cache collection for tenant' -sev Info } catch { diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheDeviceRegistrationPolicy.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheDeviceRegistrationPolicy.ps1 new file mode 100644 index 000000000000..b92e084ba031 --- /dev/null +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheDeviceRegistrationPolicy.ps1 @@ -0,0 +1,31 @@ +function Set-CIPPDBCacheDeviceRegistrationPolicy { + <# + .SYNOPSIS + Caches device registration policy for a tenant + + .PARAMETER TenantFilter + The tenant to cache device registration policy for + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter + ) + + try { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching device registration policy' -sev Info + + $DeviceRegistrationPolicy = New-GraphGetRequest -uri 'https://graph.microsoft.com/v1.0/policies/deviceRegistrationPolicy' -tenantid $TenantFilter + + if ($DeviceRegistrationPolicy) { + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'DeviceRegistrationPolicy' -Data @($DeviceRegistrationPolicy) + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached device registration policy successfully' -sev Info + } + + } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter ` + -message "Failed to cache device registration policy: $($_.Exception.Message)" ` + -sev Warning ` + -LogData (Get-CippException -Exception $_) + } +} diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21866.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21866.ps1 new file mode 100644 index 000000000000..12ed524c2c30 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21866.ps1 @@ -0,0 +1,44 @@ +function Invoke-CippTestZTNA21866 { + param($Tenant) + + $TestId = 'ZTNA21866' + + try { + # Get directory recommendations from cache + $Recommendations = New-CIPPDbRequest -TenantFilter $Tenant -Type 'DirectoryRecommendations' + + if (-not $Recommendations) { + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Directory recommendations not found in cache' -Risk 'Medium' -Name 'All Microsoft Entra recommendations are addressed' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Monitoring' + return + } + + # Filter for unaddressed recommendations (active or postponed status) + $UnaddressedRecommendations = $Recommendations | Where-Object { $_.status -in @('active', 'postponed') } + + $Passed = if ($UnaddressedRecommendations.Count -eq 0) { 'Passed' } else { 'Failed' } + + if ($Passed -eq 'Passed') { + $ResultMarkdown = '✅ All Entra Recommendations are addressed.' + } else { + $ResultMarkdown = "❌ Found $($UnaddressedRecommendations.Count) unaddressed Entra recommendations.`n`n" + $ResultMarkdown += "## Unaddressed Entra recommendations`n`n" + $ResultMarkdown += "| Display Name | Status | Insights | Priority |`n" + $ResultMarkdown += "| :--- | :--- | :--- | :--- |`n" + + foreach ($Item in $UnaddressedRecommendations) { + $DisplayName = $Item.displayName + $Status = $Item.status + $Insights = $Item.insights + $Priority = $Item.priority + $ResultMarkdown += "| $DisplayName | $Status | $Insights | $Priority |`n" + } + } + + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status $Passed -ResultMarkdown $ResultMarkdown -Risk 'Medium' -Name 'All Microsoft Entra recommendations are addressed' -UserImpact 'Low' -ImplementationEffort 'High' -Category 'Monitoring' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Error running test: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'All Microsoft Entra recommendations are addressed' -UserImpact 'Low' -ImplementationEffort 'High' -Category 'Monitoring' + } +} diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21872.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21872.ps1 new file mode 100644 index 000000000000..8695e061ddae --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21872.ps1 @@ -0,0 +1,102 @@ +function Invoke-CippTestZTNA21872 { + param($Tenant) + + $TestId = 'ZTNA21872' + + try { + # Get conditional access policies and device registration policy from cache + $CAPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ConditionalAccessPolicies' + $DeviceRegistrationPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'DeviceRegistrationPolicy' + + if (-not $CAPolicies) { + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Conditional Access policies not found in cache' -Risk 'High' -Name 'Require multifactor authentication for device join and device registration using user action' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Access control' + return + } + + if (-not $DeviceRegistrationPolicy) { + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Device registration policy not found in cache' -Risk 'High' -Name 'Require multifactor authentication for device join and device registration using user action' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Access control' + return + } + + $MfaRequiredInDeviceSettings = $DeviceRegistrationPolicy.multiFactorAuthConfiguration -eq 'required' + + # Filter for enabled device registration CA policies + $DeviceRegistrationPolicies = $CAPolicies | Where-Object { + ($_.state -eq 'enabled') -and + ($_.conditions.applications.includeUserActions -eq 'urn:user:registerdevice') + } + + # Check each policy to see if it properly requires MFA + $ValidPolicies = [System.Collections.Generic.List[object]]::new() + foreach ($Policy in $DeviceRegistrationPolicies) { + $RequiresMfa = $false + + # Check if the policy directly requires MFA + if ($Policy.grantControls.builtInControls -contains 'mfa') { + $RequiresMfa = $true + } + + # Check if the policy uses any authentication strength + if ($null -ne $Policy.grantControls.authenticationStrength) { + $RequiresMfa = $true + } + + # If the policy requires MFA, add it to valid policies + if ($RequiresMfa) { + $ValidPolicies.Add($Policy) + } + } + + # Determine pass/fail conditions + if ($MfaRequiredInDeviceSettings) { + $Passed = 'Failed' + $ResultMarkdown = "❌ **MFA is configured incorrectly.** Device Settings has 'Require Multi-Factor Authentication to register or join devices' set to Yes. According to best practices, this should be set to No, and MFA should be enforced through Conditional Access policies instead.`n`n" + } elseif ($DeviceRegistrationPolicies.Count -eq 0) { + $Passed = 'Failed' + $ResultMarkdown = "❌ **No Conditional Access policies found** for device registration or device join. Create a policy that requires MFA for these user actions.`n`n" + } elseif ($ValidPolicies.Count -eq 0) { + $Passed = 'Failed' + $ResultMarkdown = "❌ **Conditional Access policies found**, but they're not correctly configured. Policies should require MFA or appropriate authentication strength.`n`n" + } else { + $Passed = 'Passed' + $ResultMarkdown = "✅ **Properly configured Conditional Access policies found** that require MFA for device registration/join actions.`n`n" + } + + # Add device settings information + $ResultMarkdown += "## Device Settings Configuration`n`n" + $ResultMarkdown += "| Setting | Value | Recommended Value | Status |`n" + $ResultMarkdown += "| :------ | :---- | :---------------- | :----- |`n" + + $DeviceSettingStatus = if ($MfaRequiredInDeviceSettings) { '❌ Should be set to No' } else { '✅ Correctly configured' } + $DeviceSettingValue = if ($MfaRequiredInDeviceSettings) { 'Yes' } else { 'No' } + $ResultMarkdown += "| Require Multi-Factor Authentication to register or join devices | $DeviceSettingValue | No | $DeviceSettingStatus |`n" + + # Add policies information if any found + if ($DeviceRegistrationPolicies.Count -gt 0) { + $ResultMarkdown += "`n## Device Registration/Join Conditional Access Policies`n`n" + $ResultMarkdown += "| Policy Name | State | Requires MFA | Status |`n" + $ResultMarkdown += "| :---------- | :---- | :----------- | :----- |`n" + + foreach ($Policy in $DeviceRegistrationPolicies) { + $PolicyName = $Policy.displayName + $PolicyState = $Policy.state + $PolicyLink = "https://entra.microsoft.com/#view/Microsoft_AAD_ConditionalAccess/PolicyBlade/policyId/$($Policy.id)" + $PolicyNameLink = "[$PolicyName]($PolicyLink)" + + # Check if this policy is properly configured + $IsValid = $Policy -in $ValidPolicies + $RequiresMfaText = if ($IsValid) { 'Yes' } else { 'No' } + $StatusText = if ($IsValid) { '✅ Properly configured' } else { '❌ Incorrectly configured' } + + $ResultMarkdown += "| $PolicyNameLink | $PolicyState | $RequiresMfaText | $StatusText |`n" + } + } + + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status $Passed -ResultMarkdown $ResultMarkdown -Risk 'High' -Name 'Require multifactor authentication for device join and device registration using user action' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Access control' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Error running test: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Require multifactor authentication for device join and device registration using user action' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Access control' + } +} diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21874.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21874.ps1 new file mode 100644 index 000000000000..4659c9c4f2ce --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21874.ps1 @@ -0,0 +1,40 @@ +function Invoke-CippTestZTNA21874 { + param($Tenant) + + $TestId = 'ZTNA21874' + + try { + # Get B2B Management Policy from cache + $B2BManagementPolicyObject = New-CIPPDbRequest -TenantFilter $Tenant -Type 'B2BManagementPolicy' + + if (-not $B2BManagementPolicyObject) { + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'B2B Management Policy not found in cache' -Risk 'Medium' -Name 'Guest access is limited to approved tenants' -UserImpact 'Medium' -ImplementationEffort 'High' -Category 'External collaboration' + return + } + + $Passed = 'Failed' + $AllowedDomains = $null + + if ($B2BManagementPolicyObject.definition) { + $B2BManagementPolicy = ($B2BManagementPolicyObject.definition | ConvertFrom-Json).B2BManagementPolicy + $AllowedDomains = $B2BManagementPolicy.InvitationsAllowedAndBlockedDomainsPolicy.AllowedDomains + + if ($AllowedDomains -and $AllowedDomains.Count -gt 0) { + $Passed = 'Passed' + } + } + + if ($Passed -eq 'Passed') { + $ResultMarkdown = '✅ Allow/Deny lists of domains to restrict external collaboration are configured.' + } else { + $ResultMarkdown = '❌ Allow/Deny lists of domains to restrict external collaboration are not configured.' + } + + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status $Passed -ResultMarkdown $ResultMarkdown -Risk 'Medium' -Name 'Guest access is limited to approved tenants' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'External collaboration' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Error running test: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'Guest access is limited to approved tenants' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'External collaboration' + } +} From 2ceb15ced0b2680bf04a3a28a6698efa34ba5c56 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Wed, 24 Dec 2025 01:36:19 +0100 Subject: [PATCH 029/503] Add new tests --- .../Public/Tests/Invoke-CippTestZTNA21883.ps1 | 109 ++++++++++++ .../Public/Tests/Invoke-CippTestZTNA21889.ps1 | 123 +++++++++++++ .../Public/Tests/Invoke-CippTestZTNA21892.ps1 | 129 ++++++++++++++ .../Public/Tests/Invoke-CippTestZTNA21941.ps1 | 168 ++++++++++++++++++ .../Public/Tests/Invoke-CippTestZTNA21953.ps1 | 59 ++++++ .../Public/Tests/Invoke-CippTestZTNA21954.ps1 | 59 ++++++ .../Public/Tests/Invoke-CippTestZTNA21955.ps1 | 59 ++++++ .../Public/Tests/Invoke-CippTestZTNA22124.ps1 | 75 ++++++++ .../Public/Tests/Invoke-CippTestZTNA22659.ps1 | 88 +++++++++ .../Public/Tests/Invoke-CippTestZTNA24570.ps1 | 139 +++++++++++++++ .../Public/Tests/Invoke-CippTestZTNA24824.ps1 | 143 +++++++++++++++ .../Public/Tests/Invoke-CippTestZTNA24827.ps1 | 153 ++++++++++++++++ 12 files changed, 1304 insertions(+) create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21883.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21889.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21892.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21941.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21953.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21954.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21955.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA22124.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA22659.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24570.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24824.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24827.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21883.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21883.ps1 new file mode 100644 index 000000000000..11247eb87f57 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21883.ps1 @@ -0,0 +1,109 @@ +function Invoke-CippTestZTNA21883 { + <# + .SYNOPSIS + Checks if workload identities are configured with risk-based policies + + .DESCRIPTION + Verifies that Conditional Access policies exist that: + - Block authentication based on service principal risk + - Are enabled + - Target service principals + + .FUNCTIONALITY + Internal + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Tenant + ) + + try { + # Get Conditional Access policies from cache + $Policies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ConditionalAccessPolicies' + + if (-not $Policies) { + Add-CippTestResult -TestId 'ZTNA21883' -TenantFilter $Tenant -TestType 'ZeroTrustNetworkAccess' -Status 'Skipped' ` + -ResultMarkdown 'No Conditional Access policies found in cache.' ` + -Risk 'Medium' -Name 'Workload identities configured with risk-based policies' ` + -UserImpact 'High' -ImplementationEffort 'Low' ` + -Category 'Access control' + return + } + + # Filter for policies that: + # - Block authentication + # - Include service principals + # - Are enabled + $MatchedPolicies = [System.Collections.Generic.List[object]]::new() + foreach ($Policy in $Policies) { + $blocksAuth = $false + if ($Policy.grantControls.builtInControls) { + foreach ($control in $Policy.grantControls.builtInControls) { + if ($control -eq 'block') { + $blocksAuth = $true + break + } + } + } + + $includesSP = $false + if ($Policy.conditions.clientApplications.includeServicePrincipals) { + $includesSP = $true + } + + $isEnabled = $Policy.state -eq 'enabled' + + if ($blocksAuth -and $includesSP -and $isEnabled) { + $MatchedPolicies.Add($Policy) + } + } + + # Determine pass/fail + if ($MatchedPolicies.Count -ge 1) { + $Status = 'Passed' + $ResultMarkdown = "✅ **Pass**: Workload identities are protected by risk-based Conditional Access policies.`n`n" + $ResultMarkdown += "## Matching policies`n`n" + $ResultMarkdown += "| Policy name | State | Service principals | Grant controls |`n" + $ResultMarkdown += "| :---------- | :---- | :----------------- | :------------- |`n" + + foreach ($Policy in $MatchedPolicies) { + $policyLink = "https://entra.microsoft.com/#view/Microsoft_AAD_ConditionalAccess/PolicyBlade/policyId/$($Policy.id)" + $policyName = if ($Policy.displayName) { $Policy.displayName } else { 'Unnamed' } + $spTargets = if ($Policy.conditions.clientApplications.includeServicePrincipals) { + ($Policy.conditions.clientApplications.includeServicePrincipals | Select-Object -First 3) -join ', ' + if ($Policy.conditions.clientApplications.includeServicePrincipals.Count -gt 3) { + $spTargets += " (and $($Policy.conditions.clientApplications.includeServicePrincipals.Count - 3) more)" + } + $spTargets + } else { + 'None' + } + $grants = if ($Policy.grantControls.builtInControls) { + $Policy.grantControls.builtInControls -join ', ' + } else { + 'None' + } + $ResultMarkdown += "| [$policyName]($policyLink) | $($Policy.state) | $spTargets | $grants |`n" + } + } else { + $Status = 'Failed' + $ResultMarkdown = "❌ **Fail**: No Conditional Access policies found that protect workload identities with risk-based controls.`n`n" + $ResultMarkdown += 'Workload identities should be protected by policies that block authentication when service principal risk is detected.' + } + + Add-CippTestResult -TestId 'ZTNA21883' -TenantFilter $Tenant -TestType 'ZeroTrustNetworkAccess' -Status $Status ` + -ResultMarkdown $ResultMarkdown ` + -Risk 'Medium' -Name 'Workload identities configured with risk-based policies' ` + -UserImpact 'High' -ImplementationEffort 'Low' ` + -Category 'Access control' + + } catch { + Add-CippTestResult -TestId 'ZTNA21883' -TenantFilter $Tenant -TestType 'ZeroTrustNetworkAccess' -Status 'Failed' ` + -ResultMarkdown "❌ **Error**: $($_.Exception.Message)" ` + -Risk 'Medium' -Name 'Workload identities configured with risk-based policies' ` + -UserImpact 'High' -ImplementationEffort 'Low' ` + -Category 'Access control' + Write-LogMessage -API 'ZeroTrustNetworkAccess' -tenant $Tenant -message "Test ZTNA21883 failed: $($_.Exception.Message)" -sev Error + } +} diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21889.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21889.ps1 new file mode 100644 index 000000000000..c8ff7fdf08d2 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21889.ps1 @@ -0,0 +1,123 @@ +function Invoke-CippTestZTNA21889 { + <# + .SYNOPSIS + Checks if organization has reduced password surface area by enabling multiple passwordless authentication methods + + .DESCRIPTION + Verifies that both FIDO2 Security Keys and Microsoft Authenticator are enabled with proper configuration + to reduce reliance on passwords. + + .FUNCTIONALITY + Internal + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Tenant + ) + + try { + # Get authentication methods policy from cache + $AuthMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' + + if (-not $AuthMethodsPolicy) { + Add-CippTestResult -TestId 'ZTNA21889' -TenantFilter $Tenant -TestType 'ZeroTrustNetworkAccess' -Status 'Skipped' ` + -ResultMarkdown 'Unable to retrieve authentication methods policy from cache.' ` + -Risk 'High' -Name 'Reduce the user-visible password surface area' ` + -UserImpact 'Medium' -ImplementationEffort 'Medium' ` + -Category 'Access control' + return + } + + # Extract FIDO2 and Microsoft Authenticator configurations + $Fido2Config = $null + $AuthenticatorConfig = $null + + if ($AuthMethodsPolicy.authenticationMethodConfigurations) { + foreach ($config in $AuthMethodsPolicy.authenticationMethodConfigurations) { + if ($config.id -eq 'Fido2') { + $Fido2Config = $config + } + if ($config.id -eq 'MicrosoftAuthenticator') { + $AuthenticatorConfig = $config + } + } + } + + # Check FIDO2 configuration + $Fido2Enabled = $Fido2Config.state -eq 'enabled' + $Fido2HasTargets = $Fido2Config.includeTargets -and $Fido2Config.includeTargets.Count -gt 0 + $Fido2Valid = $Fido2Enabled -and $Fido2HasTargets + + # Check Microsoft Authenticator configuration + $AuthEnabled = $AuthenticatorConfig.state -eq 'enabled' + $AuthHasTargets = $AuthenticatorConfig.includeTargets -and $AuthenticatorConfig.includeTargets.Count -gt 0 + $AuthMode = $null + if ($AuthenticatorConfig.includeTargets) { + foreach ($target in $AuthenticatorConfig.includeTargets) { + if ($target.authenticationMode) { + $AuthMode = $target.authenticationMode + break + } + } + } + + if ([string]::IsNullOrEmpty($AuthMode)) { + $AuthMode = 'Not configured' + $AuthModeValid = $false + } else { + $AuthModeValid = ($AuthMode -eq 'any') -or ($AuthMode -eq 'deviceBasedPush') + } + $AuthValid = $AuthEnabled -and $AuthHasTargets -and $AuthModeValid + + # Determine pass/fail + $Status = if ($Fido2Valid -and $AuthValid) { 'Passed' } else { 'Failed' } + + # Build result message + if ($Status -eq 'Passed') { + $ResultMarkdown = "✅ **Pass**: Your organization has implemented multiple passwordless authentication methods reducing password exposure.`n`n" + } else { + $ResultMarkdown = "❌ **Fail**: Your organization relies heavily on password-based authentication, creating security vulnerabilities.`n`n" + } + + # Build detailed markdown table + $ResultMarkdown += "## Passwordless authentication methods`n`n" + $ResultMarkdown += "| Method | State | Include targets | Authentication mode | Status |`n" + $ResultMarkdown += "| :----- | :---- | :-------------- | :------------------ | :----- |`n" + + # FIDO2 row + $Fido2State = if ($Fido2Enabled) { '✅ Enabled' } else { '❌ Disabled' } + $Fido2TargetsDisplay = if ($Fido2Config.includeTargets -and $Fido2Config.includeTargets.Count -gt 0) { + "$($Fido2Config.includeTargets.Count) target(s)" + } else { + 'None' + } + $Fido2Status = if ($Fido2Valid) { '✅ Pass' } else { '❌ Fail' } + $ResultMarkdown += "| FIDO2 Security Keys | $Fido2State | $Fido2TargetsDisplay | N/A | $Fido2Status |`n" + + # Microsoft Authenticator row + $AuthState = if ($AuthEnabled) { '✅ Enabled' } else { '❌ Disabled' } + $AuthTargetsDisplay = if ($AuthenticatorConfig.includeTargets -and $AuthenticatorConfig.includeTargets.Count -gt 0) { + "$($AuthenticatorConfig.includeTargets.Count) target(s)" + } else { + 'None' + } + $AuthModeDisplay = if ($AuthModeValid) { "✅ $AuthMode" } else { "❌ $AuthMode" } + $AuthStatus = if ($AuthValid) { '✅ Pass' } else { '❌ Fail' } + $ResultMarkdown += "| Microsoft Authenticator | $AuthState | $AuthTargetsDisplay | $AuthModeDisplay | $AuthStatus |`n" + + Add-CippTestResult -TestId 'ZTNA21889' -TenantFilter $Tenant -TestType 'ZeroTrustNetworkAccess' -Status $Status ` + -ResultMarkdown $ResultMarkdown ` + -Risk 'High' -Name 'Reduce the user-visible password surface area' ` + -UserImpact 'Medium' -ImplementationEffort 'Medium' ` + -Category 'Access control' + + } catch { + Add-CippTestResult -TestId 'ZTNA21889' -TenantFilter $Tenant -TestType 'ZeroTrustNetworkAccess' -Status 'Failed' ` + -ResultMarkdown "❌ **Error**: $($_.Exception.Message)" ` + -Risk 'High' -Name 'Reduce the user-visible password surface area' ` + -UserImpact 'Medium' -ImplementationEffort 'Medium' ` + -Category 'Access control' + Write-LogMessage -API 'ZeroTrustNetworkAccess' -tenant $Tenant -message "Test ZTNA21889 failed: $($_.Exception.Message)" -sev Error + } +} diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21892.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21892.ps1 new file mode 100644 index 000000000000..c35b44a8a827 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21892.ps1 @@ -0,0 +1,129 @@ +function Invoke-CippTestZTNA21892 { + <# + .SYNOPSIS + Verifies that all sign-in activity is restricted to managed devices + + .DESCRIPTION + Checks for Conditional Access policies that: + - Apply to all users + - Apply to all applications + - Require compliant or hybrid joined devices + - Are enabled + + .FUNCTIONALITY + Internal + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Tenant + ) + + try { + # Get Conditional Access policies from cache + $Policies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ConditionalAccessPolicies' + + if (-not $Policies) { + Add-CippTestResult -TestId 'ZTNA21892' -TenantFilter $Tenant -TestType 'ZeroTrustNetworkAccess' -Status 'Skipped' ` + -ResultMarkdown 'No Conditional Access policies found in cache.' ` + -Risk 'High' -Name 'All sign-in activity comes from managed devices' ` + -UserImpact 'High' -ImplementationEffort 'High' ` + -Category 'Access control' + return + } + + # Find policies that require managed devices for all users and apps + $MatchingPolicies = [System.Collections.Generic.List[object]]::new() + foreach ($Policy in $Policies) { + # Check if applies to all users + $appliesToAllUsers = $false + if ($Policy.conditions.users.includeUsers) { + foreach ($user in $Policy.conditions.users.includeUsers) { + if ($user -eq 'All') { + $appliesToAllUsers = $true + break + } + } + } + + # Check if applies to all apps + $appliesToAllApps = $false + if ($Policy.conditions.applications.includeApplications) { + foreach ($app in $Policy.conditions.applications.includeApplications) { + if ($app -eq 'All') { + $appliesToAllApps = $true + break + } + } + } + + # Check if requires compliant or hybrid joined device + $requiresCompliantDevice = $false + $requiresHybridJoined = $false + if ($Policy.grantControls.builtInControls) { + foreach ($control in $Policy.grantControls.builtInControls) { + if ($control -eq 'compliantDevice') { + $requiresCompliantDevice = $true + } + if ($control -eq 'domainJoinedDevice') { + $requiresHybridJoined = $true + } + } + } + + $isEnabled = $Policy.state -eq 'enabled' + + # Policy matches if enabled, applies to all users/apps, and requires managed device + if ($isEnabled -and $appliesToAllUsers -and $appliesToAllApps -and ($requiresCompliantDevice -or $requiresHybridJoined)) { + $MatchingPolicies.Add([PSCustomObject]@{ + PolicyId = $Policy.id + PolicyState = $Policy.state + DisplayName = $Policy.displayName + AllUsers = $appliesToAllUsers + AllApps = $appliesToAllApps + CompliantDevice = $requiresCompliantDevice + HybridJoinedDevice = $requiresHybridJoined + IsFullyCompliant = $isEnabled -and $appliesToAllUsers -and $appliesToAllApps -and ($requiresCompliantDevice -or $requiresHybridJoined) + }) + } + } + + # Determine pass/fail + if ($MatchingPolicies.Count -gt 0) { + $Status = 'Passed' + $ResultMarkdown = "✅ **Pass**: Conditional Access policies require managed devices for all sign-in activity.`n`n" + $ResultMarkdown += "## Matching policies`n`n" + $ResultMarkdown += "| Policy name | State | All users | All apps | Compliant device | Hybrid joined |`n" + $ResultMarkdown += "| :---------- | :---- | :-------- | :------- | :--------------- | :------------ |`n" + + foreach ($Policy in $MatchingPolicies) { + $policyLink = "https://entra.microsoft.com/#view/Microsoft_AAD_ConditionalAccess/PolicyBlade/policyId/$($Policy.PolicyId)" + $policyName = if ($Policy.DisplayName) { $Policy.DisplayName } else { 'Unnamed' } + $allUsers = if ($Policy.AllUsers) { '✅' } else { '❌' } + $allApps = if ($Policy.AllApps) { '✅' } else { '❌' } + $compliant = if ($Policy.CompliantDevice) { '✅' } else { '❌' } + $hybrid = if ($Policy.HybridJoinedDevice) { '✅' } else { '❌' } + + $ResultMarkdown += "| [$policyName]($policyLink) | $($Policy.PolicyState) | $allUsers | $allApps | $compliant | $hybrid |`n" + } + } else { + $Status = 'Failed' + $ResultMarkdown = "❌ **Fail**: No Conditional Access policies found that require managed devices for all sign-in activity.`n`n" + $ResultMarkdown += 'Organizations should enforce that all sign-ins come from managed devices (compliant or hybrid Azure AD joined) to ensure security controls are applied.' + } + + Add-CippTestResult -TestId 'ZTNA21892' -TenantFilter $Tenant -TestType 'ZeroTrustNetworkAccess' -Status $Status ` + -ResultMarkdown $ResultMarkdown ` + -Risk 'High' -Name 'All sign-in activity comes from managed devices' ` + -UserImpact 'High' -ImplementationEffort 'High' ` + -Category 'Access control' + + } catch { + Add-CippTestResult -TestId 'ZTNA21892' -TenantFilter $Tenant -TestType 'ZeroTrustNetworkAccess' -Status 'Failed' ` + -ResultMarkdown "❌ **Error**: $($_.Exception.Message)" ` + -Risk 'High' -Name 'All sign-in activity comes from managed devices' ` + -UserImpact 'High' -ImplementationEffort 'High' ` + -Category 'Access control' + Write-LogMessage -API 'ZeroTrustNetworkAccess' -tenant $Tenant -message "Test ZTNA21892 failed: $($_.Exception.Message)" -sev Error + } +} diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21941.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21941.ps1 new file mode 100644 index 000000000000..cbfdee5e139e --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21941.ps1 @@ -0,0 +1,168 @@ +function Invoke-CippTestZTNA21941 { + <# + .SYNOPSIS + Checks if token protection policies are enforced for Windows platform + + .DESCRIPTION + Verifies that Conditional Access policies with token protection (secureSignInSession) are + configured for Windows devices, requiring Office 365 and Microsoft Graph access through + protected sessions to prevent token theft. + + .FUNCTIONALITY + Internal + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Tenant + ) + + try { + # Get CA policies from cache + $CAPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ConditionalAccessPolicies' + + if (-not $CAPolicies) { + Add-CippTestResult -TestId 'ZTNA21941' -TenantFilter $Tenant -TestType 'ZeroTrustNetworkAccess' -Status 'Skipped' ` + -ResultMarkdown 'Unable to retrieve Conditional Access policies from cache.' ` + -Risk 'High' -Name 'Implement token protection policies' ` + -UserImpact 'Medium' -ImplementationEffort 'Medium' ` + -Category 'Access control' + return + } + + # Required Office 365 and Graph app IDs + $RequiredAppIds = @( + '00000002-0000-0ff1-ce00-000000000000', # Office 365 Exchange Online + '00000003-0000-0ff1-ce00-000000000000' # Microsoft Graph + ) + + # Filter for policies with Windows platform and secureSignInSession control + $TokenProtectionPolicies = [System.Collections.Generic.List[object]]::new() + + foreach ($policy in $CAPolicies) { + # Check if policy has Windows platform + $hasWindows = $false + if ($policy.conditions.platforms.includePlatforms) { + if ($policy.conditions.platforms.includePlatforms -contains 'windows' -or + $policy.conditions.platforms.includePlatforms -contains 'all') { + $hasWindows = $true + } + } + + # Check if policy has secureSignInSession control + $hasTokenProtection = $false + if ($policy.sessionControls -and $policy.sessionControls.signInFrequency) { + if ($policy.sessionControls.signInFrequency.isEnabled -eq $true -and + $policy.sessionControls.signInFrequency.authenticationType -eq 'primaryAndSecondaryAuthentication') { + $hasTokenProtection = $true + } + } + + # Alternative check for newer API format + if (-not $hasTokenProtection -and $policy.sessionControls) { + foreach ($prop in $policy.sessionControls.PSObject.Properties) { + if ($prop.Name -like '*secureSignIn*' -or $prop.Name -like '*tokenProtection*') { + if ($prop.Value.isEnabled -eq $true) { + $hasTokenProtection = $true + break + } + } + } + } + + if ($hasWindows -and $hasTokenProtection -and $policy.state -eq 'enabled') { + # Check if policy includes users + $hasUsers = $false + if ($policy.conditions.users.includeUsers -and $policy.conditions.users.includeUsers.Count -gt 0) { + $hasUsers = $true + } + + # Check if policy includes required apps + $hasRequiredApps = $false + if ($policy.conditions.applications.includeApplications) { + $includeAll = $policy.conditions.applications.includeApplications -contains 'All' + if ($includeAll) { + $hasRequiredApps = $true + } else { + $foundApps = 0 + foreach ($appId in $RequiredAppIds) { + if ($policy.conditions.applications.includeApplications -contains $appId) { + $foundApps++ + } + } + if ($foundApps -eq $RequiredAppIds.Count) { + $hasRequiredApps = $true + } + } + } + + $policyStatus = 'Unknown' + if ($hasUsers -and $hasRequiredApps) { + $policyStatus = 'Pass' + } elseif (-not $hasUsers) { + $policyStatus = 'No users targeted' + } elseif (-not $hasRequiredApps) { + $policyStatus = 'Missing required apps' + } + + $TokenProtectionPolicies.Add([PSCustomObject]@{ + Name = $policy.displayName + State = $policy.state + HasUsers = $hasUsers + HasRequiredApps = $hasRequiredApps + Status = $policyStatus + }) + } + } + + # Determine overall status + $PassingPolicies = $TokenProtectionPolicies | Where-Object { $_.Status -eq 'Pass' } + $Status = if ($PassingPolicies.Count -gt 0) { 'Passed' } else { 'Failed' } + + # Build result markdown + if ($Status -eq 'Passed') { + $ResultMarkdown = "✅ **Pass**: Token protection policies are properly configured for Windows devices.`n`n" + $ResultMarkdown += "Token protection binds authentication tokens to devices, making stolen tokens unusable on other devices.`n`n" + } else { + if ($TokenProtectionPolicies.Count -eq 0) { + $ResultMarkdown = "❌ **Fail**: No token protection policies found for Windows devices.`n`n" + $ResultMarkdown += "Without token protection, authentication tokens can be stolen and replayed from other devices.`n`n" + $ResultMarkdown += '[Create token protection policies](https://entra.microsoft.com/#view/Microsoft_AAD_ConditionalAccess/ConditionalAccessBlade/~/Policies)' + } else { + $ResultMarkdown = "❌ **Fail**: Token protection policies exist but are not properly configured.`n`n" + $ResultMarkdown += "Policies must target users and include both Office 365 and Microsoft Graph applications.`n`n" + } + } + + if ($TokenProtectionPolicies.Count -gt 0) { + $ResultMarkdown += "## Token protection policies`n`n" + $ResultMarkdown += "| Policy Name | State | Has Users | Has Required Apps | Status |`n" + $ResultMarkdown += "| :---------- | :---- | :-------- | :---------------- | :----- |`n" + + foreach ($policy in $TokenProtectionPolicies) { + $stateIcon = if ($policy.State -eq 'enabled') { '✅' } else { '❌' } + $usersIcon = if ($policy.HasUsers) { '✅' } else { '❌' } + $appsIcon = if ($policy.HasRequiredApps) { '✅' } else { '❌' } + $statusIcon = if ($policy.Status -eq 'Pass') { '✅' } else { '❌' } + + $ResultMarkdown += "| $($policy.Name) | $stateIcon $($policy.State) | $usersIcon | $appsIcon | $statusIcon $($policy.Status) |`n" + } + + $ResultMarkdown += "`n[Review policies](https://entra.microsoft.com/#view/Microsoft_AAD_ConditionalAccess/ConditionalAccessBlade/~/Policies)" + } + + Add-CippTestResult -TestId 'ZTNA21941' -TenantFilter $Tenant -TestType 'ZeroTrustNetworkAccess' -Status $Status ` + -ResultMarkdown $ResultMarkdown ` + -Risk 'High' -Name 'Implement token protection policies' ` + -UserImpact 'Medium' -ImplementationEffort 'Medium' ` + -Category 'Access control' + + } catch { + Add-CippTestResult -TestId 'ZTNA21941' -TenantFilter $Tenant -TestType 'ZeroTrustNetworkAccess' -Status 'Failed' ` + -ResultMarkdown "❌ **Error**: $($_.Exception.Message)" ` + -Risk 'High' -Name 'Implement token protection policies' ` + -UserImpact 'Medium' -ImplementationEffort 'Medium' ` + -Category 'Access control' + Write-LogMessage -API 'ZeroTrustNetworkAccess' -tenant $Tenant -message "Test ZTNA21941 failed: $($_.Exception.Message)" -sev Error + } +} diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21953.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21953.ps1 new file mode 100644 index 000000000000..ea12a7238e42 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21953.ps1 @@ -0,0 +1,59 @@ +function Invoke-CippTestZTNA21953 { + <# + .SYNOPSIS + Checks if Windows Local Administrator Password Solution (LAPS) is deployed in the tenant + + .DESCRIPTION + Verifies that LAPS is enabled in the device registration policy to automatically manage + and rotate local administrator passwords on Windows devices. + + .FUNCTIONALITY + Internal + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Tenant + ) + + try { + # Get device registration policy from cache + $DeviceRegPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'DeviceRegistrationPolicy' + + if (-not $DeviceRegPolicy) { + Add-CippTestResult -TestId 'ZTNA21953' -TenantFilter $Tenant -TestType 'ZeroTrustNetworkAccess' -Status 'Skipped' ` + -ResultMarkdown 'Unable to retrieve device registration policy from cache.' ` + -Risk 'High' -Name 'Deploy Windows Local Administrator Password Solution (LAPS)' ` + -UserImpact 'Low' -ImplementationEffort 'Low' ` + -Category 'Device security' + return + } + + # Check if LAPS is enabled + $LapsEnabled = $DeviceRegPolicy.localAdminPassword.isEnabled -eq $true + + $Status = if ($LapsEnabled) { 'Passed' } else { 'Failed' } + + if ($Status -eq 'Passed') { + $ResultMarkdown = "✅ **Pass**: LAPS is deployed. Your organization can automatically manage and rotate local administrator passwords on all Entra joined and hybrid Entra joined Windows devices.`n`n" + $ResultMarkdown += '[Learn more](https://entra.microsoft.com/#view/Microsoft_AAD_Devices/DevicesMenuBlade/~/DeviceSettings/menuId/)' + } else { + $ResultMarkdown = "❌ **Fail**: LAPS is not deployed. Local administrator passwords may be weak, shared, or unchanged, increasing security risk.`n`n" + $ResultMarkdown += '[Deploy LAPS](https://entra.microsoft.com/#view/Microsoft_AAD_Devices/DevicesMenuBlade/~/DeviceSettings/menuId/)' + } + + Add-CippTestResult -TestId 'ZTNA21953' -TenantFilter $Tenant -TestType 'ZeroTrustNetworkAccess' -Status $Status ` + -ResultMarkdown $ResultMarkdown ` + -Risk 'High' -Name 'Deploy Windows Local Administrator Password Solution (LAPS)' ` + -UserImpact 'Low' -ImplementationEffort 'Low' ` + -Category 'Device security' + + } catch { + Add-CippTestResult -TestId 'ZTNA21953' -TenantFilter $Tenant -TestType 'ZeroTrustNetworkAccess' -Status 'Failed' ` + -ResultMarkdown "❌ **Error**: $($_.Exception.Message)" ` + -Risk 'High' -Name 'Deploy Windows Local Administrator Password Solution (LAPS)' ` + -UserImpact 'Low' -ImplementationEffort 'Low' ` + -Category 'Device security' + Write-LogMessage -API 'ZeroTrustNetworkAccess' -tenant $Tenant -message "Test ZTNA21953 failed: $($_.Exception.Message)" -sev Error + } +} diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21954.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21954.ps1 new file mode 100644 index 000000000000..2bae6ba273ed --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21954.ps1 @@ -0,0 +1,59 @@ +function Invoke-CippTestZTNA21954 { + <# + .SYNOPSIS + Checks if non-admin users are restricted from reading BitLocker recovery keys + + .DESCRIPTION + Verifies that the authorization policy restricts non-admin users from reading BitLocker + recovery keys for their own devices, reducing the risk of unauthorized key access. + + .FUNCTIONALITY + Internal + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Tenant + ) + + try { + # Get authorization policy from cache + $AuthPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthorizationPolicy' + + if (-not $AuthPolicy) { + Add-CippTestResult -TestId 'ZTNA21954' -TenantFilter $Tenant -TestType 'ZeroTrustNetworkAccess' -Status 'Skipped' ` + -ResultMarkdown 'Unable to retrieve authorization policy from cache.' ` + -Risk 'Low' -Name 'Restrict non-admin users from reading BitLocker recovery keys' ` + -UserImpact 'Low' -ImplementationEffort 'Low' ` + -Category 'Device security' + return + } + + # Check if BitLocker key reading is restricted (should be false) + $IsRestricted = $AuthPolicy.defaultUserRolePermissions.allowedToReadBitlockerKeysForOwnedDevice -eq $false + + $Status = if ($IsRestricted) { 'Passed' } else { 'Failed' } + + if ($Status -eq 'Passed') { + $ResultMarkdown = "✅ **Pass**: Non-admin users cannot read BitLocker recovery keys, reducing the risk of unauthorized access.`n`n" + $ResultMarkdown += '[Review settings](https://entra.microsoft.com/#view/Microsoft_AAD_IAM/PoliciesTemplateBlade)' + } else { + $ResultMarkdown = "❌ **Fail**: Non-admin users can read BitLocker recovery keys for their own devices, which may allow unauthorized access.`n`n" + $ResultMarkdown += '[Restrict access](https://entra.microsoft.com/#view/Microsoft_AAD_IAM/PoliciesTemplateBlade)' + } + + Add-CippTestResult -TestId 'ZTNA21954' -TenantFilter $Tenant -TestType 'ZeroTrustNetworkAccess' -Status $Status ` + -ResultMarkdown $ResultMarkdown ` + -Risk 'Low' -Name 'Restrict non-admin users from reading BitLocker recovery keys' ` + -UserImpact 'Low' -ImplementationEffort 'Low' ` + -Category 'Device security' + + } catch { + Add-CippTestResult -TestId 'ZTNA21954' -TenantFilter $Tenant -TestType 'ZeroTrustNetworkAccess' -Status 'Failed' ` + -ResultMarkdown "❌ **Error**: $($_.Exception.Message)" ` + -Risk 'Low' -Name 'Restrict non-admin users from reading BitLocker recovery keys' ` + -UserImpact 'Low' -ImplementationEffort 'Low' ` + -Category 'Device security' + Write-LogMessage -API 'ZeroTrustNetworkAccess' -tenant $Tenant -message "Test ZTNA21954 failed: $($_.Exception.Message)" -sev Error + } +} diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21955.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21955.ps1 new file mode 100644 index 000000000000..7974185fcc0a --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21955.ps1 @@ -0,0 +1,59 @@ +function Invoke-CippTestZTNA21955 { + <# + .SYNOPSIS + Checks if local administrator management is properly configured on Entra joined devices + + .DESCRIPTION + Verifies that Global Administrators are automatically added as local administrators on + Entra joined devices to enable emergency access and device management. + + .FUNCTIONALITY + Internal + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Tenant + ) + + try { + # Get device registration policy from cache + $DeviceRegPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'DeviceRegistrationPolicy' + + if (-not $DeviceRegPolicy) { + Add-CippTestResult -TestId 'ZTNA21955' -TenantFilter $Tenant -TestType 'ZeroTrustNetworkAccess' -Status 'Skipped' ` + -ResultMarkdown 'Unable to retrieve device registration policy from cache.' ` + -Risk 'Medium' -Name 'Manage local admins on Entra joined devices' ` + -UserImpact 'Low' -ImplementationEffort 'Low' ` + -Category 'Device security' + return + } + + # Check if global admins are added as local admins + $GlobalAdminsEnabled = $DeviceRegPolicy.azureADJoin.localAdmins.enableGlobalAdmins -eq $true + + $Status = if ($GlobalAdminsEnabled) { 'Passed' } else { 'Failed' } + + if ($Status -eq 'Passed') { + $ResultMarkdown = "✅ **Pass**: Global Administrators are automatically added as local administrators on Entra joined devices.`n`n" + $ResultMarkdown += '[Review settings](https://entra.microsoft.com/#view/Microsoft_AAD_Devices/DevicesMenuBlade/~/DeviceSettings/menuId/)' + } else { + $ResultMarkdown = "❌ **Fail**: Global Administrators are not automatically added as local administrators, which may limit emergency access capabilities.`n`n" + $ResultMarkdown += '[Configure settings](https://entra.microsoft.com/#view/Microsoft_AAD_Devices/DevicesMenuBlade/~/DeviceSettings/menuId/)' + } + + Add-CippTestResult -TestId 'ZTNA21955' -TenantFilter $Tenant -TestType 'ZeroTrustNetworkAccess' -Status $Status ` + -ResultMarkdown $ResultMarkdown ` + -Risk 'Medium' -Name 'Manage local admins on Entra joined devices' ` + -UserImpact 'Low' -ImplementationEffort 'Low' ` + -Category 'Device security' + + } catch { + Add-CippTestResult -TestId 'ZTNA21955' -TenantFilter $Tenant -TestType 'ZeroTrustNetworkAccess' -Status 'Failed' ` + -ResultMarkdown "❌ **Error**: $($_.Exception.Message)" ` + -Risk 'Medium' -Name 'Manage local admins on Entra joined devices' ` + -UserImpact 'Low' -ImplementationEffort 'Low' ` + -Category 'Device security' + Write-LogMessage -API 'ZeroTrustNetworkAccess' -tenant $Tenant -message "Test ZTNA21955 failed: $($_.Exception.Message)" -sev Error + } +} diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA22124.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA22124.ps1 new file mode 100644 index 000000000000..ab985dd1aaad --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA22124.ps1 @@ -0,0 +1,75 @@ +function Invoke-CippTestZTNA22124 { + <# + .SYNOPSIS + Checks if all high priority Entra recommendations have been addressed + + .DESCRIPTION + Verifies that there are no active or postponed high priority recommendations in the tenant, + ensuring critical security improvements have been implemented. + + .FUNCTIONALITY + Internal + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Tenant + ) + + try { + # Get directory recommendations from cache + $Recommendations = New-CIPPDbRequest -TenantFilter $Tenant -Type 'DirectoryRecommendations' + + if (-not $Recommendations) { + Add-CippTestResult -TestId 'ZTNA22124' -TenantFilter $Tenant -TestType 'ZeroTrustNetworkAccess' -Status 'Skipped' ` + -ResultMarkdown 'Unable to retrieve directory recommendations from cache.' ` + -Risk 'High' -Name 'Address high priority Entra recommendations' ` + -UserImpact 'Medium' -ImplementationEffort 'Medium' ` + -Category 'Governance' + return + } + + # Filter for high priority recommendations that are active or postponed + $HighPriorityIssues = [System.Collections.Generic.List[object]]::new() + foreach ($rec in $Recommendations) { + if ($rec.priority -eq 'high' -and ($rec.status -eq 'active' -or $rec.status -eq 'postponed')) { + $HighPriorityIssues.Add($rec) + } + } + + $Status = if ($HighPriorityIssues.Count -eq 0) { 'Passed' } else { 'Failed' } + + if ($Status -eq 'Passed') { + $ResultMarkdown = "✅ **Pass**: All high priority Entra recommendations have been addressed.`n`n" + $ResultMarkdown += '[View recommendations](https://entra.microsoft.com/#view/Microsoft_Azure_SecureScore/OverviewBlade)' + } else { + $ResultMarkdown = "❌ **Fail**: There are $($HighPriorityIssues.Count) high priority recommendation(s) that have not been addressed.`n`n" + $ResultMarkdown += "## Outstanding high priority recommendations`n`n" + $ResultMarkdown += "| Display Name | Status | Insights |`n" + $ResultMarkdown += "| :----------- | :----- | :------- |`n" + + foreach ($issue in $HighPriorityIssues) { + $displayName = if ($issue.displayName) { $issue.displayName } else { 'N/A' } + $status = if ($issue.status) { $issue.status } else { 'N/A' } + $insights = if ($issue.insights) { $issue.insights } else { 'N/A' } + $ResultMarkdown += "| $displayName | $status | $insights |`n" + } + + $ResultMarkdown += "`n[Address recommendations](https://entra.microsoft.com/#view/Microsoft_Azure_SecureScore/OverviewBlade)" + } + + Add-CippTestResult -TestId 'ZTNA22124' -TenantFilter $Tenant -TestType 'ZeroTrustNetworkAccess' -Status $Status ` + -ResultMarkdown $ResultMarkdown ` + -Risk 'High' -Name 'Address high priority Entra recommendations' ` + -UserImpact 'Medium' -ImplementationEffort 'Medium' ` + -Category 'Governance' + + } catch { + Add-CippTestResult -TestId 'ZTNA22124' -TenantFilter $Tenant -TestType 'ZeroTrustNetworkAccess' -Status 'Failed' ` + -ResultMarkdown "❌ **Error**: $($_.Exception.Message)" ` + -Risk 'High' -Name 'Address high priority Entra recommendations' ` + -UserImpact 'Medium' -ImplementationEffort 'Medium' ` + -Category 'Governance' + Write-LogMessage -API 'ZeroTrustNetworkAccess' -tenant $Tenant -message "Test ZTNA22124 failed: $($_.Exception.Message)" -sev Error + } +} diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA22659.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA22659.ps1 new file mode 100644 index 000000000000..e982aff18d9d --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA22659.ps1 @@ -0,0 +1,88 @@ +function Invoke-CippTestZTNA22659 { + <# + .SYNOPSIS + Checks if risky workload identity sign-ins have been triaged + + .DESCRIPTION + Verifies that there are no active risky sign-in detections for service principals, + ensuring that compromised workload identities are properly investigated and remediated. + + .FUNCTIONALITY + Internal + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Tenant + ) + + try { + # Get service principal risk detections from cache + $RiskDetections = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ServicePrincipalRiskDetections' + + if (-not $RiskDetections) { + Add-CippTestResult -TestId 'ZTNA22659' -TenantFilter $Tenant -TestType 'ZeroTrustNetworkAccess' -Status 'Skipped' ` + -ResultMarkdown 'Unable to retrieve service principal risk detections from cache.' ` + -Risk 'High' -Name 'Triage risky workload identity sign-ins' ` + -UserImpact 'High' -ImplementationEffort 'Low' ` + -Category 'Identity protection' + return + } + + # Filter for sign-in detections that are at risk + $RiskySignIns = [System.Collections.Generic.List[object]]::new() + foreach ($detection in $RiskDetections) { + if ($detection.activity -eq 'signIn' -and $detection.riskState -eq 'atRisk') { + $RiskySignIns.Add($detection) + } + } + + $Status = if ($RiskySignIns.Count -eq 0) { 'Passed' } else { 'Failed' } + + if ($Status -eq 'Passed') { + $ResultMarkdown = "✅ **Pass**: No risky workload identity sign-ins detected or all have been triaged.`n`n" + $ResultMarkdown += "[View identity protection](https://entra.microsoft.com/#view/Microsoft_AAD_IAM/IdentityProtectionMenuBlade/~/RiskyServicePrincipals)" + } else { + $ResultMarkdown = "❌ **Fail**: There are $($RiskySignIns.Count) risky workload identity sign-in(s) that require investigation.`n`n" + $ResultMarkdown += "## Risky service principal sign-ins`n`n" + $ResultMarkdown += "| Service Principal | App ID | Risk State | Risk Level | Last Updated |`n" + $ResultMarkdown += "| :---------------- | :----- | :--------- | :--------- | :----------- |`n" + + foreach ($signin in $RiskySignIns) { + $spName = if ($signin.servicePrincipalDisplayName) { $signin.servicePrincipalDisplayName } else { 'N/A' } + $appId = if ($signin.appId) { $signin.appId } else { 'N/A' } + $riskState = if ($signin.riskState) { $signin.riskState } else { 'N/A' } + $riskLevel = if ($signin.riskLevel) { $signin.riskLevel } else { 'N/A' } + + # Format last updated date + $lastUpdated = 'N/A' + if ($signin.lastUpdatedDateTime) { + try { + $date = [DateTime]::Parse($signin.lastUpdatedDateTime) + $lastUpdated = $date.ToString('yyyy-MM-dd HH:mm') + } catch { + $lastUpdated = $signin.lastUpdatedDateTime + } + } + + $ResultMarkdown += "| $spName | $appId | $riskState | $riskLevel | $lastUpdated |`n" + } + + $ResultMarkdown += "`n[Investigate and remediate](https://entra.microsoft.com/#view/Microsoft_AAD_IAM/IdentityProtectionMenuBlade/~/RiskyServicePrincipals)" + } + + Add-CippTestResult -TestId 'ZTNA22659' -TenantFilter $Tenant -TestType 'ZeroTrustNetworkAccess' -Status $Status ` + -ResultMarkdown $ResultMarkdown ` + -Risk 'High' -Name 'Triage risky workload identity sign-ins' ` + -UserImpact 'High' -ImplementationEffort 'Low' ` + -Category 'Identity protection' + + } catch { + Add-CippTestResult -TestId 'ZTNA22659' -TenantFilter $Tenant -TestType 'ZeroTrustNetworkAccess' -Status 'Failed' ` + -ResultMarkdown "❌ **Error**: $($_.Exception.Message)" ` + -Risk 'High' -Name 'Triage risky workload identity sign-ins' ` + -UserImpact 'High' -ImplementationEffort 'Low' ` + -Category 'Identity protection' + Write-LogMessage -API 'ZeroTrustNetworkAccess' -tenant $Tenant -message "Test ZTNA22659 failed: $($_.Exception.Message)" -sev Error + } +} diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24570.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24570.ps1 new file mode 100644 index 000000000000..3d9244258d42 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24570.ps1 @@ -0,0 +1,139 @@ +function Invoke-CippTestZTNA24570 { + <# + .SYNOPSIS + Checks if Entra Connect uses a service principal instead of a user account + + .DESCRIPTION + Verifies that if hybrid identity synchronization is enabled (Entra Connect), the + Directory Synchronization Accounts role contains only service principals and not user accounts, + reducing the risk of credential theft. + + .FUNCTIONALITY + Internal + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Tenant + ) + + try { + # Get organization info to check if hybrid identity is enabled + $OrgInfo = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Organization' + + if (-not $OrgInfo) { + Add-CippTestResult -TestId 'ZTNA24570' -TenantFilter $Tenant -TestType 'ZeroTrustNetworkAccess' -Status 'Skipped' ` + -ResultMarkdown 'Unable to retrieve organization information from cache.' ` + -Risk 'High' -Name 'Entra Connect uses a service principal' ` + -UserImpact 'Medium' -ImplementationEffort 'High' ` + -Category 'Access control' + return + } + + # Check if hybrid identity is enabled + $HybridEnabled = $OrgInfo.onPremisesSyncEnabled -eq $true + + if (-not $HybridEnabled) { + Add-CippTestResult -TestId 'ZTNA24570' -TenantFilter $Tenant -TestType 'ZeroTrustNetworkAccess' -Status 'Skipped' ` + -ResultMarkdown '✅ **N/A**: Hybrid identity synchronization is not enabled in this tenant.' ` + -Risk 'High' -Name 'Entra Connect uses a service principal' ` + -UserImpact 'Medium' -ImplementationEffort 'High' ` + -Category 'Access control' + return + } + + # Get roles to find Directory Synchronization Accounts role + $Roles = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Roles' + + if (-not $Roles) { + Add-CippTestResult -TestId 'ZTNA24570' -TenantFilter $Tenant -TestType 'ZeroTrustNetworkAccess' -Status 'Skipped' ` + -ResultMarkdown 'Unable to retrieve roles from cache.' ` + -Risk 'High' -Name 'Entra Connect uses a service principal' ` + -UserImpact 'Medium' -ImplementationEffort 'High' ` + -Category 'Access control' + return + } + + # Find Directory Synchronization Accounts role (roleTemplateId: d29b2b05-8046-44ba-8758-1e26182fcf32) + $DirSyncRole = $null + foreach ($role in $Roles) { + if ($role.roleTemplateId -eq 'd29b2b05-8046-44ba-8758-1e26182fcf32') { + $DirSyncRole = $role + break + } + } + + if (-not $DirSyncRole) { + Add-CippTestResult -TestId 'ZTNA24570' -TenantFilter $Tenant -TestType 'ZeroTrustNetworkAccess' -Status 'Failed' ` + -ResultMarkdown '❌ **Error**: Unable to find Directory Synchronization Accounts role in cache.' ` + -Risk 'High' -Name 'Entra Connect uses a service principal' ` + -UserImpact 'Medium' -ImplementationEffort 'High' ` + -Category 'Access control' + return + } + + # Check role members for enabled user accounts + $EnabledUsers = [System.Collections.Generic.List[object]]::new() + if ($DirSyncRole.members) { + foreach ($member in $DirSyncRole.members) { + # Check if it's a user (not a service principal) and if it's enabled + if ($member.'@odata.type' -eq '#microsoft.graph.user') { + $isEnabled = $member.accountEnabled -eq $true + if ($isEnabled) { + $EnabledUsers.Add([PSCustomObject]@{ + DisplayName = $member.displayName + UserPrincipalName = $member.userPrincipalName + AccountEnabled = $isEnabled + }) + } + } + } + } + + $Status = if ($EnabledUsers.Count -eq 0) { 'Passed' } else { 'Failed' } + + # Build result markdown + $lastSyncDate = if ($OrgInfo.onPremisesLastSyncDateTime) { + try { + $date = [DateTime]::Parse($OrgInfo.onPremisesLastSyncDateTime) + $date.ToString('yyyy-MM-dd HH:mm') + } catch { + $OrgInfo.onPremisesLastSyncDateTime + } + } else { + 'Never' + } + + if ($Status -eq 'Passed') { + $ResultMarkdown = "✅ **Pass**: Hybrid identity is enabled and using a service principal for synchronization.`n`n" + $ResultMarkdown += "**Last Sync**: $lastSyncDate`n`n" + $ResultMarkdown += '[Review configuration](https://entra.microsoft.com/#view/Microsoft_AAD_IAM/RolesManagementMenuBlade/~/AllRoles)' + } else { + $ResultMarkdown = "❌ **Fail**: Hybrid identity is enabled but using $($EnabledUsers.Count) enabled user account(s) for synchronization.`n`n" + $ResultMarkdown += "**Last Sync**: $lastSyncDate`n`n" + $ResultMarkdown += "## Directory Synchronization Accounts role members`n`n" + $ResultMarkdown += "| Display Name | User Principal Name | Enabled |`n" + $ResultMarkdown += "| :----------- | :------------------ | :------ |`n" + + foreach ($user in $EnabledUsers) { + $ResultMarkdown += "| $($user.DisplayName) | $($user.UserPrincipalName) | ✅ Yes |`n" + } + + $ResultMarkdown += "`n[Migrate to service principal](https://entra.microsoft.com/#view/Microsoft_AAD_IAM/RolesManagementMenuBlade/~/AllRoles)" + } + + Add-CippTestResult -TestId 'ZTNA24570' -TenantFilter $Tenant -TestType 'ZeroTrustNetworkAccess' -Status $Status ` + -ResultMarkdown $ResultMarkdown ` + -Risk 'High' -Name 'Entra Connect uses a service principal' ` + -UserImpact 'Medium' -ImplementationEffort 'High' ` + -Category 'Access control' + + } catch { + Add-CippTestResult -TestId 'ZTNA24570' -TenantFilter $Tenant -TestType 'ZeroTrustNetworkAccess' -Status 'Failed' ` + -ResultMarkdown "❌ **Error**: $($_.Exception.Message)" ` + -Risk 'High' -Name 'Entra Connect uses a service principal' ` + -UserImpact 'Medium' -ImplementationEffort 'High' ` + -Category 'Access control' + Write-LogMessage -API 'ZeroTrustNetworkAccess' -tenant $Tenant -message "Test ZTNA24570 failed: $($_.Exception.Message)" -sev Error + } +} diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24824.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24824.ps1 new file mode 100644 index 000000000000..d403677d69fc --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24824.ps1 @@ -0,0 +1,143 @@ +function Invoke-CippTestZTNA24824 { + <# + .SYNOPSIS + Checks if Conditional Access policies block access from noncompliant devices + + .DESCRIPTION + Verifies that enabled Conditional Access policies exist that require device compliance, + covering all platforms (Windows, macOS, iOS, Android) or a policy with no platform filter. + + .FUNCTIONALITY + Internal + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Tenant + ) + + try { + # Get CA policies from cache + $CAPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ConditionalAccessPolicies' + + if (-not $CAPolicies) { + Add-CippTestResult -TestId 'ZTNA24824' -TenantFilter $Tenant -TestType 'ZeroTrustNetworkAccess' -Status 'Skipped' ` + -ResultMarkdown 'Unable to retrieve Conditional Access policies from cache.' ` + -Risk 'High' -Name 'CA policies block access from noncompliant devices' ` + -UserImpact 'Medium' -ImplementationEffort 'Medium' ` + -Category 'Device security' + return + } + + # Filter for enabled policies with compliantDevice control + $CompliantDevicePolicies = [System.Collections.Generic.List[object]]::new() + foreach ($policy in $CAPolicies) { + if ($policy.state -eq 'enabled' -and + $policy.grantControls -and + $policy.grantControls.builtInControls -and + ($policy.grantControls.builtInControls -contains 'compliantDevice')) { + $CompliantDevicePolicies.Add($policy) + } + } + + if ($CompliantDevicePolicies.Count -eq 0) { + Add-CippTestResult -TestId 'ZTNA24824' -TenantFilter $Tenant -TestType 'ZeroTrustNetworkAccess' -Status 'Failed' ` + -ResultMarkdown "❌ **Fail**: No Conditional Access policies found that block access from noncompliant devices.`n`n[Create policies](https://entra.microsoft.com/#view/Microsoft_AAD_ConditionalAccess/ConditionalAccessBlade/~/Policies)" ` + -Risk 'High' -Name 'CA policies block access from noncompliant devices' ` + -UserImpact 'Medium' -ImplementationEffort 'Medium' ` + -Category 'Device security' + return + } + + # Track platform coverage + $PlatformCoverage = @{ + 'windows' = $false + 'macOS' = $false + 'iOS' = $false + 'android' = $false + } + $AllPlatformsPolicy = $false + + $PolicyDetails = [System.Collections.Generic.List[object]]::new() + + foreach ($policy in $CompliantDevicePolicies) { + $platforms = 'All platforms' + + if ($policy.conditions.platforms.includePlatforms) { + if ($policy.conditions.platforms.includePlatforms -contains 'all') { + $AllPlatformsPolicy = $true + $platforms = 'All platforms' + } else { + $platformList = $policy.conditions.platforms.includePlatforms -join ', ' + $platforms = $platformList + + # Track individual platform coverage + foreach ($platform in $policy.conditions.platforms.includePlatforms) { + $lowerPlatform = $platform.ToLower() + if ($PlatformCoverage.ContainsKey($lowerPlatform)) { + $PlatformCoverage[$lowerPlatform] = $true + } + } + } + } else { + # No platform filter = applies to all platforms + $AllPlatformsPolicy = $true + } + + $PolicyDetails.Add([PSCustomObject]@{ + Name = $policy.displayName + Platforms = $platforms + }) + } + + # Check if all platforms are covered (either by a single policy or combination) + $AllCovered = $AllPlatformsPolicy -or ( + $PlatformCoverage['windows'] -and + $PlatformCoverage['macOS'] -and + $PlatformCoverage['iOS'] -and + $PlatformCoverage['android'] + ) + + $Status = if ($AllCovered) { 'Passed' } else { 'Failed' } + + # Build result markdown + if ($Status -eq 'Passed') { + $ResultMarkdown = "✅ **Pass**: Conditional Access policies block noncompliant devices across all platforms.`n`n" + } else { + $ResultMarkdown = "❌ **Fail**: Conditional Access policies do not cover all device platforms.`n`n" + $missingPlatforms = [System.Collections.Generic.List[string]]::new() + foreach ($key in $PlatformCoverage.Keys) { + if (-not $PlatformCoverage[$key]) { + $missingPlatforms.Add($key) + } + } + if ($missingPlatforms.Count -gt 0) { + $ResultMarkdown += "**Missing platform coverage**: $($missingPlatforms -join ', ')`n`n" + } + } + + $ResultMarkdown += "## Compliant device policies`n`n" + $ResultMarkdown += "| Policy Name | Platforms |`n" + $ResultMarkdown += "| :---------- | :-------- |`n" + + foreach ($detail in $PolicyDetails) { + $ResultMarkdown += "| $($detail.Name) | $($detail.Platforms) |`n" + } + + $ResultMarkdown += "`n[Review policies](https://entra.microsoft.com/#view/Microsoft_AAD_ConditionalAccess/ConditionalAccessBlade/~/Policies)" + + Add-CippTestResult -TestId 'ZTNA24824' -TenantFilter $Tenant -TestType 'ZeroTrustNetworkAccess' -Status $Status ` + -ResultMarkdown $ResultMarkdown ` + -Risk 'High' -Name 'CA policies block access from noncompliant devices' ` + -UserImpact 'Medium' -ImplementationEffort 'Medium' ` + -Category 'Device security' + + } catch { + Add-CippTestResult -TestId 'ZTNA24824' -TenantFilter $Tenant -TestType 'ZeroTrustNetworkAccess' -Status 'Failed' ` + -ResultMarkdown "❌ **Error**: $($_.Exception.Message)" ` + -Risk 'High' -Name 'CA policies block access from noncompliant devices' ` + -UserImpact 'Medium' -ImplementationEffort 'Medium' ` + -Category 'Device security' + Write-LogMessage -API 'ZeroTrustNetworkAccess' -tenant $Tenant -message "Test ZTNA24824 failed: $($_.Exception.Message)" -sev Error + } +} diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24827.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24827.ps1 new file mode 100644 index 000000000000..9c687c36de79 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24827.ps1 @@ -0,0 +1,153 @@ +function Invoke-CippTestZTNA24827 { + <# + .SYNOPSIS + Checks if Conditional Access policies block unmanaged mobile apps + + .DESCRIPTION + Verifies that enabled Conditional Access policies exist that require compliant applications + for iOS and Android platforms, preventing unmanaged apps from accessing corporate data. + + .FUNCTIONALITY + Internal + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Tenant + ) + + try { + # Get CA policies from cache + $CAPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ConditionalAccessPolicies' + + if (-not $CAPolicies) { + Add-CippTestResult -TestId 'ZTNA24827' -TenantFilter $Tenant -TestType 'ZeroTrustNetworkAccess' -Status 'Skipped' ` + -ResultMarkdown 'Unable to retrieve Conditional Access policies from cache.' ` + -Risk 'Medium' -Name 'CA policies block unmanaged mobile apps' ` + -UserImpact 'Medium' -ImplementationEffort 'Medium' ` + -Category 'Application security' + return + } + + # Filter for enabled policies with compliantApplication control for mobile platforms + $CompliantAppPolicies = [System.Collections.Generic.List[object]]::new() + foreach ($policy in $CAPolicies) { + if ($policy.state -eq 'enabled' -and + $policy.grantControls -and + $policy.grantControls.builtInControls -and + ($policy.grantControls.builtInControls -contains 'compliantApplication')) { + + # Check if policy applies to iOS or Android + $appliesToMobile = $false + if ($policy.conditions.platforms.includePlatforms) { + if ($policy.conditions.platforms.includePlatforms -contains 'all' -or + $policy.conditions.platforms.includePlatforms -contains 'iOS' -or + $policy.conditions.platforms.includePlatforms -contains 'android') { + $appliesToMobile = $true + } + } else { + # No platform filter = applies to all platforms including mobile + $appliesToMobile = $true + } + + if ($appliesToMobile) { + $CompliantAppPolicies.Add($policy) + } + } + } + + if ($CompliantAppPolicies.Count -eq 0) { + Add-CippTestResult -TestId 'ZTNA24827' -TenantFilter $Tenant -TestType 'ZeroTrustNetworkAccess' -Status 'Failed' ` + -ResultMarkdown "❌ **Fail**: No Conditional Access policies found that block unmanaged mobile apps.`n`n[Create policies](https://entra.microsoft.com/#view/Microsoft_AAD_ConditionalAccess/ConditionalAccessBlade/~/Policies)" ` + -Risk 'Medium' -Name 'CA policies block unmanaged mobile apps' ` + -UserImpact 'Medium' -ImplementationEffort 'Medium' ` + -Category 'Application security' + return + } + + # Track platform coverage for iOS and Android + $PlatformCoverage = @{ + 'iOS' = $false + 'android' = $false + } + $AllPlatformsPolicy = $false + + $PolicyDetails = [System.Collections.Generic.List[object]]::new() + + foreach ($policy in $CompliantAppPolicies) { + $platforms = 'All platforms' + + if ($policy.conditions.platforms.includePlatforms) { + if ($policy.conditions.platforms.includePlatforms -contains 'all') { + $AllPlatformsPolicy = $true + $platforms = 'All platforms' + } else { + $platformList = $policy.conditions.platforms.includePlatforms -join ', ' + $platforms = $platformList + + # Track individual platform coverage + if ($policy.conditions.platforms.includePlatforms -contains 'iOS') { + $PlatformCoverage['iOS'] = $true + } + if ($policy.conditions.platforms.includePlatforms -contains 'android') { + $PlatformCoverage['android'] = $true + } + } + } else { + # No platform filter = applies to all platforms + $AllPlatformsPolicy = $true + } + + $PolicyDetails.Add([PSCustomObject]@{ + Name = $policy.displayName + Platforms = $platforms + }) + } + + # Check if both iOS and Android are covered + $BothCovered = $AllPlatformsPolicy -or ($PlatformCoverage['iOS'] -and $PlatformCoverage['android']) + + $Status = if ($BothCovered) { 'Passed' } else { 'Failed' } + + # Build result markdown + if ($Status -eq 'Passed') { + $ResultMarkdown = "✅ **Pass**: Conditional Access policies block unmanaged apps on both iOS and Android platforms.`n`n" + } else { + $ResultMarkdown = "❌ **Fail**: Conditional Access policies do not cover all mobile platforms.`n`n" + $missingPlatforms = [System.Collections.Generic.List[string]]::new() + if (-not $PlatformCoverage['iOS']) { + $missingPlatforms.Add('iOS') + } + if (-not $PlatformCoverage['android']) { + $missingPlatforms.Add('android') + } + if ($missingPlatforms.Count -gt 0) { + $ResultMarkdown += "**Missing platform coverage**: $($missingPlatforms -join ', ')`n`n" + } + } + + $ResultMarkdown += "## Compliant application policies`n`n" + $ResultMarkdown += "| Policy Name | Platforms |`n" + $ResultMarkdown += "| :---------- | :-------- |`n" + + foreach ($detail in $PolicyDetails) { + $ResultMarkdown += "| $($detail.Name) | $($detail.Platforms) |`n" + } + + $ResultMarkdown += "`n[Review policies](https://entra.microsoft.com/#view/Microsoft_AAD_ConditionalAccess/ConditionalAccessBlade/~/Policies)" + + Add-CippTestResult -TestId 'ZTNA24827' -TenantFilter $Tenant -TestType 'ZeroTrustNetworkAccess' -Status $Status ` + -ResultMarkdown $ResultMarkdown ` + -Risk 'Medium' -Name 'CA policies block unmanaged mobile apps' ` + -UserImpact 'Medium' -ImplementationEffort 'Medium' ` + -Category 'Application security' + + } catch { + Add-CippTestResult -TestId 'ZTNA24827' -TenantFilter $Tenant -TestType 'ZeroTrustNetworkAccess' -Status 'Failed' ` + -ResultMarkdown "❌ **Error**: $($_.Exception.Message)" ` + -Risk 'Medium' -Name 'CA policies block unmanaged mobile apps' ` + -UserImpact 'Medium' -ImplementationEffort 'Medium' ` + -Category 'Application security' + Write-LogMessage -API 'ZeroTrustNetworkAccess' -tenant $Tenant -message "Test ZTNA24827 failed: $($_.Exception.Message)" -sev Error + } +} From eaae2d8c400b8cf1fc59764bcf2649a062c9f32d Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Wed, 24 Dec 2025 01:55:25 +0100 Subject: [PATCH 030/503] Added caches --- .../Push-CIPPDBCacheData.ps1 | 52 +++++++++++++++++++ .../Set-CIPPDBCacheAppRoleAssignments.ps1 | 48 +++++++++++++++++ ...CacheCredentialUserRegistrationDetails.ps1 | 30 +++++++++++ .../Set-CIPPDBCacheExoAcceptedDomains.ps1 | 30 +++++++++++ .../Set-CIPPDBCacheExoAntiPhishPolicies.ps1 | 39 ++++++++++++++ .../Set-CIPPDBCacheExoDkimSigningConfig.ps1 | 30 +++++++++++ ...et-CIPPDBCacheExoMalwareFilterPolicies.ps1 | 39 ++++++++++++++ .../Set-CIPPDBCacheExoOrganizationConfig.ps1 | 32 ++++++++++++ ...t-CIPPDBCacheExoSafeAttachmentPolicies.ps1 | 39 ++++++++++++++ .../Set-CIPPDBCacheExoSafeLinksPolicies.ps1 | 39 ++++++++++++++ .../Set-CIPPDBCacheExoTransportRules.ps1 | 30 +++++++++++ ...PPDBCacheManagedDeviceEncryptionStates.ps1 | 30 +++++++++++ .../Set-CIPPDBCacheOAuth2PermissionGrants.ps1 | 30 +++++++++++ .../Public/Set-CIPPDBCacheSecureScore.ps1 | 14 ++++- ...Set-CIPPDBCacheUserRegistrationDetails.ps1 | 30 +++++++++++ .../Public/Tests/Invoke-CippTestZTNA21849.ps1 | 47 ----------------- 16 files changed, 510 insertions(+), 49 deletions(-) create mode 100644 Modules/CIPPCore/Public/Set-CIPPDBCacheAppRoleAssignments.ps1 create mode 100644 Modules/CIPPCore/Public/Set-CIPPDBCacheCredentialUserRegistrationDetails.ps1 create mode 100644 Modules/CIPPCore/Public/Set-CIPPDBCacheExoAcceptedDomains.ps1 create mode 100644 Modules/CIPPCore/Public/Set-CIPPDBCacheExoAntiPhishPolicies.ps1 create mode 100644 Modules/CIPPCore/Public/Set-CIPPDBCacheExoDkimSigningConfig.ps1 create mode 100644 Modules/CIPPCore/Public/Set-CIPPDBCacheExoMalwareFilterPolicies.ps1 create mode 100644 Modules/CIPPCore/Public/Set-CIPPDBCacheExoOrganizationConfig.ps1 create mode 100644 Modules/CIPPCore/Public/Set-CIPPDBCacheExoSafeAttachmentPolicies.ps1 create mode 100644 Modules/CIPPCore/Public/Set-CIPPDBCacheExoSafeLinksPolicies.ps1 create mode 100644 Modules/CIPPCore/Public/Set-CIPPDBCacheExoTransportRules.ps1 create mode 100644 Modules/CIPPCore/Public/Set-CIPPDBCacheManagedDeviceEncryptionStates.ps1 create mode 100644 Modules/CIPPCore/Public/Set-CIPPDBCacheOAuth2PermissionGrants.ps1 create mode 100644 Modules/CIPPCore/Public/Set-CIPPDBCacheUserRegistrationDetails.ps1 diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 index 9f1a43004bf8..7267e9c4945c 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 @@ -146,6 +146,58 @@ function Push-CIPPDBCacheData { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "DeviceRegistrationPolicy collection failed: $($_.Exception.Message)" -sev Error } + try { Set-CIPPDBCacheCredentialUserRegistrationDetails -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "CredentialUserRegistrationDetails collection failed: $($_.Exception.Message)" -sev Error + } + + try { Set-CIPPDBCacheUserRegistrationDetails -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "UserRegistrationDetails collection failed: $($_.Exception.Message)" -sev Error + } + + try { Set-CIPPDBCacheManagedDeviceEncryptionStates -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ManagedDeviceEncryptionStates collection failed: $($_.Exception.Message)" -sev Error + } + + try { Set-CIPPDBCacheOAuth2PermissionGrants -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "OAuth2PermissionGrants collection failed: $($_.Exception.Message)" -sev Error + } + + try { Set-CIPPDBCacheAppRoleAssignments -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "AppRoleAssignments collection failed: $($_.Exception.Message)" -sev Error + } + + try { Set-CIPPDBCacheExoAntiPhishPolicies -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoAntiPhishPolicies collection failed: $($_.Exception.Message)" -sev Error + } + + try { Set-CIPPDBCacheExoMalwareFilterPolicies -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoMalwareFilterPolicies collection failed: $($_.Exception.Message)" -sev Error + } + + try { Set-CIPPDBCacheExoSafeLinksPolicies -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoSafeLinksPolicies collection failed: $($_.Exception.Message)" -sev Error + } + + try { Set-CIPPDBCacheExoSafeAttachmentPolicies -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoSafeAttachmentPolicies collection failed: $($_.Exception.Message)" -sev Error + } + + try { Set-CIPPDBCacheExoTransportRules -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoTransportRules collection failed: $($_.Exception.Message)" -sev Error + } + + try { Set-CIPPDBCacheExoDkimSigningConfig -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoDkimSigningConfig collection failed: $($_.Exception.Message)" -sev Error + } + + try { Set-CIPPDBCacheExoOrganizationConfig -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoOrganizationConfig collection failed: $($_.Exception.Message)" -sev Error + } + + try { Set-CIPPDBCacheExoAcceptedDomains -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoAcceptedDomains collection failed: $($_.Exception.Message)" -sev Error + } + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Completed database cache collection for tenant' -sev Info } catch { diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheAppRoleAssignments.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheAppRoleAssignments.ps1 new file mode 100644 index 000000000000..a96a6f9f1923 --- /dev/null +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheAppRoleAssignments.ps1 @@ -0,0 +1,48 @@ +function Set-CIPPDBCacheAppRoleAssignments { + <# + .SYNOPSIS + Caches application role assignments for a tenant + + .PARAMETER TenantFilter + The tenant to cache app role assignments for + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter + ) + + try { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching app role assignments' -sev Info + + # Get all service principals first + $ServicePrincipals = New-GraphGetRequest -uri 'https://graph.microsoft.com/v1.0/servicePrincipals?$select=id,appId,displayName&$top=999' -tenantid $TenantFilter + + $AllAppRoleAssignments = [System.Collections.Generic.List[object]]::new() + + foreach ($SP in $ServicePrincipals) { + try { + $AppRoleAssignments = New-GraphGetRequest -uri "https://graph.microsoft.com/v1.0/servicePrincipals/$($SP.id)/appRoleAssignments?`$top=999" -tenantid $TenantFilter + + foreach ($Assignment in $AppRoleAssignments) { + # Enrich with service principal info + $Assignment | Add-Member -NotePropertyName 'servicePrincipalDisplayName' -NotePropertyValue $SP.displayName -Force + $Assignment | Add-Member -NotePropertyName 'servicePrincipalAppId' -NotePropertyValue $SP.appId -Force + $AllAppRoleAssignments.Add($Assignment) + } + } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to get app role assignments for $($SP.displayName): $($_.Exception.Message)" -sev Warning + } + } + + if ($AllAppRoleAssignments.Count -gt 0) { + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'AppRoleAssignments' -Data $AllAppRoleAssignments + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'AppRoleAssignments' -Data $AllAppRoleAssignments -Count + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($AllAppRoleAssignments.Count) app role assignments" -sev Info + } + $AllAppRoleAssignments = $null + + } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache app role assignments: $($_.Exception.Message)" -sev Error + } +} diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheCredentialUserRegistrationDetails.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheCredentialUserRegistrationDetails.ps1 new file mode 100644 index 000000000000..861d0c25a50c --- /dev/null +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheCredentialUserRegistrationDetails.ps1 @@ -0,0 +1,30 @@ +function Set-CIPPDBCacheCredentialUserRegistrationDetails { + <# + .SYNOPSIS + Caches MFA and SSPR registration details for all users in a tenant + + .PARAMETER TenantFilter + The tenant to cache credential user registration details for + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter + ) + + try { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching credential user registration details' -sev Info + + $CredentialUserRegistrationDetails = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/reports/credentialUserRegistrationDetails' -tenantid $TenantFilter + + if ($CredentialUserRegistrationDetails) { + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'CredentialUserRegistrationDetails' -Data $CredentialUserRegistrationDetails + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'CredentialUserRegistrationDetails' -Data $CredentialUserRegistrationDetails -Count + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($CredentialUserRegistrationDetails.Count) credential user registration details" -sev Info + } + $CredentialUserRegistrationDetails = $null + + } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache credential user registration details: $($_.Exception.Message)" -sev Error + } +} diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoAcceptedDomains.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoAcceptedDomains.ps1 new file mode 100644 index 000000000000..31e57744c336 --- /dev/null +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoAcceptedDomains.ps1 @@ -0,0 +1,30 @@ +function Set-CIPPDBCacheExoAcceptedDomains { + <# + .SYNOPSIS + Caches Exchange Online Accepted Domains + + .PARAMETER TenantFilter + The tenant to cache accepted domains for + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter + ) + + try { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching Exchange Accepted Domains' -sev Info + + $AcceptedDomains = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-AcceptedDomain' + + if ($AcceptedDomains) { + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoAcceptedDomains' -Data $AcceptedDomains + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoAcceptedDomains' -Data $AcceptedDomains -Count + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($AcceptedDomains.Count) Accepted Domains" -sev Info + } + $AcceptedDomains = $null + + } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache Accepted Domains: $($_.Exception.Message)" -sev Error + } +} diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoAntiPhishPolicies.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoAntiPhishPolicies.ps1 new file mode 100644 index 000000000000..cfd709c4b04b --- /dev/null +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoAntiPhishPolicies.ps1 @@ -0,0 +1,39 @@ +function Set-CIPPDBCacheExoAntiPhishPolicies { + <# + .SYNOPSIS + Caches Exchange Online Anti-Phishing policies and rules + + .PARAMETER TenantFilter + The tenant to cache Anti-Phishing data for + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter + ) + + try { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching Exchange Anti-Phishing policies and rules' -sev Info + + # Get Anti-Phishing policies + $AntiPhishPolicies = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-AntiPhishPolicy' + if ($AntiPhishPolicies) { + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoAntiPhishPolicies' -Data $AntiPhishPolicies + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoAntiPhishPolicies' -Data $AntiPhishPolicies -Count + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($AntiPhishPolicies.Count) Anti-Phishing policies" -sev Info + } + $AntiPhishPolicies = $null + + # Get Anti-Phishing rules + $AntiPhishRules = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-AntiPhishRule' + if ($AntiPhishRules) { + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoAntiPhishRules' -Data $AntiPhishRules + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoAntiPhishRules' -Data $AntiPhishRules -Count + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($AntiPhishRules.Count) Anti-Phishing rules" -sev Info + } + $AntiPhishRules = $null + + } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache Anti-Phishing data: $($_.Exception.Message)" -sev Error + } +} diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoDkimSigningConfig.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoDkimSigningConfig.ps1 new file mode 100644 index 000000000000..161d582f3fe5 --- /dev/null +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoDkimSigningConfig.ps1 @@ -0,0 +1,30 @@ +function Set-CIPPDBCacheExoDkimSigningConfig { + <# + .SYNOPSIS + Caches Exchange Online DKIM signing configuration + + .PARAMETER TenantFilter + The tenant to cache DKIM configuration for + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter + ) + + try { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching Exchange DKIM signing configuration' -sev Info + + $DkimConfig = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-DkimSigningConfig' + + if ($DkimConfig) { + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoDkimSigningConfig' -Data $DkimConfig + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoDkimSigningConfig' -Data $DkimConfig -Count + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($DkimConfig.Count) DKIM configurations" -sev Info + } + $DkimConfig = $null + + } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache DKIM configuration: $($_.Exception.Message)" -sev Error + } +} diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoMalwareFilterPolicies.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoMalwareFilterPolicies.ps1 new file mode 100644 index 000000000000..025401125b02 --- /dev/null +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoMalwareFilterPolicies.ps1 @@ -0,0 +1,39 @@ +function Set-CIPPDBCacheExoMalwareFilterPolicies { + <# + .SYNOPSIS + Caches Exchange Online Malware Filter policies and rules + + .PARAMETER TenantFilter + The tenant to cache Malware Filter data for + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter + ) + + try { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching Exchange Malware Filter policies and rules' -sev Info + + # Get Malware Filter policies + $MalwarePolicies = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-MalwareFilterPolicy' + if ($MalwarePolicies) { + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoMalwareFilterPolicies' -Data $MalwarePolicies + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoMalwareFilterPolicies' -Data $MalwarePolicies -Count + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($MalwarePolicies.Count) Malware Filter policies" -sev Info + } + $MalwarePolicies = $null + + # Get Malware Filter rules + $MalwareRules = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-MalwareFilterRule' + if ($MalwareRules) { + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoMalwareFilterRules' -Data $MalwareRules + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoMalwareFilterRules' -Data $MalwareRules -Count + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($MalwareRules.Count) Malware Filter rules" -sev Info + } + $MalwareRules = $null + + } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache Malware Filter data: $($_.Exception.Message)" -sev Error + } +} diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoOrganizationConfig.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoOrganizationConfig.ps1 new file mode 100644 index 000000000000..d0733cac780a --- /dev/null +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoOrganizationConfig.ps1 @@ -0,0 +1,32 @@ +function Set-CIPPDBCacheExoOrganizationConfig { + <# + .SYNOPSIS + Caches Exchange Online Organization Configuration + + .PARAMETER TenantFilter + The tenant to cache organization configuration for + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter + ) + + try { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching Exchange Organization configuration' -sev Info + + $OrgConfig = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-OrganizationConfig' + + if ($OrgConfig) { + # OrganizationConfig returns a single object, wrap in array for consistency + $OrgConfigArray = @($OrgConfig) + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoOrganizationConfig' -Data $OrgConfigArray + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoOrganizationConfig' -Data $OrgConfigArray -Count + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached Exchange Organization configuration' -sev Info + } + $OrgConfig = $null + + } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache Organization configuration: $($_.Exception.Message)" -sev Error + } +} diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoSafeAttachmentPolicies.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoSafeAttachmentPolicies.ps1 new file mode 100644 index 000000000000..e49576235842 --- /dev/null +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoSafeAttachmentPolicies.ps1 @@ -0,0 +1,39 @@ +function Set-CIPPDBCacheExoSafeAttachmentPolicies { + <# + .SYNOPSIS + Caches Exchange Online Safe Attachment policies and rules + + .PARAMETER TenantFilter + The tenant to cache Safe Attachment data for + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter + ) + + try { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching Exchange Safe Attachment policies and rules' -sev Info + + # Get Safe Attachment policies + $SafeAttachmentPolicies = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-SafeAttachmentPolicy' + if ($SafeAttachmentPolicies) { + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoSafeAttachmentPolicies' -Data $SafeAttachmentPolicies + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoSafeAttachmentPolicies' -Data $SafeAttachmentPolicies -Count + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($SafeAttachmentPolicies.Count) Safe Attachment policies" -sev Info + } + $SafeAttachmentPolicies = $null + + # Get Safe Attachment rules + $SafeAttachmentRules = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-SafeAttachmentRule' + if ($SafeAttachmentRules) { + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoSafeAttachmentRules' -Data $SafeAttachmentRules + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoSafeAttachmentRules' -Data $SafeAttachmentRules -Count + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($SafeAttachmentRules.Count) Safe Attachment rules" -sev Info + } + $SafeAttachmentRules = $null + + } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache Safe Attachment data: $($_.Exception.Message)" -sev Error + } +} diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoSafeLinksPolicies.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoSafeLinksPolicies.ps1 new file mode 100644 index 000000000000..33276c8fb993 --- /dev/null +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoSafeLinksPolicies.ps1 @@ -0,0 +1,39 @@ +function Set-CIPPDBCacheExoSafeLinksPolicies { + <# + .SYNOPSIS + Caches Exchange Online Safe Links policies and rules + + .PARAMETER TenantFilter + The tenant to cache Safe Links data for + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter + ) + + try { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching Exchange Safe Links policies and rules' -sev Info + + # Get Safe Links policies + $SafeLinksPolicies = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-SafeLinksPolicy' + if ($SafeLinksPolicies) { + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoSafeLinksPolicies' -Data $SafeLinksPolicies + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoSafeLinksPolicies' -Data $SafeLinksPolicies -Count + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($SafeLinksPolicies.Count) Safe Links policies" -sev Info + } + $SafeLinksPolicies = $null + + # Get Safe Links rules + $SafeLinksRules = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-SafeLinksRule' + if ($SafeLinksRules) { + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoSafeLinksRules' -Data $SafeLinksRules + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoSafeLinksRules' -Data $SafeLinksRules -Count + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($SafeLinksRules.Count) Safe Links rules" -sev Info + } + $SafeLinksRules = $null + + } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache Safe Links data: $($_.Exception.Message)" -sev Error + } +} diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoTransportRules.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoTransportRules.ps1 new file mode 100644 index 000000000000..5328e63b99af --- /dev/null +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoTransportRules.ps1 @@ -0,0 +1,30 @@ +function Set-CIPPDBCacheExoTransportRules { + <# + .SYNOPSIS + Caches Exchange Online Transport Rules (Mail Flow Rules) + + .PARAMETER TenantFilter + The tenant to cache Transport Rules for + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter + ) + + try { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching Exchange Transport Rules' -sev Info + + $TransportRules = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-TransportRule' + + if ($TransportRules) { + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoTransportRules' -Data $TransportRules + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoTransportRules' -Data $TransportRules -Count + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($TransportRules.Count) Transport Rules" -sev Info + } + $TransportRules = $null + + } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache Transport Rules: $($_.Exception.Message)" -sev Error + } +} diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheManagedDeviceEncryptionStates.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheManagedDeviceEncryptionStates.ps1 new file mode 100644 index 000000000000..9ab751d41ce0 --- /dev/null +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheManagedDeviceEncryptionStates.ps1 @@ -0,0 +1,30 @@ +function Set-CIPPDBCacheManagedDeviceEncryptionStates { + <# + .SYNOPSIS + Caches encryption states (BitLocker/FileVault) for managed devices in a tenant + + .PARAMETER TenantFilter + The tenant to cache managed device encryption states for + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter + ) + + try { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching managed device encryption states' -sev Info + + $ManagedDeviceEncryptionStates = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/deviceManagement/managedDeviceEncryptionStates?$top=999' -tenantid $TenantFilter + + if ($ManagedDeviceEncryptionStates) { + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ManagedDeviceEncryptionStates' -Data $ManagedDeviceEncryptionStates + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ManagedDeviceEncryptionStates' -Data $ManagedDeviceEncryptionStates -Count + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($ManagedDeviceEncryptionStates.Count) managed device encryption states" -sev Info + } + $ManagedDeviceEncryptionStates = $null + + } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache managed device encryption states: $($_.Exception.Message)" -sev Error + } +} diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheOAuth2PermissionGrants.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheOAuth2PermissionGrants.ps1 new file mode 100644 index 000000000000..3c4da6f7d188 --- /dev/null +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheOAuth2PermissionGrants.ps1 @@ -0,0 +1,30 @@ +function Set-CIPPDBCacheOAuth2PermissionGrants { + <# + .SYNOPSIS + Caches OAuth2 permission grants (delegated permissions) for a tenant + + .PARAMETER TenantFilter + The tenant to cache OAuth2 permission grants for + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter + ) + + try { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching OAuth2 permission grants' -sev Info + + $OAuth2PermissionGrants = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/oauth2PermissionGrants?$top=999' -tenantid $TenantFilter + + if ($OAuth2PermissionGrants) { + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'OAuth2PermissionGrants' -Data $OAuth2PermissionGrants + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'OAuth2PermissionGrants' -Data $OAuth2PermissionGrants -Count + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($OAuth2PermissionGrants.Count) OAuth2 permission grants" -sev Info + } + $OAuth2PermissionGrants = $null + + } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache OAuth2 permission grants: $($_.Exception.Message)" -sev Error + } +} diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheSecureScore.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheSecureScore.ps1 index 393ebd058fc4..b1db9a6c1234 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheSecureScore.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheSecureScore.ps1 @@ -1,7 +1,7 @@ function Set-CIPPDBCacheSecureScore { <# .SYNOPSIS - Caches secure score history (last 14 days) for a tenant + Caches secure score history (last 14 days) and control profiles for a tenant .PARAMETER TenantFilter The tenant to cache secure score for @@ -14,10 +14,20 @@ function Set-CIPPDBCacheSecureScore { try { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching secure score' -sev Info + + # Cache secure score history (last 14 days) $SecureScore = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/security/secureScores?$top=14' -tenantid $TenantFilter -noPagination $true Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'SecureScore' -Data $SecureScore + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'SecureScore' -Data $SecureScore -Count $SecureScore = $null - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached secure score successfully' -sev Info + + # Cache secure score control profiles + $SecureScoreControlProfiles = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/security/secureScoreControlProfiles' -tenantid $TenantFilter + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'SecureScoreControlProfiles' -Data $SecureScoreControlProfiles + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'SecureScoreControlProfiles' -Data $SecureScoreControlProfiles -Count + $SecureScoreControlProfiles = $null + + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached secure score and control profiles successfully' -sev Info } catch { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache secure score: $($_.Exception.Message)" -sev Error diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheUserRegistrationDetails.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheUserRegistrationDetails.ps1 new file mode 100644 index 000000000000..51ff2b467e55 --- /dev/null +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheUserRegistrationDetails.ps1 @@ -0,0 +1,30 @@ +function Set-CIPPDBCacheUserRegistrationDetails { + <# + .SYNOPSIS + Caches authentication method registration details for all users in a tenant + + .PARAMETER TenantFilter + The tenant to cache user registration details for + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter + ) + + try { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching user registration details' -sev Info + + $UserRegistrationDetails = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/reports/authenticationMethods/userRegistrationDetails' -tenantid $TenantFilter + + if ($UserRegistrationDetails) { + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'UserRegistrationDetails' -Data $UserRegistrationDetails + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'UserRegistrationDetails' -Data $UserRegistrationDetails -Count + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($UserRegistrationDetails.Count) user registration details" -sev Info + } + $UserRegistrationDetails = $null + + } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache user registration details: $($_.Exception.Message)" -sev Error + } +} diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21849.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21849.ps1 index b84e45f7a431..3457963c01b9 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21849.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21849.ps1 @@ -55,50 +55,3 @@ function Invoke-CippTestZTNA21849 { Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Error running test: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'Smart lockout duration is set to a minimum of 60' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Credential management' } } - - $passwordRuleSettings = $groupSettings | Where-Object { $_.displayName -eq 'Password Rule Settings' } - - $passed = 'Passed' - $testResultMarkdown = '' - - if ($null -eq $passwordRuleSettings) { - $mdInfo = "`n## Smart Lockout Settings`n`n" - $mdInfo += "| Setting | Value |`n" - $mdInfo += "| :---- | :---- |`n" - $mdInfo += "| Lockout Duration (seconds) | 60 (Default) |`n" - - $testResultMarkdown = "Smart Lockout duration is configured to 60 seconds or higher.$mdInfo" - } else { - $lockoutDurationSetting = $passwordRuleSettings.values | Where-Object { $_.name -eq 'LockoutDurationInSeconds' } - - if ($null -eq $lockoutDurationSetting) { - $mdInfo = "`n## Smart Lockout Settings`n`n" - $mdInfo += "| Setting | Value |`n" - $mdInfo += "| :---- | :---- |`n" - $mdInfo += "| Lockout Duration (seconds) | 60 (Default) |`n" - - $testResultMarkdown = "Smart Lockout duration is configured to 60 seconds or higher.$mdInfo" - } else { - $lockoutDuration = [int]$lockoutDurationSetting.value - - $mdInfo = "`n## Smart Lockout Settings`n`n" - $mdInfo += "| Setting | Value |`n" - $mdInfo += "| :---- | :---- |`n" - $mdInfo += "| Lockout Duration (seconds) | $lockoutDuration |`n" - - if ($lockoutDuration -ge 60) { - $testResultMarkdown = "Smart Lockout duration is configured to 60 seconds or higher.$mdInfo" - } else { - $passed = 'Failed' - $testResultMarkdown = "Smart Lockout duration is configured below 60 seconds.$mdInfo" - } - } - } - - Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21849' -TestType 'Identity' -Status $passed -ResultMarkdown $testResultMarkdown -Risk 'Medium' -Name 'Smart lockout duration is set to a minimum of 60' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Credential Management' - } catch { - $ErrorMessage = Get-CippException -Exception $_ - Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21849' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'Smart lockout duration is set to a minimum of 60' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Credential Management' - } -} From 90a084fd711076c925497760d940da48a5dd1de3 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Wed, 24 Dec 2025 02:02:16 +0100 Subject: [PATCH 031/503] Named locations --- .../Public/Tests/Invoke-CippTestZTNA21865.ps1 | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21865.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21865.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21865.ps1 new file mode 100644 index 000000000000..a321f658be00 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21865.ps1 @@ -0,0 +1,48 @@ +function Invoke-CippTestZTNA21865 { + param($Tenant) + + $TestId = 'ZTNA21865' + + try { + $NamedLocations = New-CIPPDbRequest -TenantFilter $Tenant -Type 'NamedLocations' + + if (-not $NamedLocations) { + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Named locations not found in database' -Risk 'Medium' -Name 'Named locations are configured' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Application management' + return + } + + $TrustedLocations = @($NamedLocations | Where-Object { $_.isTrusted -eq $true }) + $Passed = $TrustedLocations.Count -gt 0 + + if ($Passed) { + $ResultMarkdown = "✅ Trusted named locations are configured.`n`n" + } else { + $ResultMarkdown = "❌ No trusted named locations configured.`n`n" + } + + $ResultMarkdown += "## Named Locations`n`n" + $ResultMarkdown += "$($NamedLocations.Count) named locations found.`n`n" + + if ($NamedLocations.Count -gt 0) { + $ResultMarkdown += "| Name | Type | Trusted |`n" + $ResultMarkdown += "| :--- | :--- | :------ |`n" + + foreach ($Location in $NamedLocations) { + $Name = $Location.displayName + $Type = if ($Location.'@odata.type' -eq '#microsoft.graph.ipNamedLocation') { 'IP-based' } + elseif ($Location.'@odata.type' -eq '#microsoft.graph.countryNamedLocation') { 'Country-based' } + else { 'Unknown' } + $Trusted = if ($Location.isTrusted) { 'Yes' } else { 'No' } + $ResultMarkdown += "| $Name | $Type | $Trusted |`n" + } + } + + $Status = if ($Passed) { 'Passed' } else { 'Failed' } + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status $Status -ResultMarkdown $ResultMarkdown -Risk 'Medium' -Name 'Named locations are configured' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Application management' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Error running test: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'Named locations are configured' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Application management' + } +} From 1ef2c3044eb0b993265710ee5a8ef7aa562875f5 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Wed, 24 Dec 2025 02:08:24 +0100 Subject: [PATCH 032/503] NEw tests, not tested --- .../Push-CIPPDBCacheData.ps1 | 4 ++ ...CIPPDBCacheIntuneAppProtectionPolicies.ps1 | 39 +++++++++++++++ .../Public/Tests/Invoke-CippTestZTNA21865.ps1 | 4 +- .../Public/Tests/Invoke-CippTestZTNA21964.ps1 | 38 +++++++++++++++ .../Public/Tests/Invoke-CippTestZTNA24545.ps1 | 42 ++++++++++++++++ .../Public/Tests/Invoke-CippTestZTNA24547.ps1 | 42 ++++++++++++++++ .../Public/Tests/Invoke-CippTestZTNA24548.ps1 | 40 ++++++++++++++++ .../Public/Tests/Invoke-CippTestZTNA24549.ps1 | 40 ++++++++++++++++ .../Public/Tests/Invoke-CippTestZTNA24553.ps1 | 48 +++++++++++++++++++ 9 files changed, 295 insertions(+), 2 deletions(-) create mode 100644 Modules/CIPPCore/Public/Set-CIPPDBCacheIntuneAppProtectionPolicies.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21964.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24545.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24547.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24548.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24549.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24553.ps1 diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 index 7267e9c4945c..3ee632663761 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 @@ -198,6 +198,10 @@ function Push-CIPPDBCacheData { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoAcceptedDomains collection failed: $($_.Exception.Message)" -sev Error } + try { Set-CIPPDBCacheIntuneAppProtectionPolicies -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "IntuneAppProtectionPolicies collection failed: $($_.Exception.Message)" -sev Error + } + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Completed database cache collection for tenant' -sev Info } catch { diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheIntuneAppProtectionPolicies.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheIntuneAppProtectionPolicies.ps1 new file mode 100644 index 000000000000..3330ad5f8255 --- /dev/null +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheIntuneAppProtectionPolicies.ps1 @@ -0,0 +1,39 @@ +function Set-CIPPDBCacheIntuneAppProtectionPolicies { + <# + .SYNOPSIS + Caches Intune App Protection Policies + + .PARAMETER TenantFilter + The tenant to cache app protection policies for + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter + ) + + try { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching Intune App Protection Policies' -sev Info + + # iOS Managed App Protection Policies + $IosPolicies = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/deviceAppManagement/iosManagedAppProtections?$expand=assignments' -tenantid $TenantFilter + if ($IosPolicies) { + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'IntuneIosAppProtectionPolicies' -Data $IosPolicies + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'IntuneIosAppProtectionPolicies' -Data $IosPolicies -Count + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($IosPolicies.Count) iOS app protection policies" -sev Info + } + $IosPolicies = $null + + # Android Managed App Protection Policies + $AndroidPolicies = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/deviceAppManagement/androidManagedAppProtections?$expand=assignments' -tenantid $TenantFilter + if ($AndroidPolicies) { + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'IntuneAndroidAppProtectionPolicies' -Data $AndroidPolicies + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'IntuneAndroidAppProtectionPolicies' -Data $AndroidPolicies -Count + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($AndroidPolicies.Count) Android app protection policies" -sev Info + } + $AndroidPolicies = $null + + } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache App Protection Policies: $($_.Exception.Message)" -sev Error + } +} diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21865.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21865.ps1 index a321f658be00..f88631e35847 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21865.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21865.ps1 @@ -30,8 +30,8 @@ function Invoke-CippTestZTNA21865 { foreach ($Location in $NamedLocations) { $Name = $Location.displayName $Type = if ($Location.'@odata.type' -eq '#microsoft.graph.ipNamedLocation') { 'IP-based' } - elseif ($Location.'@odata.type' -eq '#microsoft.graph.countryNamedLocation') { 'Country-based' } - else { 'Unknown' } + elseif ($Location.'@odata.type' -eq '#microsoft.graph.countryNamedLocation') { 'Country-based' } + else { 'Unknown' } $Trusted = if ($Location.isTrusted) { 'Yes' } else { 'No' } $ResultMarkdown += "| $Name | $Type | $Trusted |`n" } diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21964.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21964.ps1 new file mode 100644 index 000000000000..b39f7bf8347f --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21964.ps1 @@ -0,0 +1,38 @@ +function Invoke-CippTestZTNA21964 { + param($Tenant) + + $TestId = 'ZTNA21964' + + try { + $AuthStrengths = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationStrengths' + + if (-not $AuthStrengths) { + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Authentication strength policies not found in database' -Risk 'High' -Name 'Enable protected actions to secure Conditional Access policy creation and changes' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Access control' + return + } + + $BuiltInStrengths = @($AuthStrengths | Where-Object { $_.policyType -eq 'builtIn' }) + $CustomStrengths = @($AuthStrengths | Where-Object { $_.policyType -eq 'custom' }) + + $ResultMarkdown = "## Authentication Strength Policies`n`n" + $ResultMarkdown += "Found $($AuthStrengths.Count) authentication strength policies ($($BuiltInStrengths.Count) built-in, $($CustomStrengths.Count) custom).`n`n" + + if ($CustomStrengths.Count -gt 0) { + $ResultMarkdown += "### Custom Authentication Strengths`n`n" + $ResultMarkdown += "| Name | Combinations |`n" + $ResultMarkdown += "| :--- | :---------- |`n" + foreach ($strength in $CustomStrengths) { + $combinations = if ($strength.allowedCombinations) { $strength.allowedCombinations.Count } else { 0 } + $ResultMarkdown += "| $($strength.displayName) | $combinations methods |`n" + } + } + + $Status = 'Passed' + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status $Status -ResultMarkdown $ResultMarkdown -Risk 'High' -Name 'Enable protected actions to secure Conditional Access policy creation and changes' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Access control' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Error running test: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Enable protected actions to secure Conditional Access policy creation and changes' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Access control' + } +} diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24545.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24545.ps1 new file mode 100644 index 000000000000..9e3aff4c71c5 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24545.ps1 @@ -0,0 +1,42 @@ +function Invoke-CippTestZTNA24545 { + param($Tenant) + + $TestId = 'ZTNA24545' + + try { + $IntunePolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'IntunePolicies' + + if (-not $IntunePolicies) { + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Devices' -Status 'Investigate' -ResultMarkdown 'Intune policies not found in database' -Risk 'High' -Name 'Compliance policies protect fully managed and corporate-owned Android devices' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Tenant' + return + } + + $AndroidPolicies = @($IntunePolicies | Where-Object { $_.'@odata.type' -eq '#microsoft.graph.androidDeviceOwnerCompliancePolicy' }) + $AssignedPolicies = @($AndroidPolicies | Where-Object { $_.assignments -and $_.assignments.Count -gt 0 }) + + $Passed = $AssignedPolicies.Count -gt 0 + + if ($Passed) { + $ResultMarkdown = "✅ At least one compliance policy for Android Enterprise Fully managed devices exists and is assigned.`n`n" + } else { + $ResultMarkdown = "❌ No compliance policy for Android Enterprise exists or none are assigned.`n`n" + } + + $ResultMarkdown += "## Android Device Owner Compliance Policies`n`n" + $ResultMarkdown += "| Policy Name | Assigned |`n" + $ResultMarkdown += "| :---------- | :------- |`n" + + foreach ($policy in $AndroidPolicies) { + $assigned = if ($policy.assignments -and $policy.assignments.Count -gt 0) { '✅ Yes' } else { '❌ No' } + $ResultMarkdown += "| $($policy.displayName) | $assigned |`n" + } + + $Status = if ($Passed) { 'Passed' } else { 'Failed' } + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Devices' -Status $Status -ResultMarkdown $ResultMarkdown -Risk 'High' -Name 'Compliance policies protect fully managed and corporate-owned Android devices' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Tenant' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Devices' -Status 'Failed' -ResultMarkdown "Error running test: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Compliance policies protect fully managed and corporate-owned Android devices' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Tenant' + } +} diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24547.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24547.ps1 new file mode 100644 index 000000000000..d9ebee496be3 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24547.ps1 @@ -0,0 +1,42 @@ +function Invoke-CippTestZTNA24547 { + param($Tenant) + + $TestId = 'ZTNA24547' + + try { + $IntunePolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'IntunePolicies' + + if (-not $IntunePolicies) { + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Devices' -Status 'Investigate' -ResultMarkdown 'Intune policies not found in database' -Risk 'High' -Name 'Compliance policies protect personally owned Android devices' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Tenant' + return + } + + $AndroidPolicies = @($IntunePolicies | Where-Object { $_.'@odata.type' -eq '#microsoft.graph.androidWorkProfileCompliancePolicy' }) + $AssignedPolicies = @($AndroidPolicies | Where-Object { $_.assignments -and $_.assignments.Count -gt 0 }) + + $Passed = $AssignedPolicies.Count -gt 0 + + if ($Passed) { + $ResultMarkdown = "✅ At least one compliance policy for Android Work Profile devices exists and is assigned.`n`n" + } else { + $ResultMarkdown = "❌ No compliance policy for Android Work Profile exists or none are assigned.`n`n" + } + + $ResultMarkdown += "## Android Work Profile Compliance Policies`n`n" + $ResultMarkdown += "| Policy Name | Assigned |`n" + $ResultMarkdown += "| :---------- | :------- |`n" + + foreach ($policy in $AndroidPolicies) { + $assigned = if ($policy.assignments -and $policy.assignments.Count -gt 0) { '✅ Yes' } else { '❌ No' } + $ResultMarkdown += "| $($policy.displayName) | $assigned |`n" + } + + $Status = if ($Passed) { 'Passed' } else { 'Failed' } + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Devices' -Status $Status -ResultMarkdown $ResultMarkdown -Risk 'High' -Name 'Compliance policies protect personally owned Android devices' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Tenant' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Devices' -Status 'Failed' -ResultMarkdown "Error running test: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Compliance policies protect personally owned Android devices' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Tenant' + } +} diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24548.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24548.ps1 new file mode 100644 index 000000000000..d9dc3308a70e --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24548.ps1 @@ -0,0 +1,40 @@ +function Invoke-CippTestZTNA24548 { + param($Tenant) + + $TestId = 'ZTNA24548' + + try { + $IosPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'IntuneIosAppProtectionPolicies' + + if (-not $IosPolicies) { + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Devices' -Status 'Investigate' -ResultMarkdown 'iOS app protection policies not found in database' -Risk 'High' -Name 'Data on iOS/iPadOS is protected by app protection policies' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Tenant' + return + } + + $AssignedPolicies = @($IosPolicies | Where-Object { $_.assignments -and $_.assignments.Count -gt 0 }) + $Passed = $AssignedPolicies.Count -gt 0 + + if ($Passed) { + $ResultMarkdown = "✅ At least one iOS app protection policy exists and is assigned.`n`n" + } else { + $ResultMarkdown = "❌ No iOS app protection policy exists or none are assigned.`n`n" + } + + $ResultMarkdown += "## iOS App Protection Policies`n`n" + $ResultMarkdown += "| Policy Name | Assigned |`n" + $ResultMarkdown += "| :---------- | :------- |`n" + + foreach ($policy in $IosPolicies) { + $assigned = if ($policy.assignments -and $policy.assignments.Count -gt 0) { '✅ Yes' } else { '❌ No' } + $ResultMarkdown += "| $($policy.displayName) | $assigned |`n" + } + + $Status = if ($Passed) { 'Passed' } else { 'Failed' } + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Devices' -Status $Status -ResultMarkdown $ResultMarkdown -Risk 'High' -Name 'Data on iOS/iPadOS is protected by app protection policies' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Tenant' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Devices' -Status 'Failed' -ResultMarkdown "Error running test: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Data on iOS/iPadOS is protected by app protection policies' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Tenant' + } +} diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24549.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24549.ps1 new file mode 100644 index 000000000000..40259bab2f67 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24549.ps1 @@ -0,0 +1,40 @@ +function Invoke-CippTestZTNA24549 { + param($Tenant) + + $TestId = 'ZTNA24549' + + try { + $AndroidPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'IntuneAndroidAppProtectionPolicies' + + if (-not $AndroidPolicies) { + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Devices' -Status 'Investigate' -ResultMarkdown 'Android app protection policies not found in database' -Risk 'High' -Name 'Data on Android is protected by app protection policies' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Tenant' + return + } + + $AssignedPolicies = @($AndroidPolicies | Where-Object { $_.assignments -and $_.assignments.Count -gt 0 }) + $Passed = $AssignedPolicies.Count -gt 0 + + if ($Passed) { + $ResultMarkdown = "✅ At least one Android app protection policy exists and is assigned.`n`n" + } else { + $ResultMarkdown = "❌ No Android app protection policy exists or none are assigned.`n`n" + } + + $ResultMarkdown += "## Android App Protection Policies`n`n" + $ResultMarkdown += "| Policy Name | Assigned |`n" + $ResultMarkdown += "| :---------- | :------- |`n" + + foreach ($policy in $AndroidPolicies) { + $assigned = if ($policy.assignments -and $policy.assignments.Count -gt 0) { '✅ Yes' } else { '❌ No' } + $ResultMarkdown += "| $($policy.displayName) | $assigned |`n" + } + + $Status = if ($Passed) { 'Passed' } else { 'Failed' } + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Devices' -Status $Status -ResultMarkdown $ResultMarkdown -Risk 'High' -Name 'Data on Android is protected by app protection policies' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Tenant' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Devices' -Status 'Failed' -ResultMarkdown "Error running test: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Data on Android is protected by app protection policies' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Tenant' + } +} diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24553.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24553.ps1 new file mode 100644 index 000000000000..45d650bb0a1b --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24553.ps1 @@ -0,0 +1,48 @@ +function Invoke-CippTestZTNA24553 { + param($Tenant) + + $TestId = 'ZTNA24553' + + try { + $IntunePolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'IntunePolicies' + + if (-not $IntunePolicies) { + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Devices' -Status 'Investigate' -ResultMarkdown 'Intune policies not found in database' -Risk 'High' -Name 'Windows Update policies are enforced to reduce risk from unpatched vulnerabilities' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Tenant' + return + } + + $UpdatePolicies = @($IntunePolicies | Where-Object { + $_.'@odata.type' -in @( + '#microsoft.graph.windowsUpdateForBusinessConfiguration', + '#microsoft.graph.windows10CompliancePolicy' + ) + }) + + $AssignedPolicies = @($UpdatePolicies | Where-Object { $_.assignments -and $_.assignments.Count -gt 0 }) + $Passed = $AssignedPolicies.Count -gt 0 + + if ($Passed) { + $ResultMarkdown = "✅ Windows Update policies are configured and assigned.`n`n" + } else { + $ResultMarkdown = "❌ No Windows Update policies are configured or assigned.`n`n" + } + + $ResultMarkdown += "## Windows Update Policies`n`n" + $ResultMarkdown += "| Policy Name | Type | Assigned |`n" + $ResultMarkdown += "| :---------- | :--- | :------- |`n" + + foreach ($policy in $UpdatePolicies) { + $type = if ($policy.'@odata.type' -eq '#microsoft.graph.windowsUpdateForBusinessConfiguration') { 'Update' } else { 'Compliance' } + $assigned = if ($policy.assignments -and $policy.assignments.Count -gt 0) { '✅ Yes' } else { '❌ No' } + $ResultMarkdown += "| $($policy.displayName) | $type | $assigned |`n" + } + + $Status = if ($Passed) { 'Passed' } else { 'Failed' } + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Devices' -Status $Status -ResultMarkdown $ResultMarkdown -Risk 'High' -Name 'Windows Update policies are enforced to reduce risk from unpatched vulnerabilities' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Tenant' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Devices' -Status 'Failed' -ResultMarkdown "Error running test: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Windows Update policies are enforced to reduce risk from unpatched vulnerabilities' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Tenant' + } +} From bdc43307148b148009c3f6baae3d6e696f57b9b3 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Wed, 24 Dec 2025 02:19:01 +0100 Subject: [PATCH 033/503] Tests --- .../Public/Tests/Invoke-CippTestZTNA24541.ps1 | 44 ++++++++++++++++ .../Public/Tests/Invoke-CippTestZTNA24542.ps1 | 41 +++++++++++++++ .../Public/Tests/Invoke-CippTestZTNA24543.ps1 | 41 +++++++++++++++ .../Public/Tests/Invoke-CippTestZTNA24569.ps1 | 50 +++++++++++++++++++ .../Public/Tests/Invoke-CippTestZTNA24572.ps1 | 47 +++++++++++++++++ .../Public/Tests/Invoke-CippTestZTNA24576.ps1 | 48 ++++++++++++++++++ 6 files changed, 271 insertions(+) create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24541.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24542.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24543.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24569.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24572.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24576.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24541.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24541.ps1 new file mode 100644 index 000000000000..19c521a99626 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24541.ps1 @@ -0,0 +1,44 @@ +function Invoke-CippTestZTNA24541 { + param($Tenant) + + $TestId = 'ZTNA24541' + + try { + $IntunePolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'IntunePolicies' + + if (-not $IntunePolicies) { + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Devices' -Status 'Investigate' -ResultMarkdown 'Intune policies not found in database' -Risk 'High' -Name 'Compliance policies protect Windows devices' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Tenant' + return + } + + $WindowsPolicies = @($IntunePolicies | Where-Object { + $_.'@odata.type' -in @('#microsoft.graph.windows10CompliancePolicy', '#microsoft.graph.windows11CompliancePolicy') + }) + + $AssignedPolicies = @($WindowsPolicies | Where-Object { $_.assignments -and $_.assignments.Count -gt 0 }) + $Passed = $AssignedPolicies.Count -gt 0 + + if ($Passed) { + $ResultMarkdown = "✅ At least one Windows compliance policy exists and is assigned.`n`n" + } else { + $ResultMarkdown = "❌ No Windows compliance policy exists or none are assigned.`n`n" + } + + $ResultMarkdown += "## Windows Compliance Policies`n`n" + $ResultMarkdown += "| Policy Name | Assigned |`n" + $ResultMarkdown += "| :---------- | :------- |`n" + + foreach ($policy in $WindowsPolicies) { + $assigned = if ($policy.assignments -and $policy.assignments.Count -gt 0) { '✅ Yes' } else { '❌ No' } + $ResultMarkdown += "| $($policy.displayName) | $assigned |`n" + } + + $Status = if ($Passed) { 'Passed' } else { 'Failed' } + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Devices' -Status $Status -ResultMarkdown $ResultMarkdown -Risk 'High' -Name 'Compliance policies protect Windows devices' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Tenant' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Devices' -Status 'Failed' -ResultMarkdown "Error running test: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Compliance policies protect Windows devices' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Tenant' + } +} diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24542.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24542.ps1 new file mode 100644 index 000000000000..1b01dd950ceb --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24542.ps1 @@ -0,0 +1,41 @@ +function Invoke-CippTestZTNA24542 { + param($Tenant) + + $TestId = 'ZTNA24542' + + try { + $IntunePolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'IntunePolicies' + + if (-not $IntunePolicies) { + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Devices' -Status 'Investigate' -ResultMarkdown 'Intune policies not found in database' -Risk 'High' -Name 'Compliance policies protect macOS devices' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Tenant' + return + } + + $MacOSPolicies = @($IntunePolicies | Where-Object { $_.'@odata.type' -eq '#microsoft.graph.macOSCompliancePolicy' }) + $AssignedPolicies = @($MacOSPolicies | Where-Object { $_.assignments -and $_.assignments.Count -gt 0 }) + $Passed = $AssignedPolicies.Count -gt 0 + + if ($Passed) { + $ResultMarkdown = "✅ At least one macOS compliance policy exists and is assigned.`n`n" + } else { + $ResultMarkdown = "❌ No macOS compliance policy exists or none are assigned.`n`n" + } + + $ResultMarkdown += "## macOS Compliance Policies`n`n" + $ResultMarkdown += "| Policy Name | Assigned |`n" + $ResultMarkdown += "| :---------- | :------- |`n" + + foreach ($policy in $MacOSPolicies) { + $assigned = if ($policy.assignments -and $policy.assignments.Count -gt 0) { '✅ Yes' } else { '❌ No' } + $ResultMarkdown += "| $($policy.displayName) | $assigned |`n" + } + + $Status = if ($Passed) { 'Passed' } else { 'Failed' } + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Devices' -Status $Status -ResultMarkdown $ResultMarkdown -Risk 'High' -Name 'Compliance policies protect macOS devices' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Tenant' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Devices' -Status 'Failed' -ResultMarkdown "Error running test: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Compliance policies protect macOS devices' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Tenant' + } +} diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24543.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24543.ps1 new file mode 100644 index 000000000000..af9593a75260 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24543.ps1 @@ -0,0 +1,41 @@ +function Invoke-CippTestZTNA24543 { + param($Tenant) + + $TestId = 'ZTNA24543' + + try { + $IntunePolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'IntunePolicies' + + if (-not $IntunePolicies) { + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Devices' -Status 'Investigate' -ResultMarkdown 'Intune policies not found in database' -Risk 'High' -Name 'Compliance policies protect iOS/iPadOS devices' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Tenant' + return + } + + $iOSPolicies = @($IntunePolicies | Where-Object { $_.'@odata.type' -eq '#microsoft.graph.iosCompliancePolicy' }) + $AssignedPolicies = @($iOSPolicies | Where-Object { $_.assignments -and $_.assignments.Count -gt 0 }) + $Passed = $AssignedPolicies.Count -gt 0 + + if ($Passed) { + $ResultMarkdown = "✅ At least one iOS/iPadOS compliance policy exists and is assigned.`n`n" + } else { + $ResultMarkdown = "❌ No iOS/iPadOS compliance policy exists or none are assigned.`n`n" + } + + $ResultMarkdown += "## iOS/iPadOS Compliance Policies`n`n" + $ResultMarkdown += "| Policy Name | Assigned |`n" + $ResultMarkdown += "| :---------- | :------- |`n" + + foreach ($policy in $iOSPolicies) { + $assigned = if ($policy.assignments -and $policy.assignments.Count -gt 0) { '✅ Yes' } else { '❌ No' } + $ResultMarkdown += "| $($policy.displayName) | $assigned |`n" + } + + $Status = if ($Passed) { 'Passed' } else { 'Failed' } + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Devices' -Status $Status -ResultMarkdown $ResultMarkdown -Risk 'High' -Name 'Compliance policies protect iOS/iPadOS devices' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Tenant' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Devices' -Status 'Failed' -ResultMarkdown "Error running test: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Compliance policies protect iOS/iPadOS devices' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Tenant' + } +} diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24569.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24569.ps1 new file mode 100644 index 000000000000..dc81c626bddb --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24569.ps1 @@ -0,0 +1,50 @@ +function Invoke-CippTestZTNA24569 { + param($Tenant) + + $TestId = 'ZTNA24569' + + try { + $DeviceConfigs = New-CIPPDbRequest -TenantFilter $Tenant -Type 'IntuneDeviceConfigurations' + + if (-not $DeviceConfigs) { + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Devices' -Status 'Investigate' -ResultMarkdown 'Device configurations not found in database' -Risk 'High' -Name 'FileVault encryption protects data on macOS devices' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Device' + return + } + + $MacOSEndpointProtectionPolicies = @($DeviceConfigs | Where-Object { + $_.'@odata.type' -eq '#microsoft.graph.macOSEndpointProtectionConfiguration' + }) + + $FileVaultEnabledPolicies = @($MacOSEndpointProtectionPolicies | Where-Object { $_.fileVaultEnabled -eq $true }) + $AssignedFileVaultPolicies = @($FileVaultEnabledPolicies | Where-Object { $_.assignments -and $_.assignments.Count -gt 0 }) + $Passed = $AssignedFileVaultPolicies.Count -gt 0 + + if ($Passed) { + $ResultMarkdown = "✅ macOS FileVault encryption policies are configured and assigned in Intune.`n`n" + } else { + $ResultMarkdown = "❌ No relevant macOS FileVault encryption policies are configured or assigned.`n`n" + } + + if ($FileVaultEnabledPolicies.Count -gt 0) { + $ResultMarkdown += "## macOS FileVault Policies`n`n" + $ResultMarkdown += "| Policy Name | FileVault Enabled | Assigned |`n" + $ResultMarkdown += "| :---------- | :---------------- | :------- |`n" + + foreach ($policy in $FileVaultEnabledPolicies) { + $fileVault = if ($policy.fileVaultEnabled -eq $true) { '✅ Yes' } else { '❌ No' } + $assigned = if ($policy.assignments -and $policy.assignments.Count -gt 0) { '✅ Yes' } else { '❌ No' } + $ResultMarkdown += "| $($policy.displayName) | $fileVault | $assigned |`n" + } + } else { + $ResultMarkdown += "No macOS Endpoint Protection policies with FileVault settings found.`n" + } + + $Status = if ($Passed) { 'Passed' } else { 'Failed' } + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Devices' -Status $Status -ResultMarkdown $ResultMarkdown -Risk 'High' -Name 'FileVault encryption protects data on macOS devices' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Device' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Devices' -Status 'Failed' -ResultMarkdown "Error running test: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'FileVault encryption protects data on macOS devices' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Device' + } +} diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24572.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24572.ps1 new file mode 100644 index 000000000000..2c60a39e18f8 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24572.ps1 @@ -0,0 +1,47 @@ +function Invoke-CippTestZTNA24572 { + param($Tenant) + + $TestId = 'ZTNA24572' + + try { + $EnrollmentConfigs = New-CIPPDbRequest -TenantFilter $Tenant -Type 'IntuneDeviceEnrollmentConfigurations' + + if (-not $EnrollmentConfigs) { + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Devices' -Status 'Investigate' -ResultMarkdown 'Device enrollment configurations not found in database' -Risk 'Medium' -Name 'Device enrollment notifications are enforced to ensure user awareness and secure onboarding' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Tenant' + return + } + + $EnrollmentNotifications = @($EnrollmentConfigs | Where-Object { + $_.'@odata.type' -eq '#microsoft.graph.windowsEnrollmentStatusScreenSettings' -or + $_.'deviceEnrollmentConfigurationType' -eq 'EnrollmentNotificationsConfiguration' + }) + + $AssignedNotifications = @($EnrollmentNotifications | Where-Object { $_.assignments -and $_.assignments.Count -gt 0 }) + $Passed = $AssignedNotifications.Count -gt 0 + + if ($Passed) { + $ResultMarkdown = "✅ At least one device enrollment notification is configured and assigned.`n`n" + } else { + $ResultMarkdown = "❌ No device enrollment notification is configured or assigned in Intune.`n`n" + } + + if ($EnrollmentNotifications.Count -gt 0) { + $ResultMarkdown += "## Device Enrollment Notifications`n`n" + $ResultMarkdown += "| Policy Name | Assigned |`n" + $ResultMarkdown += "| :---------- | :------- |`n" + + foreach ($policy in $EnrollmentNotifications) { + $assigned = if ($policy.assignments -and $policy.assignments.Count -gt 0) { '✅ Yes' } else { '❌ No' } + $ResultMarkdown += "| $($policy.displayName) | $assigned |`n" + } + } + + $Status = if ($Passed) { 'Passed' } else { 'Failed' } + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Devices' -Status $Status -ResultMarkdown $ResultMarkdown -Risk 'Medium' -Name 'Device enrollment notifications are enforced to ensure user awareness and secure onboarding' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Tenant' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Devices' -Status 'Failed' -ResultMarkdown "Error running test: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'Device enrollment notifications are enforced to ensure user awareness and secure onboarding' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Tenant' + } +} diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24576.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24576.ps1 new file mode 100644 index 000000000000..09a19afc3d2b --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24576.ps1 @@ -0,0 +1,48 @@ +function Invoke-CippTestZTNA24576 { + param($Tenant) + + $TestId = 'ZTNA24576' + + try { + $DeviceConfigs = New-CIPPDbRequest -TenantFilter $Tenant -Type 'IntuneDeviceConfigurations' + + if (-not $DeviceConfigs) { + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Devices' -Status 'Investigate' -ResultMarkdown 'Device configurations not found in database' -Risk 'Low' -Name 'Endpoint Analytics is enabled to help identify risks on Windows devices' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Tenant' + return + } + + $WindowsHealthMonitoringPolicies = @($DeviceConfigs | Where-Object { + $_.'@odata.type' -eq '#microsoft.graph.windowsHealthMonitoringConfiguration' + }) + + $AssignedPolicies = @($WindowsHealthMonitoringPolicies | Where-Object { $_.assignments -and $_.assignments.Count -gt 0 }) + $Passed = $AssignedPolicies.Count -gt 0 + + if ($Passed) { + $ResultMarkdown = "✅ An Endpoint analytics policy is created and assigned.`n`n" + } else { + $ResultMarkdown = "❌ Endpoint analytics policy is not created or not assigned.`n`n" + } + + if ($WindowsHealthMonitoringPolicies.Count -gt 0) { + $ResultMarkdown += "## Endpoint Analytics Policies`n`n" + $ResultMarkdown += "| Policy Name | Assigned |`n" + $ResultMarkdown += "| :---------- | :------- |`n" + + foreach ($policy in $WindowsHealthMonitoringPolicies) { + $assigned = if ($policy.assignments -and $policy.assignments.Count -gt 0) { '✅ Yes' } else { '❌ No' } + $ResultMarkdown += "| $($policy.displayName) | $assigned |`n" + } + } else { + $ResultMarkdown += "No Endpoint Analytics policies found in this tenant.`n" + } + + $Status = if ($Passed) { 'Passed' } else { 'Failed' } + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Devices' -Status $Status -ResultMarkdown $ResultMarkdown -Risk 'Low' -Name 'Endpoint Analytics is enabled to help identify risks on Windows devices' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Tenant' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Devices' -Status 'Failed' -ResultMarkdown "Error running test: $($ErrorMessage.NormalizedError)" -Risk 'Low' -Name 'Endpoint Analytics is enabled to help identify risks on Windows devices' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Tenant' + } +} From e9a63368bf5f6fa86da5e9a9d705067dfb68e370 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Wed, 24 Dec 2025 02:24:06 +0100 Subject: [PATCH 034/503] updates to tests --- .../Public/Tests/Invoke-CippTestZTNA24541.ps1 | 2 +- .../Public/Tests/Invoke-CippTestZTNA24542.ps1 | 2 +- .../Public/Tests/Invoke-CippTestZTNA24543.ps1 | 2 +- .../Public/Tests/Invoke-CippTestZTNA24545.ps1 | 2 +- .../Public/Tests/Invoke-CippTestZTNA24547.ps1 | 2 +- .../Public/Tests/Invoke-CippTestZTNA24553.ps1 | 2 +- .../Public/Tests/Invoke-CippTestZTNA24839.ps1 | 47 +++++++++++++++++++ .../Public/Tests/Invoke-CippTestZTNA24840.ps1 | 47 +++++++++++++++++++ .../Public/Tests/Invoke-CippTestZTNA24870.ps1 | 47 +++++++++++++++++++ 9 files changed, 147 insertions(+), 6 deletions(-) create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24839.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24840.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24870.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24541.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24541.ps1 index 19c521a99626..876008a12be8 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24541.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24541.ps1 @@ -4,7 +4,7 @@ function Invoke-CippTestZTNA24541 { $TestId = 'ZTNA24541' try { - $IntunePolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'IntunePolicies' + $IntunePolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'IntuneDeviceCompliancePolicies' if (-not $IntunePolicies) { Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Devices' -Status 'Investigate' -ResultMarkdown 'Intune policies not found in database' -Risk 'High' -Name 'Compliance policies protect Windows devices' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Tenant' diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24542.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24542.ps1 index 1b01dd950ceb..d1054428b70c 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24542.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24542.ps1 @@ -4,7 +4,7 @@ function Invoke-CippTestZTNA24542 { $TestId = 'ZTNA24542' try { - $IntunePolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'IntunePolicies' + $IntunePolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'IntuneDeviceCompliancePolicies' if (-not $IntunePolicies) { Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Devices' -Status 'Investigate' -ResultMarkdown 'Intune policies not found in database' -Risk 'High' -Name 'Compliance policies protect macOS devices' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Tenant' diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24543.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24543.ps1 index af9593a75260..58f551b9575c 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24543.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24543.ps1 @@ -4,7 +4,7 @@ function Invoke-CippTestZTNA24543 { $TestId = 'ZTNA24543' try { - $IntunePolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'IntunePolicies' + $IntunePolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'IntuneDeviceCompliancePolicies' if (-not $IntunePolicies) { Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Devices' -Status 'Investigate' -ResultMarkdown 'Intune policies not found in database' -Risk 'High' -Name 'Compliance policies protect iOS/iPadOS devices' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Tenant' diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24545.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24545.ps1 index 9e3aff4c71c5..b1aa37072217 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24545.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24545.ps1 @@ -4,7 +4,7 @@ function Invoke-CippTestZTNA24545 { $TestId = 'ZTNA24545' try { - $IntunePolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'IntunePolicies' + $IntunePolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'IntuneDeviceCompliancePolicies' if (-not $IntunePolicies) { Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Devices' -Status 'Investigate' -ResultMarkdown 'Intune policies not found in database' -Risk 'High' -Name 'Compliance policies protect fully managed and corporate-owned Android devices' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Tenant' diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24547.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24547.ps1 index d9ebee496be3..03e15fc30e94 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24547.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24547.ps1 @@ -4,7 +4,7 @@ function Invoke-CippTestZTNA24547 { $TestId = 'ZTNA24547' try { - $IntunePolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'IntunePolicies' + $IntunePolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'IntuneDeviceCompliancePolicies' if (-not $IntunePolicies) { Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Devices' -Status 'Investigate' -ResultMarkdown 'Intune policies not found in database' -Risk 'High' -Name 'Compliance policies protect personally owned Android devices' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Tenant' diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24553.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24553.ps1 index 45d650bb0a1b..3c68b99f4f5b 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24553.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24553.ps1 @@ -4,7 +4,7 @@ function Invoke-CippTestZTNA24553 { $TestId = 'ZTNA24553' try { - $IntunePolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'IntunePolicies' + $IntunePolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'IntuneDeviceCompliancePolicies' if (-not $IntunePolicies) { Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Devices' -Status 'Investigate' -ResultMarkdown 'Intune policies not found in database' -Risk 'High' -Name 'Windows Update policies are enforced to reduce risk from unpatched vulnerabilities' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Tenant' diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24839.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24839.ps1 new file mode 100644 index 000000000000..40c68b2e5da6 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24839.ps1 @@ -0,0 +1,47 @@ +function Invoke-CippTestZTNA24839 { + param($Tenant) + + $TestId = 'ZTNA24839' + + try { + $DeviceConfigs = New-CIPPDbRequest -TenantFilter $Tenant -Type 'IntuneDeviceConfigurations' + + if (-not $DeviceConfigs) { + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Devices' -Status 'Investigate' -ResultMarkdown 'Device configurations not found in database' -Risk 'High' -Name 'Secure Wi-Fi profiles protect iOS devices from unauthorized network access' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Data' + return + } + + $iOSWifiConfProfiles = @($DeviceConfigs | Where-Object { $_.'@odata.type' -eq '#microsoft.graph.iosWiFiConfiguration' }) + $CompliantIosWifiConfProfiles = @($iOSWifiConfProfiles | Where-Object { $_.wiFiSecurityType -in @('wpa2Enterprise', 'wpaEnterprise') }) + $AssignedCompliantProfiles = @($CompliantIosWifiConfProfiles | Where-Object { $_.assignments -and $_.assignments.Count -gt 0 }) + $Passed = $AssignedCompliantProfiles.Count -gt 0 + + if ($Passed) { + $ResultMarkdown = "✅ At least one Enterprise Wi-Fi profile for iOS exists and is assigned.`n`n" + } else { + $ResultMarkdown = "❌ No Enterprise Wi-Fi profile for iOS exists or none are assigned.`n`n" + } + + if ($iOSWifiConfProfiles.Count -gt 0) { + $ResultMarkdown += "## iOS WiFi Configuration Profiles`n`n" + $ResultMarkdown += "| Policy Name | Wi-Fi Security Type | Assigned |`n" + $ResultMarkdown += "| :---------- | :------------------ | :------- |`n" + + foreach ($policy in $iOSWifiConfProfiles) { + $securityType = if ($policy.wiFiSecurityType) { $policy.wiFiSecurityType } else { 'Unknown' } + $assigned = if ($policy.assignments -and $policy.assignments.Count -gt 0) { '✅ Yes' } else { '❌ No' } + $ResultMarkdown += "| $($policy.displayName) | $securityType | $assigned |`n" + } + } else { + $ResultMarkdown += "No iOS WiFi configuration profiles found.`n" + } + + $Status = if ($Passed) { 'Passed' } else { 'Failed' } + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Devices' -Status $Status -ResultMarkdown $ResultMarkdown -Risk 'High' -Name 'Secure Wi-Fi profiles protect iOS devices from unauthorized network access' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Data' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Devices' -Status 'Failed' -ResultMarkdown "Error running test: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Secure Wi-Fi profiles protect iOS devices from unauthorized network access' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Data' + } +} diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24840.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24840.ps1 new file mode 100644 index 000000000000..72f756a3adda --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24840.ps1 @@ -0,0 +1,47 @@ +function Invoke-CippTestZTNA24840 { + param($Tenant) + + $TestId = 'ZTNA24840' + + try { + $DeviceConfigs = New-CIPPDbRequest -TenantFilter $Tenant -Type 'IntuneDeviceConfigurations' + + if (-not $DeviceConfigs) { + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Devices' -Status 'Investigate' -ResultMarkdown 'Device configurations not found in database' -Risk 'High' -Name 'Secure Wi-Fi profiles protect Android devices from unauthorized network access' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Data' + return + } + + $AndroidWifiConfProfiles = @($DeviceConfigs | Where-Object { $_.'@odata.type' -eq '#microsoft.graph.androidDeviceOwnerEnterpriseWiFiConfiguration' }) + $CompliantAndroidWifiConfProfiles = @($AndroidWifiConfProfiles | Where-Object { $_.wiFiSecurityType -eq 'wpaEnterprise' }) + $AssignedCompliantProfiles = @($CompliantAndroidWifiConfProfiles | Where-Object { $_.assignments -and $_.assignments.Count -gt 0 }) + $Passed = $AssignedCompliantProfiles.Count -gt 0 + + if ($Passed) { + $ResultMarkdown = "✅ At least one Enterprise Wi-Fi profile for android exists and is assigned.`n`n" + } else { + $ResultMarkdown = "❌ No Enterprise Wi-Fi profile for android exists or none are assigned.`n`n" + } + + if ($CompliantAndroidWifiConfProfiles.Count -gt 0) { + $ResultMarkdown += "## Android Wi-Fi Configuration Profiles`n`n" + $ResultMarkdown += "| Policy Name | Wi-Fi Security Type | Assigned |`n" + $ResultMarkdown += "| :---------- | :------------------ | :------- |`n" + + foreach ($policy in $CompliantAndroidWifiConfProfiles) { + $securityType = if ($policy.wiFiSecurityType) { $policy.wiFiSecurityType } else { 'Unknown' } + $assigned = if ($policy.assignments -and $policy.assignments.Count -gt 0) { '✅ Yes' } else { '❌ No' } + $ResultMarkdown += "| $($policy.displayName) | $securityType | $assigned |`n" + } + } else { + $ResultMarkdown += "No compliant Android Enterprise WiFi configuration profiles found.`n" + } + + $Status = if ($Passed) { 'Passed' } else { 'Failed' } + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Devices' -Status $Status -ResultMarkdown $ResultMarkdown -Risk 'High' -Name 'Secure Wi-Fi profiles protect Android devices from unauthorized network access' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Data' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Devices' -Status 'Failed' -ResultMarkdown "Error running test: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Secure Wi-Fi profiles protect Android devices from unauthorized network access' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Data' + } +} diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24870.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24870.ps1 new file mode 100644 index 000000000000..3035266ab448 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24870.ps1 @@ -0,0 +1,47 @@ +function Invoke-CippTestZTNA24870 { + param($Tenant) + + $TestId = 'ZTNA24870' + + try { + $DeviceConfigs = New-CIPPDbRequest -TenantFilter $Tenant -Type 'IntuneDeviceConfigurations' + + if (-not $DeviceConfigs) { + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Devices' -Status 'Investigate' -ResultMarkdown 'Device configurations not found in database' -Risk 'High' -Name 'Secure Wi-Fi profiles protect macOS devices from unauthorized network access' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Data' + return + } + + $MacOSWifiConfProfiles = @($DeviceConfigs | Where-Object { $_.'@odata.type' -eq '#microsoft.graph.macOSWiFiConfiguration' }) + $CompliantMacOSWifiConfProfiles = @($MacOSWifiConfProfiles | Where-Object { $_.wiFiSecurityType -eq 'wpaEnterprise' }) + $AssignedCompliantProfiles = @($CompliantMacOSWifiConfProfiles | Where-Object { $_.assignments -and $_.assignments.Count -gt 0 }) + $Passed = $AssignedCompliantProfiles.Count -gt 0 + + if ($Passed) { + $ResultMarkdown = "✅ At least one Enterprise Wi-Fi profile for macOS exists and is assigned.`n`n" + } else { + $ResultMarkdown = "❌ No Enterprise Wi-Fi profile for macOS exists or none are assigned.`n`n" + } + + if ($CompliantMacOSWifiConfProfiles.Count -gt 0) { + $ResultMarkdown += "## macOS WiFi Configuration Profiles`n`n" + $ResultMarkdown += "| Policy Name | Wi-Fi Security Type | Assigned |`n" + $ResultMarkdown += "| :---------- | :------------------ | :------- |`n" + + foreach ($policy in $CompliantMacOSWifiConfProfiles) { + $securityType = if ($policy.wiFiSecurityType) { $policy.wiFiSecurityType } else { 'Unknown' } + $assigned = if ($policy.assignments -and $policy.assignments.Count -gt 0) { '✅ Yes' } else { '❌ No' } + $ResultMarkdown += "| $($policy.displayName) | $securityType | $assigned |`n" + } + } else { + $ResultMarkdown += "No compliant macOS Enterprise WiFi configuration profiles found.`n" + } + + $Status = if ($Passed) { 'Passed' } else { 'Failed' } + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Devices' -Status $Status -ResultMarkdown $ResultMarkdown -Risk 'High' -Name 'Secure Wi-Fi profiles protect macOS devices from unauthorized network access' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Data' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Devices' -Status 'Failed' -ResultMarkdown "Error running test: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Secure Wi-Fi profiles protect macOS devices from unauthorized network access' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Data' + } +} From 58681f050a266224d45946ccd53353fa38599c8f Mon Sep 17 00:00:00 2001 From: kakaiwa Date: Tue, 23 Dec 2025 23:39:51 -0500 Subject: [PATCH 035/503] Added overwrite toggle for transport rule standard --- .../Invoke-CIPPStandardTransportRuleTemplate.ps1 | 10 +++++++--- node_modules/.yarn-integrity | 10 ++++++++++ yarn.lock | 4 ++++ 3 files changed, 21 insertions(+), 3 deletions(-) create mode 100644 node_modules/.yarn-integrity create mode 100644 yarn.lock diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTransportRuleTemplate.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTransportRuleTemplate.ps1 index 52bf5c0dcba0..5ac0d53dffbf 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTransportRuleTemplate.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTransportRuleTemplate.ps1 @@ -49,9 +49,13 @@ function Invoke-CIPPStandardTransportRuleTemplate { try { if ($Existing) { Write-Host 'Found existing' - $RequestParams | Add-Member -NotePropertyValue $RequestParams.name -NotePropertyName Identity - $GraphRequest = New-ExoRequest -tenantid $Tenant -cmdlet 'Set-TransportRule' -cmdParams ($RequestParams | Select-Object -Property * -ExcludeProperty GUID, Comments, HasSenderOverride, ExceptIfHasSenderOverride, ExceptIfMessageContainsDataClassifications, MessageContainsDataClassifications, UseLegacyRegex) -useSystemMailbox $true - Write-LogMessage -API 'Standards' -tenant $tenant -message "Successfully set transport rule for $tenant" -sev 'Info' + if ($Settings.overwrite) { + $RequestParams | Add-Member -NotePropertyValue $RequestParams.name -NotePropertyName Identity + $GraphRequest = New-ExoRequest -tenantid $Tenant -cmdlet 'Set-TransportRule' -cmdParams ($RequestParams | Select-Object -Property * -ExcludeProperty GUID, Comments, HasSenderOverride, ExceptIfHasSenderOverride, ExceptIfMessageContainsDataClassifications, MessageContainsDataClassifications, UseLegacyRegex) -useSystemMailbox $true + Write-LogMessage -API 'Standards' -tenant $tenant -message "Successfully set transport rule for $tenant" -sev 'Info' + } else { + Write-LogMessage -API 'Standards' -tenant $tenant -message "Skipping transport rule for $tenant as it already exists" -sev 'Info' + } } else { Write-Host 'Creating new' $GraphRequest = New-ExoRequest -tenantid $Tenant -cmdlet 'New-TransportRule' -cmdParams ($RequestParams | Select-Object -Property * -ExcludeProperty GUID, Comments, HasSenderOverride, ExceptIfHasSenderOverride, ExceptIfMessageContainsDataClassifications, MessageContainsDataClassifications, UseLegacyRegex) -useSystemMailbox $true diff --git a/node_modules/.yarn-integrity b/node_modules/.yarn-integrity new file mode 100644 index 000000000000..9d4d6a804a9c --- /dev/null +++ b/node_modules/.yarn-integrity @@ -0,0 +1,10 @@ +{ + "systemParams": "win32-x64-127", + "modulesFolders": [], + "flags": [], + "linkedModules": [], + "topLevelPatterns": [], + "lockfileEntries": {}, + "files": [], + "artifacts": {} +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 000000000000..fb57ccd13afb --- /dev/null +++ b/yarn.lock @@ -0,0 +1,4 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + From d6dde87825f8f415b5c441d82f74174af1ec0dc9 Mon Sep 17 00:00:00 2001 From: kakaiwa Date: Tue, 23 Dec 2025 23:57:32 -0500 Subject: [PATCH 036/503] Removed yarn files --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index b88925ba01ed..ec6b5ec8902e 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,8 @@ ExcludedTenants SendNotifications/config.json .env Output/ +node_modules/.yarn-integrity +yarn.lock # Cursor IDE .cursor/rules From 749a40d14d4f6b131e943d672300cd095ebe53b4 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Wed, 24 Dec 2025 12:55:00 +0100 Subject: [PATCH 037/503] new tests --- .../CIPPCore/Public/Set-CIPPDBCacheGuests.ps1 | 2 +- .../Set-CIPPDBCacheServicePrincipals.ps1 | 2 +- .../Public/Tests/Invoke-CippTestZTNA21858.ps1 | 51 +++++++++++++++++++ .../Public/Tests/Invoke-CippTestZTNA21868.ps1 | 21 ++++++++ .../Public/Tests/Invoke-CippTestZTNA21869.ps1 | 33 ++++++++++++ .../Public/Tests/Invoke-CippTestZTNA21877.ps1 | 34 +++++++++++++ .../Public/Tests/Invoke-CippTestZTNA21886.ps1 | 32 ++++++++++++ 7 files changed, 173 insertions(+), 2 deletions(-) create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21858.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21868.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21869.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21877.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21886.ps1 diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheGuests.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheGuests.ps1 index 62eff7722789..96a1b5f02f52 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheGuests.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheGuests.ps1 @@ -15,7 +15,7 @@ function Set-CIPPDBCacheGuests { try { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching guest users' -sev Info - $Guests = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/users?`$filter=userType eq 'Guest'&`$top=999" -tenantid $TenantFilter + $Guests = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/users?`$filter=userType eq 'Guest'&`$expand=sponsors&`$top=999" -tenantid $TenantFilter Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Guests' -Data $Guests Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Guests' -Data $Guests -Count $Guests = $null diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheServicePrincipals.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheServicePrincipals.ps1 index f661d748507c..e2422e6028b7 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheServicePrincipals.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheServicePrincipals.ps1 @@ -15,7 +15,7 @@ function Set-CIPPDBCacheServicePrincipals { try { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching service principals' -sev Info - $ServicePrincipals = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/servicePrincipals?$top=999&$select=id,appId,displayName,servicePrincipalType,accountEnabled' -tenantid $TenantFilter + $ServicePrincipals = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/servicePrincipals' -tenantid $TenantFilter Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ServicePrincipals' -Data $ServicePrincipals Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ServicePrincipals' -Data $ServicePrincipals -Count $ServicePrincipals = $null diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21858.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21858.ps1 new file mode 100644 index 000000000000..a42c3f40dbec --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21858.ps1 @@ -0,0 +1,51 @@ +function Invoke-CippTestZTNA21858 { + param($Tenant) + + try { + $Guests = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Guests' + if (-not $Guests) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21858' -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Guest user data not found in database' -Risk 'Medium' -Name 'Inactive guest identities are disabled or removed from the tenant' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'External collaboration' + return + } + + $InactivityThresholdDays = 90 + $Today = Get-Date + $EnabledGuests = $Guests | Where-Object { $_.AccountEnabled -eq $true } + + if (-not $EnabledGuests) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21858' -TestType 'Identity' -Status 'Passed' -ResultMarkdown 'No guest users found in the tenant' -Risk 'Medium' -Name 'Inactive guest identities are disabled or removed from the tenant' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'External collaboration' + return + } + + $InactiveGuests = @() + foreach ($Guest in $EnabledGuests) { + $DaysSinceLastActivity = $null + + if ($Guest.signInActivity.lastSuccessfulSignInDateTime) { + $LastSignIn = [DateTime]$Guest.signInActivity.lastSuccessfulSignInDateTime + $DaysSinceLastActivity = ($Today - $LastSignIn).Days + } elseif ($Guest.createdDateTime) { + $Created = [DateTime]$Guest.createdDateTime + $DaysSinceLastActivity = ($Today - $Created).Days + } + + if ($null -ne $DaysSinceLastActivity -and $DaysSinceLastActivity -gt $InactivityThresholdDays) { + $InactiveGuests += $Guest + } + } + + if ($InactiveGuests.Count -gt 0) { + $Status = 'Failed' + $Result = "Found $($InactiveGuests.Count) inactive guest user(s) with no sign-in activity in the last $InactivityThresholdDays days" + } else { + $Status = 'Passed' + $Result = "All enabled guest users have been active within the last $InactivityThresholdDays days" + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21858' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'Inactive guest identities are disabled or removed from the tenant' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'External collaboration' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21858' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'Inactive guest identities are disabled or removed from the tenant' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'External collaboration' + } +} diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21868.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21868.ps1 new file mode 100644 index 000000000000..8ca12777bd50 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21868.ps1 @@ -0,0 +1,21 @@ +function Invoke-CippTestZTNA21868 { + param($Tenant) + + try { + $Guests = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Guests' + $Apps = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Apps' + $ServicePrincipals = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ServicePrincipals' + + if (-not $Guests -or -not $Apps -or -not $ServicePrincipals) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21868' -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Required data not found in database' -Risk 'Medium' -Name 'Guests do not own apps in the tenant' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'External collaboration' + return + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21868' -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'This test requires Graph API calls to check application and service principal ownership. Owner relationships are not cached.' -Risk 'Medium' -Name 'Guests do not own apps in the tenant' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'External collaboration' + } + catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21868' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'Guests do not own apps in the tenant' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'External collaboration' + } +} diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21869.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21869.ps1 new file mode 100644 index 000000000000..6ced50381bf5 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21869.ps1 @@ -0,0 +1,33 @@ +function Invoke-CippTestZTNA21869 { + param($Tenant) + + try { + $ServicePrincipals = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ServicePrincipals' + if (-not $ServicePrincipals) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21869' -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Service principal data not found in database' -Risk 'Medium' -Name 'Enterprise applications must require explicit assignment or scoped provisioning' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Application management' + return + } + + $AppsWithoutAssignment = $ServicePrincipals | Where-Object { + $_.appRoleAssignmentRequired -eq $false -and + $null -ne $_.preferredSingleSignOnMode -and + $_.preferredSingleSignOnMode -in @('password', 'saml', 'oidc') -and + $_.accountEnabled -eq $true + } + + if (-not $AppsWithoutAssignment) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21869' -TestType 'Identity' -Status 'Passed' -ResultMarkdown 'All enterprise applications have explicit assignment requirements' -Risk 'Medium' -Name 'Enterprise applications must require explicit assignment or scoped provisioning' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Application management' + return + } + + $Status = 'Investigate' + $Result = "Found $($AppsWithoutAssignment.Count) enterprise application(s) without assignment requirements. Full provisioning scope validation requires Graph API calls not available in cache." + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21869' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'Enterprise applications must require explicit assignment or scoped provisioning' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Application management' + } + catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21869' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'Enterprise applications must require explicit assignment or scoped provisioning' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Application management' + } +} diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21877.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21877.ps1 new file mode 100644 index 000000000000..0b94bedde485 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21877.ps1 @@ -0,0 +1,34 @@ +function Invoke-CippTestZTNA21877 { + param($Tenant) + + try { + $Guests = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Guests' + if (-not $Guests) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21877' -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Guest user data not found in database' -Risk 'Medium' -Name 'All guests have a sponsor' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Application management' + return + } + + if ($Guests.Count -eq 0) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21877' -TestType 'Identity' -Status 'Passed' -ResultMarkdown 'No guest accounts found in the tenant' -Risk 'Medium' -Name 'All guests have a sponsor' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Application management' + return + } + + $GuestsWithoutSponsors = $Guests | Where-Object { -not $_.sponsors -or $_.sponsors.Count -eq 0 } + + if ($GuestsWithoutSponsors.Count -eq 0) { + $Status = 'Passed' + $Result = 'All guest accounts in the tenant have an assigned sponsor' + } + else { + $Status = 'Failed' + $Result = "Found $($GuestsWithoutSponsors.Count) guest user(s) without sponsors out of $($Guests.Count) total guests" + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21877' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'All guests have a sponsor' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Application management' + } + catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21877' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'All guests have a sponsor' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Application management' + } +} diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21886.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21886.ps1 new file mode 100644 index 000000000000..6b499c5ce2d6 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21886.ps1 @@ -0,0 +1,32 @@ +function Invoke-CippTestZTNA21886 { + param($Tenant) + + try { + $ServicePrincipals = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ServicePrincipals' + if (-not $ServicePrincipals) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21886' -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Service principal data not found in database' -Risk 'Medium' -Name 'Applications are configured for automatic user provisioning' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Applications management' + return + } + + $AppsWithSSO = $ServicePrincipals | Where-Object { + $null -ne $_.preferredSingleSignOnMode -and + $_.preferredSingleSignOnMode -in @('password', 'saml', 'oidc') -and + $_.accountEnabled -eq $true + } + + if (-not $AppsWithSSO) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21886' -TestType 'Identity' -Status 'Passed' -ResultMarkdown 'No applications configured for SSO found' -Risk 'Medium' -Name 'Applications are configured for automatic user provisioning' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Applications management' + return + } + + $Status = 'Investigate' + $Result = "Found $($AppsWithSSO.Count) application(s) configured for SSO. Provisioning template and job validation requires Graph API synchronization endpoint not available in cache." + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21886' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'Applications are configured for automatic user provisioning' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Applications management' + } + catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21886' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'Applications are configured for automatic user provisioning' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Applications management' + } +} From 489f5079fb03f25f94f8a5cfdadb23e4f3835fcd Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Wed, 24 Dec 2025 12:55:06 +0100 Subject: [PATCH 038/503] new tests --- Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21877.ps1 | 6 ++---- Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21886.ps1 | 3 +-- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21877.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21877.ps1 index 0b94bedde485..85f43525de7c 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21877.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21877.ps1 @@ -18,15 +18,13 @@ function Invoke-CippTestZTNA21877 { if ($GuestsWithoutSponsors.Count -eq 0) { $Status = 'Passed' $Result = 'All guest accounts in the tenant have an assigned sponsor' - } - else { + } else { $Status = 'Failed' $Result = "Found $($GuestsWithoutSponsors.Count) guest user(s) without sponsors out of $($Guests.Count) total guests" } Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21877' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'All guests have a sponsor' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Application management' - } - catch { + } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21877' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'All guests have a sponsor' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Application management' diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21886.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21886.ps1 index 6b499c5ce2d6..610a1adac874 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21886.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21886.ps1 @@ -23,8 +23,7 @@ function Invoke-CippTestZTNA21886 { $Result = "Found $($AppsWithSSO.Count) application(s) configured for SSO. Provisioning template and job validation requires Graph API synchronization endpoint not available in cache." Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21886' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'Applications are configured for automatic user provisioning' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Applications management' - } - catch { + } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21886' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'Applications are configured for automatic user provisioning' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Applications management' From 020776ac54320736dc1e3e264bbcf3a6ca8fcdd5 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Wed, 24 Dec 2025 13:03:33 +0100 Subject: [PATCH 039/503] more tests --- .../Public/Tests/Invoke-CippTestZTNA21858.ps1 | 42 ++++++- .../Public/Tests/Invoke-CippTestZTNA21869.ps1 | 23 +++- .../Public/Tests/Invoke-CippTestZTNA21877.ps1 | 25 +++- .../Public/Tests/Invoke-CippTestZTNA21886.ps1 | 27 ++++- .../Public/Tests/Invoke-CippTestZTNA21896.ps1 | 56 +++++++++ .../Public/Tests/Invoke-CippTestZTNA21992.ps1 | 110 ++++++++++++++++++ .../Public/Tests/Invoke-CippTestZTNA22128.ps1 | 88 ++++++++++++++ 7 files changed, 366 insertions(+), 5 deletions(-) create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21896.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21992.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA22128.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21858.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21858.ps1 index a42c3f40dbec..db2a5f2fc25b 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21858.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21858.ps1 @@ -20,7 +20,7 @@ function Invoke-CippTestZTNA21858 { $InactiveGuests = @() foreach ($Guest in $EnabledGuests) { $DaysSinceLastActivity = $null - + if ($Guest.signInActivity.lastSuccessfulSignInDateTime) { $LastSignIn = [DateTime]$Guest.signInActivity.lastSuccessfulSignInDateTime $DaysSinceLastActivity = ($Today - $LastSignIn).Days @@ -36,7 +36,45 @@ function Invoke-CippTestZTNA21858 { if ($InactiveGuests.Count -gt 0) { $Status = 'Failed' - $Result = "Found $($InactiveGuests.Count) inactive guest user(s) with no sign-in activity in the last $InactivityThresholdDays days" + + $ResultLines = @( + "Found $($InactiveGuests.Count) inactive guest user(s) with no sign-in activity in the last $InactivityThresholdDays days." + '' + "**Total enabled guests:** $($EnabledGuests.Count)" + "**Inactive guests:** $($InactiveGuests.Count)" + "**Inactivity threshold:** $InactivityThresholdDays days" + '' + '**Top 10 inactive guest users:**' + ) + + $Top10Guests = $InactiveGuests | Sort-Object { + if ($_.signInActivity.lastSuccessfulSignInDateTime) { + [DateTime]$_.signInActivity.lastSuccessfulSignInDateTime + } else { + [DateTime]$_.createdDateTime + } + } | Select-Object -First 10 + + foreach ($Guest in $Top10Guests) { + if ($Guest.signInActivity.lastSuccessfulSignInDateTime) { + $LastActivity = [DateTime]$Guest.signInActivity.lastSuccessfulSignInDateTime + $DaysInactive = [Math]::Round(($Today - $LastActivity).TotalDays, 0) + $ResultLines += "- $($Guest.displayName) ($($Guest.userPrincipalName)) - Last sign-in: $DaysInactive days ago" + } else { + $Created = [DateTime]$Guest.createdDateTime + $DaysSinceCreated = [Math]::Round(($Today - $Created).TotalDays, 0) + $ResultLines += "- $($Guest.displayName) ($($Guest.userPrincipalName)) - Never signed in (Created $DaysSinceCreated days ago)" + } + } + + if ($InactiveGuests.Count -gt 10) { + $ResultLines += "- ... and $($InactiveGuests.Count - 10) more inactive guest(s)" + } + + $ResultLines += '' + $ResultLines += '**Recommendation:** Review and remove or disable inactive guest accounts to reduce security risks.' + + $Result = $ResultLines -join "`n" } else { $Status = 'Passed' $Result = "All enabled guest users have been active within the last $InactivityThresholdDays days" diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21869.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21869.ps1 index 6ced50381bf5..f0b854c15de5 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21869.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21869.ps1 @@ -21,7 +21,28 @@ function Invoke-CippTestZTNA21869 { } $Status = 'Investigate' - $Result = "Found $($AppsWithoutAssignment.Count) enterprise application(s) without assignment requirements. Full provisioning scope validation requires Graph API calls not available in cache." + + $ResultLines = @( + "Found $($AppsWithoutAssignment.Count) enterprise application(s) without assignment requirements." + '' + '**Applications without user assignment requirements:**' + ) + + $Top10Apps = $AppsWithoutAssignment | Select-Object -First 10 + foreach ($App in $Top10Apps) { + $ResultLines += "- $($App.displayName) (SSO: $($App.preferredSingleSignOnMode))" + } + + if ($AppsWithoutAssignment.Count -gt 10) { + $ResultLines += "- ... and $($AppsWithoutAssignment.Count - 10) more application(s)" + } + + $ResultLines += '' + $ResultLines += '**Note:** Full provisioning scope validation requires Graph API synchronization endpoint not available in cache.' + $ResultLines += '' + $ResultLines += '**Recommendation:** Enable user assignment requirements or configure scoped provisioning to limit application access.' + + $Result = $ResultLines -join "`n" Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21869' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'Enterprise applications must require explicit assignment or scoped provisioning' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Application management' } diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21877.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21877.ps1 index 85f43525de7c..abfb3045d084 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21877.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21877.ps1 @@ -20,7 +20,30 @@ function Invoke-CippTestZTNA21877 { $Result = 'All guest accounts in the tenant have an assigned sponsor' } else { $Status = 'Failed' - $Result = "Found $($GuestsWithoutSponsors.Count) guest user(s) without sponsors out of $($Guests.Count) total guests" + + $ResultLines = @( + "Found $($GuestsWithoutSponsors.Count) guest user(s) without sponsors out of $($Guests.Count) total guests." + '' + "**Total guests:** $($Guests.Count)" + "**Guests without sponsors:** $($GuestsWithoutSponsors.Count)" + "**Guests with sponsors:** $($Guests.Count - $GuestsWithoutSponsors.Count)" + '' + '**Top 10 guests without sponsors:**' + ) + + $Top10Guests = $GuestsWithoutSponsors | Select-Object -First 10 + foreach ($Guest in $Top10Guests) { + $ResultLines += "- $($Guest.displayName) ($($Guest.userPrincipalName))" + } + + if ($GuestsWithoutSponsors.Count -gt 10) { + $ResultLines += "- ... and $($GuestsWithoutSponsors.Count - 10) more guest(s)" + } + + $ResultLines += '' + $ResultLines += '**Recommendation:** Assign sponsors to all guest accounts for better accountability and lifecycle management.' + + $Result = $ResultLines -join "`n" } Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21877' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'All guests have a sponsor' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Application management' diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21886.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21886.ps1 index 610a1adac874..f2017d707627 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21886.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21886.ps1 @@ -20,7 +20,32 @@ function Invoke-CippTestZTNA21886 { } $Status = 'Investigate' - $Result = "Found $($AppsWithSSO.Count) application(s) configured for SSO. Provisioning template and job validation requires Graph API synchronization endpoint not available in cache." + + $ResultLines = @( + "Found $($AppsWithSSO.Count) application(s) configured for SSO." + '' + '**Applications with SSO enabled:**' + ) + + $SSOByType = $AppsWithSSO | Group-Object -Property preferredSingleSignOnMode + foreach ($Group in $SSOByType) { + $ResultLines += "" + $ResultLines += "**$($Group.Name.ToUpper()) SSO** ($($Group.Count) app(s)):" + $Top5 = $Group.Group | Select-Object -First 5 + foreach ($App in $Top5) { + $ResultLines += "- $($App.displayName)" + } + if ($Group.Count -gt 5) { + $ResultLines += "- ... and $($Group.Count - 5) more" + } + } + + $ResultLines += '' + $ResultLines += '**Note:** Provisioning template and job validation requires Graph API synchronization endpoint not available in cache.' + $ResultLines += '' + $ResultLines += '**Recommendation:** Configure automatic user provisioning for applications that support it to ensure consistent access management.' + + $Result = $ResultLines -join "`n" Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21886' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'Applications are configured for automatic user provisioning' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Applications management' } catch { diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21896.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21896.ps1 new file mode 100644 index 000000000000..938d25dec80d --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21896.ps1 @@ -0,0 +1,56 @@ +function Invoke-CippTestZTNA21896 { + param($Tenant) + + try { + $ServicePrincipals = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ServicePrincipals' + if (-not $ServicePrincipals) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21896' -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Service principal data not found in database' -Risk 'Medium' -Name 'Service principals do not have certificates or credentials associated with them' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Application management' + return + } + + $MicrosoftOwnerId = 'f8cdef31-a31e-4b4a-93e4-5f571e91255a' + $SPsWithPassCreds = $ServicePrincipals | Where-Object { + $_.passwordCredentials -and $_.passwordCredentials.Count -gt 0 -and $_.appOwnerOrganizationId -ne $MicrosoftOwnerId + } + $SPsWithKeyCreds = $ServicePrincipals | Where-Object { + $_.keyCredentials -and $_.keyCredentials.Count -gt 0 -and $_.appOwnerOrganizationId -ne $MicrosoftOwnerId + } + + if (-not $SPsWithPassCreds -and -not $SPsWithKeyCreds) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21896' -TestType 'Identity' -Status 'Passed' -ResultMarkdown 'Service principals do not have credentials associated with them' -Risk 'Medium' -Name 'Service principals do not have certificates or credentials associated with them' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Application management' + return + } + + $TotalWithCreds = $SPsWithPassCreds.Count + $SPsWithKeyCreds.Count + $Status = 'Investigate' + + $ResultLines = @( + "Found $TotalWithCreds service principal(s) with credentials configured in the tenant, which represents a security risk." + '' + ) + + if ($SPsWithPassCreds.Count -gt 0) { + $ResultLines += "**Service principals with password credentials:** $($SPsWithPassCreds.Count)" + $ResultLines += '' + } + + if ($SPsWithKeyCreds.Count -gt 0) { + $ResultLines += "**Service principals with key credentials (certificates):** $($SPsWithKeyCreds.Count)" + $ResultLines += '' + } + + $ResultLines += '**Security implications:**' + $ResultLines += '- Service principals with credentials can be compromised if not properly secured' + $ResultLines += '- Password credentials are less secure than managed identities or certificate-based authentication' + $ResultLines += '- Consider using managed identities where possible to eliminate credential management' + + $Result = $ResultLines -join "`n" + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21896' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'Service principals do not have certificates or credentials associated with them' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Application management' + } + catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21896' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'Service principals do not have certificates or credentials associated with them' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Application management' + } +} diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21992.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21992.ps1 new file mode 100644 index 000000000000..e3a91af31ef7 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21992.ps1 @@ -0,0 +1,110 @@ +function Invoke-CippTestZTNA21992 { + param($Tenant) + + try { + $Apps = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Apps' + $ServicePrincipals = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ServicePrincipals' + + if (-not $Apps -and -not $ServicePrincipals) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21992' -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Application and service principal data not found in database' -Risk 'High' -Name 'Application certificates must be rotated on a regular basis' -UserImpact 'Low' -ImplementationEffort 'High' -Category 'Application management' + return + } + + $RotationThresholdDays = 180 + $ThresholdDate = (Get-Date).AddDays(-$RotationThresholdDays) + + $OldAppCerts = @() + if ($Apps) { + $OldAppCerts = $Apps | Where-Object { + $_.keyCredentials -and $_.keyCredentials.Count -gt 0 + } | ForEach-Object { + $App = $_ + $OldestCert = $App.keyCredentials | Where-Object { $_.startDateTime } | ForEach-Object { + [DateTime]$_.startDateTime + } | Sort-Object | Select-Object -First 1 + + if ($OldestCert -and $OldestCert -lt $ThresholdDate) { + [PSCustomObject]@{ + Type = 'Application' + DisplayName = $App.displayName + AppId = $App.appId + Id = $App.id + OldestCertDate = $OldestCert + } + } + } + } + + $OldSPCerts = @() + if ($ServicePrincipals) { + $OldSPCerts = $ServicePrincipals | Where-Object { + $_.keyCredentials -and $_.keyCredentials.Count -gt 0 + } | ForEach-Object { + $SP = $_ + $OldestCert = $SP.keyCredentials | Where-Object { $_.startDateTime } | ForEach-Object { + [DateTime]$_.startDateTime + } | Sort-Object | Select-Object -First 1 + + if ($OldestCert -and $OldestCert -lt $ThresholdDate) { + [PSCustomObject]@{ + Type = 'ServicePrincipal' + DisplayName = $SP.displayName + AppId = $SP.appId + Id = $SP.id + OldestCertDate = $OldestCert + } + } + } + } + + if ($OldAppCerts.Count -eq 0 -and $OldSPCerts.Count -eq 0) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21992' -TestType 'Identity' -Status 'Passed' -ResultMarkdown "Certificates for applications in your tenant have been issued within $RotationThresholdDays days" -Risk 'High' -Name 'Application certificates must be rotated on a regular basis' -UserImpact 'Low' -ImplementationEffort 'High' -Category 'Application management' + return + } + + $Status = 'Failed' + + $ResultLines = @( + "Found $($OldAppCerts.Count) application(s) and $($OldSPCerts.Count) service principal(s) with certificates not rotated within $RotationThresholdDays days." + '' + "**Certificate rotation threshold:** $RotationThresholdDays days" + '' + ) + + if ($OldAppCerts.Count -gt 0) { + $ResultLines += '**Applications with old certificates:**' + $Top10Apps = $OldAppCerts | Select-Object -First 10 + foreach ($App in $Top10Apps) { + $DaysOld = [Math]::Round(((Get-Date) - $App.OldestCertDate).TotalDays, 0) + $ResultLines += "- $($App.DisplayName) (Certificate age: $DaysOld days)" + } + if ($OldAppCerts.Count -gt 10) { + $ResultLines += "- ... and $($OldAppCerts.Count - 10) more application(s)" + } + $ResultLines += '' + } + + if ($OldSPCerts.Count -gt 0) { + $ResultLines += '**Service principals with old certificates:**' + $Top10SPs = $OldSPCerts | Select-Object -First 10 + foreach ($SP in $Top10SPs) { + $DaysOld = [Math]::Round(((Get-Date) - $SP.OldestCertDate).TotalDays, 0) + $ResultLines += "- $($SP.DisplayName) (Certificate age: $DaysOld days)" + } + if ($OldSPCerts.Count -gt 10) { + $ResultLines += "- ... and $($OldSPCerts.Count - 10) more service principal(s)" + } + $ResultLines += '' + } + + $ResultLines += '**Recommendation:** Rotate certificates regularly to reduce the risk of credential compromise.' + + $Result = $ResultLines -join "`n" + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21992' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'Application certificates must be rotated on a regular basis' -UserImpact 'Low' -ImplementationEffort 'High' -Category 'Application management' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21992' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Application certificates must be rotated on a regular basis' -UserImpact 'Low' -ImplementationEffort 'High' -Category 'Application management' + } +} diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA22128.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA22128.ps1 new file mode 100644 index 000000000000..3c51b79052d4 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA22128.ps1 @@ -0,0 +1,88 @@ +function Invoke-CippTestZTNA22128 { + param($Tenant) + + try { + $Roles = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Roles' + $Guests = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Guests' + + if (-not $Roles -or -not $Guests) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA22128' -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Role or guest user data not found in database' -Risk 'High' -Name 'Guests are not assigned high privileged directory roles' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Application management' + return + } + + if ($Guests.Count -eq 0) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA22128' -TestType 'Identity' -Status 'Passed' -ResultMarkdown 'No guest users found in tenant' -Risk 'High' -Name 'Guests are not assigned high privileged directory roles' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Application management' + return + } + + $GuestIds = $Guests | ForEach-Object { $_.id } + $GuestIdHash = @{} + foreach ($Guest in $Guests) { + $GuestIdHash[$Guest.id] = $Guest + } + + $PrivilegedRoleTemplateIds = @( + '62e90394-69f5-4237-9190-012177145e10' + '194ae4cb-b126-40b2-bd5b-6091b380977d' + 'f28a1f50-f6e7-4571-818b-6a12f2af6b6c' + '29232cdf-9323-42fd-ade2-1d097af3e4de' + 'b1be1c3e-b65d-4f19-8427-f6fa0d97feb9' + '729827e3-9c14-49f7-bb1b-9608f156bbb8' + 'b0f54661-2d74-4c50-afa3-1ec803f12efe' + 'fe930be7-5e62-47db-91af-98c3a49a38b1' + ) + + $GuestsInPrivilegedRoles = @() + foreach ($Role in $Roles) { + if ($Role.roleTemplateId -in $PrivilegedRoleTemplateIds -and $Role.members) { + foreach ($Member in $Role.members) { + if ($GuestIdHash.ContainsKey($Member.id)) { + $GuestsInPrivilegedRoles += [PSCustomObject]@{ + RoleName = $Role.displayName + GuestId = $Member.id + GuestDisplayName = $Member.displayName + GuestUserPrincipalName = $Member.userPrincipalName + } + } + } + } + } + + if ($GuestsInPrivilegedRoles.Count -eq 0) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA22128' -TestType 'Identity' -Status 'Passed' -ResultMarkdown 'Guests with privileged roles were not found. All users with privileged roles are members of the tenant' -Risk 'High' -Name 'Guests are not assigned high privileged directory roles' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Application management' + return + } + + $Status = 'Failed' + + $ResultLines = @( + "Found $($GuestsInPrivilegedRoles.Count) guest user(s) with privileged role assignments." + '' + "**Total guests in tenant:** $($Guests.Count)" + "**Guests with privileged roles:** $($GuestsInPrivilegedRoles.Count)" + '' + '**Guest users in privileged roles:**' + ) + + $RoleGroups = $GuestsInPrivilegedRoles | Group-Object -Property RoleName + foreach ($RoleGroup in $RoleGroups) { + $ResultLines += "" + $ResultLines += "**$($RoleGroup.Name)** ($($RoleGroup.Count) guest(s)):" + foreach ($Guest in $RoleGroup.Group) { + $ResultLines += "- $($Guest.GuestDisplayName) ($($Guest.GuestUserPrincipalName))" + } + } + + $ResultLines += '' + $ResultLines += '**Security concern:** Guest users should not have privileged directory roles. Consider using separate admin accounts for external administrators or removing privileged access.' + + $Result = $ResultLines -join "`n" + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA22128' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'Guests are not assigned high privileged directory roles' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Application management' + } + catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA22128' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Guests are not assigned high privileged directory roles' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Application management' + } +} From 97f1ac601ff0cf0f49a21acc479856324c45b65c Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Wed, 24 Dec 2025 13:42:27 +0100 Subject: [PATCH 040/503] More tests --- .../Public/Set-CIPPDBCacheIntunePolicies.ps1 | 42 ++++++--- .../Public/Tests/Invoke-CippTestZTNA24540.ps1 | 68 ++++++++++++++ .../Public/Tests/Invoke-CippTestZTNA24550.ps1 | 92 +++++++++++++++++++ .../Public/Tests/Invoke-CippTestZTNA24552.ps1 | 89 ++++++++++++++++++ .../Public/Tests/Invoke-CippTestZTNA24560.ps1 | 66 +++++++++++++ .../Public/Tests/Invoke-CippTestZTNA24564.ps1 | 52 +++++++++++ .../Public/Tests/Invoke-CippTestZTNA24568.ps1 | 54 +++++++++++ .../Public/Tests/Invoke-CippTestZTNA24574.ps1 | 68 ++++++++++++++ .../Public/Tests/Invoke-CippTestZTNA24575.ps1 | 49 ++++++++++ .../Public/Tests/Invoke-CippTestZTNA24784.ps1 | 49 ++++++++++ 10 files changed, 618 insertions(+), 11 deletions(-) create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24540.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24550.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24552.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24560.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24564.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24568.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24574.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24575.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24784.ps1 diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheIntunePolicies.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheIntunePolicies.ps1 index eb74ae8b2c13..cac6e1120670 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheIntunePolicies.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheIntunePolicies.ps1 @@ -23,23 +23,43 @@ function Set-CIPPDBCacheIntunePolicies { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching Intune policies' -sev Info $PolicyTypes = @( - @{ Type = 'DeviceCompliancePolicies'; Uri = 'https://graph.microsoft.com/beta/deviceManagement/deviceCompliancePolicies?$top=999' } - @{ Type = 'DeviceConfigurations'; Uri = 'https://graph.microsoft.com/beta/deviceManagement/deviceConfigurations?$top=999' } - @{ Type = 'ConfigurationPolicies'; Uri = 'https://graph.microsoft.com/beta/deviceManagement/configurationPolicies?$top=999' } - @{ Type = 'GroupPolicyConfigurations'; Uri = 'https://graph.microsoft.com/beta/deviceManagement/groupPolicyConfigurations?$top=999' } - @{ Type = 'MobileAppConfigurations'; Uri = 'https://graph.microsoft.com/beta/deviceManagement/mobileAppConfigurations?$top=999' } - @{ Type = 'AppProtectionPolicies'; Uri = 'https://graph.microsoft.com/beta/deviceAppManagement/managedAppPolicies?$top=999' } - @{ Type = 'WindowsAutopilotDeploymentProfiles'; Uri = 'https://graph.microsoft.com/beta/deviceManagement/windowsAutopilotDeploymentProfiles?$top=999' } - @{ Type = 'DeviceEnrollmentConfigurations'; Uri = 'https://graph.microsoft.com/beta/deviceManagement/deviceEnrollmentConfigurations?$top=999' } - @{ Type = 'DeviceManagementScripts'; Uri = 'https://graph.microsoft.com/beta/deviceManagement/deviceManagementScripts?$top=999' } - @{ Type = 'MobileApps'; Uri = 'https://graph.microsoft.com/beta/deviceAppManagement/mobileApps?$top=999' } + @{ Type = 'DeviceCompliancePolicies'; Uri = 'https://graph.microsoft.com/beta/deviceManagement/deviceCompliancePolicies'; SupportsExpand = $true } + @{ Type = 'DeviceConfigurations'; Uri = 'https://graph.microsoft.com/beta/deviceManagement/deviceConfigurations'; SupportsExpand = $true } + @{ Type = 'ConfigurationPolicies'; Uri = 'https://graph.microsoft.com/beta/deviceManagement/configurationPolicies'; SupportsExpand = $true; ExpandSettings = $true } + @{ Type = 'GroupPolicyConfigurations'; Uri = 'https://graph.microsoft.com/beta/deviceManagement/groupPolicyConfigurations'; SupportsExpand = $true } + @{ Type = 'MobileAppConfigurations'; Uri = 'https://graph.microsoft.com/beta/deviceManagement/mobileAppConfigurations'; SupportsExpand = $true } + @{ Type = 'AppProtectionPolicies'; Uri = 'https://graph.microsoft.com/beta/deviceAppManagement/managedAppPolicies'; SupportsExpand = $false } + @{ Type = 'WindowsAutopilotDeploymentProfiles'; Uri = 'https://graph.microsoft.com/beta/deviceManagement/windowsAutopilotDeploymentProfiles'; SupportsExpand = $true } + @{ Type = 'DeviceEnrollmentConfigurations'; Uri = 'https://graph.microsoft.com/beta/deviceManagement/deviceEnrollmentConfigurations'; SupportsExpand = $false } + @{ Type = 'DeviceManagementScripts'; Uri = 'https://graph.microsoft.com/beta/deviceManagement/deviceManagementScripts'; SupportsExpand = $true } + @{ Type = 'MobileApps'; Uri = 'https://graph.microsoft.com/beta/deviceAppManagement/mobileApps'; SupportsExpand = $false } ) foreach ($PolicyType in $PolicyTypes) { try { - $Policies = New-GraphGetRequest -uri $PolicyType.Uri -tenantid $TenantFilter + $UriWithParams = $PolicyType.Uri + '?$top=999' + if ($PolicyType.SupportsExpand) { + $UriWithParams += '&$expand=assignments' + } + if ($PolicyType.ExpandSettings) { + $UriWithParams += ',settings' + } + + $Policies = New-GraphGetRequest -uri $UriWithParams -tenantid $TenantFilter if ($Policies) { + if (-not $PolicyType.SupportsExpand) { + foreach ($Policy in $Policies) { + try { + $AssignmentUri = "$($PolicyType.Uri)/$($Policy.id)/assignments" + $Assignments = New-GraphGetRequest -uri $AssignmentUri -tenantid $TenantFilter + $Policy | Add-Member -NotePropertyName 'assignments' -NotePropertyValue $Assignments -Force + } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to get assignments for $($Policy.id): $($_.Exception.Message)" -sev Verbose + } + } + } + Add-CIPPDbItem -TenantFilter $TenantFilter -Type "Intune$($PolicyType.Type)" -Data $Policies Add-CIPPDbItem -TenantFilter $TenantFilter -Type "Intune$($PolicyType.Type)" -Data $Policies -Count Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($Policies.Count) $($PolicyType.Type)" -sev Info diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24540.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24540.ps1 new file mode 100644 index 000000000000..8d54f88181fb --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24540.ps1 @@ -0,0 +1,68 @@ +function Invoke-CippTestZTNA24540 { + param($Tenant) + + try { + $ConfigurationPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'IntuneConfigurationPolicies' + if (-not $ConfigurationPolicies) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA24540' -TestType 'Devices' -Status 'Investigate' -ResultMarkdown 'Intune configuration policies not found in database' -Risk 'High' -Name 'Windows Firewall policies protect against unauthorized network access' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Device' + return + } + + $FirewallPolicies = $ConfigurationPolicies | Where-Object { + $_.templateReference -and $_.templateReference.templateFamily -eq 'endpointSecurityFirewall' + } + + if ($FirewallPolicies.Count -eq 0) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA24540' -TestType 'Devices' -Status 'Failed' -ResultMarkdown 'No Windows Firewall configuration policies found' -Risk 'High' -Name 'Windows Firewall policies protect against unauthorized network access' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Device' + return + } + + $AssignedPolicies = $FirewallPolicies | Where-Object { + $_.assignments -and $_.assignments.Count -gt 0 + } + + if ($AssignedPolicies.Count -gt 0) { + $Status = 'Passed' + $ResultLines = @( + 'At least one Windows Firewall policy is created and assigned to a group.' + '' + '**Windows Firewall Configuration Policies:**' + '' + '| Policy Name | Status | Assignment Count |' + '| :---------- | :----- | :--------------- |' + ) + + foreach ($Policy in $FirewallPolicies) { + $PolicyStatus = if ($Policy.assignments -and $Policy.assignments.Count -gt 0) { + '✅ Assigned' + } else { + '❌ Not assigned' + } + $AssignmentCount = if ($Policy.assignments) { $Policy.assignments.Count } else { 0 } + $ResultLines += "| $($Policy.name) | $PolicyStatus | $AssignmentCount |" + } + + $Result = $ResultLines -join "`n" + } else { + $Status = 'Failed' + $ResultLines = @( + 'There are no firewall policies assigned to any groups.' + '' + '**Windows Firewall Configuration Policies (Unassigned):**' + '' + ) + + foreach ($Policy in $FirewallPolicies) { + $ResultLines += "- $($Policy.name)" + } + + $Result = $ResultLines -join "`n" + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA24540' -TestType 'Devices' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'Windows Firewall policies protect against unauthorized network access' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Device' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA24540' -TestType 'Devices' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Windows Firewall policies protect against unauthorized network access' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Device' + } +} diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24550.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24550.ps1 new file mode 100644 index 000000000000..a70270bb62cd --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24550.ps1 @@ -0,0 +1,92 @@ +function Invoke-CippTestZTNA24550 { + param($Tenant) + + try { + $ConfigurationPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'IntuneConfigurationPolicies' + if (-not $ConfigurationPolicies) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA24550' -TestType 'Devices' -Status 'Investigate' -ResultMarkdown 'Intune configuration policies not found in database' -Risk 'High' -Name 'Data on Windows is protected by BitLocker encryption' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Device' + return + } + + $WindowsPolicies = $ConfigurationPolicies | Where-Object { + $_.platforms -match 'windows10' + } + + $WindowsBitLockerPolicies = @() + foreach ($WindowsPolicy in $WindowsPolicies) { + $ValidSettingValues = @('device_vendor_msft_bitlocker_requiredeviceencryption_1') + + if ($WindowsPolicy.settings.settinginstance.choicesettingvalue.value) { + $PolicySettingValues = $WindowsPolicy.settings.settinginstance.choicesettingvalue.value + if ($PolicySettingValues -isnot [array]) { + $PolicySettingValues = @($PolicySettingValues) + } + + $HasValidSetting = $false + foreach ($SettingValue in $PolicySettingValues) { + if ($ValidSettingValues -contains $SettingValue) { + $HasValidSetting = $true + break + } + } + + if ($HasValidSetting) { + $WindowsBitLockerPolicies += $WindowsPolicy + } + } + } + + $AssignedPolicies = $WindowsBitLockerPolicies | Where-Object { + $_.assignments -and $_.assignments.Count -gt 0 + } + + if ($AssignedPolicies.Count -gt 0) { + $Status = 'Passed' + $ResultLines = @( + "At least one Windows BitLocker policy is configured and assigned." + '' + "**Windows BitLocker Policies:**" + '' + "| Policy Name | Status | Assignment Count |" + "| :---------- | :----- | :--------------- |" + ) + + foreach ($Policy in $WindowsBitLockerPolicies) { + $PolicyStatus = if ($Policy.assignments -and $Policy.assignments.Count -gt 0) { + '✅ Assigned' + } else { + '❌ Not assigned' + } + $AssignmentCount = if ($Policy.assignments) { $Policy.assignments.Count } else { 0 } + $ResultLines += "| $($Policy.name) | $PolicyStatus | $AssignmentCount |" + } + + $Result = $ResultLines -join "`n" + } + else { + $Status = 'Failed' + if ($WindowsBitLockerPolicies.Count -gt 0) { + $ResultLines = @( + "Windows BitLocker policies exist but none are assigned." + '' + "**Unassigned BitLocker Policies:**" + '' + ) + foreach ($Policy in $WindowsBitLockerPolicies) { + $ResultLines += "- $($Policy.name)" + } + } + else { + $ResultLines = @("No Windows BitLocker policy is configured or assigned.") + } + $Result = $ResultLines -join "`n" + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA24550' -TestType 'Devices' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'Data on Windows is protected by BitLocker encryption' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Device' + } + catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA24550' -TestType 'Devices' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Data on Windows is protected by BitLocker encryption' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Device' + } +} diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24552.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24552.ps1 new file mode 100644 index 000000000000..d21dfd9c54c4 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24552.ps1 @@ -0,0 +1,89 @@ +function Invoke-CippTestZTNA24552 { + param($Tenant) + + try { + $ConfigurationPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'IntuneConfigurationPolicies' + if (-not $ConfigurationPolicies) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA24552' -TestType 'Devices' -Status 'Investigate' -ResultMarkdown 'Intune configuration policies not found in database' -Risk 'High' -Name 'Data on macOS is protected by firewall' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Device' + return + } + + $MacOSPolicies = $ConfigurationPolicies | Where-Object { + $_.platforms -match 'macOS' + } + + $MacOSFirewallPolicies = @() + foreach ($MacOSPolicy in $MacOSPolicies) { + $ValidSettingValues = @('com.apple.security.firewall_enablefirewall_true') + + if ($MacOSPolicy.settings.settinginstance.choicesettingvalue.value) { + $PolicySettingValues = $MacOSPolicy.settings.settinginstance.choicesettingvalue.value + if ($PolicySettingValues -isnot [array]) { + $PolicySettingValues = @($PolicySettingValues) + } + + $HasValidSetting = $false + foreach ($SettingValue in $PolicySettingValues) { + if ($ValidSettingValues -contains $SettingValue) { + $HasValidSetting = $true + break + } + } + + if ($HasValidSetting) { + $MacOSFirewallPolicies += $MacOSPolicy + } + } + } + + $AssignedPolicies = $MacOSFirewallPolicies | Where-Object { + $_.assignments -and $_.assignments.Count -gt 0 + } + + if ($AssignedPolicies.Count -gt 0) { + $Status = 'Passed' + $ResultLines = @( + 'At least one macOS Firewall policy is configured and assigned.' + '' + '**macOS Firewall Policies:**' + '' + '| Policy Name | Status | Assignment Count |' + '| :---------- | :----- | :--------------- |' + ) + + foreach ($Policy in $MacOSFirewallPolicies) { + $PolicyStatus = if ($Policy.assignments -and $Policy.assignments.Count -gt 0) { + '✅ Assigned' + } else { + '❌ Not assigned' + } + $AssignmentCount = if ($Policy.assignments) { $Policy.assignments.Count } else { 0 } + $ResultLines += "| $($Policy.name) | $PolicyStatus | $AssignmentCount |" + } + + $Result = $ResultLines -join "`n" + } else { + $Status = 'Failed' + if ($MacOSFirewallPolicies.Count -gt 0) { + $ResultLines = @( + 'macOS Firewall policies exist but none are assigned.' + '' + '**Unassigned Firewall Policies:**' + '' + ) + foreach ($Policy in $MacOSFirewallPolicies) { + $ResultLines += "- $($Policy.name)" + } + } else { + $ResultLines = @('No macOS Firewall policy is configured or assigned.') + } + $Result = $ResultLines -join "`n" + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA24552' -TestType 'Devices' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'Data on macOS is protected by firewall' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Device' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA24552' -TestType 'Devices' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Data on macOS is protected by firewall' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Device' + } +} diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24560.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24560.ps1 new file mode 100644 index 000000000000..d35e388c1ea0 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24560.ps1 @@ -0,0 +1,66 @@ +function Invoke-CippTestZTNA24560 { + param($Tenant) + + try { + $ConfigPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'IntuneConfigurationPolicies' + if (-not $ConfigPolicies) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA24560' -TestType 'Devices' -Status 'Investigate' -ResultMarkdown 'Configuration policy data not found in database' -Risk 'High' -Name 'Local administrator credentials on Windows are protected by Windows LAPS' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Device' + return + } + + $WindowsPolicies = $ConfigPolicies | Where-Object { + $_.templateReference.templateFamily -eq 'endpointSecurityAccountProtection' -and + $_.platforms -like '*windows10*' + } + + if (-not $WindowsPolicies) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA24560' -TestType 'Devices' -Status 'Failed' -ResultMarkdown 'No Windows LAPS policies found' -Risk 'High' -Name 'Local administrator credentials on Windows are protected by Windows LAPS' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Device' + return + } + + $LapsPolicies = $WindowsPolicies | Where-Object { + $settingIds = $_.settings.settingInstance.settingDefinitionId + $settingIds -contains 'device_vendor_msft_laps_policies_backupdirectory' -or + $settingIds -contains 'device_vendor_msft_laps_policies_automaticaccountmanagementenabled' + } + + if (-not $LapsPolicies) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA24560' -TestType 'Devices' -Status 'Failed' -ResultMarkdown 'No LAPS policies configured' -Risk 'High' -Name 'Local administrator credentials on Windows are protected by Windows LAPS' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Device' + return + } + + $CompliantPolicies = $LapsPolicies | Where-Object { + $settingIds = $_.settings.settingInstance.settingDefinitionId + $choiceValues = $_.settings.settingInstance.choiceSettingValue.value + + $hasBackupDir = $settingIds -contains 'device_vendor_msft_laps_policies_backupdirectory' + $hasEntraBackup = $choiceValues -contains 'device_vendor_msft_laps_policies_backupdirectory_1' + $hasAdBackup = $choiceValues -contains 'device_vendor_msft_laps_policies_backupdirectory_2' + $hasAutoMgmt = $choiceValues -contains 'device_vendor_msft_laps_policies_automaticaccountmanagementenabled_true' + + ($hasBackupDir -and ($hasEntraBackup -or $hasAdBackup) -and $hasAutoMgmt) + } + + $AssignedCompliantPolicies = $CompliantPolicies | Where-Object { + $_.assignments -and $_.assignments.Count -gt 0 + } + + if ($AssignedCompliantPolicies) { + $Status = 'Passed' + $Result = "Cloud LAPS policy is assigned and enforced. Found $($AssignedCompliantPolicies.Count) compliant and assigned policy/policies" + } else { + $Status = 'Failed' + if ($CompliantPolicies) { + $Result = "Cloud LAPS policy exists but is not assigned. Found $($CompliantPolicies.Count) compliant but unassigned policy/policies" + } else { + $Result = 'Cloud LAPS policy is not configured correctly or not enforced' + } + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA24560' -TestType 'Devices' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'Local administrator credentials on Windows are protected by Windows LAPS' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Device' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA24560' -TestType 'Devices' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Local administrator credentials on Windows are protected by Windows LAPS' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Device' + } +} diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24564.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24564.ps1 new file mode 100644 index 000000000000..fcd0167a7c89 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24564.ps1 @@ -0,0 +1,52 @@ +function Invoke-CippTestZTNA24564 { + param($Tenant) + + try { + $ConfigPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'IntuneConfigurationPolicies' + if (-not $ConfigPolicies) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA24564' -TestType 'Devices' -Status 'Investigate' -ResultMarkdown 'Configuration policy data not found in database' -Risk 'High' -Name 'Local account usage on Windows is restricted to reduce unauthorized access' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Device' + return + } + + $WindowsPolicies = $ConfigPolicies | Where-Object { + $_.platforms -like '*windows10*' + } + + if (-not $WindowsPolicies) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA24564' -TestType 'Devices' -Status 'Failed' -ResultMarkdown 'No Windows policies found' -Risk 'High' -Name 'Local account usage on Windows is restricted to reduce unauthorized access' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Device' + return + } + + $LocalUsersGroupsPolicies = $WindowsPolicies | Where-Object { + $settingIds = $_.settings.settingInstance.settingDefinitionId + if ($settingIds -is [string]) { + $settingIds -eq 'device_vendor_msft_policy_config_localusersandgroups_configure' + } else { + $settingIds -contains 'device_vendor_msft_policy_config_localusersandgroups_configure' + } + } + + if (-not $LocalUsersGroupsPolicies) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA24564' -TestType 'Devices' -Status 'Failed' -ResultMarkdown 'No Local Users and Groups policy configured' -Risk 'High' -Name 'Local account usage on Windows is restricted to reduce unauthorized access' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Device' + return + } + + $AssignedPolicies = $LocalUsersGroupsPolicies | Where-Object { + $_.assignments -and $_.assignments.Count -gt 0 + } + + if ($AssignedPolicies) { + $Status = 'Passed' + $Result = "At least one Local Users and Groups policy is configured and assigned. Found $($AssignedPolicies.Count) assigned policy/policies" + } else { + $Status = 'Failed' + $Result = "Local Users and Groups policy exists but is not assigned. Found $($LocalUsersGroupsPolicies.Count) unassigned policy/policies" + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA24564' -TestType 'Devices' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'Local account usage on Windows is restricted to reduce unauthorized access' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Device' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA24564' -TestType 'Devices' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Local account usage on Windows is restricted to reduce unauthorized access' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Device' + } +} diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24568.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24568.ps1 new file mode 100644 index 000000000000..5f7743f65dfa --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24568.ps1 @@ -0,0 +1,54 @@ +function Invoke-CippTestZTNA24568 { + param($Tenant) + + try { + $ConfigPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'IntuneConfigurationPolicies' + if (-not $ConfigPolicies) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA24568' -TestType 'Devices' -Status 'Investigate' -ResultMarkdown 'Configuration policy data not found in database' -Risk 'Medium' -Name 'Platform SSO is configured to strengthen authentication on macOS devices' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Tenant' + return + } + + $MacOSPolicies = $ConfigPolicies | Where-Object { + $_.platforms -like '*macOS*' -and + $_.technologies -like '*mdm*' -and + $_.technologies -like '*appleRemoteManagement*' + } + + if (-not $MacOSPolicies) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA24568' -TestType 'Devices' -Status 'Failed' -ResultMarkdown 'No macOS policies found' -Risk 'Medium' -Name 'Platform SSO is configured to strengthen authentication on macOS devices' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Tenant' + return + } + + $SSOPolicies = $MacOSPolicies | Where-Object { + $children = $_.settings.settingInstance.groupSettingCollectionValue.children + $extensionIdSetting = $children | Where-Object { + $_.settingDefinitionId -eq 'com.apple.extensiblesso_extensionidentifier' + } + $extensionValue = $extensionIdSetting.simpleSettingValue.value + $extensionValue -eq 'com.microsoft.CompanyPortalMac.ssoextension' + } + + if (-not $SSOPolicies) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA24568' -TestType 'Devices' -Status 'Failed' -ResultMarkdown 'No macOS SSO policies configured with Microsoft Company Portal extension' -Risk 'Medium' -Name 'Platform SSO is configured to strengthen authentication on macOS devices' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Tenant' + return + } + + $AssignedSSOPolicies = $SSOPolicies | Where-Object { + $_.assignments -and $_.assignments.Count -gt 0 + } + + if ($AssignedSSOPolicies) { + $Status = 'Passed' + $Result = "macOS SSO policies are configured and assigned. Found $($AssignedSSOPolicies.Count) assigned policy/policies" + } else { + $Status = 'Failed' + $Result = "macOS SSO policy exists but is not assigned. Found $($SSOPolicies.Count) unassigned policy/policies" + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA24568' -TestType 'Devices' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'Platform SSO is configured to strengthen authentication on macOS devices' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Tenant' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA24568' -TestType 'Devices' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'Platform SSO is configured to strengthen authentication on macOS devices' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Tenant' + } +} diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24574.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24574.ps1 new file mode 100644 index 000000000000..2fc7952d4ac6 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24574.ps1 @@ -0,0 +1,68 @@ +function Invoke-CippTestZTNA24574 { + param($Tenant) + + try { + $ConfigPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'IntuneConfigurationPolicies' + if (-not $ConfigPolicies) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA24574' -TestType 'Devices' -Status 'Investigate' -ResultMarkdown 'Configuration policy data not found in database' -Risk 'High' -Name 'Attack Surface Reduction rules are applied to Windows devices to prevent exploitation of vulnerable system components' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Device' + return + } + + $Win10MdmSensePolicies = $ConfigPolicies | Where-Object { + $_.platforms -like '*windows10*' -and + $_.technologies -like '*mdm*' -and + $_.technologies -like '*microsoftSense*' + } + + if (-not $Win10MdmSensePolicies) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA24574' -TestType 'Devices' -Status 'Failed' -ResultMarkdown 'No Windows ASR policies found' -Risk 'High' -Name 'Attack Surface Reduction rules are applied to Windows devices to prevent exploitation of vulnerable system components' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Device' + return + } + + $ASRPolicies = $Win10MdmSensePolicies | Where-Object { + $settingIds = $_.settings.settingInstance.settingDefinitionId + $settingIds -contains 'device_vendor_msft_policy_config_defender_attacksurfacereductionrules' + } + + if (-not $ASRPolicies) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA24574' -TestType 'Devices' -Status 'Failed' -ResultMarkdown 'No Attack Surface Reduction policies found' -Risk 'High' -Name 'Attack Surface Reduction rules are applied to Windows devices to prevent exploitation of vulnerable system components' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Device' + return + } + + $ObfuscatedScriptPolicies = $ASRPolicies | Where-Object { + $children = $_.settings.settingInstance.groupSettingCollectionValue.children + $settingId = 'device_vendor_msft_policy_config_defender_attacksurfacereductionrules_blockexecutionofpotentiallyobfuscatedscripts' + $matchingSetting = $children | Where-Object { $_.settingDefinitionId -eq $settingId } + $value = $matchingSetting.choiceSettingValue.value + $value -like '*_block' -or $value -like '*_warn' + } + + $Win32MacroPolicies = $ASRPolicies | Where-Object { + $children = $_.settings.settingInstance.groupSettingCollectionValue.children + $settingId = 'device_vendor_msft_policy_config_defender_attacksurfacereductionrules_blockwin32apicallsfromofficemacros' + $matchingSetting = $children | Where-Object { $_.settingDefinitionId -eq $settingId } + $value = $matchingSetting.choiceSettingValue.value + $value -like '*_block' -or $value -like '*_warn' + } + + $AssignedObfuscated = $ObfuscatedScriptPolicies | Where-Object { $_.assignments -and $_.assignments.Count -gt 0 } + $AssignedWin32Macro = $Win32MacroPolicies | Where-Object { $_.assignments -and $_.assignments.Count -gt 0 } + + if ($AssignedObfuscated -and $AssignedWin32Macro) { + $Status = 'Passed' + $Result = 'ASR policies are configured and assigned with required rules (obfuscated scripts and Win32 API calls from macros)' + } elseif ($AssignedObfuscated -or $AssignedWin32Macro) { + $Status = 'Failed' + $Result = "ASR policies partially configured. Missing: $(if (-not $AssignedObfuscated) { 'obfuscated scripts rule ' })$(if (-not $AssignedWin32Macro) { 'Win32 API calls rule' })" + } else { + $Status = 'Failed' + $Result = 'ASR policies found but not properly configured or assigned for required rules' + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA24574' -TestType 'Devices' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'Attack Surface Reduction rules are applied to Windows devices to prevent exploitation of vulnerable system components' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Device' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA24574' -TestType 'Devices' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Attack Surface Reduction rules are applied to Windows devices to prevent exploitation of vulnerable system components' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Device' + } +} diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24575.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24575.ps1 new file mode 100644 index 000000000000..33da19cdc512 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24575.ps1 @@ -0,0 +1,49 @@ +function Invoke-CippTestZTNA24575 { + param($Tenant) + + try { + $ConfigPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'IntuneConfigurationPolicies' + if (-not $ConfigPolicies) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA24575' -TestType 'Devices' -Status 'Investigate' -ResultMarkdown 'Configuration policy data not found in database' -Risk 'High' -Name 'Defender Antivirus policies protect Windows devices from malware' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Device' + return + } + + $MdmSensePolicies = $ConfigPolicies | Where-Object { + $_.platforms -like '*windows10*' -and + $_.technologies -like '*mdm*' -and + $_.technologies -like '*microsoftSense*' + } + + if (-not $MdmSensePolicies) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA24575' -TestType 'Devices' -Status 'Failed' -ResultMarkdown 'No Windows Defender policies found' -Risk 'High' -Name 'Defender Antivirus policies protect Windows devices from malware' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Device' + return + } + + $AVPolicies = $MdmSensePolicies | Where-Object { + $_.templateReference.templateFamily -eq 'endpointSecurityAntivirus' + } + + if (-not $AVPolicies) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA24575' -TestType 'Devices' -Status 'Failed' -ResultMarkdown 'No Windows Defender Antivirus policies found' -Risk 'High' -Name 'Defender Antivirus policies protect Windows devices from malware' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Device' + return + } + + $AssignedPolicies = $AVPolicies | Where-Object { + $_.assignments -and $_.assignments.Count -gt 0 + } + + if ($AssignedPolicies) { + $Status = 'Passed' + $Result = "Windows Defender Antivirus policies are configured and assigned. Found $($AssignedPolicies.Count) assigned policy/policies" + } else { + $Status = 'Failed' + $Result = "Windows Defender Antivirus policies exist but are not assigned. Found $($AVPolicies.Count) unassigned policy/policies" + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA24575' -TestType 'Devices' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'Defender Antivirus policies protect Windows devices from malware' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Device' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA24575' -TestType 'Devices' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Defender Antivirus policies protect Windows devices from malware' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Device' + } +} diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24784.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24784.ps1 new file mode 100644 index 000000000000..8c94ae6a0a26 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24784.ps1 @@ -0,0 +1,49 @@ +function Invoke-CippTestZTNA24784 { + param($Tenant) + + try { + $ConfigPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'IntuneConfigurationPolicies' + if (-not $ConfigPolicies) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA24784' -TestType 'Devices' -Status 'Investigate' -ResultMarkdown 'Configuration policy data not found in database' -Risk 'High' -Name 'Defender Antivirus policies protect macOS devices from malware' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Device' + return + } + + $MdmMacOSSensePolicies = $ConfigPolicies | Where-Object { + $_.platforms -like '*macOS*' -and + $_.technologies -like '*mdm*' -and + $_.technologies -like '*microsoftSense*' + } + + if (-not $MdmMacOSSensePolicies) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA24784' -TestType 'Devices' -Status 'Failed' -ResultMarkdown 'No macOS Defender policies found' -Risk 'High' -Name 'Defender Antivirus policies protect macOS devices from malware' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Device' + return + } + + $AVPolicies = $MdmMacOSSensePolicies | Where-Object { + $_.templateReference.templateFamily -eq 'endpointSecurityAntivirus' + } + + if (-not $AVPolicies) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA24784' -TestType 'Devices' -Status 'Failed' -ResultMarkdown 'No Defender Antivirus policies for macOS found' -Risk 'High' -Name 'Defender Antivirus policies protect macOS devices from malware' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Device' + return + } + + $AssignedPolicies = $AVPolicies | Where-Object { + $_.assignments -and $_.assignments.Count -gt 0 + } + + if ($AssignedPolicies) { + $Status = 'Passed' + $Result = "Defender Antivirus policies for macOS are configured and assigned. Found $($AssignedPolicies.Count) assigned policy/policies" + } else { + $Status = 'Failed' + $Result = "Defender Antivirus policies for macOS exist but are not assigned. Found $($AVPolicies.Count) unassigned policy/policies" + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA24784' -TestType 'Devices' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'Defender Antivirus policies protect macOS devices from malware' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Device' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA24784' -TestType 'Devices' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Defender Antivirus policies protect macOS devices from malware' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Device' + } +} From 2f885bb66b56c4ceaf7162a88b5b6a781ce9e734 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Wed, 24 Dec 2025 14:01:05 +0100 Subject: [PATCH 041/503] Updated Tests --- Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21847.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21847.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21847.ps1 index 3b9e7e4cd7d9..317342df8cf2 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21847.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21847.ps1 @@ -5,14 +5,14 @@ function Invoke-CippTestZTNA21847 { try { # Check if tenant has on-premises sync - $Settings = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Settings' + $Settings = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Organization' if (-not $Settings) { Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Organization details not found' -Risk 'High' -Name 'Password protection for on-premises is enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Credential management' return } - $Org = $Organization[0] + $Org = $Settings[0] if ($Org.onPremisesSyncEnabled -ne $true) { Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Passed' -ResultMarkdown '✅ **Pass**: This tenant is not synchronized to an on-premises environment.' -Risk 'High' -Name 'Password protection for on-premises is enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Credential management' From 291fbf9b6d4a5edb506207530937c567fabaa90f Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Wed, 24 Dec 2025 15:34:46 +0100 Subject: [PATCH 042/503] Tested first files --- .../Push-CIPPDBCacheData.ps1 | 46 +++++++++++++++++++ .../Set-CIPPDBCacheAppRoleAssignments.ps1 | 5 +- ...t-CIPPDBCacheConditionalAccessPolicies.ps1 | 2 +- .../CIPPCore/Public/Set-CIPPDBCacheRoles.ps1 | 4 +- .../Public/Tests/Invoke-CippTestZTNA21772.ps1 | 2 +- .../Public/Tests/Invoke-CippTestZTNA21773.ps1 | 2 +- .../Public/Tests/Invoke-CippTestZTNA21774.ps1 | 2 +- .../Public/Tests/Invoke-CippTestZTNA21776.ps1 | 2 +- .../Public/Tests/Invoke-CippTestZTNA21780.ps1 | 2 +- .../Public/Tests/Invoke-CippTestZTNA21783.ps1 | 2 +- .../Public/Tests/Invoke-CippTestZTNA21784.ps1 | 43 +++++++++++++---- .../Public/Tests/Invoke-CippTestZTNA21786.ps1 | 2 +- .../Public/Tests/Invoke-CippTestZTNA21787.ps1 | 2 +- .../Public/Tests/Invoke-CippTestZTNA21790.ps1 | 2 +- .../Public/Tests/Invoke-CippTestZTNA21791.ps1 | 4 +- .../Public/Tests/Invoke-CippTestZTNA21792.ps1 | 2 +- .../Public/Tests/Invoke-CippTestZTNA21793.ps1 | 2 +- .../Public/Tests/Invoke-CippTestZTNA21796.ps1 | 6 +-- .../Public/Tests/Invoke-CippTestZTNA21797.ps1 | 2 +- .../Public/Tests/Invoke-CippTestZTNA21799.ps1 | 5 +- .../Public/Tests/Invoke-CippTestZTNA21802.ps1 | 2 +- .../Public/Tests/Invoke-CippTestZTNA21803.ps1 | 2 +- 22 files changed, 106 insertions(+), 37 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 index 3ee632663761..9dad231062c0 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 @@ -18,186 +18,232 @@ function Push-CIPPDBCacheData { try { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Starting database cache collection for tenant' -sev Info + Write-Host 'Getting cache for Users' try { Set-CIPPDBCacheUsers -TenantFilter $TenantFilter } catch { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Users collection failed: $($_.Exception.Message)" -sev Error } + Write-Host 'Getting cache for Groups' try { Set-CIPPDBCacheGroups -TenantFilter $TenantFilter } catch { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Groups collection failed: $($_.Exception.Message)" -sev Error } + Write-Host 'Getting cache for Guests' try { Set-CIPPDBCacheGuests -TenantFilter $TenantFilter } catch { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Guests collection failed: $($_.Exception.Message)" -sev Error } + Write-Host 'Getting cache for ServicePrincipals' try { Set-CIPPDBCacheServicePrincipals -TenantFilter $TenantFilter } catch { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ServicePrincipals collection failed: $($_.Exception.Message)" -sev Error } + Write-Host 'Getting cache for Apps' try { Set-CIPPDBCacheApps -TenantFilter $TenantFilter } catch { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Apps collection failed: $($_.Exception.Message)" -sev Error } + Write-Host 'Getting cache for Devices' try { Set-CIPPDBCacheDevices -TenantFilter $TenantFilter } catch { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Devices collection failed: $($_.Exception.Message)" -sev Error } + Write-Host 'Getting cache for ManagedDevices' try { Set-CIPPDBCacheManagedDevices -TenantFilter $TenantFilter } catch { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ManagedDevices collection failed: $($_.Exception.Message)" -sev Error } + Write-Host 'Getting cache for Organization' try { Set-CIPPDBCacheOrganization -TenantFilter $TenantFilter } catch { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Organization collection failed: $($_.Exception.Message)" -sev Error } + Write-Host 'Getting cache for Roles' try { Set-CIPPDBCacheRoles -TenantFilter $TenantFilter } catch { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Roles collection failed: $($_.Exception.Message)" -sev Error } + Write-Host 'Getting cache for AdminConsentRequestPolicy' try { Set-CIPPDBCacheAdminConsentRequestPolicy -TenantFilter $TenantFilter } catch { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "AdminConsentRequestPolicy collection failed: $($_.Exception.Message)" -sev Error } + Write-Host 'Getting cache for AuthorizationPolicy' try { Set-CIPPDBCacheAuthorizationPolicy -TenantFilter $TenantFilter } catch { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "AuthorizationPolicy collection failed: $($_.Exception.Message)" -sev Error } + Write-Host 'Getting cache for AuthenticationMethodsPolicy' try { Set-CIPPDBCacheAuthenticationMethodsPolicy -TenantFilter $TenantFilter } catch { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "AuthenticationMethodsPolicy collection failed: $($_.Exception.Message)" -sev Error } + Write-Host 'Getting cache for DeviceSettings' try { Set-CIPPDBCacheDeviceSettings -TenantFilter $TenantFilter } catch { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "DeviceSettings collection failed: $($_.Exception.Message)" -sev Error } + Write-Host 'Getting cache for DirectoryRecommendations' try { Set-CIPPDBCacheDirectoryRecommendations -TenantFilter $TenantFilter } catch { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "DirectoryRecommendations collection failed: $($_.Exception.Message)" -sev Error } + Write-Host 'Getting cache for CrossTenantAccessPolicy' try { Set-CIPPDBCacheCrossTenantAccessPolicy -TenantFilter $TenantFilter } catch { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "CrossTenantAccessPolicy collection failed: $($_.Exception.Message)" -sev Error } + Write-Host 'Getting cache for DefaultAppManagementPolicy' try { Set-CIPPDBCacheDefaultAppManagementPolicy -TenantFilter $TenantFilter } catch { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "DefaultAppManagementPolicy collection failed: $($_.Exception.Message)" -sev Error } + Write-Host 'Getting cache for Settings' try { Set-CIPPDBCacheSettings -TenantFilter $TenantFilter } catch { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Settings collection failed: $($_.Exception.Message)" -sev Error } + Write-Host 'Getting cache for SecureScore' try { Set-CIPPDBCacheSecureScore -TenantFilter $TenantFilter } catch { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "SecureScore collection failed: $($_.Exception.Message)" -sev Error } + Write-Host 'Getting cache for IntunePolicies' try { Set-CIPPDBCacheIntunePolicies -TenantFilter $TenantFilter } catch { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "IntunePolicies collection failed: $($_.Exception.Message)" -sev Error } + Write-Host 'Getting cache for ConditionalAccessPolicies' try { Set-CIPPDBCacheConditionalAccessPolicies -TenantFilter $TenantFilter } catch { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ConditionalAccessPolicies collection failed: $($_.Exception.Message)" -sev Error } + Write-Host 'Getting cache for PIMSettings' try { Set-CIPPDBCachePIMSettings -TenantFilter $TenantFilter } catch { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "PIMSettings collection failed: $($_.Exception.Message)" -sev Error } + Write-Host 'Getting cache for Domains' try { Set-CIPPDBCacheDomains -TenantFilter $TenantFilter } catch { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Domains collection failed: $($_.Exception.Message)" -sev Error } + Write-Host 'Getting cache for RoleEligibilitySchedules' try { Set-CIPPDBCacheRoleEligibilitySchedules -TenantFilter $TenantFilter } catch { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "RoleEligibilitySchedules collection failed: $($_.Exception.Message)" -sev Error } + Write-Host 'Getting cache for RoleManagementPolicies' try { Set-CIPPDBCacheRoleManagementPolicies -TenantFilter $TenantFilter } catch { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "RoleManagementPolicies collection failed: $($_.Exception.Message)" -sev Error } + Write-Host 'Getting cache for RoleAssignmentScheduleInstances' try { Set-CIPPDBCacheRoleAssignmentScheduleInstances -TenantFilter $TenantFilter } catch { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "RoleAssignmentScheduleInstances collection failed: $($_.Exception.Message)" -sev Error } + Write-Host 'Getting cache for B2BManagementPolicy' try { Set-CIPPDBCacheB2BManagementPolicy -TenantFilter $TenantFilter } catch { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "B2BManagementPolicy collection failed: $($_.Exception.Message)" -sev Error } + Write-Host 'Getting cache for AuthenticationFlowsPolicy' try { Set-CIPPDBCacheAuthenticationFlowsPolicy -TenantFilter $TenantFilter } catch { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "AuthenticationFlowsPolicy collection failed: $($_.Exception.Message)" -sev Error } + Write-Host 'Getting cache for RiskyUsers' try { Set-CIPPDBCacheRiskyUsers -TenantFilter $TenantFilter } catch { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "RiskyUsers collection failed: $($_.Exception.Message)" -sev Error } + Write-Host 'Getting cache for RiskyServicePrincipals' try { Set-CIPPDBCacheRiskyServicePrincipals -TenantFilter $TenantFilter } catch { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "RiskyServicePrincipals collection failed: $($_.Exception.Message)" -sev Error } + Write-Host 'Getting cache for ServicePrincipalRiskDetections' try { Set-CIPPDBCacheServicePrincipalRiskDetections -TenantFilter $TenantFilter } catch { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ServicePrincipalRiskDetections collection failed: $($_.Exception.Message)" -sev Error } + Write-Host 'Getting cache for RiskDetections' try { Set-CIPPDBCacheRiskDetections -TenantFilter $TenantFilter } catch { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "RiskDetections collection failed: $($_.Exception.Message)" -sev Error } + Write-Host 'Getting cache for DeviceRegistrationPolicy' try { Set-CIPPDBCacheDeviceRegistrationPolicy -TenantFilter $TenantFilter } catch { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "DeviceRegistrationPolicy collection failed: $($_.Exception.Message)" -sev Error } + Write-Host 'Getting cache for CredentialUserRegistrationDetails' try { Set-CIPPDBCacheCredentialUserRegistrationDetails -TenantFilter $TenantFilter } catch { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "CredentialUserRegistrationDetails collection failed: $($_.Exception.Message)" -sev Error } + Write-Host 'Getting cache for UserRegistrationDetails' try { Set-CIPPDBCacheUserRegistrationDetails -TenantFilter $TenantFilter } catch { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "UserRegistrationDetails collection failed: $($_.Exception.Message)" -sev Error } + Write-Host 'Getting cache for ManagedDeviceEncryptionStates' try { Set-CIPPDBCacheManagedDeviceEncryptionStates -TenantFilter $TenantFilter } catch { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ManagedDeviceEncryptionStates collection failed: $($_.Exception.Message)" -sev Error } + Write-Host 'Getting cache for OAuth2PermissionGrants' try { Set-CIPPDBCacheOAuth2PermissionGrants -TenantFilter $TenantFilter } catch { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "OAuth2PermissionGrants collection failed: $($_.Exception.Message)" -sev Error } + Write-Host 'Getting cache for AppRoleAssignments' try { Set-CIPPDBCacheAppRoleAssignments -TenantFilter $TenantFilter } catch { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "AppRoleAssignments collection failed: $($_.Exception.Message)" -sev Error } + Write-Host 'Getting cache for ExoAntiPhishPolicies' try { Set-CIPPDBCacheExoAntiPhishPolicies -TenantFilter $TenantFilter } catch { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoAntiPhishPolicies collection failed: $($_.Exception.Message)" -sev Error } + Write-Host 'Getting cache for ExoMalwareFilterPolicies' try { Set-CIPPDBCacheExoMalwareFilterPolicies -TenantFilter $TenantFilter } catch { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoMalwareFilterPolicies collection failed: $($_.Exception.Message)" -sev Error } + Write-Host 'Getting cache for ExoSafeLinksPolicies' try { Set-CIPPDBCacheExoSafeLinksPolicies -TenantFilter $TenantFilter } catch { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoSafeLinksPolicies collection failed: $($_.Exception.Message)" -sev Error } + Write-Host 'Getting cache for ExoSafeAttachmentPolicies' try { Set-CIPPDBCacheExoSafeAttachmentPolicies -TenantFilter $TenantFilter } catch { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoSafeAttachmentPolicies collection failed: $($_.Exception.Message)" -sev Error } + Write-Host 'Getting cache for ExoTransportRules' try { Set-CIPPDBCacheExoTransportRules -TenantFilter $TenantFilter } catch { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoTransportRules collection failed: $($_.Exception.Message)" -sev Error } + Write-Host 'Getting cache for ExoDkimSigningConfig' try { Set-CIPPDBCacheExoDkimSigningConfig -TenantFilter $TenantFilter } catch { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoDkimSigningConfig collection failed: $($_.Exception.Message)" -sev Error } + Write-Host 'Getting cache for ExoOrganizationConfig' try { Set-CIPPDBCacheExoOrganizationConfig -TenantFilter $TenantFilter } catch { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoOrganizationConfig collection failed: $($_.Exception.Message)" -sev Error } + Write-Host 'Getting cache for ExoAcceptedDomains' try { Set-CIPPDBCacheExoAcceptedDomains -TenantFilter $TenantFilter } catch { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoAcceptedDomains collection failed: $($_.Exception.Message)" -sev Error } + Write-Host 'Getting cache for IntuneAppProtectionPolicies' try { Set-CIPPDBCacheIntuneAppProtectionPolicies -TenantFilter $TenantFilter } catch { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "IntuneAppProtectionPolicies collection failed: $($_.Exception.Message)" -sev Error } diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheAppRoleAssignments.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheAppRoleAssignments.ps1 index a96a6f9f1923..f056e4ec470f 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheAppRoleAssignments.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheAppRoleAssignments.ps1 @@ -16,14 +16,13 @@ function Set-CIPPDBCacheAppRoleAssignments { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching app role assignments' -sev Info # Get all service principals first - $ServicePrincipals = New-GraphGetRequest -uri 'https://graph.microsoft.com/v1.0/servicePrincipals?$select=id,appId,displayName&$top=999' -tenantid $TenantFilter + $ServicePrincipals = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/servicePrincipals?$select=id,appId,displayName&$top=999&expand=appRoleAssignments' -tenantid $TenantFilter $AllAppRoleAssignments = [System.Collections.Generic.List[object]]::new() foreach ($SP in $ServicePrincipals) { try { - $AppRoleAssignments = New-GraphGetRequest -uri "https://graph.microsoft.com/v1.0/servicePrincipals/$($SP.id)/appRoleAssignments?`$top=999" -tenantid $TenantFilter - + $AppRoleAssignments = $SP.appRoleAssignments foreach ($Assignment in $AppRoleAssignments) { # Enrich with service principal info $Assignment | Add-Member -NotePropertyName 'servicePrincipalDisplayName' -NotePropertyValue $SP.displayName -Force diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheConditionalAccessPolicies.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheConditionalAccessPolicies.ps1 index 29c4722be7ef..81a21f5bbe9f 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheConditionalAccessPolicies.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheConditionalAccessPolicies.ps1 @@ -48,7 +48,7 @@ function Set-CIPPDBCacheConditionalAccessPolicies { } try { - $AuthStrengths = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/identity/conditionalAccess/authenticationStrength/policies?$top=999' -tenantid $TenantFilter + $AuthStrengths = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/identity/conditionalAccess/authenticationStrength/policies' -tenantid $TenantFilter if ($AuthStrengths) { Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'AuthenticationStrengths' -Data $AuthStrengths diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheRoles.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheRoles.ps1 index 07482abe8e9f..1c76f2eac12e 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheRoles.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheRoles.ps1 @@ -15,11 +15,11 @@ function Set-CIPPDBCacheRoles { try { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching directory roles' -sev Info - $Roles = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/directoryRoles?$top=999' -tenantid $TenantFilter + $Roles = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/directoryRoles' -tenantid $TenantFilter $RolesWithMembers = foreach ($Role in $Roles) { try { - $Members = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/directoryRoles/$($Role.id)/members?\$top=999&\$select=id,displayName,userPrincipalName" -tenantid $TenantFilter + $Members = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/directoryRoles/$($Role.id)/members?&`$select=id,displayName,userPrincipalName" -tenantid $TenantFilter [PSCustomObject]@{ id = $Role.id displayName = $Role.displayName diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21772.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21772.ps1 index 30647ae0109a..cb8372f7d59b 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21772.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21772.ps1 @@ -1,6 +1,6 @@ function Invoke-CippTestZTNA21772 { param($Tenant) - + #tested try { $Apps = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Apps' $ServicePrincipals = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ServicePrincipals' diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21773.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21773.ps1 index 231b9d41ce1a..26b275c3b491 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21773.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21773.ps1 @@ -1,6 +1,6 @@ function Invoke-CippTestZTNA21773 { param($Tenant) - + #tested try { $Apps = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Apps' $ServicePrincipals = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ServicePrincipals' diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21774.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21774.ps1 index 6d0ec4ef9fcf..5f46cbb47e19 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21774.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21774.ps1 @@ -3,7 +3,7 @@ function Invoke-CippTestZTNA21774 { try { $ServicePrincipals = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ServicePrincipals' - + #tested if (-not $ServicePrincipals) { Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21774' -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Service principals not found in database' -Risk 'High' -Name 'Microsoft services applications do not have credentials configured' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Application Management' return diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21776.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21776.ps1 index d5c106671932..f686c201db83 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21776.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21776.ps1 @@ -1,6 +1,6 @@ function Invoke-CippTestZTNA21776 { param($Tenant) - + #tested try { $AuthPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthorizationPolicy' if (-not $AuthPolicy) { diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21780.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21780.ps1 index ec83a02e6123..7535bd516efe 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21780.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21780.ps1 @@ -1,6 +1,6 @@ function Invoke-CippTestZTNA21780 { param($Tenant) - + #tested try { $Recommendations = New-CIPPDbRequest -TenantFilter $Tenant -Type 'DirectoryRecommendations' diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21783.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21783.ps1 index a33ffee7ef09..9fc97c15eabe 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21783.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21783.ps1 @@ -1,6 +1,6 @@ function Invoke-CippTestZTNA21783 { param($Tenant) - + #tested try { $CAPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ConditionalAccessPolicies' $Roles = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Roles' diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21784.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21784.ps1 index 7241cbe6ff0b..61f18be9cc48 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21784.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21784.ps1 @@ -1,6 +1,6 @@ function Invoke-CippTestZTNA21784 { param($Tenant) - + #tested try { $CAPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ConditionalAccessPolicies' @@ -9,27 +9,54 @@ function Invoke-CippTestZTNA21784 { return } + # Get authentication strength policies from cache + $AuthStrengthPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationStrengths' + + # Define phishing-resistant methods + $PhishingResistantMethods = @( + 'windowsHelloForBusiness', + 'fido2', + 'x509CertificateMultiFactor', + 'certificateBasedAuthenticationPki' + ) + + # Find authentication strength policies with phishing-resistant methods + $PhishingResistantPolicies = $AuthStrengthPolicies | Where-Object { + $_.allowedCombinations | Where-Object { $PhishingResistantMethods -contains $_ } + } + + if (-not $PhishingResistantPolicies) { + $Status = 'Failed' + $Result = 'No phishing-resistant authentication strength policies found in tenant' + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21784' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'All user sign in activity uses phishing-resistant authentication methods' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Access Control' + return + } + $EnabledPolicies = $CAPolicies | Where-Object { $_.state -eq 'enabled' } - $AllUsersPolicies = $EnabledPolicies | Where-Object { - $_.conditions.users.includeUsers -contains 'All' -and - $_.grantControls.authenticationStrength + # Find policies that apply to all users with phishing-resistant auth strength + $RelevantPolicies = $EnabledPolicies | Where-Object { + ($_.conditions.users.includeUsers -contains 'All') -and + ($_.grantControls.authenticationStrength.id -in $PhishingResistantPolicies.id) } - if (-not $AllUsersPolicies) { + if (-not $RelevantPolicies) { $Status = 'Failed' $Result = 'No Conditional Access policies found requiring phishing-resistant authentication for all users' } else { - $PoliciesWithExclusions = $AllUsersPolicies | Where-Object { + # Check for user exclusions that create coverage gaps + $PoliciesWithExclusions = $RelevantPolicies | Where-Object { $_.conditions.users.excludeUsers.Count -gt 0 } if ($PoliciesWithExclusions.Count -gt 0) { $Status = 'Failed' - $Result = "Found $($AllUsersPolicies.Count) policies requiring phishing-resistant authentication, but $($PoliciesWithExclusions.Count) have user exclusions creating coverage gaps" + $Result = "Found $($RelevantPolicies.Count) policies requiring phishing-resistant authentication, but $($PoliciesWithExclusions.Count) have user exclusions creating coverage gaps:`n`n" + $Result += ($PoliciesWithExclusions | ForEach-Object { "- $($_.displayName) (Excludes $($_.conditions.users.excludeUsers.Count) users)" }) -join "`n" } else { $Status = 'Passed' - $Result = "All users are protected by $($AllUsersPolicies.Count) Conditional Access policies requiring phishing-resistant authentication" + $Result = "All users are protected by $($RelevantPolicies.Count) Conditional Access policies requiring phishing-resistant authentication:`n`n" + $Result += ($RelevantPolicies | ForEach-Object { "- $($_.displayName)" }) -join "`n" } } diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21786.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21786.ps1 index ce2cf12cadfc..c7e0c587de5e 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21786.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21786.ps1 @@ -1,6 +1,6 @@ function Invoke-CippTestZTNA21786 { param($Tenant) - + #tested try { $CAPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ConditionalAccessPolicies' diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21787.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21787.ps1 index cb0b822ad748..0912092a8056 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21787.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21787.ps1 @@ -1,6 +1,6 @@ function Invoke-CippTestZTNA21787 { param($Tenant) - + #tested try { $AuthPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthorizationPolicy' diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21790.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21790.ps1 index 91547c2fa4af..39804492cbf0 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21790.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21790.ps1 @@ -1,6 +1,6 @@ function Invoke-CippTestZTNA21790 { param($Tenant) - + #tested try { $CrossTenantPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'CrossTenantAccessPolicy' diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21791.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21791.ps1 index bca84e547390..6e6d23b0ff5d 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21791.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21791.ps1 @@ -1,6 +1,6 @@ function Invoke-CippTestZTNA21791 { param($Tenant) - + #tested try { $AuthPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthorizationPolicy' @@ -13,7 +13,7 @@ function Invoke-CippTestZTNA21791 { if ($AllowInvitesFrom -ne 'everyone') { $Status = 'Passed' - $Result = "Tenant restricts who can invite guests (setting: $AllowInvitesFrom)" + $Result = "Tenant restricts who can invite guests (Set to: $AllowInvitesFrom)" } else { $Status = 'Failed' $Result = 'Tenant allows any user including guests to invite other guests' diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21792.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21792.ps1 index 9f8af722e3ce..b36e7e7da340 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21792.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21792.ps1 @@ -1,6 +1,6 @@ function Invoke-CippTestZTNA21792 { param($Tenant) - + #tested try { $AuthPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthorizationPolicy' diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21793.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21793.ps1 index 716f7ea18656..6b50e520eccd 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21793.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21793.ps1 @@ -1,6 +1,6 @@ function Invoke-CippTestZTNA21793 { param($Tenant) - + #tested try { $CrossTenantPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'CrossTenantAccessPolicy' diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21796.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21796.ps1 index 56ac4ddd545e..cf2597532777 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21796.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21796.ps1 @@ -1,6 +1,6 @@ function Invoke-CippTestZTNA21796 { param($Tenant) - + #tested try { $CAPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ConditionalAccessPolicies' @@ -22,10 +22,10 @@ function Invoke-CippTestZTNA21796 { if ($EnabledBlockPolicies.Count -ge 1) { $Status = 'Passed' - $Result = "Found $($EnabledBlockPolicies.Count) properly configured policies blocking legacy authentication" + $Result = "Found $($EnabledBlockPolicies.Count) properly configured policies blocking legacy authentication:`n $($EnabledBlockPolicies | ForEach-Object { "- $($_.displayName)" } | Out-String) " } elseif ($BlockPolicies.Count -ge 1) { $Status = 'Failed' - $Result = 'Policies to block legacy authentication found but not properly configured or enabled' + $Result = "Policies to block legacy authentication found but not properly configured or enabled: `n $($BlockPolicies | ForEach-Object { "- $($_.displayName)" } | Out-String) " } else { $Status = 'Failed' $Result = 'No conditional access policies to block legacy authentication found' diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21797.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21797.ps1 index 9264c119fd39..93ce2c12d187 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21797.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21797.ps1 @@ -1,6 +1,6 @@ function Invoke-CippTestZTNA21797 { param($Tenant) - + #tested try { $allCAPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ConditionalAccessPolicies' $authMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21799.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21799.ps1 index 028cbe0804c8..833bf23642ea 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21799.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21799.ps1 @@ -1,6 +1,6 @@ function Invoke-CippTestZTNA21799 { param($Tenant) - + #tested try { $authMethodPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' $allCAPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ConditionalAccessPolicies' @@ -67,10 +67,7 @@ function Invoke-CippTestZTNA21799 { $mdInfo += "| $($policy.displayName) | $grantControls | $targetUsers |`n" } - } else { - $mdInfo = 'Some high-risk sign-in attempts are not adequately mitigated by Conditional Access policies.' } - $testResultMarkdown = $testResultMarkdown + $mdInfo Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21799' -TestType 'Identity' -Status $passed -ResultMarkdown $testResultMarkdown -Risk 'High' -Name 'Block high risk sign-ins' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Conditional Access' diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21802.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21802.ps1 index 148520a3a181..b5b6f4be7787 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21802.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21802.ps1 @@ -1,6 +1,6 @@ function Invoke-CippTestZTNA21802 { param($Tenant) - + #tested try { $AuthMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21803.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21803.ps1 index afebf9487bec..0d54fc1283fe 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21803.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21803.ps1 @@ -1,6 +1,6 @@ function Invoke-CippTestZTNA21803 { param($Tenant) - + #Tested try { $AuthMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' From c9cb11201d97eafa970eed3b3006cb70b808ca57 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Wed, 24 Dec 2025 20:45:15 +0100 Subject: [PATCH 043/503] functions that are done --- Modules/CIPPCore/Public/Get-CippDbRole.ps1 | 4 +- .../CIPPCore/Public/Set-CIPPDBCacheApps.ps1 | 2 +- ...t-CIPPDBCacheAuthenticationFlowsPolicy.ps1 | 2 +- .../Set-CIPPDBCacheB2BManagementPolicy.ps1 | 9 +- ...et-CIPPDBCacheDeviceRegistrationPolicy.ps1 | 4 +- .../Public/Tests/Invoke-CippTestZTNA21804.ps1 | 2 +- .../Public/Tests/Invoke-CippTestZTNA21806.ps1 | 2 +- .../Public/Tests/Invoke-CippTestZTNA21807.ps1 | 2 +- .../Public/Tests/Invoke-CippTestZTNA21808.ps1 | 2 +- .../Public/Tests/Invoke-CippTestZTNA21809.ps1 | 2 +- .../Public/Tests/Invoke-CippTestZTNA21810.ps1 | 2 +- .../Public/Tests/Invoke-CippTestZTNA21811.ps1 | 2 +- .../Public/Tests/Invoke-CippTestZTNA21812.ps1 | 2 +- .../Public/Tests/Invoke-CippTestZTNA21813.ps1 | 2 +- .../Public/Tests/Invoke-CippTestZTNA21814.ps1 | 2 +- .../Public/Tests/Invoke-CippTestZTNA21815.ps1 | 4 +- .../Public/Tests/Invoke-CippTestZTNA21816.ps1 | 8 +- .../Public/Tests/Invoke-CippTestZTNA21818.ps1 | 2 +- .../Public/Tests/Invoke-CippTestZTNA21819.ps1 | 4 +- .../Public/Tests/Invoke-CippTestZTNA21820.ps1 | 6 +- .../Public/Tests/Invoke-CippTestZTNA21822.ps1 | 4 +- .../Public/Tests/Invoke-CippTestZTNA21823.ps1 | 2 +- .../Public/Tests/Invoke-CippTestZTNA21824.ps1 | 2 +- .../Public/Tests/Invoke-CippTestZTNA21825.ps1 | 2 +- .../Public/Tests/Invoke-CippTestZTNA21828.ps1 | 2 +- .../Public/Tests/Invoke-CippTestZTNA21829.ps1 | 2 +- .../Public/Tests/Invoke-CippTestZTNA21830.ps1 | 2 +- .../Public/Tests/Invoke-CippTestZTNA21835.ps1 | 2 +- .../Public/Tests/Invoke-CippTestZTNA21836.ps1 | 2 +- .../Public/Tests/Invoke-CippTestZTNA21837.ps1 | 4 +- .../Public/Tests/Invoke-CippTestZTNA21838.ps1 | 2 +- .../Public/Tests/Invoke-CippTestZTNA21839.ps1 | 2 +- .../Public/Tests/Invoke-CippTestZTNA21840.ps1 | 2 +- .../Public/Tests/Invoke-CippTestZTNA21841.ps1 | 2 +- .../Public/Tests/Invoke-CippTestZTNA21842.ps1 | 2 +- .../Public/Tests/Invoke-CippTestZTNA21844.ps1 | 2 +- .../Public/Tests/Invoke-CippTestZTNA21845.ps1 | 2 +- .../Public/Tests/Invoke-CippTestZTNA21846.ps1 | 2 +- .../Public/Tests/Invoke-CippTestZTNA21847.ps1 | 2 +- .../Public/Tests/Invoke-CippTestZTNA21848.ps1 | 4 +- .../Public/Tests/Invoke-CippTestZTNA21849.ps1 | 2 +- .../Public/Tests/Invoke-CippTestZTNA21850.ps1 | 4 +- .../Public/Tests/Invoke-CippTestZTNA21858.ps1 | 2 +- .../Public/Tests/Invoke-CippTestZTNA21861.ps1 | 4 +- .../Public/Tests/Invoke-CippTestZTNA21862.ps1 | 2 +- .../Public/Tests/Invoke-CippTestZTNA21863.ps1 | 2 +- .../Public/Tests/Invoke-CippTestZTNA21865.ps1 | 6 +- .../Public/Tests/Invoke-CippTestZTNA21866.ps1 | 2 +- .../Public/Tests/Invoke-CippTestZTNA21868.ps1 | 86 ++++++++++++++++++- .../Public/Tests/Invoke-CippTestZTNA21869.ps1 | 5 +- .../Public/Tests/Invoke-CippTestZTNA21872.ps1 | 2 +- .../Public/Tests/Invoke-CippTestZTNA21874.ps1 | 2 +- .../Public/Tests/Invoke-CippTestZTNA21877.ps1 | 2 +- 53 files changed, 154 insertions(+), 78 deletions(-) diff --git a/Modules/CIPPCore/Public/Get-CippDbRole.ps1 b/Modules/CIPPCore/Public/Get-CippDbRole.ps1 index bbf42843fcd0..02313a9a96ad 100644 --- a/Modules/CIPPCore/Public/Get-CippDbRole.ps1 +++ b/Modules/CIPPCore/Public/Get-CippDbRole.ps1 @@ -34,7 +34,7 @@ function Get-CippDbRole { '4a5d8f65-41da-4de4-8968-e035b65339cf', '75941009-915a-4869-abe7-691bff18279e' ) - $Roles = $Roles | Where-Object { $PrivilegedRoleTemplateIds -contains $_.templateId } + $Roles = $Roles | Where-Object { $PrivilegedRoleTemplateIds -contains $_.RoletemplateId } } if ($CisaHighlyPrivilegedRoles) { @@ -46,7 +46,7 @@ function Get-CippDbRole { '966707d0-3269-4727-9be2-8c3a10f19b9d', 'b0f54661-2d74-4c50-afa3-1ec803f12efe' ) - $Roles = $Roles | Where-Object { $CisaRoleTemplateIds -contains $_.templateId } + $Roles = $Roles | Where-Object { $CisaRoleTemplateIds -contains $_.RoletemplateId } } return $Roles diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheApps.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheApps.ps1 index d972cc6d4bb1..9844836372d6 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheApps.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheApps.ps1 @@ -15,7 +15,7 @@ function Set-CIPPDBCacheApps { try { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching applications' -sev Info - $Apps = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/applications?$top=999' -tenantid $TenantFilter + $Apps = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/applications?$top=999&expand=owners' -tenantid $TenantFilter Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Apps' -Data $Apps Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Apps' -Data $Apps -Count $Apps = $null diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheAuthenticationFlowsPolicy.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheAuthenticationFlowsPolicy.ps1 index 9cd7afb951fb..587eaa8a587b 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheAuthenticationFlowsPolicy.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheAuthenticationFlowsPolicy.ps1 @@ -15,7 +15,7 @@ function Set-CIPPDBCacheAuthenticationFlowsPolicy { try { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching authentication flows policy' -sev Info - $AuthFlowPolicy = New-GraphGetRequest -uri 'https://graph.microsoft.com/v1.0/policies/authenticationFlowsPolicy' -tenantid $TenantFilter + $AuthFlowPolicy = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/policies/authenticationFlowsPolicy' -tenantid $TenantFilter -AsApp $true if ($AuthFlowPolicy) { Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'AuthenticationFlowsPolicy' -Data @($AuthFlowPolicy) diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheB2BManagementPolicy.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheB2BManagementPolicy.ps1 index d321bad9e705..168866b475c2 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheB2BManagementPolicy.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheB2BManagementPolicy.ps1 @@ -15,8 +15,8 @@ function Set-CIPPDBCacheB2BManagementPolicy { try { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching B2B management policy' -sev Info - $LegacyPolicies = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/policies' -tenantid $TenantFilter - $B2BManagementPolicy = $LegacyPolicies | Where-Object { $_.Type -eq 'B2BManagementPolicy' } + $LegacyPolicies = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/policies/b2bManagementPolicies' -tenantid $TenantFilter + $B2BManagementPolicy = $LegacyPolicies if ($B2BManagementPolicy) { Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'B2BManagementPolicy' -Data @($B2BManagementPolicy) @@ -26,9 +26,6 @@ function Set-CIPPDBCacheB2BManagementPolicy { } } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter ` - -message "Failed to cache B2B management policy: $($_.Exception.Message)" ` - -sev Warning ` - -LogData (Get-CippException -Exception $_) + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache B2B management policy: $($_.Exception.Message)" -sev Warning -LogData (Get-CippException -Exception $_) } } diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheDeviceRegistrationPolicy.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheDeviceRegistrationPolicy.ps1 index b92e084ba031..ec2da92abc21 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheDeviceRegistrationPolicy.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheDeviceRegistrationPolicy.ps1 @@ -15,8 +15,8 @@ function Set-CIPPDBCacheDeviceRegistrationPolicy { try { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching device registration policy' -sev Info - $DeviceRegistrationPolicy = New-GraphGetRequest -uri 'https://graph.microsoft.com/v1.0/policies/deviceRegistrationPolicy' -tenantid $TenantFilter - + $DeviceRegistrationPolicy = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/policies/deviceRegistrationPolicy' -tenantid $TenantFilter + if ($DeviceRegistrationPolicy) { Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'DeviceRegistrationPolicy' -Data @($DeviceRegistrationPolicy) Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached device registration policy successfully' -sev Info diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21804.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21804.ps1 index 62e0b08832f7..fbfe61e0cce5 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21804.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21804.ps1 @@ -1,6 +1,6 @@ function Invoke-CippTestZTNA21804 { param($Tenant) - + #Tested try { $authMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21806.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21806.ps1 index dbfbf1f27e60..fb8eb6fa158e 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21806.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21806.ps1 @@ -1,6 +1,6 @@ function Invoke-CippTestZTNA21806 { param($Tenant) - + #tested try { $allCAPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ConditionalAccessPolicies' diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21807.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21807.ps1 index 5e106a245452..a5a1cba9d851 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21807.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21807.ps1 @@ -1,6 +1,6 @@ function Invoke-CippTestZTNA21807 { param($Tenant) - + #Tested try { $AuthPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthorizationPolicy' diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21808.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21808.ps1 index c78ea236e7b2..0b74d58db63d 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21808.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21808.ps1 @@ -1,6 +1,6 @@ function Invoke-CippTestZTNA21808 { param($Tenant) - + #Tested try { $CAPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ConditionalAccessPolicies' if (-not $CAPolicies) { diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21809.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21809.ps1 index d991b301f249..049ab58f1277 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21809.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21809.ps1 @@ -1,6 +1,6 @@ function Invoke-CippTestZTNA21809 { param($Tenant) - + #Tested try { $result = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AdminConsentRequestPolicy' diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21810.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21810.ps1 index 1196e55465d5..1d60ce86273b 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21810.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21810.ps1 @@ -1,6 +1,6 @@ function Invoke-CippTestZTNA21810 { param($Tenant) - + #Tested try { $authPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthorizationPolicy' diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21811.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21811.ps1 index ad2528521029..a24b8c767975 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21811.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21811.ps1 @@ -1,6 +1,6 @@ function Invoke-CippTestZTNA21811 { param($Tenant) - + #Tested try { $domains = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Domains' diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21812.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21812.ps1 index c0e5424c47c9..69ef4c54d276 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21812.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21812.ps1 @@ -4,7 +4,7 @@ function Invoke-CippTestZTNA21812 { [Parameter(Mandatory = $true)] [string]$Tenant ) - + #Tested $TestId = 'ZTNA21812' try { diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21813.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21813.ps1 index 3a3283b8bcea..da54b451cf23 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21813.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21813.ps1 @@ -4,7 +4,7 @@ function Invoke-CippTestZTNA21813 { [Parameter(Mandatory = $true)] [string]$Tenant ) - + #Tested $TestId = 'ZTNA21813' try { diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21814.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21814.ps1 index a50e4c16bc68..4287eeb7c1ae 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21814.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21814.ps1 @@ -14,7 +14,7 @@ function Invoke-CippTestZTNA21814 { $RoleData = [System.Collections.Generic.List[object]]::new() foreach ($Role in $PrivilegedRoles) { - $RoleMembers = Get-CippDbRoleMembers -TenantFilter $Tenant -RoleTemplateId $Role.templateId + $RoleMembers = Get-CippDbRoleMembers -TenantFilter $Tenant -RoleTemplateId $Role.RoletemplateId $RoleUsers = $RoleMembers | Where-Object { $_.'@odata.type' -eq '#microsoft.graph.user' } foreach ($RoleMember in $RoleUsers) { diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21815.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21815.ps1 index 06365613ac9e..4e2fc0b24648 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21815.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21815.ps1 @@ -4,7 +4,7 @@ function Invoke-CippTestZTNA21815 { [Parameter(Mandatory = $true)] [string]$Tenant ) - + #tested $TestId = 'ZTNA21815' try { @@ -16,7 +16,7 @@ function Invoke-CippTestZTNA21815 { foreach ($Role in $PrivilegedRoles) { $ActiveAssignments = $RoleAssignmentScheduleInstances | Where-Object { - $_.roleDefinitionId -eq $Role.templateId -and + $_.roleDefinitionId -eq $Role.RoletemplateId -and $_.assignmentType -eq 'Assigned' -and $null -eq $_.endDateTime } diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21816.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21816.ps1 index e190bc18691d..7ec660d6a4f6 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21816.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21816.ps1 @@ -4,7 +4,7 @@ function Invoke-CippTestZTNA21816 { [Parameter(Mandatory = $true)] [string]$Tenant ) - + #Tested $TestId = 'ZTNA21816' try { @@ -39,11 +39,11 @@ function Invoke-CippTestZTNA21816 { foreach ($Role in $PrivilegedRoles) { if ($Role.templateId -eq $GlobalAdminRoleId) { continue } - $RoleMembers = Get-CippDbRoleMembers -TenantFilter $Tenant -RoleTemplateId $Role.templateId + $RoleMembers = Get-CippDbRoleMembers -TenantFilter $Tenant -RoleTemplateId $Role.RoletemplateId foreach ($Member in $RoleMembers) { $Assignment = $RoleAssignmentScheduleInstances | Where-Object { - $_.principalId -eq $Member.id -and $_.roleDefinitionId -eq $Role.templateId + $_.principalId -eq $Member.id -and $_.roleDefinitionId -eq $Role.RoletemplateId } | Select-Object -First 1 if (-not $Assignment -or ($Assignment.assignmentType -eq 'Assigned' -and $null -eq $Assignment.endDateTime)) { @@ -51,7 +51,7 @@ function Invoke-CippTestZTNA21816 { displayName = $Member.displayName userPrincipalName = $Member.userPrincipalName id = $Member.id - roleTemplateId = $Role.templateId + roleTemplateId = $Role.RoletemplateId roleName = $Role.displayName assignmentType = if ($Assignment) { $Assignment.assignmentType } else { 'Not in PIM' } } diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21818.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21818.ps1 index 839fd0c47c7b..d964daed6d1c 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21818.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21818.ps1 @@ -4,7 +4,7 @@ function Invoke-CippTestZTNA21818 { [Parameter(Mandatory = $true)] [string]$Tenant ) - + #Tested $TestId = 'ZTNA21818' try { diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21819.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21819.ps1 index 997d64954c69..42e78f9155ad 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21819.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21819.ps1 @@ -1,6 +1,6 @@ function Invoke-CippTestZTNA21819 { param($Tenant) - + #Tested $TestId = 'ZTNA21819' try { @@ -52,7 +52,7 @@ function Invoke-CippTestZTNA21819 { $ResultMarkdown += "| Role display name | Default recipients | Additional recipients |`n" $ResultMarkdown += "| :---------------- | :----------------- | :------------------- |`n" - $RoleLink = "https://entra.microsoft.com/#view/Microsoft_AAD_IAM/RolesManagementMenuBlade/~/AllRoles" + $RoleLink = 'https://entra.microsoft.com/#view/Microsoft_AAD_IAM/RolesManagementMenuBlade/~/AllRoles' $DisplayNameLink = "[$($GlobalAdminRole.displayName)]($RoleLink)" $DefaultRecipientsStatus = if ($IsDefaultRecipientsEnabled -eq $true) { diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21820.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21820.ps1 index f19eb2af726d..8b872f18520b 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21820.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21820.ps1 @@ -1,6 +1,6 @@ function Invoke-CippTestZTNA21820 { param($Tenant) - + #Tested $TestId = 'ZTNA21820' try { @@ -44,8 +44,8 @@ function Invoke-CippTestZTNA21820 { } # Find notification rule for requestor end-user assignment - $NotificationRule = $Policy.effectiveRules | Where-Object { - $_.id -like '*Notification_Requestor_EndUser_Assignment*' + $NotificationRule = $Policy.effectiveRules | Where-Object { + $_.id -like '*Notification_Requestor_EndUser_Assignment*' } if ($NotificationRule) { diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21822.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21822.ps1 index 2970cdc2630e..c041c8a141d4 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21822.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21822.ps1 @@ -1,6 +1,6 @@ function Invoke-CippTestZTNA21822 { param($Tenant) - + #Tested $TestId = 'ZTNA21822' try { @@ -28,7 +28,7 @@ function Invoke-CippTestZTNA21822 { } $ResultMarkdown += "`n`n## [Collaboration restrictions](https://entra.microsoft.com/#view/Microsoft_AAD_IAM/CompanyRelationshipsMenuBlade/~/Settings/menuId/)`n`n" - $ResultMarkdown += "The tenant is configured to: " + $ResultMarkdown += 'The tenant is configured to: ' if ($Passed -eq 'Passed') { $ResultMarkdown += "**Allow invitations only to the specified domains (most restrictive)** ✅`n" diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21823.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21823.ps1 index fb62b61a9ba4..abf1bfdcdcae 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21823.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21823.ps1 @@ -2,7 +2,7 @@ function Invoke-CippTestZTNA21823 { param($Tenant) $TestId = 'ZTNA21823' - + #Tested try { # Get authentication flows policy from cache $AuthFlowPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationFlowsPolicy' diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21824.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21824.ps1 index 67f4e31b0a5c..c49334aa1930 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21824.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21824.ps1 @@ -1,6 +1,6 @@ function Invoke-CippTestZTNA21824 { param($Tenant) - + #Tested try { $allCAPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ConditionalAccessPolicies' diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21825.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21825.ps1 index 2e75a2dc7871..164704c92648 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21825.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21825.ps1 @@ -2,7 +2,7 @@ function Invoke-CippTestZTNA21825 { param($Tenant) $TestId = 'ZTNA21825' - + #Tested try { # Get privileged roles $PrivilegedRoles = Get-CippDbRole -TenantFilter $Tenant -IncludePrivilegedRoles diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21828.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21828.ps1 index f92971871ceb..b0980e2f0aa5 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21828.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21828.ps1 @@ -1,6 +1,6 @@ function Invoke-CippTestZTNA21828 { param($Tenant) - + #Tested try { $allCAPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ConditionalAccessPolicies' diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21829.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21829.ps1 index 85592c9527ac..09beb6e96219 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21829.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21829.ps1 @@ -1,6 +1,6 @@ function Invoke-CippTestZTNA21829 { param($Tenant) - + #Tested $TestId = 'ZTNA21829' try { diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21830.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21830.ps1 index 7df6806fe317..5b202a4db4d5 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21830.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21830.ps1 @@ -2,7 +2,7 @@ function Invoke-CippTestZTNA21830 { param($Tenant) $TestId = 'ZTNA21830' - + #Tested try { # Get Conditional Access policies $CAPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ConditionalAccessPolicies' diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21835.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21835.ps1 index 5b0ca8756296..bfc6bd2705f9 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21835.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21835.ps1 @@ -1,6 +1,6 @@ function Invoke-CippTestZTNA21835 { param($Tenant) - + #Tested $TestId = 'ZTNA21835' try { diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21836.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21836.ps1 index 39574bf66a48..510f62a67544 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21836.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21836.ps1 @@ -1,6 +1,6 @@ function Invoke-CippTestZTNA21836 { param($Tenant) - + #Tested $TestId = 'ZTNA21836' try { diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21837.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21837.ps1 index e078ed1a10f7..ae4a632897d8 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21837.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21837.ps1 @@ -2,10 +2,10 @@ function Invoke-CippTestZTNA21837 { param($Tenant) $TestId = 'ZTNA21837' - + #Tested try { # Get device registration policy - $DeviceSettings = New-CIPPDbRequest -TenantFilter $Tenant -Type 'DeviceSettings' + $DeviceSettings = New-CIPPDbRequest -TenantFilter $Tenant -Type 'deviceRegistrationPolicy' if (-not $DeviceSettings) { Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Device settings not found in database' -Risk 'High' -Name 'Limit the maximum number of devices per user to 10' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Devices' diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21838.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21838.ps1 index 7d70724eb2c0..d6d3834987eb 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21838.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21838.ps1 @@ -2,7 +2,7 @@ function Invoke-CippTestZTNA21838 { param($Tenant) $TestId = 'ZTNA21838' - + #Tested try { # Get FIDO2 authentication method policy $AuthMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21839.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21839.ps1 index 5eff008f56f8..c05793f36f06 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21839.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21839.ps1 @@ -2,7 +2,7 @@ function Invoke-CippTestZTNA21839 { param($Tenant) $TestId = 'ZTNA21839' - + #Tested try { # Get FIDO2 authentication method policy $AuthMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21840.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21840.ps1 index 77a4eaafb722..c80c6ff1288f 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21840.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21840.ps1 @@ -1,6 +1,6 @@ function Invoke-CippTestZTNA21840 { param($Tenant) - + #Tested $TestId = 'ZTNA21840' try { diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21841.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21841.ps1 index 9c2254add39c..af69945f8016 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21841.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21841.ps1 @@ -1,6 +1,6 @@ function Invoke-CippTestZTNA21841 { param($Tenant) - + #Tested $TestId = 'ZTNA21841' try { diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21842.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21842.ps1 index 10c2d19b2370..57286a32689d 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21842.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21842.ps1 @@ -2,7 +2,7 @@ function Invoke-CippTestZTNA21842 { param($Tenant) $TestId = 'ZTNA21842' - + #Tested try { # Get authorization policy $AuthorizationPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthorizationPolicy' diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21844.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21844.ps1 index fd4d0687cf58..58ceffb6f351 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21844.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21844.ps1 @@ -2,7 +2,7 @@ function Invoke-CippTestZTNA21844 { param($Tenant) $TestId = 'ZTNA21844' - + #Tested try { # Azure AD PowerShell App ID $AzureADPowerShellAppId = '1b730954-1685-4b74-9bfd-dac224a7b894' diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21845.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21845.ps1 index b651eeda2ab0..205ce34ec927 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21845.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21845.ps1 @@ -2,7 +2,7 @@ function Invoke-CippTestZTNA21845 { param($Tenant) $TestId = 'ZTNA21845' - + #Tested try { # Get Temporary Access Pass configuration $AuthMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21846.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21846.ps1 index 3f70f6fb77d2..9c34d8e6c554 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21846.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21846.ps1 @@ -2,7 +2,7 @@ function Invoke-CippTestZTNA21846 { param($Tenant) $TestId = 'ZTNA21846' - + #Tested try { # Get Temporary Access Pass configuration $AuthMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21847.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21847.ps1 index 317342df8cf2..0da6a4bd84f4 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21847.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21847.ps1 @@ -2,7 +2,7 @@ function Invoke-CippTestZTNA21847 { param($Tenant) $TestId = 'ZTNA21847' - + #Tested try { # Check if tenant has on-premises sync $Settings = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Organization' diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21848.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21848.ps1 index 1e7e80c2d40c..6d0abd291b8b 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21848.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21848.ps1 @@ -2,7 +2,7 @@ function Invoke-CippTestZTNA21848 { param($Tenant) $TestId = 'ZTNA21848' - + #Tested try { # Get password protection settings from Settings cache $Settings = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Settings' @@ -36,7 +36,7 @@ function Invoke-CippTestZTNA21848 { # Show up to 10 banned passwords, summarize if more exist $MaxDisplay = 10 if ($BannedPasswordArray.Count -gt $MaxDisplay) { - $DisplayList = $BannedPasswordArray[0..($MaxDisplay-1)] + "...and $($BannedPasswordArray.Count - $MaxDisplay) more" + $DisplayList = $BannedPasswordArray[0..($MaxDisplay - 1)] + "...and $($BannedPasswordArray.Count - $MaxDisplay) more" } else { $DisplayList = $BannedPasswordArray } diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21849.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21849.ps1 index 3457963c01b9..17f34521d2ae 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21849.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21849.ps1 @@ -2,7 +2,7 @@ function Invoke-CippTestZTNA21849 { param($Tenant) $TestId = 'ZTNA21849' - + #Tested try { # Get password rule settings from Settings cache $Settings = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Settings' diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21850.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21850.ps1 index 0c144b7ad755..2cf320e64865 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21850.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21850.ps1 @@ -2,7 +2,7 @@ function Invoke-CippTestZTNA21850 { param($Tenant) $TestId = 'ZTNA21850' - + #Tested try { # Get password rule settings from Settings cache $Settings = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Settings' @@ -12,7 +12,7 @@ function Invoke-CippTestZTNA21850 { if ($null -eq $PasswordRuleSettings) { $Passed = 'Failed' - $ResultMarkdown = "❌ Password rule settings template not found." + $ResultMarkdown = '❌ Password rule settings template not found.' } else { $LockoutThresholdSetting = $PasswordRuleSettings.values | Where-Object { $_.name -eq 'LockoutThreshold' } diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21858.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21858.ps1 index db2a5f2fc25b..a85e39fae183 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21858.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21858.ps1 @@ -1,6 +1,6 @@ function Invoke-CippTestZTNA21858 { param($Tenant) - + #Tested try { $Guests = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Guests' if (-not $Guests) { diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21861.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21861.ps1 index a0b8fa1fc044..fd4e95775f14 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21861.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21861.ps1 @@ -2,7 +2,7 @@ function Invoke-CippTestZTNA21861 { param($Tenant) $TestId = 'ZTNA21861' - + #Tested try { # Get risky users from cache $RiskyUsers = New-CIPPDbRequest -TenantFilter $Tenant -Type 'RiskyUsers' @@ -18,7 +18,7 @@ function Invoke-CippTestZTNA21861 { $Passed = if ($UntriagedHighRiskUsers.Count -eq 0) { 'Passed' } else { 'Failed' } if ($Passed -eq 'Passed') { - $ResultMarkdown = "✅ All high-risk users are properly triaged in Entra ID Protection." + $ResultMarkdown = '✅ All high-risk users are properly triaged in Entra ID Protection.' } else { $ResultMarkdown = "❌ Found **$($UntriagedHighRiskUsers.Count)** untriaged high-risk users in Entra ID Protection.`n`n" $ResultMarkdown += "## Untriaged High-Risk Users`n`n" diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21862.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21862.ps1 index 64a2ed5ba140..bdd846220047 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21862.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21862.ps1 @@ -2,7 +2,7 @@ function Invoke-CippTestZTNA21862 { param($Tenant) $TestId = 'ZTNA21862' - + #Tested try { # Get risky service principals and risk detections from cache $UntriagedRiskyPrincipals = New-CIPPDbRequest -TenantFilter $Tenant -Type 'RiskyServicePrincipals' | Where-Object { $_.riskState -eq 'atRisk' } diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21863.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21863.ps1 index 51f98401418e..d96587aa19b2 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21863.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21863.ps1 @@ -2,7 +2,7 @@ function Invoke-CippTestZTNA21863 { param($Tenant) $TestId = 'ZTNA21863' - + #Tested try { # Get risk detections from cache and filter for high-risk untriaged sign-ins $RiskDetections = New-CIPPDbRequest -TenantFilter $Tenant -Type 'RiskDetections' diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21865.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21865.ps1 index f88631e35847..db84fdd54baa 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21865.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21865.ps1 @@ -2,7 +2,7 @@ function Invoke-CippTestZTNA21865 { param($Tenant) $TestId = 'ZTNA21865' - + #tested try { $NamedLocations = New-CIPPDbRequest -TenantFilter $Tenant -Type 'NamedLocations' @@ -29,8 +29,8 @@ function Invoke-CippTestZTNA21865 { foreach ($Location in $NamedLocations) { $Name = $Location.displayName - $Type = if ($Location.'@odata.type' -eq '#microsoft.graph.ipNamedLocation') { 'IP-based' } - elseif ($Location.'@odata.type' -eq '#microsoft.graph.countryNamedLocation') { 'Country-based' } + $Type = if ($Location.'@odata.type' -eq '#microsoft.graph.ipNamedLocation') { 'IP-based' } + elseif ($Location.'@odata.type' -eq '#microsoft.graph.countryNamedLocation') { 'Country-based' } else { 'Unknown' } $Trusted = if ($Location.isTrusted) { 'Yes' } else { 'No' } $ResultMarkdown += "| $Name | $Type | $Trusted |`n" diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21866.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21866.ps1 index 12ed524c2c30..230f4b5726ae 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21866.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21866.ps1 @@ -1,6 +1,6 @@ function Invoke-CippTestZTNA21866 { param($Tenant) - + #Tested $TestId = 'ZTNA21866' try { diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21868.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21868.ps1 index 8ca12777bd50..da2e379835a7 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21868.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21868.ps1 @@ -11,9 +11,89 @@ function Invoke-CippTestZTNA21868 { return } - Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21868' -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'This test requires Graph API calls to check application and service principal ownership. Owner relationships are not cached.' -Risk 'Medium' -Name 'Guests do not own apps in the tenant' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'External collaboration' - } - catch { + # Create a HashSet of guest user IDs for fast lookups + $GuestUserIds = [System.Collections.Generic.HashSet[string]]::new() + foreach ($guest in $Guests) { + [void]$GuestUserIds.Add($guest.id) + } + + # Initialize lists for guest owners + $GuestAppOwners = [System.Collections.Generic.List[object]]::new() + $GuestSpOwners = [System.Collections.Generic.List[object]]::new() + + # Check applications for guest owners + foreach ($app in $Apps) { + if ($app.owners -and $app.owners.Count -gt 0) { + foreach ($owner in $app.owners) { + if ($GuestUserIds.Contains($owner.id)) { + $ownerInfo = [PSCustomObject]@{ + id = $owner.id + displayName = $owner.displayName + userPrincipalName = $owner.userPrincipalName + appDisplayName = $app.displayName + appObjectId = $app.id + appId = $app.appId + } + $GuestAppOwners.Add($ownerInfo) + } + } + } + } + + # Check service principals for guest owners + foreach ($sp in $ServicePrincipals) { + if ($sp.owners -and $sp.owners.Count -gt 0) { + foreach ($owner in $sp.owners) { + if ($GuestUserIds.Contains($owner.id)) { + $ownerInfo = [PSCustomObject]@{ + id = $owner.id + displayName = $owner.displayName + userPrincipalName = $owner.userPrincipalName + spDisplayName = $sp.displayName + spObjectId = $sp.id + spAppId = $sp.appId + } + $GuestSpOwners.Add($ownerInfo) + } + } + } + } + + $HasGuestAppOwners = $GuestAppOwners.Count -gt 0 + $HasGuestSpOwners = $GuestSpOwners.Count -gt 0 + + if ($HasGuestAppOwners -or $HasGuestSpOwners) { + $Status = 'Failed' + $Result = "Guest users own applications or service principals`n`n" + + if ($HasGuestAppOwners -and $HasGuestSpOwners) { + $Result += "## Guest users own both applications and service principals`n`n" + $Result += "### Applications owned by guest users`n`n" + $Result += "| User Display Name | User Principal Name | Application |`n" + $Result += "| :---------------- | :------------------ | :---------- |`n" + $Result += ($GuestAppOwners | ForEach-Object { "| $($_.displayName) | $($_.userPrincipalName) | $($_.appDisplayName) |" }) -join "`n" + $Result += "`n`n### Service principals owned by guest users`n`n" + $Result += "| User Display Name | User Principal Name | Service Principal |`n" + $Result += "| :---------------- | :------------------ | :---------------- |`n" + $Result += ($GuestSpOwners | ForEach-Object { "| $($_.displayName) | $($_.userPrincipalName) | $($_.spDisplayName) |" }) -join "`n" + } elseif ($HasGuestAppOwners) { + $Result += "## Guest users own applications`n`n" + $Result += "| User Display Name | User Principal Name | Application |`n" + $Result += "| :---------------- | :------------------ | :---------- |`n" + $Result += ($GuestAppOwners | ForEach-Object { "| $($_.displayName) | $($_.userPrincipalName) | $($_.appDisplayName) |" }) -join "`n" + } elseif ($HasGuestSpOwners) { + $Result += "## Guest users own service principals`n`n" + $Result += "| User Display Name | User Principal Name | Service Principal |`n" + $Result += "| :---------------- | :------------------ | :---------------- |`n" + $Result += ($GuestSpOwners | ForEach-Object { "| $($_.displayName) | $($_.userPrincipalName) | $($_.spDisplayName) |" }) -join "`n" + } + } else { + $Status = 'Passed' + $Result = 'No guest users own any applications or service principals in the tenant' + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21868' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'Guests do not own apps in the tenant' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'External collaboration' + } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21868' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'Guests do not own apps in the tenant' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'External collaboration' diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21869.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21869.ps1 index f0b854c15de5..1e1c7e8d22ad 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21869.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21869.ps1 @@ -1,6 +1,6 @@ function Invoke-CippTestZTNA21869 { param($Tenant) - + #tenant try { $ServicePrincipals = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ServicePrincipals' if (-not $ServicePrincipals) { @@ -45,8 +45,7 @@ function Invoke-CippTestZTNA21869 { $Result = $ResultLines -join "`n" Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21869' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'Enterprise applications must require explicit assignment or scoped provisioning' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Application management' - } - catch { + } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21869' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'Enterprise applications must require explicit assignment or scoped provisioning' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Application management' diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21872.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21872.ps1 index 8695e061ddae..6fa29a976334 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21872.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21872.ps1 @@ -2,7 +2,7 @@ function Invoke-CippTestZTNA21872 { param($Tenant) $TestId = 'ZTNA21872' - + #Tested try { # Get conditional access policies and device registration policy from cache $CAPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ConditionalAccessPolicies' diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21874.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21874.ps1 index 4659c9c4f2ce..82f30f5f99c1 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21874.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21874.ps1 @@ -2,7 +2,7 @@ function Invoke-CippTestZTNA21874 { param($Tenant) $TestId = 'ZTNA21874' - + #Trusted try { # Get B2B Management Policy from cache $B2BManagementPolicyObject = New-CIPPDbRequest -TenantFilter $Tenant -Type 'B2BManagementPolicy' diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21877.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21877.ps1 index abfb3045d084..2d644fd0e654 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21877.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21877.ps1 @@ -1,6 +1,6 @@ function Invoke-CippTestZTNA21877 { param($Tenant) - + #Tested try { $Guests = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Guests' if (-not $Guests) { From 94663be1d6c275868dadba0e5628f991bb58f02f Mon Sep 17 00:00:00 2001 From: kakaiwa Date: Thu, 25 Dec 2025 01:59:58 -0500 Subject: [PATCH 044/503] Updated New-CIPPCAPolicy to update named locations when overwrite is true --- Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 | 23 +++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 b/Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 index 1de8340d4a6b..515c4c4934d8 100644 --- a/Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 +++ b/Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 @@ -173,13 +173,26 @@ function New-CIPPCAPolicy { if (!$location.displayName) { continue } $CheckExisting = New-GraphGETRequest -uri 'https://graph.microsoft.com/beta/identity/conditionalAccess/namedLocations' -tenantid $TenantFilter -asApp $true if ($Location.displayName -in $CheckExisting.displayName) { + $ExistingLocation = $CheckExisting | Where-Object -Property displayName -EQ $Location.displayName + if ($Overwrite) { + $LocationUpdate = $location | Select-Object * -ExcludeProperty id + Remove-ODataProperties -Object $LocationUpdate + $Body = ConvertTo-Json -InputObject $LocationUpdate -Depth 10 + try { + $null = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/identity/conditionalAccess/namedLocations/$($ExistingLocation.id)" -body $body -Type PATCH -tenantid $tenantfilter -asApp $true + Write-LogMessage -Tenant $TenantFilter -Headers $User -API $APINAME -message "Updated existing Named Location: $($location.displayName)" -Sev 'Info' + } catch { + Write-Warning "Failed to update location $($location.displayName): $_" + Write-LogMessage -Tenant $TenantFilter -Headers $User -API $APINAME -message "Failed to update existing Named Location: $($location.displayName). Error: $_" -Sev 'Error' + } + } else { + Write-LogMessage -Tenant $TenantFilter -Headers $User -API $APINAME -message "Matched a CA policy with the existing Named Location: $($location.displayName)" -Sev 'Info' + } [pscustomobject]@{ - id = ($CheckExisting | Where-Object -Property displayName -EQ $Location.displayName).id - name = ($CheckExisting | Where-Object -Property displayName -EQ $Location.displayName).displayName - templateId = $location.id + id = $ExistingLocation.id + name = $ExistingLocation.displayName + templateId = $location.id } - Write-LogMessage -Tenant $TenantFilter -Headers $User -API $APINAME -message "Matched a CA policy with the existing Named Location: $($location.displayName)" -Sev 'Info' - } else { if ($location.countriesAndRegions) { $location.countriesAndRegions = @($location.countriesAndRegions) } $location | Select-Object * -ExcludeProperty id From ac7a4dcd3441b0ecf8e8789d3388b9d0dbb508db Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Fri, 26 Dec 2025 15:21:58 +0100 Subject: [PATCH 045/503] remove backtics, mark as tested. --- .../Public/Tests/Invoke-CippTestZTNA21883.ps1 | 56 ++++++--- .../Public/Tests/Invoke-CippTestZTNA21886.ps1 | 4 +- .../Public/Tests/Invoke-CippTestZTNA21889.ps1 | 56 ++++++--- .../Public/Tests/Invoke-CippTestZTNA21892.ps1 | 56 ++++++--- .../Public/Tests/Invoke-CippTestZTNA21941.ps1 | 58 +++++++--- .../Public/Tests/Invoke-CippTestZTNA21953.ps1 | 54 ++++++--- .../Public/Tests/Invoke-CippTestZTNA21954.ps1 | 54 ++++++--- .../Public/Tests/Invoke-CippTestZTNA21955.ps1 | 54 ++++++--- .../Public/Tests/Invoke-CippTestZTNA22124.ps1 | 54 ++++++--- .../Public/Tests/Invoke-CippTestZTNA22659.ps1 | 54 ++++++--- .../Public/Tests/Invoke-CippTestZTNA24570.ps1 | 108 +++++++++++++----- .../Public/Tests/Invoke-CippTestZTNA24824.ps1 | 72 ++++++++---- .../Public/Tests/Invoke-CippTestZTNA24827.ps1 | 72 ++++++++---- 13 files changed, 540 insertions(+), 212 deletions(-) diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21883.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21883.ps1 index 11247eb87f57..797cb134fddf 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21883.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21883.ps1 @@ -17,17 +17,25 @@ function Invoke-CippTestZTNA21883 { [Parameter(Mandatory = $true)] [string]$Tenant ) - + #tested try { # Get Conditional Access policies from cache $Policies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ConditionalAccessPolicies' if (-not $Policies) { - Add-CippTestResult -TestId 'ZTNA21883' -TenantFilter $Tenant -TestType 'ZeroTrustNetworkAccess' -Status 'Skipped' ` - -ResultMarkdown 'No Conditional Access policies found in cache.' ` - -Risk 'Medium' -Name 'Workload identities configured with risk-based policies' ` - -UserImpact 'High' -ImplementationEffort 'Low' ` - -Category 'Access control' + $TestParams = @{ + TestId = 'ZTNA21883' + TenantFilter = $Tenant + TestType = 'ZeroTrustNetworkAccess' + Status = 'Skipped' + ResultMarkdown = 'No Conditional Access policies found in cache.' + Risk = 'Medium' + Name = 'Workload identities configured with risk-based policies' + UserImpact = 'High' + ImplementationEffort = 'Low' + Category = 'Access control' + } + Add-CippTestResult @TestParams return } @@ -92,18 +100,34 @@ function Invoke-CippTestZTNA21883 { $ResultMarkdown += 'Workload identities should be protected by policies that block authentication when service principal risk is detected.' } - Add-CippTestResult -TestId 'ZTNA21883' -TenantFilter $Tenant -TestType 'ZeroTrustNetworkAccess' -Status $Status ` - -ResultMarkdown $ResultMarkdown ` - -Risk 'Medium' -Name 'Workload identities configured with risk-based policies' ` - -UserImpact 'High' -ImplementationEffort 'Low' ` - -Category 'Access control' + $TestParams = @{ + TestId = 'ZTNA21883' + TenantFilter = $Tenant + TestType = 'ZeroTrustNetworkAccess' + Status = $Status + ResultMarkdown = $ResultMarkdown + Risk = 'Medium' + Name = 'Workload identities configured with risk-based policies' + UserImpact = 'High' + ImplementationEffort = 'Low' + Category = 'Access control' + } + Add-CippTestResult @TestParams } catch { - Add-CippTestResult -TestId 'ZTNA21883' -TenantFilter $Tenant -TestType 'ZeroTrustNetworkAccess' -Status 'Failed' ` - -ResultMarkdown "❌ **Error**: $($_.Exception.Message)" ` - -Risk 'Medium' -Name 'Workload identities configured with risk-based policies' ` - -UserImpact 'High' -ImplementationEffort 'Low' ` - -Category 'Access control' + $TestParams = @{ + TestId = 'ZTNA21883' + TenantFilter = $Tenant + TestType = 'ZeroTrustNetworkAccess' + Status = 'Failed' + ResultMarkdown = "❌ **Error**: $($_.Exception.Message)" + Risk = 'Medium' + Name = 'Workload identities configured with risk-based policies' + UserImpact = 'High' + ImplementationEffort = 'Low' + Category = 'Access control' + } + Add-CippTestResult @TestParams Write-LogMessage -API 'ZeroTrustNetworkAccess' -tenant $Tenant -message "Test ZTNA21883 failed: $($_.Exception.Message)" -sev Error } } diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21886.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21886.ps1 index f2017d707627..451d5d3e6a04 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21886.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21886.ps1 @@ -1,6 +1,6 @@ function Invoke-CippTestZTNA21886 { param($Tenant) - + #Tested try { $ServicePrincipals = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ServicePrincipals' if (-not $ServicePrincipals) { @@ -29,7 +29,7 @@ function Invoke-CippTestZTNA21886 { $SSOByType = $AppsWithSSO | Group-Object -Property preferredSingleSignOnMode foreach ($Group in $SSOByType) { - $ResultLines += "" + $ResultLines += '' $ResultLines += "**$($Group.Name.ToUpper()) SSO** ($($Group.Count) app(s)):" $Top5 = $Group.Group | Select-Object -First 5 foreach ($App in $Top5) { diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21889.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21889.ps1 index c8ff7fdf08d2..87caa4f5c591 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21889.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21889.ps1 @@ -15,17 +15,25 @@ function Invoke-CippTestZTNA21889 { [Parameter(Mandatory = $true)] [string]$Tenant ) - + #tested try { # Get authentication methods policy from cache $AuthMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' if (-not $AuthMethodsPolicy) { - Add-CippTestResult -TestId 'ZTNA21889' -TenantFilter $Tenant -TestType 'ZeroTrustNetworkAccess' -Status 'Skipped' ` - -ResultMarkdown 'Unable to retrieve authentication methods policy from cache.' ` - -Risk 'High' -Name 'Reduce the user-visible password surface area' ` - -UserImpact 'Medium' -ImplementationEffort 'Medium' ` - -Category 'Access control' + $TestParams = @{ + TestId = 'ZTNA21889' + TenantFilter = $Tenant + TestType = 'ZeroTrustNetworkAccess' + Status = 'Skipped' + ResultMarkdown = 'Unable to retrieve authentication methods policy from cache.' + Risk = 'High' + Name = 'Reduce the user-visible password surface area' + UserImpact = 'Medium' + ImplementationEffort = 'Medium' + Category = 'Access control' + } + Add-CippTestResult @TestParams return } @@ -106,18 +114,34 @@ function Invoke-CippTestZTNA21889 { $AuthStatus = if ($AuthValid) { '✅ Pass' } else { '❌ Fail' } $ResultMarkdown += "| Microsoft Authenticator | $AuthState | $AuthTargetsDisplay | $AuthModeDisplay | $AuthStatus |`n" - Add-CippTestResult -TestId 'ZTNA21889' -TenantFilter $Tenant -TestType 'ZeroTrustNetworkAccess' -Status $Status ` - -ResultMarkdown $ResultMarkdown ` - -Risk 'High' -Name 'Reduce the user-visible password surface area' ` - -UserImpact 'Medium' -ImplementationEffort 'Medium' ` - -Category 'Access control' + $TestParams = @{ + TestId = 'ZTNA21889' + TenantFilter = $Tenant + TestType = 'ZeroTrustNetworkAccess' + Status = $Status + ResultMarkdown = $ResultMarkdown + Risk = 'High' + Name = 'Reduce the user-visible password surface area' + UserImpact = 'Medium' + ImplementationEffort = 'Medium' + Category = 'Access control' + } + Add-CippTestResult @TestParams } catch { - Add-CippTestResult -TestId 'ZTNA21889' -TenantFilter $Tenant -TestType 'ZeroTrustNetworkAccess' -Status 'Failed' ` - -ResultMarkdown "❌ **Error**: $($_.Exception.Message)" ` - -Risk 'High' -Name 'Reduce the user-visible password surface area' ` - -UserImpact 'Medium' -ImplementationEffort 'Medium' ` - -Category 'Access control' + $TestParams = @{ + TestId = 'ZTNA21889' + TenantFilter = $Tenant + TestType = 'ZeroTrustNetworkAccess' + Status = 'Failed' + ResultMarkdown = "❌ **Error**: $($_.Exception.Message)" + Risk = 'High' + Name = 'Reduce the user-visible password surface area' + UserImpact = 'Medium' + ImplementationEffort = 'Medium' + Category = 'Access control' + } + Add-CippTestResult @TestParams Write-LogMessage -API 'ZeroTrustNetworkAccess' -tenant $Tenant -message "Test ZTNA21889 failed: $($_.Exception.Message)" -sev Error } } diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21892.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21892.ps1 index c35b44a8a827..12dd5a45cb6d 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21892.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21892.ps1 @@ -18,17 +18,25 @@ function Invoke-CippTestZTNA21892 { [Parameter(Mandatory = $true)] [string]$Tenant ) - + #tested try { # Get Conditional Access policies from cache $Policies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ConditionalAccessPolicies' if (-not $Policies) { - Add-CippTestResult -TestId 'ZTNA21892' -TenantFilter $Tenant -TestType 'ZeroTrustNetworkAccess' -Status 'Skipped' ` - -ResultMarkdown 'No Conditional Access policies found in cache.' ` - -Risk 'High' -Name 'All sign-in activity comes from managed devices' ` - -UserImpact 'High' -ImplementationEffort 'High' ` - -Category 'Access control' + $TestParams = @{ + TestId = 'ZTNA21892' + TenantFilter = $Tenant + TestType = 'ZeroTrustNetworkAccess' + Status = 'Skipped' + ResultMarkdown = 'No Conditional Access policies found in cache.' + Risk = 'High' + Name = 'All sign-in activity comes from managed devices' + UserImpact = 'High' + ImplementationEffort = 'High' + Category = 'Access control' + } + Add-CippTestResult @TestParams return } @@ -112,18 +120,34 @@ function Invoke-CippTestZTNA21892 { $ResultMarkdown += 'Organizations should enforce that all sign-ins come from managed devices (compliant or hybrid Azure AD joined) to ensure security controls are applied.' } - Add-CippTestResult -TestId 'ZTNA21892' -TenantFilter $Tenant -TestType 'ZeroTrustNetworkAccess' -Status $Status ` - -ResultMarkdown $ResultMarkdown ` - -Risk 'High' -Name 'All sign-in activity comes from managed devices' ` - -UserImpact 'High' -ImplementationEffort 'High' ` - -Category 'Access control' + $TestParams = @{ + TestId = 'ZTNA21892' + TenantFilter = $Tenant + TestType = 'ZeroTrustNetworkAccess' + Status = $Status + ResultMarkdown = $ResultMarkdown + Risk = 'High' + Name = 'All sign-in activity comes from managed devices' + UserImpact = 'High' + ImplementationEffort = 'High' + Category = 'Access control' + } + Add-CippTestResult @TestParams } catch { - Add-CippTestResult -TestId 'ZTNA21892' -TenantFilter $Tenant -TestType 'ZeroTrustNetworkAccess' -Status 'Failed' ` - -ResultMarkdown "❌ **Error**: $($_.Exception.Message)" ` - -Risk 'High' -Name 'All sign-in activity comes from managed devices' ` - -UserImpact 'High' -ImplementationEffort 'High' ` - -Category 'Access control' + $TestParams = @{ + TestId = 'ZTNA21892' + TenantFilter = $Tenant + TestType = 'ZeroTrustNetworkAccess' + Status = 'Failed' + ResultMarkdown = "❌ **Error**: $($_.Exception.Message)" + Risk = 'High' + Name = 'All sign-in activity comes from managed devices' + UserImpact = 'High' + ImplementationEffort = 'High' + Category = 'Access control' + } + Add-CippTestResult @TestParams Write-LogMessage -API 'ZeroTrustNetworkAccess' -tenant $Tenant -message "Test ZTNA21892 failed: $($_.Exception.Message)" -sev Error } } diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21941.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21941.ps1 index cbfdee5e139e..2bcb40cfdb3d 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21941.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21941.ps1 @@ -22,11 +22,19 @@ function Invoke-CippTestZTNA21941 { $CAPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ConditionalAccessPolicies' if (-not $CAPolicies) { - Add-CippTestResult -TestId 'ZTNA21941' -TenantFilter $Tenant -TestType 'ZeroTrustNetworkAccess' -Status 'Skipped' ` - -ResultMarkdown 'Unable to retrieve Conditional Access policies from cache.' ` - -Risk 'High' -Name 'Implement token protection policies' ` - -UserImpact 'Medium' -ImplementationEffort 'Medium' ` - -Category 'Access control' + $TestParams = @{ + TestId = 'ZTNA21941' + TenantFilter = $Tenant + TestType = 'ZeroTrustNetworkAccess' + Status = 'Skipped' + ResultMarkdown = 'Unable to retrieve Conditional Access policies from cache.' + Risk = 'High' + Name = 'Implement token protection policies' + UserImpact = 'Medium' + ImplementationEffort = 'Medium' + Category = 'Access control' + } + Add-CippTestResult @TestParams return } @@ -38,7 +46,7 @@ function Invoke-CippTestZTNA21941 { # Filter for policies with Windows platform and secureSignInSession control $TokenProtectionPolicies = [System.Collections.Generic.List[object]]::new() - + foreach ($policy in $CAPolicies) { # Check if policy has Windows platform $hasWindows = $false @@ -144,25 +152,41 @@ function Invoke-CippTestZTNA21941 { $usersIcon = if ($policy.HasUsers) { '✅' } else { '❌' } $appsIcon = if ($policy.HasRequiredApps) { '✅' } else { '❌' } $statusIcon = if ($policy.Status -eq 'Pass') { '✅' } else { '❌' } - + $ResultMarkdown += "| $($policy.Name) | $stateIcon $($policy.State) | $usersIcon | $appsIcon | $statusIcon $($policy.Status) |`n" } $ResultMarkdown += "`n[Review policies](https://entra.microsoft.com/#view/Microsoft_AAD_ConditionalAccess/ConditionalAccessBlade/~/Policies)" } - Add-CippTestResult -TestId 'ZTNA21941' -TenantFilter $Tenant -TestType 'ZeroTrustNetworkAccess' -Status $Status ` - -ResultMarkdown $ResultMarkdown ` - -Risk 'High' -Name 'Implement token protection policies' ` - -UserImpact 'Medium' -ImplementationEffort 'Medium' ` - -Category 'Access control' + $TestParams = @{ + TestId = 'ZTNA21941' + TenantFilter = $Tenant + TestType = 'ZeroTrustNetworkAccess' + Status = $Status + ResultMarkdown = $ResultMarkdown + Risk = 'High' + Name = 'Implement token protection policies' + UserImpact = 'Medium' + ImplementationEffort = 'Medium' + Category = 'Access control' + } + Add-CippTestResult @TestParams } catch { - Add-CippTestResult -TestId 'ZTNA21941' -TenantFilter $Tenant -TestType 'ZeroTrustNetworkAccess' -Status 'Failed' ` - -ResultMarkdown "❌ **Error**: $($_.Exception.Message)" ` - -Risk 'High' -Name 'Implement token protection policies' ` - -UserImpact 'Medium' -ImplementationEffort 'Medium' ` - -Category 'Access control' + $TestParams = @{ + TestId = 'ZTNA21941' + TenantFilter = $Tenant + TestType = 'ZeroTrustNetworkAccess' + Status = 'Failed' + ResultMarkdown = "❌ **Error**: $($_.Exception.Message)" + Risk = 'High' + Name = 'Implement token protection policies' + UserImpact = 'Medium' + ImplementationEffort = 'Medium' + Category = 'Access control' + } + Add-CippTestResult @TestParams Write-LogMessage -API 'ZeroTrustNetworkAccess' -tenant $Tenant -message "Test ZTNA21941 failed: $($_.Exception.Message)" -sev Error } } diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21953.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21953.ps1 index ea12a7238e42..0637aa913aaa 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21953.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21953.ps1 @@ -21,11 +21,19 @@ function Invoke-CippTestZTNA21953 { $DeviceRegPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'DeviceRegistrationPolicy' if (-not $DeviceRegPolicy) { - Add-CippTestResult -TestId 'ZTNA21953' -TenantFilter $Tenant -TestType 'ZeroTrustNetworkAccess' -Status 'Skipped' ` - -ResultMarkdown 'Unable to retrieve device registration policy from cache.' ` - -Risk 'High' -Name 'Deploy Windows Local Administrator Password Solution (LAPS)' ` - -UserImpact 'Low' -ImplementationEffort 'Low' ` - -Category 'Device security' + $TestParams = @{ + TestId = 'ZTNA21953' + TenantFilter = $Tenant + TestType = 'ZeroTrustNetworkAccess' + Status = 'Skipped' + ResultMarkdown = 'Unable to retrieve device registration policy from cache.' + Risk = 'High' + Name = 'Deploy Windows Local Administrator Password Solution (LAPS)' + UserImpact = 'Low' + ImplementationEffort = 'Low' + Category = 'Device security' + } + Add-CippTestResult @TestParams return } @@ -42,18 +50,34 @@ function Invoke-CippTestZTNA21953 { $ResultMarkdown += '[Deploy LAPS](https://entra.microsoft.com/#view/Microsoft_AAD_Devices/DevicesMenuBlade/~/DeviceSettings/menuId/)' } - Add-CippTestResult -TestId 'ZTNA21953' -TenantFilter $Tenant -TestType 'ZeroTrustNetworkAccess' -Status $Status ` - -ResultMarkdown $ResultMarkdown ` - -Risk 'High' -Name 'Deploy Windows Local Administrator Password Solution (LAPS)' ` - -UserImpact 'Low' -ImplementationEffort 'Low' ` - -Category 'Device security' + $TestParams = @{ + TestId = 'ZTNA21953' + TenantFilter = $Tenant + TestType = 'ZeroTrustNetworkAccess' + Status = $Status + ResultMarkdown = $ResultMarkdown + Risk = 'High' + Name = 'Deploy Windows Local Administrator Password Solution (LAPS)' + UserImpact = 'Low' + ImplementationEffort = 'Low' + Category = 'Device security' + } + Add-CippTestResult @TestParams } catch { - Add-CippTestResult -TestId 'ZTNA21953' -TenantFilter $Tenant -TestType 'ZeroTrustNetworkAccess' -Status 'Failed' ` - -ResultMarkdown "❌ **Error**: $($_.Exception.Message)" ` - -Risk 'High' -Name 'Deploy Windows Local Administrator Password Solution (LAPS)' ` - -UserImpact 'Low' -ImplementationEffort 'Low' ` - -Category 'Device security' + $TestParams = @{ + TestId = 'ZTNA21953' + TenantFilter = $Tenant + TestType = 'ZeroTrustNetworkAccess' + Status = 'Failed' + ResultMarkdown = "❌ **Error**: $($_.Exception.Message)" + Risk = 'High' + Name = 'Deploy Windows Local Administrator Password Solution (LAPS)' + UserImpact = 'Low' + ImplementationEffort = 'Low' + Category = 'Device security' + } + Add-CippTestResult @TestParams Write-LogMessage -API 'ZeroTrustNetworkAccess' -tenant $Tenant -message "Test ZTNA21953 failed: $($_.Exception.Message)" -sev Error } } diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21954.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21954.ps1 index 2bae6ba273ed..b534ad7d36ce 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21954.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21954.ps1 @@ -21,11 +21,19 @@ function Invoke-CippTestZTNA21954 { $AuthPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthorizationPolicy' if (-not $AuthPolicy) { - Add-CippTestResult -TestId 'ZTNA21954' -TenantFilter $Tenant -TestType 'ZeroTrustNetworkAccess' -Status 'Skipped' ` - -ResultMarkdown 'Unable to retrieve authorization policy from cache.' ` - -Risk 'Low' -Name 'Restrict non-admin users from reading BitLocker recovery keys' ` - -UserImpact 'Low' -ImplementationEffort 'Low' ` - -Category 'Device security' + $TestParams = @{ + TestId = 'ZTNA21954' + TenantFilter = $Tenant + TestType = 'ZeroTrustNetworkAccess' + Status = 'Skipped' + ResultMarkdown = 'Unable to retrieve authorization policy from cache.' + Risk = 'Low' + Name = 'Restrict non-admin users from reading BitLocker recovery keys' + UserImpact = 'Low' + ImplementationEffort = 'Low' + Category = 'Device security' + } + Add-CippTestResult @TestParams return } @@ -42,18 +50,34 @@ function Invoke-CippTestZTNA21954 { $ResultMarkdown += '[Restrict access](https://entra.microsoft.com/#view/Microsoft_AAD_IAM/PoliciesTemplateBlade)' } - Add-CippTestResult -TestId 'ZTNA21954' -TenantFilter $Tenant -TestType 'ZeroTrustNetworkAccess' -Status $Status ` - -ResultMarkdown $ResultMarkdown ` - -Risk 'Low' -Name 'Restrict non-admin users from reading BitLocker recovery keys' ` - -UserImpact 'Low' -ImplementationEffort 'Low' ` - -Category 'Device security' + $TestParams = @{ + TestId = 'ZTNA21954' + TenantFilter = $Tenant + TestType = 'ZeroTrustNetworkAccess' + Status = $Status + ResultMarkdown = $ResultMarkdown + Risk = 'Low' + Name = 'Restrict non-admin users from reading BitLocker recovery keys' + UserImpact = 'Low' + ImplementationEffort = 'Low' + Category = 'Device security' + } + Add-CippTestResult @TestParams } catch { - Add-CippTestResult -TestId 'ZTNA21954' -TenantFilter $Tenant -TestType 'ZeroTrustNetworkAccess' -Status 'Failed' ` - -ResultMarkdown "❌ **Error**: $($_.Exception.Message)" ` - -Risk 'Low' -Name 'Restrict non-admin users from reading BitLocker recovery keys' ` - -UserImpact 'Low' -ImplementationEffort 'Low' ` - -Category 'Device security' + $TestParams = @{ + TestId = 'ZTNA21954' + TenantFilter = $Tenant + TestType = 'ZeroTrustNetworkAccess' + Status = 'Failed' + ResultMarkdown = "❌ **Error**: $($_.Exception.Message)" + Risk = 'Low' + Name = 'Restrict non-admin users from reading BitLocker recovery keys' + UserImpact = 'Low' + ImplementationEffort = 'Low' + Category = 'Device security' + } + Add-CippTestResult @TestParams Write-LogMessage -API 'ZeroTrustNetworkAccess' -tenant $Tenant -message "Test ZTNA21954 failed: $($_.Exception.Message)" -sev Error } } diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21955.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21955.ps1 index 7974185fcc0a..ed597965ffbf 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21955.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21955.ps1 @@ -21,11 +21,19 @@ function Invoke-CippTestZTNA21955 { $DeviceRegPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'DeviceRegistrationPolicy' if (-not $DeviceRegPolicy) { - Add-CippTestResult -TestId 'ZTNA21955' -TenantFilter $Tenant -TestType 'ZeroTrustNetworkAccess' -Status 'Skipped' ` - -ResultMarkdown 'Unable to retrieve device registration policy from cache.' ` - -Risk 'Medium' -Name 'Manage local admins on Entra joined devices' ` - -UserImpact 'Low' -ImplementationEffort 'Low' ` - -Category 'Device security' + $TestParams = @{ + TestId = 'ZTNA21955' + TenantFilter = $Tenant + TestType = 'ZeroTrustNetworkAccess' + Status = 'Skipped' + ResultMarkdown = 'Unable to retrieve device registration policy from cache.' + Risk = 'Medium' + Name = 'Manage local admins on Entra joined devices' + UserImpact = 'Low' + ImplementationEffort = 'Low' + Category = 'Device security' + } + Add-CippTestResult @TestParams return } @@ -42,18 +50,34 @@ function Invoke-CippTestZTNA21955 { $ResultMarkdown += '[Configure settings](https://entra.microsoft.com/#view/Microsoft_AAD_Devices/DevicesMenuBlade/~/DeviceSettings/menuId/)' } - Add-CippTestResult -TestId 'ZTNA21955' -TenantFilter $Tenant -TestType 'ZeroTrustNetworkAccess' -Status $Status ` - -ResultMarkdown $ResultMarkdown ` - -Risk 'Medium' -Name 'Manage local admins on Entra joined devices' ` - -UserImpact 'Low' -ImplementationEffort 'Low' ` - -Category 'Device security' + $TestParams = @{ + TestId = 'ZTNA21955' + TenantFilter = $Tenant + TestType = 'ZeroTrustNetworkAccess' + Status = $Status + ResultMarkdown = $ResultMarkdown + Risk = 'Medium' + Name = 'Manage local admins on Entra joined devices' + UserImpact = 'Low' + ImplementationEffort = 'Low' + Category = 'Device security' + } + Add-CippTestResult @TestParams } catch { - Add-CippTestResult -TestId 'ZTNA21955' -TenantFilter $Tenant -TestType 'ZeroTrustNetworkAccess' -Status 'Failed' ` - -ResultMarkdown "❌ **Error**: $($_.Exception.Message)" ` - -Risk 'Medium' -Name 'Manage local admins on Entra joined devices' ` - -UserImpact 'Low' -ImplementationEffort 'Low' ` - -Category 'Device security' + $TestParams = @{ + TestId = 'ZTNA21955' + TenantFilter = $Tenant + TestType = 'ZeroTrustNetworkAccess' + Status = 'Failed' + ResultMarkdown = "❌ **Error**: $($_.Exception.Message)" + Risk = 'Medium' + Name = 'Manage local admins on Entra joined devices' + UserImpact = 'Low' + ImplementationEffort = 'Low' + Category = 'Device security' + } + Add-CippTestResult @TestParams Write-LogMessage -API 'ZeroTrustNetworkAccess' -tenant $Tenant -message "Test ZTNA21955 failed: $($_.Exception.Message)" -sev Error } } diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA22124.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA22124.ps1 index ab985dd1aaad..6619396c8bd7 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA22124.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA22124.ps1 @@ -21,11 +21,19 @@ function Invoke-CippTestZTNA22124 { $Recommendations = New-CIPPDbRequest -TenantFilter $Tenant -Type 'DirectoryRecommendations' if (-not $Recommendations) { - Add-CippTestResult -TestId 'ZTNA22124' -TenantFilter $Tenant -TestType 'ZeroTrustNetworkAccess' -Status 'Skipped' ` - -ResultMarkdown 'Unable to retrieve directory recommendations from cache.' ` - -Risk 'High' -Name 'Address high priority Entra recommendations' ` - -UserImpact 'Medium' -ImplementationEffort 'Medium' ` - -Category 'Governance' + $TestParams = @{ + TestId = 'ZTNA22124' + TenantFilter = $Tenant + TestType = 'ZeroTrustNetworkAccess' + Status = 'Skipped' + ResultMarkdown = 'Unable to retrieve directory recommendations from cache.' + Risk = 'High' + Name = 'Address high priority Entra recommendations' + UserImpact = 'Medium' + ImplementationEffort = 'Medium' + Category = 'Governance' + } + Add-CippTestResult @TestParams return } @@ -58,18 +66,34 @@ function Invoke-CippTestZTNA22124 { $ResultMarkdown += "`n[Address recommendations](https://entra.microsoft.com/#view/Microsoft_Azure_SecureScore/OverviewBlade)" } - Add-CippTestResult -TestId 'ZTNA22124' -TenantFilter $Tenant -TestType 'ZeroTrustNetworkAccess' -Status $Status ` - -ResultMarkdown $ResultMarkdown ` - -Risk 'High' -Name 'Address high priority Entra recommendations' ` - -UserImpact 'Medium' -ImplementationEffort 'Medium' ` - -Category 'Governance' + $TestParams = @{ + TestId = 'ZTNA22124' + TenantFilter = $Tenant + TestType = 'ZeroTrustNetworkAccess' + Status = $Status + ResultMarkdown = $ResultMarkdown + Risk = 'High' + Name = 'Address high priority Entra recommendations' + UserImpact = 'Medium' + ImplementationEffort = 'Medium' + Category = 'Governance' + } + Add-CippTestResult @TestParams } catch { - Add-CippTestResult -TestId 'ZTNA22124' -TenantFilter $Tenant -TestType 'ZeroTrustNetworkAccess' -Status 'Failed' ` - -ResultMarkdown "❌ **Error**: $($_.Exception.Message)" ` - -Risk 'High' -Name 'Address high priority Entra recommendations' ` - -UserImpact 'Medium' -ImplementationEffort 'Medium' ` - -Category 'Governance' + $TestParams = @{ + TestId = 'ZTNA22124' + TenantFilter = $Tenant + TestType = 'ZeroTrustNetworkAccess' + Status = 'Failed' + ResultMarkdown = "❌ **Error**: $($_.Exception.Message)" + Risk = 'High' + Name = 'Address high priority Entra recommendations' + UserImpact = 'Medium' + ImplementationEffort = 'Medium' + Category = 'Governance' + } + Add-CippTestResult @TestParams Write-LogMessage -API 'ZeroTrustNetworkAccess' -tenant $Tenant -message "Test ZTNA22124 failed: $($_.Exception.Message)" -sev Error } } diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA22659.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA22659.ps1 index e982aff18d9d..a0cde338b635 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA22659.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA22659.ps1 @@ -21,11 +21,19 @@ function Invoke-CippTestZTNA22659 { $RiskDetections = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ServicePrincipalRiskDetections' if (-not $RiskDetections) { - Add-CippTestResult -TestId 'ZTNA22659' -TenantFilter $Tenant -TestType 'ZeroTrustNetworkAccess' -Status 'Skipped' ` - -ResultMarkdown 'Unable to retrieve service principal risk detections from cache.' ` - -Risk 'High' -Name 'Triage risky workload identity sign-ins' ` - -UserImpact 'High' -ImplementationEffort 'Low' ` - -Category 'Identity protection' + $TestParams = @{ + TestId = 'ZTNA22659' + TenantFilter = $Tenant + TestType = 'ZeroTrustNetworkAccess' + Status = 'Skipped' + ResultMarkdown = 'Unable to retrieve service principal risk detections from cache.' + Risk = 'High' + Name = 'Triage risky workload identity sign-ins' + UserImpact = 'High' + ImplementationEffort = 'Low' + Category = 'Identity protection' + } + Add-CippTestResult @TestParams return } @@ -71,18 +79,34 @@ function Invoke-CippTestZTNA22659 { $ResultMarkdown += "`n[Investigate and remediate](https://entra.microsoft.com/#view/Microsoft_AAD_IAM/IdentityProtectionMenuBlade/~/RiskyServicePrincipals)" } - Add-CippTestResult -TestId 'ZTNA22659' -TenantFilter $Tenant -TestType 'ZeroTrustNetworkAccess' -Status $Status ` - -ResultMarkdown $ResultMarkdown ` - -Risk 'High' -Name 'Triage risky workload identity sign-ins' ` - -UserImpact 'High' -ImplementationEffort 'Low' ` - -Category 'Identity protection' + $TestParams = @{ + TestId = 'ZTNA22659' + TenantFilter = $Tenant + TestType = 'ZeroTrustNetworkAccess' + Status = $Status + ResultMarkdown = $ResultMarkdown + Risk = 'High' + Name = 'Triage risky workload identity sign-ins' + UserImpact = 'High' + ImplementationEffort = 'Low' + Category = 'Identity protection' + } + Add-CippTestResult @TestParams } catch { - Add-CippTestResult -TestId 'ZTNA22659' -TenantFilter $Tenant -TestType 'ZeroTrustNetworkAccess' -Status 'Failed' ` - -ResultMarkdown "❌ **Error**: $($_.Exception.Message)" ` - -Risk 'High' -Name 'Triage risky workload identity sign-ins' ` - -UserImpact 'High' -ImplementationEffort 'Low' ` - -Category 'Identity protection' + $TestParams = @{ + TestId = 'ZTNA22659' + TenantFilter = $Tenant + TestType = 'ZeroTrustNetworkAccess' + Status = 'Failed' + ResultMarkdown = "❌ **Error**: $($_.Exception.Message)" + Risk = 'High' + Name = 'Triage risky workload identity sign-ins' + UserImpact = 'High' + ImplementationEffort = 'Low' + Category = 'Identity protection' + } + Add-CippTestResult @TestParams Write-LogMessage -API 'ZeroTrustNetworkAccess' -tenant $Tenant -message "Test ZTNA22659 failed: $($_.Exception.Message)" -sev Error } } diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24570.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24570.ps1 index 3d9244258d42..3a5ca98e782f 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24570.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24570.ps1 @@ -22,11 +22,19 @@ function Invoke-CippTestZTNA24570 { $OrgInfo = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Organization' if (-not $OrgInfo) { - Add-CippTestResult -TestId 'ZTNA24570' -TenantFilter $Tenant -TestType 'ZeroTrustNetworkAccess' -Status 'Skipped' ` - -ResultMarkdown 'Unable to retrieve organization information from cache.' ` - -Risk 'High' -Name 'Entra Connect uses a service principal' ` - -UserImpact 'Medium' -ImplementationEffort 'High' ` - -Category 'Access control' + $TestParams = @{ + TestId = 'ZTNA24570' + TenantFilter = $Tenant + TestType = 'ZeroTrustNetworkAccess' + Status = 'Skipped' + ResultMarkdown = 'Unable to retrieve organization information from cache.' + Risk = 'High' + Name = 'Entra Connect uses a service principal' + UserImpact = 'Medium' + ImplementationEffort = 'High' + Category = 'Access control' + } + Add-CippTestResult @TestParams return } @@ -34,11 +42,19 @@ function Invoke-CippTestZTNA24570 { $HybridEnabled = $OrgInfo.onPremisesSyncEnabled -eq $true if (-not $HybridEnabled) { - Add-CippTestResult -TestId 'ZTNA24570' -TenantFilter $Tenant -TestType 'ZeroTrustNetworkAccess' -Status 'Skipped' ` - -ResultMarkdown '✅ **N/A**: Hybrid identity synchronization is not enabled in this tenant.' ` - -Risk 'High' -Name 'Entra Connect uses a service principal' ` - -UserImpact 'Medium' -ImplementationEffort 'High' ` - -Category 'Access control' + $TestParams = @{ + TestId = 'ZTNA24570' + TenantFilter = $Tenant + TestType = 'ZeroTrustNetworkAccess' + Status = 'Skipped' + ResultMarkdown = '✅ **N/A**: Hybrid identity synchronization is not enabled in this tenant.' + Risk = 'High' + Name = 'Entra Connect uses a service principal' + UserImpact = 'Medium' + ImplementationEffort = 'High' + Category = 'Access control' + } + Add-CippTestResult @TestParams return } @@ -46,11 +62,19 @@ function Invoke-CippTestZTNA24570 { $Roles = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Roles' if (-not $Roles) { - Add-CippTestResult -TestId 'ZTNA24570' -TenantFilter $Tenant -TestType 'ZeroTrustNetworkAccess' -Status 'Skipped' ` - -ResultMarkdown 'Unable to retrieve roles from cache.' ` - -Risk 'High' -Name 'Entra Connect uses a service principal' ` - -UserImpact 'Medium' -ImplementationEffort 'High' ` - -Category 'Access control' + $TestParams = @{ + TestId = 'ZTNA24570' + TenantFilter = $Tenant + TestType = 'ZeroTrustNetworkAccess' + Status = 'Skipped' + ResultMarkdown = 'Unable to retrieve roles from cache.' + Risk = 'High' + Name = 'Entra Connect uses a service principal' + UserImpact = 'Medium' + ImplementationEffort = 'High' + Category = 'Access control' + } + Add-CippTestResult @TestParams return } @@ -64,11 +88,19 @@ function Invoke-CippTestZTNA24570 { } if (-not $DirSyncRole) { - Add-CippTestResult -TestId 'ZTNA24570' -TenantFilter $Tenant -TestType 'ZeroTrustNetworkAccess' -Status 'Failed' ` - -ResultMarkdown '❌ **Error**: Unable to find Directory Synchronization Accounts role in cache.' ` - -Risk 'High' -Name 'Entra Connect uses a service principal' ` - -UserImpact 'Medium' -ImplementationEffort 'High' ` - -Category 'Access control' + $TestParams = @{ + TestId = 'ZTNA24570' + TenantFilter = $Tenant + TestType = 'ZeroTrustNetworkAccess' + Status = 'Failed' + ResultMarkdown = '❌ **Error**: Unable to find Directory Synchronization Accounts role in cache.' + Risk = 'High' + Name = 'Entra Connect uses a service principal' + UserImpact = 'Medium' + ImplementationEffort = 'High' + Category = 'Access control' + } + Add-CippTestResult @TestParams return } @@ -122,18 +154,34 @@ function Invoke-CippTestZTNA24570 { $ResultMarkdown += "`n[Migrate to service principal](https://entra.microsoft.com/#view/Microsoft_AAD_IAM/RolesManagementMenuBlade/~/AllRoles)" } - Add-CippTestResult -TestId 'ZTNA24570' -TenantFilter $Tenant -TestType 'ZeroTrustNetworkAccess' -Status $Status ` - -ResultMarkdown $ResultMarkdown ` - -Risk 'High' -Name 'Entra Connect uses a service principal' ` - -UserImpact 'Medium' -ImplementationEffort 'High' ` - -Category 'Access control' + $TestParams = @{ + TestId = 'ZTNA24570' + TenantFilter = $Tenant + TestType = 'ZeroTrustNetworkAccess' + Status = $Status + ResultMarkdown = $ResultMarkdown + Risk = 'High' + Name = 'Entra Connect uses a service principal' + UserImpact = 'Medium' + ImplementationEffort = 'High' + Category = 'Access control' + } + Add-CippTestResult @TestParams } catch { - Add-CippTestResult -TestId 'ZTNA24570' -TenantFilter $Tenant -TestType 'ZeroTrustNetworkAccess' -Status 'Failed' ` - -ResultMarkdown "❌ **Error**: $($_.Exception.Message)" ` - -Risk 'High' -Name 'Entra Connect uses a service principal' ` - -UserImpact 'Medium' -ImplementationEffort 'High' ` - -Category 'Access control' + $TestParams = @{ + TestId = 'ZTNA24570' + TenantFilter = $Tenant + TestType = 'ZeroTrustNetworkAccess' + Status = 'Failed' + ResultMarkdown = "❌ **Error**: $($_.Exception.Message)" + Risk = 'High' + Name = 'Entra Connect uses a service principal' + UserImpact = 'Medium' + ImplementationEffort = 'High' + Category = 'Access control' + } + Add-CippTestResult @TestParams Write-LogMessage -API 'ZeroTrustNetworkAccess' -tenant $Tenant -message "Test ZTNA24570 failed: $($_.Exception.Message)" -sev Error } } diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24824.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24824.ps1 index d403677d69fc..fe0d11172ed7 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24824.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24824.ps1 @@ -21,11 +21,19 @@ function Invoke-CippTestZTNA24824 { $CAPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ConditionalAccessPolicies' if (-not $CAPolicies) { - Add-CippTestResult -TestId 'ZTNA24824' -TenantFilter $Tenant -TestType 'ZeroTrustNetworkAccess' -Status 'Skipped' ` - -ResultMarkdown 'Unable to retrieve Conditional Access policies from cache.' ` - -Risk 'High' -Name 'CA policies block access from noncompliant devices' ` - -UserImpact 'Medium' -ImplementationEffort 'Medium' ` - -Category 'Device security' + $TestParams = @{ + TestId = 'ZTNA24824' + TenantFilter = $Tenant + TestType = 'ZeroTrustNetworkAccess' + Status = 'Skipped' + ResultMarkdown = 'Unable to retrieve Conditional Access policies from cache.' + Risk = 'High' + Name = 'CA policies block access from noncompliant devices' + UserImpact = 'Medium' + ImplementationEffort = 'Medium' + Category = 'Device security' + } + Add-CippTestResult @TestParams return } @@ -41,11 +49,19 @@ function Invoke-CippTestZTNA24824 { } if ($CompliantDevicePolicies.Count -eq 0) { - Add-CippTestResult -TestId 'ZTNA24824' -TenantFilter $Tenant -TestType 'ZeroTrustNetworkAccess' -Status 'Failed' ` - -ResultMarkdown "❌ **Fail**: No Conditional Access policies found that block access from noncompliant devices.`n`n[Create policies](https://entra.microsoft.com/#view/Microsoft_AAD_ConditionalAccess/ConditionalAccessBlade/~/Policies)" ` - -Risk 'High' -Name 'CA policies block access from noncompliant devices' ` - -UserImpact 'Medium' -ImplementationEffort 'Medium' ` - -Category 'Device security' + $TestParams = @{ + TestId = 'ZTNA24824' + TenantFilter = $Tenant + TestType = 'ZeroTrustNetworkAccess' + Status = 'Failed' + ResultMarkdown = "❌ **Fail**: No Conditional Access policies found that block access from noncompliant devices.`n`n[Create policies](https://entra.microsoft.com/#view/Microsoft_AAD_ConditionalAccess/ConditionalAccessBlade/~/Policies)" + Risk = 'High' + Name = 'CA policies block access from noncompliant devices' + UserImpact = 'Medium' + ImplementationEffort = 'Medium' + Category = 'Device security' + } + Add-CippTestResult @TestParams return } @@ -126,18 +142,34 @@ function Invoke-CippTestZTNA24824 { $ResultMarkdown += "`n[Review policies](https://entra.microsoft.com/#view/Microsoft_AAD_ConditionalAccess/ConditionalAccessBlade/~/Policies)" - Add-CippTestResult -TestId 'ZTNA24824' -TenantFilter $Tenant -TestType 'ZeroTrustNetworkAccess' -Status $Status ` - -ResultMarkdown $ResultMarkdown ` - -Risk 'High' -Name 'CA policies block access from noncompliant devices' ` - -UserImpact 'Medium' -ImplementationEffort 'Medium' ` - -Category 'Device security' + $TestParams = @{ + TestId = 'ZTNA24824' + TenantFilter = $Tenant + TestType = 'ZeroTrustNetworkAccess' + Status = $Status + ResultMarkdown = $ResultMarkdown + Risk = 'High' + Name = 'CA policies block access from noncompliant devices' + UserImpact = 'Medium' + ImplementationEffort = 'Medium' + Category = 'Device security' + } + Add-CippTestResult @TestParams } catch { - Add-CippTestResult -TestId 'ZTNA24824' -TenantFilter $Tenant -TestType 'ZeroTrustNetworkAccess' -Status 'Failed' ` - -ResultMarkdown "❌ **Error**: $($_.Exception.Message)" ` - -Risk 'High' -Name 'CA policies block access from noncompliant devices' ` - -UserImpact 'Medium' -ImplementationEffort 'Medium' ` - -Category 'Device security' + $TestParams = @{ + TestId = 'ZTNA24824' + TenantFilter = $Tenant + TestType = 'ZeroTrustNetworkAccess' + Status = 'Failed' + ResultMarkdown = "❌ **Error**: $($_.Exception.Message)" + Risk = 'High' + Name = 'CA policies block access from noncompliant devices' + UserImpact = 'Medium' + ImplementationEffort = 'Medium' + Category = 'Device security' + } + Add-CippTestResult @TestParams Write-LogMessage -API 'ZeroTrustNetworkAccess' -tenant $Tenant -message "Test ZTNA24824 failed: $($_.Exception.Message)" -sev Error } } diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24827.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24827.ps1 index 9c687c36de79..4e337c93eaff 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24827.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24827.ps1 @@ -21,11 +21,19 @@ function Invoke-CippTestZTNA24827 { $CAPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ConditionalAccessPolicies' if (-not $CAPolicies) { - Add-CippTestResult -TestId 'ZTNA24827' -TenantFilter $Tenant -TestType 'ZeroTrustNetworkAccess' -Status 'Skipped' ` - -ResultMarkdown 'Unable to retrieve Conditional Access policies from cache.' ` - -Risk 'Medium' -Name 'CA policies block unmanaged mobile apps' ` - -UserImpact 'Medium' -ImplementationEffort 'Medium' ` - -Category 'Application security' + $TestParams = @{ + TestId = 'ZTNA24827' + TenantFilter = $Tenant + TestType = 'ZeroTrustNetworkAccess' + Status = 'Skipped' + ResultMarkdown = 'Unable to retrieve Conditional Access policies from cache.' + Risk = 'Medium' + Name = 'CA policies block unmanaged mobile apps' + UserImpact = 'Medium' + ImplementationEffort = 'Medium' + Category = 'Application security' + } + Add-CippTestResult @TestParams return } @@ -57,11 +65,19 @@ function Invoke-CippTestZTNA24827 { } if ($CompliantAppPolicies.Count -eq 0) { - Add-CippTestResult -TestId 'ZTNA24827' -TenantFilter $Tenant -TestType 'ZeroTrustNetworkAccess' -Status 'Failed' ` - -ResultMarkdown "❌ **Fail**: No Conditional Access policies found that block unmanaged mobile apps.`n`n[Create policies](https://entra.microsoft.com/#view/Microsoft_AAD_ConditionalAccess/ConditionalAccessBlade/~/Policies)" ` - -Risk 'Medium' -Name 'CA policies block unmanaged mobile apps' ` - -UserImpact 'Medium' -ImplementationEffort 'Medium' ` - -Category 'Application security' + $TestParams = @{ + TestId = 'ZTNA24827' + TenantFilter = $Tenant + TestType = 'ZeroTrustNetworkAccess' + Status = 'Failed' + ResultMarkdown = "❌ **Fail**: No Conditional Access policies found that block unmanaged mobile apps.`n`n[Create policies](https://entra.microsoft.com/#view/Microsoft_AAD_ConditionalAccess/ConditionalAccessBlade/~/Policies)" + Risk = 'Medium' + Name = 'CA policies block unmanaged mobile apps' + UserImpact = 'Medium' + ImplementationEffort = 'Medium' + Category = 'Application security' + } + Add-CippTestResult @TestParams return } @@ -136,18 +152,34 @@ function Invoke-CippTestZTNA24827 { $ResultMarkdown += "`n[Review policies](https://entra.microsoft.com/#view/Microsoft_AAD_ConditionalAccess/ConditionalAccessBlade/~/Policies)" - Add-CippTestResult -TestId 'ZTNA24827' -TenantFilter $Tenant -TestType 'ZeroTrustNetworkAccess' -Status $Status ` - -ResultMarkdown $ResultMarkdown ` - -Risk 'Medium' -Name 'CA policies block unmanaged mobile apps' ` - -UserImpact 'Medium' -ImplementationEffort 'Medium' ` - -Category 'Application security' + $TestParams = @{ + TestId = 'ZTNA24827' + TenantFilter = $Tenant + TestType = 'ZeroTrustNetworkAccess' + Status = $Status + ResultMarkdown = $ResultMarkdown + Risk = 'Medium' + Name = 'CA policies block unmanaged mobile apps' + UserImpact = 'Medium' + ImplementationEffort = 'Medium' + Category = 'Application security' + } + Add-CippTestResult @TestParams } catch { - Add-CippTestResult -TestId 'ZTNA24827' -TenantFilter $Tenant -TestType 'ZeroTrustNetworkAccess' -Status 'Failed' ` - -ResultMarkdown "❌ **Error**: $($_.Exception.Message)" ` - -Risk 'Medium' -Name 'CA policies block unmanaged mobile apps' ` - -UserImpact 'Medium' -ImplementationEffort 'Medium' ` - -Category 'Application security' + $TestParams = @{ + TestId = 'ZTNA24827' + TenantFilter = $Tenant + TestType = 'ZeroTrustNetworkAccess' + Status = 'Failed' + ResultMarkdown = "❌ **Error**: $($_.Exception.Message)" + Risk = 'Medium' + Name = 'CA policies block unmanaged mobile apps' + UserImpact = 'Medium' + ImplementationEffort = 'Medium' + Category = 'Application security' + } + Add-CippTestResult @TestParams Write-LogMessage -API 'ZeroTrustNetworkAccess' -tenant $Tenant -message "Test ZTNA24827 failed: $($_.Exception.Message)" -sev Error } } From 4403e7b3d8d16136e7518adb606a3d1472811a36 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Fri, 26 Dec 2025 21:29:01 +0100 Subject: [PATCH 046/503] Updated tests after testing --- .../Public/Tests/Invoke-CippTestZTNA21896.ps1 | 5 +- .../Public/Tests/Invoke-CippTestZTNA21941.ps1 | 2 +- .../Public/Tests/Invoke-CippTestZTNA21953.ps1 | 55 ++++++------ .../Public/Tests/Invoke-CippTestZTNA21954.ps1 | 56 ++++++------- .../Public/Tests/Invoke-CippTestZTNA21955.ps1 | 56 ++++++------- .../Public/Tests/Invoke-CippTestZTNA21964.ps1 | 2 +- .../Public/Tests/Invoke-CippTestZTNA21992.ps1 | 2 +- .../Public/Tests/Invoke-CippTestZTNA22124.ps1 | 56 ++++++------- .../Public/Tests/Invoke-CippTestZTNA22128.ps1 | 13 ++- .../Public/Tests/Invoke-CippTestZTNA22659.ps1 | 58 ++++++------- .../Public/Tests/Invoke-CippTestZTNA24540.ps1 | 2 +- .../Public/Tests/Invoke-CippTestZTNA24541.ps1 | 6 +- .../Public/Tests/Invoke-CippTestZTNA24542.ps1 | 2 +- .../Public/Tests/Invoke-CippTestZTNA24543.ps1 | 1 + .../Public/Tests/Invoke-CippTestZTNA24545.ps1 | 1 + .../Public/Tests/Invoke-CippTestZTNA24547.ps1 | 1 + .../Public/Tests/Invoke-CippTestZTNA24548.ps1 | 1 + .../Public/Tests/Invoke-CippTestZTNA24549.ps1 | 1 + .../Public/Tests/Invoke-CippTestZTNA24550.ps1 | 24 +++--- .../Public/Tests/Invoke-CippTestZTNA24552.ps1 | 1 + .../Public/Tests/Invoke-CippTestZTNA24553.ps1 | 1 + .../Public/Tests/Invoke-CippTestZTNA24560.ps1 | 1 + .../Public/Tests/Invoke-CippTestZTNA24564.ps1 | 1 + .../Public/Tests/Invoke-CippTestZTNA24568.ps1 | 1 + .../Public/Tests/Invoke-CippTestZTNA24569.ps1 | 1 + .../Public/Tests/Invoke-CippTestZTNA24570.ps1 | 2 +- .../Public/Tests/Invoke-CippTestZTNA24572.ps1 | 8 +- .../Public/Tests/Invoke-CippTestZTNA24574.ps1 | 1 + .../Public/Tests/Invoke-CippTestZTNA24575.ps1 | 1 + .../Public/Tests/Invoke-CippTestZTNA24576.ps1 | 7 +- .../Public/Tests/Invoke-CippTestZTNA24784.ps1 | 1 + .../Public/Tests/Invoke-CippTestZTNA24824.ps1 | 84 +++++++++---------- .../Public/Tests/Invoke-CippTestZTNA24827.ps1 | 1 + .../Public/Tests/Invoke-CippTestZTNA24839.ps1 | 1 + .../Public/Tests/Invoke-CippTestZTNA24840.ps1 | 1 + .../Public/Tests/Invoke-CippTestZTNA24870.ps1 | 1 + 36 files changed, 237 insertions(+), 221 deletions(-) diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21896.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21896.ps1 index 938d25dec80d..425fc891dfb3 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21896.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21896.ps1 @@ -1,6 +1,6 @@ function Invoke-CippTestZTNA21896 { param($Tenant) - + #tested try { $ServicePrincipals = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ServicePrincipals' if (-not $ServicePrincipals) { @@ -47,8 +47,7 @@ function Invoke-CippTestZTNA21896 { $Result = $ResultLines -join "`n" Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21896' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'Service principals do not have certificates or credentials associated with them' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Application management' - } - catch { + } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21896' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'Service principals do not have certificates or credentials associated with them' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Application management' diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21941.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21941.ps1 index 2bcb40cfdb3d..879879680277 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21941.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21941.ps1 @@ -16,7 +16,7 @@ function Invoke-CippTestZTNA21941 { [Parameter(Mandatory = $true)] [string]$Tenant ) - + #Tested try { # Get CA policies from cache $CAPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ConditionalAccessPolicies' diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21953.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21953.ps1 index 0637aa913aaa..49d3af03ca3e 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21953.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21953.ps1 @@ -15,6 +15,7 @@ function Invoke-CippTestZTNA21953 { [Parameter(Mandatory = $true)] [string]$Tenant ) + #Tested try { # Get device registration policy from cache @@ -22,16 +23,16 @@ function Invoke-CippTestZTNA21953 { if (-not $DeviceRegPolicy) { $TestParams = @{ - TestId = 'ZTNA21953' - TenantFilter = $Tenant - TestType = 'ZeroTrustNetworkAccess' - Status = 'Skipped' - ResultMarkdown = 'Unable to retrieve device registration policy from cache.' - Risk = 'High' - Name = 'Deploy Windows Local Administrator Password Solution (LAPS)' - UserImpact = 'Low' + TestId = 'ZTNA21953' + TenantFilter = $Tenant + TestType = 'ZeroTrustNetworkAccess' + Status = 'Skipped' + ResultMarkdown = 'Unable to retrieve device registration policy from cache.' + Risk = 'High' + Name = 'Deploy Windows Local Administrator Password Solution (LAPS)' + UserImpact = 'Low' ImplementationEffort = 'Low' - Category = 'Device security' + Category = 'Device security' } Add-CippTestResult @TestParams return @@ -51,31 +52,31 @@ function Invoke-CippTestZTNA21953 { } $TestParams = @{ - TestId = 'ZTNA21953' - TenantFilter = $Tenant - TestType = 'ZeroTrustNetworkAccess' - Status = $Status - ResultMarkdown = $ResultMarkdown - Risk = 'High' - Name = 'Deploy Windows Local Administrator Password Solution (LAPS)' - UserImpact = 'Low' + TestId = 'ZTNA21953' + TenantFilter = $Tenant + TestType = 'ZeroTrustNetworkAccess' + Status = $Status + ResultMarkdown = $ResultMarkdown + Risk = 'High' + Name = 'Deploy Windows Local Administrator Password Solution (LAPS)' + UserImpact = 'Low' ImplementationEffort = 'Low' - Category = 'Device security' + Category = 'Device security' } Add-CippTestResult @TestParams } catch { $TestParams = @{ - TestId = 'ZTNA21953' - TenantFilter = $Tenant - TestType = 'ZeroTrustNetworkAccess' - Status = 'Failed' - ResultMarkdown = "❌ **Error**: $($_.Exception.Message)" - Risk = 'High' - Name = 'Deploy Windows Local Administrator Password Solution (LAPS)' - UserImpact = 'Low' + TestId = 'ZTNA21953' + TenantFilter = $Tenant + TestType = 'ZeroTrustNetworkAccess' + Status = 'Failed' + ResultMarkdown = "❌ **Error**: $($_.Exception.Message)" + Risk = 'High' + Name = 'Deploy Windows Local Administrator Password Solution (LAPS)' + UserImpact = 'Low' ImplementationEffort = 'Low' - Category = 'Device security' + Category = 'Device security' } Add-CippTestResult @TestParams Write-LogMessage -API 'ZeroTrustNetworkAccess' -tenant $Tenant -message "Test ZTNA21953 failed: $($_.Exception.Message)" -sev Error diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21954.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21954.ps1 index b534ad7d36ce..f74c4b538b44 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21954.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21954.ps1 @@ -15,23 +15,23 @@ function Invoke-CippTestZTNA21954 { [Parameter(Mandatory = $true)] [string]$Tenant ) - + #Tested try { # Get authorization policy from cache $AuthPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthorizationPolicy' if (-not $AuthPolicy) { $TestParams = @{ - TestId = 'ZTNA21954' - TenantFilter = $Tenant - TestType = 'ZeroTrustNetworkAccess' - Status = 'Skipped' - ResultMarkdown = 'Unable to retrieve authorization policy from cache.' - Risk = 'Low' - Name = 'Restrict non-admin users from reading BitLocker recovery keys' - UserImpact = 'Low' + TestId = 'ZTNA21954' + TenantFilter = $Tenant + TestType = 'ZeroTrustNetworkAccess' + Status = 'Skipped' + ResultMarkdown = 'Unable to retrieve authorization policy from cache.' + Risk = 'Low' + Name = 'Restrict non-admin users from reading BitLocker recovery keys' + UserImpact = 'Low' ImplementationEffort = 'Low' - Category = 'Device security' + Category = 'Device security' } Add-CippTestResult @TestParams return @@ -51,31 +51,31 @@ function Invoke-CippTestZTNA21954 { } $TestParams = @{ - TestId = 'ZTNA21954' - TenantFilter = $Tenant - TestType = 'ZeroTrustNetworkAccess' - Status = $Status - ResultMarkdown = $ResultMarkdown - Risk = 'Low' - Name = 'Restrict non-admin users from reading BitLocker recovery keys' - UserImpact = 'Low' + TestId = 'ZTNA21954' + TenantFilter = $Tenant + TestType = 'ZeroTrustNetworkAccess' + Status = $Status + ResultMarkdown = $ResultMarkdown + Risk = 'Low' + Name = 'Restrict non-admin users from reading BitLocker recovery keys' + UserImpact = 'Low' ImplementationEffort = 'Low' - Category = 'Device security' + Category = 'Device security' } Add-CippTestResult @TestParams } catch { $TestParams = @{ - TestId = 'ZTNA21954' - TenantFilter = $Tenant - TestType = 'ZeroTrustNetworkAccess' - Status = 'Failed' - ResultMarkdown = "❌ **Error**: $($_.Exception.Message)" - Risk = 'Low' - Name = 'Restrict non-admin users from reading BitLocker recovery keys' - UserImpact = 'Low' + TestId = 'ZTNA21954' + TenantFilter = $Tenant + TestType = 'ZeroTrustNetworkAccess' + Status = 'Failed' + ResultMarkdown = "❌ **Error**: $($_.Exception.Message)" + Risk = 'Low' + Name = 'Restrict non-admin users from reading BitLocker recovery keys' + UserImpact = 'Low' ImplementationEffort = 'Low' - Category = 'Device security' + Category = 'Device security' } Add-CippTestResult @TestParams Write-LogMessage -API 'ZeroTrustNetworkAccess' -tenant $Tenant -message "Test ZTNA21954 failed: $($_.Exception.Message)" -sev Error diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21955.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21955.ps1 index ed597965ffbf..dc63407c4576 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21955.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21955.ps1 @@ -15,23 +15,23 @@ function Invoke-CippTestZTNA21955 { [Parameter(Mandatory = $true)] [string]$Tenant ) - + #Tested try { # Get device registration policy from cache $DeviceRegPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'DeviceRegistrationPolicy' if (-not $DeviceRegPolicy) { $TestParams = @{ - TestId = 'ZTNA21955' - TenantFilter = $Tenant - TestType = 'ZeroTrustNetworkAccess' - Status = 'Skipped' - ResultMarkdown = 'Unable to retrieve device registration policy from cache.' - Risk = 'Medium' - Name = 'Manage local admins on Entra joined devices' - UserImpact = 'Low' + TestId = 'ZTNA21955' + TenantFilter = $Tenant + TestType = 'ZeroTrustNetworkAccess' + Status = 'Skipped' + ResultMarkdown = 'Unable to retrieve device registration policy from cache.' + Risk = 'Medium' + Name = 'Manage local admins on Entra joined devices' + UserImpact = 'Low' ImplementationEffort = 'Low' - Category = 'Device security' + Category = 'Device security' } Add-CippTestResult @TestParams return @@ -51,31 +51,31 @@ function Invoke-CippTestZTNA21955 { } $TestParams = @{ - TestId = 'ZTNA21955' - TenantFilter = $Tenant - TestType = 'ZeroTrustNetworkAccess' - Status = $Status - ResultMarkdown = $ResultMarkdown - Risk = 'Medium' - Name = 'Manage local admins on Entra joined devices' - UserImpact = 'Low' + TestId = 'ZTNA21955' + TenantFilter = $Tenant + TestType = 'ZeroTrustNetworkAccess' + Status = $Status + ResultMarkdown = $ResultMarkdown + Risk = 'Medium' + Name = 'Manage local admins on Entra joined devices' + UserImpact = 'Low' ImplementationEffort = 'Low' - Category = 'Device security' + Category = 'Device security' } Add-CippTestResult @TestParams } catch { $TestParams = @{ - TestId = 'ZTNA21955' - TenantFilter = $Tenant - TestType = 'ZeroTrustNetworkAccess' - Status = 'Failed' - ResultMarkdown = "❌ **Error**: $($_.Exception.Message)" - Risk = 'Medium' - Name = 'Manage local admins on Entra joined devices' - UserImpact = 'Low' + TestId = 'ZTNA21955' + TenantFilter = $Tenant + TestType = 'ZeroTrustNetworkAccess' + Status = 'Failed' + ResultMarkdown = "❌ **Error**: $($_.Exception.Message)" + Risk = 'Medium' + Name = 'Manage local admins on Entra joined devices' + UserImpact = 'Low' ImplementationEffort = 'Low' - Category = 'Device security' + Category = 'Device security' } Add-CippTestResult @TestParams Write-LogMessage -API 'ZeroTrustNetworkAccess' -tenant $Tenant -message "Test ZTNA21955 failed: $($_.Exception.Message)" -sev Error diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21964.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21964.ps1 index b39f7bf8347f..2d281cb6cbed 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21964.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21964.ps1 @@ -2,7 +2,7 @@ function Invoke-CippTestZTNA21964 { param($Tenant) $TestId = 'ZTNA21964' - + #Tested try { $AuthStrengths = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationStrengths' diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21992.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21992.ps1 index e3a91af31ef7..ee4a277727bc 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21992.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21992.ps1 @@ -4,7 +4,7 @@ function Invoke-CippTestZTNA21992 { try { $Apps = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Apps' $ServicePrincipals = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ServicePrincipals' - + #Tested if (-not $Apps -and -not $ServicePrincipals) { Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21992' -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Application and service principal data not found in database' -Risk 'High' -Name 'Application certificates must be rotated on a regular basis' -UserImpact 'Low' -ImplementationEffort 'High' -Category 'Application management' return diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA22124.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA22124.ps1 index 6619396c8bd7..defe5a2a1b8e 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA22124.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA22124.ps1 @@ -15,23 +15,23 @@ function Invoke-CippTestZTNA22124 { [Parameter(Mandatory = $true)] [string]$Tenant ) - + #Tested try { # Get directory recommendations from cache $Recommendations = New-CIPPDbRequest -TenantFilter $Tenant -Type 'DirectoryRecommendations' if (-not $Recommendations) { $TestParams = @{ - TestId = 'ZTNA22124' - TenantFilter = $Tenant - TestType = 'ZeroTrustNetworkAccess' - Status = 'Skipped' - ResultMarkdown = 'Unable to retrieve directory recommendations from cache.' - Risk = 'High' - Name = 'Address high priority Entra recommendations' - UserImpact = 'Medium' + TestId = 'ZTNA22124' + TenantFilter = $Tenant + TestType = 'ZeroTrustNetworkAccess' + Status = 'Skipped' + ResultMarkdown = 'Unable to retrieve directory recommendations from cache.' + Risk = 'High' + Name = 'Address high priority Entra recommendations' + UserImpact = 'Medium' ImplementationEffort = 'Medium' - Category = 'Governance' + Category = 'Governance' } Add-CippTestResult @TestParams return @@ -67,31 +67,31 @@ function Invoke-CippTestZTNA22124 { } $TestParams = @{ - TestId = 'ZTNA22124' - TenantFilter = $Tenant - TestType = 'ZeroTrustNetworkAccess' - Status = $Status - ResultMarkdown = $ResultMarkdown - Risk = 'High' - Name = 'Address high priority Entra recommendations' - UserImpact = 'Medium' + TestId = 'ZTNA22124' + TenantFilter = $Tenant + TestType = 'ZeroTrustNetworkAccess' + Status = $Status + ResultMarkdown = $ResultMarkdown + Risk = 'High' + Name = 'Address high priority Entra recommendations' + UserImpact = 'Medium' ImplementationEffort = 'Medium' - Category = 'Governance' + Category = 'Governance' } Add-CippTestResult @TestParams } catch { $TestParams = @{ - TestId = 'ZTNA22124' - TenantFilter = $Tenant - TestType = 'ZeroTrustNetworkAccess' - Status = 'Failed' - ResultMarkdown = "❌ **Error**: $($_.Exception.Message)" - Risk = 'High' - Name = 'Address high priority Entra recommendations' - UserImpact = 'Medium' + TestId = 'ZTNA22124' + TenantFilter = $Tenant + TestType = 'ZeroTrustNetworkAccess' + Status = 'Failed' + ResultMarkdown = "❌ **Error**: $($_.Exception.Message)" + Risk = 'High' + Name = 'Address high priority Entra recommendations' + UserImpact = 'Medium' ImplementationEffort = 'Medium' - Category = 'Governance' + Category = 'Governance' } Add-CippTestResult @TestParams Write-LogMessage -API 'ZeroTrustNetworkAccess' -tenant $Tenant -message "Test ZTNA22124 failed: $($_.Exception.Message)" -sev Error diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA22128.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA22128.ps1 index 3c51b79052d4..6196ed7aed3e 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA22128.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA22128.ps1 @@ -1,6 +1,6 @@ function Invoke-CippTestZTNA22128 { param($Tenant) - + #Tested try { $Roles = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Roles' $Guests = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Guests' @@ -38,9 +38,9 @@ function Invoke-CippTestZTNA22128 { foreach ($Member in $Role.members) { if ($GuestIdHash.ContainsKey($Member.id)) { $GuestsInPrivilegedRoles += [PSCustomObject]@{ - RoleName = $Role.displayName - GuestId = $Member.id - GuestDisplayName = $Member.displayName + RoleName = $Role.displayName + GuestId = $Member.id + GuestDisplayName = $Member.displayName GuestUserPrincipalName = $Member.userPrincipalName } } @@ -66,7 +66,7 @@ function Invoke-CippTestZTNA22128 { $RoleGroups = $GuestsInPrivilegedRoles | Group-Object -Property RoleName foreach ($RoleGroup in $RoleGroups) { - $ResultLines += "" + $ResultLines += '' $ResultLines += "**$($RoleGroup.Name)** ($($RoleGroup.Count) guest(s)):" foreach ($Guest in $RoleGroup.Group) { $ResultLines += "- $($Guest.GuestDisplayName) ($($Guest.GuestUserPrincipalName))" @@ -79,8 +79,7 @@ function Invoke-CippTestZTNA22128 { $Result = $ResultLines -join "`n" Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA22128' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'Guests are not assigned high privileged directory roles' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Application management' - } - catch { + } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA22128' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Guests are not assigned high privileged directory roles' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Application management' diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA22659.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA22659.ps1 index a0cde338b635..fa3636424a16 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA22659.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA22659.ps1 @@ -15,23 +15,23 @@ function Invoke-CippTestZTNA22659 { [Parameter(Mandatory = $true)] [string]$Tenant ) - + #Tested try { # Get service principal risk detections from cache $RiskDetections = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ServicePrincipalRiskDetections' if (-not $RiskDetections) { $TestParams = @{ - TestId = 'ZTNA22659' - TenantFilter = $Tenant - TestType = 'ZeroTrustNetworkAccess' - Status = 'Skipped' - ResultMarkdown = 'Unable to retrieve service principal risk detections from cache.' - Risk = 'High' - Name = 'Triage risky workload identity sign-ins' - UserImpact = 'High' + TestId = 'ZTNA22659' + TenantFilter = $Tenant + TestType = 'ZeroTrustNetworkAccess' + Status = 'Skipped' + ResultMarkdown = 'Unable to retrieve service principal risk detections from cache.' + Risk = 'High' + Name = 'Triage risky workload identity sign-ins' + UserImpact = 'High' ImplementationEffort = 'Low' - Category = 'Identity protection' + Category = 'Identity protection' } Add-CippTestResult @TestParams return @@ -49,7 +49,7 @@ function Invoke-CippTestZTNA22659 { if ($Status -eq 'Passed') { $ResultMarkdown = "✅ **Pass**: No risky workload identity sign-ins detected or all have been triaged.`n`n" - $ResultMarkdown += "[View identity protection](https://entra.microsoft.com/#view/Microsoft_AAD_IAM/IdentityProtectionMenuBlade/~/RiskyServicePrincipals)" + $ResultMarkdown += '[View identity protection](https://entra.microsoft.com/#view/Microsoft_AAD_IAM/IdentityProtectionMenuBlade/~/RiskyServicePrincipals)' } else { $ResultMarkdown = "❌ **Fail**: There are $($RiskySignIns.Count) risky workload identity sign-in(s) that require investigation.`n`n" $ResultMarkdown += "## Risky service principal sign-ins`n`n" @@ -80,31 +80,31 @@ function Invoke-CippTestZTNA22659 { } $TestParams = @{ - TestId = 'ZTNA22659' - TenantFilter = $Tenant - TestType = 'ZeroTrustNetworkAccess' - Status = $Status - ResultMarkdown = $ResultMarkdown - Risk = 'High' - Name = 'Triage risky workload identity sign-ins' - UserImpact = 'High' + TestId = 'ZTNA22659' + TenantFilter = $Tenant + TestType = 'ZeroTrustNetworkAccess' + Status = $Status + ResultMarkdown = $ResultMarkdown + Risk = 'High' + Name = 'Triage risky workload identity sign-ins' + UserImpact = 'High' ImplementationEffort = 'Low' - Category = 'Identity protection' + Category = 'Identity protection' } Add-CippTestResult @TestParams } catch { $TestParams = @{ - TestId = 'ZTNA22659' - TenantFilter = $Tenant - TestType = 'ZeroTrustNetworkAccess' - Status = 'Failed' - ResultMarkdown = "❌ **Error**: $($_.Exception.Message)" - Risk = 'High' - Name = 'Triage risky workload identity sign-ins' - UserImpact = 'High' + TestId = 'ZTNA22659' + TenantFilter = $Tenant + TestType = 'ZeroTrustNetworkAccess' + Status = 'Failed' + ResultMarkdown = "❌ **Error**: $($_.Exception.Message)" + Risk = 'High' + Name = 'Triage risky workload identity sign-ins' + UserImpact = 'High' ImplementationEffort = 'Low' - Category = 'Identity protection' + Category = 'Identity protection' } Add-CippTestResult @TestParams Write-LogMessage -API 'ZeroTrustNetworkAccess' -tenant $Tenant -message "Test ZTNA22659 failed: $($_.Exception.Message)" -sev Error diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24540.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24540.ps1 index 8d54f88181fb..5d2b7956a77f 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24540.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24540.ps1 @@ -1,6 +1,6 @@ function Invoke-CippTestZTNA24540 { param($Tenant) - + #Tested - Device try { $ConfigurationPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'IntuneConfigurationPolicies' if (-not $ConfigurationPolicies) { diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24541.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24541.ps1 index 876008a12be8..76861fe6d16f 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24541.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24541.ps1 @@ -2,7 +2,7 @@ function Invoke-CippTestZTNA24541 { param($Tenant) $TestId = 'ZTNA24541' - + #Tested - Device try { $IntunePolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'IntuneDeviceCompliancePolicies' @@ -12,8 +12,8 @@ function Invoke-CippTestZTNA24541 { } $WindowsPolicies = @($IntunePolicies | Where-Object { - $_.'@odata.type' -in @('#microsoft.graph.windows10CompliancePolicy', '#microsoft.graph.windows11CompliancePolicy') - }) + $_.'@odata.type' -in @('#microsoft.graph.windows10CompliancePolicy', '#microsoft.graph.windows11CompliancePolicy') + }) $AssignedPolicies = @($WindowsPolicies | Where-Object { $_.assignments -and $_.assignments.Count -gt 0 }) $Passed = $AssignedPolicies.Count -gt 0 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24542.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24542.ps1 index d1054428b70c..411b52e4d51e 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24542.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24542.ps1 @@ -2,7 +2,7 @@ function Invoke-CippTestZTNA24542 { param($Tenant) $TestId = 'ZTNA24542' - + #Tested - Device try { $IntunePolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'IntuneDeviceCompliancePolicies' diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24543.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24543.ps1 index 58f551b9575c..65060decff98 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24543.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24543.ps1 @@ -2,6 +2,7 @@ function Invoke-CippTestZTNA24543 { param($Tenant) $TestId = 'ZTNA24543' + #Tested - Device try { $IntunePolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'IntuneDeviceCompliancePolicies' diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24545.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24545.ps1 index b1aa37072217..14365a3f5022 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24545.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24545.ps1 @@ -2,6 +2,7 @@ function Invoke-CippTestZTNA24545 { param($Tenant) $TestId = 'ZTNA24545' + #Tested - Device try { $IntunePolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'IntuneDeviceCompliancePolicies' diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24547.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24547.ps1 index 03e15fc30e94..8dc4eb319501 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24547.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24547.ps1 @@ -2,6 +2,7 @@ function Invoke-CippTestZTNA24547 { param($Tenant) $TestId = 'ZTNA24547' + #Tested - Device try { $IntunePolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'IntuneDeviceCompliancePolicies' diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24548.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24548.ps1 index d9dc3308a70e..14914f41fc1e 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24548.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24548.ps1 @@ -2,6 +2,7 @@ function Invoke-CippTestZTNA24548 { param($Tenant) $TestId = 'ZTNA24548' + #Tested - Device try { $IosPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'IntuneIosAppProtectionPolicies' diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24549.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24549.ps1 index 40259bab2f67..9ad5be48f319 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24549.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24549.ps1 @@ -2,6 +2,7 @@ function Invoke-CippTestZTNA24549 { param($Tenant) $TestId = 'ZTNA24549' + #Tested - Device try { $AndroidPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'IntuneAndroidAppProtectionPolicies' diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24550.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24550.ps1 index a70270bb62cd..a66111d9891a 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24550.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24550.ps1 @@ -1,5 +1,6 @@ function Invoke-CippTestZTNA24550 { param($Tenant) + #Tested - Device try { $ConfigurationPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'IntuneConfigurationPolicies' @@ -43,12 +44,12 @@ function Invoke-CippTestZTNA24550 { if ($AssignedPolicies.Count -gt 0) { $Status = 'Passed' $ResultLines = @( - "At least one Windows BitLocker policy is configured and assigned." + 'At least one Windows BitLocker policy is configured and assigned.' '' - "**Windows BitLocker Policies:**" + '**Windows BitLocker Policies:**' '' - "| Policy Name | Status | Assignment Count |" - "| :---------- | :----- | :--------------- |" + '| Policy Name | Status | Assignment Count |' + '| :---------- | :----- | :--------------- |' ) foreach ($Policy in $WindowsBitLockerPolicies) { @@ -62,29 +63,26 @@ function Invoke-CippTestZTNA24550 { } $Result = $ResultLines -join "`n" - } - else { + } else { $Status = 'Failed' if ($WindowsBitLockerPolicies.Count -gt 0) { $ResultLines = @( - "Windows BitLocker policies exist but none are assigned." + 'Windows BitLocker policies exist but none are assigned.' '' - "**Unassigned BitLocker Policies:**" + '**Unassigned BitLocker Policies:**' '' ) foreach ($Policy in $WindowsBitLockerPolicies) { $ResultLines += "- $($Policy.name)" } - } - else { - $ResultLines = @("No Windows BitLocker policy is configured or assigned.") + } else { + $ResultLines = @('No Windows BitLocker policy is configured or assigned.') } $Result = $ResultLines -join "`n" } Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA24550' -TestType 'Devices' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'Data on Windows is protected by BitLocker encryption' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Device' - } - catch { + } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA24550' -TestType 'Devices' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Data on Windows is protected by BitLocker encryption' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Device' diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24552.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24552.ps1 index d21dfd9c54c4..e9868bbdc9da 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24552.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24552.ps1 @@ -1,5 +1,6 @@ function Invoke-CippTestZTNA24552 { param($Tenant) + #Tested - Device try { $ConfigurationPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'IntuneConfigurationPolicies' diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24553.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24553.ps1 index 3c68b99f4f5b..3f0360726ed1 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24553.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24553.ps1 @@ -1,5 +1,6 @@ function Invoke-CippTestZTNA24553 { param($Tenant) + #Tested - Device $TestId = 'ZTNA24553' diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24560.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24560.ps1 index d35e388c1ea0..08a89d64350c 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24560.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24560.ps1 @@ -1,5 +1,6 @@ function Invoke-CippTestZTNA24560 { param($Tenant) + #Tested - Device try { $ConfigPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'IntuneConfigurationPolicies' diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24564.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24564.ps1 index fcd0167a7c89..5e3e4d3b7fea 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24564.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24564.ps1 @@ -1,5 +1,6 @@ function Invoke-CippTestZTNA24564 { param($Tenant) + #Tested - Device try { $ConfigPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'IntuneConfigurationPolicies' diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24568.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24568.ps1 index 5f7743f65dfa..1b857bf9af62 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24568.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24568.ps1 @@ -1,5 +1,6 @@ function Invoke-CippTestZTNA24568 { param($Tenant) + #Tested - Device try { $ConfigPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'IntuneConfigurationPolicies' diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24569.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24569.ps1 index dc81c626bddb..30a21d9242d4 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24569.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24569.ps1 @@ -2,6 +2,7 @@ function Invoke-CippTestZTNA24569 { param($Tenant) $TestId = 'ZTNA24569' + #Tested - Device try { $DeviceConfigs = New-CIPPDbRequest -TenantFilter $Tenant -Type 'IntuneDeviceConfigurations' diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24570.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24570.ps1 index 3a5ca98e782f..08114f3e503f 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24570.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24570.ps1 @@ -16,7 +16,7 @@ function Invoke-CippTestZTNA24570 { [Parameter(Mandatory = $true)] [string]$Tenant ) - + #Tested try { # Get organization info to check if hybrid identity is enabled $OrgInfo = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Organization' diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24572.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24572.ps1 index 2c60a39e18f8..56b2b9906004 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24572.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24572.ps1 @@ -2,7 +2,7 @@ function Invoke-CippTestZTNA24572 { param($Tenant) $TestId = 'ZTNA24572' - + #Tested try { $EnrollmentConfigs = New-CIPPDbRequest -TenantFilter $Tenant -Type 'IntuneDeviceEnrollmentConfigurations' @@ -12,9 +12,9 @@ function Invoke-CippTestZTNA24572 { } $EnrollmentNotifications = @($EnrollmentConfigs | Where-Object { - $_.'@odata.type' -eq '#microsoft.graph.windowsEnrollmentStatusScreenSettings' -or - $_.'deviceEnrollmentConfigurationType' -eq 'EnrollmentNotificationsConfiguration' - }) + $_.'@odata.type' -eq '#microsoft.graph.windowsEnrollmentStatusScreenSettings' -or + $_.'deviceEnrollmentConfigurationType' -eq 'EnrollmentNotificationsConfiguration' + }) $AssignedNotifications = @($EnrollmentNotifications | Where-Object { $_.assignments -and $_.assignments.Count -gt 0 }) $Passed = $AssignedNotifications.Count -gt 0 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24574.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24574.ps1 index 2fc7952d4ac6..6b73c76e52e9 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24574.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24574.ps1 @@ -1,5 +1,6 @@ function Invoke-CippTestZTNA24574 { param($Tenant) + #Tested - Device try { $ConfigPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'IntuneConfigurationPolicies' diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24575.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24575.ps1 index 33da19cdc512..a4ba59b4d283 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24575.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24575.ps1 @@ -1,5 +1,6 @@ function Invoke-CippTestZTNA24575 { param($Tenant) + #Tested - Device try { $ConfigPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'IntuneConfigurationPolicies' diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24576.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24576.ps1 index 09a19afc3d2b..a735a0339441 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24576.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24576.ps1 @@ -2,6 +2,7 @@ function Invoke-CippTestZTNA24576 { param($Tenant) $TestId = 'ZTNA24576' + #Tested - Device try { $DeviceConfigs = New-CIPPDbRequest -TenantFilter $Tenant -Type 'IntuneDeviceConfigurations' @@ -11,10 +12,10 @@ function Invoke-CippTestZTNA24576 { return } - $WindowsHealthMonitoringPolicies = @($DeviceConfigs | Where-Object { - $_.'@odata.type' -eq '#microsoft.graph.windowsHealthMonitoringConfiguration' + $WindowsHealthMonitoringPolicies = @($DeviceConfigs | Where-Object { + $_.'@odata.type' -eq '#microsoft.graph.windowsHealthMonitoringConfiguration' }) - + $AssignedPolicies = @($WindowsHealthMonitoringPolicies | Where-Object { $_.assignments -and $_.assignments.Count -gt 0 }) $Passed = $AssignedPolicies.Count -gt 0 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24784.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24784.ps1 index 8c94ae6a0a26..143be08e164c 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24784.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24784.ps1 @@ -1,5 +1,6 @@ function Invoke-CippTestZTNA24784 { param($Tenant) + #Tested - Device try { $ConfigPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'IntuneConfigurationPolicies' diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24824.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24824.ps1 index fe0d11172ed7..9dc68cd0c59f 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24824.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24824.ps1 @@ -15,23 +15,23 @@ function Invoke-CippTestZTNA24824 { [Parameter(Mandatory = $true)] [string]$Tenant ) - + #Tested try { # Get CA policies from cache $CAPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ConditionalAccessPolicies' if (-not $CAPolicies) { $TestParams = @{ - TestId = 'ZTNA24824' - TenantFilter = $Tenant - TestType = 'ZeroTrustNetworkAccess' - Status = 'Skipped' - ResultMarkdown = 'Unable to retrieve Conditional Access policies from cache.' - Risk = 'High' - Name = 'CA policies block access from noncompliant devices' - UserImpact = 'Medium' + TestId = 'ZTNA24824' + TenantFilter = $Tenant + TestType = 'ZeroTrustNetworkAccess' + Status = 'Skipped' + ResultMarkdown = 'Unable to retrieve Conditional Access policies from cache.' + Risk = 'High' + Name = 'CA policies block access from noncompliant devices' + UserImpact = 'Medium' ImplementationEffort = 'Medium' - Category = 'Device security' + Category = 'Device security' } Add-CippTestResult @TestParams return @@ -50,16 +50,16 @@ function Invoke-CippTestZTNA24824 { if ($CompliantDevicePolicies.Count -eq 0) { $TestParams = @{ - TestId = 'ZTNA24824' - TenantFilter = $Tenant - TestType = 'ZeroTrustNetworkAccess' - Status = 'Failed' - ResultMarkdown = "❌ **Fail**: No Conditional Access policies found that block access from noncompliant devices.`n`n[Create policies](https://entra.microsoft.com/#view/Microsoft_AAD_ConditionalAccess/ConditionalAccessBlade/~/Policies)" - Risk = 'High' - Name = 'CA policies block access from noncompliant devices' - UserImpact = 'Medium' + TestId = 'ZTNA24824' + TenantFilter = $Tenant + TestType = 'ZeroTrustNetworkAccess' + Status = 'Failed' + ResultMarkdown = "❌ **Fail**: No Conditional Access policies found that block access from noncompliant devices.`n`n[Create policies](https://entra.microsoft.com/#view/Microsoft_AAD_ConditionalAccess/ConditionalAccessBlade/~/Policies)" + Risk = 'High' + Name = 'CA policies block access from noncompliant devices' + UserImpact = 'Medium' ImplementationEffort = 'Medium' - Category = 'Device security' + Category = 'Device security' } Add-CippTestResult @TestParams return @@ -68,8 +68,8 @@ function Invoke-CippTestZTNA24824 { # Track platform coverage $PlatformCoverage = @{ 'windows' = $false - 'macOS' = $false - 'iOS' = $false + 'macOS' = $false + 'iOS' = $false 'android' = $false } $AllPlatformsPolicy = $false @@ -101,9 +101,9 @@ function Invoke-CippTestZTNA24824 { } $PolicyDetails.Add([PSCustomObject]@{ - Name = $policy.displayName - Platforms = $platforms - }) + Name = $policy.displayName + Platforms = $platforms + }) } # Check if all platforms are covered (either by a single policy or combination) @@ -143,31 +143,31 @@ function Invoke-CippTestZTNA24824 { $ResultMarkdown += "`n[Review policies](https://entra.microsoft.com/#view/Microsoft_AAD_ConditionalAccess/ConditionalAccessBlade/~/Policies)" $TestParams = @{ - TestId = 'ZTNA24824' - TenantFilter = $Tenant - TestType = 'ZeroTrustNetworkAccess' - Status = $Status - ResultMarkdown = $ResultMarkdown - Risk = 'High' - Name = 'CA policies block access from noncompliant devices' - UserImpact = 'Medium' + TestId = 'ZTNA24824' + TenantFilter = $Tenant + TestType = 'ZeroTrustNetworkAccess' + Status = $Status + ResultMarkdown = $ResultMarkdown + Risk = 'High' + Name = 'CA policies block access from noncompliant devices' + UserImpact = 'Medium' ImplementationEffort = 'Medium' - Category = 'Device security' + Category = 'Device security' } Add-CippTestResult @TestParams } catch { $TestParams = @{ - TestId = 'ZTNA24824' - TenantFilter = $Tenant - TestType = 'ZeroTrustNetworkAccess' - Status = 'Failed' - ResultMarkdown = "❌ **Error**: $($_.Exception.Message)" - Risk = 'High' - Name = 'CA policies block access from noncompliant devices' - UserImpact = 'Medium' + TestId = 'ZTNA24824' + TenantFilter = $Tenant + TestType = 'ZeroTrustNetworkAccess' + Status = 'Failed' + ResultMarkdown = "❌ **Error**: $($_.Exception.Message)" + Risk = 'High' + Name = 'CA policies block access from noncompliant devices' + UserImpact = 'Medium' ImplementationEffort = 'Medium' - Category = 'Device security' + Category = 'Device security' } Add-CippTestResult @TestParams Write-LogMessage -API 'ZeroTrustNetworkAccess' -tenant $Tenant -message "Test ZTNA24824 failed: $($_.Exception.Message)" -sev Error diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24827.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24827.ps1 index 4e337c93eaff..70dff3d7135a 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24827.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24827.ps1 @@ -15,6 +15,7 @@ function Invoke-CippTestZTNA24827 { [Parameter(Mandatory = $true)] [string]$Tenant ) + #Tested - Device try { # Get CA policies from cache diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24839.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24839.ps1 index 40c68b2e5da6..aa4c3c2d8d30 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24839.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24839.ps1 @@ -1,5 +1,6 @@ function Invoke-CippTestZTNA24839 { param($Tenant) + #Tested - Device $TestId = 'ZTNA24839' diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24840.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24840.ps1 index 72f756a3adda..37faf8fce948 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24840.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24840.ps1 @@ -2,6 +2,7 @@ function Invoke-CippTestZTNA24840 { param($Tenant) $TestId = 'ZTNA24840' + #Tested - Device try { $DeviceConfigs = New-CIPPDbRequest -TenantFilter $Tenant -Type 'IntuneDeviceConfigurations' diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24870.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24870.ps1 index 3035266ab448..88d99dfd3c26 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24870.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24870.ps1 @@ -2,6 +2,7 @@ function Invoke-CippTestZTNA24870 { param($Tenant) $TestId = 'ZTNA24870' + #Tested - Device try { $DeviceConfigs = New-CIPPDbRequest -TenantFilter $Tenant -Type 'IntuneDeviceConfigurations' From 9660318064eb20af1fdca292d287d145a3254a29 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Fri, 26 Dec 2025 23:55:21 +0100 Subject: [PATCH 047/503] bug fixes for tests --- .../Push-CIPPDBCacheData.ps1 | 117 +++++++++++------- .../Public/Tests/Invoke-CippTestZTNA21837.ps1 | 2 +- Test-AllZTNATests.ps1 | 9 +- test-alignment-profile.ps1 | 30 ----- 4 files changed, 81 insertions(+), 77 deletions(-) delete mode 100644 test-alignment-profile.ps1 diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 index 9dad231062c0..4e34cea5ec06 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 @@ -18,6 +18,14 @@ function Push-CIPPDBCacheData { try { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Starting database cache collection for tenant' -sev Info + # Check tenant capabilities for license-specific features + $IntuneCapable = Test-CIPPStandardLicense -StandardName 'IntuneLicenseCheck' -TenantFilter $TenantFilter -RequiredCapabilities @('INTUNE_A', 'MDM_Services', 'EMS', 'SCCM', 'MICROSOFTINTUNEPLAN1') -SkipLog + $ConditionalAccessCapable = Test-CIPPStandardLicense -StandardName 'ConditionalAccessLicenseCheck' -TenantFilter $TenantFilter -RequiredCapabilities @('AAD_PREMIUM', 'AAD_PREMIUM_P2') -SkipLog + $AzureADPremiumP2Capable = Test-CIPPStandardLicense -StandardName 'AzureADPremiumP2LicenseCheck' -TenantFilter $TenantFilter -RequiredCapabilities @('AAD_PREMIUM_P2') -SkipLog + + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "License capabilities - Intune: $IntuneCapable, Conditional Access: $ConditionalAccessCapable, Azure AD Premium P2: $AzureADPremiumP2Capable" -sev Info + + #region All Licenses - Basic tenant data collection Write-Host 'Getting cache for Users' try { Set-CIPPDBCacheUsers -TenantFilter $TenantFilter } catch { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Users collection failed: $($_.Exception.Message)" -sev Error @@ -48,11 +56,6 @@ function Push-CIPPDBCacheData { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Devices collection failed: $($_.Exception.Message)" -sev Error } - Write-Host 'Getting cache for ManagedDevices' - try { Set-CIPPDBCacheManagedDevices -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ManagedDevices collection failed: $($_.Exception.Message)" -sev Error - } - Write-Host 'Getting cache for Organization' try { Set-CIPPDBCacheOrganization -TenantFilter $TenantFilter } catch { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Organization collection failed: $($_.Exception.Message)" -sev Error @@ -108,16 +111,6 @@ function Push-CIPPDBCacheData { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "SecureScore collection failed: $($_.Exception.Message)" -sev Error } - Write-Host 'Getting cache for IntunePolicies' - try { Set-CIPPDBCacheIntunePolicies -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "IntunePolicies collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for ConditionalAccessPolicies' - try { Set-CIPPDBCacheConditionalAccessPolicies -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ConditionalAccessPolicies collection failed: $($_.Exception.Message)" -sev Error - } - Write-Host 'Getting cache for PIMSettings' try { Set-CIPPDBCachePIMSettings -TenantFilter $TenantFilter } catch { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "PIMSettings collection failed: $($_.Exception.Message)" -sev Error @@ -153,26 +146,6 @@ function Push-CIPPDBCacheData { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "AuthenticationFlowsPolicy collection failed: $($_.Exception.Message)" -sev Error } - Write-Host 'Getting cache for RiskyUsers' - try { Set-CIPPDBCacheRiskyUsers -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "RiskyUsers collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for RiskyServicePrincipals' - try { Set-CIPPDBCacheRiskyServicePrincipals -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "RiskyServicePrincipals collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for ServicePrincipalRiskDetections' - try { Set-CIPPDBCacheServicePrincipalRiskDetections -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ServicePrincipalRiskDetections collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for RiskDetections' - try { Set-CIPPDBCacheRiskDetections -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "RiskDetections collection failed: $($_.Exception.Message)" -sev Error - } - Write-Host 'Getting cache for DeviceRegistrationPolicy' try { Set-CIPPDBCacheDeviceRegistrationPolicy -TenantFilter $TenantFilter } catch { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "DeviceRegistrationPolicy collection failed: $($_.Exception.Message)" -sev Error @@ -188,11 +161,6 @@ function Push-CIPPDBCacheData { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "UserRegistrationDetails collection failed: $($_.Exception.Message)" -sev Error } - Write-Host 'Getting cache for ManagedDeviceEncryptionStates' - try { Set-CIPPDBCacheManagedDeviceEncryptionStates -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ManagedDeviceEncryptionStates collection failed: $($_.Exception.Message)" -sev Error - } - Write-Host 'Getting cache for OAuth2PermissionGrants' try { Set-CIPPDBCacheOAuth2PermissionGrants -TenantFilter $TenantFilter } catch { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "OAuth2PermissionGrants collection failed: $($_.Exception.Message)" -sev Error @@ -242,11 +210,70 @@ function Push-CIPPDBCacheData { try { Set-CIPPDBCacheExoAcceptedDomains -TenantFilter $TenantFilter } catch { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoAcceptedDomains collection failed: $($_.Exception.Message)" -sev Error } - - Write-Host 'Getting cache for IntuneAppProtectionPolicies' - try { Set-CIPPDBCacheIntuneAppProtectionPolicies -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "IntuneAppProtectionPolicies collection failed: $($_.Exception.Message)" -sev Error - } + #endregion All Licenses + + #region Conditional Access Licensed - Azure AD Premium features + if ($ConditionalAccessCapable) { + Write-Host 'Getting cache for ConditionalAccessPolicies' + try { Set-CIPPDBCacheConditionalAccessPolicies -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ConditionalAccessPolicies collection failed: $($_.Exception.Message)" -sev Error + } + } else { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Skipping Conditional Access data collection - tenant does not have required license' -sev Info + } + #endregion Conditional Access Licensed + + #region Azure AD Premium P2 - Identity Protection features + if ($AzureADPremiumP2Capable) { + Write-Host 'Getting cache for RiskyUsers' + try { Set-CIPPDBCacheRiskyUsers -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "RiskyUsers collection failed: $($_.Exception.Message)" -sev Error + } + + Write-Host 'Getting cache for RiskyServicePrincipals' + try { Set-CIPPDBCacheRiskyServicePrincipals -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "RiskyServicePrincipals collection failed: $($_.Exception.Message)" -sev Error + } + + Write-Host 'Getting cache for ServicePrincipalRiskDetections' + try { Set-CIPPDBCacheServicePrincipalRiskDetections -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ServicePrincipalRiskDetections collection failed: $($_.Exception.Message)" -sev Error + } + + Write-Host 'Getting cache for RiskDetections' + try { Set-CIPPDBCacheRiskDetections -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "RiskDetections collection failed: $($_.Exception.Message)" -sev Error + } + } else { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Skipping Azure AD Premium P2 Identity Protection data collection - tenant does not have required license' -sev Info + } + #endregion Azure AD Premium P2 + + #region Intune Licensed - Intune management features + if ($IntuneCapable) { + Write-Host 'Getting cache for ManagedDevices' + try { Set-CIPPDBCacheManagedDevices -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ManagedDevices collection failed: $($_.Exception.Message)" -sev Error + } + + Write-Host 'Getting cache for IntunePolicies' + try { Set-CIPPDBCacheIntunePolicies -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "IntunePolicies collection failed: $($_.Exception.Message)" -sev Error + } + + Write-Host 'Getting cache for ManagedDeviceEncryptionStates' + try { Set-CIPPDBCacheManagedDeviceEncryptionStates -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ManagedDeviceEncryptionStates collection failed: $($_.Exception.Message)" -sev Error + } + + Write-Host 'Getting cache for IntuneAppProtectionPolicies' + try { Set-CIPPDBCacheIntuneAppProtectionPolicies -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "IntuneAppProtectionPolicies collection failed: $($_.Exception.Message)" -sev Error + } + } else { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Skipping Intune data collection - tenant does not have required license' -sev Info + } + #endregion Intune Licensed Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Completed database cache collection for tenant' -sev Info diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21837.ps1 b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21837.ps1 index ae4a632897d8..aeda6312a4bd 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21837.ps1 +++ b/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21837.ps1 @@ -5,7 +5,7 @@ function Invoke-CippTestZTNA21837 { #Tested try { # Get device registration policy - $DeviceSettings = New-CIPPDbRequest -TenantFilter $Tenant -Type 'deviceRegistrationPolicy' + $DeviceSettings = New-CIPPDbRequest -TenantFilter $Tenant -Type 'DeviceRegistrationPolicy' if (-not $DeviceSettings) { Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Device settings not found in database' -Risk 'High' -Name 'Limit the maximum number of devices per user to 10' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Devices' diff --git a/Test-AllZTNATests.ps1 b/Test-AllZTNATests.ps1 index 47dd04f60839..b64f12d4ce28 100644 --- a/Test-AllZTNATests.ps1 +++ b/Test-AllZTNATests.ps1 @@ -1,2 +1,9 @@ $Tenant = '7ngn50.onmicrosoft.com' -Get-ChildItem "C:\Github\CIPP-API\Modules\CIPPCore\Public\Tests\Invoke-CippTest*.ps1" | ForEach-Object { . $_.FullName; & $_.BaseName -Tenant $Tenant } +$item =0 +Get-ChildItem "C:\Github\CIPP-API\Modules\CIPPCore\Public\Tests\Invoke-CippTest*.ps1" | ForEach-Object { + $item++ + + write-host "performing test $($_.BaseName) - $($item)" + . $_.FullName; & $_.BaseName -Tenant $Tenant + +} diff --git a/test-alignment-profile.ps1 b/test-alignment-profile.ps1 deleted file mode 100644 index 5ca11bf50fd3..000000000000 --- a/test-alignment-profile.ps1 +++ /dev/null @@ -1,30 +0,0 @@ -# Test script for Get-CIPPTenantAlignment with profiling -# This will verify the function returns data in the same format - -# Import the module -Import-Module "$PSScriptRoot\Modules\CIPPCore" -Force - -# Test with a single tenant -$TestTenant = 'm365x72497814.onmicrosoft.com' # Replace with a valid test tenant - -Write-Host "Testing Get-CIPPTenantAlignment with profiling..." -ForegroundColor Cyan -Write-Host "Tenant: $TestTenant" -ForegroundColor Yellow - -try { - $Result = Get-CIPPTenantAlignment -TenantFilter $TestTenant - - Write-Host "`nResult Count: $($Result.Count)" -ForegroundColor Green - - if ($Result) { - Write-Host "`nFirst Result Properties:" -ForegroundColor Green - $Result[0] | Get-Member -MemberType Properties | Select-Object Name, Definition - - Write-Host "`nFirst Result Data:" -ForegroundColor Green - $Result[0] | ConvertTo-Json -Depth 2 - } else { - Write-Host "No results returned" -ForegroundColor Yellow - } -} catch { - Write-Host "Error: $_" -ForegroundColor Red - Write-Host $_.ScriptStackTrace -ForegroundColor Red -} From a06571145b954066e7db2cec2efce10173d96455 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Sat, 27 Dec 2025 13:41:49 +0100 Subject: [PATCH 048/503] Moved tests --- .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21772.ps1 | 0 .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21773.ps1 | 0 .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21774.ps1 | 0 .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21776.ps1 | 0 .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21780.ps1 | 0 .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21783.ps1 | 0 .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21784.ps1 | 0 .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21786.ps1 | 0 .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21787.ps1 | 0 .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21790.ps1 | 0 .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21791.ps1 | 0 .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21792.ps1 | 0 .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21793.ps1 | 0 .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21796.ps1 | 0 .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21797.ps1 | 0 .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21799.ps1 | 0 .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21802.ps1 | 0 .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21803.ps1 | 0 .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21804.ps1 | 0 .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21806.ps1 | 0 .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21807.ps1 | 0 .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21808.ps1 | 0 .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21809.ps1 | 0 .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21810.ps1 | 0 .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21811.ps1 | 0 .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21812.ps1 | 0 .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21813.ps1 | 0 .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21814.ps1 | 0 .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21815.ps1 | 0 .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21816.ps1 | 0 .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21818.ps1 | 0 .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21819.ps1 | 0 .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21820.ps1 | 0 .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21822.ps1 | 0 .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21823.ps1 | 0 .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21824.ps1 | 0 .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21825.ps1 | 0 .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21828.ps1 | 0 .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21829.ps1 | 0 .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21830.ps1 | 0 .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21835.ps1 | 4 ++-- .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21836.ps1 | 2 +- .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21837.ps1 | 0 .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21838.ps1 | 0 .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21839.ps1 | 0 .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21840.ps1 | 0 .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21841.ps1 | 0 .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21842.ps1 | 0 .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21844.ps1 | 0 .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21845.ps1 | 0 .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21846.ps1 | 0 .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21847.ps1 | 0 .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21848.ps1 | 0 .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21849.ps1 | 0 .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21850.ps1 | 0 .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21858.ps1 | 0 .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21861.ps1 | 0 .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21862.ps1 | 0 .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21863.ps1 | 0 .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21865.ps1 | 0 .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21866.ps1 | 0 .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21868.ps1 | 0 .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21869.ps1 | 0 .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21872.ps1 | 0 .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21874.ps1 | 0 .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21877.ps1 | 0 .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21883.ps1 | 0 .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21886.ps1 | 0 .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21889.ps1 | 0 .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21892.ps1 | 0 .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21896.ps1 | 0 .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21941.ps1 | 0 .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21953.ps1 | 0 .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21954.ps1 | 0 .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21955.ps1 | 0 .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21964.ps1 | 0 .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21992.ps1 | 0 .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA22124.ps1 | 0 .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA22128.ps1 | 0 .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA22659.ps1 | 0 .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA24540.ps1 | 0 .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA24541.ps1 | 0 .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA24542.ps1 | 0 .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA24543.ps1 | 0 .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA24545.ps1 | 0 .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA24547.ps1 | 0 .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA24548.ps1 | 0 .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA24549.ps1 | 0 .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA24550.ps1 | 0 .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA24552.ps1 | 0 .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA24553.ps1 | 0 .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA24560.ps1 | 0 .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA24564.ps1 | 0 .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA24568.ps1 | 0 .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA24569.ps1 | 0 .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA24570.ps1 | 0 .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA24572.ps1 | 0 .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA24574.ps1 | 0 .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA24575.ps1 | 0 .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA24576.ps1 | 0 .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA24784.ps1 | 0 .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA24824.ps1 | 0 .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA24827.ps1 | 0 .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA24839.ps1 | 0 .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA24840.ps1 | 0 .../Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA24870.ps1 | 0 106 files changed, 3 insertions(+), 3 deletions(-) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21772.ps1 (100%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21773.ps1 (100%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21774.ps1 (100%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21776.ps1 (100%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21780.ps1 (100%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21783.ps1 (100%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21784.ps1 (100%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21786.ps1 (100%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21787.ps1 (100%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21790.ps1 (100%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21791.ps1 (100%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21792.ps1 (100%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21793.ps1 (100%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21796.ps1 (100%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21797.ps1 (100%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21799.ps1 (100%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21802.ps1 (100%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21803.ps1 (100%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21804.ps1 (100%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21806.ps1 (100%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21807.ps1 (100%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21808.ps1 (100%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21809.ps1 (100%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21810.ps1 (100%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21811.ps1 (100%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21812.ps1 (100%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21813.ps1 (100%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21814.ps1 (100%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21815.ps1 (100%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21816.ps1 (100%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21818.ps1 (100%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21819.ps1 (100%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21820.ps1 (100%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21822.ps1 (100%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21823.ps1 (100%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21824.ps1 (100%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21825.ps1 (100%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21828.ps1 (100%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21829.ps1 (100%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21830.ps1 (100%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21835.ps1 (99%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21836.ps1 (99%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21837.ps1 (100%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21838.ps1 (100%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21839.ps1 (100%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21840.ps1 (100%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21841.ps1 (100%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21842.ps1 (100%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21844.ps1 (100%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21845.ps1 (100%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21846.ps1 (100%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21847.ps1 (100%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21848.ps1 (100%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21849.ps1 (100%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21850.ps1 (100%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21858.ps1 (100%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21861.ps1 (100%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21862.ps1 (100%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21863.ps1 (100%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21865.ps1 (100%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21866.ps1 (100%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21868.ps1 (100%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21869.ps1 (100%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21872.ps1 (100%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21874.ps1 (100%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21877.ps1 (100%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21883.ps1 (100%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21886.ps1 (100%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21889.ps1 (100%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21892.ps1 (100%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21896.ps1 (100%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21941.ps1 (100%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21953.ps1 (100%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21954.ps1 (100%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21955.ps1 (100%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21964.ps1 (100%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA21992.ps1 (100%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA22124.ps1 (100%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA22128.ps1 (100%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA22659.ps1 (100%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA24540.ps1 (100%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA24541.ps1 (100%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA24542.ps1 (100%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA24543.ps1 (100%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA24545.ps1 (100%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA24547.ps1 (100%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA24548.ps1 (100%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA24549.ps1 (100%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA24550.ps1 (100%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA24552.ps1 (100%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA24553.ps1 (100%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA24560.ps1 (100%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA24564.ps1 (100%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA24568.ps1 (100%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA24569.ps1 (100%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA24570.ps1 (100%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA24572.ps1 (100%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA24574.ps1 (100%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA24575.ps1 (100%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA24576.ps1 (100%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA24784.ps1 (100%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA24824.ps1 (100%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA24827.ps1 (100%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA24839.ps1 (100%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA24840.ps1 (100%) rename Modules/CIPPCore/Public/Tests/{ => ZTNA}/Invoke-CippTestZTNA24870.ps1 (100%) diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21772.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21772.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21772.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21772.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21773.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21773.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21773.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21773.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21774.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21774.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21774.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21774.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21776.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21776.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21776.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21776.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21780.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21780.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21780.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21780.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21783.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21783.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21783.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21783.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21784.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21784.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21784.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21784.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21786.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21786.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21786.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21786.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21787.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21787.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21787.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21787.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21790.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21790.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21790.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21790.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21791.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21791.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21791.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21791.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21792.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21792.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21792.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21792.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21793.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21793.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21793.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21793.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21796.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21796.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21796.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21796.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21797.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21797.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21797.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21797.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21799.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21799.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21799.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21799.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21802.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21802.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21802.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21802.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21803.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21803.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21803.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21803.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21804.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21804.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21804.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21804.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21806.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21806.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21806.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21806.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21807.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21807.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21807.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21807.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21808.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21808.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21808.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21808.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21809.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21809.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21809.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21809.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21810.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21810.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21810.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21810.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21811.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21811.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21811.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21811.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21812.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21812.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21812.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21812.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21813.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21813.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21813.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21813.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21814.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21814.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21814.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21814.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21815.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21815.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21815.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21815.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21816.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21816.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21816.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21816.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21818.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21818.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21818.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21818.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21819.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21819.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21819.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21819.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21820.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21820.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21820.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21820.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21822.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21822.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21822.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21822.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21823.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21823.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21823.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21823.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21824.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21824.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21824.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21824.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21825.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21825.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21825.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21825.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21828.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21828.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21828.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21828.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21829.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21829.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21829.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21829.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21830.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21830.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21830.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21830.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21835.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21835.ps1 similarity index 99% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21835.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21835.ps1 index bfc6bd2705f9..6608c730f8fa 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21835.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21835.ps1 @@ -1,6 +1,6 @@ function Invoke-CippTestZTNA21835 { param($Tenant) - #Tested + #Untested $TestId = 'ZTNA21835' try { @@ -14,7 +14,7 @@ function Invoke-CippTestZTNA21835 { } # Get permanent Global Administrator members - $PermanentGAMembers = Get-CippDbRoleMembers -TenantFilter $Tenant -RoleId $GlobalAdminRole.id | Where-Object { + $PermanentGAMembers = Get-CippDbRoleMembers -TenantFilter $Tenant -RoleTemplateId '62e90394-69f5-4237-9190-012177145e10' | Where-Object { $_.AssignmentType -eq 'Permanent' -and $_.'@odata.type' -eq '#microsoft.graph.user' } diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21836.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21836.ps1 similarity index 99% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21836.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21836.ps1 index 510f62a67544..52645947ce3b 100644 --- a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21836.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21836.ps1 @@ -1,6 +1,6 @@ function Invoke-CippTestZTNA21836 { param($Tenant) - #Tested + #Untested $TestId = 'ZTNA21836' try { diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21837.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21837.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21837.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21837.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21838.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21838.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21838.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21838.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21839.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21839.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21839.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21839.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21840.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21840.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21840.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21840.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21841.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21841.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21841.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21841.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21842.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21842.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21842.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21842.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21844.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21844.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21844.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21844.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21845.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21845.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21845.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21845.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21846.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21846.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21846.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21846.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21847.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21847.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21847.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21847.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21848.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21848.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21848.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21848.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21849.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21849.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21849.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21849.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21850.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21850.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21850.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21850.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21858.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21858.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21858.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21858.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21861.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21861.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21861.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21861.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21862.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21862.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21862.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21862.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21863.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21863.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21863.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21863.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21865.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21865.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21865.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21865.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21866.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21866.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21866.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21866.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21868.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21868.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21868.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21868.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21869.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21869.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21869.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21869.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21872.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21872.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21872.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21872.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21874.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21874.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21874.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21874.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21877.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21877.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21877.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21877.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21883.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21883.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21883.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21883.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21886.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21886.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21886.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21886.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21889.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21889.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21889.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21889.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21892.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21892.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21892.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21892.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21896.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21896.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21896.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21896.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21941.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21941.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21941.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21941.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21953.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21953.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21953.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21953.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21954.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21954.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21954.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21954.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21955.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21955.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21955.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21955.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21964.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21964.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21964.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21964.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21992.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21992.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA21992.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21992.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA22124.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA22124.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA22124.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA22124.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA22128.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA22128.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA22128.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA22128.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA22659.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA22659.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA22659.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA22659.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24540.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24540.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24540.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24540.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24541.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24541.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24541.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24541.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24542.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24542.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24542.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24542.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24543.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24543.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24543.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24543.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24545.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24545.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24545.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24545.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24547.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24547.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24547.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24547.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24548.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24548.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24548.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24548.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24549.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24549.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24549.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24549.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24550.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24550.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24550.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24550.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24552.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24552.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24552.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24552.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24553.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24553.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24553.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24553.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24560.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24560.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24560.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24560.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24564.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24564.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24564.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24564.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24568.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24568.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24568.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24568.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24569.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24569.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24569.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24569.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24570.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24570.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24570.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24570.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24572.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24572.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24572.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24572.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24574.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24574.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24574.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24574.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24575.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24575.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24575.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24575.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24576.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24576.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24576.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24576.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24784.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24784.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24784.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24784.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24824.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24824.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24824.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24824.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24827.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24827.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24827.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24827.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24839.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24839.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24839.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24839.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24840.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24840.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24840.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24840.ps1 diff --git a/Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24870.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24870.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/Invoke-CippTestZTNA24870.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24870.ps1 From 5536b25205b437428311358891fcb1267c9105c4 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Sat, 27 Dec 2025 14:40:13 +0100 Subject: [PATCH 049/503] Move files --- .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21772.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21773.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21774.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21776.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21780.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21783.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21784.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21786.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21787.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21790.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21791.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21792.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21793.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21796.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21797.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21799.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21802.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21803.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21804.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21806.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21807.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21808.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21809.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21810.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21811.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21812.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21813.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21814.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21815.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21816.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21818.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21819.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21820.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21822.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21823.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21824.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21825.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21828.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21829.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21830.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21835.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21836.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21837.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21838.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21839.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21840.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21841.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21842.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21844.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21845.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21846.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21847.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21848.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21849.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21850.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21858.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21861.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21862.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21863.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21865.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21866.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21868.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21869.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21872.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21874.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21877.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21883.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21886.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21889.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21892.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21896.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21941.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21953.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21954.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21955.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21964.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21992.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA22124.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA22128.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA22659.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA24540.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA24541.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA24542.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA24543.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA24545.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA24547.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA24548.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA24549.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA24550.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA24552.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA24553.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA24560.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA24564.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA24568.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA24569.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA24570.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA24572.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA24574.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA24575.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA24576.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA24784.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA24824.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA24827.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA24839.ps1 | 0 .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA24840.ps1 | 1 + .../Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA24870.ps1 | 0 106 files changed, 1 insertion(+) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21772.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21773.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21774.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21776.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21780.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21783.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21784.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21786.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21787.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21790.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21791.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21792.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21793.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21796.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21797.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21799.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21802.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21803.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21804.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21806.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21807.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21808.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21809.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21810.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21811.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21812.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21813.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21814.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21815.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21816.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21818.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21819.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21820.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21822.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21823.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21824.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21825.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21828.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21829.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21830.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21835.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21836.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21837.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21838.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21839.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21840.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21841.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21842.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21844.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21845.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21846.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21847.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21848.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21849.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21850.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21858.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21861.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21862.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21863.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21865.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21866.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21868.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21869.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21872.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21874.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21877.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21883.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21886.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21889.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21892.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21896.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21941.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21953.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21954.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21955.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21964.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA21992.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA22124.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA22128.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA22659.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA24540.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA24541.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA24542.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA24543.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA24545.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA24547.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA24548.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA24549.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA24550.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA24552.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA24553.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA24560.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA24564.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA24568.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA24569.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA24570.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA24572.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA24574.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA24575.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA24576.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA24784.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA24824.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA24827.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA24839.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA24840.ps1 (99%) rename Modules/CIPPCore/Public/Tests/ZTNA/{ => Identity}/Invoke-CippTestZTNA24870.ps1 (100%) diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21772.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21772.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21772.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21772.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21773.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21773.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21773.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21773.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21774.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21774.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21774.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21774.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21776.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21776.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21776.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21776.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21780.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21780.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21780.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21780.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21783.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21783.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21783.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21783.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21784.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21784.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21784.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21784.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21786.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21786.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21786.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21786.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21787.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21787.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21787.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21787.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21790.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21790.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21790.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21790.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21791.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21791.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21791.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21791.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21792.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21792.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21792.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21792.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21793.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21793.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21793.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21793.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21796.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21796.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21796.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21796.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21797.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21797.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21797.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21797.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21799.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21799.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21799.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21799.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21802.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21802.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21802.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21802.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21803.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21803.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21803.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21803.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21804.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21804.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21804.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21804.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21806.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21806.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21806.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21806.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21807.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21807.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21807.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21807.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21808.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21808.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21808.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21808.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21809.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21809.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21809.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21809.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21810.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21810.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21810.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21810.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21811.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21811.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21811.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21811.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21812.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21812.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21812.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21812.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21813.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21813.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21813.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21813.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21814.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21814.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21814.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21814.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21815.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21815.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21815.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21815.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21816.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21816.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21816.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21816.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21818.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21818.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21818.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21818.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21819.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21819.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21819.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21819.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21820.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21820.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21820.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21820.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21822.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21822.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21822.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21822.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21823.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21823.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21823.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21823.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21824.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21824.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21824.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21824.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21825.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21825.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21825.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21825.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21828.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21828.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21828.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21828.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21829.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21829.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21829.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21829.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21830.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21830.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21830.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21830.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21835.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21835.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21835.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21835.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21836.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21836.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21836.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21836.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21837.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21837.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21837.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21837.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21838.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21838.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21838.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21838.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21839.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21839.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21839.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21839.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21840.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21840.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21840.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21840.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21841.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21841.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21841.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21841.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21842.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21842.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21842.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21842.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21844.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21844.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21844.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21844.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21845.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21845.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21845.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21845.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21846.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21846.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21846.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21846.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21847.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21847.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21847.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21847.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21848.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21848.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21848.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21848.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21849.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21849.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21849.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21849.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21850.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21850.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21850.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21850.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21858.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21858.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21858.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21858.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21861.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21861.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21861.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21861.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21862.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21862.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21862.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21862.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21863.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21863.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21863.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21863.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21865.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21865.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21865.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21865.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21866.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21866.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21866.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21866.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21868.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21868.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21868.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21868.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21869.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21869.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21869.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21869.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21872.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21872.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21872.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21872.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21874.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21874.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21874.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21874.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21877.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21877.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21877.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21877.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21883.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21883.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21883.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21883.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21886.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21886.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21886.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21886.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21889.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21889.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21889.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21889.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21892.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21892.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21892.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21892.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21896.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21896.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21896.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21896.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21941.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21941.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21941.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21941.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21953.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21953.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21953.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21953.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21954.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21954.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21954.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21954.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21955.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21955.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21955.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21955.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21964.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21964.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21964.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21964.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21992.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21992.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA21992.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21992.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA22124.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA22124.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA22124.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA22124.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA22128.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA22128.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA22128.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA22128.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA22659.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA22659.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA22659.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA22659.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24540.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24540.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24540.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24540.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24541.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24541.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24541.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24541.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24542.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24542.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24542.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24542.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24543.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24543.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24543.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24543.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24545.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24545.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24545.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24545.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24547.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24547.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24547.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24547.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24548.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24548.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24548.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24548.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24549.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24549.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24549.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24549.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24550.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24550.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24550.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24550.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24552.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24552.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24552.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24552.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24553.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24553.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24553.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24553.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24560.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24560.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24560.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24560.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24564.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24564.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24564.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24564.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24568.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24568.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24568.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24568.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24569.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24569.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24569.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24569.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24570.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24570.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24570.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24570.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24572.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24572.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24572.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24572.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24574.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24574.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24574.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24574.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24575.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24575.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24575.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24575.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24576.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24576.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24576.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24576.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24784.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24784.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24784.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24784.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24824.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24824.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24824.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24824.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24827.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24827.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24827.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24827.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24839.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24839.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24839.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24839.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24840.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24840.ps1 similarity index 99% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24840.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24840.ps1 index 37faf8fce948..fade320d9e20 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24840.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24840.ps1 @@ -2,6 +2,7 @@ function Invoke-CippTestZTNA24840 { param($Tenant) $TestId = 'ZTNA24840' + #Tested - Device try { diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24870.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24870.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Invoke-CippTestZTNA24870.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24870.ps1 From e03c96a5cb616daaa66cf09c4ad0fa2b6976a3d5 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Sat, 27 Dec 2025 16:01:50 +0100 Subject: [PATCH 050/503] Move files --- .../Invoke-CippTestZTNA24540.ps1 | 0 .../Invoke-CippTestZTNA24541.ps1 | 0 .../Invoke-CippTestZTNA24542.ps1 | 0 .../Invoke-CippTestZTNA24543.ps1 | 0 .../Invoke-CippTestZTNA24545.ps1 | 0 .../Invoke-CippTestZTNA24547.ps1 | 0 .../Invoke-CippTestZTNA24548.ps1 | 0 .../Invoke-CippTestZTNA24549.ps1 | 0 .../Invoke-CippTestZTNA24550.ps1 | 0 .../Invoke-CippTestZTNA24552.ps1 | 0 .../Invoke-CippTestZTNA24553.ps1 | 0 .../Invoke-CippTestZTNA24560.ps1 | 0 .../Invoke-CippTestZTNA24564.ps1 | 0 .../Invoke-CippTestZTNA24568.ps1 | 0 .../Invoke-CippTestZTNA24569.ps1 | 0 .../Invoke-CippTestZTNA24574.ps1 | 0 .../Invoke-CippTestZTNA24575.ps1 | 0 .../Invoke-CippTestZTNA24576.ps1 | 0 .../Invoke-CippTestZTNA24784.ps1 | 0 .../Invoke-CippTestZTNA24839.ps1 | 0 .../Invoke-CippTestZTNA24840.ps1 | 0 .../Invoke-CippTestZTNA24870.ps1 | 0 Modules/CIPPCore/Public/Tests/ZTNA/report.json | 15 +++++++++++++++ 23 files changed, 15 insertions(+) rename Modules/CIPPCore/Public/Tests/ZTNA/{Identity => Devices}/Invoke-CippTestZTNA24540.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{Identity => Devices}/Invoke-CippTestZTNA24541.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{Identity => Devices}/Invoke-CippTestZTNA24542.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{Identity => Devices}/Invoke-CippTestZTNA24543.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{Identity => Devices}/Invoke-CippTestZTNA24545.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{Identity => Devices}/Invoke-CippTestZTNA24547.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{Identity => Devices}/Invoke-CippTestZTNA24548.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{Identity => Devices}/Invoke-CippTestZTNA24549.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{Identity => Devices}/Invoke-CippTestZTNA24550.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{Identity => Devices}/Invoke-CippTestZTNA24552.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{Identity => Devices}/Invoke-CippTestZTNA24553.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{Identity => Devices}/Invoke-CippTestZTNA24560.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{Identity => Devices}/Invoke-CippTestZTNA24564.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{Identity => Devices}/Invoke-CippTestZTNA24568.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{Identity => Devices}/Invoke-CippTestZTNA24569.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{Identity => Devices}/Invoke-CippTestZTNA24574.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{Identity => Devices}/Invoke-CippTestZTNA24575.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{Identity => Devices}/Invoke-CippTestZTNA24576.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{Identity => Devices}/Invoke-CippTestZTNA24784.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{Identity => Devices}/Invoke-CippTestZTNA24839.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{Identity => Devices}/Invoke-CippTestZTNA24840.ps1 (100%) rename Modules/CIPPCore/Public/Tests/ZTNA/{Identity => Devices}/Invoke-CippTestZTNA24870.ps1 (100%) create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/report.json diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24540.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24540.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24540.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24540.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24541.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24541.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24541.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24541.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24542.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24542.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24542.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24542.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24543.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24543.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24543.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24543.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24545.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24545.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24545.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24545.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24547.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24547.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24547.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24547.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24548.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24548.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24548.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24548.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24549.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24549.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24549.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24549.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24550.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24550.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24550.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24550.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24552.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24552.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24552.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24552.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24553.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24553.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24553.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24553.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24560.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24560.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24560.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24560.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24564.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24564.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24564.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24564.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24568.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24568.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24568.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24568.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24569.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24569.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24569.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24569.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24574.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24574.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24574.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24574.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24575.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24575.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24575.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24575.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24576.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24576.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24576.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24576.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24784.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24784.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24784.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24784.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24839.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24839.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24839.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24839.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24840.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24840.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24840.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24840.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24870.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24870.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24870.ps1 rename to Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24870.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/report.json b/Modules/CIPPCore/Public/Tests/ZTNA/report.json new file mode 100644 index 000000000000..680cf7324658 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/report.json @@ -0,0 +1,15 @@ +{ + "name": "Zero Trust Network Access Tests", + "description": "Microsoft's Comprehensive security assessment covering identity and device compliance, conditional access policies, authentication methods, and endpoint protection aligned with Zero Trust principles.", + "version": "1.0", + "categories": { + "identity": { + "title": "Identity & Access", + "description": "Authentication, authorization, conditional access, and credential management" + }, + "device": { + "title": "Device Security", + "description": "Device compliance, configuration profiles, app protection, and endpoint security" + } + } +} From 59838cd7b1da79029af2bbe4cfd5a3c9a521c3e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Sun, 28 Dec 2025 00:24:25 +0100 Subject: [PATCH 051/503] Fix: Fix broken expand for app protection policies --- .../MEM/Invoke-ListAppProtectionPolicies.ps1 | 80 +++++++++---------- 1 file changed, 39 insertions(+), 41 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListAppProtectionPolicies.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListAppProtectionPolicies.ps1 index 7fc2af495e1b..ef6b59fec416 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListAppProtectionPolicies.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListAppProtectionPolicies.ps1 @@ -25,7 +25,7 @@ @{ id = 'ManagedAppPolicies' method = 'GET' - url = '/deviceAppManagement/managedAppPolicies?$expand=assignments&$orderby=displayName' + url = '/deviceAppManagement/managedAppPolicies?$orderby=displayName' } @{ id = 'MobileAppConfigurations' @@ -41,60 +41,58 @@ $GraphRequest = [System.Collections.Generic.List[object]]::new() - # Process Managed App Policies - these need separate assignment lookups + # Process Managed App Policies - these need separate assignment lookups as the ManagedAppPolicies endpoint does not support $expand $ManagedAppPolicies = ($BulkResults | Where-Object { $_.id -eq 'ManagedAppPolicies' }).body.value if ($ManagedAppPolicies) { - # Build bulk requests for assignments of policies that support them - $AssignmentRequests = [System.Collections.Generic.List[object]]::new() - foreach ($Policy in $ManagedAppPolicies) { - # Only certain policy types support assignments endpoint - $odataType = $Policy.'@odata.type' - if ($odataType -match 'androidManagedAppProtection|iosManagedAppProtection|windowsManagedAppProtection|targetedManagedAppConfiguration') { - $urlSegment = switch -Wildcard ($odataType) { - '*androidManagedAppProtection*' { 'androidManagedAppProtections' } - '*iosManagedAppProtection*' { 'iosManagedAppProtections' } - '*windowsManagedAppProtection*' { 'windowsManagedAppProtections' } - '*targetedManagedAppConfiguration*' { 'targetedManagedAppConfigurations' } - } - if ($urlSegment) { - $AssignmentRequests.Add(@{ - id = $Policy.id - method = 'GET' - url = "/deviceAppManagement/$urlSegment('$($Policy.id)')/assignments" - }) + # Get all @odata.type and deduplicate them + $OdataTypes = ($ManagedAppPolicies | Select-Object -ExpandProperty '@odata.type' -Unique) -replace '#microsoft.graph.', '' + $ManagedAppPoliciesBulkRequests = foreach ($type in $OdataTypes) { + # Translate to URL segments + $urlSegment = switch ($type) { + 'androidManagedAppProtection' { 'androidManagedAppProtections' } + 'iosManagedAppProtection' { 'iosManagedAppProtections' } + 'mdmWindowsInformationProtectionPolicy' { 'mdmWindowsInformationProtectionPolicies' } + 'windowsManagedAppProtection' { 'windowsManagedAppProtections' } + 'targetedManagedAppConfiguration' { 'targetedManagedAppConfigurations' } + default { $null } + } + Write-Information "Type: $type => URL Segment: $urlSegment" + if ($urlSegment) { + @{ + id = $type + method = 'GET' + url = "/deviceAppManagement/${urlSegment}?`$expand=assignments&`$orderby=displayName" } } } - # Fetch assignments in bulk if we have any - $AssignmentResults = @{} - if ($AssignmentRequests.Count -gt 0) { - $AssignmentBulkResults = New-GraphBulkRequest -Requests $AssignmentRequests -tenantid $TenantFilter - foreach ($result in $AssignmentBulkResults) { - if ($result.body.value) { - $AssignmentResults[$result.id] = $result.body.value - } - } + $ManagedAppPoliciesBulkResults = New-GraphBulkRequest -Requests $ManagedAppPoliciesBulkRequests -tenantid $TenantFilter + # Do this horriblenes as a workaround, as the results dont return with a odata.type property + $ManagedAppPolicies = $ManagedAppPoliciesBulkResults | ForEach-Object { + $URLName = $_.id + $_.body.value | Add-Member -NotePropertyName 'URLName' -NotePropertyValue $URLName -Force + $_.body.value } + + foreach ($Policy in $ManagedAppPolicies) { - $policyType = switch -Wildcard ($Policy.'@odata.type') { - '*androidManagedAppProtection*' { 'Android App Protection' } - '*iosManagedAppProtection*' { 'iOS App Protection' } - '*windowsManagedAppProtection*' { 'Windows App Protection' } - '*mdmWindowsInformationProtectionPolicy*' { 'Windows Information Protection (MDM)' } - '*windowsInformationProtectionPolicy*' { 'Windows Information Protection' } - '*targetedManagedAppConfiguration*' { 'App Configuration (MAM)' } - '*defaultManagedAppProtection*' { 'Default App Protection' } + $policyType = switch ($Policy.'URLName') { + 'androidManagedAppProtection' { 'Android App Protection'; break } + 'iosManagedAppProtection' { 'iOS App Protection'; break } + 'windowsManagedAppProtection' { 'Windows App Protection'; break } + 'mdmWindowsInformationProtectionPolicy' { 'Windows Information Protection (MDM)'; break } + 'windowsInformationProtectionPolicy' { 'Windows Information Protection'; break } + 'targetedManagedAppConfiguration' { 'App Configuration (MAM)'; break } + 'defaultManagedAppProtection' { 'Default App Protection'; break } default { 'App Protection Policy' } } # Process assignments $PolicyAssignment = [System.Collections.Generic.List[string]]::new() $PolicyExclude = [System.Collections.Generic.List[string]]::new() - $Assignments = $AssignmentResults[$Policy.id] - if ($Assignments) { - foreach ($Assignment in $Assignments) { + if ($Policy.assignments) { + foreach ($Assignment in $Policy.assignments) { $target = $Assignment.target switch ($target.'@odata.type') { '#microsoft.graph.allDevicesAssignmentTarget' { $PolicyAssignment.Add('All Devices') } @@ -112,7 +110,7 @@ } $Policy | Add-Member -NotePropertyName 'PolicyTypeName' -NotePropertyValue $policyType -Force - $Policy | Add-Member -NotePropertyName 'URLName' -NotePropertyValue 'managedAppPolicies' -Force + # $Policy | Add-Member -NotePropertyName 'URLName' -NotePropertyValue 'managedAppPolicies' -Force $Policy | Add-Member -NotePropertyName 'PolicySource' -NotePropertyValue 'AppProtection' -Force $Policy | Add-Member -NotePropertyName 'PolicyAssignment' -NotePropertyValue ($PolicyAssignment -join ', ') -Force $Policy | Add-Member -NotePropertyName 'PolicyExclude' -NotePropertyValue ($PolicyExclude -join ', ') -Force From 6580150098e80916769517c496815c477d2c0c24 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 29 Dec 2025 02:17:49 +0000 Subject: [PATCH 052/503] Initial plan From 598d5d3f94b873435fc0a9727b0cc19df138e6c2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 29 Dec 2025 02:21:48 +0000 Subject: [PATCH 053/503] Fix service principal alert URL structure Changed URL structure for Add and Remove service principal alerts: - Updated path from /tenant/administration/enterprise-apps to /tenant/administration/applications/enterprise-apps - Changed query parameter from customerId=?customerId= to tenantFilter= with tenant default domain - Fixes incorrect URL generation that was using double customerId parameter Co-authored-by: Zacgoose <107489668+Zacgoose@users.noreply.github.com> --- Modules/CIPPCore/Public/New-CIPPAlertTemplate.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Modules/CIPPCore/Public/New-CIPPAlertTemplate.ps1 b/Modules/CIPPCore/Public/New-CIPPAlertTemplate.ps1 index 1a0836dba0c2..7a27d150b57c 100644 --- a/Modules/CIPPCore/Public/New-CIPPAlertTemplate.ps1 +++ b/Modules/CIPPCore/Public/New-CIPPAlertTemplate.ps1 @@ -228,7 +228,7 @@ function New-CIPPAlertTemplate { $IntroText = $IntroText + "

The (potential) location information for this IP is as follows:

$LocationTable" } $IntroText = "$($data.ObjectId) has been added by $($data.UserId)." - $ButtonUrl = "$CIPPURL/tenant/administration/enterprise-apps?customerId=?customerId=$($data.OrganizationId)" + $ButtonUrl = "$CIPPURL/tenant/administration/applications/enterprise-apps?tenantFilter=$Tenant" $ButtonText = 'Enterprise Apps' } 'Remove service principal.' { @@ -241,7 +241,7 @@ function New-CIPPAlertTemplate { $IntroText = $IntroText + "

The (potential) location information for this IP is as follows:

$LocationTable" } $IntroText = "$($data.ObjectId) has been added by $($data.UserId)." - $ButtonUrl = "$CIPPURL/tenant/administration/enterprise-apps?customerId=?customerId=$($data.OrganizationId)" + $ButtonUrl = "$CIPPURL/tenant/administration/applications/enterprise-apps?tenantFilter=$Tenant" $ButtonText = 'Enterprise Apps' } 'UserLoggedIn' { From 4c1ac081ca22185e1d8b552b95ebe7669b7a5d94 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 29 Dec 2025 02:23:37 +0000 Subject: [PATCH 054/503] Fix message text for Remove service principal alert Corrected the IntroText to say "has been removed" instead of "has been added" for the Remove service principal operation to match the actual action being performed. Co-authored-by: Zacgoose <107489668+Zacgoose@users.noreply.github.com> --- Modules/CIPPCore/Public/New-CIPPAlertTemplate.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/New-CIPPAlertTemplate.ps1 b/Modules/CIPPCore/Public/New-CIPPAlertTemplate.ps1 index 7a27d150b57c..67340eaf3ab5 100644 --- a/Modules/CIPPCore/Public/New-CIPPAlertTemplate.ps1 +++ b/Modules/CIPPCore/Public/New-CIPPAlertTemplate.ps1 @@ -240,7 +240,7 @@ function New-CIPPAlertTemplate { $LocationTable = ($LocationInfo | ConvertTo-Html -Fragment -As List | Out-String).Replace('', '
') $IntroText = $IntroText + "

The (potential) location information for this IP is as follows:

$LocationTable" } - $IntroText = "$($data.ObjectId) has been added by $($data.UserId)." + $IntroText = "$($data.ObjectId) has been removed by $($data.UserId)." $ButtonUrl = "$CIPPURL/tenant/administration/applications/enterprise-apps?tenantFilter=$Tenant" $ButtonText = 'Enterprise Apps' } From b9d0e3904bd70cc3f0bd951024c62d1b72ba4a51 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Mon, 29 Dec 2025 19:13:50 +0100 Subject: [PATCH 055/503] report update --- Modules/CIPPCore/Public/Tests/ZTNA/report.json | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/report.json b/Modules/CIPPCore/Public/Tests/ZTNA/report.json index 680cf7324658..2a5652da78fe 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/report.json +++ b/Modules/CIPPCore/Public/Tests/ZTNA/report.json @@ -1,15 +1,5 @@ { "name": "Zero Trust Network Access Tests", "description": "Microsoft's Comprehensive security assessment covering identity and device compliance, conditional access policies, authentication methods, and endpoint protection aligned with Zero Trust principles.", - "version": "1.0", - "categories": { - "identity": { - "title": "Identity & Access", - "description": "Authentication, authorization, conditional access, and credential management" - }, - "device": { - "title": "Device Security", - "description": "Device compliance, configuration profiles, app protection, and endpoint security" - } - } + "version": "1.0" } From 7fbadce4f2bc919b70a0dd2ca3c0f03ed328b01a Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Tue, 30 Dec 2025 12:38:12 +0100 Subject: [PATCH 056/503] Add MFA state and license overviews --- .../Push-CIPPDBCacheData.ps1 | 10 +++++++ .../Public/Set-CIPPDBCacheLicenseOverview.ps1 | 27 +++++++++++++++++++ .../Public/Set-CIPPDBCacheMFAState.ps1 | 27 +++++++++++++++++++ 3 files changed, 64 insertions(+) create mode 100644 Modules/CIPPCore/Public/Set-CIPPDBCacheLicenseOverview.ps1 create mode 100644 Modules/CIPPCore/Public/Set-CIPPDBCacheMFAState.ps1 diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 index 4e34cea5ec06..6152e6e2d2ce 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 @@ -210,6 +210,16 @@ function Push-CIPPDBCacheData { try { Set-CIPPDBCacheExoAcceptedDomains -TenantFilter $TenantFilter } catch { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoAcceptedDomains collection failed: $($_.Exception.Message)" -sev Error } + + Write-Host 'Getting cache for License Overview' + try { Set-CIPPDBCacheLicenseOverview -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "License Overview collection failed: $($_.Exception.Message)" -sev Error + } + + Write-Host 'Getting cache for MFA State' + try { Set-CIPPDBCacheMFAState -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "MFA State collection failed: $($_.Exception.Message)" -sev Error + } #endregion All Licenses #region Conditional Access Licensed - Azure AD Premium features diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheLicenseOverview.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheLicenseOverview.ps1 new file mode 100644 index 000000000000..5556ac3d307c --- /dev/null +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheLicenseOverview.ps1 @@ -0,0 +1,27 @@ +function Set-CIPPDBCacheLicenseOverview { + <# + .SYNOPSIS + Caches license overview for a tenant + + .PARAMETER TenantFilter + The tenant to cache license overview for + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter + ) + + try { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching license overview' -sev Info + + $LicenseOverview = Get-CIPPLicenseOverview -TenantFilter $TenantFilter + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'LicenseOverview' -Data @($LicenseOverview) + $LicenseOverview = $null + + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached license overview successfully' -sev Info + + } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache license overview: $($_.Exception.Message)" -sev Error + } +} diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheMFAState.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheMFAState.ps1 new file mode 100644 index 000000000000..131f5a1ac16c --- /dev/null +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheMFAState.ps1 @@ -0,0 +1,27 @@ +function Set-CIPPDBCacheMFAState { + <# + .SYNOPSIS + Caches MFA state for a tenant + + .PARAMETER TenantFilter + The tenant to cache MFA state for + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter + ) + + try { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching MFA state' -sev Info + + $MFAState = Get-CIPPMFAState -TenantFilter $TenantFilter + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'MFAState' -Data @($MFAState) + $MFAState = $null + + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached MFA state successfully' -sev Info + + } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache MFA state: $($_.Exception.Message)" -sev Error + } +} From ee8e3ec0a7dbd98bd2c81b444fd0f9f5f2fc9e16 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Tue, 30 Dec 2025 17:59:16 +0100 Subject: [PATCH 057/503] Add MFA state --- .../Public/Entrypoints/HTTP Functions/Invoke-ListTests.ps1 | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ListTests.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ListTests.ps1 index cd739ad7246c..8714963e0519 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ListTests.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ListTests.ps1 @@ -85,6 +85,10 @@ function Invoke-ListTests { if ($SecureScoreData) { $TestResultsData | Add-Member -NotePropertyName 'SecureScore' -NotePropertyValue $SecureScoreData -Force } + $MFAStateData = New-CIPPDbRequest -TenantFilter $TenantFilter -Type 'MFAState' + if ($MFAStateData) { + $TestResultsData | Add-Member -NotePropertyName 'MFAState' -NotePropertyValue $MFAStateData -Force + } $StatusCode = [HttpStatusCode]::OK $Body = $TestResultsData From 3338a423acd3a644aa48e301aeaf0b84d3d9f51f Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Tue, 30 Dec 2025 23:12:40 +0100 Subject: [PATCH 058/503] New creation apis --- .../HTTP Functions/Invoke-AddTestReport.ps1 | 60 ++++++++++ .../Invoke-DeleteTestReport.ps1 | 37 ++++++ .../Invoke-ListAvailableTests.ps1 | 87 ++++++++++++++ .../HTTP Functions/Invoke-ListTestReports.ps1 | 69 +++++++++++ .../HTTP Functions/Invoke-ListTests.ps1 | 65 +++++++--- .../CIPPCore/Public/Tests/ZTNA/report.json | 112 +++++++++++++++++- 6 files changed, 412 insertions(+), 18 deletions(-) create mode 100644 Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-AddTestReport.ps1 create mode 100644 Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-DeleteTestReport.ps1 create mode 100644 Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ListAvailableTests.ps1 create mode 100644 Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ListTestReports.ps1 diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-AddTestReport.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-AddTestReport.ps1 new file mode 100644 index 000000000000..2fc40dfb6533 --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-AddTestReport.ps1 @@ -0,0 +1,60 @@ +function Invoke-AddTestReport { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + CIPP.Dashboard.Read + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $TriggerMetadata.FunctionName + Write-LogMessage -user $Request.Headers.'x-ms-client-principal' -API $APIName -message 'Accessed this API' -Sev 'Debug' + + try { + $Body = $Request.Body + + # Validate required fields + if ([string]::IsNullOrEmpty($Body.name)) { + throw 'Report name is required' + } + + # Generate a unique ID + $ReportId = New-Guid + $IdentityTests = $Body.IdentityTests ? ($Body.IdentityTests.value | ConvertTo-Json) : '[]' + $DevicesTests = $Body.DevicesTests ? ($Body.DevicesTests.value | ConvertTo-Json) : '[]' + + # Create report object + $Report = [PSCustomObject]@{ + PartitionKey = 'Report' + RowKey = [string]$ReportId + name = [string]$Body.name + description = [string]$Body.description + version = '1.0' + IdentityTests = [string]$IdentityTests + DevicesTests = [string]$DevicesTests + CreatedAt = [string](Get-Date).ToString('o') + } + + # Save to table + $Table = Get-CippTable -tablename 'CippReportTemplates' + Add-CIPPAzDataTableEntity -Entity $Report @Table + $Body = [PSCustomObject]@{ + Results = 'Successfully created custom report' + ReportId = $ReportId + } + $StatusCode = [HttpStatusCode]::OK + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -user $Request.Headers.'x-ms-client-principal' -API $APIName -message "Failed to create report: $($ErrorMessage.NormalizedError)" -Sev 'Error' -LogData $ErrorMessage + $Body = [PSCustomObject]@{ + Results = "Failed to create report: $($ErrorMessage.NormalizedError)" + } + $StatusCode = [HttpStatusCode]::BadRequest + } + + Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = ConvertTo-Json -InputObject $Body -Depth 10 + }) +} diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-DeleteTestReport.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-DeleteTestReport.ps1 new file mode 100644 index 000000000000..39d7197ba028 --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-DeleteTestReport.ps1 @@ -0,0 +1,37 @@ +function Invoke-DeleteTestReport { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + CIPP.Dashboard.Read + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $TriggerMetadata.FunctionName + Write-LogMessage -user $Request.Headers.'x-ms-client-principal' -API $APIName -message 'Accessed this API' -Sev 'Debug' + + try { + $ReportId = $Request.Body.ReportId + $Table = Get-CippTable -tablename 'CippReportTemplates' + $ExistingReport = Get-CIPPAzDataTableEntity @Table -Filter "RowKey eq '$ReportId'" + Remove-AzDataTableEntity @Table -Entity $ExistingReport + + $Body = [PSCustomObject]@{ + Results = 'Successfully deleted custom report' + } + $StatusCode = [HttpStatusCode]::OK + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -user $Request.Headers.'x-ms-client-principal' -API $APIName -message "Failed to delete report: $($ErrorMessage.NormalizedError)" -Sev 'Error' -LogData $ErrorMessage + $Body = [PSCustomObject]@{ + Results = "Failed to delete report: $($ErrorMessage.NormalizedError)" + } + $StatusCode = [HttpStatusCode]::BadRequest + } + + Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = ConvertTo-Json -InputObject $Body -Depth 10 + }) +} diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ListAvailableTests.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ListAvailableTests.ps1 new file mode 100644 index 000000000000..7f70261e1120 --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ListAvailableTests.ps1 @@ -0,0 +1,87 @@ +function Invoke-ListAvailableTests { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + CIPP.Dashboard.Read + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $TriggerMetadata.FunctionName + Write-LogMessage -user $Request.Headers.'x-ms-client-principal' -API $APIName -message 'Accessed this API' -Sev 'Debug' + + try { + # Get all test folders + $TestFolders = Get-ChildItem 'Modules\CIPPCore\Public\Tests' -Directory + + # Build identity tests array + $IdentityTests = foreach ($TestFolder in $TestFolders) { + $IdentityTestFiles = Get-ChildItem "$($TestFolder.FullName)\Identity\*.ps1" -ErrorAction SilentlyContinue + foreach ($TestFile in $IdentityTestFiles) { + # Extract test ID from filename (e.g., Invoke-CippTestZTNA21772.ps1 -> ZTNA21772) + if ($TestFile.BaseName -match 'Invoke-CippTest(.+)$') { + $TestId = $Matches[1] + + # Try to get test metadata from the file + $TestContent = Get-Content $TestFile.FullName -Raw + $TestName = $TestId + + # Try to extract Synopsis from comment-based help + if ($TestContent -match '\.SYNOPSIS\s+(.+?)(?=\s+\.|\s+#>|\s+\[)') { + $TestName = $Matches[1].Trim() + } + + [PSCustomObject]@{ + id = $TestId + name = $TestName + category = 'Identity' + testFolder = $TestFolder.Name + } + } + } + } + + # Build device tests array + $DevicesTests = foreach ($TestFolder in $TestFolders) { + $DeviceTestFiles = Get-ChildItem "$($TestFolder.FullName)\Devices\*.ps1" -ErrorAction SilentlyContinue + foreach ($TestFile in $DeviceTestFiles) { + if ($TestFile.BaseName -match 'Invoke-CippTest(.+)$') { + $TestId = $Matches[1] + + $TestContent = Get-Content $TestFile.FullName -Raw + $TestName = $TestId + + if ($TestContent -match '\.SYNOPSIS\s+(.+?)(?=\s+\.|\s+#>|\s+\[)') { + $TestName = $Matches[1].Trim() + } + + [PSCustomObject]@{ + id = $TestId + name = $TestName + category = 'Devices' + testFolder = $TestFolder.Name + } + } + } + } + + $Body = [PSCustomObject]@{ + IdentityTests = $IdentityTests + DevicesTests = $DevicesTests + } + $StatusCode = [HttpStatusCode]::OK + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -user $Request.Headers.'x-ms-client-principal' -API $APIName -message "Failed to list available tests: $($ErrorMessage.NormalizedError)" -Sev 'Error' -LogData $ErrorMessage + $Body = [PSCustomObject]@{ + Results = "Failed to list available tests: $($ErrorMessage.NormalizedError)" + } + $StatusCode = [HttpStatusCode]::BadRequest + } + + Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = ConvertTo-Json -InputObject $Body -Depth 10 + }) +} diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ListTestReports.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ListTestReports.ps1 new file mode 100644 index 000000000000..4920399d70c1 --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ListTestReports.ps1 @@ -0,0 +1,69 @@ +function Invoke-ListTestReports { + <# + .SYNOPSIS + Lists all available test reports from JSON files and database + + .FUNCTIONALITY + Entrypoint + + .ROLE + Tenant.Reports.Read + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $TriggerMetadata.FunctionName + Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -message 'Accessed this API' -Sev 'Debug' + + try { + # Get reports from JSON files in test folders + $FileReports = Get-ChildItem 'Modules\CIPPCore\Public\Tests\*\report.json' -ErrorAction SilentlyContinue | ForEach-Object { + try { + $ReportContent = Get-Content $_.FullName -Raw | ConvertFrom-Json + $FolderName = $_.Directory.Name + [PSCustomObject]@{ + id = $FolderName.ToLower() + name = $ReportContent.name ?? $FolderName + description = $ReportContent.description ?? '' + version = $ReportContent.version ?? '1.0' + source = 'file' + type = $FolderName + } + } catch { + Write-LogMessage -API $APIName -message "Error reading report.json from $($_.Directory.Name): $($_.Exception.Message)" -sev Warning + } + } + + # Get custom reports from CippReportTemplates table + $ReportTable = Get-CippTable -tablename 'CippReportTemplates' + $Filter = "PartitionKey eq 'Report'" + $CustomReports = Get-CIPPAzDataTableEntity @ReportTable -Filter $Filter + + $DatabaseReports = foreach ($Report in $CustomReports) { + [PSCustomObject]@{ + id = $Report.RowKey + name = $Report.Name ?? 'Custom Report' + description = $Report.Description ?? '' + version = $Report.Version ?? '1.0' + source = 'database' + type = 'custom' + } + } + + $Reports = @($FileReports) + @($DatabaseReports) + + $StatusCode = [HttpStatusCode]::OK + $Body = @($Reports) + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API $APIName -message "Error retrieving test reports: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + $StatusCode = [HttpStatusCode]::BadRequest + $Body = @{ Error = $ErrorMessage.NormalizedError } + } + + Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = ConvertTo-Json -InputObject $Body -Depth 10 -Compress + }) +} diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ListTests.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ListTests.ps1 index 8714963e0519..eb971b87e2a2 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ListTests.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ListTests.ps1 @@ -27,31 +27,62 @@ function Invoke-ListTests { $IdentityTotal = 0 $DevicesTotal = 0 + $IdentityTests = @() + $DevicesTests = @() if ($ReportId) { - $ReportTable = Get-CippTable -tablename 'CippReportTemplates' - $Filter = "PartitionKey eq 'ReportingTemplate' and RowKey eq '{0}'" -f $ReportId - $ReportTemplate = Get-CIPPAzDataTableEntity @ReportTable -Filter $Filter - - if ($ReportTemplate) { - $IdentityTests = @() - $DeviceTests = @() - - if ($ReportTemplate.identityTests) { - $IdentityTests = $ReportTemplate.identityTests | ConvertFrom-Json - $IdentityTotal = @($IdentityTests).Count + $ReportJsonFiles = Get-ChildItem 'Modules\CIPPCore\Public\Tests\*\report.json' -ErrorAction SilentlyContinue + $ReportFound = $false + + $MatchingReport = $ReportJsonFiles | Where-Object { $_.Directory.Name.ToLower() -eq $ReportId.ToLower() } | Select-Object -First 1 + + if ($MatchingReport) { + try { + $ReportContent = Get-Content $MatchingReport.FullName -Raw | ConvertFrom-Json + if ($ReportContent.IdentityTests) { + $IdentityTests = $ReportContent.IdentityTests + $IdentityTotal = @($IdentityTests).Count + } + if ($ReportContent.DevicesTests) { + $DevicesTests = $ReportContent.DevicesTests + $DevicesTotal = @($DevicesTests).Count + } + $ReportFound = $true + } catch { + Write-LogMessage -API $APIName -tenant $TenantFilter -message "Error reading report.json: $($_.Exception.Message)" -sev Warning } + } - if ($ReportTemplate.deviceTests) { - $DeviceTests = $ReportTemplate.deviceTests | ConvertFrom-Json - $DevicesTotal = @($DeviceTests).Count + # Fall back to database if not found in JSON files + if (-not $ReportFound) { + $ReportTable = Get-CippTable -tablename 'CippReportTemplates' + $Filter = "PartitionKey eq 'Report' and RowKey eq '{0}'" -f $ReportId + $ReportTemplate = Get-CIPPAzDataTableEntity @ReportTable -Filter $Filter + + if ($ReportTemplate) { + if ($ReportTemplate.identityTests) { + $IdentityTests = $ReportTemplate.identityTests | ConvertFrom-Json + $IdentityTotal = @($IdentityTests).Count + } + + if ($ReportTemplate.DevicesTests) { + $DevicesTests = $ReportTemplate.DevicesTests | ConvertFrom-Json + $DevicesTotal = @($DevicesTests).Count + } + $ReportFound = $true + } else { + Write-LogMessage -API $APIName -tenant $TenantFilter -message "Report template '$ReportId' not found" -sev Warning } + } - $AllReportTests = $IdentityTests + $DeviceTests - $FilteredTests = $TestResultsData.TestResults | Where-Object { $AllReportTests -contains $_.RowKey } + # Filter tests if report was found + if ($ReportFound) { + $AllReportTests = $IdentityTests + $DevicesTests + # Use HashSet for O(1) lookup performance + $TestLookup = [System.Collections.Generic.HashSet[string]]::new([string[]]$AllReportTests) + $FilteredTests = $TestResultsData.TestResults | Where-Object { $TestLookup.Contains($_.RowKey) } $TestResultsData.TestResults = @($FilteredTests) } else { - Write-LogMessage -API $APIName -tenant $TenantFilter -message "Report template '$ReportId' not found" -sev Warning $TestResultsData.TestResults = @() } } else { diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/report.json b/Modules/CIPPCore/Public/Tests/ZTNA/report.json index 2a5652da78fe..21d37a9b0dd3 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/report.json +++ b/Modules/CIPPCore/Public/Tests/ZTNA/report.json @@ -1,5 +1,115 @@ { "name": "Zero Trust Network Access Tests", "description": "Microsoft's Comprehensive security assessment covering identity and device compliance, conditional access policies, authentication methods, and endpoint protection aligned with Zero Trust principles.", - "version": "1.0" + "version": "1.0", + "IdentityTests": [ + "ZTNA21772", + "ZTNA21773", + "ZTNA21774", + "ZTNA21776", + "ZTNA21780", + "ZTNA21783", + "ZTNA21784", + "ZTNA21786", + "ZTNA21787", + "ZTNA21790", + "ZTNA21791", + "ZTNA21792", + "ZTNA21793", + "ZTNA21796", + "ZTNA21797", + "ZTNA21799", + "ZTNA21802", + "ZTNA21803", + "ZTNA21804", + "ZTNA21806", + "ZTNA21807", + "ZTNA21808", + "ZTNA21809", + "ZTNA21810", + "ZTNA21811", + "ZTNA21812", + "ZTNA21813", + "ZTNA21814", + "ZTNA21815", + "ZTNA21816", + "ZTNA21818", + "ZTNA21819", + "ZTNA21820", + "ZTNA21822", + "ZTNA21823", + "ZTNA21824", + "ZTNA21825", + "ZTNA21828", + "ZTNA21829", + "ZTNA21830", + "ZTNA21835", + "ZTNA21836", + "ZTNA21837", + "ZTNA21838", + "ZTNA21839", + "ZTNA21840", + "ZTNA21841", + "ZTNA21842", + "ZTNA21844", + "ZTNA21845", + "ZTNA21846", + "ZTNA21847", + "ZTNA21848", + "ZTNA21849", + "ZTNA21850", + "ZTNA21858", + "ZTNA21861", + "ZTNA21862", + "ZTNA21863", + "ZTNA21865", + "ZTNA21866", + "ZTNA21868", + "ZTNA21869", + "ZTNA21872", + "ZTNA21874", + "ZTNA21877", + "ZTNA21883", + "ZTNA21886", + "ZTNA21889", + "ZTNA21892", + "ZTNA21896", + "ZTNA21941", + "ZTNA21953", + "ZTNA21954", + "ZTNA21955", + "ZTNA21964", + "ZTNA21992", + "ZTNA22124", + "ZTNA22128", + "ZTNA22659", + "ZTNA24570", + "ZTNA24572", + "ZTNA24824", + "ZTNA24827" + ], + "DevicesTests": [ + "ZTNA24540", + "ZTNA24541", + "ZTNA24542", + "ZTNA24543", + "ZTNA24545", + "ZTNA24547", + "ZTNA24548", + "ZTNA24549", + "ZTNA24550", + "ZTNA24552", + "ZTNA24553", + "ZTNA24560", + "ZTNA24564", + "ZTNA24568", + "ZTNA24569", + "ZTNA24574", + "ZTNA24575", + "ZTNA24576", + "ZTNA24784", + "ZTNA24839", + "ZTNA24840", + "ZTNA24870" + ] } From f28596a7427636ca407b78fe310551b0ed125cfc Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Wed, 31 Dec 2025 01:42:54 +0100 Subject: [PATCH 059/503] small updates --- .../Entrypoints/HTTP Functions/Invoke-ListAvailableTests.ps1 | 1 - 1 file changed, 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ListAvailableTests.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ListAvailableTests.ps1 index 7f70261e1120..d047ec3bdeae 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ListAvailableTests.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ListAvailableTests.ps1 @@ -73,7 +73,6 @@ function Invoke-ListAvailableTests { $StatusCode = [HttpStatusCode]::OK } catch { $ErrorMessage = Get-CippException -Exception $_ - Write-LogMessage -user $Request.Headers.'x-ms-client-principal' -API $APIName -message "Failed to list available tests: $($ErrorMessage.NormalizedError)" -Sev 'Error' -LogData $ErrorMessage $Body = [PSCustomObject]@{ Results = "Failed to list available tests: $($ErrorMessage.NormalizedError)" } From 98469300f5cd6a6b8779e7a44b65f14f5fe6864f Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Wed, 31 Dec 2025 02:22:58 +0100 Subject: [PATCH 060/503] add autocleanup --- Modules/CIPPCore/Public/Add-CIPPDbItem.ps1 | 8 +++++++- .../Entrypoints/HTTP Functions/Invoke-ListTests.ps1 | 5 +++++ Modules/CIPPCore/Public/Get-CIPPDbItem.ps1 | 1 + 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Add-CIPPDbItem.ps1 b/Modules/CIPPCore/Public/Add-CIPPDbItem.ps1 index 51ea3c530f0c..212447983b51 100644 --- a/Modules/CIPPCore/Public/Add-CIPPDbItem.ps1 +++ b/Modules/CIPPCore/Public/Add-CIPPDbItem.ps1 @@ -42,6 +42,12 @@ function Add-CIPPDbItem { try { $Table = Get-CippTable -tablename 'CippReportingDB' + #Get the existing type entries and nuke them. This ensures we don't have stale data. + $Filter = "PartitionKey eq '{0}' and RowKey ge '{1}-' and RowKey lt '{1}0'" -f $TenantFilter, $Type + $ExistingEntities = Get-CIPPAzDataTableEntity @Table -Filter $Filter + if ($ExistingEntities) { + Remove-AzDataTableEntity @Table -Entity $ExistingEntities -Force | Out-Null + } if ($Count) { $Entity = @{ @@ -54,7 +60,7 @@ function Add-CIPPDbItem { } else { $Entities = foreach ($Item in $Data) { - $ItemId = $Item.id + $ItemId = $Item.id ? $Item.id : $item.skuId @{ PartitionKey = $TenantFilter RowKey = "$Type-$ItemId" diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ListTests.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ListTests.ps1 index eb971b87e2a2..77465500552b 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ListTests.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ListTests.ps1 @@ -121,6 +121,11 @@ function Invoke-ListTests { $TestResultsData | Add-Member -NotePropertyName 'MFAState' -NotePropertyValue $MFAStateData -Force } + $LicenseData = New-CIPPDbRequest -TenantFilter $TenantFilter -Type 'LicenseOverview' + if ($LicenseData) { + $TestResultsData | Add-Member -NotePropertyName 'LicenseData' -NotePropertyValue @($LicenseData) -Force + } + $StatusCode = [HttpStatusCode]::OK $Body = $TestResultsData diff --git a/Modules/CIPPCore/Public/Get-CIPPDbItem.ps1 b/Modules/CIPPCore/Public/Get-CIPPDbItem.ps1 index d140210b0065..3a0ace7339f6 100644 --- a/Modules/CIPPCore/Public/Get-CIPPDbItem.ps1 +++ b/Modules/CIPPCore/Public/Get-CIPPDbItem.ps1 @@ -55,3 +55,4 @@ function Get-CIPPDbItem { throw } } + From 73a43114af5386df9b8eb5ab583c6b18cc37e0f2 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 1 Jan 2026 11:02:58 -0500 Subject: [PATCH 061/503] Improve scheduled task handling and rerun protection Adds duplicate RowKey checks to prevent race conditions when creating scheduled tasks. Enhances rerun protection logic in Push-ExecScheduledCommand to avoid duplicate executions within recurrence intervals. Refines orchestrator task state transitions and filtering for stuck tasks. Improves logging and filtering for scheduled item listing, and updates Test-CIPPRerun to support custom intervals and base times for scheduled tasks. --- .../CIPPCore/Public/Add-CIPPScheduledTask.ps1 | 21 +++++++--- .../Push-ExecScheduledCommand.ps1 | 42 +++++++++++++++++++ .../CIPP/Core/Invoke-ExecAppInsightsQuery.ps1 | 5 +-- .../Scheduler/Invoke-AddScheduledItem.ps1 | 2 +- .../Scheduler/Invoke-ListScheduledItems.ps1 | 11 +++-- .../Start-UserTasksOrchestrator.ps1 | 12 ++++-- Modules/CIPPCore/Public/Test-CIPPRerun.ps1 | 22 +++++++--- 7 files changed, 94 insertions(+), 21 deletions(-) diff --git a/Modules/CIPPCore/Public/Add-CIPPScheduledTask.ps1 b/Modules/CIPPCore/Public/Add-CIPPScheduledTask.ps1 index fc6f62033e82..d309ac2c9681 100644 --- a/Modules/CIPPCore/Public/Add-CIPPScheduledTask.ps1 +++ b/Modules/CIPPCore/Public/Add-CIPPScheduledTask.ps1 @@ -46,6 +46,20 @@ function Add-CIPPScheduledTask { return "Could not run task: $ErrorMessage" } } else { + # Generate RowKey early to use in duplicate check + if (!$Task.RowKey) { + $RowKey = (New-Guid).Guid + } else { + $RowKey = $Task.RowKey + } + + # Check for duplicate RowKey (prevents race conditions) + $Filter = "PartitionKey eq 'ScheduledTask' and RowKey eq '$RowKey'" + $ExistingTaskByKey = (Get-CIPPAzDataTableEntity @Table -Filter $Filter) + if ($ExistingTaskByKey) { + return "Task with ID $RowKey already exists" + } + if ($DisallowDuplicateName) { $Filter = "PartitionKey eq 'ScheduledTask' and Name eq '$($Task.Name)'" $ExistingTask = (Get-CIPPAzDataTableEntity @Table -Filter $Filter) @@ -110,11 +124,8 @@ function Add-CIPPScheduledTask { } $AdditionalProperties = ([PSCustomObject]$AdditionalProperties | ConvertTo-Json -Compress) if ($Parameters -eq 'null') { $Parameters = '' } - if (!$Task.RowKey) { - $RowKey = (New-Guid).Guid - } else { - $RowKey = $Task.RowKey - } + + # RowKey already generated during duplicate check above $Recurrence = if ([string]::IsNullOrEmpty($task.Recurrence.value)) { $task.Recurrence diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecScheduledCommand.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecScheduledCommand.ps1 index d199f7c5ada2..474ead168783 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecScheduledCommand.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecScheduledCommand.ps1 @@ -35,6 +35,48 @@ function Push-ExecScheduledCommand { Remove-Variable -Name ScheduledTaskId -Scope Script -ErrorAction SilentlyContinue return } + # Task should be 'Pending' (queued by orchestrator) or 'Running' (retry/recovery) + # We accept both to handle edge cases + + # Check for rerun protection - prevent duplicate executions within the recurrence interval + if ($task.Recurrence -and $task.Recurrence -ne '0') { + # Calculate interval in seconds from recurrence string + $IntervalSeconds = switch -Regex ($task.Recurrence) { + '^(\d+)$' { [int64]$matches[1] * 86400 } # Plain number = days + '(\d+)m$' { [int64]$matches[1] * 60 } + '(\d+)h$' { [int64]$matches[1] * 3600 } + '(\d+)d$' { [int64]$matches[1] * 86400 } + default { 0 } + } + + if ($IntervalSeconds -gt 0) { + # Round down to nearest 15-minute interval (900 seconds) since that's when orchestrator runs + # This prevents rerun blocking issues due to slight timing variations + $FifteenMinutes = 900 + $AdjustedInterval = [Math]::Floor($IntervalSeconds / $FifteenMinutes) * $FifteenMinutes + + # Ensure we have at least one 15-minute interval + if ($AdjustedInterval -lt $FifteenMinutes) { + $AdjustedInterval = $FifteenMinutes + } + # Use task RowKey as API identifier for rerun cache + $RerunParams = @{ + TenantFilter = $Tenant + Type = 'ScheduledTask' + API = $task.RowKey + Interval = $AdjustedInterval + BaseTime = [int64]$task.ScheduledTime + Headers = $Headers + } + + $IsRerun = Test-CIPPRerun @RerunParams + if ($IsRerun) { + Write-Information "Scheduled task $($task.Name) for tenant $Tenant was recently executed. Skipping to prevent duplicate execution." + Remove-Variable -Name ScheduledTaskId -Scope Script -ErrorAction SilentlyContinue + return + } + } + } if ($task.Trigger) { # Extract trigger data from the task and process diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ExecAppInsightsQuery.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ExecAppInsightsQuery.ps1 index 23868530621f..298d948ba283 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ExecAppInsightsQuery.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ExecAppInsightsQuery.ps1 @@ -24,13 +24,12 @@ function Invoke-ExecAppInsightsQuery { try { $LogData = Get-ApplicationInsightsQuery -Query $Query - $Body = @{ + $Body = ConvertTo-Json -Depth 10 -Compress -InputObject @{ Results = @($LogData) Metadata = @{ Query = $Query } - } | ConvertTo-Json -Depth 10 -Compress - + } return [HttpResponseContext]@{ StatusCode = [HttpStatusCode]::OK Body = $Body diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Scheduler/Invoke-AddScheduledItem.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Scheduler/Invoke-AddScheduledItem.ps1 index 17f35f5adf12..048c881f4a9e 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Scheduler/Invoke-AddScheduledItem.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Scheduler/Invoke-AddScheduledItem.ps1 @@ -32,7 +32,7 @@ function Invoke-AddScheduledItem { $ScheduledTask = @{ Task = $Request.Body Headers = $Request.Headers - hidden = $hidden + Hidden = $hidden DisallowDuplicateName = $Request.Query.DisallowDuplicateName DesiredStartTime = $Request.Body.DesiredStartTime } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Scheduler/Invoke-ListScheduledItems.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Scheduler/Invoke-ListScheduledItems.ps1 index edc9691e2c61..37ae04aa3a21 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Scheduler/Invoke-ListScheduledItems.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Scheduler/Invoke-ListScheduledItems.ps1 @@ -22,9 +22,9 @@ function Invoke-ListScheduledItems { $SearchTitle = $Request.query.SearchTitle ?? $Request.body.SearchTitle if ($ShowHidden -eq $true) { - $ScheduledItemFilter.Add('Hidden eq true') + $ScheduledItemFilter.Add("(Hidden eq true or Hidden eq 'True')") } else { - $ScheduledItemFilter.Add('Hidden eq false') + $ScheduledItemFilter.Add("(Hidden eq false or Hidden eq 'False')") } if ($Name) { @@ -43,6 +43,7 @@ function Invoke-ListScheduledItems { $HiddenTasks = $true } $Tasks = Get-CIPPAzDataTableEntity @Table -Filter $Filter + Write-Information "Retrieved $($Tasks.Count) scheduled tasks from storage." if ($Type) { $Tasks = $Tasks | Where-Object { $_.command -eq $Type } } @@ -58,8 +59,12 @@ function Invoke-ListScheduledItems { $AllowedTenantDomains = $TenantList | Where-Object -Property customerId -In $AllowedTenants | Select-Object -ExpandProperty defaultDomainName $Tasks = $Tasks | Where-Object -Property Tenant -In $AllowedTenantDomains } - $ScheduledTasks = foreach ($Task in $tasks) { + + Write-Information "Found $($Tasks.Count) scheduled tasks after filtering and access check." + + $ScheduledTasks = foreach ($Task in $Tasks) { if (!$Task.Tenant -or !$Task.Command) { + Write-Information "Skipping invalid scheduled task entry: $($Task.RowKey)" continue } diff --git a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-UserTasksOrchestrator.ps1 b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-UserTasksOrchestrator.ps1 index d635141637dc..272c78ee2627 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-UserTasksOrchestrator.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-UserTasksOrchestrator.ps1 @@ -10,8 +10,11 @@ function Start-UserTasksOrchestrator { param() $Table = Get-CippTable -tablename 'ScheduledTasks' - $1HourAgo = (Get-Date).AddHours(-1).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ') - $Filter = "PartitionKey eq 'ScheduledTask' and (TaskState eq 'Planned' or TaskState eq 'Failed - Planned' or (TaskState eq 'Running' and Timestamp lt datetime'$1HourAgo'))" + $30MinutesAgo = (Get-Date).AddMinutes(-30).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ') + $4HoursAgo = (Get-Date).AddHours(-4).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ') + # Pending = orchestrator queued, Running = actively executing + # Pick up: Planned, Failed-Planned, stuck Pending (>30min), or stuck Running (>4hr for large AllTenants tasks) + $Filter = "PartitionKey eq 'ScheduledTask' and (TaskState eq 'Planned' or TaskState eq 'Failed - Planned' or (TaskState eq 'Pending' and Timestamp lt datetime'$30MinutesAgo') or (TaskState eq 'Running' and Timestamp lt datetime'$4HoursAgo'))" $tasks = Get-CIPPAzDataTableEntity @Table -Filter $Filter $RateLimitTable = Get-CIPPTable -tablename 'SchedulerRateLimits' @@ -49,11 +52,14 @@ function Start-UserTasksOrchestrator { $currentUnixTime = [int64](([datetime]::UtcNow) - (Get-Date '1/1/1970')).TotalSeconds if ($currentUnixTime -ge $task.ScheduledTime) { try { + # Update task state to 'Pending' immediately to prevent concurrent orchestrator runs from picking it up + # 'Pending' = orchestrator has picked it up and is queuing commands + # 'Running' = actual execution is happening (set by Push-ExecScheduledCommand) $null = Update-AzDataTableEntity -Force @Table -Entity @{ PartitionKey = $task.PartitionKey RowKey = $task.RowKey ExecutedTime = "$currentUnixTime" - TaskState = 'Planned' + TaskState = 'Pending' } $task.Parameters = $task.Parameters | ConvertFrom-Json -AsHashtable $task.AdditionalProperties = $task.AdditionalProperties | ConvertFrom-Json diff --git a/Modules/CIPPCore/Public/Test-CIPPRerun.ps1 b/Modules/CIPPCore/Public/Test-CIPPRerun.ps1 index a329eb0a3d6a..e09d6c8cfa36 100644 --- a/Modules/CIPPCore/Public/Test-CIPPRerun.ps1 +++ b/Modules/CIPPCore/Public/Test-CIPPRerun.ps1 @@ -7,15 +7,25 @@ function Test-CIPPRerun { $Settings, $Headers, [switch]$Clear, - [switch]$ClearAll + [switch]$ClearAll, + [int64]$Interval = 0, # Custom interval in seconds (for scheduled tasks) + [int64]$BaseTime = 0 # Base time to calculate from (defaults to current time) ) $RerunTable = Get-CIPPTable -tablename 'RerunCache' - $EstimatedDifference = switch ($Type) { - 'Standard' { 9800 } # 2 hours 45 minutes ish. - 'BPA' { 85000 } # 24 hours ish. - default { throw "Unknown type: $Type" } + + # Use custom interval if provided, otherwise use type-based defaults + if ($Interval -gt 0) { + $EstimatedDifference = $Interval + } else { + $EstimatedDifference = switch ($Type) { + 'Standard' { 9800 } # 2 hours 45 minutes ish. + 'BPA' { 85000 } # 24 hours ish. + default { throw "Unknown type: $Type" } + } } - $CurrentUnixTime = [int][double]::Parse((Get-Date -UFormat %s)) + + # Use BaseTime if provided, otherwise use current time + $CurrentUnixTime = if ($BaseTime -gt 0) { $BaseTime } else { [int][double]::Parse((Get-Date -UFormat %s)) } $EstimatedNextRun = $CurrentUnixTime + $EstimatedDifference try { From acc1dba4518b122e7861a9a7351d7d6690b07c05 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Thu, 1 Jan 2026 19:46:22 +0100 Subject: [PATCH 062/503] push db cache --- .../Push-CIPPDBCacheData.ps1 | 2 +- .../Tenant/Tests/Invoke-CIPPTestsRun.ps1 | 2 +- .../Start-CIPPDBCacheOrchestrator.ps1 | 6 +- .../Timer Functions/Start-DurableCleanup.ps1 | 78 +------------------ Modules/CIPPCore/Public/Get-CIPPDbItem.ps1 | 8 +- 5 files changed, 12 insertions(+), 84 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 index 6152e6e2d2ce..4dbb7b000423 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 @@ -11,7 +11,7 @@ function Push-CIPPDBCacheData { #> [CmdletBinding()] param($Item) - + Write-Host "Starting cache collection for tenant: $($Item.TenantFilter) - Queue: $($Item.QueueName) (ID: $($Item.QueueId))" $TenantFilter = $Item.TenantFilter #This collects all data for a tenant and caches it in the CIPP Reporting database. DO NOT ADD PROCESSING OR LOGIC HERE. #The point of this file is to always be <10 minutes execution time. diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Tests/Invoke-CIPPTestsRun.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Tests/Invoke-CIPPTestsRun.ps1 index 079df93388fc..8eb2d177b2c9 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Tests/Invoke-CIPPTestsRun.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Tests/Invoke-CIPPTestsRun.ps1 @@ -25,7 +25,7 @@ function Invoke-CIPPTestsRun { Write-Information "Found $($AllTests.Count) test functions to run" $AllTenantsList = if ($TenantFilter -eq 'allTenants') { - $DbCounts = Get-CIPPDbItem -CountsOnly + $DbCounts = Get-CIPPDbItem -CountsOnly -TenantFilter 'allTenants' $TenantsWithData = $DbCounts | Where-Object { $_.Count -gt 0 } | Select-Object -ExpandProperty PartitionKey -Unique Write-Information "Found $($TenantsWithData.Count) tenants with data in database" $TenantsWithData diff --git a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-CIPPDBCacheOrchestrator.ps1 b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-CIPPDBCacheOrchestrator.ps1 index f85bc02358b9..bf14f0685005 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-CIPPDBCacheOrchestrator.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-CIPPDBCacheOrchestrator.ps1 @@ -14,7 +14,7 @@ function Start-CIPPDBCacheOrchestrator { try { Write-LogMessage -API 'CIPPDBCache' -message 'Starting database cache orchestration' -sev Info - + Write-Host 'Starting database cache orchestration' $TenantList = Get-Tenants | Where-Object { $_.defaultDomainName -ne $null } if ($TenantList.Count -eq 0) { @@ -23,7 +23,6 @@ function Start-CIPPDBCacheOrchestrator { } $Queue = New-CippQueueEntry -Name 'Database Cache Collection' -TotalTasks $TenantList.Count - $Batch = foreach ($Tenant in $TenantList) { [PSCustomObject]@{ FunctionName = 'Push-CIPPDBCacheData' @@ -32,7 +31,8 @@ function Start-CIPPDBCacheOrchestrator { QueueName = "DB Cache - $($Tenant.defaultDomainName)" } } - + Write-Host "Created queue $($Queue.RowKey) for database cache collection of $($TenantList.Count) tenants" + Write-Host "Starting batch of $($Batch.Count) cache collection activities" $InputObject = [PSCustomObject]@{ Batch = @($Batch) OrchestratorName = 'CIPPDBCacheOrchestrator' diff --git a/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-DurableCleanup.ps1 b/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-DurableCleanup.ps1 index 64e175b72e8c..454047a96d65 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-DurableCleanup.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-DurableCleanup.ps1 @@ -17,81 +17,5 @@ function Start-DurableCleanup { param( [int]$MaxDuration = 86400 ) - - $WarningPreference = 'SilentlyContinue' - $StorageContext = New-AzStorageContext -ConnectionString $env:AzureWebJobsStorage - $TargetTime = (Get-Date).ToUniversalTime().AddSeconds(-$MaxDuration) - $Context = New-AzDataTableContext -ConnectionString $env:AzureWebJobsStorage - $InstancesTables = Get-AzDataTable -Context $Context | Where-Object { $_ -match 'Instances' } - - $CleanupCount = 0 - $QueueCount = 0 - - $FunctionsWithLongRunningOrchestrators = [System.Collections.Generic.List[object]]::new() - $NonDeterministicOrchestrators = [System.Collections.Generic.List[object]]::new() - - foreach ($Table in $InstancesTables) { - $Table = Get-CippTable -TableName $Table - $FunctionName = $Table.TableName -replace 'Instances', '' - $Orchestrators = Get-CIPPAzDataTableEntity @Table -Filter "RuntimeStatus eq 'Running'" | Select-Object * -ExcludeProperty Input - $Queues = Get-AzStorageQueue -Context $StorageContext -Name ('{0}*' -f $FunctionName) | Select-Object -Property Name, ApproximateMessageCount, QueueClient - $LongRunningOrchestrators = $Orchestrators | Where-Object { $_.CreatedTime.DateTime -lt $TargetTime } - - if ($LongRunningOrchestrators.Count -gt 0) { - $FunctionsWithLongRunningOrchestrators.Add(@{'FunctionName' = $FunctionName }) - foreach ($Orchestrator in $LongRunningOrchestrators) { - $CreatedTime = [DateTime]::SpecifyKind($Orchestrator.CreatedTime.DateTime, [DateTimeKind]::Utc) - $TimeSpan = New-TimeSpan -Start $CreatedTime -End (Get-Date).ToUniversalTime() - $RunningDuration = [math]::Round($TimeSpan.TotalMinutes, 2) - Write-Information "Orchestrator: $($Orchestrator.PartitionKey), created: $CreatedTime, running for: $RunningDuration minutes" - if ($PSCmdlet.ShouldProcess($_.PartitionKey, 'Terminate Orchestrator')) { - $Orchestrator = Get-CIPPAzDataTableEntity @Table -Filter "PartitionKey eq '$($Orchestrator.PartitionKey)'" - $Orchestrator.RuntimeStatus = 'Failed' - if ($Orchestrator.PSObject.Properties.Name -contains 'CustomStatus') { - $Orchestrator.CustomStatus = "Terminated by Durable Cleanup - Exceeded max duration of $MaxDuration seconds" - } else { - $Orchestrator | Add-Member -MemberType NoteProperty -Name CustomStatus -Value "Terminated by Durable Cleanup - Exceeded max duration of $MaxDuration seconds" - } - Update-AzDataTableEntity @Table -Entity $Orchestrator - $CleanupCount++ - } - } - } - - $NonDeterministicOrchestrators = $Orchestrators | Where-Object { $_.Output -match 'Non-Deterministic workflow detected' } - if ($NonDeterministicOrchestrators.Count -gt 0) { - $NonDeterministicOrchestrators.Add(@{'FunctionName' = $FunctionName }) - foreach ($Orchestrator in $NonDeterministicOrchestrators) { - Write-Information "Orchestrator: $($Orchestrator.PartitionKey) is Non-Deterministic" - if ($PSCmdlet.ShouldProcess($_.PartitionKey, 'Terminate Orchestrator')) { - $Orchestrator = Get-CIPPAzDataTableEntity @Table -Filter "PartitionKey eq '$($Orchestrator.PartitionKey)'" - $Orchestrator.RuntimeStatus = 'Failed' - if ($Orchestrator.PSObject.Properties.Name -contains 'CustomStatus') { - $Orchestrator.CustomStatus = 'Terminated by Durable Cleanup - Non-Deterministic workflow detected' - } else { - $Orchestrator | Add-Member -MemberType NoteProperty -Name CustomStatus -Value 'Terminated by Durable Cleanup - Non-Deterministic workflow detected' - } - Update-AzDataTableEntity @Table -Entity $Orchestrator - $CleanupCount++ - } - } - } - - if (($LongRunningOrchestrators.Count -gt 0 -or $NonDeterministicOrchestrators.Count -gt 0) -and $Queues.ApproximateMessageCount -gt 0) { - $RunningQueues = $Queues | Where-Object { $_.ApproximateMessageCount -gt 0 } - foreach ($Queue in $RunningQueues) { - Write-Information "- Removing queue: $($Queue.Name), message count: $($Queue.ApproximateMessageCount)" - if ($PSCmdlet.ShouldProcess($Queue.Name, 'Clear Queue')) { - $Queue.QueueClient.ClearMessagesAsync() | Out-Null - } - $QueueCount++ - } - } - } - - if ($CleanupCount -gt 0 -or $QueueCount -gt 0) { - Write-LogMessage -api 'Durable Cleanup' -message "$CleanupCount orchestrators were terminated. $QueueCount queues were cleared." -sev 'Info' -LogData $FunctionsWithLongRunningOrchestrators - } - - Write-Information "Durable cleanup complete. $CleanupCount orchestrators were terminated. $QueueCount queues were cleared." + Write-Information "This cleanup is no longer required." } diff --git a/Modules/CIPPCore/Public/Get-CIPPDbItem.ps1 b/Modules/CIPPCore/Public/Get-CIPPDbItem.ps1 index 3a0ace7339f6..4ec9970979e0 100644 --- a/Modules/CIPPCore/Public/Get-CIPPDbItem.ps1 +++ b/Modules/CIPPCore/Public/Get-CIPPDbItem.ps1 @@ -23,7 +23,7 @@ function Get-CIPPDbItem { #> [CmdletBinding()] param( - [Parameter(Mandatory = $true)] + [Parameter(Mandatory = $false)] [string]$TenantFilter, [Parameter(Mandatory = $false)] @@ -37,7 +37,11 @@ function Get-CIPPDbItem { $Table = Get-CippTable -tablename 'CippReportingDB' if ($CountsOnly) { - $Filter = "PartitionKey eq '{0}'" -f $TenantFilter + if ($TenantFilter -eq 'allTenants') { + $Filter = $null + } else { + $Filter = "PartitionKey eq '{0}'" -f $TenantFilter + } $Results = Get-CIPPAzDataTableEntity @Table -Filter $Filter $Results = $Results | Where-Object { $_.RowKey -like '*-Count' } } else { From 52091ef3638c78536af905a80ab70f3c2b472d9a Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Thu, 1 Jan 2026 22:12:13 +0100 Subject: [PATCH 063/503] data collection fix --- .../Orchestrator Functions/Start-CIPPDBCacheOrchestrator.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-CIPPDBCacheOrchestrator.ps1 b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-CIPPDBCacheOrchestrator.ps1 index bf14f0685005..e6dc730a762c 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-CIPPDBCacheOrchestrator.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-CIPPDBCacheOrchestrator.ps1 @@ -25,7 +25,7 @@ function Start-CIPPDBCacheOrchestrator { $Queue = New-CippQueueEntry -Name 'Database Cache Collection' -TotalTasks $TenantList.Count $Batch = foreach ($Tenant in $TenantList) { [PSCustomObject]@{ - FunctionName = 'Push-CIPPDBCacheData' + FunctionName = 'CIPPDBCacheData' TenantFilter = $Tenant.defaultDomainName QueueId = $Queue.RowKey QueueName = "DB Cache - $($Tenant.defaultDomainName)" From 8f6543ece141a5d9b276735b356332197fa3f175 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Fri, 2 Jan 2026 02:22:34 +0100 Subject: [PATCH 064/503] test results --- Modules/CIPPCore/Public/Get-CIPPTestResults.ps1 | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/Modules/CIPPCore/Public/Get-CIPPTestResults.ps1 b/Modules/CIPPCore/Public/Get-CIPPTestResults.ps1 index 43f91abb9aca..312236c81c4d 100644 --- a/Modules/CIPPCore/Public/Get-CIPPTestResults.ps1 +++ b/Modules/CIPPCore/Public/Get-CIPPTestResults.ps1 @@ -28,12 +28,7 @@ function Get-CIPPTestResults { foreach ($CountRow in $CountData) { $TypeName = $CountRow.RowKey -replace '-Count$', '' $TenantCounts[$TypeName] = $CountRow.DataCount - - if ($CountRow.Timestamp) { - if (-not $LatestTimestamp -or $CountRow.Timestamp -gt $LatestTimestamp) { - $LatestTimestamp = $CountRow.Timestamp - } - } + $LatestTimestamp = $CountRow.Timestamp } return [PSCustomObject]@{ From de77f6158244a47adbf9851b832bd0c7b372fc73 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 1 Jan 2026 20:28:22 -0500 Subject: [PATCH 065/503] Refactor backup to use blob storage and enhance restore Backups are now stored as blobs in Azure Storage with table entities referencing the blob URLs, improving scalability and performance. The backup listing, creation, and retention cleanup functions have been updated to handle blob-based backups, including proper cleanup of both blob files and table entries. Restore logic is enhanced to fetch and parse blob content, and restoration tasks now provide more detailed feedback and error handling. These changes modernize the backup/restore pipeline and improve reliability for large backup data. --- .../CIPP/Core/Invoke-ExecListBackup.ps1 | 13 +- .../Settings/Invoke-ExecRestoreBackup.ps1 | 21 +- .../Start-BackupRetentionCleanup.ps1 | 138 ++++++-- Modules/CIPPCore/Public/Get-CIPPBackup.ps1 | 63 +++- Modules/CIPPCore/Public/New-CIPPBackup.ps1 | 238 ++++++++----- Modules/CIPPCore/Public/New-CIPPRestore.ps1 | 3 +- .../CIPPCore/Public/New-CIPPRestoreTask.ps1 | 334 +++++++++++++----- Modules/CIPPCore/Public/Test-CIPPRerun.ps1 | 4 +- 8 files changed, 610 insertions(+), 204 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ExecListBackup.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ExecListBackup.ps1 index d4cdfab364b2..0142225044e0 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ExecListBackup.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ExecListBackup.ps1 @@ -31,11 +31,22 @@ function Invoke-ExecListBackup { Items = $properties.Name } } else { + # Prefer stored indicator (BackupIsBlob) to avoid reading Backup field + $isBlob = $false + if ($null -ne $item.PSObject.Properties['BackupIsBlob']) { + try { $isBlob = [bool]$item.BackupIsBlob } catch { $isBlob = $false } + } else { + # Fallback heuristic for legacy rows if property missing + if ($null -ne $item.PSObject.Properties['Backup']) { + $b = $item.Backup + if ($b -is [string] -and ($b -like 'https://*' -or $b -like 'http://*')) { $isBlob = $true } + } + } [PSCustomObject]@{ BackupName = $item.RowKey Timestamp = $item.Timestamp + Source = if ($isBlob) { 'blob' } else { 'table' } } - } } $Result = $Processed | Sort-Object Timestamp -Descending diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecRestoreBackup.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecRestoreBackup.ps1 index 37f08dcd22de..61a133046e41 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecRestoreBackup.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecRestoreBackup.ps1 @@ -12,13 +12,26 @@ function Invoke-ExecRestoreBackup { try { if ($Request.Body.BackupName -like 'CippBackup_*') { - $Table = Get-CippTable -tablename 'CIPPBackup' - $Backup = Get-CippAzDataTableEntity @Table -Filter "RowKey eq '$($Request.Body.BackupName)' or OriginalEntityId eq '$($Request.Body.BackupName)'" + # Use Get-CIPPBackup which already handles fetching from blob storage + $Backup = Get-CIPPBackup -Type 'CIPP' -Name $Request.Body.BackupName if ($Backup) { - $BackupData = $Backup.Backup | ConvertFrom-Json -ErrorAction SilentlyContinue | Select-Object * -ExcludeProperty ETag, Timestamp + $raw = $Backup.Backup + $BackupData = $null + + # Get-CIPPBackup already fetches blob content, so raw should be JSON string + try { + if ($raw -is [string]) { + $BackupData = $raw | ConvertFrom-Json -ErrorAction Stop + } else { + $BackupData = $raw | Select-Object * -ExcludeProperty ETag, Timestamp + } + } catch { + throw "Failed to parse backup JSON: $($_.Exception.Message)" + } + $BackupData | ForEach-Object { $Table = Get-CippTable -tablename $_.table - $ht2 = @{ } + $ht2 = @{} $_.psobject.properties | ForEach-Object { $ht2[$_.Name] = [string]$_.Value } $Table.Entity = $ht2 Add-CIPPAzDataTableEntity @Table -Force diff --git a/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-BackupRetentionCleanup.ps1 b/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-BackupRetentionCleanup.ps1 index 9a70515884a4..ed00a0292aa1 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-BackupRetentionCleanup.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-BackupRetentionCleanup.ps1 @@ -13,12 +13,12 @@ function Start-BackupRetentionCleanup { $ConfigTable = Get-CippTable -tablename Config $Filter = "PartitionKey eq 'BackupRetention' and RowKey eq 'Settings'" $RetentionSettings = Get-CIPPAzDataTableEntity @ConfigTable -Filter $Filter - + # Default to 30 days if not set - $RetentionDays = if ($RetentionSettings.RetentionDays) { - [int]$RetentionSettings.RetentionDays - } else { - 30 + $RetentionDays = if ($RetentionSettings.RetentionDays) { + [int]$RetentionSettings.RetentionDays + } else { + 30 } # Ensure minimum retention of 7 days @@ -27,24 +27,67 @@ function Start-BackupRetentionCleanup { } Write-Host "Starting backup cleanup with retention of $RetentionDays days" - + # Calculate cutoff date $CutoffDate = (Get-Date).AddDays(-$RetentionDays).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ') - + $DeletedCounts = [System.Collections.Generic.List[int]]::new() # Clean up CIPP Backups if ($PSCmdlet.ShouldProcess('CIPPBackup', 'Cleaning up old backups')) { $CIPPBackupTable = Get-CippTable -tablename 'CIPPBackup' - $Filter = "PartitionKey eq 'CIPPBackup' and Timestamp lt datetime'$CutoffDate'" - - $OldCIPPBackups = Get-AzDataTableEntity @CIPPBackupTable -Filter $Filter -Property @('PartitionKey', 'RowKey', 'ETag') - - if ($OldCIPPBackups) { - Write-Host "Found $($OldCIPPBackups.Count) old CIPP backups to delete" - Remove-AzDataTableEntity @CIPPBackupTable -Entity $OldCIPPBackups -Force - $DeletedCounts.Add($OldCIPPBackups.Count) - Write-LogMessage -API 'BackupRetentionCleanup' -message "Deleted $($OldCIPPBackups.Count) old CIPP backups" -Sev 'Info' + $CutoffFilter = "PartitionKey eq 'CIPPBackup' and Timestamp lt datetime'$CutoffDate'" + + # Delete blob files + $BlobFilter = "$CutoffFilter and BackupIsBlob eq true" + $BlobBackups = Get-AzDataTableEntity @CIPPBackupTable -Filter $BlobFilter -Property @('PartitionKey', 'RowKey', 'Backup') + + $BlobDeletedCount = 0 + if ($BlobBackups) { + foreach ($Backup in $BlobBackups) { + if ($Backup.Backup) { + try { + $BlobPath = $Backup.Backup + # Extract container/blob path from URL + if ($BlobPath -like '*:10000/*') { + # Azurite format: http://host:10000/devstoreaccount1/container/blob + $parts = $BlobPath -split ':10000/' + if ($parts.Count -gt 1) { + # Remove account name to get container/blob + $BlobPath = ($parts[1] -split '/', 2)[-1] + } + } elseif ($BlobPath -like '*blob.core.windows.net/*') { + # Azure Storage format: https://account.blob.core.windows.net/container/blob + $BlobPath = ($BlobPath -split '.blob.core.windows.net/', 2)[-1] + } + $null = New-CIPPAzStorageRequest -Service 'blob' -Resource $BlobPath -Method 'DELETE' -ConnectionString $ConnectionString + $BlobDeletedCount++ + Write-Host "Deleted blob: $BlobPath" + } catch { + Write-LogMessage -API 'BackupRetentionCleanup' -message "Failed to delete blob $($Backup.Backup): $($_.Exception.Message)" -Sev 'Warning' + } + } + } + # Delete blob table entities + Remove-AzDataTableEntity @CIPPBackupTable -Entity $BlobBackups -Force + } + + # Delete table-only backups (no blobs) + # Fetch all old entries and filter out blob entries client-side (null check is unreliable in filters) + $AllOldBackups = Get-AzDataTableEntity @CIPPBackupTable -Filter $CutoffFilter -Property @('PartitionKey', 'RowKey', 'ETag', 'BackupIsBlob') + $TableBackups = $AllOldBackups | Where-Object { $_.BackupIsBlob -ne $true } + + $TableDeletedCount = 0 + if ($TableBackups) { + Remove-AzDataTableEntity @CIPPBackupTable -Entity $TableBackups -Force + $TableDeletedCount = ($TableBackups | Measure-Object).Count + } + + $TotalDeleted = $BlobDeletedCount + $TableDeletedCount + if ($TotalDeleted -gt 0) { + $DeletedCounts.Add($TotalDeleted) + Write-LogMessage -API 'BackupRetentionCleanup' -message "Deleted $TotalDeleted old CIPP backups ($BlobDeletedCount blobs, $TableDeletedCount table entries)" -Sev 'Info' + Write-Host "Deleted $TotalDeleted old CIPP backups" } else { Write-Host 'No old CIPP backups found' } @@ -53,15 +96,58 @@ function Start-BackupRetentionCleanup { # Clean up Scheduled/Tenant Backups if ($PSCmdlet.ShouldProcess('ScheduledBackup', 'Cleaning up old backups')) { $ScheduledBackupTable = Get-CippTable -tablename 'ScheduledBackup' - $Filter = "PartitionKey eq 'ScheduledBackup' and Timestamp lt datetime'$CutoffDate'" - - $OldScheduledBackups = Get-AzDataTableEntity @ScheduledBackupTable -Filter $Filter -Property @('PartitionKey', 'RowKey', 'ETag') - - if ($OldScheduledBackups) { - Write-Host "Found $($OldScheduledBackups.Count) old tenant backups to delete" - Remove-AzDataTableEntity @ScheduledBackupTable -Entity $OldScheduledBackups -Force - $DeletedCounts.Add($OldScheduledBackups.Count) - Write-LogMessage -API 'BackupRetentionCleanup' -message "Deleted $($OldScheduledBackups.Count) old tenant backups" -Sev 'Info' + $CutoffFilter = "PartitionKey eq 'ScheduledBackup' and Timestamp lt datetime'$CutoffDate'" + + # Delete blob files + $BlobFilter = "$CutoffFilter and BackupIsBlob eq true" + $BlobBackups = Get-AzDataTableEntity @ScheduledBackupTable -Filter $BlobFilter -Property @('PartitionKey', 'RowKey', 'Backup') + + $BlobDeletedCount = 0 + if ($BlobBackups) { + foreach ($Backup in $BlobBackups) { + if ($Backup.Backup) { + try { + $BlobPath = $Backup.Backup + # Extract container/blob path from URL + if ($BlobPath -like '*:10000/*') { + # Azurite format: http://host:10000/devstoreaccount1/container/blob + $parts = $BlobPath -split ':10000/' + if ($parts.Count -gt 1) { + # Remove account name to get container/blob + $BlobPath = ($parts[1] -split '/', 2)[-1] + } + } elseif ($BlobPath -like '*blob.core.windows.net/*') { + # Azure Storage format: https://account.blob.core.windows.net/container/blob + $BlobPath = ($BlobPath -split '.blob.core.windows.net/', 2)[-1] + } + $null = New-CIPPAzStorageRequest -Service 'blob' -Resource $BlobPath -Method 'DELETE' -ConnectionString $ConnectionString + $BlobDeletedCount++ + Write-Host "Deleted blob: $BlobPath" + } catch { + Write-LogMessage -API 'BackupRetentionCleanup' -message "Failed to delete blob $($Backup.Backup): $($_.Exception.Message)" -Sev 'Warning' + } + } + } + # Delete blob table entities + Remove-AzDataTableEntity @ScheduledBackupTable -Entity $BlobBackups -Force + } + + # Delete table-only backups (no blobs) + # Fetch all old entries and filter out blob entries client-side (null check is unreliable in filters) + $AllOldBackups = Get-AzDataTableEntity @ScheduledBackupTable -Filter $CutoffFilter -Property @('PartitionKey', 'RowKey', 'ETag', 'BackupIsBlob') + $TableBackups = $AllOldBackups | Where-Object { $_.BackupIsBlob -ne $true } + + $TableDeletedCount = 0 + if ($TableBackups) { + Remove-AzDataTableEntity @ScheduledBackupTable -Entity $TableBackups -Force + $TableDeletedCount = ($TableBackups | Measure-Object).Count + } + + $TotalDeleted = $BlobDeletedCount + $TableDeletedCount + if ($TotalDeleted -gt 0) { + $DeletedCounts.Add($TotalDeleted) + Write-LogMessage -API 'BackupRetentionCleanup' -message "Deleted $TotalDeleted old tenant backups ($BlobDeletedCount blobs, $TableDeletedCount table entries)" -Sev 'Info' + Write-Host "Deleted $TotalDeleted old tenant backups" } else { Write-Host 'No old tenant backups found' } @@ -69,7 +155,7 @@ function Start-BackupRetentionCleanup { $TotalDeleted = ($DeletedCounts | Measure-Object -Sum).Sum Write-LogMessage -API 'BackupRetentionCleanup' -message "Backup cleanup completed. Total backups deleted: $TotalDeleted (retention: $RetentionDays days)" -Sev 'Info' - + } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'BackupRetentionCleanup' -message "Failed to run backup cleanup: $($ErrorMessage.NormalizedError)" -Sev 'Error' -LogData $ErrorMessage diff --git a/Modules/CIPPCore/Public/Get-CIPPBackup.ps1 b/Modules/CIPPCore/Public/Get-CIPPBackup.ps1 index 4c714188f3c4..4ce3d174b30a 100644 --- a/Modules/CIPPCore/Public/Get-CIPPBackup.ps1 +++ b/Modules/CIPPCore/Public/Get-CIPPBackup.ps1 @@ -18,7 +18,15 @@ function Get-CIPPBackup { } if ($NameOnly.IsPresent) { - $Table.Property = @('RowKey') + if ($Type -ne 'Scheduled') { + $Table.Property = @('RowKey', 'Timestamp', 'BackupIsBlob') + } else { + $Table.Property = @('RowKey', 'Timestamp') + } + } + + if ($TenantFilter -and $TenantFilter -ne 'AllTenants') { + $Conditions.Add("RowKey gt '$($TenantFilter)' and RowKey lt '$($TenantFilter)~'") } $Filter = $Conditions -join ' and ' @@ -27,13 +35,60 @@ function Get-CIPPBackup { if ($NameOnly.IsPresent) { $Info = $Info | Where-Object { $_.RowKey -notmatch '-part[0-9]+$' } - if ($TenantFilter) { - $Info = $Info | Where-Object { $_.RowKey -match "^$($TenantFilter)_" } - } } else { if ($TenantFilter -and $TenantFilter -ne 'AllTenants') { $Info = $Info | Where-Object { $_.TenantFilter -eq $TenantFilter } } } + + # Augment results with blob-link awareness and fetch blob content when needed + if (-not $NameOnly.IsPresent -and $Info) { + foreach ($item in $Info) { + $isBlobLink = $false + $blobPath = $null + if ($null -ne $item.PSObject.Properties['Backup']) { + $b = $item.Backup + if ($b -is [string] -and ($b -like 'https://*' -or $b -like 'http://*')) { + $isBlobLink = $true + $blobPath = $b + + # Fetch the actual backup content from blob storage + try { + # Extract container/blob path from URL + $resourcePath = $blobPath + if ($resourcePath -like '*:10000/*') { + # Azurite format: http://host:10000/devstoreaccount1/container/blob + $parts = $resourcePath -split ':10000/' + if ($parts.Count -gt 1) { + # Remove account name to get container/blob + $resourcePath = ($parts[1] -split '/', 2)[-1] + } + } elseif ($resourcePath -like '*blob.core.windows.net/*') { + # Azure Storage format: https://account.blob.core.windows.net/container/blob + $resourcePath = ($resourcePath -split '.blob.core.windows.net/', 2)[-1] + } + + # Download the blob content + $ConnectionString = $env:AzureWebJobsStorage + $blobResponse = New-CIPPAzStorageRequest -Service 'blob' -Resource $resourcePath -Method 'GET' -ConnectionString $ConnectionString + + if ($blobResponse -and $blobResponse.Bytes) { + $backupContent = [System.Text.Encoding]::UTF8.GetString($blobResponse.Bytes) + # Replace the URL with the actual backup content + $item.Backup = $backupContent + Write-Verbose "Successfully retrieved backup content from blob storage for $($item.RowKey)" + } else { + Write-Warning "Failed to retrieve backup content from blob storage for $($item.RowKey)" + } + } catch { + Write-Warning "Error fetching backup from blob storage: $($_.Exception.Message)" + # Leave the URL in place if we can't fetch the content + } + } + } + $item | Add-Member -NotePropertyName 'BackupIsBlobLink' -NotePropertyValue $isBlobLink -Force + $item | Add-Member -NotePropertyName 'BlobResourcePath' -NotePropertyValue $blobPath -Force + } + } return $Info } diff --git a/Modules/CIPPCore/Public/New-CIPPBackup.ps1 b/Modules/CIPPCore/Public/New-CIPPBackup.ps1 index 728d53a608ba..b3f8409ade3e 100644 --- a/Modules/CIPPCore/Public/New-CIPPBackup.ps1 +++ b/Modules/CIPPCore/Public/New-CIPPBackup.ps1 @@ -1,110 +1,184 @@ function New-CIPPBackup { [CmdletBinding(SupportsShouldProcess = $true)] param ( - $backupType, + [Parameter(Mandatory = $true)] + [ValidateSet('CIPP', 'Scheduled')] + [string]$backupType, + $StorageOutput = 'default', - $TenantFilter, + + [Parameter(Mandatory = $false)] + [string]$TenantFilter, + $ScheduledBackupValues, $APIName = 'CIPP Backup', - $Headers + $Headers, + [Parameter(Mandatory = $false)] [string] $ConnectionString = $env:AzureWebJobsStorage ) - $BackupData = switch ($backupType) { - #If backup type is CIPP, create CIPP backup. - 'CIPP' { - try { - $BackupTables = @( - 'AppPermissions' - 'AccessRoleGroups' - 'ApiClients' - 'CippReplacemap' - 'CustomData' - 'CustomRoles' - 'Config' - 'CommunityRepos' - 'Domains' - 'GraphPresets' - 'GDAPRoles' - 'GDAPRoleTemplates' - 'ExcludedLicenses' - 'templates' - 'standards' - 'SchedulerConfig' - 'Extensions' - 'WebhookRules' - 'ScheduledTasks' - 'TenantProperties' - ) - $CSVfile = foreach ($CSVTable in $BackupTables) { - $Table = Get-CippTable -tablename $CSVTable - Get-AzDataTableEntity @Table | Select-Object * -ExcludeProperty DomainAnalyser, table, Timestamp, ETag, Results | Select-Object *, @{l = 'table'; e = { $CSVTable } } - } - $RowKey = 'CIPPBackup' + '_' + (Get-Date).ToString('yyyy-MM-dd-HHmm') - $CSVfile - $CSVFile = [string]($CSVfile | ConvertTo-Json -Compress -Depth 100) - $entity = @{ - PartitionKey = 'CIPPBackup' - RowKey = [string]$RowKey - TenantFilter = 'CIPPBackup' - Backup = $CSVfile - } - $Table = Get-CippTable -tablename 'CIPPBackup' + # Validate that TenantFilter is provided for Scheduled backups + if ($backupType -eq 'Scheduled' -and [string]::IsNullOrEmpty($TenantFilter)) { + throw 'TenantFilter is required for Scheduled backups' + } + + $State = 'Backup finished successfully' + $RowKey = $null + $BackupData = $null + $TableName = $null + $PartitionKey = $null + $ContainerName = $null + + try { + switch ($backupType) { + #If backup type is CIPP, create CIPP backup. + 'CIPP' { try { - if ($PSCmdlet.ShouldProcess('CIPP Backup', 'Create')) { - $null = Add-CIPPAzDataTableEntity @Table -Entity $entity -Force - Write-LogMessage -headers $Headers -API $APINAME -message 'Created CIPP Backup' -Sev 'Debug' + $BackupTables = @( + 'AppPermissions' + 'AccessRoleGroups' + 'ApiClients' + 'CippReplacemap' + 'CustomData' + 'CustomRoles' + 'Config' + 'CommunityRepos' + 'Domains' + 'GraphPresets' + 'GDAPRoles' + 'GDAPRoleTemplates' + 'ExcludedLicenses' + 'templates' + 'standards' + 'SchedulerConfig' + 'Extensions' + 'WebhookRules' + 'ScheduledTasks' + 'TenantProperties' + ) + $CSVfile = foreach ($CSVTable in $BackupTables) { + $Table = Get-CippTable -tablename $CSVTable + Get-AzDataTableEntity @Table | Select-Object * -ExcludeProperty DomainAnalyser, table, Timestamp, ETag, Results | Select-Object *, @{l = 'table'; e = { $CSVTable } } } + $RowKey = 'CIPPBackup' + '_' + (Get-Date).ToString('yyyy-MM-dd-HHmm') + $BackupData = [string]($CSVfile | ConvertTo-Json -Compress -Depth 100) + $TableName = 'CIPPBackup' + $PartitionKey = 'CIPPBackup' + $ContainerName = 'cipp-backups' } catch { $ErrorMessage = Get-CippException -Exception $_ - Write-LogMessage -headers $Headers -API $APINAME -message "Failed to create backup for CIPP: $($ErrorMessage.NormalizedError)" -Sev 'Error' -LogData $ErrorMessage - [pscustomobject]@{'Results' = "Backup Creation failed: $($ErrorMessage.NormalizedError)" } + Write-LogMessage -headers $Headers -API $APINAME -message "Failed to create backup: $($ErrorMessage.NormalizedError)" -Sev 'Error' -LogData $ErrorMessage + return [pscustomobject]@{'Results' = "Backup Creation failed: $($ErrorMessage.NormalizedError)" } } - - } catch { - $ErrorMessage = Get-CippException -Exception $_ - Write-LogMessage -headers $Headers -API $APINAME -message "Failed to create backup: $($ErrorMessage.NormalizedError)" -Sev 'Error' -LogData $ErrorMessage - [pscustomobject]@{'Results' = "Backup Creation failed: $($ErrorMessage.NormalizedError)" } } - } - #If Backup type is ConditionalAccess, create Conditional Access backup. - 'Scheduled' { - #Do a sub switch here based on the ScheduledBackupValues? - #Store output in tablestorage for Recovery - $RowKey = $TenantFilter + '_' + (Get-Date).ToString('yyyy-MM-dd-HHmm') - $entity = @{ - PartitionKey = 'ScheduledBackup' - RowKey = $RowKey - TenantFilter = $TenantFilter - } - Write-Information "Scheduled backup value psproperties: $(([pscustomobject]$ScheduledBackupValues).psobject.Properties)" - foreach ($ScheduledBackup in ([pscustomobject]$ScheduledBackupValues).psobject.Properties.Name) { + #If Backup type is Scheduled, create Scheduled backup. + 'Scheduled' { try { - $BackupResult = New-CIPPBackupTask -Task $ScheduledBackup -TenantFilter $TenantFilter | ConvertTo-Json -Depth 100 -Compress | Out-String - $entity[$ScheduledBackup] = "$BackupResult" + $RowKey = $TenantFilter + '_' + (Get-Date).ToString('yyyy-MM-dd-HHmm') + $entity = @{ + PartitionKey = 'ScheduledBackup' + RowKey = $RowKey + TenantFilter = $TenantFilter + } + Write-Information "Scheduled backup value psproperties: $(([pscustomobject]$ScheduledBackupValues).psobject.Properties)" + foreach ($ScheduledBackup in ([pscustomobject]$ScheduledBackupValues).psobject.Properties.Name) { + try { + $BackupResult = New-CIPPBackupTask -Task $ScheduledBackup -TenantFilter $TenantFilter + $entity[$ScheduledBackup] = $BackupResult + } catch { + Write-Information "Failed to create backup for $ScheduledBackup - $($_.Exception.Message)" + } + } + $BackupData = $entity | ConvertTo-Json -Compress -Depth 100 + $TableName = 'ScheduledBackup' + $PartitionKey = 'ScheduledBackup' + $ContainerName = 'scheduled-backups' } catch { - Write-Information "Failed to create backup for $ScheduledBackup - $($_.Exception.Message)" + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -headers $Headers -API $APINAME -message "Failed to create backup: $($ErrorMessage.NormalizedError)" -Sev 'Error' -LogData $ErrorMessage + return [pscustomobject]@{'Results' = "Backup Creation failed: $($ErrorMessage.NormalizedError)" } } } - $Table = Get-CippTable -tablename 'ScheduledBackup' + } + + # Upload backup data to blob storage + $blobUrl = $null + try { + $containers = @() + try { $containers = New-CIPPAzStorageRequest -Service 'blob' -Component 'list' -ConnectionString $ConnectionString } catch { $containers = @() } + $exists = ($containers | Where-Object { $_.Name -eq $ContainerName }) -ne $null + if (-not $exists) { + $null = New-CIPPAzStorageRequest -Service 'blob' -Resource $ContainerName -Method 'PUT' -QueryParams @{ restype = 'container' } -ConnectionString $ConnectionString + Start-Sleep -Milliseconds 500 + } + $blobName = "$RowKey.json" + $resourcePath = "$ContainerName/$blobName" + $null = New-CIPPAzStorageRequest -Service 'blob' -Resource $resourcePath -Method 'PUT' -ContentType 'application/json; charset=utf-8' -Body $BackupData -ConnectionString $ConnectionString + + # Build full blob URL for storage in table try { - measure-cipptask -TaskName 'ScheduledBackupStorage' -EventName 'CIPP.BackupCompleted' -Script { - $null = Add-CIPPAzDataTableEntity @Table -entity $entity -Force + $csParts = @{} + foreach ($p in ($ConnectionString -split ';')) { + if (-not [string]::IsNullOrWhiteSpace($p)) { + $kv = $p -split '=', 2 + if ($kv.Length -eq 2) { $csParts[$kv[0]] = $kv[1] } + } + } + + # Handle UseDevelopmentStorage=true (Azurite) + if ($csParts.ContainsKey('UseDevelopmentStorage') -and $csParts['UseDevelopmentStorage'] -eq 'true') { + $base = 'http://127.0.0.1:10000/devstoreaccount1' + } elseif ($csParts.ContainsKey('BlobEndpoint') -and $csParts['BlobEndpoint']) { + $base = ($csParts['BlobEndpoint']).TrimEnd('/') + } else { + $protocol = if ($csParts.ContainsKey('DefaultEndpointsProtocol') -and $csParts['DefaultEndpointsProtocol']) { $csParts['DefaultEndpointsProtocol'] } else { 'https' } + $suffix = if ($csParts.ContainsKey('EndpointSuffix') -and $csParts['EndpointSuffix']) { $csParts['EndpointSuffix'] } else { 'core.windows.net' } + $acct = $csParts['AccountName'] + if (-not $acct) { throw 'AccountName missing in ConnectionString' } + $base = "$protocol`://$acct.blob.$suffix" } - Write-LogMessage -headers $Headers -API $APINAME -message 'Created backup' -Sev 'Debug' - $State = 'Backup finished successfully' + $blobUrl = "$base/$resourcePath" } catch { - $State = 'Failed to write backup to table storage' - $ErrorMessage = Get-CippException -Exception $_ - Write-LogMessage -headers $Headers -API $APINAME -message "Failed to create tenant backup: $($ErrorMessage.NormalizedError)" -Sev 'Error' -LogData $ErrorMessage - [pscustomobject]@{'Results' = "Backup Creation failed: $($ErrorMessage.NormalizedError)" } + # If building full URL fails, fall back to resource path + $blobUrl = $resourcePath } + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -headers $Headers -API $APINAME -message "Blob upload failed: $($ErrorMessage.NormalizedError)" -Sev 'Error' -LogData $ErrorMessage } + # Write table entity pointing to blob resource + $entity = @{ + PartitionKey = $PartitionKey + RowKey = [string]$RowKey + Backup = $blobUrl + BackupIsBlob = $true + } + + if ($TenantFilter) { + $entity['TenantFilter'] = $TenantFilter + } + + $Table = Get-CippTable -tablename $TableName + try { + if ($PSCmdlet.ShouldProcess("$backupType Backup", 'Create')) { + $null = Add-CIPPAzDataTableEntity @Table -Entity $entity -Force + Write-LogMessage -headers $Headers -API $APINAME -message "Created $backupType Backup (link stored)" -Sev 'Debug' + } + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -headers $Headers -API $APINAME -message "Failed to create backup for $backupType : $($ErrorMessage.NormalizedError)" -Sev 'Error' -LogData $ErrorMessage + return [pscustomobject]@{'Results' = "Backup Creation failed: $($ErrorMessage.NormalizedError)" } + } + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -headers $Headers -API $APINAME -message "Failed to create backup: $($ErrorMessage.NormalizedError)" -Sev 'Error' -LogData $ErrorMessage + return [pscustomobject]@{'Results' = "Backup Creation failed: $($ErrorMessage.NormalizedError)" } + } + + return [pscustomobject]@{ + BackupName = $RowKey + BackupState = $State } - return @([pscustomobject]@{ - BackupName = $RowKey - BackupState = $State - }) } diff --git a/Modules/CIPPCore/Public/New-CIPPRestore.ps1 b/Modules/CIPPCore/Public/New-CIPPRestore.ps1 index 52042e261780..5668b3f48484 100644 --- a/Modules/CIPPCore/Public/New-CIPPRestore.ps1 +++ b/Modules/CIPPCore/Public/New-CIPPRestore.ps1 @@ -10,7 +10,8 @@ function New-CIPPRestore { Write-Host "Scheduled Restore psproperties: $(([pscustomobject]$RestoreValues).psobject.Properties)" Write-LogMessage -headers $Headers -API $APINAME -message 'Restored backup' -Sev 'Debug' - $RestoreData = foreach ($ScheduledBackup in ([pscustomobject]$RestoreValues).psobject.Properties.Name | Where-Object { $_ -notin 'email', 'webhook', 'psa', 'backup', 'overwrite' }) { + $RestoreData = foreach ($ScheduledBackup in ([pscustomobject]$RestoreValues).psobject.Properties | Where-Object { $_.Value -eq $true -and $_.Name -notin 'email', 'webhook', 'psa', 'backup', 'overwrite' } | Select-Object -ExpandProperty Name) { + Write-Information "Restoring $ScheduledBackup for tenant $TenantFilter" New-CIPPRestoreTask -Task $ScheduledBackup -TenantFilter $TenantFilter -backup $RestoreValues.backup -overwrite $RestoreValues.overwrite -Headers $Headers -APIName $APIName } return $RestoreData diff --git a/Modules/CIPPCore/Public/New-CIPPRestoreTask.ps1 b/Modules/CIPPCore/Public/New-CIPPRestoreTask.ps1 index 2e19b3f3f0a2..aec6db68de03 100644 --- a/Modules/CIPPCore/Public/New-CIPPRestoreTask.ps1 +++ b/Modules/CIPPCore/Public/New-CIPPRestoreTask.ps1 @@ -8,13 +8,67 @@ function New-CIPPRestoreTask { $APINAME, $Headers ) - $Table = Get-CippTable -tablename 'ScheduledBackup' - $BackupData = Get-CIPPAzDataTableEntity @Table -Filter "RowKey eq '$backup'" - $RestoreData = switch ($Task) { + # Use Get-CIPPBackup which handles blob storage fetching + $BackupData = Get-CIPPBackup -Type 'Scheduled' -Name $backup + + # If this is a blob-based backup, parse the Backup property to get the actual data structure + if ($BackupData.BackupIsBlob -or $BackupData.BackupIsBlobLink) { + try { + $BackupData = $BackupData.Backup | ConvertFrom-Json + } catch { + Write-Warning "Failed to parse blob backup data: $($_.Exception.Message)" + } + } + + + # Initialize restoration counters + $restorationStats = @{ + 'CustomVariables' = @{ success = 0; failed = 0 } + 'Users' = @{ success = 0; failed = 0 } + 'Groups' = @{ success = 0; failed = 0 } + 'ConditionalAccess' = @{ success = 0; failed = 0 } + 'IntuneConfig' = @{ success = 0; failed = 0 } + 'IntunCompliance' = @{ success = 0; failed = 0 } + 'IntuneProtection' = @{ success = 0; failed = 0 } + 'AntiSpam' = @{ success = 0; failed = 0 } + 'AntiPhishing' = @{ success = 0; failed = 0 } + 'WebhookAlerts' = @{ success = 0; failed = 0 } + 'ScriptedAlerts' = @{ success = 0; failed = 0 } + } + + # Helper function to clean user object for Graph API - removes reference properties, nulls, and empty strings + function Clean-GraphObject { + param($Object, [switch]$ExcludeId) + $excludeProps = @('password', 'passwordProfile', '@odata.type', 'manager', 'memberOf', 'createdOnBehalfOf', 'createdByAppId', 'deletedDateTime', 'authorizationInfo') + if ($ExcludeId) { + $excludeProps += @('id') + } + + $cleaned = $Object | Select-Object * -ExcludeProperty $excludeProps + $result = @{} + + foreach ($prop in $cleaned.PSObject.Properties) { + $propValue = $prop.Value + # Skip empty strings, nulls, and complex objects (except known-good arrays like businessPhones) + if ($propValue -ne '' -and $null -ne $propValue) { + # Skip complex objects/dictionaries but allow simple arrays + if ($propValue -is [System.Collections.IDictionary]) { + continue + } + $result[$prop.Name] = $propValue + } + } + + return $result + } + + $RestoreData = [System.Collections.Generic.List[string]]::new() + + switch ($Task) { 'CippCustomVariables' { Write-Host "Restore Custom Variables for $TenantFilter" $ReplaceTable = Get-CIPPTable -TableName 'CippReplacemap' - $Backup = $BackupData.CippCustomVariables | ConvertFrom-Json + $Backup = if ($BackupData.CippCustomVariables -is [string]) { $BackupData.CippCustomVariables | ConvertFrom-Json } else { $BackupData.CippCustomVariables } $Tenant = Get-Tenants -TenantFilter $TenantFilter $CustomerId = $Tenant.customerId @@ -28,68 +82,122 @@ function New-CIPPRestoreTask { Description = $variable.Description } - if ($overwrite) { - Add-CIPPAzDataTableEntity @ReplaceTable -Entity $entity -Force - Write-LogMessage -message "Restored custom variable $($variable.RowKey) from backup" -Sev 'info' - "Restored custom variable $($variable.RowKey) from backup" - } else { - # Check if variable already exists - $existing = Get-CIPPAzDataTableEntity @ReplaceTable -Filter "PartitionKey eq '$CustomerId' and RowKey eq '$($variable.RowKey)'" - if (!$existing) { + try { + if ($overwrite) { Add-CIPPAzDataTableEntity @ReplaceTable -Entity $entity -Force Write-LogMessage -message "Restored custom variable $($variable.RowKey) from backup" -Sev 'info' - "Restored custom variable $($variable.RowKey) from backup" + $restorationStats['CustomVariables'].success++ + $RestoreData.Add("Restored custom variable $($variable.RowKey) from backup") } else { - Write-LogMessage -message "Custom variable $($variable.RowKey) already exists and overwrite is disabled" -Sev 'info' - "Custom variable $($variable.RowKey) already exists and overwrite is disabled" + # Check if variable already exists + $existing = Get-CIPPAzDataTableEntity @ReplaceTable -Filter "PartitionKey eq '$CustomerId' and RowKey eq '$($variable.RowKey)'" + if (!$existing) { + Add-CIPPAzDataTableEntity @ReplaceTable -Entity $entity -Force + Write-LogMessage -message "Restored custom variable $($variable.RowKey) from backup" -Sev 'info' + $restorationStats['CustomVariables'].success++ + $RestoreData.Add("Restored custom variable $($variable.RowKey) from backup") + } else { + Write-LogMessage -message "Custom variable $($variable.RowKey) already exists and overwrite is disabled" -Sev 'info' + $RestoreData.Add("Custom variable $($variable.RowKey) already exists and overwrite is disabled") + } } + } catch { + $restorationStats['CustomVariables'].failed++ + Write-LogMessage -message "Failed to restore custom variable $($variable.RowKey): $($_.Exception.Message)" -Sev 'Warning' + $RestoreData.Add("Failed to restore custom variable $($variable.RowKey)") } } } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message - "Could not restore Custom Variables: $ErrorMessage" + $RestoreData.Add("Could not restore Custom Variables: $ErrorMessage") Write-LogMessage -Headers $Headers -API $APINAME -message "Could not restore Custom Variables: $ErrorMessage" -Sev 'Error' } } 'users' { $currentUsers = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/users?$top=999&select=id,userPrincipalName' -tenantid $TenantFilter - $backupUsers = $BackupData.users | ConvertFrom-Json + $backupUsers = if ($BackupData.users -is [string]) { $BackupData.users | ConvertFrom-Json } else { $BackupData.users } + + Write-Host "Restore users for $TenantFilter" + Write-Information "User count in backup: $($backupUsers.Count)" $BackupUsers | ForEach-Object { try { - $JSON = $_ | ConvertTo-Json -Depth 100 -Compress - $DisplayName = $_.displayName - $UPN = $_.userPrincipalName + $userObject = $_ + $UPN = $userObject.userPrincipalName + if ($overwrite) { - if ($_.id -in $currentUsers.id) { - New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/users/$($_.id)" -tenantid $TenantFilter -body $JSON -type PATCH + if ($userObject.id -in $currentUsers.id -or $userObject.userPrincipalName -in $currentUsers.userPrincipalName) { + # Patch existing user - clean object to remove reference properties, nulls, and empty strings + $cleanedUser = Clean-GraphObject -Object $userObject + $patchBody = $cleanedUser | ConvertTo-Json -Depth 100 -Compress + $null = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/users/$($userObject.id)" -tenantid $TenantFilter -body $patchBody -type PATCH Write-LogMessage -message "Restored $($UPN) from backup by patching the existing object." -Sev 'info' - "The user existed. Restored $($UPN) from backup" + $restorationStats['Users'].success++ + $RestoreData.Add("The user existed. Restored $($UPN) from backup") } else { - New-GraphPOSTRequest -uri 'https://graph.microsoft.com/beta/users' -tenantid $TenantFilter -body $JSON -type POST - Write-LogMessage -message "Restored $($UPN) from backup by creating a new object." -Sev 'info' - "The user did not exist. Restored $($UPN) from backup" + # Create new user - need to add password and clean object + $tempPassword = New-passwordString + # Remove reference properties that may not exist in target tenant, nulls, and empty strings + $cleanedUser = Clean-GraphObject -Object $userObject -ExcludeId + $cleanedUser['passwordProfile'] = @{ + 'forceChangePasswordNextSignIn' = $true + 'password' = $tempPassword + } + $JSON = $cleanedUser | ConvertTo-Json -Depth 100 -Compress + + $null = New-GraphPOSTRequest -uri 'https://graph.microsoft.com/beta/users' -tenantid $TenantFilter -body $JSON -type POST + # Try to wrap password in PwPush link + $displayPassword = $tempPassword + try { + $PasswordLink = New-PwPushLink -Payload $tempPassword + if ($PasswordLink) { + $displayPassword = $PasswordLink + } + } catch { + # If PwPush fails, use plain password + } + Write-LogMessage -message "Restored $($UPN) from backup by creating a new object with temporary password. Password: $displayPassword" -Sev 'info' -tenant $TenantFilter + $restorationStats['Users'].success++ + $RestoreData.Add("The user did not exist. Restored $($UPN) from backup with temporary password: $displayPassword") } } if (!$overwrite) { - if ($_.id -notin $backupUsers.id) { - New-GraphPOSTRequest -uri 'https://graph.microsoft.com/beta/users' -tenantid $TenantFilter -body $JSON -type POST - Write-LogMessage -message "Restored $($UPN) from backup" -Sev 'info' - "Restored $($UPN) from backup" - } else { - Write-LogMessage -message "User $($UPN) already exists in tenant $TenantFilter and overwrite is disabled" -Sev 'info' - "User $($UPN) already exists in tenant $TenantFilter and overwrite is disabled" + if ($userObject.id -notin $currentUsers.id -and $userObject.userPrincipalName -notin $currentUsers.userPrincipalName) { + # Create new user - need to add password and clean object + $tempPassword = New-passwordString + # Remove reference properties that may not exist in target tenant, nulls, and empty strings + $cleanedUser = Clean-GraphObject -Object $userObject -ExcludeId + $cleanedUser['passwordProfile'] = @{ + 'forceChangePasswordNextSignIn' = $true + 'password' = $tempPassword + } + $JSON = $cleanedUser | ConvertTo-Json -Depth 100 -Compress + $null = New-GraphPOSTRequest -uri 'https://graph.microsoft.com/beta/users' -tenantid $TenantFilter -body $JSON -type POST + # Try to wrap password in PwPush link + $displayPassword = $tempPassword + try { + $PasswordLink = New-PwPushLink -Payload $tempPassword + if ($PasswordLink) { + $displayPassword = $PasswordLink + } + } catch { + # If PwPush fails, use plain password + } + Write-LogMessage -message "Restored $($UPN) from backup with temporary password. Password: $displayPassword" -Sev 'info' + $restorationStats['Users'].success++ + $RestoreData.Add("Restored $($UPN) from backup with temporary password: $displayPassword") } } } catch { + $restorationStats['Users'].failed++ $ErrorMessage = Get-CippException -Exception $_ - "Could not restore user $($UPN): $($ErrorMessage.NormalizedError) " + $RestoreData.Add("Could not restore user $($UPN): $($ErrorMessage.NormalizedError) ") Write-LogMessage -Headers $Headers -API $APINAME -message "Could not restore user $($UPN): $($ErrorMessage.NormalizedError) " -Sev 'Error' -LogData $ErrorMessage } } } 'groups' { Write-Host "Restore groups for $TenantFilter" - $backupGroups = $BackupData.groups | ConvertFrom-Json + $backupGroups = if ($BackupData.groups -is [string]) { $BackupData.groups | ConvertFrom-Json } else { $BackupData.groups } $Groups = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/groups?$top=999' -tenantid $TenantFilter $BackupGroups | ForEach-Object { try { @@ -97,67 +205,77 @@ function New-CIPPRestoreTask { $DisplayName = $_.displayName if ($overwrite) { if ($_.id -in $Groups.id) { - New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/groups/$($_.id)" -tenantid $TenantFilter -body $JSON -type PATCH + $null = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/groups/$($_.id)" -tenantid $TenantFilter -body $JSON -type PATCH Write-LogMessage -message "Restored $DisplayName from backup by patching the existing object." -Sev 'info' - "The group existed. Restored $DisplayName from backup" + $restorationStats['Groups'].success++ + $RestoreData.Add("The group existed. Restored $DisplayName from backup") } else { - New-GraphPOSTRequest -uri 'https://graph.microsoft.com/beta/groups' -tenantid $TenantFilter -body $JSON -type POST + $null = New-GraphPOSTRequest -uri 'https://graph.microsoft.com/beta/groups' -tenantid $TenantFilter -body $JSON -type POST Write-LogMessage -message "Restored $DisplayName from backup" -Sev 'info' - "Restored $DisplayName from backup" + $restorationStats['Groups'].success++ + $RestoreData.Add("Restored $DisplayName from backup") } } if (!$overwrite) { if ($_.id -notin $Groups.id) { - New-GraphPOSTRequest -uri 'https://graph.microsoft.com/beta/groups' -tenantid $TenantFilter -body $JSON -type POST + $null = New-GraphPOSTRequest -uri 'https://graph.microsoft.com/beta/groups' -tenantid $TenantFilter -body $JSON -type POST Write-LogMessage -message "Restored $DisplayName from backup" -Sev 'info' - "Restored $DisplayName from backup" + $restorationStats['Groups'].success++ + $RestoreData.Add("Restored $DisplayName from backup") } else { Write-LogMessage -message "Group $DisplayName already exists in tenant $TenantFilter and overwrite is disabled" -Sev 'info' - "Group $DisplayName already exists in tenant $TenantFilter and overwrite is disabled" + $RestoreData.Add("Group $DisplayName already exists in tenant $TenantFilter and overwrite is disabled") } } } catch { + $restorationStats['Groups'].failed++ $ErrorMessage = Get-CippException -Exception $_ - "Could not restore group $DisplayName : $($ErrorMessage.NormalizedError) " + $RestoreData.Add("Could not restore group $DisplayName : $($ErrorMessage.NormalizedError) ") Write-LogMessage -Headers $Headers -API $APINAME -message "Could not restore group $DisplayName : $($ErrorMessage.NormalizedError) " -Sev 'Error' -LogData $ErrorMessage } } } 'ca' { Write-Host "Restore Conditional Access Policies for $TenantFilter" - $BackupCAPolicies = $BackupData.ca | ConvertFrom-Json + $BackupCAPolicies = if ($BackupData.ca -is [string]) { $BackupData.ca | ConvertFrom-Json } else { $BackupData.ca } $BackupCAPolicies | ForEach-Object { $JSON = $_ try { - New-CIPPCAPolicy -replacePattern 'displayName' -Overwrite $overwrite -TenantFilter $TenantFilter -state 'donotchange' -RawJSON $JSON -APIName 'CIPP Restore' -ErrorAction SilentlyContinue + $null = New-CIPPCAPolicy -replacePattern 'displayName' -Overwrite $overwrite -TenantFilter $TenantFilter -state 'donotchange' -RawJSON $JSON -APIName 'CIPP Restore' -ErrorAction SilentlyContinue + $restorationStats['ConditionalAccess'].success++ + $RestoreData.Add('Restored Conditional Access policy from backup') } catch { + $restorationStats['ConditionalAccess'].failed++ $ErrorMessage = Get-CippException -Exception $_ - "Could not restore Conditional Access Policy $DisplayName : $($ErrorMessage.NormalizedError) " + $RestoreData.Add("Could not restore Conditional Access Policy $DisplayName : $($ErrorMessage.NormalizedError) ") Write-LogMessage -Headers $Headers -API $APINAME -message "Could not restore Conditional Access Policy $DisplayName : $($ErrorMessage.NormalizedError) " -Sev 'Error' -LogData $ErrorMessage } } } 'intuneconfig' { - $BackupConfig = $BackupData.intuneconfig | ConvertFrom-Json + $BackupConfig = if ($BackupData.intuneconfig -is [string]) { $BackupData.intuneconfig | ConvertFrom-Json } else { $BackupData.intuneconfig } foreach ($backup in $backupConfig) { try { - Set-CIPPIntunePolicy -TemplateType $backup.Type -TenantFilter $TenantFilter -DisplayName $backup.DisplayName -Description $backup.Description -RawJSON ($backup.TemplateJson) -Headers $Headers -APINAME $APINAME -ErrorAction SilentlyContinue + $null = Set-CIPPIntunePolicy -TemplateType $backup.Type -TenantFilter $TenantFilter -DisplayName $backup.DisplayName -Description $backup.Description -RawJSON ($backup.TemplateJson) -Headers $Headers -APINAME $APINAME -ErrorAction SilentlyContinue } catch { $ErrorMessage = Get-CippException -Exception $_ - "Could not restore Intune Configuration $DisplayName : $($ErrorMessage.NormalizedError) " + $RestoreData.Add("Could not restore Intune Configuration $DisplayName : $($ErrorMessage.NormalizedError) ") Write-LogMessage -Headers $Headers -API $APINAME -message "Could not restore Intune Configuration $DisplayName : $($ErrorMessage.NormalizedError) " -Sev 'Error' -LogData $ErrorMessage } } #Convert the manual method to a function } 'intunecompliance' { - $BackupConfig = $BackupData.intunecompliance | ConvertFrom-Json + $BackupConfig = if ($BackupData.intunecompliance -is [string]) { $BackupData.intunecompliance | ConvertFrom-Json } else { $BackupData.intunecompliance } foreach ($backup in $backupConfig) { try { - Set-CIPPIntunePolicy -TemplateType $backup.Type -TenantFilter $TenantFilter -DisplayName $backup.DisplayName -Description $backup.Description -RawJSON ($backup.TemplateJson) -Headers $Headers -APINAME $APINAME -ErrorAction SilentlyContinue + $null = Set-CIPPIntunePolicy -TemplateType $backup.Type -TenantFilter $TenantFilter -DisplayName $backup.DisplayName -Description $backup.Description -RawJSON ($backup.TemplateJson) -Headers $Headers -APINAME $APINAME -ErrorAction SilentlyContinue + $restorationStats['IntuneConfig'].success++ + $RestoreData.Add('Restored Intune configuration from backup') } catch { + $restorationStats['IntuneConfig'].failed++ $ErrorMessage = Get-CippException -Exception $_ - "Could not restore Intune Compliance $DisplayName : $($ErrorMessage.NormalizedError) " + $RestoreData.Add("Could not restore Intune Compliance $DisplayName : $($ErrorMessage.NormalizedError) ") Write-LogMessage -Headers $Headers -API $APINAME -message "Could not restore Intune Configuration $DisplayName : $($ErrorMessage.NormalizedError) " -Sev 'Error' -LogData $ErrorMessage } } @@ -165,13 +283,16 @@ function New-CIPPRestoreTask { } 'intuneprotection' { - $BackupConfig = $BackupData.intuneprotection | ConvertFrom-Json + $BackupConfig = if ($BackupData.intuneprotection -is [string]) { $BackupData.intuneprotection | ConvertFrom-Json } else { $BackupData.intuneprotection } foreach ($backup in $backupConfig) { try { - Set-CIPPIntunePolicy -TemplateType $backup.Type -TenantFilter $TenantFilter -DisplayName $backup.DisplayName -Description $backup.Description -RawJSON ($backup.TemplateJson) -Headers $Headers -APINAME $APINAME -ErrorAction SilentlyContinue + $null = Set-CIPPIntunePolicy -TemplateType $backup.Type -TenantFilter $TenantFilter -DisplayName $backup.DisplayName -Description $backup.Description -RawJSON ($backup.TemplateJson) -Headers $Headers -APINAME $APINAME -ErrorAction SilentlyContinue + $restorationStats['IntuneProtection'].success++ + $RestoreData.Add('Restored Intune protection policy from backup') } catch { + $restorationStats['IntuneProtection'].failed++ $ErrorMessage = Get-CippException -Exception $_ - "Could not restore Intune Protection $DisplayName : $($ErrorMessage.NormalizedError) " + $RestoreData.Add("Could not restore Intune Protection $DisplayName : $($ErrorMessage.NormalizedError) ") Write-LogMessage -Headers $Headers -API $APINAME -message "Could not restore Intune Configuration $DisplayName : $($ErrorMessage.NormalizedError) " -Sev 'Error' -LogData $ErrorMessage } } @@ -180,14 +301,15 @@ function New-CIPPRestoreTask { 'antispam' { try { - $BackupConfig = $BackupData.antispam | ConvertFrom-Json | ConvertFrom-Json + $BackupConfig = if ($BackupData.antispam -is [string]) { $BackupData.antispam | ConvertFrom-Json } else { $BackupData.antispam } + if ($BackupConfig -is [string]) { $BackupConfig = $BackupConfig | ConvertFrom-Json } $BackupPolicies = $BackupConfig.policies $BackupRules = $BackupConfig.rules $CurrentPolicies = New-ExoRequest -tenantid $Tenantfilter -cmdlet 'Get-HostedContentFilterPolicy' | Select-Object * -ExcludeProperty *odata*, *data.type* $CurrentRules = New-ExoRequest -tenantid $Tenantfilter -cmdlet 'Get-HostedContentFilterRule' | Select-Object * -ExcludeProperty *odata*, *data.type* } catch { $ErrorMessage = Get-CippException -Exception $_ - "Could not obtain Anti-Spam Configuration: $($ErrorMessage.NormalizedError) " + $RestoreData.Add("Could not obtain Anti-Spam Configuration: $($ErrorMessage.NormalizedError) ") Write-LogMessage -Headers $Headers -API $APINAME -message "Could not obtain Anti-Spam Configuration: $($ErrorMessage.NormalizedError) " -Sev 'Error' -LogData $ErrorMessage } @@ -280,10 +402,11 @@ function New-CIPPRestoreTask { } } - New-ExoRequest -TenantId $Tenantfilter -cmdlet 'Set-HostedContentFilterPolicy' -cmdparams $cmdparams -UseSystemMailbox $true + $null = New-ExoRequest -TenantId $Tenantfilter -cmdlet 'Set-HostedContentFilterPolicy' -cmdparams $cmdparams -UseSystemMailbox $true Write-LogMessage -message "Restored $($policy.Identity) from backup" -Sev 'info' - "Restored $($policy.Identity) from backup." + $restorationStats['AntiSpam'].success++ + $RestoreData.Add("Restored $($policy.Identity) from backup.") } } else { $cmdparams = @{ @@ -300,14 +423,16 @@ function New-CIPPRestoreTask { } } - New-ExoRequest -TenantId $Tenantfilter -cmdlet 'New-HostedContentFilterPolicy' -cmdparams $cmdparams -UseSystemMailbox $true + $null = New-ExoRequest -TenantId $Tenantfilter -cmdlet 'New-HostedContentFilterPolicy' -cmdparams $cmdparams -UseSystemMailbox $true Write-LogMessage -message "Restored $($policy.Identity) from backup" -Sev 'info' - "Restored $($policy.Identity) from backup." + $restorationStats['AntiSpam'].success++ + $RestoreData.Add("Restored $($policy.Identity) from backup.") } } catch { + $restorationStats['AntiSpam'].failed++ $ErrorMessage = Get-CippException -Exception $_ - "Could not restore Anti-spam policy $($policy.Identity) : $($ErrorMessage.NormalizedError) " + $RestoreData.Add("Could not restore Anti-spam policy $($policy.Identity) : $($ErrorMessage.NormalizedError) ") Write-LogMessage -Headers $Headers -API $APINAME -message "Could not restore Anti-spam policy $($policy.Identity) : $($ErrorMessage.NormalizedError) " -Sev 'Error' -LogData $ErrorMessage } } @@ -330,10 +455,11 @@ function New-CIPPRestoreTask { } } - New-ExoRequest -TenantId $Tenantfilter -cmdlet 'Set-HostedContentFilterRule' -cmdparams $cmdparams -UseSystemMailbox $true + $null = New-ExoRequest -TenantId $Tenantfilter -cmdlet 'Set-HostedContentFilterRule' -cmdparams $cmdparams -UseSystemMailbox $true Write-LogMessage -message "Restored $($rule.Identity) from backup" -Sev 'info' - "Restored $($rule.Identity) from backup." + $restorationStats['AntiSpam'].success++ + $RestoreData.Add("Restored $($rule.Identity) from backup.") } } else { $cmdparams = @{ @@ -350,14 +476,16 @@ function New-CIPPRestoreTask { } } - New-ExoRequest -TenantId $Tenantfilter -cmdlet 'New-HostedContentFilterRule' -cmdparams $cmdparams -UseSystemMailbox $true + $null = New-ExoRequest -TenantId $Tenantfilter -cmdlet 'New-HostedContentFilterRule' -cmdparams $cmdparams -UseSystemMailbox $true Write-LogMessage -message "Restored $($rule.Identity) from backup" -Sev 'info' - "Restored $($rule.Identity) from backup." + $restorationStats['AntiSpam'].success++ + $RestoreData.Add("Restored $($rule.Identity) from backup.") } } catch { + $restorationStats['AntiSpam'].failed++ $ErrorMessage = Get-CippException -Exception $_ - "Could not restore Anti-spam rule $($rule.Identity) : $($ErrorMessage.NormalizedError) " + $RestoreData.Add("Could not restore Anti-spam rule $($rule.Identity) : $($ErrorMessage.NormalizedError) ") Write-LogMessage -Headers $Headers -API $APINAME -message "Could not restore Anti-spam rule $($rule.Identity) : $($ErrorMessage.NormalizedError) " -Sev 'Error' -LogData $ErrorMessage } } @@ -365,14 +493,15 @@ function New-CIPPRestoreTask { 'antiphishing' { try { - $BackupConfig = $BackupData.antiphishing | ConvertFrom-Json | ConvertFrom-Json + $BackupConfig = if ($BackupData.antiphishing -is [string]) { $BackupData.antiphishing | ConvertFrom-Json } else { $BackupData.antiphishing } + if ($BackupConfig -is [string]) { $BackupConfig = $BackupConfig | ConvertFrom-Json } $BackupPolicies = $BackupConfig.policies $BackupRules = $BackupConfig.rules $CurrentPolicies = New-ExoRequest -tenantid $Tenantfilter -cmdlet 'Get-AntiPhishPolicy' | Select-Object * -ExcludeProperty *odata*, *data.type* $CurrentRules = New-ExoRequest -tenantid $Tenantfilter -cmdlet 'Get-AntiPhishRule' | Select-Object * -ExcludeProperty *odata*, *data.type* } catch { $ErrorMessage = Get-CippException -Exception $_ - "Could not obtain Anti-Phishing Configuration: $($ErrorMessage.NormalizedError) " + $RestoreData.Add("Could not obtain Anti-Phishing Configuration: $($ErrorMessage.NormalizedError) ") Write-LogMessage -Headers $Headers -API $APINAME -message "Could not obtain Anti-Phishing Configuration: $($ErrorMessage.NormalizedError) " -Sev 'Error' -LogData $ErrorMessage } @@ -441,10 +570,11 @@ function New-CIPPRestoreTask { } } - New-ExoRequest -TenantId $Tenantfilter -cmdlet 'Set-AntiPhishPolicy' -cmdparams $cmdparams -UseSystemMailbox $true + $null = New-ExoRequest -TenantId $Tenantfilter -cmdlet 'Set-AntiPhishPolicy' -cmdparams $cmdparams -UseSystemMailbox $true Write-LogMessage -message "Restored $($policy.Identity) from backup" -Sev 'info' - "Restored $($policy.Identity) from backup." + $restorationStats['AntiPhishing'].success++ + $RestoreData.Add("Restored $($policy.Identity) from backup.") } } else { $cmdparams = @{ @@ -457,14 +587,16 @@ function New-CIPPRestoreTask { } } - New-ExoRequest -TenantId $Tenantfilter -cmdlet 'New-AntiPhishPolicy' -cmdparams $cmdparams -UseSystemMailbox $true + $null = New-ExoRequest -TenantId $Tenantfilter -cmdlet 'New-AntiPhishPolicy' -cmdparams $cmdparams -UseSystemMailbox $true Write-LogMessage -message "Restored $($policy.Identity) from backup" -Sev 'info' - "Restored $($policy.Identity) from backup." + $restorationStats['AntiPhishing'].success++ + $RestoreData.Add("Restored $($policy.Identity) from backup.") } } catch { + $restorationStats['AntiPhishing'].failed++ $ErrorMessage = Get-CippException -Exception $_ - "Could not restore Anti-phishing policy $($policy.Identity) : $($ErrorMessage.NormalizedError) " + $RestoreData.Add("Could not restore Anti-phishing policy $($policy.Identity) : $($ErrorMessage.NormalizedError) ") Write-LogMessage -Headers $Headers -API $APINAME -message "Could not restore Anti-phishing policy $($policy.Identity) : $($ErrorMessage.NormalizedError) " -Sev 'Error' -LogData $ErrorMessage } } @@ -487,10 +619,11 @@ function New-CIPPRestoreTask { } } - New-ExoRequest -TenantId $Tenantfilter -cmdlet 'Set-AntiPhishRule' -cmdparams $cmdparams -UseSystemMailbox $true + $null = New-ExoRequest -TenantId $Tenantfilter -cmdlet 'Set-AntiPhishRule' -cmdparams $cmdparams -UseSystemMailbox $true Write-LogMessage -message "Restored $($rule.Identity) from backup" -Sev 'info' - "Restored $($rule.Identity) from backup." + $restorationStats['AntiPhishing'].success++ + $RestoreData.Add("Restored $($rule.Identity) from backup.") } } else { $cmdparams = @{ @@ -507,14 +640,16 @@ function New-CIPPRestoreTask { } } - New-ExoRequest -TenantId $Tenantfilter -cmdlet 'New-AntiPhishRule' -cmdparams $cmdparams -UseSystemMailbox $true + $null = New-ExoRequest -TenantId $Tenantfilter -cmdlet 'New-AntiPhishRule' -cmdparams $cmdparams -UseSystemMailbox $true Write-LogMessage -message "Restored $($rule.Identity) from backup" -Sev 'info' - "Restored $($rule.Identity) from backup." + $restorationStats['AntiPhishing'].success++ + $RestoreData.Add("Restored $($rule.Identity) from backup.") } } catch { + $restorationStats['AntiPhishing'].failed++ $ErrorMessage = Get-CippException -Exception $_ - "Could not restore Anti-phishing rule $($rule.Identity) : $($ErrorMessage.NormalizedError) " + $RestoreData.Add("Could not restore Anti-phishing rule $($rule.Identity) : $($ErrorMessage.NormalizedError) ") Write-LogMessage -Headers $Headers -API $APINAME -message "Could not restore Anti-phishing rule $($rule.Identity) : $($ErrorMessage.NormalizedError) " -Sev 'Error' -LogData $ErrorMessage } } @@ -522,26 +657,57 @@ function New-CIPPRestoreTask { 'CippWebhookAlerts' { Write-Host "Restore Webhook Alerts for $TenantFilter" $WebhookTable = Get-CIPPTable -TableName 'WebhookRules' - $Backup = $BackupData.CippWebhookAlerts | ConvertFrom-Json + $Backup = if ($BackupData.CippWebhookAlerts -is [string]) { $BackupData.CippWebhookAlerts | ConvertFrom-Json } else { $BackupData.CippWebhookAlerts } try { Add-CIPPAzDataTableEntity @WebhookTable -Entity $Backup -Force + $restorationStats['WebhookAlerts'].success++ + $RestoreData.Add('Restored Webhook Alerts from backup') } catch { + $restorationStats['WebhookAlerts'].failed++ $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message - "Could not restore Webhook Alerts $ErrorMessage" + $RestoreData.Add("Could not restore Webhook Alerts $ErrorMessage") } } 'CippScriptedAlerts' { Write-Host "Restore Scripted Alerts for $TenantFilter" $ScheduledTasks = Get-CIPPTable -TableName 'ScheduledTasks' - $Backup = $BackupData.CippScriptedAlerts | ConvertFrom-Json + $Backup = if ($BackupData.CippScriptedAlerts -is [string]) { $BackupData.CippScriptedAlerts | ConvertFrom-Json } else { $BackupData.CippScriptedAlerts } try { Add-CIPPAzDataTableEntity @ScheduledTasks -Entity $Backup -Force + $restorationStats['ScriptedAlerts'].success++ + $RestoreData.Add('Restored Scripted Alerts from backup') } catch { + $restorationStats['ScriptedAlerts'].failed++ $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message - "Could not restore Scripted Alerts $ErrorMessage " + $RestoreData.Add("Could not restore Scripted Alerts $ErrorMessage ") } } } + + # Build summary message + $summaryParts = @() + $successCount = 0 + $failureCount = 0 + + foreach ($type in $restorationStats.Keys) { + $successCount += $restorationStats[$type].success + $failureCount += $restorationStats[$type].failed + + if ($restorationStats[$type].success -gt 0) { + $pluralForm = if ($restorationStats[$type].success -eq 1) { $type.TrimEnd('s') } else { $type } + $summaryParts += "$($restorationStats[$type].success) $pluralForm" + } + } + + if ($summaryParts.Count -gt 0) { + $summary = 'Restored: ' + ($summaryParts -join ', ') + ' from backup' + if ($failureCount -gt 0) { + $summary += " ($failureCount items failed)" + } + $RestoreData.Add($summary) + } elseif ($failureCount -eq 0 -and $successCount -eq 0) { + $RestoreData.Add('No items were restored from backup.') + } + return $RestoreData } - diff --git a/Modules/CIPPCore/Public/Test-CIPPRerun.ps1 b/Modules/CIPPCore/Public/Test-CIPPRerun.ps1 index e09d6c8cfa36..87caf6fc8950 100644 --- a/Modules/CIPPCore/Public/Test-CIPPRerun.ps1 +++ b/Modules/CIPPCore/Public/Test-CIPPRerun.ps1 @@ -12,7 +12,7 @@ function Test-CIPPRerun { [int64]$BaseTime = 0 # Base time to calculate from (defaults to current time) ) $RerunTable = Get-CIPPTable -tablename 'RerunCache' - + # Use custom interval if provided, otherwise use type-based defaults if ($Interval -gt 0) { $EstimatedDifference = $Interval @@ -23,7 +23,7 @@ function Test-CIPPRerun { default { throw "Unknown type: $Type" } } } - + # Use BaseTime if provided, otherwise use current time $CurrentUnixTime = if ($BaseTime -gt 0) { $BaseTime } else { [int][double]::Parse((Get-Date -UFormat %s)) } $EstimatedNextRun = $CurrentUnixTime + $EstimatedDifference From 0f5dd3c824b21b58fdd133c13feca14d15e7c57d Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 1 Jan 2026 20:36:01 -0500 Subject: [PATCH 066/503] Create Test-BackupStorageComparison.ps1 --- Tools/Test-BackupStorageComparison.ps1 | 249 +++++++++++++++++++++++++ 1 file changed, 249 insertions(+) create mode 100644 Tools/Test-BackupStorageComparison.ps1 diff --git a/Tools/Test-BackupStorageComparison.ps1 b/Tools/Test-BackupStorageComparison.ps1 new file mode 100644 index 000000000000..ea14bd8a94bf --- /dev/null +++ b/Tools/Test-BackupStorageComparison.ps1 @@ -0,0 +1,249 @@ +param( + [Parameter(Mandatory = $false)] [string] $ConnectionString = $env:AzureWebJobsStorage, + [Parameter(Mandatory = $false)] [ValidateSet('Small', 'Medium', 'Large', 'All')] [string] $TestSize = 'All', + [Parameter(Mandatory = $false)] [bool] $Cleanup = $true +) + +$ErrorActionPreference = 'Stop' + +# Import CIPPCore module from repository +$modulePath = Join-Path $PSScriptRoot '..' 'Modules' 'CIPPCore' 'CIPPCore.psm1' +if (-not (Test-Path -LiteralPath $modulePath)) { + throw "CIPPCore module not found at $modulePath" +} +Import-Module -Force $modulePath + +if (-not $ConnectionString) { + throw 'Azure Storage connection string not provided. Set AzureWebJobsStorage or pass -ConnectionString.' +} + +Write-Host '================================' -ForegroundColor Cyan +Write-Host 'Backup Storage Comparison Tests' -ForegroundColor Cyan +Write-Host '================================' -ForegroundColor Cyan + +# Test data configurations +$testConfigs = @( + @{ + Name = 'Small' + ItemCount = 10 + PropertiesPerItem = 5 + Description = 'Small payload (~5KB)' + }, + @{ + Name = 'Medium' + ItemCount = 100 + PropertiesPerItem = 15 + Description = 'Medium payload (~250KB)' + }, + @{ + Name = 'Large' + ItemCount = 500 + PropertiesPerItem = 30 + Description = 'Large payload (~2.5MB)' + } +) + +function Generate-TestData { + param( + [int]$ItemCount, + [int]$PropertiesPerItem, + [string]$Type + ) + + $data = @() + for ($i = 0; $i -lt $ItemCount; $i++) { + $item = @{ + id = [guid]::NewGuid().ToString() + rowKey = "item_$i" + timestamp = (Get-Date).ToUniversalTime() + table = $Type + } + + for ($p = 0; $p -lt $PropertiesPerItem; $p++) { + $item["property_$p"] = "This is test property $p with some additional content to make it realistic. Lorem ipsum dolor sit amet." * 3 + } + + $data += $item + } + + return $data +} + +function Test-TableStorage { + param( + [array]$TestData, + [string]$TestName + ) + + Write-Host "`n[TABLE STORAGE] Testing $TestName..." -ForegroundColor Yellow + + $tableName = "TestBackup$(Get-Random -Maximum 100000)" + $Table = Get-CippTable -tablename $tableName + + $jsonString = $TestData | ConvertTo-Json -Depth 100 -Compress + $jsonSizeKB = [math]::Round(($jsonString | Measure-Object -Character).Characters / 1KB, 2) + + Write-Host " JSON Size: $jsonSizeKB KB" + + # Time the storage operation + $stopwatch = [System.Diagnostics.Stopwatch]::StartNew() + try { + $entity = @{ + PartitionKey = 'TestBackup' + RowKey = $TestName + Backup = [string]$jsonString + } + Add-CIPPAzDataTableEntity @Table -Entity $entity -Force -ErrorAction Stop + $stopwatch.Stop() + + Write-Host " Write Time: $($stopwatch.ElapsedMilliseconds)ms" -ForegroundColor Green + Write-Host ' Status: Success ✓' -ForegroundColor Green + + return @{ + Method = 'Table Storage' + TestName = $TestName + Size = $jsonSizeKB + WriteTime = $stopwatch.ElapsedMilliseconds + Success = $true + Details = "Stored in table '$tableName'" + } + } catch { + $stopwatch.Stop() + Write-Host " Status: Failed ✗ - $($_.Exception.Message)" -ForegroundColor Red + + return @{ + Method = 'Table Storage' + TestName = $TestName + Size = $jsonSizeKB + WriteTime = $stopwatch.ElapsedMilliseconds + Success = $false + Details = $_.Exception.Message + } + } +} + +function Test-BlobStorage { + param( + [array]$TestData, + [string]$TestName + ) + + Write-Host "`n[BLOB STORAGE] Testing $TestName..." -ForegroundColor Yellow + + $containerName = 'test-backup-comparison' + $blobName = "backup_$TestName`_$(Get-Random -Maximum 100000).json" + + $jsonString = $TestData | ConvertTo-Json -Depth 100 -Compress + $jsonSizeKB = [math]::Round(($jsonString | Measure-Object -Character).Characters / 1KB, 2) + + Write-Host " JSON Size: $jsonSizeKB KB" + + try { + # Ensure container exists + $containers = @() + try { + $containers = New-CIPPAzStorageRequest -Service 'blob' -Component 'list' -ConnectionString $ConnectionString + } catch { $containers = @() } + + $exists = ($containers | Where-Object { $_.Name -eq $containerName }) -ne $null + if (-not $exists) { + Write-Host " Creating container '$containerName'..." -ForegroundColor Gray + $null = New-CIPPAzStorageRequest -Service 'blob' -Resource $containerName -Method 'PUT' -QueryParams @{ restype = 'container' } -ConnectionString $ConnectionString + Start-Sleep -Milliseconds 500 + } + + # Time the upload operation + $stopwatch = [System.Diagnostics.Stopwatch]::StartNew() + $null = New-CIPPAzStorageRequest -Service 'blob' -Resource "$containerName/$blobName" -Method 'PUT' -ContentType 'application/json; charset=utf-8' -Body $jsonString -ConnectionString $ConnectionString + $stopwatch.Stop() + + Write-Host " Write Time: $($stopwatch.ElapsedMilliseconds)ms" -ForegroundColor Green + Write-Host ' Status: Success ✓' -ForegroundColor Green + Write-Host " Location: $containerName/$blobName" -ForegroundColor Gray + + return @{ + Method = 'Blob Storage' + TestName = $TestName + Size = $jsonSizeKB + WriteTime = $stopwatch.ElapsedMilliseconds + Success = $true + Details = "$containerName/$blobName" + } + } catch { + $stopwatch.Stop() + Write-Host " Status: Failed ✗ - $($_.Exception.Message)" -ForegroundColor Red + + return @{ + Method = 'Blob Storage' + TestName = $TestName + Size = $jsonSizeKB + WriteTime = $stopwatch.ElapsedMilliseconds + Success = $false + Details = $_.Exception.Message + } + } +} + +# Run tests +$results = @() +$configsToRun = if ($TestSize -eq 'All') { $testConfigs } else { $testConfigs | Where-Object { $_.Name -eq $TestSize } } + +foreach ($config in $configsToRun) { + Write-Host "`n`n$($config.Description)" -ForegroundColor Magenta + Write-Host "Generating test data ($($config.ItemCount) items, $($config.PropertiesPerItem) properties)..." -ForegroundColor Gray + + $testData = Generate-TestData -ItemCount $config.ItemCount -PropertiesPerItem $config.PropertiesPerItem -Type "Backup_$($config.Name)" + + # Test table storage + $tableResult = Test-TableStorage -TestData $testData -TestName $config.Name + $results += $tableResult + + Start-Sleep -Milliseconds 500 + + # Test blob storage + $blobResult = Test-BlobStorage -TestData $testData -TestName $config.Name + $results += $blobResult +} + +# Summary +Write-Host "`n`n================================" -ForegroundColor Cyan +Write-Host 'Test Summary' -ForegroundColor Cyan +Write-Host '================================' -ForegroundColor Cyan + +$results | Group-Object -Property TestName | ForEach-Object { + $testGroup = $_ + Write-Host "`n$($testGroup.Name):" -ForegroundColor Magenta + + $testGroup.Group | ForEach-Object { + $status = if ($_.Success) { '✓' } else { '✗' } + Write-Host " $($_.Method): $($_.Size)KB | Write: $($_.WriteTime)ms | $status" -ForegroundColor $(if ($_.Success) { 'Green' } else { 'Red' }) + } +} + +# Detailed comparison +Write-Host "`n`n================================" -ForegroundColor Cyan +Write-Host 'Performance Comparison' -ForegroundColor Cyan +Write-Host '================================' -ForegroundColor Cyan + +$results | Group-Object -Property TestName | ForEach-Object { + $testGroup = $_ + $tableResult = $testGroup.Group | Where-Object { $_.Method -eq 'Table Storage' } + $blobResult = $testGroup.Group | Where-Object { $_.Method -eq 'Blob Storage' } + + if ($tableResult -and $blobResult -and $tableResult.Success -and $blobResult.Success) { + $timeDiff = $blobResult.WriteTime - $tableResult.WriteTime + $timePercentage = [math]::Round(($timeDiff / $tableResult.WriteTime) * 100, 2) + + Write-Host "`n$($testGroup.Name):" -ForegroundColor Magenta + Write-Host " Table Write Time: $($tableResult.WriteTime)ms" -ForegroundColor Gray + Write-Host " Blob Write Time: $($blobResult.WriteTime)ms" -ForegroundColor Gray + + if ($timeDiff -gt 0) { + Write-Host " Blob is $($timeDiff)ms slower ($($timePercentage)% slower)" -ForegroundColor Yellow + } else { + Write-Host " Blob is $((-$timeDiff))ms faster ($($(-$timePercentage))% faster)" -ForegroundColor Green + } + } +} + +Write-Host "`n`nTest Complete!" -ForegroundColor Green From ab15097357c6bcb38a2d97eca469aa71f8ac2d12 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 1 Jan 2026 20:36:18 -0500 Subject: [PATCH 067/503] Update Start-DurableCleanup.ps1 --- .../Timer Functions/Start-DurableCleanup.ps1 | 77 ++++++++++++++++++- 1 file changed, 76 insertions(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-DurableCleanup.ps1 b/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-DurableCleanup.ps1 index 454047a96d65..7abf8bb608f5 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-DurableCleanup.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-DurableCleanup.ps1 @@ -17,5 +17,80 @@ function Start-DurableCleanup { param( [int]$MaxDuration = 86400 ) - Write-Information "This cleanup is no longer required." + + $WarningPreference = 'SilentlyContinue' + $TargetTime = (Get-Date).ToUniversalTime().AddSeconds(-$MaxDuration) + $Context = New-AzDataTableContext -ConnectionString $env:AzureWebJobsStorage + $InstancesTables = Get-AzDataTable -Context $Context | Where-Object { $_ -match 'Instances' } + + $CleanupCount = 0 + $QueueCount = 0 + + $FunctionsWithLongRunningOrchestrators = [System.Collections.Generic.List[object]]::new() + $NonDeterministicOrchestrators = [System.Collections.Generic.List[object]]::new() + + foreach ($Table in $InstancesTables) { + $Table = Get-CippTable -TableName $Table + $FunctionName = $Table.TableName -replace 'Instances', '' + $Orchestrators = Get-CIPPAzDataTableEntity @Table -Filter "RuntimeStatus eq 'Running'" | Select-Object * -ExcludeProperty Input + $Queues = Get-AzStorageQueue -Context $StorageContext -Name ('{0}*' -f $FunctionName) | Select-Object -Property Name, ApproximateMessageCount, QueueClient + $LongRunningOrchestrators = $Orchestrators | Where-Object { $_.CreatedTime.DateTime -lt $TargetTime } + + if ($LongRunningOrchestrators.Count -gt 0) { + $FunctionsWithLongRunningOrchestrators.Add(@{'FunctionName' = $FunctionName }) + foreach ($Orchestrator in $LongRunningOrchestrators) { + $CreatedTime = [DateTime]::SpecifyKind($Orchestrator.CreatedTime.DateTime, [DateTimeKind]::Utc) + $TimeSpan = New-TimeSpan -Start $CreatedTime -End (Get-Date).ToUniversalTime() + $RunningDuration = [math]::Round($TimeSpan.TotalMinutes, 2) + Write-Information "Orchestrator: $($Orchestrator.PartitionKey), created: $CreatedTime, running for: $RunningDuration minutes" + if ($PSCmdlet.ShouldProcess($_.PartitionKey, 'Terminate Orchestrator')) { + $Orchestrator = Get-CIPPAzDataTableEntity @Table -Filter "PartitionKey eq '$($Orchestrator.PartitionKey)'" + $Orchestrator.RuntimeStatus = 'Failed' + if ($Orchestrator.PSObject.Properties.Name -contains 'CustomStatus') { + $Orchestrator.CustomStatus = "Terminated by Durable Cleanup - Exceeded max duration of $MaxDuration seconds" + } else { + $Orchestrator | Add-Member -MemberType NoteProperty -Name CustomStatus -Value "Terminated by Durable Cleanup - Exceeded max duration of $MaxDuration seconds" + } + Update-AzDataTableEntity @Table -Entity $Orchestrator + $CleanupCount++ + } + } + } + + $NonDeterministicOrchestrators = $Orchestrators | Where-Object { $_.Output -match 'Non-Deterministic workflow detected' } + if ($NonDeterministicOrchestrators.Count -gt 0) { + $NonDeterministicOrchestrators.Add(@{'FunctionName' = $FunctionName }) + foreach ($Orchestrator in $NonDeterministicOrchestrators) { + Write-Information "Orchestrator: $($Orchestrator.PartitionKey) is Non-Deterministic" + if ($PSCmdlet.ShouldProcess($_.PartitionKey, 'Terminate Orchestrator')) { + $Orchestrator = Get-CIPPAzDataTableEntity @Table -Filter "PartitionKey eq '$($Orchestrator.PartitionKey)'" + $Orchestrator.RuntimeStatus = 'Failed' + if ($Orchestrator.PSObject.Properties.Name -contains 'CustomStatus') { + $Orchestrator.CustomStatus = 'Terminated by Durable Cleanup - Non-Deterministic workflow detected' + } else { + $Orchestrator | Add-Member -MemberType NoteProperty -Name CustomStatus -Value 'Terminated by Durable Cleanup - Non-Deterministic workflow detected' + } + Update-AzDataTableEntity @Table -Entity $Orchestrator + $CleanupCount++ + } + } + } + + if (($LongRunningOrchestrators.Count -gt 0 -or $NonDeterministicOrchestrators.Count -gt 0) -and $Queues.ApproximateMessageCount -gt 0) { + $RunningQueues = $Queues | Where-Object { $_.ApproximateMessageCount -gt 0 } + foreach ($Queue in $RunningQueues) { + Write-Information "- Removing queue: $($Queue.Name), message count: $($Queue.ApproximateMessageCount)" + if ($PSCmdlet.ShouldProcess($Queue.Name, 'Clear Queue')) { + $Queue.QueueClient.ClearMessagesAsync() | Out-Null + } + $QueueCount++ + } + } + } + + if ($CleanupCount -gt 0 -or $QueueCount -gt 0) { + Write-LogMessage -api 'Durable Cleanup' -message "$CleanupCount orchestrators were terminated. $QueueCount queues were cleared." -sev 'Info' -LogData $FunctionsWithLongRunningOrchestrators + } + + Write-Information "Durable cleanup complete. $CleanupCount orchestrators were terminated. $QueueCount queues were cleared." } From ec101efa877d83b25f34a1477f034b2922773b57 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Fri, 2 Jan 2026 16:45:00 +0800 Subject: [PATCH 068/503] Check accountEnabled property for shared mailbox user Apparently, I removed this a while ago while doing some other stuff... --- .../Reports/Invoke-ListSharedMailboxAccountEnabled.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Reports/Invoke-ListSharedMailboxAccountEnabled.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Reports/Invoke-ListSharedMailboxAccountEnabled.ps1 index 1249f3f14a6c..a0e307c1cba8 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Reports/Invoke-ListSharedMailboxAccountEnabled.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Reports/Invoke-ListSharedMailboxAccountEnabled.ps1 @@ -19,7 +19,7 @@ function Invoke-ListSharedMailboxAccountEnabled { # Match the User $User = $AllUsersInfo | Where-Object { $_.userPrincipalName -eq $SharedMailbox.userPrincipalName } | Select-Object -First 1 - if ($User) { + if ($User.accountEnabled) { # Return all shared mailboxes with license information [PSCustomObject]@{ UserPrincipalName = $User.userPrincipalName From 89828df5c09ee65ac67452fbe6facd585bf25359 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Fri, 2 Jan 2026 13:43:30 +0100 Subject: [PATCH 069/503] Fix deleted data bug --- Modules/CIPPCore/Public/Add-CIPPDbItem.ps1 | 13 +++++++------ .../Entrypoints/HTTP Functions/Invoke-ListTests.ps1 | 4 ++-- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/Modules/CIPPCore/Public/Add-CIPPDbItem.ps1 b/Modules/CIPPCore/Public/Add-CIPPDbItem.ps1 index 212447983b51..1e15830182fd 100644 --- a/Modules/CIPPCore/Public/Add-CIPPDbItem.ps1 +++ b/Modules/CIPPCore/Public/Add-CIPPDbItem.ps1 @@ -42,12 +42,7 @@ function Add-CIPPDbItem { try { $Table = Get-CippTable -tablename 'CippReportingDB' - #Get the existing type entries and nuke them. This ensures we don't have stale data. - $Filter = "PartitionKey eq '{0}' and RowKey ge '{1}-' and RowKey lt '{1}0'" -f $TenantFilter, $Type - $ExistingEntities = Get-CIPPAzDataTableEntity @Table -Filter $Filter - if ($ExistingEntities) { - Remove-AzDataTableEntity @Table -Entity $ExistingEntities -Force | Out-Null - } + if ($Count) { $Entity = @{ @@ -59,6 +54,12 @@ function Add-CIPPDbItem { Add-CIPPAzDataTableEntity @Table -Entity $Entity -Force | Out-Null } else { + #Get the existing type entries and nuke them. This ensures we don't have stale data. + $Filter = "PartitionKey eq '{0}' and RowKey ge '{1}-' and RowKey lt '{1}0'" -f $TenantFilter, $Type + $ExistingEntities = Get-CIPPAzDataTableEntity @Table -Filter $Filter + if ($ExistingEntities) { + Remove-AzDataTableEntity @Table -Entity $ExistingEntities -Force | Out-Null + } $Entities = foreach ($Item in $Data) { $ItemId = $Item.id ? $Item.id : $item.skuId @{ diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ListTests.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ListTests.ps1 index 77465500552b..9e155d08f38b 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ListTests.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ListTests.ps1 @@ -114,11 +114,11 @@ function Invoke-ListTests { $SecureScoreData = New-CIPPDbRequest -TenantFilter $TenantFilter -Type 'SecureScore' if ($SecureScoreData) { - $TestResultsData | Add-Member -NotePropertyName 'SecureScore' -NotePropertyValue $SecureScoreData -Force + $TestResultsData | Add-Member -NotePropertyName 'SecureScore' -NotePropertyValue @($SecureScoreData) -Force } $MFAStateData = New-CIPPDbRequest -TenantFilter $TenantFilter -Type 'MFAState' if ($MFAStateData) { - $TestResultsData | Add-Member -NotePropertyName 'MFAState' -NotePropertyValue $MFAStateData -Force + $TestResultsData | Add-Member -NotePropertyName 'MFAState' -NotePropertyValue @($MFAStateData) -Force } $LicenseData = New-CIPPDbRequest -TenantFilter $TenantFilter -Type 'LicenseOverview' From 1591d0403a7d80358230c3830668b440d6b46b8e Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Fri, 2 Jan 2026 15:59:11 +0100 Subject: [PATCH 070/503] frontend updates --- .../CIPPCore/Public/Add-CippTestResult.ps1 | 2 +- .../HTTP Functions/Invoke-ListTests.ps1 | 17 ++++++++++++++++ .../ZTNA/Devices/Invoke-CippTestZTNA24518.md | 9 +++++++++ .../ZTNA/Devices/Invoke-CippTestZTNA24540.md | 18 +++++++++++++++++ .../ZTNA/Devices/Invoke-CippTestZTNA24541.md | 11 ++++++++++ .../ZTNA/Devices/Invoke-CippTestZTNA24542.md | 12 +++++++++++ .../ZTNA/Devices/Invoke-CippTestZTNA24543.md | 12 +++++++++++ .../ZTNA/Devices/Invoke-CippTestZTNA24545.md | 11 ++++++++++ .../ZTNA/Devices/Invoke-CippTestZTNA24546.md | 14 +++++++++++++ .../ZTNA/Devices/Invoke-CippTestZTNA24547.md | 12 +++++++++++ .../ZTNA/Devices/Invoke-CippTestZTNA24548.md | 14 +++++++++++++ .../ZTNA/Devices/Invoke-CippTestZTNA24549.md | 16 +++++++++++++++ .../ZTNA/Devices/Invoke-CippTestZTNA24550.md | 12 +++++++++++ .../ZTNA/Devices/Invoke-CippTestZTNA24551.md | 11 ++++++++++ .../ZTNA/Devices/Invoke-CippTestZTNA24552.md | 15 ++++++++++++++ .../ZTNA/Devices/Invoke-CippTestZTNA24553.md | 16 +++++++++++++++ .../ZTNA/Devices/Invoke-CippTestZTNA24554.md | 11 ++++++++++ .../ZTNA/Devices/Invoke-CippTestZTNA24555.md | 12 +++++++++++ .../ZTNA/Devices/Invoke-CippTestZTNA24560.md | 14 +++++++++++++ .../ZTNA/Devices/Invoke-CippTestZTNA24561.md | 9 +++++++++ .../ZTNA/Devices/Invoke-CippTestZTNA24564.md | 13 ++++++++++++ .../ZTNA/Devices/Invoke-CippTestZTNA24568.md | 12 +++++++++++ .../ZTNA/Devices/Invoke-CippTestZTNA24569.md | 12 +++++++++++ .../ZTNA/Devices/Invoke-CippTestZTNA24570.md | 9 +++++++++ .../ZTNA/Devices/Invoke-CippTestZTNA24572.md | 16 +++++++++++++++ .../ZTNA/Devices/Invoke-CippTestZTNA24573.md | 11 ++++++++++ .../ZTNA/Devices/Invoke-CippTestZTNA24574.md | 14 +++++++++++++ .../ZTNA/Devices/Invoke-CippTestZTNA24575.md | 13 ++++++++++++ .../ZTNA/Devices/Invoke-CippTestZTNA24576.md | 14 +++++++++++++ .../ZTNA/Devices/Invoke-CippTestZTNA24690.md | 11 ++++++++++ .../ZTNA/Devices/Invoke-CippTestZTNA24784.md | 11 ++++++++++ .../ZTNA/Devices/Invoke-CippTestZTNA24794.md | 10 ++++++++++ .../ZTNA/Devices/Invoke-CippTestZTNA24802.md | 15 ++++++++++++++ .../ZTNA/Devices/Invoke-CippTestZTNA24823.md | 12 +++++++++++ .../ZTNA/Devices/Invoke-CippTestZTNA24824.md | 15 ++++++++++++++ .../ZTNA/Devices/Invoke-CippTestZTNA24827.md | 15 ++++++++++++++ .../ZTNA/Devices/Invoke-CippTestZTNA24839.md | 15 ++++++++++++++ .../ZTNA/Devices/Invoke-CippTestZTNA24840.md | 18 +++++++++++++++++ .../ZTNA/Devices/Invoke-CippTestZTNA24870.md | 15 ++++++++++++++ .../ZTNA/Devices/Invoke-CippTestZTNA24871.md | 12 +++++++++++ .../ZTNA/Devices/Invoke-CippTestZTNA25370.md | 8 ++++++++ .../ZTNA/Devices/Invoke-CippTestZTNA25381.md | 20 +++++++++++++++++++ .../ZTNA/Devices/Invoke-CippTestZTNA25391.md | 12 +++++++++++ .../ZTNA/Devices/Invoke-CippTestZTNA25392.md | 10 ++++++++++ .../ZTNA/Devices/Invoke-CippTestZTNA25399.md | 8 ++++++++ .../ZTNA/Devices/Invoke-CippTestZTNA25405.md | 11 ++++++++++ .../ZTNA/Devices/Invoke-CippTestZTNA25406.md | 8 ++++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21770.md | 10 ++++++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21771.md | 10 ++++++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21772.md | 16 +++++++++++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21773.md | 12 +++++++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21774.md | 13 ++++++++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21775.md | 10 ++++++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21776.md | 14 +++++++++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21777.md | 8 ++++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21778.md | 6 ++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21779.md | 6 ++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21780.md | 8 ++++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21781.md | 13 ++++++++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21782.md | 13 ++++++++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21783.md | 13 ++++++++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21784.md | 12 +++++++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21786.md | 10 ++++++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21787.md | 12 +++++++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21788.md | 13 ++++++++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21789.md | 6 ++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21790.md | 11 ++++++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21791.md | 10 ++++++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21792.md | 10 ++++++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21793.md | 10 ++++++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21795.md | 16 +++++++++++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21796.md | 14 +++++++++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21797.md | 13 ++++++++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21798.md | 8 ++++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21799.md | 8 ++++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21800.md | 13 ++++++++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21801.md | 13 ++++++++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21802.md | 8 ++++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21803.md | 13 ++++++++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21804.md | 12 +++++++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21806.md | 10 ++++++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21807.md | 12 +++++++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21808.md | 8 ++++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21809.md | 11 ++++++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21810.md | 8 ++++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21811.md | 17 ++++++++++++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21812.md | 7 +++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21813.md | 8 ++++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21814.md | 15 ++++++++++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21815.md | 10 ++++++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21816.md | 8 ++++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21817.md | 13 ++++++++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21818.md | 8 ++++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21819.md | 8 ++++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21820.md | 6 ++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21821.md | 6 ++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21822.md | 8 ++++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21823.md | 10 ++++++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21824.md | 12 +++++++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21825.md | 15 ++++++++++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21828.md | 8 ++++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21829.md | 8 ++++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21830.md | 11 ++++++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21831.md | 10 ++++++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21832.md | 6 ++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21833.md | 6 ++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21834.md | 6 ++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21835.md | 8 ++++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21836.md | 8 ++++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21837.md | 8 ++++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21838.md | 10 ++++++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21839.md | 11 ++++++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21840.md | 9 +++++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21841.md | 10 ++++++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21842.md | 10 ++++++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21843.md | 6 ++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21844.md | 10 ++++++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21845.md | 12 +++++++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21846.md | 8 ++++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21847.md | 12 +++++++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21848.md | 10 ++++++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21849.md | 8 ++++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21850.md | 10 ++++++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21851.md | 14 +++++++++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21854.md | 6 ++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21855.md | 6 ++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21857.md | 6 ++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21858.md | 11 ++++++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21859.md | 6 ++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21860.md | 12 +++++++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21861.md | 11 ++++++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21862.md | 9 +++++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21863.md | 11 ++++++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21864.md | 6 ++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21865.md | 7 +++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21866.md | 8 ++++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21867.md | 9 +++++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21868.md | 9 +++++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21869.md | 10 ++++++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21870.md | 9 +++++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21872.md | 8 ++++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21874.md | 10 ++++++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21875.md | 9 +++++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21876.md | 6 ++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21877.md | 15 ++++++++++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21878.md | 7 +++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21879.md | 11 ++++++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21881.md | 6 ++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21882.md | 6 ++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21883.md | 8 ++++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21884.md | 8 ++++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21885.md | 10 ++++++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21886.md | 15 ++++++++++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21887.md | 6 ++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21888.md | 8 ++++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21889.md | 10 ++++++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21890.md | 6 ++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21891.md | 8 ++++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21892.md | 1 + .../ZTNA/Identity/Invoke-CippTestZTNA21893.md | 8 ++++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21894.md | 6 ++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21895.md | 6 ++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21896.md | 9 +++++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21897.md | 6 ++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21898.md | 6 ++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21899.md | 6 ++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21912.md | 6 ++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21929.md | 9 +++++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21941.md | 18 +++++++++++++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21953.md | 10 ++++++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21954.md | 8 ++++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21955.md | 8 ++++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21964.md | 6 ++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21983.md | 6 ++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21984.md | 6 ++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21985.md | 9 +++++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21992.md | 13 ++++++++++++ .../ZTNA/Identity/Invoke-CippTestZTNA22072.md | 11 ++++++++++ .../ZTNA/Identity/Invoke-CippTestZTNA22124.md | 8 ++++++++ .../ZTNA/Identity/Invoke-CippTestZTNA22128.md | 9 +++++++++ .../ZTNA/Identity/Invoke-CippTestZTNA22659.md | 19 ++++++++++++++++++ 181 files changed, 1857 insertions(+), 1 deletion(-) create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24518.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24540.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24541.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24542.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24543.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24545.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24546.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24547.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24548.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24549.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24550.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24551.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24552.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24553.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24554.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24555.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24560.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24561.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24564.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24568.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24569.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24570.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24572.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24573.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24574.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24575.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24576.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24690.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24784.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24794.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24802.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24823.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24824.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24827.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24839.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24840.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24870.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24871.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA25370.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA25381.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA25391.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA25392.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA25399.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA25405.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA25406.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21770.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21771.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21772.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21773.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21774.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21775.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21776.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21777.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21778.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21779.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21780.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21781.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21782.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21783.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21784.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21786.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21787.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21788.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21789.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21790.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21791.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21792.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21793.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21795.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21796.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21797.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21798.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21799.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21800.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21801.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21802.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21803.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21804.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21806.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21807.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21808.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21809.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21810.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21811.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21812.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21813.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21814.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21815.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21816.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21817.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21818.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21819.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21820.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21821.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21822.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21823.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21824.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21825.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21828.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21829.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21830.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21831.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21832.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21833.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21834.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21835.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21836.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21837.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21838.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21839.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21840.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21841.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21842.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21843.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21844.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21845.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21846.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21847.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21848.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21849.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21850.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21851.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21854.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21855.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21857.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21858.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21859.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21860.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21861.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21862.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21863.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21864.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21865.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21866.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21867.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21868.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21869.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21870.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21872.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21874.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21875.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21876.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21877.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21878.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21879.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21881.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21882.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21883.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21884.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21885.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21886.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21887.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21888.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21889.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21890.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21891.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21892.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21893.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21894.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21895.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21896.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21897.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21898.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21899.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21912.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21929.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21941.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21953.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21954.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21955.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21964.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21983.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21984.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21985.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21992.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA22072.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA22124.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA22128.md create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA22659.md diff --git a/Modules/CIPPCore/Public/Add-CippTestResult.ps1 b/Modules/CIPPCore/Public/Add-CippTestResult.ps1 index 446401d69045..4d8bc95bc0f2 100644 --- a/Modules/CIPPCore/Public/Add-CippTestResult.ps1 +++ b/Modules/CIPPCore/Public/Add-CippTestResult.ps1 @@ -47,7 +47,7 @@ function Add-CippTestResult { [Parameter(Mandatory = $true)] [string]$TestId, - [Parameter(Mandatory = $true)] + [Parameter(Mandatory = $false)] [string]$testType = 'identity', [Parameter(Mandatory = $true)] diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ListTests.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ListTests.ps1 index 9e155d08f38b..8f34cbee373e 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ListTests.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ListTests.ps1 @@ -93,6 +93,23 @@ function Invoke-ListTests { $IdentityResults = $TestResultsData.TestResults | Where-Object { $_.TestType -eq 'Identity' } $DeviceResults = $TestResultsData.TestResults | Where-Object { $_.TestType -eq 'Devices' } + # Add descriptions from markdown files to each test result + foreach ($TestResult in $TestResultsData.TestResults) { + $MdFile = Get-ChildItem -Path 'Modules\CIPPCore\Public\Tests' -Filter "*$($TestResult.RowKey).md" -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1 + if ($MdFile) { + try { + $MdContent = Get-Content $MdFile.FullName -Raw -ErrorAction SilentlyContinue + if ($MdContent) { + $Description = ($MdContent -split '')[0].Trim() + $Description = ($Description -split '%TestResult%')[0].Trim() + $TestResult | Add-Member -NotePropertyName 'Description' -NotePropertyValue $Description -Force + } + } catch { + #Test + } + } + } + $TestCounts = @{ Identity = @{ Passed = @($IdentityResults | Where-Object { $_.Status -eq 'Passed' }).Count diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24518.md b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24518.md new file mode 100644 index 000000000000..99c5096fc32b --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24518.md @@ -0,0 +1,9 @@ +Without owners, enterprise applications become orphaned assets that threat actors can exploit through credential harvesting and privilege escalation techniques, as these applications often retain elevated permissions and access to sensitive resources while lacking proper oversight and security governance. The elevation of privilege to owners can raise a security concern in some cases depending on the application's permissions, but more critically, applications without owner create a blind spot in security monitoring where threat actors can establish persistence by leveraging existing application permissions to access data or create backdoor accounts without triggering ownership-based detection mechanisms. When applications lack owners, security teams cannot effectively conduct application lifecycle management, leaving applications with potentially excessive permissions, outdated configurations, or compromised credentials that threat actors can discover through enumeration techniques and exploit to move laterally within the environment. The absence of ownership also prevents proper access reviews and permission audits, allowing threat actors to maintain long-term access through applications that should have been decommissioned or had their permissions reduced, ultimately providing persistent access vectors that can be leveraged for data exfiltration or further compromise of the environment. + + +**Remediation action** + +- [Assign owners to the application](https://learn.microsoft.com/en-us/entra/identity/enterprise-apps/assign-app-owners?pivots=portal) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24540.md b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24540.md new file mode 100644 index 000000000000..ac6dfafe2063 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24540.md @@ -0,0 +1,18 @@ +If policies for Windows Firewall aren't configured and assigned, threat actors can exploit unprotected endpoints to gain unauthorized access, move laterally, and escalate privileges within the environment. Without enforced firewall rules, attackers can bypass network segmentation, exfiltrate data, or deploy malware, increasing the risk of widespread compromise. + +Enforcing Windows Firewall policies ensures consistent application of inbound and outbound traffic controls, reducing exposure to unauthorized access and supporting Zero Trust through network segmentation and device-level protection. + +**Remediation action** + +Configure and assign firewall policies for Windows in Intune to block unauthorized traffic and enforce consistent network protections across all managed devices: + +- [Configure firewall policies for Windows devices](https://learn.microsoft.com/intune/intune-service/protect/endpoint-security-firewall-policy?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci). Intune uses two complementary profiles to manage firewall settings: + - **Windows Firewall** - Use this profile to configure overall firewall behavior based on network type. + - **Windows Firewall rules** - Use this profile to define traffic rules for apps, ports, or IPs, tailored to specific groups or workloads. This Intune profile also supports use of [reusable settings groups](https://learn.microsoft.com/intune/intune-service/protect/endpoint-security-firewall-policy?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci#add-reusable-settings-groups-to-profiles-for-firewall-rules) to help simplify management of common settings you use for different profile instances. +- [Assign policies in Intune](https://learn.microsoft.com/intune/intune-service/configuration/device-profile-assign?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci#assign-a-policy-to-users-or-groups) + +For more information, see: +- [Available Windows Firewall settings](https://learn.microsoft.com/intune/intune-service/protect/endpoint-security-firewall-profile-settings?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci#windows-firewall-profile) + +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24541.md b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24541.md new file mode 100644 index 000000000000..38309e2747a4 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24541.md @@ -0,0 +1,11 @@ +If compliance policies for Windows devices aren't configured and assigned, threat actors can exploit unmanaged or noncompliant endpoints to gain unauthorized access to corporate resources, bypass security controls, and persist within the environment. Without enforced compliance, devices can lack critical security configurations like BitLocker encryption, password requirements, firewall settings, and OS version controls. These gaps increase the risk of data leakage, privilege escalation, and lateral movement. Inconsistent device compliance weakens the organization’s security posture and makes it harder to detect and remediate threats before significant damage occurs. + +Enforcing compliance policies ensures Windows devices meet core security requirements and supports Zero Trust by validating device health and reducing exposure to misconfigured endpoints. + +**Remediation action** + +Create and assign Intune compliance policies to Windows devices to enforce organizational standards for secure access and management: +- [Create and assign Intune compliance policies](https://learn.microsoft.com/intune/intune-service/protect/create-compliance-policy?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci#create-the-policy) +- [Review the Windows compliance settings you can manage with Intune](https://learn.microsoft.com/intune/intune-service/protect/compliance-policy-create-windows?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24542.md b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24542.md new file mode 100644 index 000000000000..9f2fc9e48eb8 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24542.md @@ -0,0 +1,12 @@ +If compliance policies for macOS devices aren't configured and assigned, threat actors can exploit unmanaged or noncompliant endpoints to gain unauthorized access to corporate resources, bypass security controls, and persist within the environment. Without enforced compliance, macOS devices can lack critical security configurations like data storage encryption, password requirements, and OS version controls. These gaps increase the risk of data leakage, privilege escalation, and lateral movement. Inconsistent device compliance weakens the organization’s security posture and makes it harder to detect and remediate threats before significant damage occurs. + +Enforcing compliance policies ensures macOS devices meet core security requirements and supports Zero Trust by validating device health and reducing exposure to misconfigured endpoints. + +**Remediation actions** + +Create and assign Intune compliance policies to macOS devices to enforce organizational standards for secure access and management: +- [Create and assign Intune compliance policies](https://learn.microsoft.com/intune/intune-service/protect/create-compliance-policy?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci#create-the-policy) +- [Review the macOS compliance settings you can manage with Intune](https://learn.microsoft.com/intune/intune-service/protect/compliance-policy-create-mac-os?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) + +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24543.md b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24543.md new file mode 100644 index 000000000000..cc5957a9245a --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24543.md @@ -0,0 +1,12 @@ +If compliance policies aren't assigned to iOS/iPadOS devices in Intune, threat actors can exploit noncompliant endpoints to gain unauthorized access to corporate resources, bypass security controls, and persist in the environment. Without enforced compliance, devices can lack critical security configurations like passcode requirements and OS version controls. These gaps increase the risk of data leakage, privilege escalation, and lateral movement. Inconsistent device compliance weakens the organization’s security posture and makes it harder to detect and remediate threats before significant damage occurs. + +Enforcing compliance policies ensures iOS/iPadOS devices meet core security requirements and supports Zero Trust by validating device health and reducing exposure to misconfigured or unmanaged endpoints. + +**Remediation action** + +Create and assign Intune compliance policies to iOS/iPadOS devices to enforce organizational standards for secure access and management: +- [Create a compliance policy in Microsoft Intune](https://learn.microsoft.com/intune/intune-service/protect/create-compliance-policy?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci#create-the-policy) +- [Review the iOS/iPadOS compliance settings you can manage with Intune](https://learn.microsoft.com/intune/intune-service/protect/compliance-policy-create-ios?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) + +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24545.md b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24545.md new file mode 100644 index 000000000000..7f319eaf0e79 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24545.md @@ -0,0 +1,11 @@ +If compliance policies aren't assigned to fully managed Android Enterprise devices in Intune, threat actors can exploit noncompliant endpoints to gain unauthorized access to corporate resources, bypass security controls, and persist in the environment. Without enforced compliance, devices can lack critical security configurations such as passcode requirements, data storage encryption, and OS version controls. These gaps increase the risk of data leakage, privilege escalation, and lateral movement. Inconsistent device compliance weakens the organization’s security posture and makes it harder to detect and remediate threats before significant damage occurs. + +Enforcing compliance policies ensures Android Enterprise devices meet core security requirements and supports Zero Trust by validating device health and reducing exposure to misconfigured or unmanaged endpoints. + +**Remediation action** + +Create and assign Intune compliance policies to fully managed and corporate-owned Android Enterprise devices to enforce organizational standards for secure access and management: +- [Create a compliance policy in Microsoft Intune](https://learn.microsoft.com/intune/intune-service/protect/create-compliance-policy?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci#create-the-policy) +- [Review the Android Enterprise compliance settings you can manage with Intune](https://learn.microsoft.com/intune/intune-service/protect/compliance-policy-create-android-for-work?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24546.md b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24546.md new file mode 100644 index 000000000000..6f28c5111ac9 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24546.md @@ -0,0 +1,14 @@ +If Windows automatic enrollment isn't enabled, unmanaged devices can become an entry point for attackers. Threat actors might use these devices to access corporate data, bypass compliance policies, and introduce vulnerabilities into the environment. Devices joined to Microsoft Entra without Intune enrollment create gaps in visibility and control. These unmanaged endpoints can expose weaknesses in the operating system or misconfigured applications that attackers can exploit. + +Enforcing automatic enrollment ensures Windows devices are managed from the start, enabling consistent policy enforcement and visibility into compliance. This supports Zero Trust by ensuring all devices are verified, monitored, and governed by security controls. + +**Remediation action** + +Enable automatic enrollment for Windows devices using Intune and Microsoft Entra to ensure all domain-joined or Entra-joined devices are managed: +- [Enable Windows automatic enrollment](https://learn.microsoft.com/intune/intune-service/enrollment/windows-enroll?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci#enable-windows-automatic-enrollment) + +For more information, see: +- [Deployment guide - Enrollment for Windows](https://learn.microsoft.com/intune/intune-service/fundamentals/deployment-guide-enroll?tabs=work-profile%2Ccorporate-owned-apple%2Cautomatic-enrollment&wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci#enrollment-for-windows) + +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24547.md b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24547.md new file mode 100644 index 000000000000..3aaa5280e921 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24547.md @@ -0,0 +1,12 @@ +If compliance policies aren't assigned to Android Enterprise personally owned devices in Intune, threat actors can exploit noncompliant endpoints to gain unauthorized access to corporate resources, bypass security controls, and introduce vulnerabilities. Without enforced compliance, devices can lack critical security configurations like passcode requirements, data storage encryption, and OS version controls. These gaps increase the risk of data leakage and unauthorized access. Inconsistent device compliance weakens the organization’s security posture and makes it harder to detect and remediate threats before significant damage occurs. + +Enforcing compliance policies ensures that personally owned Android devices meet core security requirements and supports Zero Trust by validating device health and reducing exposure to misconfigured or unmanaged endpoints. + +**Remediation action** + +Create and assign Intune compliance policies to Android Enterprise personally owned devices to enforce organizational standards for secure access and management: +- [Create a compliance policy in Microsoft Intune](https://learn.microsoft.com/intune/intune-service/protect/create-compliance-policy?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci#create-the-policy) +- [Review the Android Enterprise compliance settings you can manage with Intune](https://learn.microsoft.com/intune/intune-service/protect/compliance-policy-create-android-for-work?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) + +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24548.md b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24548.md new file mode 100644 index 000000000000..dae6ea117f95 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24548.md @@ -0,0 +1,14 @@ +Without app protection policies, corporate data accessed on iOS/iPadOS devices is vulnerable to leakage through unmanaged or personal apps. Users can unintentionally copy sensitive information into unsecured apps, store data outside corporate boundaries, or bypass authentication controls. This risk is especially high on BYOD devices, where personal and work contexts coexist, increasing the likelihood of data exfiltration or unauthorized access. + +App protection policies ensure corporate data remains secure within approved apps, even on personal devices. These policies enforce encryption, restrict data sharing, and require authentication, reducing the risk of data leakage and aligning with Zero Trust principles of data protection and conditional access. + +**Remediation action** + +Deploy Intune app protection policies that encrypt corporate data, restrict sharing, and require authentication in approved iOS/iPadOS apps: +- [Deploy Intune app protection policies](https://learn.microsoft.com/intune/intune-service/apps/app-protection-policies?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci#create-an-iosipados-or-android-app-protection-policy) +- [Review the iOS app protection settings reference](https://learn.microsoft.com/intune/intune-service/apps/app-protection-policy-settings-ios?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) + +For more information, see: +- [Learn about using app protection policies](https://learn.microsoft.com/intune/intune-service/apps/app-protection-policy?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24549.md b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24549.md new file mode 100644 index 000000000000..2c0db7f420e3 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24549.md @@ -0,0 +1,16 @@ +Without app protection policies, corporate data accessed on Android devices is vulnerable to leakage through unmanaged or malicious apps. Users can unintentionally copy sensitive information into personal apps, store data insecurely, or bypass authentication controls. This risk is amplified on devices that aren't fully managed, where corporate and personal contexts coexist, increasing the likelihood of data exfiltration or unauthorized access. + +Enforcing app protection policies ensures that corporate data is only accessible through trusted apps and remains protected even on personal or BYOD Android devices. + +These policies enforce encryption, restrict data sharing, and require authentication, reducing the risk of data leakage and aligning with Zero Trust principles of data protection and Conditional Access. + +**Remediation action** + +Deploy Intune app protection policies that encrypt data, restrict sharing, and require authentication in approved Android apps: +- [Deploy Intune app protection policies](https://learn.microsoft.com/intune/intune-service/apps/app-protection-policies?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci#create-an-iosipados-or-android-app-protection-policy) +- [Review the Android app protection settings reference](https://learn.microsoft.com/intune/intune-service/apps/app-protection-policy-settings-android?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) + +For more information, see: +- [Learn about using app protection policies](https://learn.microsoft.com/intune/intune-service/apps/app-protection-policy?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24550.md b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24550.md new file mode 100644 index 000000000000..69fd867841a0 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24550.md @@ -0,0 +1,12 @@ +Without a properly configured and assigned BitLocker policy in Intune, threat actors can exploit unencrypted Windows devices to gain unauthorized access to sensitive corporate data. Devices that lack enforced encryption are vulnerable to physical attacks, like disk removal or booting from external media, allowing attackers to bypass operating system security controls. These attacks can result in data exfiltration, credential theft, and further lateral movement within the environment. + +Enforcing BitLocker across managed Windows devices is critical for compliance with data protection regulations and for reducing the risk of data breaches. + +**Remediation action** + +Use Intune to enforce BitLocker encryption and monitor compliance across all managed Windows devices: +- [Create a BitLocker policy for Windows devices in Intune](https://learn.microsoft.com/intune/intune-service/protect/encrypt-devices?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci#create-and-deploy-policy) +- [Assign policies in Intune](https://learn.microsoft.com/intune/intune-service/configuration/device-profile-assign?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci#assign-a-policy-to-users-or-groups) +- [Monitor device encryption with Intune](https://learn.microsoft.com/intune/intune-service/protect/encryption-monitor?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24551.md b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24551.md new file mode 100644 index 000000000000..441a4d65d1a1 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24551.md @@ -0,0 +1,11 @@ +If policies for Windows Hello for Business (WHfB) aren't configured and assigned to all users and devices, threat actors can exploit weak authentication mechanisms—like passwords—to gain unauthorized access. This can lead to credential theft, privilege escalation, and lateral movement within the environment. Without strong, policy-driven authentication like WHfB, attackers can compromise devices and accounts, increasing the risk of widespread impact. + +Enforcing WHfB disrupts this attack chain by requiring strong, multifactor authentication, which helps reduce the risk of credential-based attacks and unauthorized access. + +**Remediation action** + +Deploy Windows Hello for Business in Intune to enforce strong, multifactor authentication: +- [Configure a tenant-wide Windows Hello for Business policy](https://learn.microsoft.com/intune/intune-service/protect/windows-hello?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci#create-a-windows-hello-for-business-policy-for-device-enrollment) that applies at the time a device enrolls with Intune. +- After enrollment, [configure Account protection profiles](https://learn.microsoft.com/intune/intune-service/protect/endpoint-security-account-protection-policy?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci#account-protection-profiles) and [assign](https://learn.microsoft.com/intune/intune-service/configuration/device-profile-assign?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci#assign-a-policy-to-users-or-groups) different configurations for Windows Hello for Business to different groups of users and devices. +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24552.md b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24552.md new file mode 100644 index 000000000000..47161b346847 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24552.md @@ -0,0 +1,15 @@ +Without a centrally managed firewall policy, macOS devices might rely on default or user-modified settings, which often fail to meet corporate security standards. This exposes devices to unsolicited inbound connections, enabling threat actors to exploit vulnerabilities, establish outbound command-and-control (C2) traffic for data exfiltration, and move laterally within the network—significantly escalating the scope and impact of a breach. + +Enforcing macOS Firewall policies ensures consistent control over inbound and outbound traffic, reducing exposure to unauthorized access and supporting Zero Trust through device-level protection and network segmentation. + +**Remediation action** + +Configure and assign **macOS Firewall** profiles in Intune to block unauthorized traffic and enforce consistent network protections across all managed macOS devices: + +- [Configure the built-in firewall on macOS devices](https://learn.microsoft.com/intune/intune-service/protect/endpoint-security-firewall-policy?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) +- [Assign policies in Intune](https://learn.microsoft.com/intune/intune-service/configuration/device-profile-assign?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci#assign-a-policy-to-users-or-groups) + +For more information, see: +- [Available macOS firewall settings](https://learn.microsoft.com/intune/intune-service/protect/endpoint-security-firewall-profile-settings?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci#macos-firewall-profile) +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24553.md b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24553.md new file mode 100644 index 000000000000..23dcce5aa180 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24553.md @@ -0,0 +1,16 @@ +If Windows Update policies aren't enforced across all corporate Windows devices, threat actors can exploit unpatched vulnerabilities to gain unauthorized access, escalate privileges, and move laterally within the environment. The attack chain often begins with device compromise via phishing, malware, or exploitation of known vulnerabilities, and is followed by attempts to bypass security controls. Without enforced update policies, attackers leverage outdated software to persist in the environment, increasing the risk of privilege escalation and domain-wide compromise. + +Enforcing Windows Update policies ensures timely patching of security flaws, disrupting attacker persistence, and reducing the risk of widespread compromise. + +**Remediation action** + +Start with [Manage Windows software updates in Intune](https://learn.microsoft.com/intune/intune-service/protect/windows-update-for-business-configure?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) to understand the available Windows Update policy types and how to configure them. + +Intune includes the following Windows update policy type: +- [Windows quality updates policy](https://learn.microsoft.com/intune/intune-service/protect/windows-quality-update-policy?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) - *to install the regular monthly updates for Windows.* +- [Expedite updates policy](https://learn.microsoft.com/intune/intune-service/protect/windows-10-expedite-updates?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) - *to quickly install critical security patches.* +- [Feature updates policy](https://learn.microsoft.com/intune/intune-service/protect/windows-10-feature-updates?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) +- [Update rings policy](https://learn.microsoft.com/intune/intune-service/protect/windows-10-update-rings?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) - *to manage how and when devices install feature and quality updates.* +- [Windows driver updates](https://learn.microsoft.com/intune/intune-service/protect/windows-driver-updates-overview?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) - *to update hardware components.* + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24554.md b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24554.md new file mode 100644 index 000000000000..780d3db02237 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24554.md @@ -0,0 +1,11 @@ +If iOS update policies aren’t configured and assigned, threat actors can exploit unpatched vulnerabilities in outdated operating systems on managed devices. The absence of enforced update policies allows attackers to use known exploits to gain initial access, escalate privileges, and move laterally within the environment. Without timely updates, devices remain susceptible to exploits that have already been addressed by Apple, enabling threat actors to bypass security controls, deploy malware, or exfiltrate sensitive data. This attack chain begins with device compromise through an unpatched vulnerability, followed by persistence and potential data breach that impacts both organizational security and compliance posture. + +Enforcing update policies disrupts this chain by ensuring devices are consistently protected against known threats. + +**Remediation action** + +Configure and assign iOS/iPadOS update policies in Intune to enforce timely patching and reduce risk from unpatched vulnerabilities: +- [Manage iOS/iPadOS software updates in Intune](https://learn.microsoft.com/intune/intune-service/protect/software-updates-guide-ios-ipados?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) +- [Assign policies in Intune](https://learn.microsoft.com/intune/intune-service/configuration/device-profile-assign?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci#assign-a-policy-to-users-or-groups) +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24555.md b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24555.md new file mode 100644 index 000000000000..9419a8e0b59d --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24555.md @@ -0,0 +1,12 @@ +If Intune scope tags aren't properly configured for delegated administration, attackers who gain privileged access to Intune or Microsoft Entra ID can escalate privileges and access sensitive device configurations across the tenant. Without granular scope tags, administrative boundaries are unclear, allowing attackers to move laterally, manipulate device policies, exfiltrate configuration data, or deploy malicious settings to all users and devices. A single compromised admin account can impact the entire environment. The absence of delegated administration also undermines least-privileged access, making it difficult to contain breaches and enforce accountability. Attackers might exploit global administrator roles or misconfigured role-based access control (RBAC) assignments to bypass compliance policies and gain broad control over device management. + +Enforcing scope tags segments administrative access and aligns it with organizational boundaries. This limits the blast radius of compromised accounts, supports least-privilege access, and aligns with Zero Trust principles of segmentation, role-based control, and containment. + +**Remediation action** + +Use Intune scope tags and RBAC roles to limit admin access based on role, geography, or business unit: +- [Learn how to create and deploy scope tags for distributed IT](https://learn.microsoft.com/intune/intune-service/fundamentals/scope-tags?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) +- [Implement role-based access control with Microsoft Intune](https://learn.microsoft.com/intune/intune-service/fundamentals/role-based-access-control?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) + +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24560.md b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24560.md new file mode 100644 index 000000000000..2e5273c0032d --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24560.md @@ -0,0 +1,14 @@ +Without enforcing Local Administrator Password Solution (LAPS) policies, threat actors who gain access to endpoints can exploit static or weak local administrator passwords to escalate privileges, move laterally, and establish persistence. The attack chain typically begins with device compromise—via phishing, malware, or physical access—followed by attempts to harvest local admin credentials. Without LAPS, attackers can reuse compromised credentials across multiple devices, increasing the risk of privilege escalation and domain-wide compromise. + +Enforcing Windows LAPS on all corporate Windows devices ensures unique, regularly rotated local administrator passwords. This disrupts the attack chain at the credential access and lateral movement stages, significantly reducing the risk of widespread compromise. + +**Remediation action** + +Use Intune to enforce Windows LAPS policies that rotate strong and unique local admin passwords, and that back them up securely: +- [Deploy Windows LAPS policy with Microsoft Intune](https://learn.microsoft.com/intune/intune-service/protect/windows-laps-policy?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci#create-a-laps-policy) + +For more information, see: +- [Windows LAPS policy settings reference](https://learn.microsoft.com/windows-server/identity/laps/laps-management-policy-settings?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) +- [Learn about Intune support for Windows LAPS](https://learn.microsoft.com/intune/intune-service/protect/windows-laps-overview?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24561.md b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24561.md new file mode 100644 index 000000000000..0ab82d59bffc --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24561.md @@ -0,0 +1,9 @@ +If a macOS cloud LAPS (Local Administrator Password Solution) policy is not configured and assigned in Intune, local admin accounts on enrolled macOS devices may remain unmanaged, increasing the risk of unauthorized access, privilege escalation, and lateral movement by threat actors. Without enforced LAPS policies, organizations cannot ensure that admin account credentials are rotated, unique, and securely managed, exposing sensitive systems to potential compromise. + +**Remediation Resources** + +- [Configure macOS LAPS in Microsoft Intune](https://learn.microsoft.com/en-us/intune/intune-service/enrollment/macos-laps) +- [depOnboardingSetting resource type - Microsoft Graph beta](https://learn.microsoft.com/en-us/graph/api/resources/intune-enrollment-deponboardingsetting?view=graph-rest-beta) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24564.md b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24564.md new file mode 100644 index 000000000000..e3746624d5b3 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24564.md @@ -0,0 +1,13 @@ +Without a properly configured and assigned Local Users and Groups policy in Intune, threat actors can exploit unmanaged or misconfigured local accounts on Windows devices. This can lead to unauthorized privilege escalation, persistence, and lateral movement within the environment. If local administrator accounts aren't controlled, attackers can create hidden accounts or elevate privileges, bypassing compliance and security controls. This gap increases the risk of data exfiltration, ransomware deployment, and regulatory noncompliance. + +Ensuring that Local Users and Groups policies are enforced on managed Windows devices, by using account protection profiles, is critical to maintaining a secure and compliant device fleet. + + +**Remediation action** + +Configure and deploy a **Local user group membership** profile from Intune account protection policy to restrict and manage local account usage on Windows devices: +- Create an [Account protection policy for endpoint security in Intune](https://learn.microsoft.com/intune/intune-service/protect/endpoint-security-account-protection-policy?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci#account-protection-profiles) +- [Assign policies in Intune](https://learn.microsoft.com/intune/intune-service/configuration/device-profile-assign?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci#assign-a-policy-to-users-or-groups) + +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24568.md b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24568.md new file mode 100644 index 000000000000..f1363b0e0739 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24568.md @@ -0,0 +1,12 @@ +If Platform SSO policies aren't enforced on macOS devices, endpoints might rely on insecure or inconsistent authentication mechanisms, allowing attackers to bypass Conditional Access and compliance policies. This opens the door to lateral movement across cloud services and on-premises resources, especially when federated identities are used. Threat actors can persist by leveraging stolen tokens or cached credentials and exfiltrate sensitive data through unmanaged apps or browser sessions. The absence of SSO enforcement also undermines app protection policies and device posture assessments, making it difficult to detect and contain breaches. Ultimately, failure to configure and assign macOS Platform SSO policies compromises identity security and weakens the organization's Zero Trust posture. + +Enforcing Platform SSO policies on macOS devices ensures consistent, secure authentication across apps and services. This strengthens identity protection, supports Conditional Access enforcement, and aligns with Zero Trust by reducing reliance on local credentials and improving posture assessments. + +**Remediation action** + +Use Intune to configure and assign Platform SSO policies for macOS devices to enforce secure authentication and strengthen identity protection, see: + +- [Configure Platform SSO for macOS in Intune](https://learn.microsoft.com/intune/intune-service/configuration/platform-sso-macos?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) – *Step-by-step guidance for enabling Platform SSO on macOS devices.* +- [Single sign-on (SSO) overview and options for Apple devices in Microsoft Intune](https://learn.microsoft.com/intune/intune-service/configuration/use-enterprise-sso-plug-in-ios-ipados-macos?pivots=macos&wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) – *Overview of SSO options available for Apple platforms.* +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24569.md b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24569.md new file mode 100644 index 000000000000..b83531c58838 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24569.md @@ -0,0 +1,12 @@ +Without properly configured and assigned FileVault encryption policies in Intune, threat actors can exploit physical access to unmanaged or misconfigured macOS devices to extract sensitive corporate data. Unencrypted devices allow attackers to bypass operating system-level security by booting from external media or removing the storage drive. These attacks can expose credentials, certificates, and cached authentication tokens, enabling privilege escalation and lateral movement. Additionally, unencrypted devices undermine compliance with data protection regulations and increase the risk of reputational damage and financial penalties in the event of a breach. + +Enforcing FileVault encryption protects data at rest on macOS devices, even if lost or stolen. It disrupts credential harvesting and lateral movement, supports regulatory compliance, and aligns with Zero Trust principles of device trust. + +**Remediation action** + +Use Intune to enforce FileVault encryption and monitor compliance on all managed macOS devices: +- [Create a FileVault disk encryption policy for macOS in Intune](https://learn.microsoft.com/intune/intune-service/protect/encrypt-devices-filevault?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci#create-endpoint-security-policy-for-filevault) +- [Assign policies in Intune](https://learn.microsoft.com/intune/intune-service/configuration/device-profile-assign?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci#assign-a-policy-to-users-or-groups) +- [Monitor device encryption with Intune](https://learn.microsoft.com/intune/intune-service/protect/encryption-monitor?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24570.md b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24570.md new file mode 100644 index 000000000000..d25a00389cea --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24570.md @@ -0,0 +1,9 @@ +Microsoft Entra Connect Sync using user accounts instead of service principals creates security vulnerabilities. Legacy user account authentication with passwords is more susceptible to credential theft and password attacks than service principal authentication with certificates. Compromised connector accounts allow threat actors to manipulate identity synchronization, create backdoor accounts, escalate privileges, or disrupt hybrid identity infrastructure. + +**Remediation action** + +- [Configure service principal authentication for Entra Connect](https://learn.microsoft.com/entra/identity/hybrid/connect/authenticate-application-id?tabs=default&wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci#onboard-to-application-based-authentication) +- [Remove legacy Directory Synchronization Accounts](https://learn.microsoft.com/entra/identity/hybrid/connect/authenticate-application-id?tabs=default&wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci#remove-a-legacy-service-account) + +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24572.md b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24572.md new file mode 100644 index 000000000000..a16520f6611d --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24572.md @@ -0,0 +1,16 @@ +Without device enrollment notifications, users might be unaware that their device has been enrolled in Intune—particularly in cases of unauthorized or unexpected enrollment. This lack of visibility can delay user reporting of suspicious activity and increase the risk of unmanaged or compromised devices gaining access to corporate resources. Attackers who obtain user credentials or exploit self-enrollment flows can silently onboard devices, bypassing user scrutiny and enabling data exposure or lateral movement. + +Enrollment notifications provide users with improved visibility into device onboarding activity. They help detect unauthorized enrollment, reinforce secure provisioning practices, and support Zero Trust principles of visibility, verification, and user engagement. + +**Remediation action** + +Configure Intune enrollment notifications to alert users when their device is enrolled and reinforce secure onboarding practices: +- [Set up enrollment notifications in Intune](https://learn.microsoft.com/intune/intune-service/enrollment/enrollment-notifications?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) + + + + + + +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24573.md b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24573.md new file mode 100644 index 000000000000..d769655b0fae --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24573.md @@ -0,0 +1,11 @@ +Without properly configured and assigned Intune security baselines for Windows, devices remain vulnerable to a wide array of attack vectors that threat actors exploit to gain persistence and escalate privileges. Adversaries leverage default Windows configurations that lack hardened security settings to perform lateral movement using techniques like credential dumping, privilege escalation via unpatched vulnerabilities, and exploitation of weak authentication mechanisms. In the absence of enforced security baselines, threat actors can bypass critical security controls, maintain persistence through registry modifications, and exfiltrate sensitive data through unmonitored channels. Failing to implement a defense-in-depth strategy makes devices easier to exploit as attackers progress through the attack chain—from initial access to data exfiltration—ultimately compromising the organization’s security posture and increasing the risk of compliance violations. + +Applying security baselines ensures Windows devices are configured with hardened settings, reducing attack surface, enforcing defense-in-depth, and supporting Zero Trust by standardizing security controls across the environment. + +**Remediation action** + +Configure and assign Intune security baselines to Windows devices to enforce standardized security settings and monitor compliance: +- [Deploy security baselines to help secure Windows devices](https://learn.microsoft.com/intune/intune-service/protect/security-baselines-configure?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci#create-a-profile-for-a-security-baseline) +- [Monitor security baseline compliance](https://learn.microsoft.com/intune/intune-service/protect/security-baselines-monitor?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24574.md b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24574.md new file mode 100644 index 000000000000..4f43396f391a --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24574.md @@ -0,0 +1,14 @@ +If Intune profiles for Attack Surface Reduction (ASR) rules aren't properly configured and assigned to Windows devices, threat actors can exploit unprotected endpoints to execute obfuscated scripts and invoke Win32 API calls from Office macros. These techniques are commonly used in phishing campaigns and malware delivery, allowing attackers to bypass traditional antivirus defenses and gain initial access. Once inside, attackers escalate privileges, establish persistence, and move laterally across the network. Without ASR enforcement, devices remain vulnerable to script-based attacks and macro abuse, undermining the effectiveness of Microsoft Defender and exposing sensitive data to exfiltration. This gap in endpoint protection increases the likelihood of successful compromise and reduces the organization’s ability to contain and respond to threats. + +Enforcing ASR rules helps block common attack techniques such as script-based execution and macro abuse, reducing the risk of initial compromise and supporting Zero Trust by hardening endpoint defenses. + +**Remediation action** + +Use Intune to deploy **Attack Surface Reduction Rules** profiles for Windows devices to block high-risk behaviors and strengthen endpoint protection: +- [Configure Intune profiles for Attack Surface Reduction Rules](https://learn.microsoft.com/intune/intune-service/protect/endpoint-security-asr-policy?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci#devices-managed-by-intune) +- [Assign policies in Intune](https://learn.microsoft.com/intune/intune-service/configuration/device-profile-assign?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci#assign-a-policy-to-users-or-groups) + +For more information, see: +- [Attack surface reduction rules reference](https://learn.microsoft.com/defender-endpoint/attack-surface-reduction-rules-reference?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) in the Microsoft Defender documentation. +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24575.md b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24575.md new file mode 100644 index 000000000000..05f917a22a85 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24575.md @@ -0,0 +1,13 @@ +If policies for Microsoft Defender Antivirus aren't properly configured and assigned in Intune, threat actors can exploit unprotected endpoints to execute malware, disable antivirus protections, and persist within the environment. Without enforced antivirus policies, devices operate with outdated definitions, disabled real-time protection, or misconfigured scan schedules. These gaps allow attackers to bypass detection, escalate privileges, and move laterally across the network. The absence of antivirus enforcement undermines device compliance, increases exposure to zero-day threats, and can result in regulatory noncompliance. Attackers leverage these weaknesses to maintain persistence and evade detection, especially in environments lacking centralized policy enforcement. + +Enforcing Defender Antivirus policies ensures consistent protection against malware, supports real-time threat detection, and aligns with Zero Trust by maintaining a secure and compliant endpoint posture. + +**Remediation action** + +Configure and assign Intune policies for Microsoft Defender Antivirus to enforce real-time protection, maintain up-to-date definitions, and reduce exposure to malware: + +- [Configure Intune policies to manage Microsoft Defender Antivirus](https://learn.microsoft.com/intune/intune-service/protect/endpoint-security-antivirus-policy?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci#windows) +- [Assign policies in Intune](https://learn.microsoft.com/intune/intune-service/configuration/device-profile-assign?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci#assign-a-policy-to-users-or-groups) + +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24576.md b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24576.md new file mode 100644 index 000000000000..a9852ccab073 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24576.md @@ -0,0 +1,14 @@ +If endpoint analytics isn't enabled, threat actors can exploit gaps in device health, performance, and security posture. Without the visibility Endpoint analytics brings, it can be difficult for an organization to detect indicators such as anomalous device behavior, delayed patching, or configuration drift. These gaps allow attackers to establish persistence, escalate privileges, and move laterally across the environment. An absence of analytics data can impede rapid detection and response, allowing attackers to exploit unmonitored endpoints for command and control, data exfiltration, or further compromise. + +Enabling Endpoint Analytics provides visibility into device health and behavior, helping organizations detect risks, respond quickly to threats, and maintain a strong Zero Trust posture. + +**Remediation action** + +Enroll Windows devices into Endpoint Analytics in Intune to monitor device health and identify risks: +- [Enroll Intune devices into Endpoint analytics](https://learn.microsoft.com/intune/analytics/enroll-intune?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) + +For more information, see: +- [What is Endpoint analytics?](https://learn.microsoft.com/intune/analytics?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) + +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24690.md b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24690.md new file mode 100644 index 000000000000..1503d4f1d531 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24690.md @@ -0,0 +1,11 @@ +If macOS update policies aren’t properly configured and assigned, threat actors can exploit unpatched vulnerabilities in macOS devices within the organization. Without enforced update policies, devices remain on outdated software versions, increasing the attack surface for privilege escalation, remote code execution, or persistence techniques. Threat actors can leverage these weaknesses to gain initial access, escalate privileges, and move laterally within the environment. If policies exist but aren’t assigned to device groups, endpoints remain unprotected, and compliance gaps go undetected. This can result in widespread compromise, data exfiltration, and operational disruption. + +Enforcing macOS update policies ensures devices receive timely patches, reducing the risk of exploitation and supporting Zero Trust by maintaining a secure, compliant device fleet. + +**Remediation action** + +Configure and assign macOS update policies in Intune to enforce timely patching and reduce risk from unpatched vulnerabilities: +- [Manage macOS software updates in Intune](https://learn.microsoft.com/intune/intune-service/protect/software-updates-macos?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) + +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24784.md b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24784.md new file mode 100644 index 000000000000..8bb2009ede0f --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24784.md @@ -0,0 +1,11 @@ +If Microsoft Defender Antivirus policies aren't properly configured and assigned to macOS devices in Intune, attackers can exploit unprotected endpoints to execute malware, disable antivirus protections, and persist in the environment. Without enforced policies, devices run outdated definitions, lack real-time protection, or have misconfigured scan schedules, increasing the risk of undetected threats and privilege escalation. This enables lateral movement across the network, credential harvesting, and data exfiltration. The absence of antivirus enforcement undermines device compliance, increases exposure of endpoints to zero-day threats, and can result in regulatory noncompliance. Attackers use these gaps to maintain persistence and evade detection, especially in environments without centralized policy enforcement. + +Enforcing Defender Antivirus policies ensures that macOS devices are consistently protected against malware, supports real-time threat detection, and aligns with Zero Trust by maintaining a secure and compliant endpoint posture. + +**Remediation action** + +Use Intune to configure and assign Microsoft Defender Antivirus policies for macOS devices to enforce real-time protection, maintain up-to-date definitions, and reduce exposure to malware: +- [Configure Intune policies to manage Microsoft Defender Antivirus](https://learn.microsoft.com/intune/intune-service/protect/endpoint-security-antivirus-policy?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci#macos) +- [Assign policies in Intune](https://learn.microsoft.com/intune/intune-service/configuration/device-profile-assign?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci#assign-a-policy-to-users-or-groups) +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24794.md b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24794.md new file mode 100644 index 000000000000..36309a2e234e --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24794.md @@ -0,0 +1,10 @@ +If Terms and Conditions policies aren't configured and assigned in Intune, users can access corporate resources without agreeing to required legal, security, or usage terms. This omission exposes the organization to compliance risks, legal liabilities, and potential misuse of resources. + +Enforcing Terms and Conditions ensures users acknowledge and accept company policies before accessing sensitive data or systems, supporting regulatory compliance and responsible resource use. + +**Remediation action** + +Create and assign Terms and Conditions policies in Intune to require user acceptance before granting access to corporate resources: +- [Create terms and conditions policy](https://learn.microsoft.com/intune/intune-service/enrollment/terms-and-conditions-create?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24802.md b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24802.md new file mode 100644 index 000000000000..33d8505d0aa4 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24802.md @@ -0,0 +1,15 @@ +If device cleanup rules aren't configured in Intune, stale or inactive devices can remain visible in the tenant indefinitely. This leads to cluttered device lists, inaccurate reporting, and reduced visibility into the active device landscape. Unused devices might retain access credentials or tokens, increasing the risk of unauthorized access or misinformed policy decisions. + +Device cleanup rules automatically hide inactive devices from admin views and reports, improving tenant hygiene and reducing administrative burden. This supports Zero Trust by maintaining an accurate and trustworthy device inventory while preserving historical data for audit or investigation. + +**Remediation action** + +Configure Intune device cleanup rules to automatically hide inactive devices from the tenant: +- [Create a device cleanup rule](https://learn.microsoft.com/intune/intune-service/fundamentals/device-cleanup-rules?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci#how-to-create-a-device-cleanup-rule) + +For more information, see: +- [Using Intune device cleanup rules](https://techcommunity.microsoft.com/blog/devicemanagementmicrosoft/using-intune-device-cleanup-rules-updated-version/3760854) *on the Microsoft Tech Community blog* + + +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24823.md b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24823.md new file mode 100644 index 000000000000..846bef7a09b7 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24823.md @@ -0,0 +1,12 @@ +If the Intune Company Portal branding isn't configured to represent your organization’s details, users can encounter a generic interface and lack direct support information. This reduces user trust, increases support overhead, and can lead to confusion or delays in resolving issues. + +Customizing the Company Portal with your organization’s branding and support contact details improves user trust, streamlines support, and reinforces the legitimacy of device management communications. + + +**Remediation action** + +Configure the Intune Company Portal with your organization’s branding and support contact information to enhance user experience and reduce support overhead: +- [Configure the Intune Company Portal](https://learn.microsoft.com/intune/intune-service/apps/company-portal-app?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) + +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24824.md b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24824.md new file mode 100644 index 000000000000..355aaf0621c4 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24824.md @@ -0,0 +1,15 @@ +If Microsoft Entra Conditional Access policies don't enforce device compliance, users can connect to corporate resources from devices that don't meet security standards. This exposes sensitive data to risks like malware, unauthorized access, and regulatory noncompliance. Without controls like encryption enforcement, device health checks, and access restrictions, threat actors can exploit noncompliant devices to bypass security measures and maintain persistence. + + +Requiring device compliance in Conditional Access policies ensures only trusted and secure devices can access corporate resources. This supports Zero Trust by enforcing access decisions based on device health and compliance posture. + +**Remediation action** + +Configure Conditional Access policies in Microsoft Entra to require device compliance before granting access to corporate resources: +- [Create a device compliance-based Conditional Access policy](https://learn.microsoft.com/intune/intune-service/protect/create-conditional-access-intune?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) + +For more information, see: +- [What is Conditional Access?](https://learn.microsoft.com/entra/identity/conditional-access/overview?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) +- [Integrate device compliance results with Conditional Access](https://learn.microsoft.com/intune/intune-service/protect/device-compliance-get-started?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci#integrate-with-conditional-access) +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24827.md b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24827.md new file mode 100644 index 000000000000..a13c08cc8982 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24827.md @@ -0,0 +1,15 @@ +If Microsoft Entra Conditional Access policies aren't combined with app protection controls, users can connect to corporate resources through unmanaged or unsecured applications. This exposes sensitive data to risks such as data leakage, unauthorized access, and regulatory noncompliance. Without safeguards like app-level data protection, access restrictions, and data loss prevention, threat actors can exploit unprotected apps to bypass security controls and compromise organizational data. + +Enforcing Intune app protection policies within Conditional Access ensures only trusted apps can access corporate data. This supports Zero Trust by enforcing access decisions based on app trust, data containment, and usage restrictions. + +**Remediation action** + +Configure app-based Conditional Access policies in Microsoft Entra and Intune to require app protection for access to corporate resources: +- [Set up app-based Conditional Access policies with Intune](https://learn.microsoft.com/intune/intune-service/protect/app-based-conditional-access-intune-create?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) + +For more information, see: +- [What is Conditional Access?](https://learn.microsoft.com/entra/identity/conditional-access/overview?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) +- [Learn about app-based Conditional Access policies with Intune](https://learn.microsoft.com/intune/intune-service/protect/app-based-conditional-access-intune?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) + +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24839.md b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24839.md new file mode 100644 index 000000000000..be254bba4f88 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24839.md @@ -0,0 +1,15 @@ +If Wi-Fi profiles aren't properly configured and assigned, users can connect insecurely or fail to connect to trusted networks, exposing corporate data to interception or unauthorized access. Without centralized management, devices rely on manual configuration, increasing the risk of misconfiguration, weak authentication, and connection to rogue networks. + +Centrally managing Wi-Fi profiles for iOS devices in Intune ensures secure and consistent connectivity to enterprise networks. This enforces authentication and encryption standards, simplifies onboarding, and supports Zero Trust by reducing exposure to untrusted networks. + +**Remediation action** + +Use Intune to configure and assign secure Wi-Fi profiles for iOS/iPadOS devices to enforce authentication and encryption standards: + +- [Deploy Wi-Fi profiles to devices in Microsoft Intune](https://learn.microsoft.com/intune/intune-service/configuration/wi-fi-settings-configure?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci#create-the-profile) + +For more information, see: +- [Review the available Wi-Fi settings for iOS and iPadOS devices in Microsoft Intune](https://learn.microsoft.com/intune/intune-service/configuration/wi-fi-settings-ios?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) + +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24840.md b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24840.md new file mode 100644 index 000000000000..fbfac84b107a --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24840.md @@ -0,0 +1,18 @@ +If Wi-Fi profiles aren't properly configured and assigned, Android devices can fail to connect to secure networks or connect insecurely, exposing corporate data to interception or unauthorized access. Without centralized management, devices rely on manual configuration, increasing the risk of misconfiguration, weak authentication, and connection to rogue networks. + +Centrally managing Wi-Fi profiles for Android devices in Intune ensures secure and consistent connectivity to enterprise networks. This enforces authentication and encryption standards, simplifies onboarding, and supports Zero Trust by reducing exposure to untrusted networks. + + + +Use Intune to configure secure Wi-Fi profiles that enforce authentication and encryption standards. + +**Remediation action** + +Use Intune to configure and assign secure Wi-Fi profiles for Android devices to enforce authentication and encryption standards: +- [Deploy Wi-Fi profiles to devices in Microsoft Intune](https://learn.microsoft.com/intune/intune-service/configuration/wi-fi-settings-configure?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci#create-the-profile) + +For more information, see: +- [Review the available Wi-Fi settings for Android devices in Microsoft Intune](https://learn.microsoft.com/intune/intune-service/configuration/wi-fi-settings-android-enterprise?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) + +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24870.md b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24870.md new file mode 100644 index 000000000000..d0b2121a7e67 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24870.md @@ -0,0 +1,15 @@ +If Wi-Fi profiles aren't properly configured and assigned, macOS devices can fail to connect to secure networks or connect insecurely, exposing corporate data to interception or unauthorized access. Without centralized management, devices rely on manual configuration, increasing the risk of misconfiguration, weak authentication, and connection to rogue networks. These gaps can lead to data interception, unauthorized network access, and compliance violations. + +Centrally managing Wi-Fi profiles for macOS devices in Intune ensures secure and consistent connectivity to enterprise networks. This enforces authentication and encryption standards, simplifies onboarding, and supports Zero Trust by reducing exposure to untrusted networks. + +**Remediation action** + +Use Intune to configure and assign secure Wi-Fi profiles for macOS devices to enforce authentication and encryption standards: + +- [Configure Wi-Fi settings for macOS devices in Intune](https://learn.microsoft.com/intune/intune-service/configuration/wi-fi-settings-configure?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci#create-the-profile) + +For more information, see: + +- [Review the available Wi-Fi settings for macOS devices in Microsoft Intune](https://learn.microsoft.com/intune/intune-service/configuration/wi-fi-settings-macos?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24871.md b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24871.md new file mode 100644 index 000000000000..a326a1b7c66d --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24871.md @@ -0,0 +1,12 @@ +If automatic enrollment into Microsoft Defender for Endpoint isn't configured for Android devices in Intune, managed endpoints might remain unprotected against mobile threats. Without Defender onboarding, devices lack advanced threat detection and response capabilities, increasing the risk of malware, phishing, and other mobile-based attacks. Unprotected devices can bypass security policies, access corporate resources, and expose sensitive data to compromise. This gap in mobile threat defense weakens the organization's Zero Trust posture and reduces visibility into endpoint health. + +Enabling automatic Defender enrollment ensures Android devices are protected by advanced threat detection and response capabilities. This supports Zero Trust by enforcing mobile threat protection, improving visibility, and reducing exposure to unmanaged or compromised endpoints. + +**Remediation action** + +Use Intune to configure automatic enrollment into Microsoft Defender for Endpoint for Android devices to enforce mobile threat protection: + +- [Integrate Microsoft Defender for Endpoint with Intune and Onboard Devices](https://learn.microsoft.com/intune/intune-service/protect/advanced-threat-protection-configure?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) + +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA25370.md b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA25370.md new file mode 100644 index 000000000000..7292aeb5fa36 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA25370.md @@ -0,0 +1,8 @@ +When organizations deploy Global Secure Access as their cloud-based network proxy, user traffic is routed through Microsoft's Secure Service Edge infrastructure. Without source IP restoration enabled, all authentication requests and resource access appear to originate from the proxy's IP address rather than the user's actual public egress IP. This creates a significant security gap: threat actors who compromise user credentials can authenticate from any location, and the organization's Conditional Access policies that rely on IP-based location controls become ineffective since all traffic appears to come from the same proxy addresses. Microsoft Entra ID Protection risk detections lose visibility into the original user IP address, degrading the accuracy of risk scoring algorithms that depend on geographic and network anomaly detection. Sign-in logs and audit trails no longer reflect the true source of authentication attempts, hampering incident investigation and forensic analysis. A threat actor exploiting this gap could perform credential stuffing or phishing attacks and subsequently authenticate to tenant resources while bypassing named location policies, trusted IP controls, and IP-based continuous access evaluation enforcement. The attacker's activity would blend with legitimate proxy traffic, delaying detection and extending dwell time. Enabling Global Secure Access signalling in Conditional Access restores the original user source IP to Microsoft Entra ID, Microsoft Graph, sign-in logs, and audit logs, preserving the integrity of IP-based security controls and risk assessments. + +**Remediation action** +- [Enable Global Secure Access signaling in Conditional Access](https://learn.microsoft.com/en-us/entra/global-secure-access/how-to-source-ip-restoration) + +- [Use Microsoft Graph API to enable signaling](https://learn.microsoft.com/en-us/graph/api/networkaccess-conditionalaccesssettings-update ) + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA25381.md b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA25381.md new file mode 100644 index 000000000000..4926d8187932 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA25381.md @@ -0,0 +1,20 @@ +Traffic forwarding profiles are the foundational mechanism through which Global Secure Access captures and routes network traffic to Microsoft's Security Service Edge (SSE) infrastructure. Without enabling the appropriate traffic forwarding profiles, network traffic bypasses the Global Secure Access service entirely, leaving users without zero trust network access protections. + +There are three distinct profiles: the **Microsoft traffic profile** captures Microsoft Entra ID, Microsoft Graph, SharePoint Online, Exchange Online, and other Microsoft 365 workloads; the **Private Access profile** captures traffic destined for internal corporate resources configured through Quick Access or per-app access; and the **Internet Access profile** captures traffic to the public internet including non-Microsoft SaaS applications. + +When these profiles are disabled, corresponding network traffic is not tunneled through Global Secure Access, meaning security policies, web content filtering, threat protection, and Universal Continuous Access Evaluation cannot be enforced. A threat actor who compromises user credentials can access corporate resources without the security controls that Global Secure Access would otherwise apply. + +For **Private Access**, disabled profiles mean remote users cannot securely connect to internal applications, file servers, or Remote Desktop sessions through the zero-trust model—potentially forcing fallback to legacy VPN solutions with broader network access. + +For **Internet Access**, disabled profiles mean users accessing external SaaS applications, collaboration tools, or web resources are not protected by security policies, and data exfiltration to unauthorized internet destinations cannot be prevented. + +**Remediation action** + +Enable all traffic forwarding profiles to ensure comprehensive protection: + +- [Enable the Microsoft 365 traffic forwarding profile](https://learn.microsoft.com/en-us/entra/global-secure-access/how-to-manage-microsoft-profile) +- [Enable the Private Access traffic forwarding profile](https://learn.microsoft.com/en-us/entra/global-secure-access/how-to-manage-private-access-profile) +- [Enable the Internet Access traffic forwarding profile](https://learn.microsoft.com/en-us/entra/global-secure-access/how-to-manage-internet-access-profile) +- [Understand traffic forwarding profile concepts](https://learn.microsoft.com/en-us/entra/global-secure-access/concept-traffic-forwarding) + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA25391.md b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA25391.md new file mode 100644 index 000000000000..91d0f532b3ed --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA25391.md @@ -0,0 +1,12 @@ +When Entra Private Network Connectors are inactive or unhealthy, threat actors operating under assume breach conditions can exploit the lack of secure remote access control. Connectors create outbound connections to the Private Access services to reach internal resources, and when these connectors fail, organizations may resort to alternatives such as exposing applications directly or using less secure access methods. This creates initial access opportunities where threat actors can target externally exposed services or leverage compromised VPN credentials. Following successful authentication through weakened access controls, threat actors can establish persistence by maintaining access to internal resources that would otherwise require connector-based authentication and authorization checks. + +The absence of functional connectors eliminates the token-based authentication and authorization performed for all Private Access scenarios, enabling lateral movement as threat actors traverse the network without the granular access controls enforced by connector groups. The service routes new requests to an available connector, and if a connector is temporarily unavailable, it does not receive traffic meaning connector failures directly disrupt zero trust network access controls. Organizations may then implement workarounds that bypass intended security boundaries, facilitating privilege escalation as threat actors exploit the degraded security posture to access resources beyond their authorization scope. + +**Remediation action** + +- [Troubleshoot connector installation and connectivity issues](https://learn.microsoft.com/en-us/entra/global-secure-access/troubleshoot-connectors) +- [Configure connectors for high availability](https://learn.microsoft.com/en-us/entra/global-secure-access/how-to-configure-connectors) +- [Monitor connector health and performance](https://learn.microsoft.com/en-us/entra/global-secure-access/concept-connectors) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA25392.md b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA25392.md new file mode 100644 index 000000000000..2093de19c302 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA25392.md @@ -0,0 +1,10 @@ +The Private Network Connector is a key component of Entra Private Access and Entra Application Proxy. To maintain security, stability, and performance, it's essential that all connector machines run the latest software version. This check reviews every private network connector in your environment, compares the installed version with the most recent release, and flags any connectors that are not up to date. If any connector is outdated, the check will fail and provide a detailed list of current versions. + +**Remediation action** + +Please check this article which shows the release notes and latest version of the private network connector. +- [Microsoft Entra private network connector version release notes - Global Secure Access](https://learn.microsoft.com/entra/global-secure-access/reference-version-history) + +**Note**: Please be aware that not every connector update is an auto-update and some need to be applied manually. Auto-update will only work if the connector updater process on your machine is running. + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA25399.md b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA25399.md new file mode 100644 index 000000000000..e62759175a00 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA25399.md @@ -0,0 +1,8 @@ +Without Private DNS configuration, remote users cannot resolve internal domain names through Entra Private Access, forcing them to rely on public DNS servers or manually configure DNS settings. Threat actors can exploit this gap through DNS spoofing attacks, where corrupt DNS data is introduced into resolver caches, causing name servers to return incorrect IP addresses. When users attempt to access internal resources by FQDN without proper DNS resolution through the secure tunnel, threat actors can redirect users from legitimate websites to sites of the attacker's choosing. This enables credential harvesting as users authenticate to what appears to be the correct internal resource but is actually controlled by the threat actor. Through this redirection, threat actors can steal sensitive data from users who believe they are accessing legitimate internal systems. The compromised credentials can then be used to establish persistence within the environment by creating additional access paths or escalating privileges. Without centralized DNS resolution through Private Access, organizations lose visibility into DNS queries and cannot apply consistent security policies, making it harder to detect when threat actors are performing reconnaissance or establishing command and control channels through DNS tunneling. + +**Remediation action** + +- [Enable Private DNS and configure DNS suffix segments for internal domains](https://learn.microsoft.com/en-us/entra/global-secure-access/concept-private-name-resolution) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA25405.md b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA25405.md new file mode 100644 index 000000000000..40ef6074e0d4 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA25405.md @@ -0,0 +1,11 @@ +Intelligent Local Access (ILA) is a key capability that merges ZTNA policy enforcement with the efficiency of routing Private Access traffic locally—instead of sending all data through the cloud backend, as is typical with standard Private Access. Using ILA is crucial; otherwise, users may turn off Private Access in the GSA client to boost performance, which would bypass all ZTNA policy controls such as user assignment or Entra ID conditional access. + +This verification ensures that private networks are set up within the Entra ID tenant. If private networks exist, the check is successful, indicating that Intelligent Local Access is being used. + +**Remediation action** + +You should consider configuring one or multiple private networks for your user sites and assigning the appropriate applications to it. This will ensure that private access traffic is routed locally when the user is located at these sites to improve performance while maintaining ZTNA policy enforcement. + +- [Enable Intelligent Local Network - Global Secure Access](https://learn.microsoft.com/en-us/entra/global-secure-access/enable-intelligent-local-access?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA25406.md b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA25406.md new file mode 100644 index 000000000000..6c579aa18ae3 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA25406.md @@ -0,0 +1,8 @@ +When the Internet Access forwarding profile remains disabled, users access internet resources without routing traffic through the Secure Web Gateway, bypassing security controls that block threats, malicious content, and unsafe destinations. Threat actors exploit this gap by delivering malware, establishing command and control connections, or exfiltrating data through unmonitored internet channels. Without sufficient controls to prevent unauthorized access, threat actors leverage compromised credentials or social engineering to establish initial access, then use unfiltered internet connectivity to download tools, establish persistence mechanisms, or communicate with external infrastructure. Organizations lose visibility into internet traffic patterns through Traffic Logs, preventing detection of data exfiltration attempts, connections to known malicious domains, or unauthorized access to external resources. The absence of identity-based access controls for internet traffic enables threat actors operating from compromised accounts to blend with normal user behavior, accessing external resources to stage attacks, download exploitation frameworks, or communicate with adversary infrastructure without triggering security alerts based on user context, device compliance, or location. + +**Remediation action** + +- [Enable Internet Access forwarding profile to route traffic through the Secure Web Gateway](https://learn.microsoft.com/en-us/entra/global-secure-access/how-to-manage-internet-access-profile) +- [Assign users and groups to the Internet Access profile to scope traffic forwarding to specific users](https://learn.microsoft.com/en-us/entra/global-secure-access/concept-traffic-forwarding) + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21770.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21770.md new file mode 100644 index 000000000000..63936bdc8ea5 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21770.md @@ -0,0 +1,10 @@ +Attackers might exploit valid but inactive applications that still have elevated privileges. These applications can be used to gain initial access without raising alarm because they’re legitimate applications. From there, attackers can use the application privileges to plan or execute other attacks. Attackers might also maintain access by manipulating the inactive application, such as by adding credentials. This persistence ensures that even if their primary access method is detected, they can regain access later. + +**Remediation action** + +- [Disable privileged service principals](https://learn.microsoft.com/graph/api/serviceprincipal-update?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) +- Investigate if the application has legitimate use cases +- [If service principal doesn't have legitimate use cases, delete it](https://learn.microsoft.com/graph/api/serviceprincipal-delete?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) + +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21771.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21771.md new file mode 100644 index 000000000000..198c587988cc --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21771.md @@ -0,0 +1,10 @@ +Attackers might exploit valid but inactive applications that still have elevated privileges. These applications can be used to gain initial access without raising alarm because they're legitimate applications. From there, attackers can use the application privileges to plan or execute other attacks. Attackers might also maintain access by manipulating the inactive application, such as by adding credentials. This persistence ensures that even if their primary access method is detected, they can regain access later. + +**Remediation action** + +- [Disable inactive privileged service principals](https://learn.microsoft.com/graph/api/serviceprincipal-update?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) +- Investigate if the application has legitimate use cases. If so, [analyze if a OAuth2 permission is a better fit](https://learn.microsoft.com/entra/identity-platform/v2-app-types?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) +- [If service principal doesn't have legitimate use cases, delete it](https://learn.microsoft.com/graph/api/serviceprincipal-delete?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) + +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21772.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21772.md new file mode 100644 index 000000000000..ef04040bf051 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21772.md @@ -0,0 +1,16 @@ +Applications that use client secrets might store them in configuration files, hardcode them in scripts, or risk their exposure in other ways. The complexities of secret management make client secrets susceptible to leaks and attractive to attackers. Client secrets, when exposed, provide attackers with the ability to blend their activities with legitimate operations, making it easier to bypass security controls. If an attacker compromises an application's client secret, they can escalate their privileges within the system, leading to broader access and control, depending on the permissions of the application. + +Applications and service principals that have permissions for Microsoft Graph APIs or other APIs have a higher risk because an attacker can potentially exploit these additional permissions. + +**Remediation action** + +- [Move applications away from shared secrets to managed identities and adopt more secure practices](https://learn.microsoft.com/entra/identity/enterprise-apps/migrate-applications-from-secrets?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci). + - Use managed identities for Azure resources + - Deploy Conditional Access policies for workload identities + - Implement secret scanning + - Deploy application authentication policies to enforce secure authentication practices + - Create a least-privileged custom role to rotate application credentials + - Ensure you have a process to triage and monitor applications + +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21773.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21773.md new file mode 100644 index 000000000000..7e776cd485e2 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21773.md @@ -0,0 +1,12 @@ +Certificates, if not securely stored, can be extracted and exploited by attackers, leading to unauthorized access. Long-lived certificates are more likely to be exposed over time. Credentials, when exposed, provide attackers with the ability to blend their activities with legitimate operations, making it easier to bypass security controls. If an attacker compromises an application's certificate, they can escalate their privileges within the system, leading to broader access and control, depending on the privileges of the application. + +**Remediation action** + +- [Define certificate based application configuration](https://devblogs.microsoft.com/identity/app-management-policy/) +- [Define trusted certificate authorities for apps and service principals in the tenant](https://learn.microsoft.com/graph/api/resources/certificatebasedapplicationconfiguration?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) +- [Define application management policies](https://learn.microsoft.com/graph/api/resources/applicationauthenticationmethodpolicy?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) +- [Enforce secret and certificate standards](https://learn.microsoft.com/en-us/entra/identity/enterprise-apps/tutorial-enforce-secret-standards?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) +- [Create a least-privileged custom role to rotate application credentials](https://learn.microsoft.com/entra/identity/role-based-access-control/custom-create?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) + +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21774.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21774.md new file mode 100644 index 000000000000..71fed1768f8a --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21774.md @@ -0,0 +1,13 @@ +Microsoft services applications that operate in your tenant are identified as service principals with the owner organization ID "f8cdef31-a31e-4b4a-93e4-5f571e91255a." When these service principals have credentials configured in your tenant, they might create potential attack vectors that threat actors can exploit. If an administrator added the credentials and they're no longer needed, they can become a target for attackers. Although less likely when proper preventive and detective controls are in place on privileged activities, threat actors can also maliciously add credentials. In either case, threat actors can use these credentials to authenticate as the service principal, gaining the same permissions and access rights as the Microsoft service application. This initial access can lead to privilege escalation if the application has high-level permissions, allowing lateral movement across the tenant. Attackers can then proceed to data exfiltration or persistence establishment through creating other backdoor credentials. + +When credentials (like client secrets or certificates) are configured for these service principals in your tenant, it means someone - either an administrator or a malicious actor - enabled them to authenticate independently within your environment. These credentials should be investigated to determine their legitimacy and necessity. If they're no longer needed, they should be removed to reduce the risk. + +If this check doesn't pass, the recommendation is to "investigate" because you need to identify and review any applications with unused credentials configured. + +**Remediation action** + +- Confirm if the credentials added are still valid use cases. If not, remove credentials from Microsoft service applications to reduce security risk. + - In the Microsoft Entra admin center, browse to **Entra ID** > **App registrations** and select the affected application. + - Go to the **Certificates & secrets** section and remove any credentials that are no longer needed. +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21775.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21775.md new file mode 100644 index 000000000000..b2245df4e438 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21775.md @@ -0,0 +1,10 @@ +Without proper application management policies, threat actors can exploit weak or misconfigured application credentials to get unauthorized access to organizational resources. Applications using long-lived password secrets or certificates create extended attack windows where compromised credentials stay valid for extended periods. If an application uses client secrets that are hardcoded in configuration files or have weak password requirements, threat actors can extract these credentials through different means, including source code repositories, configuration dumps, or memory analysis. If threat actors get these credentials, they can perform lateral movement within the environment, escalate privileges if the application has elevated permissions, establish persistence by creating more backdoor credentials, modify application configuration, or exfiltrate data. The lack of credential lifecycle management lets compromised credentials remain active indefinitely, giving threat actors sustained access to organizational assets and the ability to conduct data exfiltration, system manipulation, or deploy more malicious tools without detection. + +Configuring appropriate app management policies helps organizations stay ahead of these threats. + +**Remediation action** + +- [Learn how to enforce secret and certificate standards using application management policies](https://learn.microsoft.com/entra/identity/enterprise-apps/tutorial-enforce-secret-standards?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) + +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21776.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21776.md new file mode 100644 index 000000000000..9dbaf31c5b70 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21776.md @@ -0,0 +1,14 @@ +Without restricted user consent settings, threat actors can exploit permissive application consent configurations to gain unauthorized access to sensitive organizational data. When user consent is unrestricted, attackers can: + +- Use social engineering and illicit consent grant attacks to trick users into approving malicious applications. +- Impersonate legitimate services to request broad permissions, such as access to email, files, calendars, and other critical business data. +- Obtain legitimate OAuth tokens that bypass perimeter security controls, making access appear normal to security monitoring systems. +- Establish persistent access to organizational resources, conduct reconnaissance across Microsoft 365 services, move laterally through connected systems, and potentially escalate privileges. + +Unrestricted user consent also limits an organization's ability to enforce centralized governance over application access, making it difficult to maintain visibility into which non-Microsoft applications have access to sensitive data. This gap creates compliance risks where unauthorized applications might violate data protection regulations or organizational security policies. + +**Remediation action** + +- [Configure restricted user consent settings](https://learn.microsoft.com/en-us/entra/identity/enterprise-apps/configure-user-consent?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) to prevent illicit consent grants by disabling user consent or limiting it to verified publishers with low-risk permissions only. +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21777.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21777.md new file mode 100644 index 000000000000..7f418136d2ed --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21777.md @@ -0,0 +1,8 @@ +App instance property lock prevents changes to sensitive properties of a multitenant application after the application is provisioned in another tenant. Without a lock, critical properties such as application credentials can be maliciously or unintentionally modified, causing disruptions, increased risk, unauthorized access, or privilege escalations. + +**Remediation action** +Enable the app instance property lock for all multitenant applications and specify the properties to lock. +- [Configure an app instance lock](https://learn.microsoft.com/en-us/entra/identity-platform/howto-configure-app-instance-property-locks?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci#configure-an-app-instance-lock) + +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21778.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21778.md new file mode 100644 index 000000000000..6a740c4322e2 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21778.md @@ -0,0 +1,6 @@ +... + +**Remediation action** + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21779.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21779.md new file mode 100644 index 000000000000..6a740c4322e2 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21779.md @@ -0,0 +1,6 @@ +... + +**Remediation action** + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21780.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21780.md new file mode 100644 index 000000000000..f3911fd1bf14 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21780.md @@ -0,0 +1,8 @@ +Microsoft ended support and security fixes for ADAL on June 30, 2023. Continued ADAL usage bypasses modern security protections available only in MSAL, including Conditional Access enforcement, Continuous Access Evaluation (CAE), and advanced token protection. ADAL applications create security vulnerabilities by using weaker legacy authentication patterns, often calling deprecated Azure AD Graph endpoints, and preventing adoption of hardened authentication flows that could mitigate future security advisories. + +**Remediation action** + +- [Migrate applications to the Microsoft Authentication Library (MSAL)](https://learn.microsoft.com/entra/identity-platform/msal-migration?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) + +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21781.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21781.md new file mode 100644 index 000000000000..4a9fec301780 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21781.md @@ -0,0 +1,13 @@ +Without phishing-resistant authentication methods, privileged users are more vulnerable to phishing attacks. These types of attacks trick users into revealing their credentials to grant unauthorized access to attackers. If non-phishing-resistant authentication methods are used, attackers might intercept credentials and tokens, through methods like adversary-in-the-middle attacks, undermining the security of the privileged account. + +Once a privileged account or session is compromised due to weak authentication methods, attackers might manipulate the account to maintain long-term access, create other backdoors, or modify user permissions. Attackers can also use the compromised privileged account to escalate their access even further, potentially gaining control over more sensitive systems. + +**Remediation action** + +- [Get started with a phishing-resistant passwordless authentication deployment](https://learn.microsoft.com/entra/identity/authentication/how-to-plan-prerequisites-phishing-resistant-passwordless-authentication?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) +- [Ensure that privileged accounts register and use phishing resistant methods](https://learn.microsoft.com/entra/identity/authentication/concept-authentication-strengths?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci#authentication-strengths) +- [Deploy a Conditional Access policy to target privileged accounts and require phishing resistant credentials](https://learn.microsoft.com/entra/identity/conditional-access/policy-admin-phish-resistant-mfa?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) +- [Monitor authentication method activity](https://learn.microsoft.com/entra/identity/monitoring-health/concept-usage-insights-report?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci#authentication-methods-activity) + +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21782.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21782.md new file mode 100644 index 000000000000..4a9fec301780 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21782.md @@ -0,0 +1,13 @@ +Without phishing-resistant authentication methods, privileged users are more vulnerable to phishing attacks. These types of attacks trick users into revealing their credentials to grant unauthorized access to attackers. If non-phishing-resistant authentication methods are used, attackers might intercept credentials and tokens, through methods like adversary-in-the-middle attacks, undermining the security of the privileged account. + +Once a privileged account or session is compromised due to weak authentication methods, attackers might manipulate the account to maintain long-term access, create other backdoors, or modify user permissions. Attackers can also use the compromised privileged account to escalate their access even further, potentially gaining control over more sensitive systems. + +**Remediation action** + +- [Get started with a phishing-resistant passwordless authentication deployment](https://learn.microsoft.com/entra/identity/authentication/how-to-plan-prerequisites-phishing-resistant-passwordless-authentication?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) +- [Ensure that privileged accounts register and use phishing resistant methods](https://learn.microsoft.com/entra/identity/authentication/concept-authentication-strengths?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci#authentication-strengths) +- [Deploy a Conditional Access policy to target privileged accounts and require phishing resistant credentials](https://learn.microsoft.com/entra/identity/conditional-access/policy-admin-phish-resistant-mfa?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) +- [Monitor authentication method activity](https://learn.microsoft.com/entra/identity/monitoring-health/concept-usage-insights-report?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci#authentication-methods-activity) + +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21783.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21783.md new file mode 100644 index 000000000000..4a9fec301780 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21783.md @@ -0,0 +1,13 @@ +Without phishing-resistant authentication methods, privileged users are more vulnerable to phishing attacks. These types of attacks trick users into revealing their credentials to grant unauthorized access to attackers. If non-phishing-resistant authentication methods are used, attackers might intercept credentials and tokens, through methods like adversary-in-the-middle attacks, undermining the security of the privileged account. + +Once a privileged account or session is compromised due to weak authentication methods, attackers might manipulate the account to maintain long-term access, create other backdoors, or modify user permissions. Attackers can also use the compromised privileged account to escalate their access even further, potentially gaining control over more sensitive systems. + +**Remediation action** + +- [Get started with a phishing-resistant passwordless authentication deployment](https://learn.microsoft.com/entra/identity/authentication/how-to-plan-prerequisites-phishing-resistant-passwordless-authentication?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) +- [Ensure that privileged accounts register and use phishing resistant methods](https://learn.microsoft.com/entra/identity/authentication/concept-authentication-strengths?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci#authentication-strengths) +- [Deploy a Conditional Access policy to target privileged accounts and require phishing resistant credentials](https://learn.microsoft.com/entra/identity/conditional-access/policy-admin-phish-resistant-mfa?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) +- [Monitor authentication method activity](https://learn.microsoft.com/entra/identity/monitoring-health/concept-usage-insights-report?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci#authentication-methods-activity) + +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21784.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21784.md new file mode 100644 index 000000000000..49b91b174bf7 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21784.md @@ -0,0 +1,12 @@ +## Description + +Verifies that all user sign-ins are protected by Conditional Access policies requiring phishing-resistant authentication methods (Windows Hello for Business, FIDO2 security keys, or certificate-based authentication). + +**Remediation action** + +- [Configure Conditional Access policies to enforce phishing-resistant authentication](https://learn.microsoft.com/en-us/entra/identity/conditional-access/policy-all-users-mfa-strength) + +- [Deploy phishing-resistant authentication methods](https://learn.microsoft.com/en-us/entra/identity/authentication/how-to-deploy-phishing-resistant-passwordless-authentication) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21786.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21786.md new file mode 100644 index 000000000000..9687b064b0dc --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21786.md @@ -0,0 +1,10 @@ +A threat actor can intercept or extract authentication tokens from memory, local storage on a legitimate device, or by inspecting network traffic. The attacker might replay those tokens to bypass authentication controls on users and devices, get unauthorized access to sensitive data, or run further attacks. Because these tokens are valid and time bound, traditional anomaly detection often fails to flag the activity, which might allow sustained access until the token expires or is revoked. + +Token protection, also called token binding, helps prevent token theft by making sure a token is usable only from the intended device. Token protection uses cryptography so that without the client device key, no one can use the token. + +**Remediation action** + +- [Deploy a Conditional Access policy to require token protection](https://learn.microsoft.com/en-us/entra/identity/conditional-access/concept-token-protection?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) + +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21787.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21787.md new file mode 100644 index 000000000000..b6a91771ec1e --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21787.md @@ -0,0 +1,12 @@ +A threat actor or a well-intentioned but uninformed employee can create a new Microsoft Entra tenant if there are no restrictions in place. By default, the user who creates a tenant is automatically assigned the Global Administrator role. Without proper controls, this action fractures the identity perimeter by creating a tenant outside the organization's governance and visibility. It introduces risk though a shadow identity platform that can be exploited for token issuance, brand impersonation, consent phishing, or persistent staging infrastructure. Since the rogue tenant might not be tethered to the enterprise’s administrative or monitoring planes, traditional defenses are blind to its creation, activity, and potential misuse. + +**Remediation action** + +Enable the **Restrict non-admin users from creating tenants** setting. For users that need the ability to create tenants, assign them the Tenant Creator role. You can also review tenant creation events in the Microsoft Entra audit logs. + +- [Restrict member users' default permissions](https://learn.microsoft.com/entra/fundamentals/users-default-permissions?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci#restrict-member-users-default-permissions) +- [Assign the Tenant Creator role](https://learn.microsoft.com/entra/identity/role-based-access-control/permissions-reference?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci#tenant-creator) +- [Review tenant creation events](https://learn.microsoft.com/entra/identity/monitoring-health/reference-audit-activities?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci#core-directory). Look for OperationName=="Create Company", Category == "DirectoryManagement". + +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21788.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21788.md new file mode 100644 index 000000000000..1c232fd4b0fe --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21788.md @@ -0,0 +1,13 @@ +Global Administrators with persistent access to Azure subscriptions expand the attack surface for threat actors. If a Global Administrator account is compromised, attackers can immediately enumerate resources, modify configurations, assign roles, and exfiltrate sensitive data across all subscriptions. Requiring just-in-time elevation for subscription access introduces detectable signals, slows attacker velocity, and routes high-impact operations through observable control points. + +**Remediation action** + +- [Get started with a phishing-resistant passwordless authentication deployment](https://learn.microsoft.com/entra/identity/authentication/how-to-plan-prerequisites-phishing-resistant-passwordless-authentication?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) + +- [Ensure that privileged accounts register and use phishing resistant methods](https://learn.microsoft.com/entra/identity/authentication/concept-authentication-strengths?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci#authentication-strengths.md) + +- [Deploy Conditional Access policy to target privileged accounts and require phishing resistant credentials using authentication strengths](https://learn.microsoft.com/entra/identity/conditional-access/policy-admin-phish-resistant-mfa?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) + +- [Monitor authentication method activity](https://learn.microsoft.com/entra/identity/monitoring-health/concept-usage-insights-report?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci#authentication-methods-activity.md) +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21789.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21789.md new file mode 100644 index 000000000000..6a740c4322e2 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21789.md @@ -0,0 +1,6 @@ +... + +**Remediation action** + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21790.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21790.md new file mode 100644 index 000000000000..d434ab123c6f --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21790.md @@ -0,0 +1,11 @@ +Allowing unrestricted external collaboration with unverified organizations can increase the risk surface area of the tenant because it allows guest accounts that might not have proper security controls. Threat actors can attempt to gain access by compromising identities in these loosely governed external tenants. Once granted guest access, they can then use legitimate collaboration pathways to infiltrate resources in your tenant and attempt to gain sensitive information. Threat actors can also exploit misconfigured permissions to escalate privileges and try different types of attacks. + +Without vetting the security of organizations you collaborate with, malicious external accounts can persist undetected, exfiltrate confidential data, and inject malicious payloads. This type of exposure can weaken organizational control and enable cross-tenant attacks that bypass traditional perimeter defenses and undermine both data integrity and operational resilience. Cross-tenant settings for outbound access in Microsoft Entra provide the ability to block collaboration with unknown organizations by default, reducing the attack surface. + +**Remediation action** + +- [Cross-tenant access overview](https://learn.microsoft.com/en-us/entra/external-id/cross-tenant-access-overview?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) +- [Configure cross-tenant access settings](https://learn.microsoft.com/en-us/entra/external-id/cross-tenant-access-settings-b2b-collaboration?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci#configure-default-settings) +- [Modify outbound access settings](https://learn.microsoft.com/en-us/entra/external-id/cross-tenant-access-settings-b2b-collaboration?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21791.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21791.md new file mode 100644 index 000000000000..7e85d896cdfa --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21791.md @@ -0,0 +1,10 @@ +External user accounts are often used to provide access to business partners who belong to organizations that have a business relationship with your enterprise. If these accounts are compromised in their organization, attackers can use the valid credentials to gain initial access to your environment, often bypassing traditional defenses due to their legitimacy. + +Allowing external users to onboard other external users increases the risk of unauthorized access. If an attacker compromises an external user's account, they can use it to create more external accounts, multiplying their access points and making it harder to detect the intrusion. + +**Remediation action** + +- [Restrict who can invite guests to only users assigned to specific admin roles](https://learn.microsoft.com/entra/external-id/external-collaboration-settings-configure?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci#to-configure-guest-invite-settings) + +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21792.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21792.md new file mode 100644 index 000000000000..e3d4505e32c7 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21792.md @@ -0,0 +1,10 @@ +External user accounts are often used to provide access to business partners who belong to organizations that have a business relationship with your enterprise. If these accounts are compromised in their organization, attackers can use the valid credentials to gain initial access to your environment, often bypassing traditional defenses due to their legitimacy. + +External accounts with permissions to read directory object permissions provide attackers with broader initial access if compromised. These accounts allow attackers to gather additional information from the directory for reconnaissance. + +**Remediation action** + +- [Restrict guest access to their own directory objects](https://learn.microsoft.com/entra/external-id/external-collaboration-settings-configure?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci#to-configure-guest-user-access) + +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21793.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21793.md new file mode 100644 index 000000000000..fe4e0f3ea03f --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21793.md @@ -0,0 +1,10 @@ +Tenant Restrictions v2 (TRv2) allows organizations to enforce policies that restrict access to specified Microsoft Entra tenants, preventing unauthorized exfiltration of corporate data to external tenants using local accounts. Without TRv2, threat actors can exploit this vulnerability, which leads to potential data exfiltration and compliance violations, followed by credential harvesting if those external tenants have weaker controls. Once credentials are obtained, threat actors can gain initial access to these external tenants. TRv2 provides the mechanism to prevent users from authenticating to unauthorized tenants. Otherwise, threat actors can move laterally, escalate privileges, and potentially exfiltrate sensitive data, all while appearing as legitimate user activity that bypasses traditional data loss prevention controls focused on internal tenant monitoring. + +Implementing TRv2 enforces policies that restrict access to specified tenants, mitigating these risks by ensuring that authentication and data access are confined to authorized tenants only. + +If this check passes, your tenant has a TRv2 policy configured but more steps are required to validate the scenario end-to-end. + +**Remediation action** +- [Set up Tenant Restrictions v2](https://learn.microsoft.com/en-us/entra/external-id/tenant-restrictions-v2?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21795.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21795.md new file mode 100644 index 000000000000..7e00a1e3cb84 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21795.md @@ -0,0 +1,16 @@ +Legacy authentication protocols such as basic authentication for SMTP and IMAP don't support modern security features like multifactor authentication (MFA), which is crucial for protecting against unauthorized access. This lack of protection makes accounts using these protocols vulnerable to password-based attacks, and provides attackers with a means to gain initial access using stolen or guessed credentials. + +When an attacker successfully gains unauthorized access to credentials, they can use them to access linked services, using the weak authentication method as an entry point. Attackers who gain access through legacy authentication might make changes to Microsoft Exchange, such as configuring mail forwarding rules or changing other settings, allowing them to maintain continued access to sensitive communications. + +Legacy authentication also provides attackers with a consistent method to reenter a system using compromised credentials without triggering security alerts or requiring reauthentication. + +From there, attackers can use legacy protocols to access other systems that are accessible via the compromised account, facilitating lateral movement. Attackers using legacy protocols can blend in with legitimate user activities, making it difficult for security teams to distinguish between normal usage and malicious behavior. + +**Remediation action** + +- [Exchange protocols can be deactivated in Exchange](https://learn.microsoft.com/exchange/clients-and-mobile-in-exchange-online/disable-basic-authentication-in-exchange-online?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) +- [Legacy authentication protocols can be blocked with Conditional Access](https://learn.microsoft.com/entra/identity/conditional-access/policy-block-legacy-authentication?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) +- [Sign-ins using legacy authentication workbook to help determine whether it's safe to turn off legacy authentication](https://learn.microsoft.com/entra/identity/monitoring-health/workbook-legacy-authentication?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) + +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21796.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21796.md new file mode 100644 index 000000000000..cef984099328 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21796.md @@ -0,0 +1,14 @@ +Legacy authentication protocols such as basic authentication for SMTP and IMAP don't support modern security features like multifactor authentication (MFA), which is crucial for protecting against unauthorized access. This lack of protection makes accounts using these protocols vulnerable to password-based attacks, and provides attackers with a means to gain initial access using stolen or guessed credentials. + +When an attacker successfully gains unauthorized access to credentials, they can use them to access linked services, using the weak authentication method as an entry point. Attackers who gain access through legacy authentication might make changes to Microsoft Exchange, such as configuring mail forwarding rules or changing other settings, allowing them to maintain continued access to sensitive communications. + +Legacy authentication also provides attackers with a consistent method to reenter a system using compromised credentials without triggering security alerts or requiring reauthentication. + +From there, attackers can use legacy protocols to access other systems that are accessible via the compromised account, facilitating lateral movement. Attackers using legacy protocols can blend in with legitimate user activities, making it difficult for security teams to distinguish between normal usage and malicious behavior. + +**Remediation action** + +- [Deploy a Conditional Access policy to Block legacy authentication](https://learn.microsoft.com/entra/identity/conditional-access/policy-block-legacy-authentication?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) + +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21797.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21797.md new file mode 100644 index 000000000000..5f34835602c2 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21797.md @@ -0,0 +1,13 @@ +Assume high risk users are compromised by threat actors. Without investigation and remediation, threat actors can execute scripts, deploy malicious applications, or manipulate API calls to establish persistence, based on the potentially compromised user's permissions. Threat actors can then exploit misconfigurations or abuse OAuth tokens to move laterally across workloads like documents, SaaS applications, or Azure resources. Threat actors can gain access to sensitive files, customer records, or proprietary code and exfiltrate it to external repositories while maintaining stealth through legitimate cloud services. Finally, threat actors might disrupt operations by modifying configurations, encrypting data for ransom, or using the stolen information for further attacks, resulting in financial, reputational, and regulatory consequences. + +Organizations using passwords can rely on password reset to automatically remediate risky users. + +Organizations using passwordless credentials already mitigate most risk events that accrue to user risk levels, thus the volume of risky users should be considerably lower. Risky users in an organization that uses passwordless credentials must be blocked from access until the user risk is investigated and remediated. + +**Remediation action** + +- [Deploy a Conditional Access policy to require a secure password change for elevated user risk](https://learn.microsoft.com/entra/identity/conditional-access/policy-risk-based-user?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci). +- Use Microsoft Entra ID Protection to [investigate risk further](https://learn.microsoft.com/entra/id-protection/howto-identity-protection-investigate-risk?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci). + +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21798.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21798.md new file mode 100644 index 000000000000..3c34e31de3ed --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21798.md @@ -0,0 +1,8 @@ +If you don't enable ID Protection notifications, your organization loses critical real-time alerts when threat actors compromise user accounts or conduct reconnaissance activities. When Microsoft Entra ID Protection detects accounts at risk, it sends email alerts with **Users at risk detected** as the subject and links to the **Users flagged for risk** report. Without these notifications, security teams remain unaware of active threats, allowing threat actors to maintain persistence in compromised accounts without being detected. You can feed these risks into tools like Conditional Access to make access decisions or send them to a security information and event management (SIEM) tool for investigation and correlation. Threat actors can use this detection gap to conduct lateral movement activities, privilege escalation attempts, or data exfiltration operations while administrators remain unaware of the ongoing compromise. The delayed response enables threat actors to establish more persistence mechanisms, change user permissions, or access sensitive resources before you can fix the issue. Without proactive notification of risk detections, organizations must rely solely on manual monitoring of risk reports, which significantly increases the time it takes to detect and respond to identity-based attacks. + +**Remediation action** + +- [Configure users at risk detected alerts](https://learn.microsoft.com/en-us/entra/id-protection/howto-identity-protection-configure-notifications?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci#configure-users-at-risk-detected-alerts) + +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21799.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21799.md new file mode 100644 index 000000000000..68af01ead6db --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21799.md @@ -0,0 +1,8 @@ +When high-risk sign-ins are not properly restricted through Conditional Access policies, organizations expose themselves to security vulnerabilities. Threat actors can exploit these gaps for initial access through compromised credentials, credential stuffing attacks, or anomalous sign-in patterns that Microsoft Entra ID Protection identifies as risky behaviors. Without appropriate restrictions, threat actors who successfully authenticate during high-risk scenarios can perform privilege escalation by misusing the authenticated session to access sensitive resources, modify security configurations, or conduct reconnaissance activities within the environment. Once threat actors establish access through uncontrolled high-risk sign-ins, they can achieve persistence by creating additional accounts, installing backdoors, or modifying authentication policies to maintain long-term access to the organization's resources. The unrestricted access enables threat actors to conduct lateral movement across systems and applications using the authenticated session, potentially accessing sensitive data stores, administrative interfaces, or critical business applications. Finally, threat actors achieve impact through data exfiltration, or compromise business-critical systems while maintaining plausible deniability by exploiting the fact that their risky authentication was not properly challenged or blocked. + +**Remediation action** + +- [Deploy a Conditional Access policy to require MFA for elevated sign-in risk](https://learn.microsoft.com/en-us/entra/identity/conditional-access/policy-risk-based-sign-in?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) + +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21800.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21800.md new file mode 100644 index 000000000000..a010c4484edc --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21800.md @@ -0,0 +1,13 @@ +Attackers might gain access if multifactor authentication (MFA) isn't universally enforced or if there are exceptions in place. Attackers might gain access by exploiting vulnerabilities of weaker MFA methods like SMS and phone calls through social engineering techniques. These techniques might include SIM swapping or phishing, to intercept authentication codes. + +Attackers might use these accounts as entry points into the tenant. By using intercepted user sessions, attackers can disguise their activities as legitimate user actions, evade detection, and continue their attack without raising suspicion. From there, they might attempt to manipulate MFA settings to establish persistence, plan, and execute further attacks based on the privileges of compromised accounts. + +**Remediation action** + +- [Deploy multifactor authentication](https://learn.microsoft.com/entra/identity/authentication/howto-mfa-getstarted?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) +- [Get started with a phishing-resistant passwordless authentication deployment](https://learn.microsoft.com/entra/identity/authentication/how-to-plan-prerequisites-phishing-resistant-passwordless-authentication?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) +- [Deploy a Conditional Access policy to require phishing-resistant MFA for all users](https://learn.microsoft.com/entra/identity/conditional-access/policy-all-users-mfa-strength?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) +- [Review authentication methods activity](https://learn.microsoft.com/entra/identity/monitoring-health/concept-usage-insights-report?tabs=microsoft-entra-admin-center&wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci#authentication-methods-activity) + +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21801.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21801.md new file mode 100644 index 000000000000..a010c4484edc --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21801.md @@ -0,0 +1,13 @@ +Attackers might gain access if multifactor authentication (MFA) isn't universally enforced or if there are exceptions in place. Attackers might gain access by exploiting vulnerabilities of weaker MFA methods like SMS and phone calls through social engineering techniques. These techniques might include SIM swapping or phishing, to intercept authentication codes. + +Attackers might use these accounts as entry points into the tenant. By using intercepted user sessions, attackers can disguise their activities as legitimate user actions, evade detection, and continue their attack without raising suspicion. From there, they might attempt to manipulate MFA settings to establish persistence, plan, and execute further attacks based on the privileges of compromised accounts. + +**Remediation action** + +- [Deploy multifactor authentication](https://learn.microsoft.com/entra/identity/authentication/howto-mfa-getstarted?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) +- [Get started with a phishing-resistant passwordless authentication deployment](https://learn.microsoft.com/entra/identity/authentication/how-to-plan-prerequisites-phishing-resistant-passwordless-authentication?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) +- [Deploy a Conditional Access policy to require phishing-resistant MFA for all users](https://learn.microsoft.com/entra/identity/conditional-access/policy-all-users-mfa-strength?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) +- [Review authentication methods activity](https://learn.microsoft.com/entra/identity/monitoring-health/concept-usage-insights-report?tabs=microsoft-entra-admin-center&wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci#authentication-methods-activity) + +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21802.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21802.md new file mode 100644 index 000000000000..61cf321650cd --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21802.md @@ -0,0 +1,8 @@ +Without sign-in context, threat actors can exploit authentication fatigue by flooding users with push notifications, increasing the chance that a user accidentally approves a malicious request. When users get generic push notifications without the application name or geographic location, they don't have the information they need to make informed approval decisions. This lack of context makes users vulnerable to social engineering attacks, especially when threat actors time their requests during periods of legitimate user activity. This vulnerability is especially dangerous when threat actors gain initial access through credential harvesting or password spraying attacks and then try to establish persistence by approving multifactor authentication (MFA) requests from unexpected applications or locations. Without contextual information, users can't detect unusual sign-in attempts, allowing threat actors to maintain access and escalate privileges by moving laterally through systems after bypassing the initial authentication barrier. Without application and location context, security teams also lose valuable telemetry for detecting suspicious authentication patterns that can indicate ongoing compromise or reconnaissance activities. + +**Remediation action** +Give users the context they need to make informed approval decisions. Configure Microsoft Authenticator notifications by setting the Authentication methods policy to include the application name and geographic location. +- [Use additional context in Authenticator notifications - Authentication methods policy](https://learn.microsoft.com/en-us/entra/identity/authentication/how-to-mfa-additional-context?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) + +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21803.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21803.md new file mode 100644 index 000000000000..6f99cf455fac --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21803.md @@ -0,0 +1,13 @@ +Legacy multifactor authentication (MFA) and self-service password reset (SSPR) policies in Microsoft Entra ID manage authentication methods separately, leading to fragmented configurations and suboptimal user experience. Moreover, managing these policies independently increases administrative overhead and the risk of misconfiguration. + +Migrating to the combined Authentication Methods policy consolidates the management of MFA, SSPR, and passwordless authentication methods into a single policy framework. This unification allows for more granular control, enabling administrators to target specific authentication methods to user groups and enforce consistent security measures across the organization. Additionally, the unified policy supports modern authentication methods, such as FIDO2 security keys and Windows Hello for Business, enhancing the organization's security posture. + +Microsoft announced the deprecation of legacy MFA and SSPR policies, with a retirement date set for September 30, 2025. Organizations are advised to complete the migration to the Authentication Methods policy before this date to avoid potential disruptions and to benefit from the enhanced security and management capabilities of the unified policy. + +**Remediation action** + +- [Enable combined security information registration](https://learn.microsoft.com/entra/identity/authentication/howto-registration-mfa-sspr-combined?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) +- [How to migrate MFA and SSPR policy settings to the Authentication methods policy for Microsoft Entra ID](https://learn.microsoft.com/entra/identity/authentication/how-to-authentication-methods-manage?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) + +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21804.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21804.md new file mode 100644 index 000000000000..c4a3d5bd3265 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21804.md @@ -0,0 +1,12 @@ +When weak authentication methods like SMS and voice calls remain enabled in Microsoft Entra ID, threat actors can exploit these vulnerabilities through multiple attack vectors. Initially, attackers often conduct reconnaissance to identify organizations using these weaker authentication methods through social engineering or technical scanning. Then they can execute initial access through credential stuffing attacks, password spraying, or phishing campaigns targeting user credentials. + +Once basic credentials are compromised, threat actors use these weaknesses in SMS and voice-based authentication. SMS messages can be intercepted through SIM swapping attacks, SS7 network vulnerabilities, or malware on mobile devices, while voice calls are susceptible to voice phishing (vishing) and call forwarding manipulation. With these weak second factors bypassed, attackers achieve persistence by registering their own authentication methods. Compromised accounts can be used to target higher-privileged users through internal phishing or social engineering, allowing attackers to escalate privileges within the organization. Finally, threat actors achieve their objectives through data exfiltration, lateral movement to critical systems, or deployment of other malicious tools, all while maintaining stealth by using legitimate authentication pathways that appear normal in security logs. + +**Remediation action** + +- [Deploy authentication method registration campaigns to encourage stronger methods](https://learn.microsoft.com/graph/api/authenticationmethodspolicy-update?view=graph-rest-beta&preserve-view=true&wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) +- [Disable authentication methods](https://learn.microsoft.com/en-us/entra/identity/authentication/concept-authentication-methods-manage?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) +- [Disable phone-based methods in legacy MFA settings](https://learn.microsoft.com/en-us/entra/identity/authentication/howto-mfa-mfasettings?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) +- [Deploy Conditional Access policies using authentication strength](https://learn.microsoft.com/en-us/entra/identity/authentication/concept-authentication-strength-how-it-works?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21806.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21806.md new file mode 100644 index 000000000000..f0b3ce89d4e6 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21806.md @@ -0,0 +1,10 @@ +Without Conditional Access policies protecting security information registration, threat actors can exploit unprotected registration flows to compromise authentication methods. When users register multifactor authentication and self-service password reset methods without proper controls, threat actors can intercept these registration sessions through adversary-in-the-middle attacks or exploit unmanaged devices accessing registration from untrusted locations. Once threat actors gain access to an unprotected registration flow, they can register their own authentication methods, effectively hijacking the target's authentication profile. The threat actors can bypass security controls and potentially escalate privileges throughout the environment because they can maintain persistent access by controlling the MFA methods. The compromised authentication methods then become the foundation for lateral movement as threat actors can authenticate as the legitimate user across multiple services and applications. + +**Remediation action** + +- [Deploy a Conditional Access policy for security info registration](https://learn.microsoft.com/en-us/entra/identity/conditional-access/policy-all-users-security-info-registration?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) +- [Configure known network locations](https://learn.microsoft.com/en-us/entra/identity/conditional-access/concept-assignment-network?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) +- [Enable combined security info registration](https://learn.microsoft.com/en-us/entra/identity/authentication/howto-registration-mfa-sspr-combined?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) + +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21807.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21807.md new file mode 100644 index 000000000000..f20e5690891a --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21807.md @@ -0,0 +1,12 @@ +If nonprivileged users can create applications and service principals, these accounts might be misconfigured or be granted more permissions than necessary, creating new vectors for attackers to gain initial access. Attackers can exploit these accounts to establish valid credentials in the environment and bypass some security controls. + +If these nonprivileged accounts are mistakenly granted elevated application owner permissions, attackers can use them to move from a lower level of access to a more privileged level of access. Attackers who compromise nonprivileged accounts might add their own credentials or change the permissions associated with the applications created by the nonprivileged users to ensure they can continue to access the environment undetected. + +Attackers can use service principals to blend in with legitimate system processes and activities. Because service principals often perform automated tasks, malicious activities carried out under these accounts might not be flagged as suspicious. + +**Remediation action** + +- [Block nonprivileged users from creating apps](https://learn.microsoft.com/entra/identity/role-based-access-control/delegate-app-roles?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) + +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21808.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21808.md new file mode 100644 index 000000000000..de6e6b429a7d --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21808.md @@ -0,0 +1,8 @@ +Device code flow is a cross-device authentication flow designed for input-constrained devices. It can be exploited in phishing attacks, where an attacker initiates the flow and tricks a user into completing it on their device, thereby sending the user's tokens to the attacker. Given the security risks and the infrequent legitimate use of device code flow, you should enable a Conditional Access policy to block this flow by default. + +**Remediation action** + +- [Deploy a Conditional Access policy to block device code flow](https://learn.microsoft.com/entra/identity/conditional-access/policy-block-authentication-flows?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci#device-code-flow-policies). + +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21809.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21809.md new file mode 100644 index 000000000000..3689a7af5877 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21809.md @@ -0,0 +1,11 @@ +Enabling the Admin consent workflow in a Microsoft Entra tenant is a vital security measure that mitigates risks associated with unauthorized application access and privilege escalation. This check is important because it ensures that any application requesting elevated permission undergoes a review process by designated administrators before consent is granted. The admin consent workflow in Microsoft Entra ID notifies reviewers who evaluate and approve or deny consent requests based on the application's legitimacy and necessity. If this check doesn't pass, meaning the workflow is disabled, any application can request and potentially receive elevated permissions without administrative review. This poses a substantial security risk, as malicious actors could exploit this lack of oversight to gain unauthorized access to sensitive data, perform privilege escalation, or execute other malicious activities. + +**Remediation action** + +For admin consent requests, set the **Users can request admin consent to apps they are unable to consent to** setting to **Yes**. Specify other settings, such as who can review requests. + +- [Enable the admin consent workflow](https://learn.microsoft.com/entra/identity/enterprise-apps/configure-admin-consent-workflow?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci#enable-the-admin-consent-workflow) +- Or use the [Update adminConsentRequestPolicy](https://learn.microsoft.com/graph/api/adminconsentrequestpolicy-update?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) API to set the `isEnabled` property to true and other settings + +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21810.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21810.md new file mode 100644 index 000000000000..2ac799e81e92 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21810.md @@ -0,0 +1,8 @@ +Letting group owners consent to applications in Microsoft Entra ID creates a lateral escalation path that lets threat actors persist and steal data without admin credentials. If an attacker compromises a group owner account, they can register or use a malicious application and consent to high-privilege Graph API permissions scoped to the group. Attackers can potentially read all Teams messages, access SharePoint files, or manage group membership. This consent action creates a long-lived application identity with delegated or application permissions. The attacker maintains persistence with OAuth tokens, steals sensitive data from team channels and files, and impersonates users through messaging or email permissions. Without centralized enforcement of app consent policies, security teams lose visibility, and malicious applications spread under the radar, enabling multi-stage attacks across collaboration platforms. + +**Remediation action** +Configure preapproval of Resource-Specific Consent (RSC) permissions. +- [Preapproval of RSC permissions](https://learn.microsoft.com/microsoftteams/platform/graph-api/rsc/preapproval-instruction-docs?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) + +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21811.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21811.md new file mode 100644 index 000000000000..fc521a90eb64 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21811.md @@ -0,0 +1,17 @@ +When password expiration policies remain enabled, threat actors can exploit the predictable password rotation patterns that users typically follow when forced to change passwords regularly. Users frequently create weaker passwords by making minimal modifications to existing ones, such as incrementing numbers or adding sequential characters. Threat actors can easily anticipate and exploit these types of changes through credential stuffing attacks or targeted password spraying campaigns. These predictable patterns enable threat actors to establish persistence through: + +- Compromised credentials +- Escalated privileges by targeting administrative accounts with weak rotated passwords +- Maintaining long-term access by predicting future password variations + +Research shows that users create weaker, more predictable passwords when they are forced to expire. These predictable passwords are easier for experienced attackers to crack, as they often make simple modifications to existing passwords rather than creating entirely new, strong passwords. Additionally, when users are required to frequently change passwords, they might resort to insecure practices such as writing down passwords or storing them in easily accessible locations, creating more attack vectors for threat actors to exploit during physical reconnaissance or social engineering campaigns. + +**Remediation action** + +- [Set the password expiration policy for your organization](https://learn.microsoft.com/microsoft-365/admin/manage/set-password-expiration-policy?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci). + - Sign in to the [Microsoft 365 admin center](https://admin.microsoft.com/). Go to **Settings** > **Org Settings** >** Security & Privacy** > **Password expiration policy**. Ensure the **Set passwords to never expire** setting is checked. +- [Disable password expiration using Microsoft Graph](https://learn.microsoft.com/graph/api/domain-update?view=graph-rest-1.0&preserve-view=true&wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci). +- [Set individual user passwords to never expire using Microsoft Graph PowerShell](https://learn.microsoft.com/microsoft-365/admin/add-users/set-password-to-never-expire?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) + - `Update-MgUser -UserId -PasswordPolicies DisablePasswordExpiration` +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21812.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21812.md new file mode 100644 index 000000000000..53d298d4756b --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21812.md @@ -0,0 +1,7 @@ +An excessive number of Global Administrator accounts creates an expanded attack surface that threat actors can exploit through various initial access vectors. Each extra privileged account represents a potential entry point for threat actors. An excess of Global Administrator accounts undermines the principle of least privilege. Microsoft recommends that organizations have no more than eight Global Administrators. + +**Remediation action** + +- [Follow best practices for Microsoft Entra roles](https://learn.microsoft.com/en-us/entra/identity/role-based-access-control/best-practices?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21813.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21813.md new file mode 100644 index 000000000000..4071a19bede4 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21813.md @@ -0,0 +1,8 @@ +When organizations maintain a disproportionately high ratio of Global Administrators relative to their total privileged user population, they expose themselves to significant security risks that threat actors might exploit through various attack vectors. Excessive Global Administrator assignments create multiple high-value targets for threat actors who might leverage initial access through credential compromise, phishing attacks, or insider threats to gain unrestricted access to the entire Microsoft Entra ID tenant and connected Microsoft 365 services. + +**Remediation action** + +- [Minimize the number of Global Administrator role assignments](https://learn.microsoft.com/entra/identity/role-based-access-control/best-practices?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci#5-limit-the-number-of-global-administrators-to-less-than-5) + +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21814.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21814.md new file mode 100644 index 000000000000..9892d3765a35 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21814.md @@ -0,0 +1,15 @@ +If an on-premises account is compromised and is synchronized to Microsoft Entra, the attacker might gain access to the tenant as well. This risk increases because on-premises environments typically have more attack surfaces due to older infrastructure and limited security controls. Attackers might also target the infrastructure and tools used to enable connectivity between on-premises environments and Microsoft Entra. These targets might include tools like Microsoft Entra Connect or Active Directory Federation Services, where they could impersonate or otherwise manipulate other on-premises user accounts. + +If privileged cloud accounts are synchronized with on-premises accounts, an attacker who acquires credentials for on-premises can use those same credentials to access cloud resources and move laterally to the cloud environment. + +**Remediation action** + +- [Protecting Microsoft 365 from on-premises attacks](https://learn.microsoft.com/entra/architecture/protect-m365-from-on-premises-attacks?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci#specific-security-recommendations) + +For each role with high privileges (assigned permanently or eligible through Microsoft Entra Privileged Identity Management), you should do the following actions: + +- Review the users that have onPremisesImmutableId and onPremisesSyncEnabled set. See [Microsoft Graph API user resource type](https://learn.microsoft.com/graph/api/resources/user?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci). +- Create cloud-only user accounts for those individuals and remove their hybrid identity from privileged roles. + +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21815.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21815.md new file mode 100644 index 000000000000..a2b1ef6e0a15 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21815.md @@ -0,0 +1,10 @@ +Threat actors target privileged accounts because they have access to the data and resources they want. This might include more access to your Microsoft Entra tenant, data in Microsoft SharePoint, or the ability to establish long-term persistence. Without a just-in-time (JIT) activation model, administrative privileges remain continuously exposed, providing attackers with an extended window to operate undetected. Just-in-time access mitigates risk by enforcing time-limited privilege activation with extra controls such as approvals, justification, and Conditional Access policy, ensuring that high-risk permissions are granted only when needed and for a limited duration. This restriction minimizes the attack surface, disrupts lateral movement, and forces adversaries to trigger actions that can be specially monitored and denied when not expected. Without just-in-time access, compromised admin accounts grant indefinite control, letting attackers disable security controls, erase logs, and maintain stealth, amplifying the impact of a compromise. + +Use Microsoft Entra Privileged Identity Management (PIM) to provide time-bound just-in-time access to privileged role assignments. Use access reviews in Microsoft Entra ID Governance to regularly review privileged access to ensure continued need. + +**Remediation action** + +- [Start using Privileged Identity Management](https://learn.microsoft.com/entra/id-governance/privileged-identity-management/pim-getting-started?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) +- [Create an access review of Azure resource and Microsoft Entra roles in PIM](https://learn.microsoft.com/entra/id-governance/privileged-identity-management/pim-create-roles-and-resource-roles-review?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21816.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21816.md new file mode 100644 index 000000000000..0a29cd021f6a --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21816.md @@ -0,0 +1,8 @@ +Threat actors who compromise a permanently assigned privileged account (e.g., Global Administrator or Privileged Role Administrator) gain continuous, uninterrupted access to high-impact directory operations. This extended dwell time enables attackers to more easily establish persistent backdoors, delete critical data and security configurations, disable monitoring systems, and register malicious applications for data exfiltration and lateral movement. These actions can result in full organizational disruption, widespread data compromise, and total loss of operational control over the tenant. Microsoft Entra PIM’s eligible role assignment model narrows escalation pathways, constrains attacker dwell time and provides the option of role elevation approval workflows. + +**Remediation action** +- [Use Privileged Identity Management to manage privileged Microsoft Entra roles](https://learn.microsoft.com/en-us/entra/id-governance/privileged-identity-management/pim-getting-started) +- [Manage emergency access admin accounts](https://learn.microsoft.com/en-us/entra/identity/role-based-access-control/security-emergency-access) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21817.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21817.md new file mode 100644 index 000000000000..b0b563352276 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21817.md @@ -0,0 +1,13 @@ +Without approval workflows, threat actors who compromise Global Administrator credentials through phishing, credential stuffing, or other authentication bypass techniques can immediately activate the most privileged role in a tenant without any other verification or oversight. Privileged Identity Management (PIM) allows eligible role activations to become active within seconds, so compromised credentials can allow near-instant privilege escalation. Once activated, threat actors can use the Global Administrator role to use the following attack paths to gain persistent access to the tenant: +- Create new privileged accounts +- Modify Conditional Access policies to exclude those new accounts +- Establish alternate authentication methods such as certificate-based authentication or application registrations with high privileges + +The Global Administrator role provides access to administrative features in Microsoft Entra ID and services that use Microsoft Entra identities, including Microsoft Defender XDR, Microsoft Purview, Exchange Online, and SharePoint Online. Without approval gates, threat actors can rapidly escalate to complete tenant takeover, exfiltrating sensitive data, compromising all user accounts, and establishing long-term backdoors through service principals or federation modifications that persist even after the initial compromise is detected. + +**Remediation action** + +- [Configure role settings to require approval for Global Administrator activation](https://learn.microsoft.com/en-us/entra/id-governance/privileged-identity-management/pim-how-to-change-default-settings?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) +- [Set up approval workflow for privileged roles](https://learn.microsoft.com/en-us/entra/id-governance/privileged-identity-management/pim-approval-workflow?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21818.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21818.md new file mode 100644 index 000000000000..4432e91f337a --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21818.md @@ -0,0 +1,8 @@ +Organizations without proper activation alerts for highly privileged roles lack visibility into when users access these critical permissions. Threat actors can exploit this monitoring gap to perform privilege escalation by activating highly privileged roles without detection, then establish persistence through admin account creation or security policy modifications. The absence of real-time alerts enables attackers to conduct lateral movement, modify audit configurations, and disable security controls without triggering immediate response procedures. + +**Remediation action** + +- [Configure Microsoft Entra role settings in Privileged Identity Management](https://learn.microsoft.com/en-us/entra/id-governance/privileged-identity-management/pim-how-to-change-default-settings?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci#require-justification-on-activation) + +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21819.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21819.md new file mode 100644 index 000000000000..7f99f06ac13a --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21819.md @@ -0,0 +1,8 @@ +Without activation alerts for Global Administrator role assignments, threat actors can perform role activation without detection, allowing them to establish persistence in the environment. When Global Administrator roles are activated without notification mechanisms, threat actors who have compromised accounts can escalate privileges, bypassing security monitoring. The absence of alerts creates a blind spot where threat actors can activate the most privileged role in the tenant and perform actions such as creating backdoor accounts, modifying security policies, or accessing sensitive data without immediate detection. This lack of visibility allows threat actors to maintain access and execute their objectives while appearing to use legitimate administrative functions, making it difficult for security teams to distinguish between authorized and unauthorized privilege escalation activities. + +**Remediation action** + +- [Configure Microsoft Entra role settings in Privileged Identity Management](https://learn.microsoft.com/entra/id-governance/privileged-identity-management/pim-how-to-change-default-settings) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21820.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21820.md new file mode 100644 index 000000000000..e6327b29dd4b --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21820.md @@ -0,0 +1,6 @@ +Without activation alerts for privileged role assignments, threat actors who compromise user credentials through phishing, password attacks, or credential stuffing can activate privileged roles without detection. When privileged roles are activated without notification mechanisms, security teams lack visibility into when elevated permissions are being used, allowing threat actors to operate within the environment undetected during the initial access phase. During the persistence phase, threat actors can leverage activated privileged roles to create backdoors, modify security configurations, or establish additional access methods without triggering security alerts. The lack of activation notifications prevents security teams from correlating privileged role usage with other security events, enabling threat actors to conduct lateral movement and privilege escalation activities while maintaining stealth. + +**Remediation action** +- [Configure notifications for privileged roles](https://learn.microsoft.com/en-us/entra/id-governance/privileged-identity-management/pim-how-to-change-default-settings#require-justification-on-active-assignment) + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21821.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21821.md new file mode 100644 index 000000000000..6a740c4322e2 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21821.md @@ -0,0 +1,6 @@ +... + +**Remediation action** + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21822.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21822.md new file mode 100644 index 000000000000..506922d1426c --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21822.md @@ -0,0 +1,8 @@ +Without limiting guest access to approved tenants, threat actors can exploit unrestricted guest access to establish initial access through compromised external accounts or by creating accounts in untrusted tenants. Organizations can configure an allowlist or blocklist to control B2B collaboration invitations from specific organizations, and without these controls, threat actors can leverage social engineering techniques to obtain invitations from legitimate internal users. Once threat actors gain guest access through unrestricted domains, they can perform discovery activities to enumerate internal resources, users, and applications that guest accounts can access. The compromised guest account then serves as a persistent foothold, allowing threat actors to execute collection activities against accessible SharePoint sites, Teams channels, and other resources granted to guest users. From this position, threat actors can attempt lateral movement by exploiting trust relationships between the compromised tenant and partner organizations, or by leveraging guest permissions to access sensitive data that can be used for further credential compromise or business email compromise attacks. + +**Remediation action** + +- [Configure Domain-Based Allow or Deny Lists](https://learn.microsoft.com/en-us/entra/external-id/allow-deny-list) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21823.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21823.md new file mode 100644 index 000000000000..577af3c21731 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21823.md @@ -0,0 +1,10 @@ +When guest self-service sign-up is enabled, threat actors can exploit it to establish unauthorized access by creating legitimate guest accounts without requiring approval from authorized personnel. These accounts can be scoped to specific services to reduce detection and effectively bypass invitation-based controls that validate external user legitimacy. + +Once created, self-provisioned guest accounts provide persistent access to organizational resources and applications. Threat actors can use them to conduct reconnaissance activities to map internal systems, identify sensitive data repositories, and plan further attack vectors. This persistence allows adversaries to maintain access across restarts, credential changes, and other interruptions, while the guest account itself offers a seemingly legitimate identity that might evade security monitoring focused on external threats. + +Additionally, compromised guest identities can be used to establish credential persistence and potentially escalate privileges. Attackers can exploit trust relationships between guest accounts and internal resources, or use the guest account as a staging ground for lateral movement toward more privileged organizational assets. + +**Remediation action** +- [Configure guest self-service sign-up With Microsoft Entra External ID](https://learn.microsoft.com/en-us/entra/external-id/external-collaboration-settings-configure?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci#to-configure-guest-self-service-sign-up) +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21824.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21824.md new file mode 100644 index 000000000000..b6f19e7039e7 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21824.md @@ -0,0 +1,12 @@ +Guest accounts with extended sign-in sessions increase the risk surface area that threat actors can exploit. When guest sessions persist beyond necessary timeframes, threat actors often attempt to gain initial access through credential stuffing, password spraying, or social engineering attacks. Once they gain access, they can maintain unauthorized access for extended periods without reauthentication challenges. These compromised and extended sessions: + +- Allow unauthorized access to Microsoft Entra artifacts, enabling threat actors to identify sensitive resources and map organizational structures. +- Allow threat actors to persist within the network by using legitimate authentication tokens, making detection more challenging as the activity appears as typical user behavior. +- Provides threat actors with a longer window of time to escalate privileges through techniques like accessing shared resources, discovering more credentials, or exploiting trust relationships between systems. + +Without proper session controls, threat actors can achieve lateral movement across the organization's infrastructure, accessing critical data and systems that extend far beyond the original guest account's intended scope of access. + +**Remediation action** +- [Configure adaptive session lifetime policies](https://learn.microsoft.com/en-us/entra/identity/conditional-access/howto-conditional-access-session-lifetime?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) so sign-in frequency policies have shorter live sign-in sessions. +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21825.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21825.md new file mode 100644 index 000000000000..25e38ec74568 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21825.md @@ -0,0 +1,15 @@ +When privileged users are allowed to maintain long-lived sign-in sessions without periodic reauthentication, threat actors can gain extended windows of opportunity to exploit compromised credentials or hijack active sessions. Once a privileged account is compromised through techniques like credential theft, phishing, or session fixation, extended session timeouts allow threat actors to maintain persistence within the environment for prolonged periods. With long-lived sessions, threat actors can perform lateral movement across systems, escalate privileges further, and access sensitive resources without triggering another authentication challenge. The extended session duration also increases the window for session hijacking attacks, where threat actors can steal session tokens and impersonate the privileged user. Once a threat actor is established in a privileged session, they can: + +- Create backdoor accounts +- Modify security policies +- Access sensitive data +- Establish more persistence mechanisms + +The lack of periodic reauthentication requirements means that even if the original compromise is detected, the threat actor might continue operating undetected using the hijacked privileged session until the session naturally expires or the user manually signs out. + +**Remediation action** + +- [Learn about Conditional Access adaptive session lifetime policies](https://learn.microsoft.com/en-us/entra/identity/conditional-access/concept-session-lifetime?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) +- [Configure sign-in frequency for privileged users with Conditional Access policies ](https://learn.microsoft.com/en-us/entra/identity/conditional-access/howto-conditional-access-session-lifetime?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21828.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21828.md new file mode 100644 index 000000000000..5f23b96e0682 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21828.md @@ -0,0 +1,8 @@ +Blocking authentication transfer in Microsoft Entra ID is a critical security control. It helps protect against token theft and replay attacks by preventing the use of device tokens to silently authenticate on other devices or browsers. When authentication transfer is enabled, a threat actor who gains access to one device can access resources to nonapproved devices, bypassing standard authentication and device compliance checks. When administrators block this flow, organizations can ensure that each authentication request must originate from the original device, maintaining the integrity of the device compliance and user session context. + +**Remediation action** + +- [Deploy a Conditional Access policy to block authentication transfer](https://learn.microsoft.com/en-us/entra/identity/conditional-access/policy-block-authentication-flows?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci#authentication-transfer-policies) + +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21829.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21829.md new file mode 100644 index 000000000000..470f74876eea --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21829.md @@ -0,0 +1,8 @@ +An on-premises federation server introduces a critical attack surface by serving as a central authentication point for cloud applications. Threat actors often gain a foothold by compromising a privileged user such as a help desk representative or an operations engineer through attacks like phishing, credential stuffing, or exploiting weak passwords. They might also target unpatched vulnerabilities in infrastructure, use remote code execution exploits, attack the Kerberos protocol, or use pass-the-hash attacks to escalate privileges. Misconfigured remote access tools like remote desktop protocol (RDP), virtual private network (VPN), or jump servers provide other entry points, while supply chain compromises or malicious insiders further increase exposure. Once inside, threat actors can manipulate authentication flows, forge security tokens to impersonate any user, and pivot into cloud environments. Establishing persistence, they can disable security logs, evade detection, and exfiltrate sensitive data. + +**Remediation action** + +- [Migrate from federation to cloud authentication like Microsoft Entra Password hash synchronization (PHS)](https://learn.microsoft.com/entra/identity/hybrid/connect/migrate-from-federation-to-cloud-authentication?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci). + +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21830.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21830.md new file mode 100644 index 000000000000..790102b27622 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21830.md @@ -0,0 +1,11 @@ +If privileged role activations aren't restricted to dedicated Privileged Access Workstations (PAWs), threat actors can exploit compromised endpoint devices to perform privileged escalation attacks from unmanaged or noncompliant workstations. Standard productivity workstations often contain attack vectors such as unrestricted web browsing, email clients vulnerable to phishing, and locally installed applications with potential vulnerabilities. When administrators activated privileged roles from these workstations, threat actors who gain initial access through malware, browser exploits, or social engineering can then use the locally cached privileged credentials or hijack existing authenticated sessions to escalate their privileges. Privileged role activations grant extensive administrative rights across Microsoft Entra ID and connected services, so attackers can create new administrative accounts, modify security policies, access sensitive data across all organizational resources, and deploy malware or backdoors throughout the environment to establish persistent access. This lateral movement from a compromised endpoint to privileged cloud resources represents a critical attack path that bypasses many traditional security controls. The privileged access appears legitimate when originating from an authenticated administrator's session. + +If this check passes, your tenant has a Conditional Access policy that restricts privileged role access to PAW devices, but it isn't the only control required to fully enable a PAW solution. You also need to configure an Intune device configuration and compliance policy and a device filter. + +**Remediation action** + +- [Deploy a privileged access workstation solution](https://learn.microsoft.com/security/privileged-access-workstations/privileged-access-deployment?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) + - Provides guidance for configuring the Conditional Access and Intune device configuration and compliance policies. +- [Configure device filters in Conditional Access to restrict privileged access](https://learn.microsoft.com/en-us/entra/identity/conditional-access/concept-condition-filters-for-devices?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21831.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21831.md new file mode 100644 index 000000000000..abcf2871d752 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21831.md @@ -0,0 +1,10 @@ +Threat actors who gain privileged access to a tenant can manipulate identity, access, and security configurations. This type of attack can result in environment-wide compromise and loss of control over organizational assets. Take action to protect high-impact management tasks associated with Conditional Access policies, cross-tenant access settings, hard deletions, and network locations that are critical to maintaining security. + +Protected actions let administrators secure these tasks with extra security controls, such as stronger authentication methods (passwordless MFA or phishing-resistant MFA), the use of Privileged Access Workstation (PAW) devices, or shorter session timeouts. + +**Remediation action** + +- [Add, test, or remove protected actions in Microsoft Entra ID](https://learn.microsoft.com/entra/identity/role-based-access-control/protected-actions-add?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) + +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21832.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21832.md new file mode 100644 index 000000000000..6a740c4322e2 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21832.md @@ -0,0 +1,6 @@ +... + +**Remediation action** + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21833.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21833.md new file mode 100644 index 000000000000..6a740c4322e2 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21833.md @@ -0,0 +1,6 @@ +... + +**Remediation action** + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21834.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21834.md new file mode 100644 index 000000000000..6a740c4322e2 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21834.md @@ -0,0 +1,6 @@ +... + +**Remediation action** + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21835.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21835.md new file mode 100644 index 000000000000..be68f5749ef6 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21835.md @@ -0,0 +1,8 @@ +Microsoft recommends that organizations have two cloud-only emergency access accounts permanently assigned the [Global Administrator](https://learn.microsoft.com/entra/identity/role-based-access-control/permissions-reference?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci#global-administrator) role. These accounts are highly privileged and aren't assigned to specific individuals. The accounts are limited to emergency or "break glass" scenarios where normal accounts can't be used or all other administrators are accidentally locked out. + +**Remediation action** + +- Create accounts following the [emergency access account recommendations](https://learn.microsoft.com/entra/identity/role-based-access-control/security-emergency-access?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci). + +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21836.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21836.md new file mode 100644 index 000000000000..08ff6ceb70f4 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21836.md @@ -0,0 +1,8 @@ +If administrators assign privileged roles to workload identities, such as service principals or managed identities, the tenant can be exposed to significant risk if those identities are compromised. Threat actors who gain access to a privileged workload identity can perform reconnaissance to enumerate resources, escalate privileges, and manipulate or exfiltrate sensitive data. The attack chain typically begins with credential theft or abuse of a vulnerable application. Next step is privilege escalation through the assigned role, lateral movement across cloud resources, and finally persistence via other role assignments or credential updates. Workload identities are often used in automation and might not be monitored as closely as user accounts. Compromise can then go undetected, allowing threat actors to maintain access and control over critical resources. Workload identities aren't subject to user-centric protections like MFA, making least-privilege assignment and regular review essential. + +**Remediation action** +- [Review and remove privileged roles assignments](https://learn.microsoft.com/en-us/entra/id-governance/privileged-identity-management/pim-resource-roles-assign-roles?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci#update-or-remove-an-existing-role-assignment). +- [Follow the best practices for workload identities](https://learn.microsoft.com/en-us/entra/workload-id/workload-identities-overview?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci#key-scenarios). +- [Learn about privileged roles and permissions in Microsoft Entra ID](https://learn.microsoft.com/en-us/entra/identity/role-based-access-control/privileged-roles-permissions?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21837.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21837.md new file mode 100644 index 000000000000..c97fc5a08508 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21837.md @@ -0,0 +1,8 @@ +Controlling device proliferation is important. Set a reasonable limit on the number of devices each user can register in your Microsoft Entra ID tenant. Limiting device registration maintains security while allowing business flexibility. Microsoft Entra ID lets users register up to 50 devices by default. Reducing this number to 10 minimizes the attack surface and simplifies device management. + +**Remediation action** + +- Learn how to [limit the maximum number of devices per user](https://learn.microsoft.com/entra/identity/devices/manage-device-identities?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci#configure-device-settings). + +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21838.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21838.md new file mode 100644 index 000000000000..966d20d34799 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21838.md @@ -0,0 +1,10 @@ +Enabling the security key authentication method in Microsoft Entra ID mitigates the risk of credential theft and unauthorized access by requiring hardware-backed, phishing-resistant authentication. If this best practice is not followed, threat actors can exploit weak or reused passwords, perform credential stuffing attacks, and escalate privileges through compromised accounts. The kill chain begins with reconnaissance where attackers gather information about user accounts, followed by credential harvesting through various techniques like social engineering or data breaches. Attackers then gain initial access using stolen credentials, move laterally within the network by exploiting trust relationships, and establish persistence to maintain long-term access. Without hardware-backed authentication like FIDO2 security keys, attackers can bypass basic password defenses and multi-factor authentication, increasing the likelihood of data exfiltration and business disruption. Security keys provide cryptographic proof of identity that is bound to the specific device and cannot be replicated or phished, effectively breaking the attack chain at the initial access stage. + +**Remediation action** + +* [Enable passkey (FIDO2) authentication method](https://learn.microsoft.com/en-us/entra/identity/authentication/how-to-enable-passkey-fido2#enable-passkey-fido2-authentication-method) + +* [Authentication method policy management](https://learn.microsoft.com/en-us/entra/identity/authentication/concept-authentication-methods-manage) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21839.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21839.md new file mode 100644 index 000000000000..036e0c9d2b1b --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21839.md @@ -0,0 +1,11 @@ +When passkey authentication isn't enabled in Microsoft Entra ID, organizations rely on password-based authentication methods that are vulnerable to phishing, credential theft, and replay attacks. Attackers can use stolen passwords to gain initial access, bypass traditional multifactor authentication through Adversary-in-the-Middle (AiTM) attacks, and establish persistent access through token theft. + +Passkeys provide phishing-resistant authentication using cryptographic proof that attackers can't phish, intercept, or replay. Enabling passkeys eliminates the foundational vulnerability that enables credential-based attack chains. + +**Remediation action** + +- Learn how to [enable the passkey authentication method](https://learn.microsoft.com/entra/identity/authentication/how-to-enable-passkey-fido2?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci#enable-passkey-fido2-authentication-method). +- Learn how to [plan a phishing-resistant passwordless authentication deployment](https://learn.microsoft.com/entra/identity/authentication/how-to-plan-prerequisites-phishing-resistant-passwordless-authentication?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci). + +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21840.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21840.md new file mode 100644 index 000000000000..b83dff41f9dc --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21840.md @@ -0,0 +1,9 @@ +When security key attestation isn't enforced, threat actors can exploit weak or compromised authentication hardware to establish persistent presence within organizational environments. Without attestation validation, malicious actors can register unauthorized or counterfeit FIDO2 security keys that bypass hardware-backed security controls, enabling them to perform credential stuffing attacks using fabricated authenticators that mimic legitimate security keys. This initial access lets threat actors escalate privileges by using the trusted nature of hardware authentication methods, then move laterally through the environment by registering more compromised security keys on high-privilege accounts. The lack of attestation enforcement creates a pathway for threat actors to establish command and control through persistent hardware-based authentication methods, ultimately leading to data exfiltration or system compromise while maintaining the appearance of legitimate hardware-secured authentication throughout the attack chain. + +**Remediation action** + +- [Enable attestation enforcement through the Authentication methods policy configuration](https://learn.microsoft.com/entra/identity/authentication/how-to-enable-passkey-fido2?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci#enable-passkey-fido2-authentication-method). +- [Configure approved list of security keys by Authenticator Attestation Globally Unique Identifier (AAGUID)](https://learn.microsoft.com/entra/identity/authentication/concept-fido2-hardware-vendor?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci). + +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21841.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21841.md new file mode 100644 index 000000000000..e88e14ab14ad --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21841.md @@ -0,0 +1,10 @@ +Threat actors increasingly rely on prompt bombing and real-time phishing proxies to coerce or trick users into approving fraudulent multifactor authentication (MFA) challenges. Without the Microsoft Authenticator app's **Report suspicious activity** capability enabled, an attacker can iterate until a fatigued user accepts. This type of attack can lead to privilege escalation, persistence, lateral movement into sensitive workloads, data exfiltration, or destructive actions. + +When reporting is enabled for all users, any unexpected push or phone prompt can be actively flagged, immediately elevating the user to high user risk and generating a high-fidelity user risk detection (userReportedSuspiciousActivity) that risk-based Conditional Access policies or other response automation can use to block or require secure remediation. + +**Remediation action** + +- [Enable the report suspicious activity setting in the Microsoft Authenticator app](https://learn.microsoft.com/entra/identity/authentication/howto-mfa-mfasettings?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci#report-suspicious-activity) + +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21842.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21842.md new file mode 100644 index 000000000000..3fe9d727b41d --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21842.md @@ -0,0 +1,10 @@ +Self-Service Password Reset (SSPR) for administrators allows password changes to happen without strong secondary authentication factors or administrative oversight. Threat actors who compromise administrative credentials can use this capability to bypass other security controls and maintain persistent access to the environment. + +Once compromised, attackers can immediately reset the password to lock out legitimate administrators. They can then establish persistence, escalate privileges, and deploy malicious payloads undetected. + +**Remediation action** + +- [Disable SSPR for administrators by updating the authorization policy](https://learn.microsoft.com/entra/identity/authentication/concept-sspr-policy?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci#administrator-reset-policy-differences) + +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21843.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21843.md new file mode 100644 index 000000000000..6a740c4322e2 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21843.md @@ -0,0 +1,6 @@ +... + +**Remediation action** + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21844.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21844.md new file mode 100644 index 000000000000..a74cead90b2c --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21844.md @@ -0,0 +1,10 @@ +Threat actors frequently target legacy management interfaces such as the Azure AD PowerShell module (AzureAD and AzureADPreview), which don't support modern authentication, Conditional Access enforcement, or advanced audit logging. Continued use of these modules exposes the environment to risks including weak authentication, bypass of security controls, and incomplete visibility into administrative actions. Attackers can exploit these weaknesses to gain unauthorized access, escalate privileges, and perform malicious changes. + +Block the Azure AD PowerShell module and enforce the use of Microsoft Graph PowerShell or Microsoft Entra PowerShell to ensure that only secure, supported, and auditable management channels are available, which closes critical gaps in the attack chain. + +**Remediation action** + +- [Disable user sign-in for application](https://learn.microsoft.com/entra/identity/enterprise-apps/disable-user-sign-in-portal?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) + +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21845.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21845.md new file mode 100644 index 000000000000..a46f492f1452 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21845.md @@ -0,0 +1,12 @@ +Without Temporary Access Pass (TAP) enabled, organizations face significant challenges in securely bootstrapping user credentials, creating a vulnerability where users rely on weaker authentication mechanisms during their initial setup. When users cannot register phishing-resistant credentials like FIDO2 security keys or Windows Hello for Business due to lack of existing strong authentication methods, they remain exposed to credential-based attacks including phishing, password spray, or similar attacks. Threat actors can exploit this registration gap by targeting users during their most vulnerable state, when they have limited authentication options available and must rely on traditional username + password combinations. This exposure enables threat actors to compromise user accounts during the critical bootstrapping phase, allowing them to intercept or manipulate the registration process for stronger authentication methods, ultimately gaining persistent access to organizational resources and potentially escalating privileges before security controls are fully established. + +Enable TAP and use it with security info registration to secure this potential gap in your defenses. + +**Remediation action** + +- [Learn how to enable Temporary Access Pass in the Authentication methods policy](https://learn.microsoft.com/entra/identity/authentication/howto-authentication-temporary-access-pass?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci#enable-the-temporary-access-pass-policy) +- [Learn how to update authentication strength policies to include Temporary Access Pass](https://learn.microsoft.com/entra/identity/authentication/concept-authentication-strength-advanced-options?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) +- [Learn how to create a Conditional Access policy for security info registration with authentication strength enforcement](https://learn.microsoft.com/entra/identity/conditional-access/policy-all-users-security-info-registration?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) + +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21846.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21846.md new file mode 100644 index 000000000000..dc8bcf1e108f --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21846.md @@ -0,0 +1,8 @@ +When Temporary Access Pass (TAP) is configured to allow multiple uses, threat actors who compromise the credential can reuse it repeatedly during its validity period, extending their unauthorized access window beyond the intended single bootstrapping event. This situation creates an extended opportunity for threat actors to establish persistence by registering additional strong authentication methods under the compromised account during the credential lifetime. A reusable TAP that falls into the wrong hands lets threat actors conduct reconnaissance activities across multiple sessions, gradually mapping the environment and identifying high-value targets while maintaining legitimate-looking access patterns. The compromised TAP can also serve as a reliable backdoor mechanism, allowing threat actors to maintain access even if other compromised credentials are detected and revoked, since the TAP appears as a legitimate administrative tool in security logs. + +**Remediation action** + +- [Configure Temporary Access Pass for one-time use in authentication methods policy](https://learn.microsoft.com/entra/identity/authentication/howto-authentication-temporary-access-pass?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci#enable-the-temporary-access-pass-policy) + +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21847.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21847.md new file mode 100644 index 000000000000..3c5921bf5a0b --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21847.md @@ -0,0 +1,12 @@ +When on-premises password protection isn’t enabled or enforced, threat actors can use low-and-slow password spray with common variants, such as season+year+symbol or local terms, to gain initial access to Active Directory Domain Services accounts. Domain Controllers (DCs) can accept weak passwords when either of the following statements are true: + +- Microsoft Entra Password Protection DC agent isn't installed +- The password protection tenant setting is disabled or in audit-only mode + +With valid on-premises credentials, attackers laterally move by reusing passwords across endpoints, escalate to domain admin through local admin reuse or service accounts, and persist by adding backdoors, while weak or disabled enforcement produces fewer blocking events and predictable signals. Microsoft’s design requires a proxy that brokers policy from Microsoft Entra ID and a DC agent that enforces the combined global and tenant custom banned lists on password change/reset; consistent enforcement requires DC agent coverage on all DCs in a domain and using Enforced mode after audit evaluation. + +**Remediation action** + +- [Deploy Microsoft Entra password protection](https://learn.microsoft.com/en-us/entra/identity/authentication/howto-password-ban-bad-on-premises-deploy?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21848.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21848.md new file mode 100644 index 000000000000..2e03a7464592 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21848.md @@ -0,0 +1,10 @@ +Organizations that don't populate and enforce the custom banned password list expose themselves to a systematic attack chain where threat actors exploit predictable organizational password patterns. These threat actors typically start with reconnaissance phases, where they gather open-source intelligence (OSINT) from websites, social media, and public records to identify likely password components. With this knowledge, they launch password spray attacks that test organization-specific password variations across multiple user accounts, staying under lockout thresholds to avoid detection. Without the protection the custom banned password list offers, employees often add familiar organizational terms to their passwords, like locations, product names, and industry terms, creating consistent attack vectors. + +The custom banned password list helps organizations plug this critical gap to prevent easily guessed passwords that could lead to initial access and subsequent lateral movement within the environment. + +**Remediation action** + +- [Learn how to enable custom banned password protection and add organizational terms](https://learn.microsoft.com/entra/identity/authentication/tutorial-configure-custom-password-protection?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) + +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21849.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21849.md new file mode 100644 index 000000000000..1d3d17582ab0 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21849.md @@ -0,0 +1,8 @@ +When Smart Lockout duration is configured below the default 60 seconds, threat actors can exploit shortened lockout periods to conduct password spray and credential stuffing attacks more effectively. Reduced lockout windows allow attackers to resume authentication attempts more rapidly, increasing their success probability while potentially evading detection systems that rely on longer observation periods. + +**Remediation action** + +- [Set Smart Lockout duration to 60 seconds or higher](https://learn.microsoft.com/entra/identity/authentication/howto-password-smart-lockout?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci#manage-microsoft-entra-smart-lockout-values) + +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21850.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21850.md new file mode 100644 index 000000000000..fb2a9d166304 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21850.md @@ -0,0 +1,10 @@ +When the smart lockout threshold is set to more than 10, threat actors can exploit the configuration to conduct reconnaissance, identify valid user accounts without triggering lockout protections, and establish initial access without detection. Once attackers gain initial access, they can move laterally through the environment by using the compromised account to access resources and escalate privileges. + +Smart lockout helps lock out bad actors who try to guess your users' passwords or use brute force methods to get in. Smart lockout recognizes sign-ins that come from valid users and treats them differently than ones of attackers and other unknown sources. A threshold of more than 10 provides insufficient protection against automated password spray attacks, making it easier for threat actors to compromise accounts while evading detection mechanisms. + +**Remediation action** + +- [Set Microsoft Entra smart lockout threshold to 10 or less](https://learn.microsoft.com/entra/identity/authentication/howto-password-smart-lockout?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci). + +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21851.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21851.md new file mode 100644 index 000000000000..833ca37a43d4 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21851.md @@ -0,0 +1,14 @@ +External user accounts are often used to provide access to business partners who belong to organizations that have a business relationship with your organization. If these accounts are compromised in their organization, attackers can use the valid credentials to gain initial access to your environment, often bypassing traditional defenses due to their legitimacy. + +Attackers might gain access with external user accounts, if multifactor authentication (MFA) isn't universally enforced or if there are exceptions in place. They might also gain access by exploiting the vulnerabilities of weaker MFA methods like SMS and phone calls using social engineering techniques, such as SIM swapping or phishing, to intercept the authentication codes. + +Once an attacker gains access to an account without MFA or a session with weak MFA methods, they might attempt to manipulate MFA settings (for example, registering attacker controlled methods) to establish persistence to plan and execute further attacks based on the privileges of the compromised accounts. + +**Remediation action** + +- [Deploy a Conditional Access policy to enforce authentication strength for guests](https://learn.microsoft.com/entra/identity/conditional-access/policy-guests-mfa-strength?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci). +- For organizations with a closer business relationship and vetting on their MFA practices, consider deploying cross-tenant access settings to accept the MFA claim. + - [Configure B2B collaboration cross-tenant access settings](https://learn.microsoft.com/entra/external-id/cross-tenant-access-settings-b2b-collaboration?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci#to-change-inbound-trust-settings-for-mfa-and-device-claims) + +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21854.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21854.md new file mode 100644 index 000000000000..6a740c4322e2 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21854.md @@ -0,0 +1,6 @@ +... + +**Remediation action** + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21855.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21855.md new file mode 100644 index 000000000000..6a740c4322e2 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21855.md @@ -0,0 +1,6 @@ +... + +**Remediation action** + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21857.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21857.md new file mode 100644 index 000000000000..6a740c4322e2 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21857.md @@ -0,0 +1,6 @@ +... + +**Remediation action** + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21858.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21858.md new file mode 100644 index 000000000000..362bc8627a4c --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21858.md @@ -0,0 +1,11 @@ +When guest identities remain active but unused for extended periods, threat actors can exploit these dormant accounts as entry vectors into the organization. Inactive guest accounts represent a significant attack surface because they often maintain persistent access permissions to resources, applications, and data while remaining unmonitored by security teams. Threat actors frequently target these accounts through credential stuffing, password spraying, or by compromising the guest's home organization to gain lateral access. Once an inactive guest account is compromised, attackers can utilize existing access grants to: +- Move laterally within the tenant +- Escalate privileges through group memberships or application permissions +- Establish persistence through techniques like creating more service principals or modifying existing permissions + +The prolonged dormancy of these accounts provides attackers with extended dwell time to conduct reconnaissance, exfiltrate sensitive data, and establish backdoors without detection, as organizations typically focus monitoring efforts on active internal users rather than external guest accounts. + +**Remediation action** +- [Monitor and clean up stale guest accounts](https://learn.microsoft.com/en-us/entra/identity/users/clean-up-stale-guest-accounts?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21859.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21859.md new file mode 100644 index 000000000000..6a740c4322e2 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21859.md @@ -0,0 +1,6 @@ +... + +**Remediation action** + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21860.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21860.md new file mode 100644 index 000000000000..cc0b180268cd --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21860.md @@ -0,0 +1,12 @@ +The activity logs and reports in Microsoft Entra can help detect unauthorized access attempts or identify when tenant configuration changes. When logs are archived or integrated with Security Information and Event Management (SIEM) tools, security teams can implement powerful monitoring and detection security controls, proactive threat hunting, and incident response processes. The logs and monitoring features can be used to assess tenant health and provide evidence for compliance and audits. + +If logs aren't regularly archived or sent to a SIEM tool for querying, it's challenging to investigate sign-in issues. The absence of historical logs means that security teams might miss patterns of failed sign-in attempts, unusual activity, and other indicators of compromise. This lack of visibility can prevent the timely detection of breaches, allowing attackers to maintain undetected access for extended periods. + +**Remediation action** + +- [Configure Microsoft Entra diagnostic settings](https://learn.microsoft.com/entra/identity/monitoring-health/howto-configure-diagnostic-settings?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) +- [Integrate Microsoft Entra logs with Azure Monitor logs](https://learn.microsoft.com/entra/identity/monitoring-health/howto-integrate-activity-logs-with-azure-monitor-logs?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) +- [Stream Microsoft Entra logs to an event hub](https://learn.microsoft.com/entra/identity/monitoring-health/howto-stream-logs-to-event-hub?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) + +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21861.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21861.md new file mode 100644 index 000000000000..df6a5fb37715 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21861.md @@ -0,0 +1,11 @@ +Users considered at high risk by Microsoft Entra ID Protection have a high probability of compromise by threat actors. Threat actors can gain initial access via compromised valid accounts, where their suspicious activities continue despite triggering risk indicators. This oversight can enable persistence as threat actors perform activities that normally warrant investigation, such as unusual login patterns or suspicious inbox manipulation. + +A lack of triage of these risky users allows for expanded reconnaissance activities and lateral movement, with anomalous behavior patterns continuing to generate uninvestigated alerts. Threat actors become emboldened as security teams show they aren't actively responding to risk indicators. + +**Remediation action** + +- [Investigate high risk users](https://learn.microsoft.com/entra/id-protection/howto-identity-protection-investigate-risk?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) in Microsoft Entra ID Protection +- [Remediate high risk users and unblock](https://learn.microsoft.com/entra/id-protection/howto-identity-protection-remediate-unblock?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) in Microsoft Entra ID Protection + +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21862.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21862.md new file mode 100644 index 000000000000..5d6fdcb41824 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21862.md @@ -0,0 +1,9 @@ +Compromised workload identities (service principals and applications) allow threat actors to gain persistent access without user interaction or multifactor authentication. Microsoft Entra ID Protection monitors these identities for suspicious activities like leaked credentials, anomalous API traffic, and malicious applications. Unaddressed risky workload identities enable privilege escalation, lateral movement, data exfiltration, and persistent backdoors that bypass traditional security controls. Organizations must systematically investigate and remediate these risks to prevent unauthorized access. + +**Remediation action** + +- [Investigate and remediate risky workload identities](https://learn.microsoft.com/entra/id-protection/concept-workload-identity-risk?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci#investigate-risky-workload-identities) +- [Apply Conditional Access policies for workload identities](https://learn.microsoft.com/entra/identity/conditional-access/workload-identity?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) + +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21863.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21863.md new file mode 100644 index 000000000000..7729b0c365ac --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21863.md @@ -0,0 +1,11 @@ +Risky sign-ins flagged by Microsoft Entra ID Protection indicate a high probability of unauthorized access attempts. Threat actors use these sign-ins to gain an initial foothold. If these sign-ins remain uninvestigated, adversaries can establish persistence by repeatedly authenticating under the guise of legitimate users. + +A lack of response lets attackers execute reconnaissance, attempt to escalate their access, and blend into normal patterns. When untriaged sign-ins continue to generate alerts and there's no intervention, security gaps widen, facilitating lateral movement and defense evasion, as adversaries recognize the absence of an active security response. + +**Remediation action** + +- [Investigate risky sign-ins](https://learn.microsoft.com/entra/id-protection/howto-identity-protection-investigate-risk?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) +- [Remediate risks and unblock users](https://learn.microsoft.com/entra/id-protection/howto-identity-protection-remediate-unblock?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) + +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21864.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21864.md new file mode 100644 index 000000000000..6a740c4322e2 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21864.md @@ -0,0 +1,6 @@ +... + +**Remediation action** + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21865.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21865.md new file mode 100644 index 000000000000..fba7fae508d8 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21865.md @@ -0,0 +1,7 @@ +Without named locations configured in Microsoft Entra ID, threat actors can exploit the absence of location intelligence to conduct attacks without triggering location-based risk detections or security controls. When organizations fail to define named locations for trusted networks, branch offices, and known geographic regions, Microsoft Entra ID Protection can't assess location-based risk signals. Not having these policies in place can lead to increased false positives that create alert fatigue and potentially mask genuine threats. This configuration gap prevents the system from distinguishing between legitimate and illegitimate locations. For example, legitimate sign-ins from corporate networks and suspicious authentication attempts from high-risk locations (anonymous proxy networks, Tor exit nodes, or regions where the organization has no business presence). Threat actors can use this uncertainty to conduct credential stuffing attacks, password spray campaigns, and initial access attempts from malicious infrastructure without triggering location-based detections that would normally flag such activity as suspicious. Organizations can also lose the ability to implement adaptive security policies that could automatically apply stricter authentication requirements or block access entirely from untrusted geographic regions. Threat actors can maintain persistence and conduct lateral movement from any global location without encountering location-based security barriers, which should serve as an extra layer of defense against unauthorized access attempts. + +**Remediation action** + +- [Configure named locations to define trusted IP ranges and geographic regions for enhanced location-based risk detection and Conditional Access policy enforcement](https://learn.microsoft.com/en-us/entra/identity/conditional-access/concept-assignment-network?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21866.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21866.md new file mode 100644 index 000000000000..bdfe46a9f74d --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21866.md @@ -0,0 +1,8 @@ +Microsoft Entra recommendations give organizations opportunities to implement best practices and optimize their security posture. Not acting on these items might result in an increased attack surface area, suboptimal operations, or poor user experience. + +**Remediation action** + +- [Address all active or postponed recommendations in the Microsoft Entra admin center](https://learn.microsoft.com/entra/identity/monitoring-health/overview-recommendations?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci#how-does-it-work) + +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21867.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21867.md new file mode 100644 index 000000000000..d10b375bc681 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21867.md @@ -0,0 +1,9 @@ +Without owners, enterprise applications become orphaned assets that threat actors can exploit through credential harvesting and privilege escalation techniques. These applications often retain elevated permissions and access to sensitive resources while lacking proper oversight and security governance. The elevation of privilege to owners can raise a security concern, depending on the application's permissions. More critically, applications without an owner can create uncertainty in security monitoring where threat actors can establish persistence by using existing application permissions to access data or create backdoor accounts without triggering ownership-based detection mechanisms. + +When applications lack owners, security teams can't effectively conduct application lifecycle management. This gap leaves applications with potentially excessive permissions, outdated configurations, or compromised credentials that threat actors can discover through enumeration techniques and exploit to move laterally within the environment. The absence of ownership also prevents proper access reviews and permission audits, allowing threat actors to maintain long-term access through applications that should be decommissioned or had their permissions reduced. Not maintaining a clean application portfolio can provide persistent access vectors that can be used for data exfiltration or further compromise of the environment. + +**Remediation action** + +- [Assign owners to applications](https://learn.microsoft.com/en-us/entra/identity/enterprise-apps/assign-app-owners?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21868.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21868.md new file mode 100644 index 000000000000..c970477e6b2f --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21868.md @@ -0,0 +1,9 @@ +Without restrictions preventing guest users from registering and owning applications, threat actors can exploit external user accounts to establish persistent backdoor access to organizational resources through application registrations that might evade traditional security monitoring. When guest users own applications, compromised guest accounts can be used to exploit guest-owned applications that might have broad permissions. This vulnerability enables threat actors to request access to sensitive organizational data such as emails, files, and user information without the same level of scrutiny for internal user-owned applications. + +This attack vector is dangerous because guest-owned applications can be configured to request high-privilege permissions and, once granted consent, provide threat actors with legitimate OAuth tokens. Furthermore, guest-owned applications can serve as command and control infrastructure, so threat actors can maintain access even after the compromised guest account is detected and remediated. Application credentials and permissions might persist independently of the original guest user account, so threat actors can retain access. Guest-owned applications also complicate security auditing and governance efforts, as organizations might have limited visibility into the purpose and security posture of applications registered by external users. These hidden weaknesses in the application lifecycle management make it difficult to assess the true scope of data access granted to non-Microsoft entities through seemingly legitimate application registrations. + +**Remediation action** +- Remove guest users as owners from applications and service principals, and implement controls to prevent future guest user application ownership. +- [Restrict guest user access permissions](https://learn.microsoft.com/en-us/entra/identity/users/users-restrict-guest-permissions?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21869.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21869.md new file mode 100644 index 000000000000..17fa7ba85d04 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21869.md @@ -0,0 +1,10 @@ +When enterprise applications lack both explicit assignment requirements AND scoped provisioning controls, threat actors can exploit this dual weakness to gain unauthorized access to sensitive applications and data. The highest risk occurs when applications are configured with the default setting: "Assignment required" is set to "No" *and* provisioning isn't required or scoped. This dangerous combination allows threat actors who compromise any user account within the tenant to immediately access applications with broad user bases, expanding their attack surface and potential for lateral movement within the organization. + +While an application with open assignment but proper provisioning scoping (such as department-based filters or group membership requirements) maintains security controls through the provisioning layer, applications lacking both controls create unrestricted access pathways that threat actors can exploit. When applications provision accounts for all users without assignment restrictions, threat actors can abuse compromised accounts to conduct reconnaissance activities, enumerate sensitive data across multiple systems, or use the applications as staging points for further attacks against connected resources. This unrestricted access model is dangerous for applications that have elevated permissions or are connected to critical business systems. Threat actors can use any compromised user account to access sensitive information, modify data, or perform unauthorized actions that the application's permissions allow. The absence of both assignment controls and provisioning scoping also prevents organizations from implementing proper access governance. Without proper governance, it's difficult to track who has access to which applications, when access was granted, and whether access should be revoked based on role changes or employment status. Furthermore, applications with broad provisioning scopes can create cascading security risks where a single compromised account provides access to an entire ecosystem of connected applications and services. + +**Remediation action** +- Evaluate business requirements to determine appropriate access control method. [Restrict a Microsoft Entra app to a set of users](https://learn.microsoft.com/en-us/entra/identity-platform/howto-restrict-your-app-to-a-set-of-users?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci). +- Configure enterprise applications to require assignment for sensitive applications. [Learn about the "Assignment required" enterprise application property](https://learn.microsoft.com/en-us/entra/identity/enterprise-apps/application-properties?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci#assignment-required). +- Implement scoped provisioning based on groups, departments, or attributes. [Create scoping filters](https://learn.microsoft.com/en-us/entra/identity/app-provisioning/define-conditional-rules-for-provisioning-user-accounts?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci#create-scoping-filters). +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21870.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21870.md new file mode 100644 index 000000000000..91ed1541e019 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21870.md @@ -0,0 +1,9 @@ +Without Self-Service Password Reset (SSPR) enabled, users with password-related issues must contact help desk support, which can cause in operational delays and lost productivity. There are also potential security vulnerabilities during the extended timeframe required for administrative password resets. These delays not only reduce employee efficiency (especially in time-sensitive roles), but also increase support costs and strain IT resources. During these periods, threat actors might exploit locked accounts through social engineering attacks targeting help desk personnel. Threat actors can potentially convince support staff to reset passwords for accounts they don't legitimately control, enabling initial access to user credentials. + +When users are unable to reset their own passwords through secure, automated processes, they frequently resort to insecure workarounds. Examples include sharing accounts with colleagues, using weak passwords that are easier to remember, or writing down passwords in discoverable locations, all of which expand the attack surface for credential harvesting techniques. The lack of SSPR forces users to maintain static passwords for longer periods between administrative resets. This type of password policy increases the likelihood that compromised credentials from previous breaches or password spray attacks remain valid and usable by threat actors. The absence of user-controlled password reset capabilities also delays the response time for users to secure their accounts when they suspect compromise. This delay allows threat actors extended persistence within compromised accounts to perform reconnaissance, establish other access methods, or exfiltrate sensitive data before the account is eventually reset through administrative channels + +**Remediation action** + +- [Enable Self-Service Password Reset](https://learn.microsoft.com/en-us/entra/identity/authentication/tutorial-enable-sspr?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21872.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21872.md new file mode 100644 index 000000000000..41edcdf8e024 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21872.md @@ -0,0 +1,8 @@ +Threat actors can exploit the lack of multifactor authentication during new device registration. Once authenticated, they can register rogue devices, establish persistence, and circumvent security controls tied to trusted endpoints. This foothold enables attackers to exfiltrate sensitive data, deploy malicious applications, or move laterally, depending on the permissions of the accounts being used by the attacker. Without MFA enforcement, risk escalates as adversaries can continuously reauthenticate, evade detection, and execute objectives. + +**Remediation action** + +- [Deploy a Conditional Access policy to require multifactor authentication for device registration](https://learn.microsoft.com/entra/identity/conditional-access/policy-all-users-device-registration?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci). + +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21874.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21874.md new file mode 100644 index 000000000000..9b70ca640329 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21874.md @@ -0,0 +1,10 @@ +Limiting guest access to a known and approved list of tenants helps to prevent threat actors from exploiting unrestricted guest access to establish initial access through compromised external accounts or by creating accounts in untrusted tenants. Threat actors who gain access through an unrestricted domain can discover internal resources, users, and applications to perform additional attacks. + +Organizations should take inventory and configure an allowlist or blocklist to control B2B collaboration invitations from specific organizations. Without these controls, threat actors might use social engineering techniques to obtain invitations from legitimate internal users. + +**Remediation action** + +- Learn how to [set up a list of approved domains](https://learn.microsoft.com/entra/external-id/allow-deny-list?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci#add-an-allowlist). + +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21875.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21875.md new file mode 100644 index 000000000000..df03f46ad2ee --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21875.md @@ -0,0 +1,9 @@ +Access packages configured to allow "All users" instead of specific connected organizations expose your organization to uncontrolled external access. Threat actors can exploit this by requesting access through compromised external accounts from unauthorized organizations, bypassing the principle of least privilege. This enables initial access, reconnaissance, privilege escalation, and lateral movement within your environment. + +**Remediation action** + +- [Define trusted organizations as connected organizations](https://learn.microsoft.com/entra/id-governance/entitlement-management-organization?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci#view-the-list-of-connected-organizations) +- [Configure access packages to only allow specific connected organizations](https://learn.microsoft.com/entra/id-governance/entitlement-management-access-package-create?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci#allow-users-not-in-your-directory-to-request-the-access-package) + +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21876.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21876.md new file mode 100644 index 000000000000..6a740c4322e2 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21876.md @@ -0,0 +1,6 @@ +... + +**Remediation action** + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21877.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21877.md new file mode 100644 index 000000000000..ed479604ad5b --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21877.md @@ -0,0 +1,15 @@ +Inviting external guests is beneficial for organizational collaboration. However, in the absence of an assigned internal sponsor for each guest, these accounts might persist within the directory without clear accountability. This oversight creates a risk: threat actors could potentially compromise an unused or unmonitored guest account, and then establish an initial foothold within the tenant. Once granted access as an apparent "legitimate" user, an attacker might explore accessible resources and attempt privilege escalation, which could ultimately expose sensitive information or critical systems. An unmonitored guest account might therefore become the vector for unauthorized data access or a significant security breach. A typical attack sequence might use the following pattern, all achieved under the guise of a standard external collaborator: + +1. Initial access gained through compromised guest credentials +1. Persistence due to a lack of oversight. +1. Further escalation or lateral movement if the guest account possesses group memberships or elevated permissions. +1. Execution of malicious objectives. + +Mandating that every guest account is assigned to a sponsor directly mitigates this risk. Such a requirement ensures that each external user is linked to a responsible internal party who is expected to regularly monitor and attest to the guest's ongoing need for access. The sponsor feature within Microsoft Entra ID supports accountability by tracking the inviter and preventing the proliferation of "orphaned" guest accounts. When a sponsor manages the guest account lifecycle, such as removing access when collaboration concludes, the opportunity for threat actors to exploit neglected accounts is substantially reduced. This best practice is consistent with Microsoft’s guidance to require sponsorship for business guests as part of an effective guest access governance strategy. It strikes a balance between enabling collaboration and enforcing security, as it guarantees that each guest user's presence and permissions remain under ongoing internal oversight. + +**Remediation action** +- For each guest user that has no sponsor, assign a sponsor in Microsoft Entra ID. + - [Add a sponsor to a guest user in the Microsoft Entra admin center](https://learn.microsoft.com/en-us/entra/external-id/b2b-sponsors?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) + - [Add a sponsor to a guest user using Microsoft Graph](https://learn.microsoft.com/graph/api/user-post-sponsors?view=graph-rest-1.0&preserve-view=true&wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21878.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21878.md new file mode 100644 index 000000000000..762389bcce22 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21878.md @@ -0,0 +1,7 @@ +Entitlement management policies without expiration dates create persistent access that threat actors can exploit. When user assignments lack time bounds, compromised credentials maintain indefinite access, enabling attackers to establish persistence, escalate privileges through additional access packages, and conduct long-term malicious activities while remaining undetected. + +**Remediation action** + +- [Configure expiration settings for access packages](https://learn.microsoft.com/entra/id-governance/entitlement-management-access-package-lifecycle-policy?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci#specify-a-lifecycle) +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21879.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21879.md new file mode 100644 index 000000000000..5d2c99793491 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21879.md @@ -0,0 +1,11 @@ +## Overview + +Without enforced approval on entitlement management policies that allow external users, a threat actor can self-orchestrate initial access by submitting unattended requests that are auto-approved. Each successful request provisions or reuses a guest user object and grant access to resources included in the access package, immediately expanding reconnaissance surface. From that foothold the actor can enumerate additional collaboration surfaces, harvest shared files, and probe mis-scoped app permissions to escalate (e.g., abusing over-privileged group-based roles or app role assignments). They can persist by requesting multiple packages with overlapping or escalating privileges, re-extending assignments if expiration or reviews are lax, or by creating indirect sharing links inside granted SharePoint or Teams resources. Absence of an approval gate also removes a human anomaly check (sponsor/internal/external approver) that would otherwise filter suspicious volume, timing, geography, or improbable justification patterns, shrinking detection dwell-time. This accelerates lateral movement (pivoting through granted group memberships to additional workloads), facilitates data staging and exfiltration from SharePoint/Teams or app APIs, and increases the blast radius before downstream controls (access reviews, expirations) eventually trigger. Microsoft’s guidance explicitly states approval should be required when external users can request access to ensure oversight; bypassing it effectively converts a governed onboarding path into an unsupervised provisioning for external identities, expanding tenant-wide risk until manual discovery or periodic governance cycles intervene. + +**Remediation action** + +- [Configure approval for external request policies (toggle Require approval)](https://learn.microsoft.com/en-us/entra/id-governance/entitlement-management-access-package-approval-policy ) + + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21881.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21881.md new file mode 100644 index 000000000000..6a740c4322e2 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21881.md @@ -0,0 +1,6 @@ +... + +**Remediation action** + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21882.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21882.md new file mode 100644 index 000000000000..6a740c4322e2 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21882.md @@ -0,0 +1,6 @@ +... + +**Remediation action** + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21883.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21883.md new file mode 100644 index 000000000000..9440b98d3f84 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21883.md @@ -0,0 +1,8 @@ +Set up risk-based Conditional Access policies for workload identities based on risk policy in Microsoft Entra ID to make sure only trusted and verified workloads use sensitive resources. Without these policies, threat actors can compromise workload identities with minimal detection and perform further attacks. Without conditional controls to detect anomalous activity and other risks, there's no check against malicious operations like token forgery, access to sensitive resources, and disruption of workloads. The lack of automated containment mechanisms increases dwell time and affects the confidentiality, integrity, and availability of critical services. + +**Remediation action** +Create a risk-based Conditional Access policy for workload identities. +- [Create a risk-based Conditional Access policy](https://learn.microsoft.com/en-us/entra/identity/conditional-access/workload-identity?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci#create-a-risk-based-conditional-access-policy) + +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21884.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21884.md new file mode 100644 index 000000000000..58e372f02ff9 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21884.md @@ -0,0 +1,8 @@ +When workload identities operate without network-based Conditional Access restrictions, threat actors can compromise service principal credentials through various methods, such as exposed secrets in code repositories or intercepted authentication tokens. The threat actors can then use these credentials from any location globally. This unrestricted access enables threat actors to perform reconnaissance activities, enumerate resources, and map the tenant's infrastructure while appearing legitimate. Once the threat actor is established within the environment, they can move laterally between services, access sensitive data stores, and potentially escalate privileges by exploiting overly permissive service-to-service permissions. The lack of network restrictions makes it impossible to detect anomalous access patterns based on location. This gap allows threat actors to maintain persistent access and exfiltrate data over extended periods without triggering security alerts that would normally flag connections from unexpected networks or geographic locations. + +**Remediation action** + +- [Configure Conditional Access for workload identities](https://learn.microsoft.com/en-us/entra/identity/conditional-access/workload-identity?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) +- [Create named locations](https://learn.microsoft.com/en-us/entra/identity/conditional-access/concept-assignment-network?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) +- [Follow best practices for securing workload identities](https://learn.microsoft.com/en-us/entra/workload-id/workload-identities-overview?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21885.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21885.md new file mode 100644 index 000000000000..27edc026f6e2 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21885.md @@ -0,0 +1,10 @@ +OAuth applications configured with URLs that include wildcards, or URL shorteners increase the attack surface for threat actors. Insecure redirect URIs (reply URLs) might allow adversaries to manipulate authentication requests, hijack authorization codes, and intercept tokens by directing users to attacker-controlled endpoints. Wildcard entries expand the risk by permitting unintended domains to process authentication responses, while shortener URLs might facilitate phishing and token theft in uncontrolled environments. + +Without strict validation of redirect URIs, attackers can bypass security controls, impersonate legitimate applications, and escalate their privileges. This misconfiguration enables persistence, unauthorized access, and lateral movement, as adversaries exploit weak OAuth enforcement to infiltrate protected resources undetected. + +**Remediation action** + +- [Check the redirect URIs for your application registrations.](https://learn.microsoft.com/entra/identity-platform/reply-url?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) Make sure the redirect URIs don't have *.azurewebsites.net, wildcards, or URL shorteners. + +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21886.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21886.md new file mode 100644 index 000000000000..988a4cbca44d --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21886.md @@ -0,0 +1,15 @@ +When applications that support both authentication and provisioning through Microsoft Entra aren't configured for automatic provisioning, organizations become vulnerable to identity lifecycle gaps that threat actors can exploit. Without automated provisioning, user accounts might persist in applications after employees leave the organization. This vulnerability creates dormant accounts that threat actors can discover through reconnaissance activities. These orphaned accounts often retain their original access permissions but lack active monitoring, making them attractive targets for initial access. + +Threat actors who gain access to these dormant accounts can use them to establish persistence in the target application, as the accounts appear legitimate and might not trigger security alerts. From these compromised application accounts, attackers can: + +- Attempt to escalate their privileges by exploring application-specific permissions +- Access sensitive data stored within the application +- Use the application as a pivot point to access other connected systems + +The lack of centralized identity lifecycle management also makes it difficult for security teams to detect when an attacker is using these orphaned accounts, as the accounts might not be properly correlated with the organization's active user directory. + +**Remediation action** + +- [Configure application provisioning for missing applications](https://learn.microsoft.com/en-us/entra/identity/app-provisioning/configure-automatic-user-provisioning-portal?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21887.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21887.md new file mode 100644 index 000000000000..6a740c4322e2 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21887.md @@ -0,0 +1,6 @@ +... + +**Remediation action** + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21888.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21888.md new file mode 100644 index 000000000000..cb1d03b47129 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21888.md @@ -0,0 +1,8 @@ +Unmaintained or orphaned redirect URIs in app registrations create significant security vulnerabilities when they reference domains that no longer point to active resources. Threat actors can exploit these "dangling" DNS entries by provisioning resources at abandoned domains, effectively taking control of redirect endpoints. This vulnerability enables attackers to intercept authentication tokens and credentials during OAuth 2.0 flows, which can lead to unauthorized access, session hijacking, and potential broader organizational compromise. + +**Remediation action** + +- [Redirect URI (reply URL) outline and restrictions](https://learn.microsoft.com/entra/identity-platform/reply-url?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) + +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21889.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21889.md new file mode 100644 index 000000000000..d5f422aa0a93 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21889.md @@ -0,0 +1,10 @@ +Organizations with extensive user-facing password surfaces expose multiple entry points for threat actors to launch credential-based attacks. Frequent user interactions with password prompts across applications, devices, and workflows increase the risk of exploitation. Threat actors often begin with credential stuffing—using compromised credentials from data breaches—followed by password spraying to test common passwords across multiple accounts. Once initial access is gained, they conduct credential discovery by examining browser password stores, cached credentials in memory, and credential managers to harvest additional authentication materials. These stolen credentials enable lateral movement, allowing attackers to access more systems and applications, often escalating privileges by targeting administrative accounts that still rely on password authentication. In the persistence phase, attackers may create backdoor accounts with password-based access or weaken defenses by altering password policies. To evade detection, they leverage legitimate authentication channels, blending in with normal user activity while maintaining persistent access to organizational resources. + +**Remediation action** + + * [Enable passwordless authentication methods](https://learn.microsoft.com/en-us/entra/identity/authentication/how-to-plan-prerequisites-phishing-resistant-passwordless-authentication) + + * [Deploy FIDO2 security keys](https://learn.microsoft.com/en-us/entra/identity/authentication/how-to-enable-passkey-fido2) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21890.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21890.md new file mode 100644 index 000000000000..6a740c4322e2 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21890.md @@ -0,0 +1,6 @@ +... + +**Remediation action** + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21891.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21891.md new file mode 100644 index 000000000000..9c49450beb91 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21891.md @@ -0,0 +1,8 @@ +Configuring password reset notifications for administrator roles in Microsoft Entra ID enhances security by notifying privileged administrators when another administrator resets their password. This visibility helps detect unauthorized or suspicious activity that could indicate credential compromise or insider threats. Without these notifications, malicious actors could exploit elevated privileges to establish persistence, escalate access, or extract sensitive data. Proactive notifications support quick action, preserve privileged access integrity, and strengthen the overall security posture. + +**Remediation action** + +- [Notify all admins when other admins reset their passwords](https://learn.microsoft.com/en-us/entra/identity/authentication/concept-sspr-howitworks?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci#notify-all-admins-when-other-admins-reset-their-passwords) + +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21892.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21892.md new file mode 100644 index 000000000000..2d46aed74112 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21892.md @@ -0,0 +1 @@ +When sign-ins are not restricted to managed devices, threat actors can use unmanaged devices to establish initial access to organizational resources. Unmanaged devices lack organizational security controls, endpoint protection, and compliance verification, creating entry points for threat actors to exploit. Unmanaged devices lack centralized security controls, compliance monitoring, and policy enforcement, creating gaps in the organization's security perimeter. Threat actors can compromise these devices through malware, keyloggers, or credential harvesting tools, then use the captured credentials to authenticate corporate resources without detection. Accounts that are assigned administrative rights are a target for attackers. Requiring users with these highly privileged rights to perform actions from devices marked as compliant or Microsoft Entra hybrid joined can help limit possible exposure. Without device compliance requirements, threat actors can maintain persistence through uncontrolled endpoints, bypass security monitoring that would typically detect anomalous behavior on managed devices and use unmanaged devices as staging areas for lateral movement across network resources. diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21893.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21893.md new file mode 100644 index 000000000000..c27b1172c0bb --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21893.md @@ -0,0 +1,8 @@ +Require multifactor authentication (MFA) registration for all users. Based on studies, your account is more than 99% less likely to be compromised if you're using MFA. Even if you don't require MFA all the time, this policy ensures your users are ready when it's needed. + +**Remediation action** + +- [Configure the multifactor authentication registration policy](https://learn.microsoft.com/entra/id-protection/howto-identity-protection-configure-mfa-policy?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) + +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21894.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21894.md new file mode 100644 index 000000000000..6a740c4322e2 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21894.md @@ -0,0 +1,6 @@ +... + +**Remediation action** + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21895.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21895.md new file mode 100644 index 000000000000..6a740c4322e2 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21895.md @@ -0,0 +1,6 @@ +... + +**Remediation action** + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21896.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21896.md new file mode 100644 index 000000000000..4e2033aae288 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21896.md @@ -0,0 +1,9 @@ +Service principals without proper authentication credentials (certificates or client secrets) create security vulnerabilities that allow threat actors to impersonate these identities. This can lead to unauthorized access, lateral movement within your environment, privilege escalation, and persistent access that's difficult to detect and remediate. + +**Remediation action** + +- For your organization's service principals: [Add certificates or client secrets to the app registration](https://learn.microsoft.com/entra/identity-platform/how-to-add-credentials?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) +- For external service principals: Review and remove any unnecessary credentials to reduce security risk + +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21897.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21897.md new file mode 100644 index 000000000000..6a740c4322e2 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21897.md @@ -0,0 +1,6 @@ +... + +**Remediation action** + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21898.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21898.md new file mode 100644 index 000000000000..6a740c4322e2 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21898.md @@ -0,0 +1,6 @@ +... + +**Remediation action** + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21899.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21899.md new file mode 100644 index 000000000000..6a740c4322e2 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21899.md @@ -0,0 +1,6 @@ +... + +**Remediation action** + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21912.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21912.md new file mode 100644 index 000000000000..6a740c4322e2 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21912.md @@ -0,0 +1,6 @@ +... + +**Remediation action** + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21929.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21929.md new file mode 100644 index 000000000000..98fe3bbbd22b --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21929.md @@ -0,0 +1,9 @@ +Access packages for guest users without expiration dates or access reviews allow indefinite access to organizational resources. Compromised or stale guest accounts enable threat actors to maintain persistent, undetected access for lateral movement, privilege escalation, and data exfiltration. Without periodic validation, organizations cannot identify when business relationships change or when guest access is no longer needed. + +**Remediation action** + +- [Configure lifecycle settings](https://learn.microsoft.com/entra/id-governance/entitlement-management-access-package-lifecycle-policy?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) +- [Configure access reviews](https://learn.microsoft.com/entra/id-governance/entitlement-management-access-reviews-create?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) + +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21941.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21941.md new file mode 100644 index 000000000000..d00aeef5317c --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21941.md @@ -0,0 +1,18 @@ +Token protection policies in Entra ID tenants are crucial for safeguarding authentication tokens from misuse and unauthorized access. Without these policies, threat actors can intercept and manipulate tokens, leading to unauthorized access to sensitive resources. This can result in data exfiltration, lateral movement within the network, and potential compromise of privileged accounts. + +When token protection is not properly configured, threat actors can exploit several attack vectors: + +1. **Token theft and replay attacks** - Attackers can steal authentication tokens from compromised devices and replay them from different locations +2. **Session hijacking** - Without secure sign-in session controls, attackers can hijack legitimate user sessions +3. **Cross-platform token abuse** - Tokens issued for one platform (like mobile) can be misused on other platforms (like web browsers) +4. **Persistent access** - Compromised tokens can provide long-term unauthorized access without triggering security alerts + +The attack chain typically involves initial access through token theft, followed by privilege escalation and persistence, ultimately leading to data exfiltration and impact across the organization's Microsoft 365 environment. + +**Remediation action** +- [Configure Conditional Access policies as per the best practices](https://learn.microsoft.com/en-us/entra/identity/conditional-access/concept-token-protection#create-a-conditional-access-policy) +- [Microsoft Entra Conditional Access token protection explained](https://learn.microsoft.com/en-us/entra/identity/conditional-access/concept-token-protection) +- [Configure session controls in Conditional Access](https://learn.microsoft.com/en-us/entra/identity/conditional-access/concept-conditional-access-session) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21953.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21953.md new file mode 100644 index 000000000000..eeb1984a2ec4 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21953.md @@ -0,0 +1,10 @@ +Without Local Admin Password Solution (LAPS) deployed, threat actors exploit static local administrator passwords to establish initial access. After threat actors compromise a single device with a shared local administrator credential, they can move laterally across the environment and authenticate to other systems sharing the same password. Compromised local administrator access gives threat actors system-level privileges, letting them disable security controls, install persistent backdoors, exfiltrate sensitive data, and establish command and control channels. + +The automated password rotation and centralized management of LAPS closes this security gap and adds controls to help manage who has access to these critical accounts. Without solutions like LAPS, you can't detect or respond to unauthorized use of local administrator accounts, giving threat actors extended dwell time to achieve their objectives while remaining undetected. + +**Remediation action** + +- [Configure Windows Local Administrator Password Solution](https://learn.microsoft.com/entra/identity/devices/howto-manage-local-admin-passwords?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci). + +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21954.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21954.md new file mode 100644 index 000000000000..a265a8360a51 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21954.md @@ -0,0 +1,8 @@ +When non-administrator users can access their own BitLocker keys, threat actors who compromise user credentials through phishing, credential stuffing, or malware-based keyloggers gain direct access to encryption keys without requiring privilege escalation. This access vector enables threat actors to persist on the compromised device by accessing encrypted volumes. Once threat actors obtain BitLocker keys, they can decrypt sensitive data stored on the device, including cached credentials, local databases, and confidential files. Without proper restrictions, a single compromised user account provides immediate access to all encrypted data on that device, negating the primary security benefit of disk encryption and creating a pathway for lateral movement to network resources accessed from the compromised system. + +**Remediation action** + +[Configure BitLocker key access restrictions through Microsoft Entra admin](https://learn.microsoft.com/en-us/entra/identity/devices/manage-device-identities#view-or-copy-bitlocker-keys) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21955.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21955.md new file mode 100644 index 000000000000..09ba1be61884 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21955.md @@ -0,0 +1,8 @@ +When local administrators on Microsoft Entra joined devices aren't properly managed, threat actors with compromised credentials can execute device takeover attacks by removing organizational administrators and disabling the device's connection to Microsoft Entra. This lack of control results in complete loss of organizational control, creating orphaned assets that can't be managed or recovered. + +**Remediation action** + +- [Manage the local administrators on Microsoft Entra joined devices](https://learn.microsoft.com/entra/identity/devices/assign-local-admin?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci#manage-the-microsoft-entra-joined-device-local-administrator-role) + +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21964.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21964.md new file mode 100644 index 000000000000..0dc276f4daf8 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21964.md @@ -0,0 +1,6 @@ +Configure protected actions for Conditional Access policy create, update and delete permissions, and Authentication Context update permission. Refer to the guidance on common stronger Conditional Access policies: + +**Remediation action** +[What are protected actions in Microsoft Entra ID? - Microsoft Entra ID | Microsoft Learn](https://learn.microsoft.com/en-us/entra/identity/role-based-access-control/protected-actions-overview) + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21983.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21983.md new file mode 100644 index 000000000000..6a740c4322e2 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21983.md @@ -0,0 +1,6 @@ +... + +**Remediation action** + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21984.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21984.md new file mode 100644 index 000000000000..6a740c4322e2 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21984.md @@ -0,0 +1,6 @@ +... + +**Remediation action** + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21985.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21985.md new file mode 100644 index 000000000000..fa3494bd6918 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21985.md @@ -0,0 +1,9 @@ +Microsoft Entra seamless single sign-on (Seamless SSO) is a legacy authentication feature designed to provide passwordless access for domain-joined devices that are not hybrid Microsoft Entra ID joined. Seamless SSO relies on Kerberos authentication and is primarily beneficial for older operating systems like Windows 7 and Windows 8.1, which do not support Primary Refresh Tokens (PRT). If these legacy systems are no longer present in the environment, continuing to use Seamless SSO introduces unnecessary complexity and potential security exposure. Threat actors could exploit misconfigured or stale Kerberos tickets, or compromise the `AZUREADSSOACC` computer account in Active Directory, which holds the Kerberos decryption key used by Microsoft Entra ID. Once compromised, attackers could impersonate users, bypass modern authentication controls, and gain unauthorized access to cloud resources. Disabling Seamless SSO in environments where it is no longer needed reduces the attack surface and enforces the use of modern, token-based authentication mechanisms that offer stronger protections. + +**Remediation action** + +- [Review how Seamless SSO works](https://learn.microsoft.com/en-us/entra/identity/hybrid/connect/how-to-connect-sso-how-it-works?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) +- [Disable Seamless SSO](https://learn.microsoft.com/en-us/entra/identity/hybrid/connect/how-to-connect-sso-faq?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci#how-can-i-disable-seamless-sso-) +- [Clean up stale devices in Microsoft Entra ID](https://learn.microsoft.com/en-us/entra/identity/devices/manage-stale-devices?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21992.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21992.md new file mode 100644 index 000000000000..feff2e1e7706 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21992.md @@ -0,0 +1,13 @@ +If certificates aren't rotated regularly, they can give threat actors an extended window to extract and exploit them, leading to unauthorized access. When credentials like these are exposed, attackers can blend their malicious activities with legitimate operations, making it easier to bypass security controls. If an attacker compromises an application’s certificate, they can escalate their privileges within the system, leading to broader access and control, depending on the application's privileges. + +Query all of your service principals and application registrations that have certificate credentials. Make sure the certificate start date is less than 180 days. + +**Remediation action** + +- [Define an application management policy to manage certificate lifetimes](https://learn.microsoft.com/graph/api/resources/applicationauthenticationmethodpolicy?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) +- [Define a trusted certificate chain of trust](https://learn.microsoft.com/graph/api/resources/certificatebasedapplicationconfiguration?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) +- [Create a least privileged custom role to rotate application credentials](https://learn.microsoft.com/entra/identity/role-based-access-control/custom-create?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) +- [Learn more about app management policies to manage certificate based credentials](https://devblogs.microsoft.com/identity/app-management-policy/) + +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA22072.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA22072.md new file mode 100644 index 000000000000..a44892e995ac --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA22072.md @@ -0,0 +1,11 @@ +Allowing security questions as a self-service password reset (SSPR) method weakens the password reset process because answers are frequently guessable, reused across sites, or discoverable through open-source intelligence (OSINT). Threat actors enumerate or phish users, derive likely responses (family names, schools, and locations), and then trigger password reset flows to bypass stronger methods by exploiting the weaker knowledge-based gate. After they successfully reset a password on an account that isn't protected by multifactor authentication they can: gain valid primary credentials, establish session tokens, and laterally expand by registering more durable authentication methods, add forwarding rules, or exfiltrate sensitive data. + +Eliminating this method removes a weak link in the password reset process. Some organizations might have specific business reasons for leaving security questions enabled, but this isn't recommended. + +**Remediation action** + +- [Disable security questions in SSPR policy](https://learn.microsoft.com/entra/identity/authentication/concept-authentication-security-questions?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) +- [Select authentication methods and registration options](https://learn.microsoft.com/entra/identity/authentication/tutorial-enable-sspr?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci#select-authentication-methods-and-registration-options) + +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA22124.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA22124.md new file mode 100644 index 000000000000..ed0906a131a4 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA22124.md @@ -0,0 +1,8 @@ +Leaving high-priority Microsoft Entra recommendations unaddressed can create a gap in an organization’s security posture, offering threat actors opportunities to exploit known weaknesses. Not acting on these items might result in an increased attack surface area, suboptimal operations, or poor user experience. + +**Remediation action** + +- [Address all high priority recommendations in the Microsoft Entra admin center](https://learn.microsoft.com/entra/identity/monitoring-health/overview-recommendations?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci#how-does-it-work) + +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA22128.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA22128.md new file mode 100644 index 000000000000..ec419c5d76e3 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA22128.md @@ -0,0 +1,9 @@ +When guest users are assigned highly privileged directory roles such as Global Administrator or Privileged Role Administrator, organizations create significant security vulnerabilities that threat actors can exploit for initial access through compromised external accounts or business partner environments. Since guest users originate from external organizations without direct control of security policies, threat actors who compromise these external identities can gain privileged access to the target organization's Microsoft Entra tenant. + +When threat actors obtain access through compromised guest accounts with elevated privileges, they can escalate their own privilege to create other backdoor accounts, modify security policies, or assign themselves permanent roles within the organization. The compromised privileged guest accounts enable threat actors to establish persistence and then make all the changes they need to remain undetected. For example they could create cloud-only accounts, bypass Conditional Access policies applied to internal users, and maintain access even after the guest's home organization detects the compromise. Threat actors can then conduct lateral movement using administrative privileges to access sensitive resources, modify audit settings, or disable security monitoring across the entire tenant. Threat actors can reach complete compromise of the organization's identity infrastructure while maintaining plausible deniability through the external guest account origin. + +**Remediation action** + +- [Remove Guest users from privileged roles](https://learn.microsoft.com/en-us/entra/identity/role-based-access-control/best-practices?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) +%TestResult% + diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA22659.md b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA22659.md new file mode 100644 index 000000000000..a030b51bb467 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA22659.md @@ -0,0 +1,19 @@ +Threat actors increasingly target workload identities (applications, service principals, and managed identities) because they lack human factors and often use long-lived credentials. A compromise often looks like the following path: + +1. Credential abuse or key theft. +1. Non-interactive sign-ins to cloud resources. +1. Lateral movement via app permissions. +1. Persistence through new secrets or role assignments. + +Microsoft Entra ID Protection continuously generates risky workload identity detections and flags sign-in events with risk state and detail. Risky workload identity sign-ins that aren’t triaged (confirmed compromised, dismissed, or marked safe), detection fatigue, and a large alert backlog can be challenging for IT admins to manage. This heavy workload can let repeated malicious access, privilege escalation, and token replay to continue to go unnoticed. To make the workload manageable, address risky workload identity sign-ins in two parts: + +- Close the loop: Triage sign-ins and record an authoritative decision on each risky event. +- Drive containment: Disable the service principal, rotate credentials, or revoke sessions. + +**Remediation action** + +- [Investigate risky workload identities and perform appropriate remediation ](https://learn.microsoft.com/en-us/entra/id-protection/concept-workload-identity-risk?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) +- [Dismiss workload identity risks when determined to be false positives](https://learn.microsoft.com/graph/api/riskyserviceprincipal-dismiss?view=graph-rest-1.0&preserve-view=true&wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) +- [Confirm compromised workload identities when risks are validated](https://learn.microsoft.com/graph/api/riskyserviceprincipal-confirmcompromised?view=graph-rest-1.0&preserve-view=true&wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci) +%TestResult% + From c58f485e99e6c597123e6cb6eae80646411a4439 Mon Sep 17 00:00:00 2001 From: Logan Cook <2997336+MWG-Logan@users.noreply.github.com> Date: Fri, 2 Jan 2026 11:44:51 -0500 Subject: [PATCH 071/503] feat(alerts): add Get-CIPPAlertIntunePolicyConflicts function --- .../Get-CIPPAlertIntunePolicyConflicts.ps1 | 144 ++++++++++++++++ ...t-CIPPAlertIntunePolicyConflicts.Tests.ps1 | 155 ++++++++++++++++++ 2 files changed, 299 insertions(+) create mode 100644 Modules/CIPPCore/Public/Alerts/Get-CIPPAlertIntunePolicyConflicts.ps1 create mode 100644 Tests/Alerts/Get-CIPPAlertIntunePolicyConflicts.Tests.ps1 diff --git a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertIntunePolicyConflicts.ps1 b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertIntunePolicyConflicts.ps1 new file mode 100644 index 000000000000..2bc6170cd6fb --- /dev/null +++ b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertIntunePolicyConflicts.ps1 @@ -0,0 +1,144 @@ +function Get-CIPPAlertIntunePolicyConflicts { + <# + .FUNCTIONALITY + Entrypoint + #> + [CmdletBinding()] + param ( + [Parameter(Mandatory = $false)] + [Alias('input')] + $InputValue, + $TenantFilter + ) + + # Normalize JSON/string input to object when possible + if ($InputValue -is [string]) { + try { + if ($InputValue.Trim().StartsWith('{')) { + $InputValue = $InputValue | ConvertFrom-Json -ErrorAction Stop + } + } catch { + # Leave as-is if parsing fails + } + } + + $Config = [ordered]@{ + AlertEachIssue = $false # align with AlertEachAdmin convention (false = aggregated) + IncludePolicies = $true + IncludeApplications = $true + AlertConflicts = $true + AlertErrors = $true + } + + if ($InputValue -is [hashtable] -or $InputValue -is [pscustomobject]) { + # Primary key follows AlertEach* convention; legacy Aggregate supported (true == aggregated) + if ($null -ne $InputValue.AlertEachIssue) { $Config.AlertEachIssue = [bool]$InputValue.AlertEachIssue } + if ($null -ne $InputValue.Aggregate) { $Config.AlertEachIssue = -not [bool]$InputValue.Aggregate } + + $Config.IncludePolicies = if ($null -ne $InputValue.IncludePolicies) { [bool]$InputValue.IncludePolicies } else { $Config.IncludePolicies } + $Config.IncludeApplications = if ($null -ne $InputValue.IncludeApplications) { [bool]$InputValue.IncludeApplications } else { $Config.IncludeApplications } + $Config.AlertConflicts = if ($null -ne $InputValue.AlertConflicts) { [bool]$InputValue.AlertConflicts } else { $Config.AlertConflicts } + $Config.AlertErrors = if ($null -ne $InputValue.AlertErrors) { [bool]$InputValue.AlertErrors } else { $Config.AlertErrors } + } elseif ($InputValue -is [bool]) { + # Back-compat for boolean toggle used as Aggregate previously + $Config.AlertEachIssue = -not [bool]$InputValue + } + + if (-not $Config.IncludePolicies -and -not $Config.IncludeApplications) { + return + } + + $AlertableStatuses = @() + if ($Config.AlertErrors) { $AlertableStatuses += 'error', 'failed' } + if ($Config.AlertConflicts) { $AlertableStatuses += 'conflict' } + + if (-not $AlertableStatuses) { + return + } + + $HasLicense = Test-CIPPStandardLicense -StandardName 'IntunePolicyStatus' -TenantFilter $TenantFilter -RequiredCapabilities @( + 'INTUNE_A', + 'MDM_Services', + 'EMS', + 'SCCM', + 'MICROSOFTINTUNEPLAN1' + ) + + if (-not $HasLicense) { + return + } + + $Issues = @() + + if ($Config.IncludePolicies) { + try { + $ManagedDevices = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/deviceManagement/managedDevices?`$select=id,deviceName,userPrincipalName&`$expand=deviceConfigurationStates(`$select=displayName,state,settingStates)" -tenantid $TenantFilter + + foreach ($Device in $ManagedDevices) { + $PolicyStates = $Device.deviceConfigurationStates | Where-Object { $_.state -and ($AlertableStatuses -contains $_.state.ToLowerInvariant()) } + foreach ($State in $PolicyStates) { + $Issues += [PSCustomObject]@{ + Message = "Policy '$($State.displayName)' is $($State.state) on device '$($Device.deviceName)' for $($Device.userPrincipalName)." + Tenant = $TenantFilter + Type = 'Policy' + PolicyName = $State.displayName + IssueStatus = $State.state + DeviceName = $Device.deviceName + UserPrincipalName = $Device.userPrincipalName + DeviceId = $Device.id + } + } + } + } catch { + Write-AlertMessage -tenant $TenantFilter -message "Failed to query Intune policy states: $(Get-NormalizedError -message $_.Exception.Message)" + } + } + + if ($Config.IncludeApplications) { + try { + $Applications = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/deviceAppManagement/mobileApps?`$select=id,displayName&`$expand=deviceStatuses(`$select=installState,deviceName,userPrincipalName,deviceId)" -tenantid $TenantFilter + + foreach ($App in $Applications) { + $BadStatuses = $App.deviceStatuses | Where-Object { + $_.installState -and ($AlertableStatuses -contains $_.installState.ToLowerInvariant()) + } + + foreach ($Status in $BadStatuses) { + $Issues += [PSCustomObject]@{ + Message = "App '$($App.displayName)' install is $($Status.installState) on device '$($Status.deviceName)' for $($Status.userPrincipalName)." + Tenant = $TenantFilter + Type = 'Application' + AppName = $App.displayName + IssueStatus = $Status.installState + DeviceName = $Status.deviceName + UserPrincipalName = $Status.userPrincipalName + DeviceId = $Status.deviceId + } + } + } + } catch { + Write-AlertMessage -tenant $TenantFilter -message "Failed to query Intune application states: $(Get-NormalizedError -message $_.Exception.Message)" + } + } + + if (-not $Issues) { + return + } + + if (-not $Config.AlertEachIssue) { + $PolicyCount = ($Issues | Where-Object { $_.Type -eq 'Policy' }).Count + $AppCount = ($Issues | Where-Object { $_.Type -eq 'Application' }).Count + + $AlertData = @([PSCustomObject]@{ + Message = "Found $PolicyCount policy issues and $AppCount application issues in Intune." + Tenant = $TenantFilter + PolicyIssues = $PolicyCount + AppIssues = $AppCount + Issues = $Issues + }) + } else { + $AlertData = $Issues + } + + Write-AlertTrace -cmdletName $MyInvocation.MyCommand -tenantFilter $TenantFilter -data $AlertData +} diff --git a/Tests/Alerts/Get-CIPPAlertIntunePolicyConflicts.Tests.ps1 b/Tests/Alerts/Get-CIPPAlertIntunePolicyConflicts.Tests.ps1 new file mode 100644 index 000000000000..e37958e93814 --- /dev/null +++ b/Tests/Alerts/Get-CIPPAlertIntunePolicyConflicts.Tests.ps1 @@ -0,0 +1,155 @@ +# Pester tests for Get-CIPPAlertIntunePolicyConflicts +# Verifies aggregation defaults, toggles, and error handling + +BeforeAll { + $RepoRoot = Split-Path -Parent (Split-Path -Parent (Split-Path -Parent $PSCommandPath)) + $AlertPath = Join-Path $RepoRoot 'Modules/CIPPCore/Public/Alerts/Get-CIPPAlertIntunePolicyConflicts.ps1' + + function New-GraphGetRequest { param($uri, $tenantid) } + function Write-AlertTrace { param($cmdletName, $tenantFilter, $data) } + function Write-AlertMessage { param($tenant, $message) } + function Get-NormalizedError { param($message) $message } + function Test-CIPPStandardLicense { param($StandardName, $TenantFilter, $RequiredCapabilities) } + + . $AlertPath +} + +Describe 'Get-CIPPAlertIntunePolicyConflicts' { + BeforeEach { + $script:CapturedData = $null + $script:CapturedTenant = $null + $script:CapturedErrorMessage = $null + + Mock -CommandName Test-CIPPStandardLicense -MockWith { $true } + + Mock -CommandName Write-AlertTrace -MockWith { + param($cmdletName, $tenantFilter, $data) + $script:CapturedData = $data + $script:CapturedTenant = $tenantFilter + } + + Mock -CommandName Write-AlertMessage -MockWith { + param($tenant, $message) + $script:CapturedErrorMessage = $message + } + + Mock -CommandName New-GraphGetRequest -MockWith { + param($uri, $tenantid) + if ($uri -like '*deviceManagement/managedDevices*') { + @( + [pscustomobject]@{ + deviceName = 'PC-01' + userPrincipalName = 'user1@contoso.com' + id = 'device-1' + deviceConfigurationStates = @( + [pscustomobject]@{ displayName = 'Policy A'; state = 'conflict' } + ) + } + ) + } elseif ($uri -like '*deviceAppManagement/mobileApps*') { + @( + [pscustomobject]@{ + displayName = 'App A' + deviceStatuses = @( + [pscustomobject]@{ installState = 'error'; deviceName = 'PC-01'; userPrincipalName = 'user1@contoso.com'; deviceId = 'device-1' } + ) + } + ) + } + } + } + + It 'defaults to aggregated alerting with all mechanisms and statuses' { + Get-CIPPAlertIntunePolicyConflicts -TenantFilter 'contoso.onmicrosoft.com' + + $CapturedTenant | Should -Be 'contoso.onmicrosoft.com' + $CapturedData | Should -Not -BeNullOrEmpty + $CapturedData.Count | Should -Be 1 + $CapturedData[0].PolicyIssues | Should -Be 1 + $CapturedData[0].AppIssues | Should -Be 1 + $CapturedData[0].Issues.Count | Should -Be 2 + } + + It 'emits per-issue alerts when AlertEachIssue is true' { + Get-CIPPAlertIntunePolicyConflicts -TenantFilter 'contoso.onmicrosoft.com' -InputValue @{ AlertEachIssue = $true } + + $CapturedData | Should -Not -BeNullOrEmpty + $CapturedData.Count | Should -Be 2 + ($CapturedData | Where-Object { $_.Type -eq 'Policy' }).Count | Should -Be 1 + ($CapturedData | Where-Object { $_.Type -eq 'Application' }).Count | Should -Be 1 + } + + It 'supports legacy Aggregate=false for per-issue alerts' { + Get-CIPPAlertIntunePolicyConflicts -TenantFilter 'contoso.onmicrosoft.com' -InputValue @{ Aggregate = $false } + + $CapturedData | Should -Not -BeNullOrEmpty + $CapturedData.Count | Should -Be 2 + ($CapturedData | Where-Object { $_.Type -eq 'Policy' }).Count | Should -Be 1 + ($CapturedData | Where-Object { $_.Type -eq 'Application' }).Count | Should -Be 1 + } + + It 'honors IncludePolicies toggle' { + Get-CIPPAlertIntunePolicyConflicts -TenantFilter 'contoso.onmicrosoft.com' -InputValue @{ IncludePolicies = $false } + + $CapturedData | Should -Not -BeNullOrEmpty + $CapturedData.Count | Should -Be 1 + $CapturedData[0].PolicyIssues | Should -Be 0 + $CapturedData[0].AppIssues | Should -Be 1 + $CapturedData[0].Issues.Count | Should -Be 1 + ($CapturedData[0].Issues | Where-Object { $_.Type -eq 'Policy' }).Count | Should -Be 0 + } + + It 'suppresses conflict-only alerts when AlertConflicts is false' { + # conflict for policy, error for app; expect only app when conflicts suppressed + Mock -CommandName New-GraphGetRequest -MockWith { + param($uri, $tenantid) + if ($uri -like '*deviceManagement/managedDevices*') { + @( + [pscustomobject]@{ + deviceName = 'PC-02' + userPrincipalName = 'user2@contoso.com' + id = 'device-2' + deviceConfigurationStates = @( + [pscustomobject]@{ displayName = 'Policy B'; state = 'conflict' } + ) + } + ) + } elseif ($uri -like '*deviceAppManagement/mobileApps*') { + @( + [pscustomobject]@{ + displayName = 'App B' + deviceStatuses = @( + [pscustomobject]@{ installState = 'error'; deviceName = 'PC-02'; userPrincipalName = 'user2@contoso.com'; deviceId = 'device-2' } + ) + } + ) + } + } + + Get-CIPPAlertIntunePolicyConflicts -TenantFilter 'contoso.onmicrosoft.com' -InputValue @{ AlertConflicts = $false; Aggregate = $false } + + $CapturedData | Should -Not -BeNullOrEmpty + $CapturedData.Count | Should -Be 1 + $CapturedData[0].Type | Should -Be 'Application' + $CapturedData[0].IssueStatus | Should -Be 'error' + } + + It 'skips processing when license check fails' { + Mock -CommandName Test-CIPPStandardLicense -MockWith { $false } -Verifiable + + Get-CIPPAlertIntunePolicyConflicts -TenantFilter 'contoso.onmicrosoft.com' + + $CapturedData | Should -BeNullOrEmpty + $CapturedTenant | Should -BeNullOrEmpty + } + + It 'writes alert message when Graph call fails' { + Mock -CommandName New-GraphGetRequest -MockWith { throw 'Graph failure' } -Verifiable + + Get-CIPPAlertIntunePolicyConflicts -TenantFilter 'contoso.onmicrosoft.com' + + $CapturedData | Should -BeNullOrEmpty + $CapturedErrorMessage | Should -Match 'Failed to query Intune (policy|application) states' + $CapturedErrorMessage | Should -Match 'Graph failure' + } +} From 90c90bce8a82a60b90f1ca10dee309d073491d75 Mon Sep 17 00:00:00 2001 From: Logan Cook <2997336+MWG-Logan@users.noreply.github.com> Date: Fri, 2 Jan 2026 11:48:37 -0500 Subject: [PATCH 072/503] fix(alerts): correct state comparison in Get-CIPPAlertIntunePolicyConflicts `-contains` is case-insensitive as-is, `.ToLowerInvariant()` is wasteful in this case. --- .../Public/Alerts/Get-CIPPAlertIntunePolicyConflicts.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertIntunePolicyConflicts.ps1 b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertIntunePolicyConflicts.ps1 index 2bc6170cd6fb..684f6bd0fc87 100644 --- a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertIntunePolicyConflicts.ps1 +++ b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertIntunePolicyConflicts.ps1 @@ -75,7 +75,7 @@ function Get-CIPPAlertIntunePolicyConflicts { $ManagedDevices = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/deviceManagement/managedDevices?`$select=id,deviceName,userPrincipalName&`$expand=deviceConfigurationStates(`$select=displayName,state,settingStates)" -tenantid $TenantFilter foreach ($Device in $ManagedDevices) { - $PolicyStates = $Device.deviceConfigurationStates | Where-Object { $_.state -and ($AlertableStatuses -contains $_.state.ToLowerInvariant()) } + $PolicyStates = $Device.deviceConfigurationStates | Where-Object { $_.state -and ($AlertableStatuses -contains $_.state) } foreach ($State in $PolicyStates) { $Issues += [PSCustomObject]@{ Message = "Policy '$($State.displayName)' is $($State.state) on device '$($Device.deviceName)' for $($Device.userPrincipalName)." From 4820ac2cdb3cf9892bc778c4156621d222150b47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Fri, 2 Jan 2026 19:21:14 +0100 Subject: [PATCH 073/503] Fix: hashtable alert errors --- .../Public/Alerts/Get-CIPPAlertOnedriveQuota.ps1 | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertOnedriveQuota.ps1 b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertOnedriveQuota.ps1 index 4cbb4580d160..b39ea94d9a48 100644 --- a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertOnedriveQuota.ps1 +++ b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertOnedriveQuota.ps1 @@ -33,15 +33,13 @@ function Get-CIPPAlertOneDriveQuota { if ($UsagePercent -gt $InputValue) { $GBLeft = [math]::Round(($_.storageAllocatedInBytes - $_.storageUsedInBytes) / 1GB) [PSCustomObject]@{ - Details = @{ - Message = "$($_.ownerPrincipalName): OneDrive is $UsagePercent% full. OneDrive has $($GBLeft)GB storage left" - Owner = $_.ownerPrincipalName - UsagePercent = $UsagePercent - GBLeft = $GBLeft - StorageUsedInBytes = $_.storageUsedInBytes - StorageAllocatedInBytes = $_.storageAllocatedInBytes - Tenant = $TenantFilter - } + Message = "$($_.ownerPrincipalName): OneDrive is $UsagePercent% full. OneDrive has $($GBLeft)GB storage left" + Owner = $_.ownerPrincipalName + UsagePercent = $UsagePercent + GBLeft = $GBLeft + StorageUsedInBytes = $_.storageUsedInBytes + StorageAllocatedInBytes = $_.storageAllocatedInBytes + Tenant = $TenantFilter } } From a02c796f25ea4bd49f5716f42a4e892ec7b4083f Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 2 Jan 2026 13:37:52 -0500 Subject: [PATCH 074/503] Skip templates with empty tenant filter groups Templates with tenant filters that resolve to no tenants (empty groups) are now skipped instead of being assigned an empty tenant list. This prevents unnecessary processing of templates that do not apply to any tenants. --- .../CIPPCore/Public/Functions/Get-CIPPTenantAlignment.ps1 | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Modules/CIPPCore/Public/Functions/Get-CIPPTenantAlignment.ps1 b/Modules/CIPPCore/Public/Functions/Get-CIPPTenantAlignment.ps1 index cb81201cfc29..9e48681c1b02 100644 --- a/Modules/CIPPCore/Public/Functions/Get-CIPPTenantAlignment.ps1 +++ b/Modules/CIPPCore/Public/Functions/Get-CIPPTenantAlignment.ps1 @@ -125,13 +125,14 @@ function Get-CIPPTenantAlignment { $TenantValues.Add($filterItem.value) } } - - if ($TenantValues -contains 'AllTenants') { +` + if ($TenantValues -contains 'AllTenants') { $AppliestoAllTenants = $true } elseif ($TenantValues.Count -gt 0) { $TemplateAssignedTenants = @($TenantValues) } else { - $TemplateAssignedTenants = @() + # Filter was specified but resolved to no tenants (empty group) - skip this template + continue } } else { $AppliestoAllTenants = $true From 9e444552d65a053198d4dff035d15d79aba5794e Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Fri, 2 Jan 2026 22:54:20 +0100 Subject: [PATCH 075/503] Add synopsis --- .../Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24540.ps1 | 4 ++++ .../Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24541.ps1 | 4 ++++ .../Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24542.ps1 | 4 ++++ .../Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24543.ps1 | 4 ++++ .../Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24545.ps1 | 4 ++++ .../Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24547.ps1 | 4 ++++ .../Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24548.ps1 | 4 ++++ .../Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24549.ps1 | 4 ++++ .../Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24550.ps1 | 4 ++++ .../Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24552.ps1 | 4 ++++ .../Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24553.ps1 | 4 ++++ .../Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24560.ps1 | 4 ++++ .../Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24564.ps1 | 4 ++++ .../Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24568.ps1 | 4 ++++ .../Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24569.ps1 | 4 ++++ .../Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24574.ps1 | 4 ++++ .../Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24575.ps1 | 4 ++++ .../Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24576.ps1 | 4 ++++ .../Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24784.ps1 | 4 ++++ .../Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24839.ps1 | 4 ++++ .../Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24840.ps1 | 4 ++++ .../Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24870.ps1 | 4 ++++ .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21772.ps1 | 4 ++++ .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21773.ps1 | 4 ++++ .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21774.ps1 | 4 ++++ .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21776.ps1 | 4 ++++ .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21780.ps1 | 4 ++++ .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21783.ps1 | 4 ++++ .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21784.ps1 | 4 ++++ .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21786.ps1 | 4 ++++ .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21787.ps1 | 4 ++++ .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21790.ps1 | 4 ++++ .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21791.ps1 | 4 ++++ .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21792.ps1 | 4 ++++ .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21793.ps1 | 4 ++++ .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21796.ps1 | 4 ++++ .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21797.ps1 | 4 ++++ .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21799.ps1 | 4 ++++ .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21802.ps1 | 4 ++++ .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21803.ps1 | 4 ++++ .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21804.ps1 | 4 ++++ .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21806.ps1 | 4 ++++ .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21807.ps1 | 4 ++++ .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21808.ps1 | 4 ++++ .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21809.ps1 | 4 ++++ .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21810.ps1 | 4 ++++ .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21811.ps1 | 4 ++++ .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21819.ps1 | 4 ++++ .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21820.ps1 | 4 ++++ .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21822.ps1 | 4 ++++ .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21823.ps1 | 4 ++++ .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21825.ps1 | 4 ++++ 52 files changed, 208 insertions(+) diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24540.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24540.ps1 index 5d2b7956a77f..6d5a0779280f 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24540.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24540.ps1 @@ -1,4 +1,8 @@ function Invoke-CippTestZTNA24540 { + <# + .SYNOPSIS + Windows Firewall policies protect against unauthorized network access + #> param($Tenant) #Tested - Device try { diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24541.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24541.ps1 index 76861fe6d16f..0fdfbeabcd90 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24541.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24541.ps1 @@ -1,4 +1,8 @@ function Invoke-CippTestZTNA24541 { + <# + .SYNOPSIS + Compliance policies protect Windows devices + #> param($Tenant) $TestId = 'ZTNA24541' diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24542.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24542.ps1 index 411b52e4d51e..3007668bbd2c 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24542.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24542.ps1 @@ -1,4 +1,8 @@ function Invoke-CippTestZTNA24542 { + <# + .SYNOPSIS + Compliance policies protect macOS devices + #> param($Tenant) $TestId = 'ZTNA24542' diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24543.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24543.ps1 index 65060decff98..34609066d498 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24543.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24543.ps1 @@ -1,4 +1,8 @@ function Invoke-CippTestZTNA24543 { + <# + .SYNOPSIS + Compliance policies protect iOS/iPadOS devices + #> param($Tenant) $TestId = 'ZTNA24543' diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24545.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24545.ps1 index 14365a3f5022..4bd08c9e4c0f 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24545.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24545.ps1 @@ -1,4 +1,8 @@ function Invoke-CippTestZTNA24545 { + <# + .SYNOPSIS + Compliance policies protect fully managed and corporate-owned Android devices + #> param($Tenant) $TestId = 'ZTNA24545' diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24547.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24547.ps1 index 8dc4eb319501..fa0276e25538 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24547.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24547.ps1 @@ -1,4 +1,8 @@ function Invoke-CippTestZTNA24547 { + <# + .SYNOPSIS + Compliance policies protect personally owned Android devices + #> param($Tenant) $TestId = 'ZTNA24547' diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24548.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24548.ps1 index 14914f41fc1e..6a17dd5fce14 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24548.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24548.ps1 @@ -1,4 +1,8 @@ function Invoke-CippTestZTNA24548 { + <# + .SYNOPSIS + Data on iOS/iPadOS is protected by app protection policies + #> param($Tenant) $TestId = 'ZTNA24548' diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24549.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24549.ps1 index 9ad5be48f319..1f64dd363403 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24549.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24549.ps1 @@ -1,4 +1,8 @@ function Invoke-CippTestZTNA24549 { + <# + .SYNOPSIS + Data on Android is protected by app protection policies + #> param($Tenant) $TestId = 'ZTNA24549' diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24550.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24550.ps1 index a66111d9891a..25c21ea87021 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24550.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24550.ps1 @@ -1,4 +1,8 @@ function Invoke-CippTestZTNA24550 { + <# + .SYNOPSIS + Data on Windows is protected by BitLocker encryption + #> param($Tenant) #Tested - Device diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24552.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24552.ps1 index e9868bbdc9da..2538953695e4 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24552.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24552.ps1 @@ -1,4 +1,8 @@ function Invoke-CippTestZTNA24552 { + <# + .SYNOPSIS + Data on macOS is protected by firewall + #> param($Tenant) #Tested - Device diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24553.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24553.ps1 index 3f0360726ed1..59e9c6fa7656 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24553.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24553.ps1 @@ -1,4 +1,8 @@ function Invoke-CippTestZTNA24553 { + <# + .SYNOPSIS + Windows Update policies are enforced to reduce risk from unpatched vulnerabilities + #> param($Tenant) #Tested - Device diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24560.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24560.ps1 index 08a89d64350c..474d5e0ea143 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24560.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24560.ps1 @@ -1,4 +1,8 @@ function Invoke-CippTestZTNA24560 { + <# + .SYNOPSIS + Local administrator credentials on Windows are protected by Windows LAPS + #> param($Tenant) #Tested - Device diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24564.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24564.ps1 index 5e3e4d3b7fea..7e1cf0930194 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24564.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24564.ps1 @@ -1,4 +1,8 @@ function Invoke-CippTestZTNA24564 { + <# + .SYNOPSIS + Local account usage on Windows is restricted to reduce unauthorized access + #> param($Tenant) #Tested - Device diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24568.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24568.ps1 index 1b857bf9af62..847153b271f4 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24568.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24568.ps1 @@ -1,4 +1,8 @@ function Invoke-CippTestZTNA24568 { + <# + .SYNOPSIS + Platform SSO is configured to strengthen authentication on macOS devices + #> param($Tenant) #Tested - Device diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24569.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24569.ps1 index 30a21d9242d4..9160e651165c 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24569.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24569.ps1 @@ -1,4 +1,8 @@ function Invoke-CippTestZTNA24569 { + <# + .SYNOPSIS + FileVault encryption protects data on macOS devices + #> param($Tenant) $TestId = 'ZTNA24569' diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24574.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24574.ps1 index 6b73c76e52e9..53a148070a39 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24574.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24574.ps1 @@ -1,4 +1,8 @@ function Invoke-CippTestZTNA24574 { + <# + .SYNOPSIS + Attack Surface Reduction rules are applied to Windows devices to prevent exploitation of vulnerable system components + #> param($Tenant) #Tested - Device diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24575.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24575.ps1 index a4ba59b4d283..06b2d03b151c 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24575.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24575.ps1 @@ -1,4 +1,8 @@ function Invoke-CippTestZTNA24575 { + <# + .SYNOPSIS + Defender Antivirus policies protect Windows devices from malware + #> param($Tenant) #Tested - Device diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24576.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24576.ps1 index a735a0339441..5eea31149f19 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24576.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24576.ps1 @@ -1,4 +1,8 @@ function Invoke-CippTestZTNA24576 { + <# + .SYNOPSIS + Endpoint Analytics is enabled to help identify risks on Windows devices + #> param($Tenant) $TestId = 'ZTNA24576' diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24784.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24784.ps1 index 143be08e164c..ef50fd2f69ba 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24784.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24784.ps1 @@ -1,4 +1,8 @@ function Invoke-CippTestZTNA24784 { + <# + .SYNOPSIS + Defender Antivirus policies protect macOS devices from malware + #> param($Tenant) #Tested - Device diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24839.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24839.ps1 index aa4c3c2d8d30..8670f6c80c06 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24839.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24839.ps1 @@ -1,4 +1,8 @@ function Invoke-CippTestZTNA24839 { + <# + .SYNOPSIS + Secure Wi-Fi profiles protect iOS devices from unauthorized network access + #> param($Tenant) #Tested - Device diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24840.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24840.ps1 index fade320d9e20..368dc561c37c 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24840.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24840.ps1 @@ -1,4 +1,8 @@ function Invoke-CippTestZTNA24840 { + <# + .SYNOPSIS + Secure Wi-Fi profiles protect Android devices from unauthorized network access + #> param($Tenant) $TestId = 'ZTNA24840' diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24870.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24870.ps1 index 88d99dfd3c26..54b077462543 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24870.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24870.ps1 @@ -1,4 +1,8 @@ function Invoke-CippTestZTNA24870 { + <# + .SYNOPSIS + Secure Wi-Fi profiles protect macOS devices from unauthorized network access + #> param($Tenant) $TestId = 'ZTNA24870' diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21772.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21772.ps1 index cb8372f7d59b..84ab88e00648 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21772.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21772.ps1 @@ -1,4 +1,8 @@ function Invoke-CippTestZTNA21772 { + <# + .SYNOPSIS + Applications do not have client secrets configured + #> param($Tenant) #tested try { diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21773.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21773.ps1 index 26b275c3b491..f6d8885d7ff9 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21773.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21773.ps1 @@ -1,4 +1,8 @@ function Invoke-CippTestZTNA21773 { + <# + .SYNOPSIS + Applications do not have certificates with expiration longer than 180 days + #> param($Tenant) #tested try { diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21774.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21774.ps1 index 5f46cbb47e19..f4705649b532 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21774.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21774.ps1 @@ -1,4 +1,8 @@ function Invoke-CippTestZTNA21774 { + <# + .SYNOPSIS + Microsoft services applications do not have credentials configured + #> param($Tenant) try { diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21776.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21776.ps1 index f686c201db83..145aba1e2113 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21776.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21776.ps1 @@ -1,4 +1,8 @@ function Invoke-CippTestZTNA21776 { + <# + .SYNOPSIS + User consent settings are restricted + #> param($Tenant) #tested try { diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21780.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21780.ps1 index 7535bd516efe..b66a67e8f0d5 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21780.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21780.ps1 @@ -1,4 +1,8 @@ function Invoke-CippTestZTNA21780 { + <# + .SYNOPSIS + No usage of ADAL in the tenant + #> param($Tenant) #tested try { diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21783.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21783.ps1 index 9fc97c15eabe..ba66d83a030b 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21783.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21783.ps1 @@ -1,4 +1,8 @@ function Invoke-CippTestZTNA21783 { + <# + .SYNOPSIS + Privileged Microsoft Entra built-in roles are targeted with Conditional Access policies to enforce phishing-resistant methods + #> param($Tenant) #tested try { diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21784.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21784.ps1 index 61f18be9cc48..dfdd58c1b0d5 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21784.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21784.ps1 @@ -1,4 +1,8 @@ function Invoke-CippTestZTNA21784 { + <# + .SYNOPSIS + All user sign in activity uses phishing-resistant authentication methods + #> param($Tenant) #tested try { diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21786.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21786.ps1 index c7e0c587de5e..c4a2ba5e6ba5 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21786.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21786.ps1 @@ -1,4 +1,8 @@ function Invoke-CippTestZTNA21786 { + <# + .SYNOPSIS + User sign-in activity uses token protection + #> param($Tenant) #tested try { diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21787.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21787.ps1 index 0912092a8056..d42327c88ab3 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21787.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21787.ps1 @@ -1,4 +1,8 @@ function Invoke-CippTestZTNA21787 { + <# + .SYNOPSIS + Permissions to create new tenants are limited to the Tenant Creator role + #> param($Tenant) #tested try { diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21790.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21790.ps1 index 39804492cbf0..8b5a4447e647 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21790.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21790.ps1 @@ -1,4 +1,8 @@ function Invoke-CippTestZTNA21790 { + <# + .SYNOPSIS + Outbound cross-tenant access settings are configured + #> param($Tenant) #tested try { diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21791.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21791.ps1 index 6e6d23b0ff5d..4144c20f7c60 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21791.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21791.ps1 @@ -1,4 +1,8 @@ function Invoke-CippTestZTNA21791 { + <# + .SYNOPSIS + Guests cannot invite other guests + #> param($Tenant) #tested try { diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21792.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21792.ps1 index b36e7e7da340..9ebd3434fc68 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21792.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21792.ps1 @@ -1,4 +1,8 @@ function Invoke-CippTestZTNA21792 { + <# + .SYNOPSIS + Guests have restricted access to directory objects + #> param($Tenant) #tested try { diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21793.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21793.ps1 index 6b50e520eccd..7d2371837330 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21793.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21793.ps1 @@ -1,4 +1,8 @@ function Invoke-CippTestZTNA21793 { + <# + .SYNOPSIS + Tenant restrictions v2 policy is configured + #> param($Tenant) #tested try { diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21796.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21796.ps1 index cf2597532777..b6efc7edfeb3 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21796.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21796.ps1 @@ -1,4 +1,8 @@ function Invoke-CippTestZTNA21796 { + <# + .SYNOPSIS + Block legacy authentication policy is configured + #> param($Tenant) #tested try { diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21797.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21797.ps1 index 93ce2c12d187..e743b8903290 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21797.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21797.ps1 @@ -1,4 +1,8 @@ function Invoke-CippTestZTNA21797 { + <# + .SYNOPSIS + Restrict access to high risk users + #> param($Tenant) #tested try { diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21799.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21799.ps1 index 833bf23642ea..d54ea0ebe666 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21799.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21799.ps1 @@ -1,4 +1,8 @@ function Invoke-CippTestZTNA21799 { + <# + .SYNOPSIS + Restrict high risk sign-ins + #> param($Tenant) #tested try { diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21802.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21802.ps1 index b5b6f4be7787..08aa35895dde 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21802.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21802.ps1 @@ -1,4 +1,8 @@ function Invoke-CippTestZTNA21802 { + <# + .SYNOPSIS + Microsoft Authenticator app shows sign-in context + #> param($Tenant) #tested try { diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21803.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21803.ps1 index 0d54fc1283fe..3136648578e3 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21803.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21803.ps1 @@ -1,4 +1,8 @@ function Invoke-CippTestZTNA21803 { + <# + .SYNOPSIS + Migrate from legacy MFA and SSPR policies + #> param($Tenant) #Tested try { diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21804.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21804.ps1 index fbfe61e0cce5..54b956583cce 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21804.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21804.ps1 @@ -1,4 +1,8 @@ function Invoke-CippTestZTNA21804 { + <# + .SYNOPSIS + SMS and Voice Call authentication methods are disabled + #> param($Tenant) #Tested try { diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21806.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21806.ps1 index fb8eb6fa158e..ef7719563dbb 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21806.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21806.ps1 @@ -1,4 +1,8 @@ function Invoke-CippTestZTNA21806 { + <# + .SYNOPSIS + Secure the MFA registration (My Security Info) page + #> param($Tenant) #tested try { diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21807.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21807.ps1 index a5a1cba9d851..8afc5ac006c5 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21807.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21807.ps1 @@ -1,4 +1,8 @@ function Invoke-CippTestZTNA21807 { + <# + .SYNOPSIS + Creating new applications and service principals is restricted to privileged users + #> param($Tenant) #Tested try { diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21808.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21808.ps1 index 0b74d58db63d..1832ece21887 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21808.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21808.ps1 @@ -1,4 +1,8 @@ function Invoke-CippTestZTNA21808 { + <# + .SYNOPSIS + Restrict device code flow + #> param($Tenant) #Tested try { diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21809.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21809.ps1 index 049ab58f1277..bd14f97cbc60 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21809.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21809.ps1 @@ -1,4 +1,8 @@ function Invoke-CippTestZTNA21809 { + <# + .SYNOPSIS + Admin consent workflow is enabled + #> param($Tenant) #Tested try { diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21810.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21810.ps1 index 1d60ce86273b..c88c54bf41cb 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21810.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21810.ps1 @@ -1,4 +1,8 @@ function Invoke-CippTestZTNA21810 { + <# + .SYNOPSIS + Resource-specific consent is restricted + #> param($Tenant) #Tested try { diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21811.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21811.ps1 index a24b8c767975..898baadc8614 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21811.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21811.ps1 @@ -1,4 +1,8 @@ function Invoke-CippTestZTNA21811 { + <# + .SYNOPSIS + Password expiration is disabled + #> param($Tenant) #Tested try { diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21819.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21819.ps1 index 42e78f9155ad..647b8707a78a 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21819.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21819.ps1 @@ -1,4 +1,8 @@ function Invoke-CippTestZTNA21819 { + <# + .SYNOPSIS + Activation alert for Global Administrator role assignment + #> param($Tenant) #Tested $TestId = 'ZTNA21819' diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21820.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21820.ps1 index 8b872f18520b..845dfc5560d3 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21820.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21820.ps1 @@ -1,4 +1,8 @@ function Invoke-CippTestZTNA21820 { + <# + .SYNOPSIS + Activation alert for all privileged role assignments + #> param($Tenant) #Tested $TestId = 'ZTNA21820' diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21822.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21822.ps1 index c041c8a141d4..f9a9e2427c23 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21822.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21822.ps1 @@ -1,4 +1,8 @@ function Invoke-CippTestZTNA21822 { + <# + .SYNOPSIS + Guest access is limited to approved tenants + #> param($Tenant) #Tested $TestId = 'ZTNA21822' diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21823.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21823.ps1 index abf1bfdcdcae..2001fd0d0a9b 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21823.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21823.ps1 @@ -1,4 +1,8 @@ function Invoke-CippTestZTNA21823 { + <# + .SYNOPSIS + Guest self-service sign-up via user flow is disabled + #> param($Tenant) $TestId = 'ZTNA21823' diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21825.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21825.ps1 index 164704c92648..05865853b6bc 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21825.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21825.ps1 @@ -1,4 +1,8 @@ function Invoke-CippTestZTNA21825 { + <# + .SYNOPSIS + Privileged users have short-lived sign-in sessions + #> param($Tenant) $TestId = 'ZTNA21825' From 1c1bba8b2aea26c4502a6ec50aa35286d3b8ab5e Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Fri, 2 Jan 2026 22:58:37 +0100 Subject: [PATCH 076/503] Add synopsis --- .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21812.ps1 | 4 ++++ .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21813.ps1 | 4 ++++ .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21814.ps1 | 4 ++++ .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21815.ps1 | 4 ++++ .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21816.ps1 | 4 ++++ .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21818.ps1 | 4 ++++ .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21824.ps1 | 4 ++++ .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21828.ps1 | 4 ++++ .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21829.ps1 | 4 ++++ .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21830.ps1 | 4 ++++ .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21835.ps1 | 4 ++++ .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21836.ps1 | 4 ++++ .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21837.ps1 | 4 ++++ .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21838.ps1 | 4 ++++ .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21839.ps1 | 4 ++++ .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21840.ps1 | 4 ++++ .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21841.ps1 | 4 ++++ .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21842.ps1 | 4 ++++ .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21844.ps1 | 4 ++++ .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21845.ps1 | 4 ++++ .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21846.ps1 | 4 ++++ .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21847.ps1 | 4 ++++ .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21848.ps1 | 4 ++++ .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21849.ps1 | 4 ++++ .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21850.ps1 | 4 ++++ .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21858.ps1 | 4 ++++ .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21861.ps1 | 4 ++++ .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21862.ps1 | 4 ++++ .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21863.ps1 | 4 ++++ .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21865.ps1 | 4 ++++ .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21866.ps1 | 4 ++++ .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21868.ps1 | 4 ++++ .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21869.ps1 | 4 ++++ .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21872.ps1 | 4 ++++ .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21874.ps1 | 4 ++++ .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21877.ps1 | 4 ++++ .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21886.ps1 | 4 ++++ .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21896.ps1 | 4 ++++ .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21964.ps1 | 4 ++++ .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21992.ps1 | 4 ++++ .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA22128.ps1 | 4 ++++ .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24572.ps1 | 4 ++++ 42 files changed, 168 insertions(+) diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21812.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21812.ps1 index 69ef4c54d276..5bd0291730a5 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21812.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21812.ps1 @@ -1,4 +1,8 @@ function Invoke-CippTestZTNA21812 { + <# + .SYNOPSIS + Maximum number of Global Administrators doesn't exceed five users + #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21813.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21813.ps1 index da54b451cf23..84552c025074 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21813.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21813.ps1 @@ -1,4 +1,8 @@ function Invoke-CippTestZTNA21813 { + <# + .SYNOPSIS + High Global Administrator to privileged user ratio + #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21814.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21814.ps1 index 4287eeb7c1ae..6a4324dc6302 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21814.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21814.ps1 @@ -1,4 +1,8 @@ function Invoke-CippTestZTNA21814 { + <# + .SYNOPSIS + Privileged accounts are cloud native identities + #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21815.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21815.ps1 index 4e2fc0b24648..cb7eeca9b2f4 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21815.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21815.ps1 @@ -1,4 +1,8 @@ function Invoke-CippTestZTNA21815 { + <# + .SYNOPSIS + All privileged role assignments are activated just in time and not permanently active + #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21816.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21816.ps1 index 7ec660d6a4f6..659a9e6d239d 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21816.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21816.ps1 @@ -1,4 +1,8 @@ function Invoke-CippTestZTNA21816 { + <# + .SYNOPSIS + All Microsoft Entra privileged role assignments are managed with PIM + #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21818.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21818.ps1 index d964daed6d1c..b739f447cf85 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21818.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21818.ps1 @@ -1,4 +1,8 @@ function Invoke-CippTestZTNA21818 { + <# + .SYNOPSIS + Privileged role activations have monitoring and alerting configured + #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21824.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21824.ps1 index c49334aa1930..402ccc64c613 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21824.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21824.ps1 @@ -1,4 +1,8 @@ function Invoke-CippTestZTNA21824 { + <# + .SYNOPSIS + Guests don't have long lived sign-in sessions + #> param($Tenant) #Tested try { diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21828.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21828.ps1 index b0980e2f0aa5..71826e660f64 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21828.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21828.ps1 @@ -1,4 +1,8 @@ function Invoke-CippTestZTNA21828 { + <# + .SYNOPSIS + Authentication transfer is blocked + #> param($Tenant) #Tested try { diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21829.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21829.ps1 index 09beb6e96219..0cbf936ffe26 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21829.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21829.ps1 @@ -1,4 +1,8 @@ function Invoke-CippTestZTNA21829 { + <# + .SYNOPSIS + Use cloud authentication + #> param($Tenant) #Tested $TestId = 'ZTNA21829' diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21830.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21830.ps1 index 5b202a4db4d5..15c5f1976be4 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21830.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21830.ps1 @@ -1,4 +1,8 @@ function Invoke-CippTestZTNA21830 { + <# + .SYNOPSIS + Conditional Access policies for Privileged Access Workstations are configured + #> param($Tenant) $TestId = 'ZTNA21830' diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21835.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21835.ps1 index 6608c730f8fa..55219b6eed5f 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21835.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21835.ps1 @@ -1,4 +1,8 @@ function Invoke-CippTestZTNA21835 { + <# + .SYNOPSIS + Emergency access accounts are configured appropriately + #> param($Tenant) #Untested $TestId = 'ZTNA21835' diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21836.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21836.ps1 index 52645947ce3b..a150ccda9371 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21836.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21836.ps1 @@ -1,4 +1,8 @@ function Invoke-CippTestZTNA21836 { + <# + .SYNOPSIS + Workload Identities are not assigned privileged roles + #> param($Tenant) #Untested $TestId = 'ZTNA21836' diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21837.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21837.ps1 index aeda6312a4bd..cfb87a22c04b 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21837.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21837.ps1 @@ -1,4 +1,8 @@ function Invoke-CippTestZTNA21837 { + <# + .SYNOPSIS + Limit the maximum number of devices per user to 10 + #> param($Tenant) $TestId = 'ZTNA21837' diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21838.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21838.ps1 index d6d3834987eb..cd9d1c3d9e33 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21838.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21838.ps1 @@ -1,4 +1,8 @@ function Invoke-CippTestZTNA21838 { + <# + .SYNOPSIS + Security key authentication method enabled + #> param($Tenant) $TestId = 'ZTNA21838' diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21839.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21839.ps1 index c05793f36f06..8a47e09669cb 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21839.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21839.ps1 @@ -1,4 +1,8 @@ function Invoke-CippTestZTNA21839 { + <# + .SYNOPSIS + Passkey authentication method enabled + #> param($Tenant) $TestId = 'ZTNA21839' diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21840.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21840.ps1 index c80c6ff1288f..59cf3bf89426 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21840.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21840.ps1 @@ -1,4 +1,8 @@ function Invoke-CippTestZTNA21840 { + <# + .SYNOPSIS + Security key attestation is enforced + #> param($Tenant) #Tested $TestId = 'ZTNA21840' diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21841.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21841.ps1 index af69945f8016..5b7cd3c273da 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21841.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21841.ps1 @@ -1,4 +1,8 @@ function Invoke-CippTestZTNA21841 { + <# + .SYNOPSIS + Microsoft Authenticator app report suspicious activity setting is enabled + #> param($Tenant) #Tested $TestId = 'ZTNA21841' diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21842.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21842.ps1 index 57286a32689d..94f63825a6dd 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21842.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21842.ps1 @@ -1,4 +1,8 @@ function Invoke-CippTestZTNA21842 { + <# + .SYNOPSIS + Block administrators from using SSPR + #> param($Tenant) $TestId = 'ZTNA21842' diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21844.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21844.ps1 index 58ceffb6f351..e35322790b18 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21844.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21844.ps1 @@ -1,4 +1,8 @@ function Invoke-CippTestZTNA21844 { + <# + .SYNOPSIS + Block legacy Azure AD PowerShell module + #> param($Tenant) $TestId = 'ZTNA21844' diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21845.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21845.ps1 index 205ce34ec927..9c8a591dbda5 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21845.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21845.ps1 @@ -1,4 +1,8 @@ function Invoke-CippTestZTNA21845 { + <# + .SYNOPSIS + Temporary access pass is enabled + #> param($Tenant) $TestId = 'ZTNA21845' diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21846.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21846.ps1 index 9c34d8e6c554..a6d113b9d6e0 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21846.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21846.ps1 @@ -1,4 +1,8 @@ function Invoke-CippTestZTNA21846 { + <# + .SYNOPSIS + Restrict Temporary Access Pass to Single Use + #> param($Tenant) $TestId = 'ZTNA21846' diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21847.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21847.ps1 index 0da6a4bd84f4..332e75dd3908 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21847.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21847.ps1 @@ -1,4 +1,8 @@ function Invoke-CippTestZTNA21847 { + <# + .SYNOPSIS + Password protection for on-premises is enabled + #> param($Tenant) $TestId = 'ZTNA21847' diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21848.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21848.ps1 index 6d0abd291b8b..f258c596f218 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21848.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21848.ps1 @@ -1,4 +1,8 @@ function Invoke-CippTestZTNA21848 { + <# + .SYNOPSIS + Add organizational terms to the banned password list + #> param($Tenant) $TestId = 'ZTNA21848' diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21849.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21849.ps1 index 17f34521d2ae..3014ca59afee 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21849.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21849.ps1 @@ -1,4 +1,8 @@ function Invoke-CippTestZTNA21849 { + <# + .SYNOPSIS + Smart lockout duration is set to a minimum of 60 + #> param($Tenant) $TestId = 'ZTNA21849' diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21850.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21850.ps1 index 2cf320e64865..cc372297ec95 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21850.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21850.ps1 @@ -1,4 +1,8 @@ function Invoke-CippTestZTNA21850 { + <# + .SYNOPSIS + Smart lockout threshold set to 10 or less + #> param($Tenant) $TestId = 'ZTNA21850' diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21858.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21858.ps1 index a85e39fae183..3c1430225936 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21858.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21858.ps1 @@ -1,4 +1,8 @@ function Invoke-CippTestZTNA21858 { + <# + .SYNOPSIS + Inactive guest identities are disabled or removed from the tenant + #> param($Tenant) #Tested try { diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21861.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21861.ps1 index fd4e95775f14..80a1e3296ce9 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21861.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21861.ps1 @@ -1,4 +1,8 @@ function Invoke-CippTestZTNA21861 { + <# + .SYNOPSIS + All high-risk users are triaged + #> param($Tenant) $TestId = 'ZTNA21861' diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21862.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21862.ps1 index bdd846220047..c0f407fb7e74 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21862.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21862.ps1 @@ -1,4 +1,8 @@ function Invoke-CippTestZTNA21862 { + <# + .SYNOPSIS + All risky workload identities are triaged + #> param($Tenant) $TestId = 'ZTNA21862' diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21863.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21863.ps1 index d96587aa19b2..004c4698b883 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21863.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21863.ps1 @@ -1,4 +1,8 @@ function Invoke-CippTestZTNA21863 { + <# + .SYNOPSIS + All high-risk sign-ins are triaged + #> param($Tenant) $TestId = 'ZTNA21863' diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21865.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21865.ps1 index db84fdd54baa..cb7c13a419c9 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21865.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21865.ps1 @@ -1,4 +1,8 @@ function Invoke-CippTestZTNA21865 { + <# + .SYNOPSIS + Named locations are configured + #> param($Tenant) $TestId = 'ZTNA21865' diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21866.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21866.ps1 index 230f4b5726ae..b1fdc8d32ab1 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21866.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21866.ps1 @@ -1,4 +1,8 @@ function Invoke-CippTestZTNA21866 { + <# + .SYNOPSIS + All Microsoft Entra recommendations are addressed + #> param($Tenant) #Tested $TestId = 'ZTNA21866' diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21868.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21868.ps1 index da2e379835a7..87345f01e9e9 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21868.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21868.ps1 @@ -1,4 +1,8 @@ function Invoke-CippTestZTNA21868 { + <# + .SYNOPSIS + Guests do not own apps in the tenant + #> param($Tenant) try { diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21869.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21869.ps1 index 1e1c7e8d22ad..6dac00b5974a 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21869.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21869.ps1 @@ -1,4 +1,8 @@ function Invoke-CippTestZTNA21869 { + <# + .SYNOPSIS + Enterprise applications must require explicit assignment or scoped provisioning + #> param($Tenant) #tenant try { diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21872.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21872.ps1 index 6fa29a976334..128dc5579251 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21872.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21872.ps1 @@ -1,4 +1,8 @@ function Invoke-CippTestZTNA21872 { + <# + .SYNOPSIS + Require multifactor authentication for device join and device registration using user action + #> param($Tenant) $TestId = 'ZTNA21872' diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21874.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21874.ps1 index 82f30f5f99c1..d50649a51137 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21874.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21874.ps1 @@ -1,4 +1,8 @@ function Invoke-CippTestZTNA21874 { + <# + .SYNOPSIS + Guest access is limited to approved tenants + #> param($Tenant) $TestId = 'ZTNA21874' diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21877.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21877.ps1 index 2d644fd0e654..4fed8b3cca45 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21877.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21877.ps1 @@ -1,4 +1,8 @@ function Invoke-CippTestZTNA21877 { + <# + .SYNOPSIS + All guests have a sponsor + #> param($Tenant) #Tested try { diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21886.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21886.ps1 index 451d5d3e6a04..aec2ad6c6b1b 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21886.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21886.ps1 @@ -1,4 +1,8 @@ function Invoke-CippTestZTNA21886 { + <# + .SYNOPSIS + Applications are configured for automatic user provisioning + #> param($Tenant) #Tested try { diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21896.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21896.ps1 index 425fc891dfb3..20fc29dfef3f 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21896.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21896.ps1 @@ -1,4 +1,8 @@ function Invoke-CippTestZTNA21896 { + <# + .SYNOPSIS + Service principals do not have certificates or credentials associated with them + #> param($Tenant) #tested try { diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21964.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21964.ps1 index 2d281cb6cbed..cee729bcafec 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21964.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21964.ps1 @@ -1,4 +1,8 @@ function Invoke-CippTestZTNA21964 { + <# + .SYNOPSIS + Enable protected actions to secure Conditional Access policy creation and changes + #> param($Tenant) $TestId = 'ZTNA21964' diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21992.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21992.ps1 index ee4a277727bc..c0f29d74e0c4 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21992.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21992.ps1 @@ -1,4 +1,8 @@ function Invoke-CippTestZTNA21992 { + <# + .SYNOPSIS + Application certificates must be rotated on a regular basis + #> param($Tenant) try { diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA22128.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA22128.ps1 index 6196ed7aed3e..a5dc82d83502 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA22128.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA22128.ps1 @@ -1,4 +1,8 @@ function Invoke-CippTestZTNA22128 { + <# + .SYNOPSIS + Guests are not assigned high privileged directory roles + #> param($Tenant) #Tested try { diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24572.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24572.ps1 index 56b2b9906004..4fc9b3dda0e3 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24572.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24572.ps1 @@ -1,4 +1,8 @@ function Invoke-CippTestZTNA24572 { + <# + .SYNOPSIS + Device enrollment notifications are enforced to ensure user awareness and secure onboarding + #> param($Tenant) $TestId = 'ZTNA24572' From adf2d8f631bc8438b1e3a2477cacf0787b580a9c Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Sat, 3 Jan 2026 00:28:58 +0100 Subject: [PATCH 077/503] skipped message no data --- .../Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24540.ps1 | 2 +- .../Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24541.ps1 | 2 +- .../Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24542.ps1 | 2 +- .../Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24543.ps1 | 2 +- .../Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24545.ps1 | 2 +- .../Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24547.ps1 | 2 +- .../Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24548.ps1 | 2 +- .../Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24549.ps1 | 2 +- .../Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24550.ps1 | 2 +- .../Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24552.ps1 | 2 +- .../Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24553.ps1 | 2 +- .../Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24560.ps1 | 2 +- .../Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24564.ps1 | 2 +- .../Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24568.ps1 | 2 +- .../Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24569.ps1 | 2 +- .../Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24574.ps1 | 2 +- .../Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24575.ps1 | 2 +- .../Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24576.ps1 | 2 +- .../Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24784.ps1 | 2 +- .../Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24839.ps1 | 2 +- .../Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24840.ps1 | 2 +- .../Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24870.ps1 | 2 +- .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21776.ps1 | 2 +- .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21780.ps1 | 2 +- 24 files changed, 24 insertions(+), 24 deletions(-) diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24540.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24540.ps1 index 6d5a0779280f..40e130efdae4 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24540.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24540.ps1 @@ -8,7 +8,7 @@ function Invoke-CippTestZTNA24540 { try { $ConfigurationPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'IntuneConfigurationPolicies' if (-not $ConfigurationPolicies) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA24540' -TestType 'Devices' -Status 'Investigate' -ResultMarkdown 'Intune configuration policies not found in database' -Risk 'High' -Name 'Windows Firewall policies protect against unauthorized network access' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Device' + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA24540' -TestType 'Devices' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'Windows Firewall policies protect against unauthorized network access' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Device' return } diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24541.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24541.ps1 index 0fdfbeabcd90..acf03ea73a68 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24541.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24541.ps1 @@ -11,7 +11,7 @@ function Invoke-CippTestZTNA24541 { $IntunePolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'IntuneDeviceCompliancePolicies' if (-not $IntunePolicies) { - Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Devices' -Status 'Investigate' -ResultMarkdown 'Intune policies not found in database' -Risk 'High' -Name 'Compliance policies protect Windows devices' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Tenant' + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Devices' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'Compliance policies protect Windows devices' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Tenant' return } diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24542.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24542.ps1 index 3007668bbd2c..81215ce30efa 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24542.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24542.ps1 @@ -11,7 +11,7 @@ function Invoke-CippTestZTNA24542 { $IntunePolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'IntuneDeviceCompliancePolicies' if (-not $IntunePolicies) { - Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Devices' -Status 'Investigate' -ResultMarkdown 'Intune policies not found in database' -Risk 'High' -Name 'Compliance policies protect macOS devices' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Tenant' + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Devices' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'Compliance policies protect macOS devices' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Tenant' return } diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24543.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24543.ps1 index 34609066d498..3130d6e531fa 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24543.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24543.ps1 @@ -12,7 +12,7 @@ function Invoke-CippTestZTNA24543 { $IntunePolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'IntuneDeviceCompliancePolicies' if (-not $IntunePolicies) { - Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Devices' -Status 'Investigate' -ResultMarkdown 'Intune policies not found in database' -Risk 'High' -Name 'Compliance policies protect iOS/iPadOS devices' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Tenant' + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Devices' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'Compliance policies protect iOS/iPadOS devices' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Tenant' return } diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24545.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24545.ps1 index 4bd08c9e4c0f..c40f080a415b 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24545.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24545.ps1 @@ -12,7 +12,7 @@ function Invoke-CippTestZTNA24545 { $IntunePolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'IntuneDeviceCompliancePolicies' if (-not $IntunePolicies) { - Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Devices' -Status 'Investigate' -ResultMarkdown 'Intune policies not found in database' -Risk 'High' -Name 'Compliance policies protect fully managed and corporate-owned Android devices' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Tenant' + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Devices' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'Compliance policies protect fully managed and corporate-owned Android devices' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Tenant' return } diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24547.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24547.ps1 index fa0276e25538..e1ede894c0c1 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24547.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24547.ps1 @@ -12,7 +12,7 @@ function Invoke-CippTestZTNA24547 { $IntunePolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'IntuneDeviceCompliancePolicies' if (-not $IntunePolicies) { - Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Devices' -Status 'Investigate' -ResultMarkdown 'Intune policies not found in database' -Risk 'High' -Name 'Compliance policies protect personally owned Android devices' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Tenant' + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Devices' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'Compliance policies protect personally owned Android devices' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Tenant' return } diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24548.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24548.ps1 index 6a17dd5fce14..35d55fc52c15 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24548.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24548.ps1 @@ -12,7 +12,7 @@ function Invoke-CippTestZTNA24548 { $IosPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'IntuneIosAppProtectionPolicies' if (-not $IosPolicies) { - Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Devices' -Status 'Investigate' -ResultMarkdown 'iOS app protection policies not found in database' -Risk 'High' -Name 'Data on iOS/iPadOS is protected by app protection policies' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Tenant' + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Devices' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'Data on iOS/iPadOS is protected by app protection policies' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Tenant' return } diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24549.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24549.ps1 index 1f64dd363403..93ec5e0072d1 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24549.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24549.ps1 @@ -12,7 +12,7 @@ function Invoke-CippTestZTNA24549 { $AndroidPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'IntuneAndroidAppProtectionPolicies' if (-not $AndroidPolicies) { - Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Devices' -Status 'Investigate' -ResultMarkdown 'Android app protection policies not found in database' -Risk 'High' -Name 'Data on Android is protected by app protection policies' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Tenant' + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Devices' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'Data on Android is protected by app protection policies' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Tenant' return } diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24550.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24550.ps1 index 25c21ea87021..46c285740cb6 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24550.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24550.ps1 @@ -9,7 +9,7 @@ function Invoke-CippTestZTNA24550 { try { $ConfigurationPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'IntuneConfigurationPolicies' if (-not $ConfigurationPolicies) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA24550' -TestType 'Devices' -Status 'Investigate' -ResultMarkdown 'Intune configuration policies not found in database' -Risk 'High' -Name 'Data on Windows is protected by BitLocker encryption' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Device' + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA24550' -TestType 'Devices' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'Data on Windows is protected by BitLocker encryption' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Device' return } diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24552.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24552.ps1 index 2538953695e4..2d87afc2142b 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24552.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24552.ps1 @@ -9,7 +9,7 @@ function Invoke-CippTestZTNA24552 { try { $ConfigurationPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'IntuneConfigurationPolicies' if (-not $ConfigurationPolicies) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA24552' -TestType 'Devices' -Status 'Investigate' -ResultMarkdown 'Intune configuration policies not found in database' -Risk 'High' -Name 'Data on macOS is protected by firewall' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Device' + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA24552' -TestType 'Devices' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'Data on macOS is protected by firewall' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Device' return } diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24553.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24553.ps1 index 59e9c6fa7656..befe29a27e39 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24553.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24553.ps1 @@ -12,7 +12,7 @@ function Invoke-CippTestZTNA24553 { $IntunePolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'IntuneDeviceCompliancePolicies' if (-not $IntunePolicies) { - Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Devices' -Status 'Investigate' -ResultMarkdown 'Intune policies not found in database' -Risk 'High' -Name 'Windows Update policies are enforced to reduce risk from unpatched vulnerabilities' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Tenant' + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Devices' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'Windows Update policies are enforced to reduce risk from unpatched vulnerabilities' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Tenant' return } diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24560.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24560.ps1 index 474d5e0ea143..0df6b175853d 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24560.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24560.ps1 @@ -9,7 +9,7 @@ function Invoke-CippTestZTNA24560 { try { $ConfigPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'IntuneConfigurationPolicies' if (-not $ConfigPolicies) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA24560' -TestType 'Devices' -Status 'Investigate' -ResultMarkdown 'Configuration policy data not found in database' -Risk 'High' -Name 'Local administrator credentials on Windows are protected by Windows LAPS' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Device' + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA24560' -TestType 'Devices' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'Local administrator credentials on Windows are protected by Windows LAPS' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Device' return } diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24564.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24564.ps1 index 7e1cf0930194..8d969033aab5 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24564.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24564.ps1 @@ -9,7 +9,7 @@ function Invoke-CippTestZTNA24564 { try { $ConfigPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'IntuneConfigurationPolicies' if (-not $ConfigPolicies) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA24564' -TestType 'Devices' -Status 'Investigate' -ResultMarkdown 'Configuration policy data not found in database' -Risk 'High' -Name 'Local account usage on Windows is restricted to reduce unauthorized access' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Device' + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA24564' -TestType 'Devices' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'Local account usage on Windows is restricted to reduce unauthorized access' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Device' return } diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24568.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24568.ps1 index 847153b271f4..7f2b58180c5b 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24568.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24568.ps1 @@ -9,7 +9,7 @@ function Invoke-CippTestZTNA24568 { try { $ConfigPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'IntuneConfigurationPolicies' if (-not $ConfigPolicies) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA24568' -TestType 'Devices' -Status 'Investigate' -ResultMarkdown 'Configuration policy data not found in database' -Risk 'Medium' -Name 'Platform SSO is configured to strengthen authentication on macOS devices' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Tenant' + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA24568' -TestType 'Devices' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'Platform SSO is configured to strengthen authentication on macOS devices' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Tenant' return } diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24569.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24569.ps1 index 9160e651165c..821983deda18 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24569.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24569.ps1 @@ -12,7 +12,7 @@ function Invoke-CippTestZTNA24569 { $DeviceConfigs = New-CIPPDbRequest -TenantFilter $Tenant -Type 'IntuneDeviceConfigurations' if (-not $DeviceConfigs) { - Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Devices' -Status 'Investigate' -ResultMarkdown 'Device configurations not found in database' -Risk 'High' -Name 'FileVault encryption protects data on macOS devices' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Device' + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Devices' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'FileVault encryption protects data on macOS devices' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Device' return } diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24574.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24574.ps1 index 53a148070a39..50a4ff1efbf2 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24574.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24574.ps1 @@ -9,7 +9,7 @@ function Invoke-CippTestZTNA24574 { try { $ConfigPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'IntuneConfigurationPolicies' if (-not $ConfigPolicies) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA24574' -TestType 'Devices' -Status 'Investigate' -ResultMarkdown 'Configuration policy data not found in database' -Risk 'High' -Name 'Attack Surface Reduction rules are applied to Windows devices to prevent exploitation of vulnerable system components' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Device' + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA24574' -TestType 'Devices' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'Attack Surface Reduction rules are applied to Windows devices to prevent exploitation of vulnerable system components' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Device' return } diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24575.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24575.ps1 index 06b2d03b151c..a86aafd31fb9 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24575.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24575.ps1 @@ -9,7 +9,7 @@ function Invoke-CippTestZTNA24575 { try { $ConfigPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'IntuneConfigurationPolicies' if (-not $ConfigPolicies) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA24575' -TestType 'Devices' -Status 'Investigate' -ResultMarkdown 'Configuration policy data not found in database' -Risk 'High' -Name 'Defender Antivirus policies protect Windows devices from malware' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Device' + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA24575' -TestType 'Devices' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'Defender Antivirus policies protect Windows devices from malware' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Device' return } diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24576.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24576.ps1 index 5eea31149f19..eac9abcb2266 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24576.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24576.ps1 @@ -12,7 +12,7 @@ function Invoke-CippTestZTNA24576 { $DeviceConfigs = New-CIPPDbRequest -TenantFilter $Tenant -Type 'IntuneDeviceConfigurations' if (-not $DeviceConfigs) { - Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Devices' -Status 'Investigate' -ResultMarkdown 'Device configurations not found in database' -Risk 'Low' -Name 'Endpoint Analytics is enabled to help identify risks on Windows devices' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Tenant' + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Devices' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Low' -Name 'Endpoint Analytics is enabled to help identify risks on Windows devices' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Tenant' return } diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24784.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24784.ps1 index ef50fd2f69ba..75a6b173b5ae 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24784.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24784.ps1 @@ -9,7 +9,7 @@ function Invoke-CippTestZTNA24784 { try { $ConfigPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'IntuneConfigurationPolicies' if (-not $ConfigPolicies) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA24784' -TestType 'Devices' -Status 'Investigate' -ResultMarkdown 'Configuration policy data not found in database' -Risk 'High' -Name 'Defender Antivirus policies protect macOS devices from malware' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Device' + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA24784' -TestType 'Devices' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'Defender Antivirus policies protect macOS devices from malware' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Device' return } diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24839.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24839.ps1 index 8670f6c80c06..0ea86eb3dcef 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24839.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24839.ps1 @@ -12,7 +12,7 @@ function Invoke-CippTestZTNA24839 { $DeviceConfigs = New-CIPPDbRequest -TenantFilter $Tenant -Type 'IntuneDeviceConfigurations' if (-not $DeviceConfigs) { - Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Devices' -Status 'Investigate' -ResultMarkdown 'Device configurations not found in database' -Risk 'High' -Name 'Secure Wi-Fi profiles protect iOS devices from unauthorized network access' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Data' + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Devices' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'Secure Wi-Fi profiles protect iOS devices from unauthorized network access' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Data' return } diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24840.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24840.ps1 index 368dc561c37c..1468e4345741 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24840.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24840.ps1 @@ -13,7 +13,7 @@ function Invoke-CippTestZTNA24840 { $DeviceConfigs = New-CIPPDbRequest -TenantFilter $Tenant -Type 'IntuneDeviceConfigurations' if (-not $DeviceConfigs) { - Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Devices' -Status 'Investigate' -ResultMarkdown 'Device configurations not found in database' -Risk 'High' -Name 'Secure Wi-Fi profiles protect Android devices from unauthorized network access' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Data' + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Devices' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'Secure Wi-Fi profiles protect Android devices from unauthorized network access' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Data' return } diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24870.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24870.ps1 index 54b077462543..5e2183226768 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24870.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Devices/Invoke-CippTestZTNA24870.ps1 @@ -12,7 +12,7 @@ function Invoke-CippTestZTNA24870 { $DeviceConfigs = New-CIPPDbRequest -TenantFilter $Tenant -Type 'IntuneDeviceConfigurations' if (-not $DeviceConfigs) { - Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Devices' -Status 'Investigate' -ResultMarkdown 'Device configurations not found in database' -Risk 'High' -Name 'Secure Wi-Fi profiles protect macOS devices from unauthorized network access' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Data' + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Devices' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'Secure Wi-Fi profiles protect macOS devices from unauthorized network access' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Data' return } diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21776.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21776.ps1 index 145aba1e2113..88752ce98bd3 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21776.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21776.ps1 @@ -8,7 +8,7 @@ function Invoke-CippTestZTNA21776 { try { $AuthPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthorizationPolicy' if (-not $AuthPolicy) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21776' -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Authorization policy not found in database' -Risk 'High' -Name 'User consent settings are restricted' -UserImpact 'High' -ImplementationEffort 'Medium' -Category 'Application Management' + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21776' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'User consent settings are restricted' -UserImpact 'High' -ImplementationEffort 'Medium' -Category 'Application Management' return } diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21780.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21780.ps1 index b66a67e8f0d5..cb4567b0b95f 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21780.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21780.ps1 @@ -9,7 +9,7 @@ function Invoke-CippTestZTNA21780 { $Recommendations = New-CIPPDbRequest -TenantFilter $Tenant -Type 'DirectoryRecommendations' if (-not $Recommendations) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21780' -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Directory recommendations not found in database' -Risk 'Medium' -Name 'No usage of ADAL in the tenant' -UserImpact 'Low' -ImplementationEffort 'High' -Category 'Application Management' + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21780' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'No usage of ADAL in the tenant' -UserImpact 'Low' -ImplementationEffort 'High' -Category 'Application Management' return } From a1ce380e59c2aba25ea9cccad38b2c2923d8f13c Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Sat, 3 Jan 2026 00:38:32 +0100 Subject: [PATCH 078/503] skipped when data is missing --- .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21772.ps1 | 2 +- .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21773.ps1 | 2 +- .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21774.ps1 | 2 +- .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21783.ps1 | 2 +- .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21784.ps1 | 2 +- .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21786.ps1 | 2 +- .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21787.ps1 | 2 +- .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21790.ps1 | 2 +- .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21791.ps1 | 2 +- .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21792.ps1 | 2 +- .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21793.ps1 | 2 +- .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21796.ps1 | 2 +- .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21797.ps1 | 2 +- .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21799.ps1 | 2 +- .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21802.ps1 | 2 +- .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21803.ps1 | 2 +- .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21804.ps1 | 2 +- .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21806.ps1 | 2 +- .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21807.ps1 | 2 +- .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21808.ps1 | 2 +- .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21809.ps1 | 2 +- .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21810.ps1 | 2 +- .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21811.ps1 | 2 +- .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21819.ps1 | 2 +- .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21820.ps1 | 2 +- .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21823.ps1 | 2 +- .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21824.ps1 | 2 +- .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21825.ps1 | 2 +- .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21828.ps1 | 2 +- .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21829.ps1 | 2 +- .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21830.ps1 | 2 +- .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21835.ps1 | 2 +- .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21836.ps1 | 2 +- .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21837.ps1 | 2 +- .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21838.ps1 | 4 ++-- .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21839.ps1 | 4 ++-- .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21840.ps1 | 4 ++-- .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21841.ps1 | 2 +- .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21842.ps1 | 2 +- .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21844.ps1 | 2 +- .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21845.ps1 | 2 +- .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21846.ps1 | 2 +- .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21847.ps1 | 4 ++-- .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21848.ps1 | 2 +- .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21858.ps1 | 2 +- .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21861.ps1 | 2 +- .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21862.ps1 | 2 +- .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21863.ps1 | 2 +- .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21865.ps1 | 2 +- .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21866.ps1 | 2 +- .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21868.ps1 | 2 +- .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21869.ps1 | 2 +- .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21872.ps1 | 4 ++-- .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21874.ps1 | 2 +- .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21877.ps1 | 2 +- .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21886.ps1 | 2 +- .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21896.ps1 | 2 +- .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21964.ps1 | 2 +- .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21992.ps1 | 2 +- .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA22128.ps1 | 2 +- .../Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24572.ps1 | 2 +- 61 files changed, 66 insertions(+), 66 deletions(-) diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21772.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21772.ps1 index 84ab88e00648..8f874c1799bf 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21772.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21772.ps1 @@ -10,7 +10,7 @@ function Invoke-CippTestZTNA21772 { $ServicePrincipals = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ServicePrincipals' if (-not $Apps -and -not $ServicePrincipals) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21772' -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Applications and service principals not found in database' -Risk 'High' -Name 'Applications do not have client secrets configured' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Application Management' + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21772' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'Applications do not have client secrets configured' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Application Management' return } diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21773.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21773.ps1 index f6d8885d7ff9..6ce664830428 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21773.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21773.ps1 @@ -10,7 +10,7 @@ function Invoke-CippTestZTNA21773 { $ServicePrincipals = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ServicePrincipals' if (-not $Apps -and -not $ServicePrincipals) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21773' -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Applications and service principals not found in database' -Risk 'Medium' -Name 'Applications do not have certificates with expiration longer than 180 days' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Application Management' + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21773' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'Applications do not have certificates with expiration longer than 180 days' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Application Management' return } diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21774.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21774.ps1 index f4705649b532..659e9c4656b7 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21774.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21774.ps1 @@ -9,7 +9,7 @@ function Invoke-CippTestZTNA21774 { $ServicePrincipals = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ServicePrincipals' #tested if (-not $ServicePrincipals) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21774' -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Service principals not found in database' -Risk 'High' -Name 'Microsoft services applications do not have credentials configured' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Application Management' + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21774' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'Microsoft services applications do not have credentials configured' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Application Management' return } diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21783.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21783.ps1 index ba66d83a030b..1749977c6dee 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21783.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21783.ps1 @@ -10,7 +10,7 @@ function Invoke-CippTestZTNA21783 { $Roles = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Roles' if (-not $CAPolicies -or -not $Roles) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21783' -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Conditional Access policies or roles not found in database' -Risk 'High' -Name 'Privileged Microsoft Entra built-in roles are targeted with Conditional Access policies to enforce phishing-resistant methods' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Access Control' + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21783' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'Privileged Microsoft Entra built-in roles are targeted with Conditional Access policies to enforce phishing-resistant methods' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Access Control' return } diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21784.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21784.ps1 index dfdd58c1b0d5..3df5ac94ee3b 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21784.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21784.ps1 @@ -9,7 +9,7 @@ function Invoke-CippTestZTNA21784 { $CAPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ConditionalAccessPolicies' if (-not $CAPolicies) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21784' -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Conditional Access policies not found in database' -Risk 'Medium' -Name 'All user sign in activity uses phishing-resistant authentication methods' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Access Control' + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21784' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'All user sign in activity uses phishing-resistant authentication methods' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Access Control' return } diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21786.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21786.ps1 index c4a2ba5e6ba5..f44d613a07e4 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21786.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21786.ps1 @@ -9,7 +9,7 @@ function Invoke-CippTestZTNA21786 { $CAPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ConditionalAccessPolicies' if (-not $CAPolicies) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21786' -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Conditional Access policies not found in database' -Risk 'High' -Name 'User sign-in activity uses token protection' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Access Control' + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21786' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'User sign-in activity uses token protection' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Access Control' return } diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21787.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21787.ps1 index d42327c88ab3..0e0a9223dc11 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21787.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21787.ps1 @@ -9,7 +9,7 @@ function Invoke-CippTestZTNA21787 { $AuthPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthorizationPolicy' if (-not $AuthPolicy) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21787' -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Authorization policy not found in database' -Risk 'High' -Name 'Permissions to create new tenants are limited to the Tenant Creator role' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Privileged Access' + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21787' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'Permissions to create new tenants are limited to the Tenant Creator role' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Privileged Access' return } diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21790.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21790.ps1 index 8b5a4447e647..d9aa7a95f316 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21790.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21790.ps1 @@ -9,7 +9,7 @@ function Invoke-CippTestZTNA21790 { $CrossTenantPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'CrossTenantAccessPolicy' if (-not $CrossTenantPolicy) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21790' -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Cross-tenant access policy not found in database' -Risk 'High' -Name 'Outbound cross-tenant access settings are configured' -UserImpact 'Medium' -ImplementationEffort 'High' -Category 'Application Management' + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21790' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'Outbound cross-tenant access settings are configured' -UserImpact 'Medium' -ImplementationEffort 'High' -Category 'Application Management' return } diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21791.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21791.ps1 index 4144c20f7c60..870346bf350b 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21791.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21791.ps1 @@ -9,7 +9,7 @@ function Invoke-CippTestZTNA21791 { $AuthPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthorizationPolicy' if (-not $AuthPolicy) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21791' -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Authorization policy not found in database' -Risk 'Medium' -Name 'Guests cannot invite other guests' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'External Collaboration' + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21791' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'Guests cannot invite other guests' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'External Collaboration' return } diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21792.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21792.ps1 index 9ebd3434fc68..037e94fe2a3b 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21792.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21792.ps1 @@ -9,7 +9,7 @@ function Invoke-CippTestZTNA21792 { $AuthPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthorizationPolicy' if (-not $AuthPolicy) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21792' -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Authorization policy not found in database' -Risk 'Medium' -Name 'Guests have restricted access to directory objects' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'External Collaboration' + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21792' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'Guests have restricted access to directory objects' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'External Collaboration' return } diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21793.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21793.ps1 index 7d2371837330..3c21654fbc66 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21793.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21793.ps1 @@ -9,7 +9,7 @@ function Invoke-CippTestZTNA21793 { $CrossTenantPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'CrossTenantAccessPolicy' if (-not $CrossTenantPolicy) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21793' -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Cross-tenant access policy not found in database' -Risk 'High' -Name 'Tenant restrictions v2 policy is configured' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Application Management' + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21793' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'Tenant restrictions v2 policy is configured' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Application Management' return } diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21796.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21796.ps1 index b6efc7edfeb3..88695f31b7c4 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21796.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21796.ps1 @@ -9,7 +9,7 @@ function Invoke-CippTestZTNA21796 { $CAPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ConditionalAccessPolicies' if (-not $CAPolicies) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21796' -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Conditional Access policies not found in database' -Risk 'Medium' -Name 'Block legacy authentication policy is configured' -UserImpact 'High' -ImplementationEffort 'Low' -Category 'Access Control' + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21796' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'Block legacy authentication policy is configured' -UserImpact 'High' -ImplementationEffort 'Low' -Category 'Access Control' return } diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21797.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21797.ps1 index e743b8903290..b35dd6b651c5 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21797.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21797.ps1 @@ -10,7 +10,7 @@ function Invoke-CippTestZTNA21797 { $authMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' if (-not $allCAPolicies -or -not $authMethodsPolicy) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21797' -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Required policies not found in database' -Risk 'High' -Name 'Restrict access to high risk users' -UserImpact 'High' -ImplementationEffort 'Medium' -Category 'Conditional Access' + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21797' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'Restrict access to high risk users' -UserImpact 'High' -ImplementationEffort 'Medium' -Category 'Conditional Access' return } diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21799.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21799.ps1 index d54ea0ebe666..0fd5396bd764 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21799.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21799.ps1 @@ -10,7 +10,7 @@ function Invoke-CippTestZTNA21799 { $allCAPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ConditionalAccessPolicies' if (-not $allCAPolicies -or -not $authMethodPolicy) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21799' -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Required policies not found in database' -Risk 'High' -Name 'Restrict high risk sign-ins' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Conditional Access' + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21799' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'Restrict high risk sign-ins' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Conditional Access' return } diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21802.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21802.ps1 index 08aa35895dde..24956ef8276c 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21802.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21802.ps1 @@ -9,7 +9,7 @@ function Invoke-CippTestZTNA21802 { $AuthMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' if (-not $AuthMethodsPolicy) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21802' -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Authentication methods policy not found in database' -Risk 'Medium' -Name 'Microsoft Authenticator app shows sign-in context' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Access Control' + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21802' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'Microsoft Authenticator app shows sign-in context' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Access Control' return } diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21803.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21803.ps1 index 3136648578e3..38cfc0f542a8 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21803.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21803.ps1 @@ -9,7 +9,7 @@ function Invoke-CippTestZTNA21803 { $AuthMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' if (-not $AuthMethodsPolicy) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21803' -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Authentication methods policy not found in database' -Risk 'Medium' -Name 'Migrate from legacy MFA and SSPR policies' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Credential Management' + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21803' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'Migrate from legacy MFA and SSPR policies' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Credential Management' return } diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21804.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21804.ps1 index 54b956583cce..9882b9e0713c 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21804.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21804.ps1 @@ -9,7 +9,7 @@ function Invoke-CippTestZTNA21804 { $authMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' if (-not $authMethodsPolicy) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21804' -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Authentication methods policy not found in database' -Risk 'High' -Name 'SMS and Voice Call authentication methods are disabled' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Credential Management' + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21804' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'SMS and Voice Call authentication methods are disabled' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Credential Management' return } diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21806.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21806.ps1 index ef7719563dbb..6a7864fb17c8 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21806.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21806.ps1 @@ -9,7 +9,7 @@ function Invoke-CippTestZTNA21806 { $allCAPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ConditionalAccessPolicies' if (-not $allCAPolicies) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21806' -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Conditional Access policies not found in database' -Risk 'High' -Name 'Secure the MFA registration (My Security Info) page' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Conditional Access' + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21806' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'Secure the MFA registration (My Security Info) page' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Conditional Access' return } diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21807.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21807.ps1 index 8afc5ac006c5..2e5f195f5c94 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21807.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21807.ps1 @@ -9,7 +9,7 @@ function Invoke-CippTestZTNA21807 { $AuthPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthorizationPolicy' if (-not $AuthPolicy) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21807' -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Authorization policy not found in database' -Risk 'Medium' -Name 'Creating new applications and service principals is restricted to privileged users' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Application Management' + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21807' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'Creating new applications and service principals is restricted to privileged users' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Application Management' return } diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21808.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21808.ps1 index 1832ece21887..289ae9e1f36d 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21808.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21808.ps1 @@ -8,7 +8,7 @@ function Invoke-CippTestZTNA21808 { try { $CAPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ConditionalAccessPolicies' if (-not $CAPolicies) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21808' -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Conditional Access policies not found in database' -Risk 'High' -Name 'Restrict device code flow' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Access Control' + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21808' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'Restrict device code flow' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Access Control' return } diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21809.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21809.ps1 index bd14f97cbc60..f3422476b53c 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21809.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21809.ps1 @@ -9,7 +9,7 @@ function Invoke-CippTestZTNA21809 { $result = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AdminConsentRequestPolicy' if (-not $result) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21809' -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Admin consent request policy not found in database' -Risk 'High' -Name 'Admin consent workflow is enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Application Management' + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21809' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'Admin consent workflow is enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Application Management' return } diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21810.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21810.ps1 index c88c54bf41cb..3f7d1d03f123 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21810.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21810.ps1 @@ -9,7 +9,7 @@ function Invoke-CippTestZTNA21810 { $authPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthorizationPolicy' if (-not $authPolicy) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21810' -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Authorization policy not found in database' -Risk 'Medium' -Name 'Resource-specific consent is restricted' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Application Management' + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21810' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'Resource-specific consent is restricted' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Application Management' return } diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21811.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21811.ps1 index 898baadc8614..6315dc929433 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21811.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21811.ps1 @@ -9,7 +9,7 @@ function Invoke-CippTestZTNA21811 { $domains = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Domains' if (-not $domains) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21811' -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Domains not found in database' -Risk 'Medium' -Name 'Password expiration is disabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Credential Management' + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21811' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'Password expiration is disabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Credential Management' return } diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21819.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21819.ps1 index 647b8707a78a..8fe1afbea01c 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21819.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21819.ps1 @@ -13,7 +13,7 @@ function Invoke-CippTestZTNA21819 { $GlobalAdminRole = $Roles | Where-Object { $_.roleTemplateId -eq '62e90394-69f5-4237-9190-012177145e10' } if (-not $GlobalAdminRole) { - Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Could not find the Global Administrator role definition.' -Risk 'Low' -Name 'Activation alert for Global Administrator role assignment' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Privileged access' + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Low' -Name 'Activation alert for Global Administrator role assignment' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Privileged access' return } diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21820.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21820.ps1 index 845dfc5560d3..88b626c023dd 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21820.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21820.ps1 @@ -12,7 +12,7 @@ function Invoke-CippTestZTNA21820 { $PrivilegedRoles = Get-CippDbRole -TenantFilter $Tenant -IncludePrivilegedRoles if (-not $PrivilegedRoles -or $PrivilegedRoles.Count -eq 0) { - Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'No privileged roles found in database' -Risk 'Low' -Name 'Activation alert for all privileged role assignments' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Privileged access' + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Low' -Name 'Activation alert for all privileged role assignments' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Privileged access' return } diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21823.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21823.ps1 index 2001fd0d0a9b..24ddb005d5bb 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21823.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21823.ps1 @@ -12,7 +12,7 @@ function Invoke-CippTestZTNA21823 { $AuthFlowPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationFlowsPolicy' if (-not $AuthFlowPolicy) { - Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Authentication flows policy not found' -Risk 'Medium' -Name 'Guest self-service sign-up via user flow is disabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'External collaboration' + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'Guest self-service sign-up via user flow is disabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'External collaboration' return } diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21824.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21824.ps1 index 402ccc64c613..a6993c280c02 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21824.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21824.ps1 @@ -9,7 +9,7 @@ function Invoke-CippTestZTNA21824 { $allCAPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ConditionalAccessPolicies' if (-not $allCAPolicies) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21824' -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Conditional Access policies not found in database' -Risk 'Medium' -Name "Guests don't have long lived sign-in sessions" -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Conditional Access' + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21824' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name "Guests don't have long lived sign-in sessions" -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Conditional Access' return } diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21825.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21825.ps1 index 05865853b6bc..2ad49609f454 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21825.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21825.ps1 @@ -12,7 +12,7 @@ function Invoke-CippTestZTNA21825 { $PrivilegedRoles = Get-CippDbRole -TenantFilter $Tenant -IncludePrivilegedRoles if (-not $PrivilegedRoles -or $PrivilegedRoles.Count -eq 0) { - Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'No privileged roles found in database' -Risk 'Medium' -Name 'Privileged users have short-lived sign-in sessions' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Access control' + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'Privileged users have short-lived sign-in sessions' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Access control' return } diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21828.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21828.ps1 index 71826e660f64..8fa1c672570e 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21828.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21828.ps1 @@ -9,7 +9,7 @@ function Invoke-CippTestZTNA21828 { $allCAPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ConditionalAccessPolicies' if (-not $allCAPolicies) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21828' -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Conditional Access policies not found in database' -Risk 'High' -Name 'Authentication transfer is blocked' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Conditional Access' + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21828' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'Authentication transfer is blocked' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Conditional Access' return } diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21829.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21829.ps1 index 0cbf936ffe26..6dcd4437bd47 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21829.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21829.ps1 @@ -12,7 +12,7 @@ function Invoke-CippTestZTNA21829 { $Domains = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Domains' if (-not $Domains) { - Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Domains not found in database' -Risk 'High' -Name 'Use cloud authentication' -UserImpact 'High' -ImplementationEffort 'High' -Category 'Access control' + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'Use cloud authentication' -UserImpact 'High' -ImplementationEffort 'High' -Category 'Access control' return } diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21830.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21830.ps1 index 15c5f1976be4..8dd9f35942b8 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21830.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21830.ps1 @@ -16,7 +16,7 @@ function Invoke-CippTestZTNA21830 { $PrivilegedRoles = Get-CippDbRole -TenantFilter $Tenant -IncludePrivilegedRoles if (-not $PrivilegedRoles) { - Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'No privileged roles found in database' -Risk 'High' -Name 'Conditional Access policies for Privileged Access Workstations are configured' -UserImpact 'Low' -ImplementationEffort 'High' -Category 'Application management' + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'Conditional Access policies for Privileged Access Workstations are configured' -UserImpact 'Low' -ImplementationEffort 'High' -Category 'Application management' return } diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21835.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21835.ps1 index 55219b6eed5f..f9f7e021dd98 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21835.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21835.ps1 @@ -13,7 +13,7 @@ function Invoke-CippTestZTNA21835 { $GlobalAdminRole = $Roles | Where-Object { $_.roleTemplateId -eq '62e90394-69f5-4237-9190-012177145e10' } if (-not $GlobalAdminRole) { - Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Global Administrator role not found in database' -Risk 'High' -Name 'Emergency access accounts are configured appropriately' -UserImpact 'Low' -ImplementationEffort 'High' -Category 'Application management' + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'Emergency access accounts are configured appropriately' -UserImpact 'Low' -ImplementationEffort 'High' -Category 'Application management' return } diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21836.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21836.ps1 index a150ccda9371..e6129bd4a135 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21836.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21836.ps1 @@ -12,7 +12,7 @@ function Invoke-CippTestZTNA21836 { $PrivilegedRoles = Get-CippDbRole -TenantFilter $Tenant -IncludePrivilegedRoles if (-not $PrivilegedRoles) { - Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'No privileged roles found in database' -Risk 'High' -Name 'Workload Identities are not assigned privileged roles' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Application management' + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'Workload Identities are not assigned privileged roles' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Application management' return } diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21837.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21837.ps1 index cfb87a22c04b..e42e48ac94d2 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21837.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21837.ps1 @@ -12,7 +12,7 @@ function Invoke-CippTestZTNA21837 { $DeviceSettings = New-CIPPDbRequest -TenantFilter $Tenant -Type 'DeviceRegistrationPolicy' if (-not $DeviceSettings) { - Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Device settings not found in database' -Risk 'High' -Name 'Limit the maximum number of devices per user to 10' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Devices' + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'Limit the maximum number of devices per user to 10' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Devices' return } diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21838.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21838.ps1 index cd9d1c3d9e33..87bc89f02394 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21838.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21838.ps1 @@ -12,14 +12,14 @@ function Invoke-CippTestZTNA21838 { $AuthMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' if (-not $AuthMethodsPolicy) { - Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Authentication methods policy not found in database' -Risk 'High' -Name 'Security key authentication method enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Access control' + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'Security key authentication method enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Access control' return } $Fido2Config = $AuthMethodsPolicy.authenticationMethodConfigurations | Where-Object { $_.id -eq 'Fido2' } if (-not $Fido2Config) { - Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'FIDO2 authentication method configuration not found' -Risk 'High' -Name 'Security key authentication method enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Access control' + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'Security key authentication method enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Access control' return } diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21839.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21839.ps1 index 8a47e09669cb..743461e1ff9e 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21839.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21839.ps1 @@ -12,14 +12,14 @@ function Invoke-CippTestZTNA21839 { $AuthMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' if (-not $AuthMethodsPolicy) { - Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Authentication methods policy not found in database' -Risk 'High' -Name 'Passkey authentication method enabled' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Credential management' + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'Passkey authentication method enabled' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Credential management' return } $Fido2Config = $AuthMethodsPolicy.authenticationMethodConfigurations | Where-Object { $_.id -eq 'Fido2' } if (-not $Fido2Config) { - Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'FIDO2 authentication method configuration not found' -Risk 'High' -Name 'Passkey authentication method enabled' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Credential management' + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'Passkey authentication method enabled' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Credential management' return } diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21840.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21840.ps1 index 59cf3bf89426..9c007bd3b1f4 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21840.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21840.ps1 @@ -12,14 +12,14 @@ function Invoke-CippTestZTNA21840 { $AuthMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' if (-not $AuthMethodsPolicy) { - Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Authentication methods policy not found in database' -Risk 'High' -Name 'Security key attestation is enforced' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Credential management' + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'Security key attestation is enforced' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Credential management' return } $Fido2Config = $AuthMethodsPolicy.authenticationMethodConfigurations | Where-Object { $_.id -eq 'Fido2' } if (-not $Fido2Config) { - Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'FIDO2 authentication method configuration not found' -Risk 'High' -Name 'Security key attestation is enforced' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Credential management' + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'Security key attestation is enforced' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Credential management' return } diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21841.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21841.ps1 index 5b7cd3c273da..4f17c2b6d6c2 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21841.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21841.ps1 @@ -12,7 +12,7 @@ function Invoke-CippTestZTNA21841 { $AuthMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' if (-not $AuthMethodsPolicy) { - Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Authentication methods policy not found' -Risk 'Medium' -Name 'Microsoft Authenticator app report suspicious activity setting is enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Credential management' + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'Microsoft Authenticator app report suspicious activity setting is enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Credential management' return } diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21842.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21842.ps1 index 94f63825a6dd..1f2629709fec 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21842.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21842.ps1 @@ -12,7 +12,7 @@ function Invoke-CippTestZTNA21842 { $AuthorizationPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthorizationPolicy' if (-not $AuthorizationPolicy) { - Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Authorization policy not found in database' -Risk 'High' -Name 'Block administrators from using SSPR' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Credential management' + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'Block administrators from using SSPR' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Credential management' return } diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21844.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21844.ps1 index e35322790b18..e0ddd6289188 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21844.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21844.ps1 @@ -69,7 +69,7 @@ function Invoke-CippTestZTNA21844 { $ResultMarkdown = $SummaryLines -join "`n" if ($InvestigateStatus) { - Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Investigate' -ResultMarkdown $ResultMarkdown -Risk 'Medium' -Name 'Block legacy Azure AD PowerShell module' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Access control' + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'Block legacy Azure AD PowerShell module' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Access control' } else { Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status $Passed -ResultMarkdown $ResultMarkdown -Risk 'Medium' -Name 'Block legacy Azure AD PowerShell module' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Access control' } diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21845.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21845.ps1 index 9c8a591dbda5..b8d816cc2c73 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21845.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21845.ps1 @@ -13,7 +13,7 @@ function Invoke-CippTestZTNA21845 { $TAPConfig = $AuthMethodsPolicy.authenticationMethodConfigurations | Where-Object { $_.id -eq 'TemporaryAccessPass' } if (-not $TAPConfig) { - Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Temporary Access Pass configuration not found' -Risk 'Medium' -Name 'Temporary access pass is enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Credential management' + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'Temporary access pass is enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Credential management' return } diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21846.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21846.ps1 index a6d113b9d6e0..7788f63b92cb 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21846.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21846.ps1 @@ -13,7 +13,7 @@ function Invoke-CippTestZTNA21846 { $TAPConfig = $AuthMethodsPolicy.authenticationMethodConfigurations | Where-Object { $_.id -eq 'TemporaryAccessPass' } if (-not $TAPConfig) { - Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Temporary Access Pass configuration not found' -Risk 'Medium' -Name 'Restrict Temporary Access Pass to Single Use' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Credential management' + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'Restrict Temporary Access Pass to Single Use' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Credential management' return } diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21847.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21847.ps1 index 332e75dd3908..89d23d50c6c1 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21847.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21847.ps1 @@ -12,7 +12,7 @@ function Invoke-CippTestZTNA21847 { $Settings = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Organization' if (-not $Settings) { - Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Organization details not found' -Risk 'High' -Name 'Password protection for on-premises is enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Credential management' + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'Password protection for on-premises is enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Credential management' return } @@ -25,7 +25,7 @@ function Invoke-CippTestZTNA21847 { # Note: Password protection settings require groupSettings API which is not cached # This test requires direct API access to check EnableBannedPasswordCheckOnPremises and BannedPasswordCheckOnPremisesMode - Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Password protection settings require API access not available in cache. Manual verification required.' -Risk 'High' -Name 'Password protection for on-premises is enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Credential management' + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'Password protection for on-premises is enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Credential management' return Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status $Passed -ResultMarkdown $ResultMarkdown -Risk 'High' -Name 'Password protection for on-premises is enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Credential management' diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21848.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21848.ps1 index f258c596f218..2622b1616cb2 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21848.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21848.ps1 @@ -13,7 +13,7 @@ function Invoke-CippTestZTNA21848 { $PasswordProtectionSettings = $Settings | Where-Object { $_.templateId -eq '5cf42378-d67d-4f36-ba46-e8b86229381d' } if (-not $PasswordProtectionSettings) { - Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Password protection settings not found' -Risk 'Medium' -Name 'Add organizational terms to the banned password list' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Credential management' + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'Add organizational terms to the banned password list' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Credential management' return } diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21858.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21858.ps1 index 3c1430225936..053e4f97af4a 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21858.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21858.ps1 @@ -8,7 +8,7 @@ function Invoke-CippTestZTNA21858 { try { $Guests = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Guests' if (-not $Guests) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21858' -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Guest user data not found in database' -Risk 'Medium' -Name 'Inactive guest identities are disabled or removed from the tenant' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'External collaboration' + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21858' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'Inactive guest identities are disabled or removed from the tenant' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'External collaboration' return } diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21861.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21861.ps1 index 80a1e3296ce9..539f2fc8d3db 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21861.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21861.ps1 @@ -12,7 +12,7 @@ function Invoke-CippTestZTNA21861 { $RiskyUsers = New-CIPPDbRequest -TenantFilter $Tenant -Type 'RiskyUsers' if (-not $RiskyUsers) { - Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Risky users data not found. This may indicate Identity Protection is not available (requires P2 licensing) or no risky users exist.' -Risk 'High' -Name 'All high-risk users are triaged' -UserImpact 'Low' -ImplementationEffort 'High' -Category 'Monitoring' + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'All high-risk users are triaged' -UserImpact 'Low' -ImplementationEffort 'High' -Category 'Monitoring' return } diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21862.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21862.ps1 index c0f407fb7e74..ed352e74e4fd 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21862.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21862.ps1 @@ -14,7 +14,7 @@ function Invoke-CippTestZTNA21862 { $UntriagedRiskDetections = $ServicePrincipalRiskDetections | Where-Object { $_.riskState -eq 'atRisk' } if (-not $UntriagedRiskyPrincipals -and -not $ServicePrincipalRiskDetections) { - Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Risky service principals data not found. This may indicate Workload Identity Protection is not available (requires Workload Identity Premium licensing) or no risky workload identities exist.' -Risk 'High' -Name 'All risky workload identities are triaged' -UserImpact 'Low' -ImplementationEffort 'High' -Category 'Monitoring' + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'All risky workload identities are triaged' -UserImpact 'Low' -ImplementationEffort 'High' -Category 'Monitoring' return } diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21863.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21863.ps1 index 004c4698b883..eb20d361776d 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21863.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21863.ps1 @@ -12,7 +12,7 @@ function Invoke-CippTestZTNA21863 { $RiskDetections = New-CIPPDbRequest -TenantFilter $Tenant -Type 'RiskDetections' if (-not $RiskDetections) { - Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Risk detections data not found. This may indicate Identity Protection is not available (requires P2 licensing) or no risk detections exist.' -Risk 'High' -Name 'All high-risk sign-ins are triaged' -UserImpact 'Low' -ImplementationEffort 'High' -Category 'Monitoring' + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'All high-risk sign-ins are triaged' -UserImpact 'Low' -ImplementationEffort 'High' -Category 'Monitoring' return } diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21865.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21865.ps1 index cb7c13a419c9..b250a508c639 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21865.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21865.ps1 @@ -11,7 +11,7 @@ function Invoke-CippTestZTNA21865 { $NamedLocations = New-CIPPDbRequest -TenantFilter $Tenant -Type 'NamedLocations' if (-not $NamedLocations) { - Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Named locations not found in database' -Risk 'Medium' -Name 'Named locations are configured' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Application management' + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'Named locations are configured' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Application management' return } diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21866.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21866.ps1 index b1fdc8d32ab1..754dacc9ff22 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21866.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21866.ps1 @@ -12,7 +12,7 @@ function Invoke-CippTestZTNA21866 { $Recommendations = New-CIPPDbRequest -TenantFilter $Tenant -Type 'DirectoryRecommendations' if (-not $Recommendations) { - Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Directory recommendations not found in cache' -Risk 'Medium' -Name 'All Microsoft Entra recommendations are addressed' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Monitoring' + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'All Microsoft Entra recommendations are addressed' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Monitoring' return } diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21868.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21868.ps1 index 87345f01e9e9..367b8a2a511f 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21868.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21868.ps1 @@ -11,7 +11,7 @@ function Invoke-CippTestZTNA21868 { $ServicePrincipals = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ServicePrincipals' if (-not $Guests -or -not $Apps -or -not $ServicePrincipals) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21868' -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Required data not found in database' -Risk 'Medium' -Name 'Guests do not own apps in the tenant' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'External collaboration' + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21868' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'Guests do not own apps in the tenant' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'External collaboration' return } diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21869.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21869.ps1 index 6dac00b5974a..dcaa09e51496 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21869.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21869.ps1 @@ -8,7 +8,7 @@ function Invoke-CippTestZTNA21869 { try { $ServicePrincipals = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ServicePrincipals' if (-not $ServicePrincipals) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21869' -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Service principal data not found in database' -Risk 'Medium' -Name 'Enterprise applications must require explicit assignment or scoped provisioning' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Application management' + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21869' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'Enterprise applications must require explicit assignment or scoped provisioning' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Application management' return } diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21872.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21872.ps1 index 128dc5579251..0ccf79cf8d6b 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21872.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21872.ps1 @@ -13,12 +13,12 @@ function Invoke-CippTestZTNA21872 { $DeviceRegistrationPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'DeviceRegistrationPolicy' if (-not $CAPolicies) { - Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Conditional Access policies not found in cache' -Risk 'High' -Name 'Require multifactor authentication for device join and device registration using user action' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Access control' + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'Require multifactor authentication for device join and device registration using user action' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Access control' return } if (-not $DeviceRegistrationPolicy) { - Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Device registration policy not found in cache' -Risk 'High' -Name 'Require multifactor authentication for device join and device registration using user action' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Access control' + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'Require multifactor authentication for device join and device registration using user action' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Access control' return } diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21874.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21874.ps1 index d50649a51137..3adfce12208b 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21874.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21874.ps1 @@ -12,7 +12,7 @@ function Invoke-CippTestZTNA21874 { $B2BManagementPolicyObject = New-CIPPDbRequest -TenantFilter $Tenant -Type 'B2BManagementPolicy' if (-not $B2BManagementPolicyObject) { - Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'B2B Management Policy not found in cache' -Risk 'Medium' -Name 'Guest access is limited to approved tenants' -UserImpact 'Medium' -ImplementationEffort 'High' -Category 'External collaboration' + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'Guest access is limited to approved tenants' -UserImpact 'Medium' -ImplementationEffort 'High' -Category 'External collaboration' return } diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21877.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21877.ps1 index 4fed8b3cca45..a2272933ab8d 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21877.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21877.ps1 @@ -8,7 +8,7 @@ function Invoke-CippTestZTNA21877 { try { $Guests = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Guests' if (-not $Guests) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21877' -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Guest user data not found in database' -Risk 'Medium' -Name 'All guests have a sponsor' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Application management' + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21877' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'All guests have a sponsor' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Application management' return } diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21886.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21886.ps1 index aec2ad6c6b1b..674390fef96b 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21886.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21886.ps1 @@ -8,7 +8,7 @@ function Invoke-CippTestZTNA21886 { try { $ServicePrincipals = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ServicePrincipals' if (-not $ServicePrincipals) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21886' -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Service principal data not found in database' -Risk 'Medium' -Name 'Applications are configured for automatic user provisioning' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Applications management' + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21886' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'Applications are configured for automatic user provisioning' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Applications management' return } diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21896.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21896.ps1 index 20fc29dfef3f..facaf67d0489 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21896.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21896.ps1 @@ -8,7 +8,7 @@ function Invoke-CippTestZTNA21896 { try { $ServicePrincipals = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ServicePrincipals' if (-not $ServicePrincipals) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21896' -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Service principal data not found in database' -Risk 'Medium' -Name 'Service principals do not have certificates or credentials associated with them' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Application management' + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21896' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'Service principals do not have certificates or credentials associated with them' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Application management' return } diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21964.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21964.ps1 index cee729bcafec..6e62ea83874f 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21964.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21964.ps1 @@ -11,7 +11,7 @@ function Invoke-CippTestZTNA21964 { $AuthStrengths = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationStrengths' if (-not $AuthStrengths) { - Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Authentication strength policies not found in database' -Risk 'High' -Name 'Enable protected actions to secure Conditional Access policy creation and changes' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Access control' + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'Enable protected actions to secure Conditional Access policy creation and changes' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Access control' return } diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21992.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21992.ps1 index c0f29d74e0c4..738197a548a8 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21992.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21992.ps1 @@ -10,7 +10,7 @@ function Invoke-CippTestZTNA21992 { $ServicePrincipals = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ServicePrincipals' #Tested if (-not $Apps -and -not $ServicePrincipals) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21992' -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Application and service principal data not found in database' -Risk 'High' -Name 'Application certificates must be rotated on a regular basis' -UserImpact 'Low' -ImplementationEffort 'High' -Category 'Application management' + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21992' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'Application certificates must be rotated on a regular basis' -UserImpact 'Low' -ImplementationEffort 'High' -Category 'Application management' return } diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA22128.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA22128.ps1 index a5dc82d83502..654c9549461d 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA22128.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA22128.ps1 @@ -10,7 +10,7 @@ function Invoke-CippTestZTNA22128 { $Guests = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Guests' if (-not $Roles -or -not $Guests) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA22128' -TestType 'Identity' -Status 'Investigate' -ResultMarkdown 'Role or guest user data not found in database' -Risk 'High' -Name 'Guests are not assigned high privileged directory roles' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Application management' + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA22128' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'Guests are not assigned high privileged directory roles' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Application management' return } diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24572.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24572.ps1 index 4fc9b3dda0e3..26fa10a7152d 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24572.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA24572.ps1 @@ -11,7 +11,7 @@ function Invoke-CippTestZTNA24572 { $EnrollmentConfigs = New-CIPPDbRequest -TenantFilter $Tenant -Type 'IntuneDeviceEnrollmentConfigurations' if (-not $EnrollmentConfigs) { - Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Devices' -Status 'Investigate' -ResultMarkdown 'Device enrollment configurations not found in database' -Risk 'Medium' -Name 'Device enrollment notifications are enforced to ensure user awareness and secure onboarding' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Tenant' + Add-CippTestResult -TenantFilter $Tenant -TestId $TestId -TestType 'Devices' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'Device enrollment notifications are enforced to ensure user awareness and secure onboarding' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Tenant' return } From ed4fae1d0d5840bf73e96c6d0caa798066c4d376 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Sat, 3 Jan 2026 00:55:33 +0100 Subject: [PATCH 079/503] Updates for tests --- .../Public/Entrypoints/HTTP Functions/Invoke-ListTests.ps1 | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ListTests.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ListTests.ps1 index 8f34cbee373e..cf52de3d8de2 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ListTests.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ListTests.ps1 @@ -79,7 +79,10 @@ function Invoke-ListTests { if ($ReportFound) { $AllReportTests = $IdentityTests + $DevicesTests # Use HashSet for O(1) lookup performance - $TestLookup = [System.Collections.Generic.HashSet[string]]::new([string[]]$AllReportTests) + $TestLookup = [System.Collections.Generic.HashSet[string]]::new() + foreach ($test in $AllReportTests) { + [void]$TestLookup.Add($test) + } $FilteredTests = $TestResultsData.TestResults | Where-Object { $TestLookup.Contains($_.RowKey) } $TestResultsData.TestResults = @($FilteredTests) } else { From f124bbfbda085a29f47c3b11c123a2891b57fa6a Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Sat, 3 Jan 2026 03:36:54 +0100 Subject: [PATCH 080/503] create ability to run tests --- .../Tests/Invoke-CIPPTestsRun.ps1 | 2 +- .../Tests/Push-CIPPTestsRun.ps1 | 36 ++++++++++++ .../HTTP Functions/Invoke-ExecTestRun.ps1 | 57 +++++++++++++++++++ 3 files changed, 94 insertions(+), 1 deletion(-) rename Modules/CIPPCore/Public/Entrypoints/{HTTP Functions/Tenant => Activity Triggers}/Tests/Invoke-CIPPTestsRun.ps1 (99%) create mode 100644 Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Tests/Push-CIPPTestsRun.ps1 create mode 100644 Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ExecTestRun.ps1 diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Tests/Invoke-CIPPTestsRun.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Tests/Invoke-CIPPTestsRun.ps1 similarity index 99% rename from Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Tests/Invoke-CIPPTestsRun.ps1 rename to Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Tests/Invoke-CIPPTestsRun.ps1 index 8eb2d177b2c9..9cc264d9af27 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Tests/Invoke-CIPPTestsRun.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Tests/Invoke-CIPPTestsRun.ps1 @@ -73,6 +73,6 @@ function Invoke-CIPPTestsRun { } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -message "Failed to start tests orchestration: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - throw + throw $ErrorMessage } } diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Tests/Push-CIPPTestsRun.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Tests/Push-CIPPTestsRun.ps1 new file mode 100644 index 000000000000..7bea0f014457 --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Tests/Push-CIPPTestsRun.ps1 @@ -0,0 +1,36 @@ +function Push-CIPPTestsRun { + <# + .SYNOPSIS + PostExecution function to run tests after data collection completes + .FUNCTIONALITY + Entrypoint + #> + param($Item) + + try { + $TenantFilter = $Item.Parameters.TenantFilter + Write-Information "PostExecution: Starting tests for tenant: $TenantFilter after data collection completed" + Write-LogMessage -API 'Tests' -tenant $TenantFilter -message 'Starting test run after data collection' -sev Info + + # Call the test run function + $Result = Invoke-CIPPTestsRun -TenantFilter $TenantFilter + + Write-LogMessage -API 'Tests' -tenant $TenantFilter -message "Test run started. Instance ID: $($Result.InstanceId)" -sev Info + Write-Information "PostExecution: Tests started with Instance ID: $($Result.InstanceId)" + + return @{ + Success = $true + InstanceId = $Result.InstanceId + Message = $Result.Message + } + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $TenantFilter -message "Failed to start test run: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Write-Warning "PostExecution: Error starting tests - $($ErrorMessage.NormalizedError)" + + return @{ + Success = $false + Error = $ErrorMessage.NormalizedError + } + } +} diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ExecTestRun.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ExecTestRun.ps1 new file mode 100644 index 000000000000..9557b43b1678 --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ExecTestRun.ps1 @@ -0,0 +1,57 @@ +function Invoke-ExecTestRun { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Tenant.Tests.ReadWrite + #> + param($Request, $TriggerMetadata) + + $APIName = $TriggerMetadata.FunctionName + Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -message 'Accessed this API' -Sev 'Debug' + + try { + $TenantFilter = $Request.Query.tenantFilter ?? $Request.Body.tenantFilter + Write-LogMessage -API $APIName -tenant $TenantFilter -message "Starting data collection and test run for tenant: $TenantFilter" -sev Info + $Batch = @( + @{ + FunctionName = 'CIPPDBCacheData' + TenantFilter = $TenantFilter + QueueId = $Queue.RowKey + QueueName = "Cache - $TenantFilter" + } + ) + $InputObject = [PSCustomObject]@{ + OrchestratorName = 'TestDataCollectionAndRun' + Batch = $Batch + PostExecution = @{ + FunctionName = 'CIPPTestsRun' + Parameters = @{ + TenantFilter = $TenantFilter + } + } + SkipLog = $false + } + + $InstanceId = Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Depth 5 -Compress) + + $StatusCode = [HttpStatusCode]::OK + $Body = [PSCustomObject]@{ + Results = "Successfully started data collection and test run for $TenantFilter" + InstanceId = $InstanceId + } + + Write-LogMessage -API $APIName -tenant $TenantFilter -message "Data collection and test run orchestration started. Instance ID: $InstanceId" -sev Info + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API $APIName -tenant $TenantFilter -message "Failed to start data collection/test run: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + $StatusCode = [HttpStatusCode]::BadRequest + $Body = @{ Message = "Failed to start data collection/test run for $TenantFilter" } + } + + return ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = $Body + }) +} From d4e6b209f3a3a1339ed3729b93cd133dbce21b22 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Sat, 3 Jan 2026 03:39:44 +0100 Subject: [PATCH 081/503] output binding fixes --- .../CIPP/Settings/Invoke-ExecExchangeRoleRepair.ps1 | 2 +- .../Endpoint/MEM/Invoke-ExecDevicePasscodeAction.ps1 | 2 +- .../Public/Entrypoints/HTTP Functions/Invoke-AddTestReport.ps1 | 2 +- .../Entrypoints/HTTP Functions/Invoke-DeleteTestReport.ps1 | 2 +- .../Entrypoints/HTTP Functions/Invoke-ListAvailableTests.ps1 | 2 +- .../Entrypoints/HTTP Functions/Invoke-ListTestReports.ps1 | 2 +- .../Public/Entrypoints/HTTP Functions/Invoke-ListTests.ps1 | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecExchangeRoleRepair.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecExchangeRoleRepair.ps1 index c208252b3dca..facaa423a70a 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecExchangeRoleRepair.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecExchangeRoleRepair.ps1 @@ -89,7 +89,7 @@ function Invoke-ExecExchangeRoleRepair { } } - Push-OutputBinding -Name 'Response' -Value ([HttpResponseContext]@{ + returns ([HttpResponseContext]@{ StatusCode = [System.Net.HttpStatusCode]::OK Body = $Results }) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ExecDevicePasscodeAction.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ExecDevicePasscodeAction.ps1 index 2207fa5761b0..cf0da9778d60 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ExecDevicePasscodeAction.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ExecDevicePasscodeAction.ps1 @@ -46,7 +46,7 @@ function Invoke-ExecDevicePasscodeAction { $Results = $Result } - Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + return ([HttpResponseContext]@{ StatusCode = $StatusCode Body = @{Results = $Results } }) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-AddTestReport.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-AddTestReport.ps1 index 2fc40dfb6533..534dc7ed2896 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-AddTestReport.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-AddTestReport.ps1 @@ -53,7 +53,7 @@ function Invoke-AddTestReport { $StatusCode = [HttpStatusCode]::BadRequest } - Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + return ([HttpResponseContext]@{ StatusCode = $StatusCode Body = ConvertTo-Json -InputObject $Body -Depth 10 }) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-DeleteTestReport.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-DeleteTestReport.ps1 index 39d7197ba028..ed502f467c1e 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-DeleteTestReport.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-DeleteTestReport.ps1 @@ -30,7 +30,7 @@ function Invoke-DeleteTestReport { $StatusCode = [HttpStatusCode]::BadRequest } - Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + return ([HttpResponseContext]@{ StatusCode = $StatusCode Body = ConvertTo-Json -InputObject $Body -Depth 10 }) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ListAvailableTests.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ListAvailableTests.ps1 index d047ec3bdeae..ceee5d026d31 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ListAvailableTests.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ListAvailableTests.ps1 @@ -79,7 +79,7 @@ function Invoke-ListAvailableTests { $StatusCode = [HttpStatusCode]::BadRequest } - Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + return ([HttpResponseContext]@{ StatusCode = $StatusCode Body = ConvertTo-Json -InputObject $Body -Depth 10 }) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ListTestReports.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ListTestReports.ps1 index 4920399d70c1..eae3538134ce 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ListTestReports.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ListTestReports.ps1 @@ -62,7 +62,7 @@ function Invoke-ListTestReports { $Body = @{ Error = $ErrorMessage.NormalizedError } } - Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + return([HttpResponseContext]@{ StatusCode = $StatusCode Body = ConvertTo-Json -InputObject $Body -Depth 10 -Compress }) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ListTests.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ListTests.ps1 index cf52de3d8de2..08946b093fe9 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ListTests.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ListTests.ps1 @@ -156,7 +156,7 @@ function Invoke-ListTests { $Body = @{ Error = $ErrorMessage.NormalizedError } } - Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + return ([HttpResponseContext]@{ StatusCode = $StatusCode Body = $Body }) From b41127c2537328fd27db31e01a2cac8b2e14abc0 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Sat, 3 Jan 2026 13:57:45 +0100 Subject: [PATCH 082/503] clean code --- .../Public/Entrypoints/HTTP Functions/Invoke-ExecTestRun.ps1 | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ExecTestRun.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ExecTestRun.ps1 index 9557b43b1678..bbf455e74581 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ExecTestRun.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ExecTestRun.ps1 @@ -36,10 +36,7 @@ function Invoke-ExecTestRun { $InstanceId = Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Depth 5 -Compress) $StatusCode = [HttpStatusCode]::OK - $Body = [PSCustomObject]@{ - Results = "Successfully started data collection and test run for $TenantFilter" - InstanceId = $InstanceId - } + $Body = [PSCustomObject]@{ Results = "Successfully started data collection and test run for $TenantFilter" } Write-LogMessage -API $APIName -tenant $TenantFilter -message "Data collection and test run orchestration started. Instance ID: $InstanceId" -sev Info From 9d2831db4caffe3f20f877e82d51ceed52a8166f Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Sat, 3 Jan 2026 23:07:16 +0100 Subject: [PATCH 083/503] three new tests --- .../Identity/Invoke-CippTestZTNA21782.ps1 | 94 ++++++++++++++++++ .../Identity/Invoke-CippTestZTNA21801.ps1 | 95 +++++++++++++++++++ .../Identity/Invoke-CippTestZTNA21817.ps1 | 80 ++++++++++++++++ 3 files changed, 269 insertions(+) create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21782.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21801.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21817.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21782.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21782.ps1 new file mode 100644 index 000000000000..40441197b262 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21782.ps1 @@ -0,0 +1,94 @@ +function Invoke-CippTestZTNA21782 { + <# + .SYNOPSIS + Privileged accounts have phishing-resistant methods registered + #> + param($Tenant) + + try { + $UserRegistrationDetails = New-CIPPDbRequest -TenantFilter $Tenant -Type 'UserRegistrationDetails' + $RoleAssignments = New-CIPPDbRequest -TenantFilter $Tenant -Type 'RoleAssignments' + + if (-not $UserRegistrationDetails -or -not $RoleAssignments) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21782' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'Privileged accounts have phishing-resistant methods registered' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Privileged Access' + return + } + + $PhishResistantMethods = @('passKeyDeviceBound', 'passKeyDeviceBoundAuthenticator', 'windowsHelloForBusiness') + + # Join user registration details with role assignments + $results = $UserRegistrationDetails | Where-Object { + $userId = $_.id + $RoleAssignments | Where-Object { $_.principalId -eq $userId } + } | ForEach-Object { + $user = $_ + $userRoles = $RoleAssignments | Where-Object { $_.principalId -eq $user.id } + $hasPhishResistant = $false + + if ($user.methodsRegistered) { + foreach ($method in $PhishResistantMethods) { + if ($user.methodsRegistered -contains $method) { + $hasPhishResistant = $true + break + } + } + } + + [PSCustomObject]@{ + id = $user.id + userDisplayName = $user.userDisplayName + roleDisplayName = ($userRoles.roleDefinitionName -join ', ') + methodsRegistered = $user.methodsRegistered + phishResistantAuthMethod = $hasPhishResistant + } + } + + $totalUserCount = $results.Length + $phishResistantPrivUsers = $results | Where-Object { $_.phishResistantAuthMethod } + $phishablePrivUsers = $results | Where-Object { !$_.phishResistantAuthMethod } + + $phishResistantPrivUserCount = $phishResistantPrivUsers.Length + + $passed = $totalUserCount -eq $phishResistantPrivUserCount + + $testResultMarkdown = if ($passed) { + "Validated that all privileged users have registered phishing resistant authentication methods.`n`n%TestResult%" + } else { + "Found privileged users that have not yet registered phishing resistant authentication methods`n`n%TestResult%" + } + + $mdInfo = "## Privileged users`n`n" + + if ($passed) { + $mdInfo = "All privileged users have registered phishing resistant authentication methods.`n`n" + } else { + $mdInfo = "Found privileged users that have not registered phishing resistant authentication methods.`n`n" + } + + $mdInfo = $mdInfo + "| User | Role Name | Phishing resistant method registered |`n" + $mdInfo = $mdInfo + "| :--- | :--- | :---: |`n" + + $userLinkFormat = 'https://entra.microsoft.com/#view/Microsoft_AAD_UsersAndTenants/UserProfileMenuBlade/~/UserAuthMethods/userId/{0}/hidePreviewBanner~/true' + + $mdLines = @($phishablePrivUsers | Sort-Object userDisplayName | ForEach-Object { + $userLink = $userLinkFormat -f $_.id + "|[$($_.userDisplayName)]($userLink)| $($_.roleDisplayName) | ❌ |`n" + }) + $mdInfo = $mdInfo + ($mdLines -join '') + + $mdLines = @($phishResistantPrivUsers | Sort-Object userDisplayName | ForEach-Object { + $userLink = $userLinkFormat -f $_.id + "|[$($_.userDisplayName)]($userLink)| $($_.roleDisplayName) | ✅ |`n" + }) + $mdInfo = $mdInfo + ($mdLines -join '') + + $testResultMarkdown = $testResultMarkdown -replace '%TestResult%', $mdInfo + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21782' -TestType 'Identity' -Status $(if ($passed) { 'Passed' } else { 'Failed' }) -ResultMarkdown $testResultMarkdown -Risk 'High' -Name 'Privileged accounts have phishing-resistant methods registered' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Privileged Access' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21782' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Privileged accounts have phishing-resistant methods registered' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Privileged Access' + } +} diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21801.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21801.ps1 new file mode 100644 index 000000000000..1b3783d09304 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21801.ps1 @@ -0,0 +1,95 @@ +function Invoke-CippTestZTNA21801 { + <# + .SYNOPSIS + Users have strong authentication methods configured + #> + param($Tenant) + + try { + $UserRegistrationDetails = New-CIPPDbRequest -TenantFilter $Tenant -Type 'UserRegistrationDetails' + $Users = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Users' + + if (-not $UserRegistrationDetails -or -not $Users) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21801' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'Users have strong authentication methods configured' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Credential Management' + return + } + + $PhishResistantMethods = @('passKeyDeviceBound', 'passKeyDeviceBoundAuthenticator', 'windowsHelloForBusiness') + + $results = $UserRegistrationDetails | Where-Object { + $userId = $_.id + $matchingUser = $Users | Where-Object { $_.id -eq $userId -and $_.accountEnabled } + $matchingUser + } | ForEach-Object { + $regDetail = $_ + $matchingUser = $Users | Where-Object { $_.id -eq $regDetail.id } + $hasPhishResistant = $false + + if ($regDetail.methodsRegistered) { + foreach ($method in $PhishResistantMethods) { + if ($regDetail.methodsRegistered -contains $method) { + $hasPhishResistant = $true + break + } + } + } + + [PSCustomObject]@{ + id = $regDetail.id + displayName = $regDetail.userDisplayName + phishResistantAuthMethod = $hasPhishResistant + lastSuccessfulSignInDateTime = $matchingUser.signInActivity.lastSuccessfulSignInDateTime + } + } + + $totalUserCount = $results.Length + $phishResistantUsers = $results | Where-Object { $_.phishResistantAuthMethod } + $phishableUsers = $results | Where-Object { !$_.phishResistantAuthMethod } + + $phishResistantUserCount = $phishResistantUsers.Length + + $passed = $totalUserCount -eq $phishResistantUserCount + + $testResultMarkdown = if ($passed) { + "Validated that all users have registered phishing resistant authentication methods.`n`n%TestResult%" + } else { + "Found users that have not yet registered phishing resistant authentication methods`n`n%TestResult%" + } + + $mdInfo = "## Users strong authentication methods`n`n" + + if ($passed) { + $mdInfo = "All users have registered phishing resistant authentication methods.`n`n" + } else { + $mdInfo = "Found users that have not registered phishing resistant authentication methods.`n`n" + } + + $mdInfo = $mdInfo + "| User | Last sign in | Phishing resistant method registered |`n" + $mdInfo = $mdInfo + "| :--- | :--- | :---: |`n" + + $userLinkFormat = 'https://entra.microsoft.com/#view/Microsoft_AAD_UsersAndTenants/UserProfileMenuBlade/~/UserAuthMethods/userId/{0}/hidePreviewBanner~/true' + + $mdLines = @($phishableUsers | Sort-Object displayName | ForEach-Object { + $userLink = $userLinkFormat -f $_.id + $lastSignInDate = if ($_.lastSuccessfulSignInDateTime) { (Get-Date $_.lastSuccessfulSignInDateTime -Format 'yyyy-MM-dd') } else { 'Never' } + "|[$($_.displayName)]($userLink)| $lastSignInDate | ❌ |`n" + }) + $mdInfo = $mdInfo + ($mdLines -join '') + + $mdLines = @($phishResistantUsers | Sort-Object displayName | ForEach-Object { + $userLink = $userLinkFormat -f $_.id + $lastSignInDate = if ($_.lastSuccessfulSignInDateTime) { (Get-Date $_.lastSuccessfulSignInDateTime -Format 'yyyy-MM-dd') } else { 'Never' } + "|[$($_.displayName)]($userLink)| $lastSignInDate | ✅ |`n" + }) + $mdInfo = $mdInfo + ($mdLines -join '') + + $testResultMarkdown = $testResultMarkdown -replace '%TestResult%', $mdInfo + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21801' -TestType 'Identity' -Status $(if ($passed) { 'Passed' } else { 'Failed' }) -ResultMarkdown $testResultMarkdown -Risk 'Medium' -Name 'Users have strong authentication methods configured' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Credential Management' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21801' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'Users have strong authentication methods configured' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Credential Management' + } +} diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21817.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21817.ps1 new file mode 100644 index 000000000000..cfe4f469749a --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21817.ps1 @@ -0,0 +1,80 @@ +function Invoke-CippTestZTNA21817 { + <# + .SYNOPSIS + Global Administrator role activation triggers an approval workflow + #> + param($Tenant) + + try { + $RoleManagementPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'RoleManagementPolicies' + + if (-not $RoleManagementPolicies) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21817' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'Global Administrator role activation triggers an approval workflow' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Application Management' + return + } + + $globalAdminRoleId = '62e90394-69f5-4237-9190-012177145e10' + + $globalAdminPolicy = $RoleManagementPolicies | Where-Object { + $_.scopeId -eq '/' -and + $_.scopeType -eq 'DirectoryRole' -and + $_.roleDefinitionId -eq $globalAdminRoleId + } + + $tableRows = '' + $result = $false + + if ($globalAdminPolicy) { + $approvalRule = $globalAdminPolicy.rules | Where-Object { $_.id -like '*Approval_EndUser_Assignment*' } + + if ($approvalRule -and $approvalRule.setting.isApprovalRequired -eq $true) { + $approverCount = 0 + foreach ($stage in $approvalRule.setting.approvalStages) { + $approverCount = $approverCount + ($stage.primaryApprovers | Measure-Object).Count + } + + if ($approverCount -gt 0) { + $result = $true + $testResultMarkdown = "✅ **Pass**: Approval required with $approverCount primary approver(s) configured.`n`n%TestResult%" + $primaryApprovers = ($approvalRule.setting.approvalStages[0].primaryApprovers.description -join ', ') + $escalationApprovers = ($approvalRule.setting.approvalStages[0].escalationApprovers.description -join ', ') + $tableRows = "| Yes | $primaryApprovers | $escalationApprovers |`n" + } else { + $testResultMarkdown = "❌ **Fail**: Approval required but no approvers configured.`n`n%TestResult%" + $tableRows = "| Yes | None | None |`n" + } + } else { + $testResultMarkdown = "❌ **Fail**: Approval not required for Global Administrator role activation.`n`n%TestResult%" + $tableRows = "| No | N/A | N/A |`n" + } + } else { + $testResultMarkdown = "❌ **Fail**: No PIM policy found for Global Administrator role.`n`n%TestResult%" + $tableRows = "| N/A | N/A | N/A |`n" + } + + $passed = $result + + $reportTitle = 'Global Administrator role activation and approval workflow' + + $formatTemplate = @' + +## {0} + + +| Approval Required | Primary Approvers | Escalation Approvers | +| :---------------- | :---------------- | :------------------- | +{1} + +'@ + + $mdInfo = $formatTemplate -f $reportTitle, $tableRows + $testResultMarkdown = $testResultMarkdown -replace '%TestResult%', $mdInfo + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21817' -TestType 'Identity' -Status $(if ($passed) { 'Passed' } else { 'Failed' }) -ResultMarkdown $testResultMarkdown -Risk 'High' -Name 'Global Administrator role activation triggers an approval workflow' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Application Management' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21817' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Global Administrator role activation triggers an approval workflow' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Application Management' + } +} From 6ad08e890dfabc623fa357d7b62acc4cc9942c67 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Sun, 4 Jan 2026 22:26:11 +0100 Subject: [PATCH 084/503] updates to cippstandardscomparefield for new planning. --- .../Functions/Test-CIPPStandardLicense.ps1 | 2 +- .../Public/Set-CIPPStandardsCompareField.ps1 | 40 +++++++++++++------ 2 files changed, 29 insertions(+), 13 deletions(-) diff --git a/Modules/CIPPCore/Public/Functions/Test-CIPPStandardLicense.ps1 b/Modules/CIPPCore/Public/Functions/Test-CIPPStandardLicense.ps1 index b0bec0aee247..851822afe56c 100644 --- a/Modules/CIPPCore/Public/Functions/Test-CIPPStandardLicense.ps1 +++ b/Modules/CIPPCore/Public/Functions/Test-CIPPStandardLicense.ps1 @@ -46,7 +46,7 @@ function Test-CIPPStandardLicense { if ($Capabilities.Count -le 0) { if (!$SkipLog.IsPresent) { Write-LogMessage -API 'Standards' -tenant $TenantFilter -message "Tenant does not have the required capability to run standard $StandardName`: The tenant needs one of the following service plans: $($RequiredCapabilities -join ',')" -sev Info - Set-CIPPStandardsCompareField -FieldName "standards.$StandardName" -FieldValue "License Missing: This tenant is not licensed for the following capabilities: $($RequiredCapabilities -join ',')" -Tenant $TenantFilter + Set-CIPPStandardsCompareField -FieldName "standards.$StandardName" -LicenseAvailable $false -FieldValue "License Missing: This tenant is not licensed for the following capabilities: $($RequiredCapabilities -join ',')" -Tenant $TenantFilter Write-Verbose "Tenant does not have the required capability to run standard $StandardName - $($RequiredCapabilities -join ','). Exiting" } return $false diff --git a/Modules/CIPPCore/Public/Set-CIPPStandardsCompareField.ps1 b/Modules/CIPPCore/Public/Set-CIPPStandardsCompareField.ps1 index 5bf3b8635f6e..ed868fbb2663 100644 --- a/Modules/CIPPCore/Public/Set-CIPPStandardsCompareField.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPStandardsCompareField.ps1 @@ -2,15 +2,19 @@ function Set-CIPPStandardsCompareField { [CmdletBinding(SupportsShouldProcess = $true)] param ( $FieldName, - $FieldValue, + $FieldValue, #FieldValue is here for backward compatibility. + $CurrentValue, #The latest actual value in raw json + $ExpectedValue, #The expected value - e.g. the settings object from our standard $TenantFilter, [Parameter()] + [bool]$LicenseAvailable = $true, + [Parameter()] [array]$BulkFields ) $Table = Get-CippTable -tablename 'CippStandardsReports' $TenantName = Get-Tenants -TenantFilter $TenantFilter - # Helper function to normalize field values + # Helper function to normalize field values. This can go in a couple of releases tbh. function ConvertTo-NormalizedFieldValue { param($Value) if ($Value -is [System.Boolean]) { @@ -34,20 +38,26 @@ function Set-CIPPStandardsCompareField { # Build array of entities to insert/update $EntitiesToProcess = [System.Collections.Generic.List[object]]::new() - + foreach ($Field in $BulkFields) { $NormalizedValue = ConvertTo-NormalizedFieldValue -Value $Field.FieldValue - + if ($ExistingHash.ContainsKey($Field.FieldName)) { $Entity = $ExistingHash[$Field.FieldName] $Entity.Value = $NormalizedValue $Entity | Add-Member -NotePropertyName TemplateId -NotePropertyValue ([string]$script:CippStandardInfoStorage.Value.StandardTemplateId) -Force + $Entity | Add-Member -NotePropertyName LicenseAvailable -NotePropertyValue ([bool]$Field.LicenseAvailable) -Force + $Entity | Add-Member -NotePropertyName CurrentValue -NotePropertyValue ([string]$Field.CurrentValue) -Force + $Entity | Add-Member -NotePropertyName ExpectedValue -NotePropertyValue ([string]$Field.ExpectedValue) -Force } else { $Entity = [PSCustomObject]@{ - PartitionKey = [string]$TenantName.defaultDomainName - RowKey = [string]$Field.FieldName - Value = $NormalizedValue - TemplateId = [string]$script:CippStandardInfoStorage.Value.StandardTemplateId + PartitionKey = [string]$TenantName.defaultDomainName + RowKey = [string]$Field.FieldName + Value = $NormalizedValue + TemplateId = [string]$script:CippStandardInfoStorage.Value.StandardTemplateId + LicenseAvailable = [bool]$Field.LicenseAvailable + CurrentValue = [string]$Field.CurrentValue + ExpectedValue = [string]$Field.ExpectedValue } } $EntitiesToProcess.Add($Entity) @@ -72,13 +82,19 @@ function Set-CIPPStandardsCompareField { if ($Existing) { $Existing.Value = $NormalizedValue $Existing | Add-Member -NotePropertyName TemplateId -NotePropertyValue ([string]$script:CippStandardInfoStorage.Value.StandardTemplateId) -Force + $Existing | Add-Member -NotePropertyName LicenseAvailable -NotePropertyValue ([bool]$LicenseAvailable) -Force + $Existing | Add-Member -NotePropertyName CurrentValue -NotePropertyValue ([string]$CurrentValue) -Force + $Existing | Add-Member -NotePropertyName ExpectedValue -NotePropertyValue ([string]$ExpectedValue) -Force Add-CIPPAzDataTableEntity @Table -Entity $Existing -Force } else { $Result = [PSCustomObject]@{ - PartitionKey = [string]$TenantName.defaultDomainName - RowKey = [string]$FieldName - Value = $NormalizedValue - TemplateId = [string]$script:CippStandardInfoStorage.Value.StandardTemplateId + PartitionKey = [string]$TenantName.defaultDomainName + RowKey = [string]$FieldName + Value = $NormalizedValue + TemplateId = [string]$script:CippStandardInfoStorage.Value.StandardTemplateId + LicenseAvailable = [bool]$LicenseAvailable + CurrentValue = [string]$CurrentValue + ExpectedValue = [string]$ExpectedValue } Add-CIPPAzDataTableEntity @Table -Entity $Result -Force } From 470c0243545e3fb05542b8c766d6eb9e1c3bbbf1 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Mon, 5 Jan 2026 00:12:13 +0100 Subject: [PATCH 085/503] updates to compares and prettification --- .../Standards/Invoke-ListStandardsCompare.ps1 | 22 ++++++++++++++++--- .../Functions/Get-CIPPTenantAlignment.ps1 | 13 +++++++++-- Modules/CIPPCore/Public/Get-CIPPDrift.ps1 | 6 +++-- .../Invoke-CIPPStandardEXODirectSend.ps1 | 11 +++++++--- 4 files changed, 42 insertions(+), 10 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ListStandardsCompare.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ListStandardsCompare.ps1 index d2aaddf9a694..0b94eb63d919 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ListStandardsCompare.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ListStandardsCompare.ps1 @@ -53,13 +53,29 @@ function Invoke-ListStandardsCompare { $FieldValue = [string]$FieldValue } + # Parse CurrentValue and ExpectedValue from JSON if they are JSON strings + $ParsedCurrentValue = if ($Standard.CurrentValue -and (Test-Json -Json $Standard.CurrentValue -ErrorAction SilentlyContinue)) { + ConvertFrom-Json -InputObject $Standard.CurrentValue -ErrorAction SilentlyContinue + } else { + $Standard.CurrentValue + } + + $ParsedExpectedValue = if ($Standard.ExpectedValue -and (Test-Json -Json $Standard.ExpectedValue -ErrorAction SilentlyContinue)) { + ConvertFrom-Json -InputObject $Standard.ExpectedValue -ErrorAction SilentlyContinue + } else { + $Standard.ExpectedValue + } + if (-not $TenantStandards.ContainsKey($Tenant)) { $TenantStandards[$Tenant] = @{} } $TenantStandards[$Tenant][$FieldName] = @{ - Value = $FieldValue - LastRefresh = $Standard.TimeStamp.ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ') - TemplateId = $Standard.TemplateId + Value = $FieldValue + LastRefresh = $Standard.TimeStamp.ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ') + TemplateId = $Standard.TemplateId + LicenseAvailable = $Standard.LicenseAvailable + CurrentValue = $ParsedCurrentValue + ExpectedValue = $ParsedExpectedValue } } diff --git a/Modules/CIPPCore/Public/Functions/Get-CIPPTenantAlignment.ps1 b/Modules/CIPPCore/Public/Functions/Get-CIPPTenantAlignment.ps1 index 9e48681c1b02..92743f5f55b4 100644 --- a/Modules/CIPPCore/Public/Functions/Get-CIPPTenantAlignment.ps1 +++ b/Modules/CIPPCore/Public/Functions/Get-CIPPTenantAlignment.ps1 @@ -90,8 +90,11 @@ function Get-CIPPTenantAlignment { $tenantData[$Tenant] = @{} } $tenantData[$Tenant][$FieldName] = @{ - Value = $FieldValue - LastRefresh = $Standard.TimeStamp.ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ') + Value = $FieldValue + LastRefresh = $Standard.TimeStamp.ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ') + LicenseAvailable = $Standard.LicenseAvailable + CurrentValue = $Standard.CurrentValue + ExpectedValue = $Standard.ExpectedValue } } $TenantStandards = $tenantData @@ -276,6 +279,9 @@ function Get-CIPPTenantAlignment { StandardValue = $StandardValueJson ComplianceStatus = $ComplianceStatus ReportingDisabled = $IsReportingDisabled + LicenseAvailable = $StandardObject.LicenseAvailable + CurrentValue = $StandardObject.CurrentValue + ExpectedValue = $StandardObject.ExpectedValue }) } else { $ComplianceStatus = if ($IsReportingDisabled) { @@ -290,6 +296,9 @@ function Get-CIPPTenantAlignment { StandardValue = 'NOT FOUND' ComplianceStatus = $ComplianceStatus ReportingDisabled = $IsReportingDisabled + LicenseAvailable = $null + CurrentValue = $null + ExpectedValue = $null }) } } diff --git a/Modules/CIPPCore/Public/Get-CIPPDrift.ps1 b/Modules/CIPPCore/Public/Get-CIPPDrift.ps1 index b93b683b67cd..17da95a2ad11 100644 --- a/Modules/CIPPCore/Public/Get-CIPPDrift.ps1 +++ b/Modules/CIPPCore/Public/Get-CIPPDrift.ps1 @@ -129,12 +129,14 @@ function Get-CIPPDrift { standardName = $ComparisonItem.StandardName standardDisplayName = $displayName standardDescription = $standardDescription - expectedValue = 'Compliant' receivedValue = $ComparisonItem.StandardValue state = 'current' Status = $Status Reason = $reason lastChangedByUser = $User + LicenseAvailable = $ComparisonItem.LicenseAvailable + CurrentValue = $ComparisonItem.CurrentValue + ExpectedValue = $ComparisonItem.ExpectedValue }) } } @@ -286,7 +288,7 @@ function Get-CIPPDrift { standardName = $PolicyKey standardDisplayName = "Intune - $TenantPolicyName" expectedValue = 'This policy only exists in the tenant, not in the template.' - receivedValue = $TenantPolicy.Policy + receivedValue = ($TenantPolicy.Policy | ConvertTo-Json -Depth 10 -Compress) state = 'current' Status = $Status Reason = $reason diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEXODirectSend.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEXODirectSend.ps1 index 1d21de6a55e3..3cf07e245b89 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEXODirectSend.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEXODirectSend.ps1 @@ -38,7 +38,7 @@ function Invoke-CIPPStandardEXODirectSend { # Input validation if ([string]::IsNullOrWhiteSpace($DesiredStateName) -or $DesiredStateName -eq 'Select a value') { Write-LogMessage -API 'Standards' -tenant $Tenant -message 'EXODirectSend: Invalid state parameter set' -sev Error - Return + return } # Get current organization config @@ -85,8 +85,13 @@ function Invoke-CIPPStandardEXODirectSend { # Report if needed if ($Settings.report -eq $true) { - - Set-CIPPStandardsCompareField -FieldName 'standards.EXODirectSend' -FieldValue $StateIsCorrect -Tenant $Tenant + $ExpectedState = @{ + RejectDirectSend = $DesiredState + } | ConvertTo-Json -Depth 10 -Compress + $CurrentState = @{ + RejectDirectSend = $CurrentConfig + } | ConvertTo-Json -Depth 10 -Compress + Set-CIPPStandardsCompareField -FieldName 'standards.EXODirectSend' -CurrentValue $CurrentState -ExpectedValue $ExpectedState -Tenant $Tenant Add-CIPPBPAField -FieldName 'EXODirectSend' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $Tenant } } From e96a91e7bd085dfc89e2a897737e715a692ea923 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Mon, 5 Jan 2026 10:29:31 -0500 Subject: [PATCH 086/503] Fix DynamicRules assignment in Invoke-ExecTenantGroup Replaces direct property assignment with Add-Member for 'DynamicRules' to ensure consistency with other property additions in the group entity. --- .../HTTP Functions/CIPP/Settings/Invoke-ExecTenantGroup.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecTenantGroup.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecTenantGroup.ps1 index 0e33e92067e3..73b1a1bce23b 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecTenantGroup.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecTenantGroup.ps1 @@ -45,7 +45,7 @@ function Invoke-ExecTenantGroup { } $GroupEntity | Add-Member -NotePropertyName 'GroupType' -NotePropertyValue $groupType -Force if ($groupType -eq 'dynamic' -and $dynamicRules) { - $GroupEntity.DynamicRules = "$($dynamicRules | ConvertTo-Json -Depth 100 -Compress)" + $GroupEntity | Add-Member -NotePropertyName 'DynamicRules' -NotePropertyValue "$($dynamicRules | ConvertTo-Json -Depth 100 -Compress)" -Force $GroupEntity | Add-Member -NotePropertyName 'RuleLogic' -NotePropertyValue $ruleLogic -Force } else { $GroupEntity | Add-Member -NotePropertyName 'RuleLogic' -NotePropertyValue $null -Force From bd2dd91f1f9a4fd60c65871a5ccaa3e47f1ccdb2 Mon Sep 17 00:00:00 2001 From: "Chase M (Velocigo)" <168204519+chase-vgo@users.noreply.github.com> Date: Mon, 5 Jan 2026 10:01:55 -0600 Subject: [PATCH 087/503] Add secret name / ID to table adds the description (displayName) and secret ID to the table for easier identification / automation. --- Modules/CIPPCore/Public/Alerts/Get-CIPPAlertAppSecretExpiry.ps1 | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertAppSecretExpiry.ps1 b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertAppSecretExpiry.ps1 index ac0a0026ae58..30097cd36268 100644 --- a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertAppSecretExpiry.ps1 +++ b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertAppSecretExpiry.ps1 @@ -31,6 +31,8 @@ function Get-CIPPAlertAppSecretExpiry { AppName = $App.displayName AppId = $App.appId Expires = $Credential.endDateTime + SecretName = $Credential.displayName + SecretID = $Credential.keyId Tenant = $TenantFilter } $AlertData.Add($Message) From 1b1e1c798ca7916780c7a5ecc92d410726a60ac4 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Mon, 5 Jan 2026 12:16:11 -0500 Subject: [PATCH 088/503] Simplify log date range filter in Invoke-ListLogs Replaced the loop generating multiple partition key filters with a single filter using 'PartitionKey ge' and 'PartitionKey le' for date ranges. This streamlines the query and improves readability. --- Modules/CIPPCore/Public/Entrypoints/Invoke-ListLogs.ps1 | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/Invoke-ListLogs.ps1 b/Modules/CIPPCore/Public/Entrypoints/Invoke-ListLogs.ps1 index 92682df3bf50..dd504b9700e7 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Invoke-ListLogs.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Invoke-ListLogs.ps1 @@ -92,12 +92,8 @@ function Invoke-ListLogs { $EndDate = $Request.Query.EndDate ?? $Request.Query.DateFilter if ($StartDate -and $EndDate) { - # Collect logs for each partition key date in range - $PartitionKeys = for ($Date = [datetime]::ParseExact($StartDate, 'yyyyMMdd', $null); $Date -le [datetime]::ParseExact($EndDate, 'yyyyMMdd', $null); $Date = $Date.AddDays(1)) { - $PartitionKey = $Date.ToString('yyyyMMdd') - "PartitionKey eq '$PartitionKey'" - } - $Filter = $PartitionKeys -join ' or ' + # Collect logs for date range + $Filter = "PartitionKey ge '$StartDate' and PartitionKey le '$EndDate'" } elseif ($StartDate) { $Filter = "PartitionKey eq '{0}'" -f $StartDate } else { From bfe9a6f93c6cfb2428f83aa0193a63b0ae96b91e Mon Sep 17 00:00:00 2001 From: John Duprey Date: Mon, 5 Jan 2026 13:31:55 -0500 Subject: [PATCH 089/503] Add null checks for tenant and standard keys Added explicit checks to ensure $Tenant and $StandardKey are not null or empty before accessing or modifying related data structures. This improves robustness and prevents potential runtime errors. --- .../CIPPCore/Public/Functions/Get-CIPPTenantAlignment.ps1 | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Modules/CIPPCore/Public/Functions/Get-CIPPTenantAlignment.ps1 b/Modules/CIPPCore/Public/Functions/Get-CIPPTenantAlignment.ps1 index 92743f5f55b4..1f080836f9aa 100644 --- a/Modules/CIPPCore/Public/Functions/Get-CIPPTenantAlignment.ps1 +++ b/Modules/CIPPCore/Public/Functions/Get-CIPPTenantAlignment.ps1 @@ -86,7 +86,7 @@ function Get-CIPPTenantAlignment { } } - if (-not $tenantData.ContainsKey($Tenant)) { + if ($Tenant -and -not $tenantData.ContainsKey($Tenant)) { $tenantData[$Tenant] = @{} } $tenantData[$Tenant][$FieldName] = @{ @@ -245,7 +245,8 @@ function Get-CIPPTenantAlignment { # Use HashSet for Contains $IsReportingDisabled = $ReportingDisabledSet.Contains($StandardKey) # Use cached tenant data - $HasStandard = $CurrentTenantStandards.ContainsKey($StandardKey) + + $HasStandard = $StandardKey -and $CurrentTenantStandards.ContainsKey($StandardKey) if ($HasStandard) { $StandardObject = $CurrentTenantStandards[$StandardKey] From e9b6d29e8705078161c49970ddae30eba5fef679 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Mon, 5 Jan 2026 19:12:57 +0100 Subject: [PATCH 090/503] Fix: Remove measure command --- .../Alerts/Get-CIPPAlertNewAppApproval.ps1 | 72 ++++++++----------- 1 file changed, 31 insertions(+), 41 deletions(-) diff --git a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertNewAppApproval.ps1 b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertNewAppApproval.ps1 index 34e8f6a87918..d6899a8af1f4 100644 --- a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertNewAppApproval.ps1 +++ b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertNewAppApproval.ps1 @@ -13,55 +13,45 @@ function Get-CIPPAlertNewAppApproval { $Headers ) - Measure-CippTask -TaskName 'NewAppApprovalAlert' -EventName 'CIPP.AlertProfile' -Script { - try { - $Approvals = Measure-CippTask -TaskName 'GetAppConsentRequests' -EventName 'CIPP.AlertProfile' -Script { - New-GraphGetRequest -Uri "https://graph.microsoft.com/beta/identityGovernance/appConsent/appConsentRequests?`$top=100&`$filter=userConsentRequests/any (u:u/status eq 'InProgress')" -tenantid $TenantFilter - } - - if ($Approvals.count -gt 0) { - Measure-CippTask -TaskName 'ProcessApprovals' -EventName 'CIPP.AlertProfile' -Script { - $TenantGUID = (Get-Tenants -TenantFilter $TenantFilter -SkipDomains).customerId - $AlertData = [System.Collections.Generic.List[PSCustomObject]]::new() + try { + $Approvals = New-GraphGetRequest -Uri "https://graph.microsoft.com/beta/identityGovernance/appConsent/appConsentRequests?`$top=100&`$filter=userConsentRequests/any (u:u/status eq 'InProgress')" -tenantid $TenantFilter - foreach ($App in $Approvals) { - $userConsentRequests = Measure-CippTask -TaskName 'GetUserConsentRequests' -EventName 'CIPP.AlertProfile' -Script { - New-GraphGetRequest -Uri "https://graph.microsoft.com/v1.0/identityGovernance/appConsent/appConsentRequests/$($App.id)/userConsentRequests" -tenantid $TenantFilter - } + if ($Approvals.count -gt 0) { + $TenantGUID = (Get-Tenants -TenantFilter $TenantFilter -SkipDomains).customerId + $AlertData = [System.Collections.Generic.List[PSCustomObject]]::new() - $userConsentRequests | ForEach-Object { - $consentUrl = if ($App.consentType -eq 'Static') { - # if something is going wrong here you've probably stumbled on a fourth variation - rvdwegen - "https://login.microsoftonline.com/$($TenantFilter)/adminConsent?client_id=$($App.appId)&bf_id=$($App.id)&redirect_uri=https://entra.microsoft.com/TokenAuthorize" - } elseif ($App.pendingScopes.displayName) { - "https://login.microsoftonline.com/$($TenantFilter)/v2.0/adminConsent?client_id=$($App.appId)&scope=$($App.pendingScopes.displayName -Join(' '))&bf_id=$($App.id)&redirect_uri=https://entra.microsoft.com/TokenAuthorize" - } else { - "https://login.microsoftonline.com/$($TenantFilter)/adminConsent?client_id=$($App.appId)&bf_id=$($App.id)&redirect_uri=https://entra.microsoft.com/TokenAuthorize" - } + foreach ($App in $Approvals) { + $userConsentRequests = New-GraphGetRequest -Uri "https://graph.microsoft.com/v1.0/identityGovernance/appConsent/appConsentRequests/$($App.id)/userConsentRequests" -tenantid $TenantFilter - $Message = [PSCustomObject]@{ - RequestId = $_.id - AppName = $App.appDisplayName - RequestUser = $_.createdBy.user.userPrincipalName - Reason = $_.reason - RequestDate = $_.createdDateTime - Status = $_.status # Will allways be InProgress as we filter to only get these but this will reduce confusion when an alert is generated - AppId = $App.appId - Scopes = ($App.pendingScopes.displayName -join ', ') - ConsentURL = $consentUrl - Tenant = $TenantFilter - TenantId = $TenantGUID - } - $AlertData.Add($Message) - } + $userConsentRequests | ForEach-Object { + $consentUrl = if ($App.consentType -eq 'Static') { + # if something is going wrong here you've probably stumbled on a fourth variation - rvdwegen + "https://login.microsoftonline.com/$($TenantFilter)/adminConsent?client_id=$($App.appId)&bf_id=$($App.id)&redirect_uri=https://entra.microsoft.com/TokenAuthorize" + } elseif ($App.pendingScopes.displayName) { + "https://login.microsoftonline.com/$($TenantFilter)/v2.0/adminConsent?client_id=$($App.appId)&scope=$($App.pendingScopes.displayName -Join(' '))&bf_id=$($App.id)&redirect_uri=https://entra.microsoft.com/TokenAuthorize" + } else { + "https://login.microsoftonline.com/$($TenantFilter)/adminConsent?client_id=$($App.appId)&bf_id=$($App.id)&redirect_uri=https://entra.microsoft.com/TokenAuthorize" } - Measure-CippTask -TaskName 'WriteAlertTrace' -EventName 'CIPP.AlertProfile' -Script { - Write-AlertTrace -cmdletName $MyInvocation.MyCommand -tenantFilter $TenantFilter -data $AlertData + $Message = [PSCustomObject]@{ + RequestId = $_.id + AppName = $App.appDisplayName + RequestUser = $_.createdBy.user.userPrincipalName + Reason = $_.reason + RequestDate = $_.createdDateTime + Status = $_.status # Will always be InProgress as we filter to only get these but this will reduce confusion when an alert is generated + AppId = $App.appId + Scopes = ($App.pendingScopes.displayName -join ', ') + ConsentURL = $consentUrl + Tenant = $TenantFilter + TenantId = $TenantGUID } + $AlertData.Add($Message) } } - } catch { + + Write-AlertTrace -cmdletName $MyInvocation.MyCommand -tenantFilter $TenantFilter -data $AlertData } + } catch { } } From f5489dee992d4f1939cde884089532f77fb650fc Mon Sep 17 00:00:00 2001 From: John Duprey Date: Mon, 5 Jan 2026 15:25:49 -0500 Subject: [PATCH 091/503] Improve template update logic and compress JSON output Update template update checks to ensure the source matches the current template repository before updating or skipping. Also, add the -Compress flag to ConvertTo-Json calls to reduce JSON size when storing entities. --- Modules/CIPPCore/Public/New-CIPPTemplateRun.ps1 | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Modules/CIPPCore/Public/New-CIPPTemplateRun.ps1 b/Modules/CIPPCore/Public/New-CIPPTemplateRun.ps1 index a7f2176ab723..76ff020eb9ef 100644 --- a/Modules/CIPPCore/Public/New-CIPPTemplateRun.ps1 +++ b/Modules/CIPPCore/Public/New-CIPPTemplateRun.ps1 @@ -52,12 +52,12 @@ function New-CIPPTemplateRun { $ExistingTemplate = $ExistingTemplates | Where-Object { (![string]::IsNullOrEmpty($_.displayName) -and (Get-SanitizedFilename -filename $_.displayName) -eq (Get-SanitizedFilename -filename $File.name)) -or (![string]::IsNullOrEmpty($_.templateName) -and (Get-SanitizedFilename -filename $_.templateName) -eq (Get-SanitizedFilename -filename $File.name) ) -and ![string]::IsNullOrEmpty($_.SHA) } | Select-Object -First 1 $UpdateNeeded = $false - if ($ExistingTemplate -and $ExistingTemplate.SHA -ne $File.sha) { + if ($ExistingTemplate -and $ExistingTemplate.SHA -ne $File.sha -and $ExistingTemplate.Source -eq $TemplateSettings.templateRepo.value) { $Name = $ExistingTemplate.displayName ?? $ExistingTemplate.templateName Write-Information "Existing template $($Name) found, but SHA is different. Updating template." $UpdateNeeded = $true "Template $($Name) needs to be updated as the SHA is different" - } elseif ($ExistingTemplate -and $ExistingTemplate.SHA -eq $File.sha) { + } elseif ($ExistingTemplate -and $ExistingTemplate.SHA -eq $File.sha -and $ExistingTemplate.Source -eq $TemplateSettings.templateRepo.value) { Write-Information "Existing template $($File.name) found, but SHA is the same. No update needed." "Template $($File.name) found, but SHA is the same. No update needed." } @@ -263,7 +263,7 @@ function New-CIPPTemplateRun { RAWJson = $Template.TemplateJson Type = $Template.Type GUID = $ExistingPolicy.GUID - } | ConvertTo-Json + } | ConvertTo-Json -Compress Add-CIPPAzDataTableEntity @Table -Entity @{ JSON = "$object" @@ -283,7 +283,7 @@ function New-CIPPTemplateRun { RAWJson = $Template.TemplateJson Type = $Template.Type GUID = $GUID - } | ConvertTo-Json + } | ConvertTo-Json -Compress Add-CIPPAzDataTableEntity @Table -Entity @{ JSON = "$object" @@ -317,7 +317,7 @@ function New-CIPPTemplateRun { RAWJson = $Template.TemplateJson Type = $Template.Type GUID = $ExistingPolicy.GUID - } | ConvertTo-Json + } | ConvertTo-Json -Compress Add-CIPPAzDataTableEntity @Table -Entity @{ JSON = "$object" @@ -337,7 +337,7 @@ function New-CIPPTemplateRun { RAWJson = $Template.TemplateJson Type = $Template.Type GUID = $GUID - } | ConvertTo-Json + } | ConvertTo-Json -Compress Add-CIPPAzDataTableEntity @Table -Entity @{ JSON = "$object" From f4c5ed0f90d2d2ef385219da4cc18a9cd7826325 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Mon, 5 Jan 2026 15:27:10 -0500 Subject: [PATCH 092/503] Add Source property to package tag entity The Source property is now included when creating the entity for Add-CIPPAzDataTableEntity, using the value from $Template.Source if available. --- .../HTTP Functions/CIPP/Core/Invoke-ExecSetPackageTag.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ExecSetPackageTag.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ExecSetPackageTag.ps1 index 4898e5bb8def..e0cc80a3bc9a 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ExecSetPackageTag.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ExecSetPackageTag.ps1 @@ -39,9 +39,9 @@ function Invoke-ExecSetPackageTag { GUID = "$GUID" Package = $PackageValue SHA = $Template.SHA ?? $null + Source = $Template.Source ?? $null } - Add-CIPPAzDataTableEntity @Table -Entity $Entity -Force if ($Remove -eq $true) { From 98ce1a3b32d8d0ae69058dd9bf62e8216588a1b3 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Mon, 5 Jan 2026 18:23:55 -0500 Subject: [PATCH 093/503] undo rowkey check --- Modules/CIPPCore/Public/Add-CIPPScheduledTask.ps1 | 9 --------- 1 file changed, 9 deletions(-) diff --git a/Modules/CIPPCore/Public/Add-CIPPScheduledTask.ps1 b/Modules/CIPPCore/Public/Add-CIPPScheduledTask.ps1 index d309ac2c9681..6728082f8750 100644 --- a/Modules/CIPPCore/Public/Add-CIPPScheduledTask.ps1 +++ b/Modules/CIPPCore/Public/Add-CIPPScheduledTask.ps1 @@ -46,20 +46,12 @@ function Add-CIPPScheduledTask { return "Could not run task: $ErrorMessage" } } else { - # Generate RowKey early to use in duplicate check if (!$Task.RowKey) { $RowKey = (New-Guid).Guid } else { $RowKey = $Task.RowKey } - # Check for duplicate RowKey (prevents race conditions) - $Filter = "PartitionKey eq 'ScheduledTask' and RowKey eq '$RowKey'" - $ExistingTaskByKey = (Get-CIPPAzDataTableEntity @Table -Filter $Filter) - if ($ExistingTaskByKey) { - return "Task with ID $RowKey already exists" - } - if ($DisallowDuplicateName) { $Filter = "PartitionKey eq 'ScheduledTask' and Name eq '$($Task.Name)'" $ExistingTask = (Get-CIPPAzDataTableEntity @Table -Filter $Filter) @@ -125,7 +117,6 @@ function Add-CIPPScheduledTask { $AdditionalProperties = ([PSCustomObject]$AdditionalProperties | ConvertTo-Json -Compress) if ($Parameters -eq 'null') { $Parameters = '' } - # RowKey already generated during duplicate check above $Recurrence = if ([string]::IsNullOrEmpty($task.Recurrence.value)) { $task.Recurrence From 5c96f7bec3ab5043923b088e0cba492d21714b72 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Tue, 6 Jan 2026 11:25:53 -0500 Subject: [PATCH 094/503] fix mx record alert --- .../Public/Alerts/Get-CIPPAlertMXRecordChanged.ps1 | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertMXRecordChanged.ps1 b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertMXRecordChanged.ps1 index 429f342e60fa..f2c268a4ed75 100644 --- a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertMXRecordChanged.ps1 +++ b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertMXRecordChanged.ps1 @@ -18,8 +18,10 @@ function Get-CIPPAlertMXRecordChanged { $ChangedDomains = foreach ($Domain in $DomainData) { $PreviousDomain = $PreviousResults | Where-Object { $_.Domain -eq $Domain.Domain } - if ($PreviousDomain -and $PreviousDomain.ActualMXRecords -ne $Domain.ActualMXRecords) { - "$($Domain.Domain): MX records changed from [$($PreviousDomain.ActualMXRecords -join ', ')] to [$($Domain.ActualMXRecords -join ', ')]" + $PreviousRecords = $PreviousDomain.ActualMXRecords -split ',' | Sort-Object + $CurrentRecords = $Domain.ActualMXRecords.Hostname | Sort-Object + if ($PreviousDomain -and $PreviousRecords -ne $CurrentRecords) { + "$($Domain.Domain): MX records changed from [$($PreviousRecords -join ', ')] to [$($CurrentRecords -join ', ')]" } } @@ -29,11 +31,12 @@ function Get-CIPPAlertMXRecordChanged { # Update cache with current data foreach ($Domain in $DomainData) { + $CurrentRecords = $Domain.ActualMXRecords.Hostname | Sort-Object $CacheEntity = @{ PartitionKey = [string]$TenantFilter RowKey = [string]$Domain.Domain Domain = [string]$Domain.Domain - ActualMXRecords = [string]$Domain.ActualMXRecords + ActualMXRecords = [string]($CurrentRecords -join ',') LastRefresh = [string]$Domain.LastRefresh MailProvider = [string]$Domain.MailProvider } From df1333369cc4145a7e0718a6cb59821ced1cb481 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Tue, 6 Jan 2026 16:27:57 -0500 Subject: [PATCH 095/503] Add SkipCache option to tenant group retrieval Introduces a -SkipCache switch to Get-TenantGroups and updates Invoke-ListTenantGroups to use it, allowing cache bypass for fresh data retrieval. Also improves Update-CIPPDynamicTenantGroups to handle multiple referenced tenant group IDs for 'in' and 'notin' operators, aggregating member IDs across groups. --- .../CIPP/Settings/Invoke-ListTenantGroups.ps1 | 2 +- .../Public/TenantGroups/Get-TenantGroups.ps1 | 9 ++++-- .../Update-CIPPDynamicTenantGroups.ps1 | 32 ++++++++++++++++--- 3 files changed, 34 insertions(+), 9 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ListTenantGroups.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ListTenantGroups.ps1 index 7de5c13c4ad9..4aae7a0858df 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ListTenantGroups.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ListTenantGroups.ps1 @@ -11,7 +11,7 @@ function Invoke-ListTenantGroups { param($Request, $TriggerMetadata) $groupFilter = $Request.Query.groupId ?? $Request.Body.groupId - $TenantGroups = (Get-TenantGroups -GroupId $groupFilter) ?? @() + $TenantGroups = (Get-TenantGroups -GroupId $groupFilter -SkipCache) ?? @() $Body = @{ Results = @($TenantGroups) } return ([HttpResponseContext]@{ diff --git a/Modules/CIPPCore/Public/TenantGroups/Get-TenantGroups.ps1 b/Modules/CIPPCore/Public/TenantGroups/Get-TenantGroups.ps1 index 5ac9ecb20621..76db653fa17f 100644 --- a/Modules/CIPPCore/Public/TenantGroups/Get-TenantGroups.ps1 +++ b/Modules/CIPPCore/Public/TenantGroups/Get-TenantGroups.ps1 @@ -30,11 +30,14 @@ function Get-TenantGroups { param( [string]$GroupId, [string]$TenantFilter, - [switch]$Dynamic + [switch]$Dynamic, + [switch]$SkipCache ) $CacheKey = "$GroupId|$TenantFilter|$($Dynamic.IsPresent)" - if ($script:TenantGroupsResultCache.ContainsKey($CacheKey)) { + if ($SkipCache) { + Write-Verbose "Skipping cache for: $CacheKey" + } elseif ($script:TenantGroupsResultCache.ContainsKey($CacheKey)) { Write-Verbose "Returning cached result for: $CacheKey" return $script:TenantGroupsResultCache[$CacheKey] } @@ -47,7 +50,7 @@ function Get-TenantGroups { } # Load table data into cache if not already loaded - if (-not $script:TenantGroupsCache.Groups -or -not $script:TenantGroupsCache.Members) { + if (-not $script:TenantGroupsCache.Groups -or -not $script:TenantGroupsCache.Members -or $SkipCache) { Write-Verbose 'Loading TenantGroups and TenantGroupMembers tables into cache' $GroupTable = Get-CippTable -tablename 'TenantGroups' diff --git a/Modules/CIPPCore/Public/TenantGroups/Update-CIPPDynamicTenantGroups.ps1 b/Modules/CIPPCore/Public/TenantGroups/Update-CIPPDynamicTenantGroups.ps1 index 8ea1a508d4c6..da149f6fc71f 100644 --- a/Modules/CIPPCore/Public/TenantGroups/Update-CIPPDynamicTenantGroups.ps1 +++ b/Modules/CIPPCore/Public/TenantGroups/Update-CIPPDynamicTenantGroups.ps1 @@ -96,12 +96,34 @@ function Update-CIPPDynamicTenantGroups { } } 'tenantGroupMember' { - # Get members of the referenced tenant group - $ReferencedGroupId = $Value.value - if ($Operator -eq 'in') { - "`$_.customerId -in `$script:TenantGroupMembersCache['$ReferencedGroupId']" + # Get members of the referenced tenant group(s) + if ($Operator -in @('in', 'notin')) { + # Handle array of group IDs + $ReferencedGroupIds = @($Value.value) + + # Collect all unique member customerIds from all referenced groups + $AllMembers = [System.Collections.Generic.HashSet[string]]::new() + foreach ($GroupId in $ReferencedGroupIds) { + if ($script:TenantGroupMembersCache.ContainsKey($GroupId)) { + foreach ($MemberId in $script:TenantGroupMembersCache[$GroupId]) { + [void]$AllMembers.Add($MemberId) + } + } + } + + # Convert to array string for condition + $MemberArray = $AllMembers | ForEach-Object { "'$_'" } + $MemberArrayString = $MemberArray -join ', ' + + if ($Operator -eq 'in') { + "`$_.customerId -in @($MemberArrayString)" + } else { + "`$_.customerId -notin @($MemberArrayString)" + } } else { - "`$_.customerId -notin `$script:TenantGroupMembersCache['$ReferencedGroupId']" + # Single value with other operators + $ReferencedGroupId = $Value.value + "`$_.customerId -$Operator `$script:TenantGroupMembersCache['$ReferencedGroupId']" } } 'customVariable' { From 1a89c01cee0dcde855968ce94fcb3d9cedeec43c Mon Sep 17 00:00:00 2001 From: John Duprey Date: Wed, 7 Jan 2026 13:43:30 -0500 Subject: [PATCH 096/503] Add IP range restrictions to roles and enforce in access checks Introduces IP range management for both custom and default roles, storing allowed IPs in a dedicated table. Updates role creation, cloning, deletion, and listing to handle IP ranges, and enforces IP-based access restrictions in Test-CIPPAccess. Superadmin roles are exempt from IP restrictions to prevent lockout. --- .../Authentication/Get-CIPPRoleIPRanges.ps1 | 51 +++++++++++++ .../Public/Authentication/Test-CIPPAccess.ps1 | 43 ++++++++++- .../CIPP/Settings/Invoke-ExecCustomRole.ps1 | 76 +++++++++++++++++++ .../CIPP/Settings/Invoke-ListCustomRole.ps1 | 30 +++++++- 4 files changed, 197 insertions(+), 3 deletions(-) create mode 100644 Modules/CIPPCore/Public/Authentication/Get-CIPPRoleIPRanges.ps1 diff --git a/Modules/CIPPCore/Public/Authentication/Get-CIPPRoleIPRanges.ps1 b/Modules/CIPPCore/Public/Authentication/Get-CIPPRoleIPRanges.ps1 new file mode 100644 index 000000000000..cd4745d5c1b4 --- /dev/null +++ b/Modules/CIPPCore/Public/Authentication/Get-CIPPRoleIPRanges.ps1 @@ -0,0 +1,51 @@ +function Get-CIPPRoleIPRanges { + <# + .SYNOPSIS + Gets combined IP ranges from a list of roles + .DESCRIPTION + This function retrieves IP range restrictions from custom roles and returns a consolidated list. + Superadmin roles are excluded from IP restrictions. + .PARAMETER Roles + Array of role names to check for IP restrictions + .FUNCTIONALITY + Internal + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [array]$Roles + ) + + $CombinedIPRanges = [System.Collections.Generic.List[string]]::new() + + # Superadmin is never restricted by IP + if ($Roles -contains 'superadmin') { + return @('Any') + } + + $AccessIPRangeTable = Get-CippTable -tablename 'AccessIPRanges' + + foreach ($Role in $Roles) { + try { + $IPRangeEntity = Get-CIPPAzDataTableEntity @AccessIPRangeTable -Filter "RowKey eq '$($Role.ToLower())'" + if ($IPRangeEntity -and $IPRangeEntity.IPRanges) { + $IPRanges = @($IPRangeEntity.IPRanges | ConvertFrom-Json) + foreach ($IPRange in $IPRanges) { + if ($IPRange -and -not $CombinedIPRanges.Contains($IPRange)) { + $CombinedIPRanges.Add($IPRange) + } + } + } + } catch { + Write-Information "Failed to get IP ranges for role '$Role': $($_.Exception.Message)" + continue + } + } + + # If no IP ranges were found in any role, allow all + if ($CombinedIPRanges.Count -eq 0) { + return @('Any') + } + + return @($CombinedIPRanges) | Sort-Object -Unique +} diff --git a/Modules/CIPPCore/Public/Authentication/Test-CIPPAccess.ps1 b/Modules/CIPPCore/Public/Authentication/Test-CIPPAccess.ps1 index 6a35cee4fa9e..7def9e058199 100644 --- a/Modules/CIPPCore/Public/Authentication/Test-CIPPAccess.ps1 +++ b/Modules/CIPPCore/Public/Authentication/Test-CIPPAccess.ps1 @@ -148,8 +148,35 @@ function Test-CIPPAccess { $AccessTimings['ResolveUserRoles'] = $swResolveUserRoles.Elapsed.TotalMilliseconds } - #Write-Information ($User | ConvertTo-Json -Depth 5) - # Return user permissions + $swIPCheck = [System.Diagnostics.Stopwatch]::StartNew() + $AllowedIPRanges = Get-CIPPRoleIPRanges -Roles $User.userRoles + + if ($AllowedIPRanges -notcontains 'Any') { + $ForwardedFor = $Request.Headers.'x-forwarded-for' -split ',' | Select-Object -First 1 + $IPRegex = '^(?(?:\d{1,3}(?:\.\d{1,3}){3}|\[[0-9a-fA-F:]+\]|[0-9a-fA-F:]+))(?::\d+)?$' + $IPAddress = $ForwardedFor -replace $IPRegex, '$1' -replace '[\[\]]', '' + if ($IPAddress) { + $IPAllowed = $false + foreach ($Range in $AllowedIPRanges) { + if ($IPAddress -eq $Range -or (Test-IpInRange -IPAddress $IPAddress -Range $Range)) { + $IPAllowed = $true + break + } + } + + if (-not $IPAllowed -and -not $Request.Params.CIPPEndpoint -eq 'me') { + throw "Access to this CIPP API endpoint is not allowed, your IP address ($IPAddress) is not in the allowed range for your role(s)" + } + } else { + $IPAllowed = $true + } + } else { + $IPAllowed = $true + } + + $swIPCheck.Stop() + $AccessTimings['IPRangeCheck'] = $swIPCheck.Elapsed.TotalMilliseconds + if ($Request.Params.CIPPEndpoint -eq 'me') { if (!$User.userRoles) { @@ -163,6 +190,18 @@ function Test-CIPPAccess { }) } + if (!$IPAllowed) { + return ([HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::OK + Body = ( + @{ + 'clientPrincipal' = $null + 'permissions' = @() + 'message' = "Your IP address ($IPAddress) is not in the allowed range for your role(s)" + } | ConvertTo-Json -Depth 5) + }) + } + $swPermsMe = [System.Diagnostics.Stopwatch]::StartNew() $Permissions = Get-CippAllowedPermissions -UserRoles $User.userRoles $swPermsMe.Stop() diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecCustomRole.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecCustomRole.ps1 index efd32dc5b5d4..dc3a91b75e05 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecCustomRole.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecCustomRole.ps1 @@ -10,6 +10,7 @@ function Invoke-ExecCustomRole { $Table = Get-CippTable -tablename 'CustomRoles' $AccessRoleGroupTable = Get-CippTable -tablename 'AccessRoleGroups' + $AccessIPRangeTable = Get-CippTable -tablename 'AccessIPRanges' $Action = $Request.Query.Action ?? $Request.Body.Action $CIPPCore = (Get-Module -Name CIPPCore).ModuleBase @@ -33,6 +34,20 @@ function Invoke-ExecCustomRole { try { $Results = [System.Collections.Generic.List[string]]::new() Write-LogMessage -headers $Request.Headers -API 'ExecCustomRole' -message "Saved custom role $($Request.Body.RoleName)" -Sev 'Info' + + # Process IP Range if provided (but not for superadmin to prevent lockout) + if ($Request.Body.IpRange -and $Request.Body.RoleName -ne 'superadmin') { + $IpRange = [System.Collections.Generic.List[string]]::new() + $regexPattern = '^(?:(?:[0-9]{1,3}\.){3}[0-9]{1,3}(?:/\d{1,2})?|(?:[0-9A-Fa-f]{1,4}:){1,7}[0-9A-Fa-f]{1,4}(?:/\d{1,3})?)$' + foreach ($IP in @($Request.Body.IpRange)) { + if ($IP -match $regexPattern) { + $IpRange.Add($IP) + } + } + } else { + $IpRange = @() + } + if ($Request.Body.RoleName -notin $DefaultRoles.PSObject.Properties.Name) { $Role = @{ 'PartitionKey' = 'CustomRoles' @@ -45,6 +60,28 @@ function Invoke-ExecCustomRole { Add-CIPPAzDataTableEntity @Table -Entity $Role -Force | Out-Null $Results.Add("Custom role $($Request.Body.RoleName) saved") } + if ($Request.Body.RoleName -eq 'superadmin' -and $Request.Body.IpRange) { + $Results.Add('Note: IP restrictions are not allowed on the superadmin role to prevent lockout issues.') + } + # Store IP ranges in separate table (works for both custom and default roles) + if ($IpRange.Count -gt 0 -and $Request.Body.RoleName -ne 'superadmin') { + $IPRangeEntity = @{ + 'PartitionKey' = 'AccessIPRanges' + 'RowKey' = "$($Request.Body.RoleName.ToLower())" + 'IPRanges' = "$(@($IpRange) | ConvertTo-Json -Compress)" + } + Add-CIPPAzDataTableEntity @AccessIPRangeTable -Entity $IPRangeEntity -Force | Out-Null + $Results.Add("IP ranges configured for '$($Request.Body.RoleName)' role.") + } else { + # Remove IP ranges if none provided or role is superadmin + $ExistingIPRange = Get-CIPPAzDataTableEntity @AccessIPRangeTable -Filter "RowKey eq '$($Request.Body.RoleName.ToLower())'" + if ($ExistingIPRange) { + Remove-AzDataTableEntity -Force @AccessIPRangeTable -Entity $ExistingIPRange + if ($Request.Body.RoleName -ne 'superadmin') { + $Results.Add("IP ranges removed from '$($Request.Body.RoleName)' role.") + } + } + } if ($Request.Body.EntraGroup) { $RoleGroup = @{ 'PartitionKey' = 'AccessRoleGroups' @@ -98,6 +135,16 @@ function Invoke-ExecCustomRole { 'BlockedEndpoints' = $ExistingRole.BlockedEndpoints } Add-CIPPAzDataTableEntity @Table -Entity $NewRole -Force | Out-Null + # Clone IP ranges if they exist + $ExistingIPRange = Get-CIPPAzDataTableEntity @AccessIPRangeTable -Filter "RowKey eq '$($Request.Body.RoleName.ToLower())'" + if ($ExistingIPRange) { + $NewIPRangeEntity = @{ + 'PartitionKey' = 'AccessIPRanges' + 'RowKey' = "$($Request.Body.NewRoleName.ToLower())" + 'IPRanges' = $ExistingIPRange.IPRanges + } + Add-CIPPAzDataTableEntity @AccessIPRangeTable -Entity $NewIPRangeEntity -Force | Out-Null + } $Body = @{Results = "Custom role '$($Request.Body.NewRoleName)' cloned from '$($Request.Body.RoleName)'" } Write-LogMessage -headers $Request.Headers -API 'ExecCustomRole' -message "Cloned custom role $($Request.Body.RoleName) to $($Request.Body.NewRoleName)" -Sev 'Info' } catch { @@ -114,6 +161,10 @@ function Invoke-ExecCustomRole { if ($AccessRoleGroup) { Remove-AzDataTableEntity -Force @AccessRoleGroupTable -Entity $AccessRoleGroup } + $AccessIPRange = Get-CIPPAzDataTableEntity @AccessIPRangeTable -Filter "PartitionKey eq 'AccessIPRanges' and RowKey eq '$($Request.Body.RoleName)'" + if ($AccessIPRange) { + Remove-AzDataTableEntity -Force @AccessIPRangeTable -Entity $AccessIPRange + } $Body = @{Results = 'Custom role deleted' } Write-LogMessage -headers $Request.Headers -API 'ExecCustomRole' -message "Deleted custom role $($Request.Body.RoleName)" -Sev 'Info' } @@ -129,6 +180,7 @@ function Invoke-ExecCustomRole { default { $Body = Get-CIPPAzDataTableEntity @Table $EntraRoleGroups = Get-CIPPAzDataTableEntity @AccessRoleGroupTable + $AccessIPRanges = Get-CIPPAzDataTableEntity @AccessIPRangeTable if (!$Body) { $Body = @( @{ @@ -175,6 +227,18 @@ function Invoke-ExecCustomRole { $Role | Add-Member -NotePropertyName EntraGroup -NotePropertyValue $EntraGroup -Force } + # Load IP ranges from separate table + $IPRangeEntity = $AccessIPRanges | Where-Object -Property RowKey -EQ $Role.RowKey + if ($IPRangeEntity) { + try { + $IPRanges = @($IPRangeEntity.IPRanges | ConvertFrom-Json) + } catch { + $IPRanges = @() + } + $Role | Add-Member -NotePropertyName IPRange -NotePropertyValue $IPRanges -Force + } else { + $Role | Add-Member -NotePropertyName IPRange -NotePropertyValue @() -Force + } $Role } $DefaultRoles = foreach ($DefaultRole in $DefaultRoles.PSObject.Properties.Name) { @@ -189,6 +253,18 @@ function Invoke-ExecCustomRole { if ($EntraRoleGroup) { $Role.EntraGroup = $EntraRoleGroups | Where-Object -Property RowKey -EQ $Role.RowKey | Select-Object @{Name = 'label'; Expression = { $_.GroupName } }, @{Name = 'value'; Expression = { $_.GroupId } } } + # Load IP ranges from separate table + $IPRangeEntity = $AccessIPRanges | Where-Object -Property RowKey -EQ $DefaultRole + if ($IPRangeEntity) { + try { + $IPRanges = @($IPRangeEntity.IPRanges | ConvertFrom-Json) + } catch { + $IPRanges = @() + } + $Role.IPRange = $IPRanges + } else { + $Role.IPRange = @() + } $Role } $Body = @($DefaultRoles + $CustomRoles) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ListCustomRole.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ListCustomRole.ps1 index 0fac4036a55f..3c59304b3771 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ListCustomRole.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ListCustomRole.ps1 @@ -13,13 +13,27 @@ function Invoke-ListCustomRole { $AccessRoleGroupTable = Get-CippTable -tablename 'AccessRoleGroups' $RoleGroups = Get-CIPPAzDataTableEntity @AccessRoleGroupTable + + $AccessIPRangeTable = Get-CippTable -tablename 'AccessIPRanges' + $AccessIPRanges = Get-CIPPAzDataTableEntity @AccessIPRangeTable $TenantList = Get-Tenants -IncludeErrors $RoleList = [System.Collections.Generic.List[pscustomobject]]::new() foreach ($Role in $DefaultRoles) { $RoleGroup = $RoleGroups | Where-Object -Property RowKey -EQ $Role - + + $IPRangeEntity = $AccessIPRanges | Where-Object -Property RowKey -EQ $Role + if ($IPRangeEntity) { + try { + $IPRanges = @($IPRangeEntity.IPRanges | ConvertFrom-Json) + } catch { + $IPRanges = @() + } + } else { + $IPRanges = @() + } + $RoleList.Add([pscustomobject]@{ RoleName = $Role Type = 'Built-In' @@ -28,6 +42,7 @@ function Invoke-ListCustomRole { BlockedTenants = @() EntraGroup = $RoleGroup.GroupName ?? $null EntraGroupId = $RoleGroup.GroupId ?? $null + IPRange = $IPRanges }) } foreach ($Role in $CustomRoles) { @@ -129,3 +144,16 @@ function Invoke-ListCustomRole { Body = ConvertTo-Json -InputObject $Body -Depth 5 }) } + + $IPRangeEntity = $AccessIPRanges | Where-Object -Property RowKey -EQ $Role.RowKey + if ($IPRangeEntity) { + try { + $IPRanges = @($IPRangeEntity.IPRanges | ConvertFrom-Json) + } catch { + $IPRanges = @() + } + $Role | Add-Member -NotePropertyName IPRange -NotePropertyValue $IPRanges -Force + } else { + $Role | Add-Member -NotePropertyName IPRange -NotePropertyValue @() -Force + } + From cd60ad2770d0f831a9b1241fff54fe19c306fe80 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Wed, 7 Jan 2026 13:43:39 -0500 Subject: [PATCH 097/503] Update Update-CIPPDynamicTenantGroups.ps1 --- .../Public/TenantGroups/Update-CIPPDynamicTenantGroups.ps1 | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/TenantGroups/Update-CIPPDynamicTenantGroups.ps1 b/Modules/CIPPCore/Public/TenantGroups/Update-CIPPDynamicTenantGroups.ps1 index da149f6fc71f..9cbc794e05fd 100644 --- a/Modules/CIPPCore/Public/TenantGroups/Update-CIPPDynamicTenantGroups.ps1 +++ b/Modules/CIPPCore/Public/TenantGroups/Update-CIPPDynamicTenantGroups.ps1 @@ -53,7 +53,9 @@ function Update-CIPPDynamicTenantGroups { $script:TenantGroupMembersCache[$Member.GroupId] = [system.collections.generic.list[string]]::new() } $script:TenantGroupMembersCache[$Member.GroupId].Add($Member.customerId) - } foreach ($Group in $DynamicGroups) { + } + + foreach ($Group in $DynamicGroups) { try { Write-LogMessage -API 'TenantGroups' -message "Processing dynamic group: $($Group.Name)" -sev Info $Rules = @($Group.DynamicRules | ConvertFrom-Json) From 72065dac609a6777426f3d8872e296330406ddf8 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Wed, 7 Jan 2026 15:18:39 -0500 Subject: [PATCH 098/503] fix group type in edituser --- .../Identity/Administration/Users/Invoke-EditUser.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-EditUser.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-EditUser.ps1 index acda628e0a00..2ffcc5041778 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-EditUser.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-EditUser.ps1 @@ -172,7 +172,7 @@ function Invoke-EditUser { if ($AddToGroups) { $AddToGroups | ForEach-Object { - $GroupType = $_.addedFields.calculatedGroupType + $GroupType = $_.addedFields.groupType $GroupID = $_.value $GroupName = $_.label Write-Host "About to add $($UserObj.userPrincipalName) to $GroupName. Group ID is: $GroupID and type is: $GroupType" @@ -204,7 +204,7 @@ function Invoke-EditUser { if ($RemoveFromGroups) { $RemoveFromGroups | ForEach-Object { - $GroupType = $_.addedFields.calculatedGroupType + $GroupType = $_.addedFields.groupType $GroupID = $_.value $GroupName = $_.label Write-Host "About to remove $($UserObj.userPrincipalName) from $GroupName. Group ID is: $GroupID and type is: $GroupType" From 37387c71b47225b2d7c62be1ab500b217abe1a88 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Fri, 9 Jan 2026 00:19:20 +0800 Subject: [PATCH 099/503] Optimize MFA state retrieval and policy mapping Refactored Get-CIPPMFAState to improve performance by indexing MFA registration data and mapping conditional access policies to users more efficiently. Updated API queries to select only necessary fields and replaced repeated filtering with hash table lookups. --- Modules/CIPPCore/Public/Get-CIPPMFAState.ps1 | 50 ++++++++++---------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/Modules/CIPPCore/Public/Get-CIPPMFAState.ps1 b/Modules/CIPPCore/Public/Get-CIPPMFAState.ps1 index 651261d3bdfb..6f5eef720383 100644 --- a/Modules/CIPPCore/Public/Get-CIPPMFAState.ps1 +++ b/Modules/CIPPCore/Public/Get-CIPPMFAState.ps1 @@ -1,4 +1,3 @@ - function Get-CIPPMFAState { [CmdletBinding()] param ( @@ -10,7 +9,7 @@ function Get-CIPPMFAState { $users = foreach ($user in (New-GraphGetRequest -uri 'https://graph.microsoft.com/v1.0/users?$top=999&$select=id,UserPrincipalName,DisplayName,accountEnabled,assignedLicenses,perUserMfaState' -tenantid $TenantFilter)) { [PSCustomObject]@{ UserPrincipalName = $user.UserPrincipalName - isLicensed = [boolean]$user.assignedLicenses.skuid + isLicensed = [boolean]$user.assignedLicenses.Count accountEnabled = $user.accountEnabled DisplayName = $user.DisplayName ObjectId = $user.id @@ -28,7 +27,11 @@ function Get-CIPPMFAState { $CAState = [System.Collections.Generic.List[object]]::new() Try { - $MFARegistration = (New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/reports/authenticationMethods/userRegistrationDetails' -tenantid $TenantFilter -asapp $true) + $MFARegistration = (New-GraphGetRequest -uri "https://graph.microsoft.com/beta/reports/authenticationMethods/userRegistrationDetails?$top=999&$select=userPrincipalName,isMfaRegistered,isMfaCapable,methodsRegistered" -tenantid $TenantFilter -asapp $true) + $MFAIndex = @{} + foreach ($MFAEntry in $MFARegistration) { + $MFAIndex[$MFAEntry.userPrincipalName] = $MFAEntry + } } catch { $CAState.Add('Not Licensed for Conditional Access') | Out-Null $MFARegistration = $null @@ -36,29 +39,22 @@ function Get-CIPPMFAState { $Errors.Add(@{Step = 'MFARegistration'; Message = $_.Exception.Message }) } Write-Host "User registration details not available: $($_.Exception.Message)" + $MFAIndex = @{} } if ($null -ne $MFARegistration) { $CASuccess = $true try { - $CAPolicies = (New-GraphGetRequest -Uri 'https://graph.microsoft.com/beta/identity/conditionalAccess/policies?$top=999' -tenantid $TenantFilter -ErrorAction Stop ) + $CAPolicies = (New-GraphGetRequest -Uri 'https://graph.microsoft.com/beta/identity/conditionalAccess/policies?$top=999&$filter=state eq 'enabled'&$select=id,displayName,state,grantControls,conditions' -tenantid $TenantFilter -ErrorAction Stop) + $PolicyTable = @{} foreach ($Policy in $CAPolicies) { - $IsMFAControl = $policy.grantControls.builtincontrols -eq 'mfa' -or $Policy.grantControls.authenticationStrength.requirementsSatisfied -eq 'mfa' -or $Policy.grantControls.customAuthenticationFactors -eq 'RequireDuoMfa' - $IsAllApps = [bool]($Policy.conditions.applications.includeApplications -eq 'All') - $IsAllUsers = [bool]($Policy.conditions.users.includeUsers -eq 'All') - $Platforms = $Policy.conditions.clientAppTypes - - if ($IsMFAControl) { - $CAState.Add([PSCustomObject]@{ - DisplayName = $Policy.displayName - State = $Policy.state - IncludedApps = $Policy.conditions.applications.includeApplications - IncludedUsers = $Policy.conditions.users.includeUsers - ExcludedUsers = $Policy.conditions.users.excludeUsers - IsAllApps = $IsAllApps - IsAllUsers = $IsAllUsers - Platforms = $Platforms - }) + if ($Policy.conditions.users.includeUsers -ne $null) { + foreach ($UserId in $Policy.conditions.users.includeUsers) { + if (-not $PolicyTable.ContainsKey($UserId)) { + $PolicyTable[$UserId] = [System.Collections.Generic.List[object]]::new() + } + $PolicyTable[$UserId].Add($Policy) + } } } } catch { @@ -69,7 +65,7 @@ function Get-CIPPMFAState { } if ($CAState.count -eq 0) { $CAState.Add('None') | Out-Null } - + $assignments = New-GraphGetRequest -uri "https://graph.microsoft.com/v1.0/roleManagement/directory/roleAssignments?`$expand=principal" -tenantid $TenantFilter -ErrorAction SilentlyContinue $adminObjectIds = $assignments | @@ -111,7 +107,11 @@ function Get-CIPPMFAState { $PerUser = $_.PerUserMFAState - $MFARegUser = if ($null -eq ($MFARegistration | Where-Object -Property UserPrincipalName -EQ $_.userPrincipalName).isMFARegistered) { $false } else { ($MFARegistration | Where-Object -Property UserPrincipalName -EQ $_.userPrincipalName) } + $MFARegUser = if ($null -eq ($MFAIndex[$_.UserPrincipalName])) { + $false + } else { + $MFAIndex[$_.UserPrincipalName] + } [PSCustomObject]@{ Tenant = $TenantFilter @@ -121,9 +121,9 @@ function Get-CIPPMFAState { AccountEnabled = $_.accountEnabled PerUser = $PerUser isLicensed = $_.isLicensed - MFARegistration = $MFARegUser.isMFARegistered - MFACapable = $MFARegUser.isMFACapable - MFAMethods = $MFARegUser.methodsRegistered + MFARegistration = if ($MFARegUser) { $MFARegUser.isMfaRegistered } else { $false } + MFACapable = if ($MFARegUser) { $MFARegUser.isMfaCapable } else { $false } + MFAMethods = if ($MFARegUser) { $MFARegUser.methodsRegistered } else { @() } CoveredByCA = $CoveredByCA CAPolicies = $UserCAState CoveredBySD = $SecureDefaultsState From beecc6e2f607be03e64d560dda71f58382327c4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Thu, 8 Jan 2026 17:21:36 +0100 Subject: [PATCH 100/503] Fix: Sort group members and owners by displayName --- .../Identity/Administration/Groups/Invoke-ListGroups.ps1 | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-ListGroups.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-ListGroups.ps1 index 75f51bee09ae..fc66527eb142 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-ListGroups.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-ListGroups.ps1 @@ -15,7 +15,7 @@ function Invoke-ListGroups { $ExpandMembers = $Request.Query.expandMembers ?? $false - $SelectString = 'id,createdDateTime,displayName,description,mail,mailEnabled,mailNickname,resourceProvisioningOptions,securityEnabled,visibility,organizationId,onPremisesSamAccountName,membershipRule,groupTypes,onPremisesSyncEnabled,resourceProvisioningOptions,userPrincipalName' + $SelectString = 'id,createdDateTime,displayName,description,mail,mailEnabled,mailNickname,resourceProvisioningOptions,securityEnabled,visibility,organizationId,onPremisesSamAccountName,membershipRule,groupTypes,onPremisesSyncEnabled,resourceProvisioningOptions,assignedLicenses,userPrincipalName' if ($ExpandMembers -ne $false) { $SelectString = '{0}&$expand=members($select=userPrincipalName)' -f $SelectString } @@ -24,7 +24,7 @@ function Invoke-ListGroups { $BulkRequestArrayList = [System.Collections.Generic.List[object]]::new() if ($Request.Query.GroupID) { - $SelectString = 'id,createdDateTime,displayName,description,mail,mailEnabled,mailNickname,resourceProvisioningOptions,securityEnabled,visibility,organizationId,onPremisesSamAccountName,membershipRule,groupTypes,userPrincipalName,onPremisesSyncEnabled' + $SelectString = 'id,createdDateTime,displayName,description,mail,mailEnabled,mailNickname,resourceProvisioningOptions,securityEnabled,visibility,organizationId,onPremisesSamAccountName,membershipRule,groupTypes,assignedLicenses,userPrincipalName,onPremisesSyncEnabled' $BulkRequestArrayList.add(@{ id = 1 method = 'GET' @@ -102,8 +102,8 @@ function Invoke-ListGroups { } }, @{Name = 'dynamicGroupBool'; Expression = { if ($_.groupTypes -contains 'DynamicMembership') { $true } else { $false } } } - members = ($RawGraphRequest | Where-Object { $_.id -eq 2 }).body.value - owners = ($RawGraphRequest | Where-Object { $_.id -eq 3 }).body.value + members = ($RawGraphRequest | Where-Object { $_.id -eq 2 }).body.value | Sort-Object displayName + owners = ($RawGraphRequest | Where-Object { $_.id -eq 3 }).body.value | Sort-Object displayName allowExternal = (!$OnlyAllowInternal) sendCopies = $SendCopies hideFromOutlookClients = if ($GroupType -eq 'Microsoft 365') { $UnifiedGroupInfo.HiddenFromExchangeClientsEnabled } else { $null } From de57f93c4ee743bd1520ed0d94408db9afbfe823 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 9 Jan 2026 10:42:05 -0500 Subject: [PATCH 101/503] Update Invoke-ExecUniversalSearch.ps1 --- .../Public/Entrypoints/Invoke-ExecUniversalSearch.ps1 | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/Invoke-ExecUniversalSearch.ps1 b/Modules/CIPPCore/Public/Entrypoints/Invoke-ExecUniversalSearch.ps1 index 44f8009edea1..a984cada9ddc 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Invoke-ExecUniversalSearch.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Invoke-ExecUniversalSearch.ps1 @@ -1,7 +1,7 @@ -Function Invoke-ExecUniversalSearch { +function Invoke-ExecUniversalSearch { <# .FUNCTIONALITY - Entrypoint + Entrypoint,AnyTenant .ROLE CIPP.Core.Read #> @@ -41,8 +41,8 @@ Function Invoke-ExecUniversalSearch { $GraphRequest = "Could not connect to Azure Lighthouse API: $($ErrorMessage)" } return [HttpResponseContext]@{ - StatusCode = $StatusCode - Body = @($GraphRequest) - } + StatusCode = $StatusCode + Body = @($GraphRequest) + } } From 3542562e42e97d9182310229e0207aea6a49cccc Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Sun, 11 Jan 2026 11:33:40 +0100 Subject: [PATCH 102/503] EIDSCA tests --- .../Identity/Invoke-CippTestEIDSCA_AF01.ps1 | 47 ++++++++++++++++ .../Identity/Invoke-CippTestEIDSCA_AF02.ps1 | 47 ++++++++++++++++ .../Identity/Invoke-CippTestEIDSCA_AF03.ps1 | 47 ++++++++++++++++ .../Identity/Invoke-CippTestEIDSCA_AF04.ps1 | 47 ++++++++++++++++ .../Identity/Invoke-CippTestEIDSCA_AF05.ps1 | 48 ++++++++++++++++ .../Identity/Invoke-CippTestEIDSCA_AF06.ps1 | 52 +++++++++++++++++ .../Identity/Invoke-CippTestEIDSCA_AG01.ps1 | 42 ++++++++++++++ .../Identity/Invoke-CippTestEIDSCA_AG02.ps1 | 42 ++++++++++++++ .../Identity/Invoke-CippTestEIDSCA_AG03.ps1 | 42 ++++++++++++++ .../Identity/Invoke-CippTestEIDSCA_AM01.ps1 | 32 +++++++++++ .../Identity/Invoke-CippTestEIDSCA_AM02.ps1 | 32 +++++++++++ .../Identity/Invoke-CippTestEIDSCA_AM03.ps1 | 32 +++++++++++ .../Identity/Invoke-CippTestEIDSCA_AM04.ps1 | 32 +++++++++++ .../Identity/Invoke-CippTestEIDSCA_AM06.ps1 | 32 +++++++++++ .../Identity/Invoke-CippTestEIDSCA_AM07.ps1 | 32 +++++++++++ .../Identity/Invoke-CippTestEIDSCA_AM09.ps1 | 33 +++++++++++ .../Identity/Invoke-CippTestEIDSCA_AM10.ps1 | 32 +++++++++++ .../Identity/Invoke-CippTestEIDSCA_AP01.ps1 | 42 ++++++++++++++ .../Identity/Invoke-CippTestEIDSCA_AP04.ps1 | 42 ++++++++++++++ .../Identity/Invoke-CippTestEIDSCA_AP05.ps1 | 42 ++++++++++++++ .../Identity/Invoke-CippTestEIDSCA_AP06.ps1 | 42 ++++++++++++++ .../Identity/Invoke-CippTestEIDSCA_AP07.ps1 | 43 ++++++++++++++ .../Identity/Invoke-CippTestEIDSCA_AP08.ps1 | 43 ++++++++++++++ .../Identity/Invoke-CippTestEIDSCA_AP09.ps1 | 42 ++++++++++++++ .../Identity/Invoke-CippTestEIDSCA_AP10.ps1 | 42 ++++++++++++++ .../Identity/Invoke-CippTestEIDSCA_AP14.ps1 | 42 ++++++++++++++ .../Identity/Invoke-CippTestEIDSCA_AS04.ps1 | 56 +++++++++++++++++++ .../Identity/Invoke-CippTestEIDSCA_AT01.ps1 | 47 ++++++++++++++++ .../Identity/Invoke-CippTestEIDSCA_AT02.ps1 | 47 ++++++++++++++++ .../Identity/Invoke-CippTestEIDSCA_AV01.ps1 | 47 ++++++++++++++++ .../Identity/Invoke-CippTestEIDSCA_CP01.ps1 | 40 +++++++++++++ .../Identity/Invoke-CippTestEIDSCA_CP03.ps1 | 40 +++++++++++++ .../Identity/Invoke-CippTestEIDSCA_CP04.ps1 | 40 +++++++++++++ .../Identity/Invoke-CippTestEIDSCA_CR01.ps1 | 40 +++++++++++++ .../Identity/Invoke-CippTestEIDSCA_CR02.ps1 | 40 +++++++++++++ .../Identity/Invoke-CippTestEIDSCA_CR03.ps1 | 40 +++++++++++++ .../Identity/Invoke-CippTestEIDSCA_CR04.ps1 | 42 ++++++++++++++ .../Identity/Invoke-CippTestEIDSCA_PR01.ps1 | 40 +++++++++++++ .../Identity/Invoke-CippTestEIDSCA_PR02.ps1 | 40 +++++++++++++ .../Identity/Invoke-CippTestEIDSCA_PR03.ps1 | 40 +++++++++++++ .../Identity/Invoke-CippTestEIDSCA_PR05.ps1 | 40 +++++++++++++ .../Identity/Invoke-CippTestEIDSCA_PR06.ps1 | 40 +++++++++++++ .../Identity/Invoke-CippTestEIDSCA_ST08.ps1 | 40 +++++++++++++ .../Identity/Invoke-CippTestEIDSCA_ST09.ps1 | 40 +++++++++++++ .../CIPPCore/Public/Tests/EIDSCA/report.json | 53 ++++++++++++++++++ node_modules/.yarn-integrity | 10 ---- 46 files changed, 1863 insertions(+), 10 deletions(-) create mode 100644 Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF01.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF02.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF03.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF04.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF05.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF06.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AG01.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AG02.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AG03.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM01.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM02.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM03.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM04.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM06.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM07.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM09.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM10.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP01.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP04.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP05.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP06.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP07.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP08.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP09.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP10.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP14.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AS04.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AT01.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AT02.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AV01.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CP01.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CP03.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CP04.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR01.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR02.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR03.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR04.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR01.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR02.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR03.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR05.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR06.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_ST08.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_ST09.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/EIDSCA/report.json delete mode 100644 node_modules/.yarn-integrity diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF01.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF01.ps1 new file mode 100644 index 000000000000..04c53778574d --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF01.ps1 @@ -0,0 +1,47 @@ +function Invoke-CippTestEIDSCA_AF01 { + <# + .SYNOPSIS + FIDO2 - State + #> + param($Tenant) + + try { + $AuthMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' + + if (-not $AuthMethodsPolicy) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AF01' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Low' -Name 'EIDSCA.AF01: FIDO2 - State' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Authentication Methods' + return + } + + $Fido2Config = $AuthMethodsPolicy.authenticationMethodConfigurations | Where-Object { $_.id -eq 'Fido2' } + + if (-not $Fido2Config) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AF01' -TestType 'Identity' -Status 'Failed' -ResultMarkdown 'FIDO2 configuration not found in Authentication Methods Policy.' -Risk 'Low' -Name 'EIDSCA.AF01: FIDO2 - State' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Authentication Methods' + return + } + + if ($Fido2Config.state -eq 'enabled') { + $Status = 'Passed' + $Result = 'FIDO2 authentication method is enabled' + } else { + $Status = 'Failed' + $Result = @" +FIDO2 security keys should be enabled to provide strong, phishing-resistant authentication. + +**Current Configuration:** +- State: $($Fido2Config.state) + +**Recommended Configuration:** +- State: enabled + +Enabling FIDO2 provides users with a secure, passwordless authentication option. +"@ + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AF01' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Low' -Name 'EIDSCA.AF01: FIDO2 - State' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Authentication Methods' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AF01' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Low' -Name 'EIDSCA.AF01: FIDO2 - State' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Authentication Methods' + } +} diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF02.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF02.ps1 new file mode 100644 index 000000000000..5b0bd6f6f701 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF02.ps1 @@ -0,0 +1,47 @@ +function Invoke-CippTestEIDSCA_AF02 { + <# + .SYNOPSIS + FIDO2 - Self-Service + #> + param($Tenant) + + try { + $AuthMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' + + if (-not $AuthMethodsPolicy) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AF02' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Low' -Name 'EIDSCA.AF02: FIDO2 - Self-Service' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + return + } + + $Fido2Config = $AuthMethodsPolicy.authenticationMethodConfigurations | Where-Object { $_.id -eq 'Fido2' } + + if (-not $Fido2Config) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AF02' -TestType 'Identity' -Status 'Failed' -ResultMarkdown 'FIDO2 configuration not found in Authentication Methods Policy.' -Risk 'Low' -Name 'EIDSCA.AF02: FIDO2 - Self-Service' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + return + } + + if ($Fido2Config.isSelfServiceRegistrationAllowed -eq $true) { + $Status = 'Passed' + $Result = 'FIDO2 self-service registration is enabled' + } else { + $Status = 'Failed' + $Result = @" +FIDO2 self-service registration should be enabled to allow users to register their own security keys. + +**Current Configuration:** +- isSelfServiceRegistrationAllowed: $($Fido2Config.isSelfServiceRegistrationAllowed) + +**Recommended Configuration:** +- isSelfServiceRegistrationAllowed: true + +Enabling self-service registration improves user experience and adoption of FIDO2 security keys. +"@ + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AF02' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Low' -Name 'EIDSCA.AF02: FIDO2 - Self-Service' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AF02' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Low' -Name 'EIDSCA.AF02: FIDO2 - Self-Service' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + } +} diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF03.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF03.ps1 new file mode 100644 index 000000000000..2036e2d99873 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF03.ps1 @@ -0,0 +1,47 @@ +function Invoke-CippTestEIDSCA_AF03 { + <# + .SYNOPSIS + FIDO2 - Attestation + #> + param($Tenant) + + try { + $AuthMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' + + if (-not $AuthMethodsPolicy) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AF03' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'EIDSCA.AF03: FIDO2 - Attestation' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Authentication Methods' + return + } + + $Fido2Config = $AuthMethodsPolicy.authenticationMethodConfigurations | Where-Object { $_.id -eq 'Fido2' } + + if (-not $Fido2Config) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AF03' -TestType 'Identity' -Status 'Failed' -ResultMarkdown 'FIDO2 configuration not found in Authentication Methods Policy.' -Risk 'Medium' -Name 'EIDSCA.AF03: FIDO2 - Attestation' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Authentication Methods' + return + } + + if ($Fido2Config.isAttestationEnforced -eq $true) { + $Status = 'Passed' + $Result = 'FIDO2 attestation enforcement is enabled' + } else { + $Status = 'Failed' + $Result = @" +FIDO2 attestation should be enforced to verify the authenticity and security of FIDO2 security keys. + +**Current Configuration:** +- isAttestationEnforced: $($Fido2Config.isAttestationEnforced) + +**Recommended Configuration:** +- isAttestationEnforced: true + +Enforcing attestation ensures that only trusted and verified security keys can be registered. +"@ + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AF03' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'EIDSCA.AF03: FIDO2 - Attestation' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Authentication Methods' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AF03' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'EIDSCA.AF03: FIDO2 - Attestation' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Authentication Methods' + } +} diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF04.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF04.ps1 new file mode 100644 index 000000000000..a323bf955a1b --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF04.ps1 @@ -0,0 +1,47 @@ +function Invoke-CippTestEIDSCA_AF04 { + <# + .SYNOPSIS + FIDO2 - Key Restrictions + #> + param($Tenant) + + try { + $AuthMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' + + if (-not $AuthMethodsPolicy) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AF04' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'EIDSCA.AF04: FIDO2 - Key Restrictions' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Authentication Methods' + return + } + + $Fido2Config = $AuthMethodsPolicy.authenticationMethodConfigurations | Where-Object { $_.id -eq 'Fido2' } + + if (-not $Fido2Config) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AF04' -TestType 'Identity' -Status 'Failed' -ResultMarkdown 'FIDO2 configuration not found in Authentication Methods Policy.' -Risk 'Medium' -Name 'EIDSCA.AF04: FIDO2 - Key Restrictions' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Authentication Methods' + return + } + + if ($Fido2Config.keyRestrictions.isEnforced -eq $true) { + $Status = 'Passed' + $Result = 'FIDO2 key restrictions are enforced' + } else { + $Status = 'Failed' + $Result = @" +FIDO2 key restrictions should be enforced to control which security keys can be registered. + +**Current Configuration:** +- keyRestrictions.isEnforced: $($Fido2Config.keyRestrictions.isEnforced) + +**Recommended Configuration:** +- keyRestrictions.isEnforced: true + +Enforcing key restrictions helps ensure only approved security keys are used in your organization. +"@ + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AF04' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'EIDSCA.AF04: FIDO2 - Key Restrictions' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Authentication Methods' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AF04' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'EIDSCA.AF04: FIDO2 - Key Restrictions' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Authentication Methods' + } +} diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF05.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF05.ps1 new file mode 100644 index 000000000000..3ce66189a41b --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF05.ps1 @@ -0,0 +1,48 @@ +function Invoke-CippTestEIDSCA_AF05 { + <# + .SYNOPSIS + FIDO2 - Restricted Keys + #> + param($Tenant) + + try { + $AuthMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' + + if (-not $AuthMethodsPolicy) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AF05' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'EIDSCA.AF05: FIDO2 - Restricted Keys' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Authentication Methods' + return + } + + $Fido2Config = $AuthMethodsPolicy.authenticationMethodConfigurations | Where-Object { $_.id -eq 'Fido2' } + + if (-not $Fido2Config) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AF05' -TestType 'Identity' -Status 'Failed' -ResultMarkdown 'FIDO2 configuration not found in Authentication Methods Policy.' -Risk 'Medium' -Name 'EIDSCA.AF05: FIDO2 - Restricted Keys' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Authentication Methods' + return + } + + $aaGuids = $Fido2Config.keyRestrictions.aaGuids + if ($aaGuids -and $aaGuids.Count -gt 0) { + $Status = 'Passed' + $Result = "FIDO2 key restrictions have specific AAGuids configured ($($aaGuids.Count) GUIDs)" + } else { + $Status = 'Failed' + $Result = @" +FIDO2 key restrictions should specify AAGuids to control which authenticator models can be registered. + +**Current Configuration:** +- keyRestrictions.aaGuids: Empty or not configured + +**Recommended Configuration:** +- keyRestrictions.aaGuids: Should contain one or more AAGuids + +Specifying AAGuids allows you to restrict registration to specific, approved security key models. +"@ + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AF05' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'EIDSCA.AF05: FIDO2 - Restricted Keys' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Authentication Methods' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AF05' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'EIDSCA.AF05: FIDO2 - Restricted Keys' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Authentication Methods' + } +} diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF06.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF06.ps1 new file mode 100644 index 000000000000..1089abf10567 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF06.ps1 @@ -0,0 +1,52 @@ +function Invoke-CippTestEIDSCA_AF06 { + <# + .SYNOPSIS + FIDO2 - Specific Keys + #> + param($Tenant) + + try { + $AuthMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' + + if (-not $AuthMethodsPolicy) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AF06' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'EIDSCA.AF06: FIDO2 - Specific Keys' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Authentication Methods' + return + } + + $Fido2Config = $AuthMethodsPolicy.authenticationMethodConfigurations | Where-Object { $_.id -eq 'Fido2' } + + if (-not $Fido2Config) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AF06' -TestType 'Identity' -Status 'Failed' -ResultMarkdown 'FIDO2 configuration not found in Authentication Methods Policy.' -Risk 'Medium' -Name 'EIDSCA.AF06: FIDO2 - Specific Keys' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Authentication Methods' + return + } + + $aaGuids = $Fido2Config.keyRestrictions.aaGuids + $enforcementType = $Fido2Config.keyRestrictions.enforcementType + + if ($aaGuids -and $aaGuids.Count -gt 0 -and $enforcementType -in @('allow', 'block')) { + $Status = 'Passed' + $Result = "FIDO2 key restrictions are properly configured with enforcement type '$enforcementType' and $($aaGuids.Count) AAGuids" + } else { + $Status = 'Failed' + $Result = @" +FIDO2 key restrictions should have both AAGuids configured and a valid enforcement type (allow or block). + +**Current Configuration:** +- keyRestrictions.aaGuids: $($aaGuids.Count) GUIDs configured +- keyRestrictions.enforcementType: $enforcementType + +**Recommended Configuration:** +- keyRestrictions.aaGuids: One or more AAGuids +- keyRestrictions.enforcementType: 'allow' or 'block' + +Proper enforcement type ensures the AAGuids list is actively used to control key registration. +"@ + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AF06' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'EIDSCA.AF06: FIDO2 - Specific Keys' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Authentication Methods' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AF06' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'EIDSCA.AF06: FIDO2 - Specific Keys' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Authentication Methods' + } +} diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AG01.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AG01.ps1 new file mode 100644 index 000000000000..662367060748 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AG01.ps1 @@ -0,0 +1,42 @@ +function Invoke-CippTestEIDSCA_AG01 { + <# + .SYNOPSIS + Authentication Method - General Settings - Manage migration + #> + param($Tenant) + + try { + $AuthMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' + + if (-not $AuthMethodsPolicy) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AG01' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'EIDSCA.AG01: Authentication Methods - Policy Migration' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + return + } + + $MigrationState = $AuthMethodsPolicy.policyMigrationState + + if ($MigrationState -in @('migrationComplete', '')) { + $Status = 'Passed' + $Result = "Policy migration is complete or not applicable: $MigrationState" + } else { + $Status = 'Failed' + $Result = @" +The authentication methods policy migration should be complete. + +**Current Configuration:** +- policyMigrationState: $MigrationState + +**Recommended Configuration:** +- policyMigrationState: migrationComplete or empty + +Complete the migration to use the modern authentication methods policy. +"@ + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AG01' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'EIDSCA.AG01: Authentication Methods - Policy Migration' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AG01' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'EIDSCA.AG01: Authentication Methods - Policy Migration' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + } +} diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AG02.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AG02.ps1 new file mode 100644 index 000000000000..6366436772ff --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AG02.ps1 @@ -0,0 +1,42 @@ +function Invoke-CippTestEIDSCA_AG02 { + <# + .SYNOPSIS + Authentication Method - General Settings - Report suspicious activity State + #> + param($Tenant) + + try { + $AuthMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' + + if (-not $AuthMethodsPolicy) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AG02' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'EIDSCA.AG02: Authentication Methods - Report Suspicious Activity' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + return + } + + $SuspiciousActivityState = $AuthMethodsPolicy.reportSuspiciousActivitySettings.state + + if ($SuspiciousActivityState -eq 'enabled') { + $Status = 'Passed' + $Result = 'Report suspicious activity is enabled' + } else { + $Status = 'Failed' + $Result = @" +Report suspicious activity should be enabled to allow users to report fraudulent MFA attempts. + +**Current Configuration:** +- reportSuspiciousActivitySettings.state: $SuspiciousActivityState + +**Recommended Configuration:** +- reportSuspiciousActivitySettings.state: enabled + +This feature helps detect and prevent unauthorized access attempts. +"@ + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AG02' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'EIDSCA.AG02: Authentication Methods - Report Suspicious Activity' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AG02' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'EIDSCA.AG02: Authentication Methods - Report Suspicious Activity' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + } +} diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AG03.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AG03.ps1 new file mode 100644 index 000000000000..22f50b8c2a0f --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AG03.ps1 @@ -0,0 +1,42 @@ +function Invoke-CippTestEIDSCA_AG03 { + <# + .SYNOPSIS + Authentication Method - General Settings - Report suspicious activity users/groups + #> + param($Tenant) + + try { + $AuthMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' + + if (-not $AuthMethodsPolicy) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AG03' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'EIDSCA.AG03: Authentication Methods - Suspicious Activity Target' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + return + } + + $IncludeTargetId = $AuthMethodsPolicy.reportSuspiciousActivitySettings.includeTarget.id + + if ($IncludeTargetId -eq 'all_users') { + $Status = 'Passed' + $Result = 'Report suspicious activity is enabled for all users' + } else { + $Status = 'Failed' + $Result = @" +Report suspicious activity should be enabled for all users. + +**Current Configuration:** +- reportSuspiciousActivitySettings.includeTarget.id: $IncludeTargetId + +**Recommended Configuration:** +- reportSuspiciousActivitySettings.includeTarget.id: all_users + +All users should be able to report suspicious authentication attempts. +"@ + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AG03' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'EIDSCA.AG03: Authentication Methods - Suspicious Activity Target' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AG03' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'EIDSCA.AG03: Authentication Methods - Suspicious Activity Target' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + } +} diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM01.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM01.ps1 new file mode 100644 index 000000000000..7432fafbf3c7 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM01.ps1 @@ -0,0 +1,32 @@ +function Invoke-CippTestEIDSCA_AM01 { + <# + .SYNOPSIS + Checks if Microsoft Authenticator authentication method is enabled + #> + param($Tenant) + + try { + $AuthMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' + + if (-not $AuthMethodsPolicy) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AM01' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'EIDSCA.AM01: MS Authenticator - State' -UserImpact 'High' -ImplementationEffort 'Low' -Category 'Authentication Methods' + return + } + + $MethodConfig = $AuthMethodsPolicy.authenticationMethodConfigurations | Where-Object { $_.id -eq 'MicrosoftAuthenticator' } + + if ($MethodConfig.state -eq 'enabled') { + $Status = 'Pass' + $Result = 'Microsoft Authenticator authentication method is enabled.' + } else { + $Status = 'Fail' + $Result = "Microsoft Authenticator authentication method is not enabled. Current state: $($MethodConfig.state)" + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AM01' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'EIDSCA.AM01: MS Authenticator - State' -UserImpact 'High' -ImplementationEffort 'Low' -Category 'Authentication Methods' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AM01' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'EIDSCA.AM01: MS Authenticator - State' -UserImpact 'High' -ImplementationEffort 'Low' -Category 'Authentication Methods' + } +} diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM02.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM02.ps1 new file mode 100644 index 000000000000..21081028b020 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM02.ps1 @@ -0,0 +1,32 @@ +function Invoke-CippTestEIDSCA_AM02 { + <# + .SYNOPSIS + Checks if Microsoft Authenticator software OATH is disabled + #> + param($Tenant) + + try { + $AuthMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' + + if (-not $AuthMethodsPolicy) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AM02' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'EIDSCA.AM02: MS Authenticator - OTP Disabled' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authentication Methods' + return + } + + $MethodConfig = $AuthMethodsPolicy.authenticationMethodConfigurations | Where-Object { $_.id -eq 'MicrosoftAuthenticator' } + + if ($MethodConfig.isSoftwareOathEnabled -eq $false) { + $Status = 'Pass' + $Result = 'Microsoft Authenticator software OATH is disabled.' + } else { + $Status = 'Fail' + $Result = "Microsoft Authenticator software OATH is enabled. It should be disabled." + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AM02' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'EIDSCA.AM02: MS Authenticator - OTP Disabled' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authentication Methods' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AM02' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'EIDSCA.AM02: MS Authenticator - OTP Disabled' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authentication Methods' + } +} diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM03.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM03.ps1 new file mode 100644 index 000000000000..8cc830b14674 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM03.ps1 @@ -0,0 +1,32 @@ +function Invoke-CippTestEIDSCA_AM03 { + <# + .SYNOPSIS + Checks if Microsoft Authenticator number matching is enabled + #> + param($Tenant) + + try { + $AuthMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' + + if (-not $AuthMethodsPolicy) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AM03' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'EIDSCA.AM03: MS Authenticator - Number Matching' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authentication Methods' + return + } + + $MethodConfig = $AuthMethodsPolicy.authenticationMethodConfigurations | Where-Object { $_.id -eq 'MicrosoftAuthenticator' } + + if ($MethodConfig.featureSettings.numberMatchingRequiredState.state -eq 'enabled') { + $Status = 'Pass' + $Result = 'Microsoft Authenticator number matching is enabled.' + } else { + $Status = 'Fail' + $Result = "Microsoft Authenticator number matching is not enabled. Current state: $($MethodConfig.featureSettings.numberMatchingRequiredState.state)" + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AM03' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'EIDSCA.AM03: MS Authenticator - Number Matching' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authentication Methods' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AM03' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'EIDSCA.AM03: MS Authenticator - Number Matching' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authentication Methods' + } +} diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM04.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM04.ps1 new file mode 100644 index 000000000000..d4d1527f4cd8 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM04.ps1 @@ -0,0 +1,32 @@ +function Invoke-CippTestEIDSCA_AM04 { + <# + .SYNOPSIS + Checks if Microsoft Authenticator number matching targets all users + #> + param($Tenant) + + try { + $AuthMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' + + if (-not $AuthMethodsPolicy) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AM04' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'EIDSCA.AM04: MS Authenticator - Number Matching Target' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + return + } + + $MethodConfig = $AuthMethodsPolicy.authenticationMethodConfigurations | Where-Object { $_.id -eq 'MicrosoftAuthenticator' } + + if ($MethodConfig.featureSettings.numberMatchingRequiredState.includeTarget.id -eq 'all_users') { + $Status = 'Pass' + $Result = 'Microsoft Authenticator number matching targets all users.' + } else { + $Status = 'Fail' + $Result = "Microsoft Authenticator number matching does not target all users. Current target: $($MethodConfig.featureSettings.numberMatchingRequiredState.includeTarget.id)" + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AM04' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'EIDSCA.AM04: MS Authenticator - Number Matching Target' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AM04' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'EIDSCA.AM04: MS Authenticator - Number Matching Target' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + } +} diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM06.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM06.ps1 new file mode 100644 index 000000000000..6126c9a4b2ae --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM06.ps1 @@ -0,0 +1,32 @@ +function Invoke-CippTestEIDSCA_AM06 { + <# + .SYNOPSIS + Checks if Microsoft Authenticator app information display is enabled + #> + param($Tenant) + + try { + $AuthMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' + + if (-not $AuthMethodsPolicy) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AM06' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'EIDSCA.AM06: MS Authenticator - Show App Name' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + return + } + + $MethodConfig = $AuthMethodsPolicy.authenticationMethodConfigurations | Where-Object { $_.id -eq 'MicrosoftAuthenticator' } + + if ($MethodConfig.featureSettings.displayAppInformationRequiredState.state -eq 'enabled') { + $Status = 'Pass' + $Result = 'Microsoft Authenticator app information display is enabled.' + } else { + $Status = 'Fail' + $Result = "Microsoft Authenticator app information display is not enabled. Current state: $($MethodConfig.featureSettings.displayAppInformationRequiredState.state)" + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AM06' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'EIDSCA.AM06: MS Authenticator - Show App Name' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AM06' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'EIDSCA.AM06: MS Authenticator - Show App Name' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + } +} diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM07.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM07.ps1 new file mode 100644 index 000000000000..abbe3eae3864 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM07.ps1 @@ -0,0 +1,32 @@ +function Invoke-CippTestEIDSCA_AM07 { + <# + .SYNOPSIS + Checks if Microsoft Authenticator app information display targets all users + #> + param($Tenant) + + try { + $AuthMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' + + if (-not $AuthMethodsPolicy) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AM07' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'EIDSCA.AM07: MS Authenticator - Show App Name Target' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + return + } + + $MethodConfig = $AuthMethodsPolicy.authenticationMethodConfigurations | Where-Object { $_.id -eq 'MicrosoftAuthenticator' } + + if ($MethodConfig.featureSettings.displayAppInformationRequiredState.includeTarget.id -eq 'all_users') { + $Status = 'Pass' + $Result = 'Microsoft Authenticator app information display targets all users.' + } else { + $Status = 'Fail' + $Result = "Microsoft Authenticator app information display does not target all users. Current target: $($MethodConfig.featureSettings.displayAppInformationRequiredState.includeTarget.id)" + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AM07' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'EIDSCA.AM07: MS Authenticator - Show App Name Target' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AM07' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'EIDSCA.AM07: MS Authenticator - Show App Name Target' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + } +} diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM09.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM09.ps1 new file mode 100644 index 000000000000..d12332bade23 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM09.ps1 @@ -0,0 +1,33 @@ +function Invoke-CippTestEIDSCA_AM09 { + <# + .SYNOPSIS + Checks if Microsoft Authenticator location information display is enabled + #> + param($Tenant) + + try { + $AuthMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' + + if (-not $AuthMethodsPolicy) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AM09' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'EIDSCA.AM09: MS Authenticator - Show Location' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + return + } + + $MethodConfig = $AuthMethodsPolicy.authenticationMethodConfigurations | Where-Object { $_.id -eq 'MicrosoftAuthenticator' } + + if ($MethodConfig.featureSettings.displayLocationInformationRequiredState.state -eq 'enabled') { + $Status = 'Pass' + $Result = 'Microsoft Authenticator location information display is enabled.' + } else { + $Status = 'Fail' + $Result = "Microsoft Authenticator location information display is not enabled. Current state: $($MethodConfig.featureSettings.displayLocationInformationRequiredState.state)" + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AM09' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'EIDSCA.AM09: MS Authenticator - Show Location' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AM09' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'EIDSCA.AM09: MS Authenticator - Show Location' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + } +} + diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM10.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM10.ps1 new file mode 100644 index 000000000000..0271bf0934e5 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM10.ps1 @@ -0,0 +1,32 @@ +function Invoke-CippTestEIDSCA_AM10 { + <# + .SYNOPSIS + Checks if Microsoft Authenticator location information display targets all users + #> + param($Tenant) + + try { + $AuthMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' + + if (-not $AuthMethodsPolicy) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AM10' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'EIDSCA.AM10: MS Authenticator - Show Location Target' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + return + } + + $MethodConfig = $AuthMethodsPolicy.authenticationMethodConfigurations | Where-Object { $_.id -eq 'MicrosoftAuthenticator' } + + if ($MethodConfig.featureSettings.displayLocationInformationRequiredState.includeTarget.id -eq 'all_users') { + $Status = 'Pass' + $Result = 'Microsoft Authenticator location information display targets all users.' + } else { + $Status = 'Fail' + $Result = "Microsoft Authenticator location information display does not target all users. Current target: $($MethodConfig.featureSettings.displayLocationInformationRequiredState.includeTarget.id)" + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AM10' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'EIDSCA.AM10: MS Authenticator - Show Location Target' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AM10' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'EIDSCA.AM10: MS Authenticator - Show Location Target' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + } +} diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP01.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP01.ps1 new file mode 100644 index 000000000000..1817bcad5a56 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP01.ps1 @@ -0,0 +1,42 @@ +function Invoke-CippTestEIDSCA_AP01 { + <# + .SYNOPSIS + Default Authorization Settings - Enabled Self service password reset for administrators + #> + param($Tenant) + + try { + $AuthorizationPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthorizationPolicy' + + if (-not $AuthorizationPolicy) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AP01' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'EIDSCA.AP01: Authorization Policy - Self-Service Password Reset for Admins' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authorization Policy' + return + } + + $AllowedToUseSSPR = $AuthorizationPolicy.allowedToUseSSPR + + if ($AllowedToUseSSPR -eq $false) { + $Status = 'Passed' + $Result = 'Self-service password reset for administrators is disabled' + } else { + $Status = 'Failed' + $Result = @" +Self-service password reset for administrators should be disabled for enhanced security. + +**Current Configuration:** +- allowedToUseSSPR: $AllowedToUseSSPR + +**Recommended Configuration:** +- allowedToUseSSPR: false + +Administrators should follow more stringent password reset procedures rather than self-service options. +"@ + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AP01' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'EIDSCA.AP01: Authorization Policy - Self-Service Password Reset for Admins' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authorization Policy' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AP01' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'EIDSCA.AP01: Authorization Policy - Self-Service Password Reset for Admins' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authorization Policy' + } +} diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP04.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP04.ps1 new file mode 100644 index 000000000000..dea3b98cb36b --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP04.ps1 @@ -0,0 +1,42 @@ +function Invoke-CippTestEIDSCA_AP04 { + <# + .SYNOPSIS + Default Authorization Settings - Guest invite restrictions + #> + param($Tenant) + + try { + $AuthorizationPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthorizationPolicy' + + if (-not $AuthorizationPolicy) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AP04' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'EIDSCA.AP04: Authorization Policy - Guest Invite Restrictions' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authorization Policy' + return + } + + $AllowInvitesFrom = $AuthorizationPolicy.allowInvitesFrom + + if ($AllowInvitesFrom -in @('adminsAndGuestInviters', 'none')) { + $Status = 'Passed' + $Result = "Guest invite restrictions are properly configured: $AllowInvitesFrom" + } else { + $Status = 'Failed' + $Result = @" +Guest invite restrictions should be set to limit who can invite guests for enhanced security. + +**Current Configuration:** +- allowInvitesFrom: $AllowInvitesFrom + +**Recommended Configuration:** +- allowInvitesFrom: adminsAndGuestInviters OR none + +Restricting guest invitations helps maintain control over external access to your tenant. +"@ + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AP04' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'EIDSCA.AP04: Authorization Policy - Guest Invite Restrictions' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authorization Policy' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AP04' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'EIDSCA.AP04: Authorization Policy - Guest Invite Restrictions' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authorization Policy' + } +} diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP05.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP05.ps1 new file mode 100644 index 000000000000..e48a004c97f4 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP05.ps1 @@ -0,0 +1,42 @@ +function Invoke-CippTestEIDSCA_AP05 { + <# + .SYNOPSIS + Default Authorization Settings - Sign-up for email based subscription + #> + param($Tenant) + + try { + $AuthorizationPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthorizationPolicy' + + if (-not $AuthorizationPolicy) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AP05' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'EIDSCA.AP05: Authorization Policy - Email-Based Subscription Sign-up' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authorization Policy' + return + } + + $AllowedToSignUp = $AuthorizationPolicy.allowedToSignUpEmailBasedSubscriptions + + if ($AllowedToSignUp -eq $false) { + $Status = 'Passed' + $Result = 'Email-based subscription sign-up is disabled' + } else { + $Status = 'Failed' + $Result = @" +Email-based subscription sign-up should be disabled to prevent unauthorized subscriptions. + +**Current Configuration:** +- allowedToSignUpEmailBasedSubscriptions: $AllowedToSignUp + +**Recommended Configuration:** +- allowedToSignUpEmailBasedSubscriptions: false + +Disabling email-based subscriptions helps maintain control over tenant access. +"@ + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AP05' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'EIDSCA.AP05: Authorization Policy - Email-Based Subscription Sign-up' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authorization Policy' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AP05' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'EIDSCA.AP05: Authorization Policy - Email-Based Subscription Sign-up' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authorization Policy' + } +} diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP06.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP06.ps1 new file mode 100644 index 000000000000..bd2f29c3b8af --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP06.ps1 @@ -0,0 +1,42 @@ +function Invoke-CippTestEIDSCA_AP06 { + <# + .SYNOPSIS + Default Authorization Settings - User can join the tenant by email validation + #> + param($Tenant) + + try { + $AuthorizationPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthorizationPolicy' + + if (-not $AuthorizationPolicy) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AP06' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'EIDSCA.AP06: Authorization Policy - Email Validation Join' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authorization Policy' + return + } + + $AllowEmailVerified = $AuthorizationPolicy.allowEmailVerifiedUsersToJoinOrganization + + if ($AllowEmailVerified -eq $false) { + $Status = 'Passed' + $Result = 'Users cannot join the tenant by email validation' + } else { + $Status = 'Failed' + $Result = @" +Email-validated users should not be allowed to join the organization to prevent unauthorized access. + +**Current Configuration:** +- allowEmailVerifiedUsersToJoinOrganization: $AllowEmailVerified + +**Recommended Configuration:** +- allowEmailVerifiedUsersToJoinOrganization: false + +Disabling this feature prevents unauthorized users from self-registering into your tenant. +"@ + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AP06' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'EIDSCA.AP06: Authorization Policy - Email Validation Join' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authorization Policy' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AP06' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'EIDSCA.AP06: Authorization Policy - Email Validation Join' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authorization Policy' + } +} diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP07.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP07.ps1 new file mode 100644 index 000000000000..426e855505af --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP07.ps1 @@ -0,0 +1,43 @@ +function Invoke-CippTestEIDSCA_AP07 { + <# + .SYNOPSIS + Default Authorization Settings - Guest user access + #> + param($Tenant) + + try { + $AuthorizationPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthorizationPolicy' + + if (-not $AuthorizationPolicy) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AP07' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'EIDSCA.AP07: Authorization Policy - Guest User Access' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authorization Policy' + return + } + + $GuestUserRoleId = $AuthorizationPolicy.guestUserRoleId + $ExpectedRoleId = '2af84b1e-32c8-42b7-82bc-daa82404023b' + + if ($GuestUserRoleId -eq $ExpectedRoleId) { + $Status = 'Passed' + $Result = 'Guest user access is restricted (most restrictive)' + } else { + $Status = 'Failed' + $Result = @" +Guest user access should be set to the most restrictive level for enhanced security. + +**Current Configuration:** +- guestUserRoleId: $GuestUserRoleId + +**Recommended Configuration:** +- guestUserRoleId: $ExpectedRoleId (Most restrictive guest permissions) + +This setting limits what guest users can see and do in your directory. +"@ + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AP07' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'EIDSCA.AP07: Authorization Policy - Guest User Access' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authorization Policy' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AP07' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'EIDSCA.AP07: Authorization Policy - Guest User Access' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authorization Policy' + } +} diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP08.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP08.ps1 new file mode 100644 index 000000000000..201186288412 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP08.ps1 @@ -0,0 +1,43 @@ +function Invoke-CippTestEIDSCA_AP08 { + <# + .SYNOPSIS + Default Authorization Settings - User consent policy assigned for applications + #> + param($Tenant) + + try { + $AuthorizationPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthorizationPolicy' + + if (-not $AuthorizationPolicy) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AP08' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'EIDSCA.AP08: Authorization Policy - User Consent Policy' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authorization Policy' + return + } + + $ConsentPolicy = $AuthorizationPolicy.permissionGrantPolicyIdsAssignedToDefaultUserRole + $ExpectedPolicy = 'ManagePermissionGrantsForSelf.microsoft-user-default-low' + + if ($ConsentPolicy -contains $ExpectedPolicy) { + $Status = 'Passed' + $Result = 'User consent policy is set to low-risk permissions' + } else { + $Status = 'Failed' + $Result = @" +User consent policy should be configured to only allow consent for low-risk applications. + +**Current Configuration:** +- permissionGrantPolicyIdsAssignedToDefaultUserRole: $($ConsentPolicy -join ', ') + +**Recommended Configuration:** +- permissionGrantPolicyIdsAssignedToDefaultUserRole: $ExpectedPolicy + +This limits users to only consent to applications with low-risk permissions. +"@ + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AP08' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'EIDSCA.AP08: Authorization Policy - User Consent Policy' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authorization Policy' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AP08' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'EIDSCA.AP08: Authorization Policy - User Consent Policy' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authorization Policy' + } +} diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP09.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP09.ps1 new file mode 100644 index 000000000000..a0aca7f273b5 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP09.ps1 @@ -0,0 +1,42 @@ +function Invoke-CippTestEIDSCA_AP09 { + <# + .SYNOPSIS + Default Authorization Settings - Allow user consent on risk-based apps + #> + param($Tenant) + + try { + $AuthorizationPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthorizationPolicy' + + if (-not $AuthorizationPolicy) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AP09' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'EIDSCA.AP09: Authorization Policy - Consent for Risky Apps' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authorization Policy' + return + } + + $AllowConsentRiskyApps = $AuthorizationPolicy.allowUserConsentForRiskyApps + + if ($AllowConsentRiskyApps -eq $false) { + $Status = 'Passed' + $Result = 'User consent for risky apps is disabled' + } else { + $Status = 'Failed' + $Result = @" +User consent for risk-based apps should be disabled to prevent users from consenting to potentially malicious applications. + +**Current Configuration:** +- allowUserConsentForRiskyApps: $AllowConsentRiskyApps + +**Recommended Configuration:** +- allowUserConsentForRiskyApps: false + +Disabling this prevents users from consenting to apps identified as risky. +"@ + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AP09' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'EIDSCA.AP09: Authorization Policy - Consent for Risky Apps' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authorization Policy' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AP09' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'EIDSCA.AP09: Authorization Policy - Consent for Risky Apps' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authorization Policy' + } +} diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP10.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP10.ps1 new file mode 100644 index 000000000000..47771dc6a4ef --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP10.ps1 @@ -0,0 +1,42 @@ +function Invoke-CippTestEIDSCA_AP10 { + <# + .SYNOPSIS + Default Authorization Settings - Default User Role Permissions - Allowed to create Apps + #> + param($Tenant) + + try { + $AuthorizationPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthorizationPolicy' + + if (-not $AuthorizationPolicy) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AP10' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'EIDSCA.AP10: Authorization Policy - Users Can Create Apps' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authorization Policy' + return + } + + $AllowedToCreateApps = $AuthorizationPolicy.defaultUserRolePermissions.allowedToCreateApps + + if ($AllowedToCreateApps -eq $false) { + $Status = 'Passed' + $Result = 'Users cannot create application registrations' + } else { + $Status = 'Failed' + $Result = @" +Users should not be allowed to create application registrations by default to maintain control over applications. + +**Current Configuration:** +- defaultUserRolePermissions.allowedToCreateApps: $AllowedToCreateApps + +**Recommended Configuration:** +- defaultUserRolePermissions.allowedToCreateApps: false + +Only authorized users should be able to register applications in your tenant. +"@ + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AP10' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'EIDSCA.AP10: Authorization Policy - Users Can Create Apps' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authorization Policy' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AP10' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'EIDSCA.AP10: Authorization Policy - Users Can Create Apps' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authorization Policy' + } +} diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP14.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP14.ps1 new file mode 100644 index 000000000000..f05c9933958e --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP14.ps1 @@ -0,0 +1,42 @@ +function Invoke-CippTestEIDSCA_AP14 { + <# + .SYNOPSIS + Default Authorization Settings - Default User Role Permissions - Allowed to read other users + #> + param($Tenant) + + try { + $AuthorizationPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthorizationPolicy' + + if (-not $AuthorizationPolicy) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AP14' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Low' -Name 'EIDSCA.AP14: Authorization Policy - Users Can Read Other Users' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authorization Policy' + return + } + + $AllowedToReadOtherUsers = $AuthorizationPolicy.defaultUserRolePermissions.allowedToReadOtherUsers + + if ($AllowedToReadOtherUsers -eq $true) { + $Status = 'Passed' + $Result = 'Users can read other users (standard behavior for collaboration)' + } else { + $Status = 'Failed' + $Result = @" +Users should be allowed to read other users' basic profile information for collaboration purposes. + +**Current Configuration:** +- defaultUserRolePermissions.allowedToReadOtherUsers: $AllowedToReadOtherUsers + +**Recommended Configuration:** +- defaultUserRolePermissions.allowedToReadOtherUsers: true + +This setting enables basic collaboration features like Teams and SharePoint. +"@ + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AP14' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Low' -Name 'EIDSCA.AP14: Authorization Policy - Users Can Read Other Users' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authorization Policy' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AP14' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Low' -Name 'EIDSCA.AP14: Authorization Policy - Users Can Read Other Users' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authorization Policy' + } +} diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AS04.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AS04.ps1 new file mode 100644 index 000000000000..fc58fe113b94 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AS04.ps1 @@ -0,0 +1,56 @@ +function Invoke-CippTestEIDSCA_AS04 { + <# + .SYNOPSIS + SMS - No Sign-In + #> + param($Tenant) + + try { + $AuthMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' + + if (-not $AuthMethodsPolicy) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AS04' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'EIDSCA.AS04: SMS - No Sign-In' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authentication Methods' + return + } + + $SmsConfig = $AuthMethodsPolicy.authenticationMethodConfigurations | Where-Object { $_.id -eq 'Sms' } + + if (-not $SmsConfig) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AS04' -TestType 'Identity' -Status 'Failed' -ResultMarkdown 'SMS authentication configuration not found in Authentication Methods Policy.' -Risk 'High' -Name 'EIDSCA.AS04: SMS - No Sign-In' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authentication Methods' + return + } + + $InvalidTargets = @() + if ($SmsConfig.includeTargets) { + foreach ($target in $SmsConfig.includeTargets) { + if ($target.isUsableForSignIn -ne $false) { + $InvalidTargets += $target.id + } + } + } + + if ($InvalidTargets.Count -eq 0) { + $Status = 'Passed' + $Result = 'SMS authentication is not allowed for sign-in on any targets' + } else { + $Status = 'Failed' + $Result = @" +SMS should not be allowed for sign-in as it is vulnerable to SIM swap and interception attacks. SMS should only be used for MFA verification, not primary authentication. + +**Current Configuration:** +- Targets with sign-in enabled: $($InvalidTargets.Count) + +**Recommended Configuration:** +- All includeTargets should have isUsableForSignIn: false + +Disabling SMS for sign-in while keeping it for MFA provides better security. +"@ + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AS04' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'EIDSCA.AS04: SMS - No Sign-In' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authentication Methods' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AS04' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'EIDSCA.AS04: SMS - No Sign-In' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authentication Methods' + } +} diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AT01.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AT01.ps1 new file mode 100644 index 000000000000..500c3bb6dc26 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AT01.ps1 @@ -0,0 +1,47 @@ +function Invoke-CippTestEIDSCA_AT01 { + <# + .SYNOPSIS + Temp Access Pass - State + #> + param($Tenant) + + try { + $AuthMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' + + if (-not $AuthMethodsPolicy) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AT01' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'EIDSCA.AT01: Temp Access Pass - State' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + return + } + + $TAPConfig = $AuthMethodsPolicy.authenticationMethodConfigurations | Where-Object { $_.id -eq 'TemporaryAccessPass' } + + if (-not $TAPConfig) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AT01' -TestType 'Identity' -Status 'Failed' -ResultMarkdown 'Temporary Access Pass configuration not found in Authentication Methods Policy.' -Risk 'Medium' -Name 'EIDSCA.AT01: Temp Access Pass - State' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + return + } + + if ($TAPConfig.state -eq 'enabled') { + $Status = 'Passed' + $Result = 'Temporary Access Pass is enabled' + } else { + $Status = 'Failed' + $Result = @" +Temporary Access Pass should be enabled to facilitate secure onboarding of passwordless authentication methods. + +**Current Configuration:** +- State: $($TAPConfig.state) + +**Recommended Configuration:** +- State: enabled + +Enabling TAP allows administrators to securely onboard users to passwordless authentication. +"@ + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AT01' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'EIDSCA.AT01: Temp Access Pass - State' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AT01' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'EIDSCA.AT01: Temp Access Pass - State' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + } +} diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AT02.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AT02.ps1 new file mode 100644 index 000000000000..34cfe29861a5 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AT02.ps1 @@ -0,0 +1,47 @@ +function Invoke-CippTestEIDSCA_AT02 { + <# + .SYNOPSIS + Temp Access Pass - One-Time + #> + param($Tenant) + + try { + $AuthMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' + + if (-not $AuthMethodsPolicy) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AT02' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'EIDSCA.AT02: Temp Access Pass - One-Time' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + return + } + + $TAPConfig = $AuthMethodsPolicy.authenticationMethodConfigurations | Where-Object { $_.id -eq 'TemporaryAccessPass' } + + if (-not $TAPConfig) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AT02' -TestType 'Identity' -Status 'Failed' -ResultMarkdown 'Temporary Access Pass configuration not found in Authentication Methods Policy.' -Risk 'Medium' -Name 'EIDSCA.AT02: Temp Access Pass - One-Time' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + return + } + + if ($TAPConfig.isUsableOnce -eq $true) { + $Status = 'Passed' + $Result = 'Temporary Access Pass is configured for one-time use' + } else { + $Status = 'Failed' + $Result = @" +Temporary Access Pass should be configured for one-time use to minimize security risks. + +**Current Configuration:** +- isUsableOnce: $($TAPConfig.isUsableOnce) + +**Recommended Configuration:** +- isUsableOnce: true + +One-time use reduces the risk of TAP credential theft or misuse. +"@ + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AT02' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'EIDSCA.AT02: Temp Access Pass - One-Time' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AT02' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'EIDSCA.AT02: Temp Access Pass - One-Time' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + } +} diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AV01.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AV01.ps1 new file mode 100644 index 000000000000..e079a3c781c4 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AV01.ps1 @@ -0,0 +1,47 @@ +function Invoke-CippTestEIDSCA_AV01 { + <# + .SYNOPSIS + Voice Call - Disabled + #> + param($Tenant) + + try { + $AuthMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' + + if (-not $AuthMethodsPolicy) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AV01' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'EIDSCA.AV01: Voice Call - Disabled' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authentication Methods' + return + } + + $VoiceConfig = $AuthMethodsPolicy.authenticationMethodConfigurations | Where-Object { $_.id -eq 'Voice' } + + if (-not $VoiceConfig) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AV01' -TestType 'Identity' -Status 'Failed' -ResultMarkdown 'Voice authentication configuration not found in Authentication Methods Policy.' -Risk 'Medium' -Name 'EIDSCA.AV01: Voice Call - Disabled' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authentication Methods' + return + } + + if ($VoiceConfig.state -eq 'disabled') { + $Status = 'Passed' + $Result = 'Voice call authentication is disabled' + } else { + $Status = 'Failed' + $Result = @" +Voice call authentication should be disabled as it is susceptible to social engineering and SIM swap attacks. + +**Current Configuration:** +- State: $($VoiceConfig.state) + +**Recommended Configuration:** +- State: disabled + +Disabling voice calls reduces the attack surface by eliminating a less secure authentication method. +"@ + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AV01' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'EIDSCA.AV01: Voice Call - Disabled' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authentication Methods' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AV01' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'EIDSCA.AV01: Voice Call - Disabled' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authentication Methods' + } +} diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CP01.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CP01.ps1 new file mode 100644 index 000000000000..a85e0691a3a1 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CP01.ps1 @@ -0,0 +1,40 @@ +function Invoke-CippTestEIDSCA_CP01 { + <# + .SYNOPSIS + Consent Policy Settings - Group owner consent for apps accessing data + #> + param($Tenant) + + try { + $Settings = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Settings' + + if (-not $Settings) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.CP01' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'EIDSCA.CP01: Consent Policy Settings - Group owner consent for apps accessing data' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Consent Policy' + return + } + + $SettingValue = ($Settings.values | Where-Object { $_.name -eq 'EnableGroupSpecificConsent' }).value + + if ($SettingValue -eq 'False') { + $Status = 'Passed' + $Result = 'Group owner consent for apps is disabled' + } else { + $Status = 'Failed' + $Result = @" +Group owner consent should be disabled to prevent unauthorized app permissions. + +**Current Configuration:** +- EnableGroupSpecificConsent: $SettingValue + +**Recommended Configuration:** +- EnableGroupSpecificConsent: False +"@ + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.CP01' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'EIDSCA.CP01: Consent Policy Settings - Group owner consent for apps accessing data' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Consent Policy' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.CP01' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'EIDSCA.CP01: Consent Policy Settings - Group owner consent for apps accessing data' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Consent Policy' + } +} diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CP03.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CP03.ps1 new file mode 100644 index 000000000000..3e1dc20c51d1 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CP03.ps1 @@ -0,0 +1,40 @@ +function Invoke-CippTestEIDSCA_CP03 { + <# + .SYNOPSIS + Consent Policy Settings - Block user consent for risky apps + #> + param($Tenant) + + try { + $Settings = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Settings' + + if (-not $Settings) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.CP03' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'EIDSCA.CP03: Consent Policy Settings - Block user consent for risky apps' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Consent Policy' + return + } + + $SettingValue = ($Settings.values | Where-Object { $_.name -eq 'BlockUserConsentForRiskyApps' }).value + + if ($SettingValue -eq 'true') { + $Status = 'Passed' + $Result = 'User consent for risky apps is blocked' + } else { + $Status = 'Failed' + $Result = @" +User consent for risky apps should be blocked to prevent security risks. + +**Current Configuration:** +- BlockUserConsentForRiskyApps: $SettingValue + +**Recommended Configuration:** +- BlockUserConsentForRiskyApps: true +"@ + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.CP03' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'EIDSCA.CP03: Consent Policy Settings - Block user consent for risky apps' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Consent Policy' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.CP03' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'EIDSCA.CP03: Consent Policy Settings - Block user consent for risky apps' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Consent Policy' + } +} diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CP04.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CP04.ps1 new file mode 100644 index 000000000000..f4e13307f89c --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CP04.ps1 @@ -0,0 +1,40 @@ +function Invoke-CippTestEIDSCA_CP04 { + <# + .SYNOPSIS + Consent Policy Settings - Users can request admin consent + #> + param($Tenant) + + try { + $Settings = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Settings' + + if (-not $Settings) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.CP04' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'EIDSCA.CP04: Consent Policy Settings - Users can request admin consent' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Consent Policy' + return + } + + $SettingValue = ($Settings.values | Where-Object { $_.name -eq 'EnableAdminConsentRequests' }).value + + if ($SettingValue -eq 'true') { + $Status = 'Passed' + $Result = 'Users can request admin consent for apps' + } else { + $Status = 'Failed' + $Result = @" +Users should be able to request admin consent to enable proper app approval workflows. + +**Current Configuration:** +- EnableAdminConsentRequests: $SettingValue + +**Recommended Configuration:** +- EnableAdminConsentRequests: true +"@ + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.CP04' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'EIDSCA.CP04: Consent Policy Settings - Users can request admin consent' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Consent Policy' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.CP04' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'EIDSCA.CP04: Consent Policy Settings - Users can request admin consent' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Consent Policy' + } +} diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR01.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR01.ps1 new file mode 100644 index 000000000000..fd8ab7ca6d0f --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR01.ps1 @@ -0,0 +1,40 @@ +function Invoke-CippTestEIDSCA_CR01 { + <# + .SYNOPSIS + Admin Consent - Enabled + #> + param($Tenant) + + try { + $AdminConsentPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AdminConsentRequestPolicy' + + if (-not $AdminConsentPolicy) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.CR01' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'EIDSCA.CR01: Admin Consent - Enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Consent Policy' + return + } + + if ($AdminConsentPolicy.isEnabled -eq $true) { + $Status = 'Passed' + $Result = 'Admin consent request workflow is enabled' + } else { + $Status = 'Failed' + $Result = @" +Admin consent request workflow should be enabled to allow users to request administrator approval for applications. + +**Current Configuration:** +- isEnabled: $($AdminConsentPolicy.isEnabled) + +**Recommended Configuration:** +- isEnabled: true + +Enabling this workflow provides a secure process for users to request access to applications requiring admin consent. +"@ + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.CR01' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'EIDSCA.CR01: Admin Consent - Enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Consent Policy' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.CR01' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'EIDSCA.CR01: Admin Consent - Enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Consent Policy' + } +} diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR02.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR02.ps1 new file mode 100644 index 000000000000..a4847f42c38c --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR02.ps1 @@ -0,0 +1,40 @@ +function Invoke-CippTestEIDSCA_CR02 { + <# + .SYNOPSIS + Admin Consent - Notify Reviewers + #> + param($Tenant) + + try { + $AdminConsentPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AdminConsentRequestPolicy' + + if (-not $AdminConsentPolicy) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.CR02' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'EIDSCA.CR02: Admin Consent - Notify Reviewers' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Consent Policy' + return + } + + if ($AdminConsentPolicy.notifyReviewers -eq $true) { + $Status = 'Passed' + $Result = 'Admin consent reviewers are notified of new requests' + } else { + $Status = 'Failed' + $Result = @" +Admin consent reviewers should be notified when new consent requests are submitted. + +**Current Configuration:** +- notifyReviewers: $($AdminConsentPolicy.notifyReviewers) + +**Recommended Configuration:** +- notifyReviewers: true + +Enabling notifications ensures reviewers are promptly informed of pending consent requests. +"@ + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.CR02' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'EIDSCA.CR02: Admin Consent - Notify Reviewers' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Consent Policy' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.CR02' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'EIDSCA.CR02: Admin Consent - Notify Reviewers' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Consent Policy' + } +} diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR03.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR03.ps1 new file mode 100644 index 000000000000..c9b473b0680d --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR03.ps1 @@ -0,0 +1,40 @@ +function Invoke-CippTestEIDSCA_CR03 { + <# + .SYNOPSIS + Admin Consent - Reminders + #> + param($Tenant) + + try { + $AdminConsentPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AdminConsentRequestPolicy' + + if (-not $AdminConsentPolicy) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.CR03' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Low' -Name 'EIDSCA.CR03: Admin Consent - Reminders' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Consent Policy' + return + } + + if ($AdminConsentPolicy.remindersEnabled -eq $true) { + $Status = 'Passed' + $Result = 'Admin consent request reminders are enabled' + } else { + $Status = 'Failed' + $Result = @" +Admin consent request reminders should be enabled to ensure timely review of pending requests. + +**Current Configuration:** +- remindersEnabled: $($AdminConsentPolicy.remindersEnabled) + +**Recommended Configuration:** +- remindersEnabled: true + +Enabling reminders helps prevent consent requests from being overlooked or delayed. +"@ + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.CR03' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Low' -Name 'EIDSCA.CR03: Admin Consent - Reminders' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Consent Policy' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.CR03' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Low' -Name 'EIDSCA.CR03: Admin Consent - Reminders' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Consent Policy' + } +} diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR04.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR04.ps1 new file mode 100644 index 000000000000..356e6a3b31a0 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR04.ps1 @@ -0,0 +1,42 @@ +function Invoke-CippTestEIDSCA_CR04 { + <# + .SYNOPSIS + Admin Consent - Duration + #> + param($Tenant) + + try { + $AdminConsentPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AdminConsentRequestPolicy' + + if (-not $AdminConsentPolicy) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.CR04' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Low' -Name 'EIDSCA.CR04: Admin Consent - Duration' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Consent Policy' + return + } + + $RequestDuration = $AdminConsentPolicy.requestDurationInDays + + if ($RequestDuration -le 30) { + $Status = 'Passed' + $Result = "Admin consent request duration is set to $RequestDuration days (30 days or less)" + } else { + $Status = 'Failed' + $Result = @" +Admin consent request duration should be set to 30 days or less to ensure timely review. + +**Current Configuration:** +- requestDurationInDays: $RequestDuration + +**Recommended Configuration:** +- requestDurationInDays: 30 or less + +A shorter duration ensures consent requests are reviewed and processed in a timely manner. +"@ + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.CR04' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Low' -Name 'EIDSCA.CR04: Admin Consent - Duration' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Consent Policy' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.CR04' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Low' -Name 'EIDSCA.CR04: Admin Consent - Duration' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Consent Policy' + } +} diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR01.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR01.ps1 new file mode 100644 index 000000000000..c2fc06d1a5ad --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR01.ps1 @@ -0,0 +1,40 @@ +function Invoke-CippTestEIDSCA_PR01 { + <# + .SYNOPSIS + Password Rule Settings - Password Protection Mode + #> + param($Tenant) + + try { + $Settings = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Settings' + + if (-not $Settings) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.PR01' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'EIDSCA.PR01: Password Rule Settings - Password Protection Mode' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Password Policy' + return + } + + $SettingValue = ($Settings.values | Where-Object { $_.name -eq 'BannedPasswordCheckOnPremisesMode' }).value + + if ($SettingValue -eq 'Enforce') { + $Status = 'Passed' + $Result = 'Password protection mode is set to Enforce' + } else { + $Status = 'Failed' + $Result = @" +Password protection mode should be set to Enforce to prevent weak passwords. + +**Current Configuration:** +- BannedPasswordCheckOnPremisesMode: $SettingValue + +**Recommended Configuration:** +- BannedPasswordCheckOnPremisesMode: Enforce +"@ + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.PR01' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'EIDSCA.PR01: Password Rule Settings - Password Protection Mode' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Password Policy' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.PR01' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'EIDSCA.PR01: Password Rule Settings - Password Protection Mode' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Password Policy' + } +} diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR02.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR02.ps1 new file mode 100644 index 000000000000..c68580ba4498 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR02.ps1 @@ -0,0 +1,40 @@ +function Invoke-CippTestEIDSCA_PR02 { + <# + .SYNOPSIS + Password Rule Settings - Enable password protection on Windows Server Active Directory + #> + param($Tenant) + + try { + $Settings = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Settings' + + if (-not $Settings) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.PR02' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'EIDSCA.PR02: Password Rule Settings - Enable password protection on Windows Server Active Directory' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Password Policy' + return + } + + $SettingValue = ($Settings.values | Where-Object { $_.name -eq 'EnableBannedPasswordCheckOnPremises' }).value + + if ($SettingValue -eq 'True') { + $Status = 'Passed' + $Result = 'Password protection is enabled for on-premises Active Directory' + } else { + $Status = 'Failed' + $Result = @" +Password protection should be enabled for on-premises Active Directory to prevent weak passwords. + +**Current Configuration:** +- EnableBannedPasswordCheckOnPremises: $SettingValue + +**Recommended Configuration:** +- EnableBannedPasswordCheckOnPremises: True +"@ + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.PR02' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'EIDSCA.PR02: Password Rule Settings - Enable password protection on Windows Server Active Directory' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Password Policy' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.PR02' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'EIDSCA.PR02: Password Rule Settings - Enable password protection on Windows Server Active Directory' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Password Policy' + } +} diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR03.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR03.ps1 new file mode 100644 index 000000000000..e4e84f7d68c7 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR03.ps1 @@ -0,0 +1,40 @@ +function Invoke-CippTestEIDSCA_PR03 { + <# + .SYNOPSIS + Password Rule Settings - Enforce custom list + #> + param($Tenant) + + try { + $Settings = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Settings' + + if (-not $Settings) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.PR03' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'EIDSCA.PR03: Password Rule Settings - Enforce custom list' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Password Policy' + return + } + + $SettingValue = ($Settings.values | Where-Object { $_.name -eq 'EnableBannedPasswordCheck' }).value + + if ($SettingValue -eq 'True') { + $Status = 'Passed' + $Result = 'Custom banned password list is enforced' + } else { + $Status = 'Failed' + $Result = @" +Custom banned password list should be enforced to prevent common weak passwords. + +**Current Configuration:** +- EnableBannedPasswordCheck: $SettingValue + +**Recommended Configuration:** +- EnableBannedPasswordCheck: True +"@ + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.PR03' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'EIDSCA.PR03: Password Rule Settings - Enforce custom list' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Password Policy' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.PR03' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'EIDSCA.PR03: Password Rule Settings - Enforce custom list' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Password Policy' + } +} diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR05.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR05.ps1 new file mode 100644 index 000000000000..356b1a1436ac --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR05.ps1 @@ -0,0 +1,40 @@ +function Invoke-CippTestEIDSCA_PR05 { + <# + .SYNOPSIS + Password Rule Settings - Lockout duration in seconds + #> + param($Tenant) + + try { + $Settings = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Settings' + + if (-not $Settings) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.PR05' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'EIDSCA.PR05: Password Rule Settings - Lockout duration in seconds' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Password Policy' + return + } + + $SettingValue = ($Settings.values | Where-Object { $_.name -eq 'LockoutDurationInSeconds' }).value + + if ([int]$SettingValue -ge 60) { + $Status = 'Passed' + $Result = "Lockout duration is set to $SettingValue seconds (minimum 60 seconds required)" + } else { + $Status = 'Failed' + $Result = @" +Lockout duration should be at least 60 seconds to protect against brute force attacks. + +**Current Configuration:** +- LockoutDurationInSeconds: $SettingValue + +**Recommended Configuration:** +- LockoutDurationInSeconds: 60 or greater +"@ + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.PR05' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'EIDSCA.PR05: Password Rule Settings - Lockout duration in seconds' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Password Policy' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.PR05' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'EIDSCA.PR05: Password Rule Settings - Lockout duration in seconds' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Password Policy' + } +} diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR06.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR06.ps1 new file mode 100644 index 000000000000..38eb1d54af22 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR06.ps1 @@ -0,0 +1,40 @@ +function Invoke-CippTestEIDSCA_PR06 { + <# + .SYNOPSIS + Password Rule Settings - Lockout threshold + #> + param($Tenant) + + try { + $Settings = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Settings' + + if (-not $Settings) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.PR06' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'EIDSCA.PR06: Password Rule Settings - Lockout threshold' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Password Policy' + return + } + + $SettingValue = ($Settings.values | Where-Object { $_.name -eq 'LockoutThreshold' }).value + + if ([int]$SettingValue -le 10) { + $Status = 'Passed' + $Result = "Lockout threshold is set to $SettingValue failed attempts (maximum 10 attempts recommended)" + } else { + $Status = 'Failed' + $Result = @" +Lockout threshold should be 10 or fewer failed attempts to protect against brute force attacks. + +**Current Configuration:** +- LockoutThreshold: $SettingValue + +**Recommended Configuration:** +- LockoutThreshold: 10 or fewer +"@ + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.PR06' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'EIDSCA.PR06: Password Rule Settings - Lockout threshold' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Password Policy' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.PR06' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'EIDSCA.PR06: Password Rule Settings - Lockout threshold' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Password Policy' + } +} diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_ST08.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_ST08.ps1 new file mode 100644 index 000000000000..e3c46cf02bfc --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_ST08.ps1 @@ -0,0 +1,40 @@ +function Invoke-CippTestEIDSCA_ST08 { + <# + .SYNOPSIS + Classification and M365 Groups - Allow Guests to become Group Owner + #> + param($Tenant) + + try { + $Settings = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Settings' + + if (-not $Settings) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.ST08' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'EIDSCA.ST08: Classification and M365 Groups - Allow Guests to become Group Owner' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Group Settings' + return + } + + $SettingValue = ($Settings.values | Where-Object { $_.name -eq 'AllowGuestsToBeGroupOwner' }).value + + if ($SettingValue -eq 'false') { + $Status = 'Passed' + $Result = 'Guests are not allowed to become group owners' + } else { + $Status = 'Failed' + $Result = @" +Guests should not be allowed to become group owners to maintain proper access control. + +**Current Configuration:** +- AllowGuestsToBeGroupOwner: $SettingValue + +**Recommended Configuration:** +- AllowGuestsToBeGroupOwner: false +"@ + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.ST08' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'EIDSCA.ST08: Classification and M365 Groups - Allow Guests to become Group Owner' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Group Settings' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.ST08' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'EIDSCA.ST08: Classification and M365 Groups - Allow Guests to become Group Owner' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Group Settings' + } +} diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_ST09.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_ST09.ps1 new file mode 100644 index 000000000000..f7510ad2f60e --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_ST09.ps1 @@ -0,0 +1,40 @@ +function Invoke-CippTestEIDSCA_ST09 { + <# + .SYNOPSIS + Classification and M365 Groups - Allow Guests to have access to groups content + #> + param($Tenant) + + try { + $Settings = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Settings' + + if (-not $Settings) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.ST09' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Low' -Name 'EIDSCA.ST09: Classification and M365 Groups - Allow Guests to have access to groups content' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Group Settings' + return + } + + $SettingValue = ($Settings.values | Where-Object { $_.name -eq 'AllowGuestsToAccessGroups' }).value + + if ($SettingValue -eq 'True') { + $Status = 'Passed' + $Result = 'Guests are allowed to access groups content' + } else { + $Status = 'Failed' + $Result = @" +Guests should be allowed to access groups content for proper collaboration. + +**Current Configuration:** +- AllowGuestsToAccessGroups: $SettingValue + +**Recommended Configuration:** +- AllowGuestsToAccessGroups: True +"@ + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.ST09' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Low' -Name 'EIDSCA.ST09: Classification and M365 Groups - Allow Guests to have access to groups content' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Group Settings' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.ST09' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Low' -Name 'EIDSCA.ST09: Classification and M365 Groups - Allow Guests to have access to groups content' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Group Settings' + } +} diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/report.json b/Modules/CIPPCore/Public/Tests/EIDSCA/report.json new file mode 100644 index 000000000000..efec36e1280c --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/report.json @@ -0,0 +1,53 @@ +{ + "name": "Entra ID Security Configuration Analyzer (EIDSCA) Tests", + "description": "Comprehensive security assessment for Microsoft Entra ID (formerly Azure AD) covering authorization policies, authentication methods, consent policies, password policies, and group settings. Based on Microsoft's EIDSCA framework for identity security best practices.", + "version": "1.0", + "source": "https://github.com/maester365/maester", + "category": "Identity Security", + "IdentityTests": [ + "EIDSCA_AP01", + "EIDSCA_AP04", + "EIDSCA_AP05", + "EIDSCA_AP06", + "EIDSCA_AP07", + "EIDSCA_AP08", + "EIDSCA_AP09", + "EIDSCA_AP10", + "EIDSCA_AP14", + "EIDSCA_CP01", + "EIDSCA_CP03", + "EIDSCA_CP04", + "EIDSCA_PR01", + "EIDSCA_PR02", + "EIDSCA_PR03", + "EIDSCA_PR05", + "EIDSCA_PR06", + "EIDSCA_ST08", + "EIDSCA_ST09", + "EIDSCA_AG01", + "EIDSCA_AG02", + "EIDSCA_AG03", + "EIDSCA_AM01", + "EIDSCA_AM02", + "EIDSCA_AM03", + "EIDSCA_AM04", + "EIDSCA_AM06", + "EIDSCA_AM07", + "EIDSCA_AM09", + "EIDSCA_AM10", + "EIDSCA_AF01", + "EIDSCA_AF02", + "EIDSCA_AF03", + "EIDSCA_AF04", + "EIDSCA_AF05", + "EIDSCA_AF06", + "EIDSCA_AT01", + "EIDSCA_AT02", + "EIDSCA_AV01", + "EIDSCA_AS04", + "EIDSCA_CR01", + "EIDSCA_CR02", + "EIDSCA_CR03", + "EIDSCA_CR04" + ] +} diff --git a/node_modules/.yarn-integrity b/node_modules/.yarn-integrity deleted file mode 100644 index 9d4d6a804a9c..000000000000 --- a/node_modules/.yarn-integrity +++ /dev/null @@ -1,10 +0,0 @@ -{ - "systemParams": "win32-x64-127", - "modulesFolders": [], - "flags": [], - "linkedModules": [], - "topLevelPatterns": [], - "lockfileEntries": {}, - "files": [], - "artifacts": {} -} \ No newline at end of file From ae712bfe961b7f1d53ebf36d0bab56005481c5ac Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Sun, 11 Jan 2026 11:34:03 +0100 Subject: [PATCH 103/503] EIDSCA --- .../Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF01.ps1 | 2 +- .../Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF02.ps1 | 2 +- .../Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF03.ps1 | 2 +- .../Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF04.ps1 | 2 +- .../Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF05.ps1 | 2 +- .../Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF06.ps1 | 2 +- .../Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AG01.ps1 | 2 +- .../Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AG02.ps1 | 2 +- .../Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AG03.ps1 | 2 +- .../Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM01.ps1 | 4 ++-- .../Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM02.ps1 | 4 ++-- .../Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM03.ps1 | 4 ++-- .../Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM04.ps1 | 4 ++-- .../Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM06.ps1 | 4 ++-- .../Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM07.ps1 | 4 ++-- .../Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM10.ps1 | 4 ++-- .../Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP01.ps1 | 2 +- .../Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP04.ps1 | 2 +- .../Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP05.ps1 | 2 +- .../Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP06.ps1 | 2 +- .../Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP07.ps1 | 2 +- .../Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP08.ps1 | 2 +- .../Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP09.ps1 | 2 +- .../Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP10.ps1 | 2 +- .../Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP14.ps1 | 2 +- .../Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AT01.ps1 | 2 +- .../Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CP01.ps1 | 2 +- .../Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CP03.ps1 | 2 +- .../Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CP04.ps1 | 2 +- .../Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR01.ps1 | 2 +- .../Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR02.ps1 | 2 +- .../Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR03.ps1 | 2 +- .../Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR04.ps1 | 2 +- .../Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR02.ps1 | 2 +- .../Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR03.ps1 | 2 +- .../Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR05.ps1 | 2 +- .../Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR06.ps1 | 2 +- .../Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_ST08.ps1 | 2 +- .../Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_ST09.ps1 | 2 +- 39 files changed, 46 insertions(+), 46 deletions(-) diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF01.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF01.ps1 index 04c53778574d..3c49e4249089 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF01.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF01.ps1 @@ -4,7 +4,7 @@ function Invoke-CippTestEIDSCA_AF01 { FIDO2 - State #> param($Tenant) - + try { $AuthMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF02.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF02.ps1 index 5b0bd6f6f701..173ba9b78a8d 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF02.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF02.ps1 @@ -4,7 +4,7 @@ function Invoke-CippTestEIDSCA_AF02 { FIDO2 - Self-Service #> param($Tenant) - + try { $AuthMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF03.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF03.ps1 index 2036e2d99873..0b02f34c7cec 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF03.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF03.ps1 @@ -4,7 +4,7 @@ function Invoke-CippTestEIDSCA_AF03 { FIDO2 - Attestation #> param($Tenant) - + try { $AuthMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF04.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF04.ps1 index a323bf955a1b..367c7752aa61 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF04.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF04.ps1 @@ -4,7 +4,7 @@ function Invoke-CippTestEIDSCA_AF04 { FIDO2 - Key Restrictions #> param($Tenant) - + try { $AuthMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF05.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF05.ps1 index 3ce66189a41b..aac3f2e780b4 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF05.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF05.ps1 @@ -4,7 +4,7 @@ function Invoke-CippTestEIDSCA_AF05 { FIDO2 - Restricted Keys #> param($Tenant) - + try { $AuthMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF06.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF06.ps1 index 1089abf10567..86d2c86eda88 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF06.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF06.ps1 @@ -4,7 +4,7 @@ function Invoke-CippTestEIDSCA_AF06 { FIDO2 - Specific Keys #> param($Tenant) - + try { $AuthMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AG01.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AG01.ps1 index 662367060748..4ee1ec5a52d4 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AG01.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AG01.ps1 @@ -4,7 +4,7 @@ function Invoke-CippTestEIDSCA_AG01 { Authentication Method - General Settings - Manage migration #> param($Tenant) - + try { $AuthMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AG02.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AG02.ps1 index 6366436772ff..55b9e23167ea 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AG02.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AG02.ps1 @@ -4,7 +4,7 @@ function Invoke-CippTestEIDSCA_AG02 { Authentication Method - General Settings - Report suspicious activity State #> param($Tenant) - + try { $AuthMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AG03.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AG03.ps1 index 22f50b8c2a0f..a3e39de3d781 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AG03.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AG03.ps1 @@ -4,7 +4,7 @@ function Invoke-CippTestEIDSCA_AG03 { Authentication Method - General Settings - Report suspicious activity users/groups #> param($Tenant) - + try { $AuthMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM01.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM01.ps1 index 7432fafbf3c7..a5c9acb5e329 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM01.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM01.ps1 @@ -4,7 +4,7 @@ function Invoke-CippTestEIDSCA_AM01 { Checks if Microsoft Authenticator authentication method is enabled #> param($Tenant) - + try { $AuthMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' @@ -14,7 +14,7 @@ function Invoke-CippTestEIDSCA_AM01 { } $MethodConfig = $AuthMethodsPolicy.authenticationMethodConfigurations | Where-Object { $_.id -eq 'MicrosoftAuthenticator' } - + if ($MethodConfig.state -eq 'enabled') { $Status = 'Pass' $Result = 'Microsoft Authenticator authentication method is enabled.' diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM02.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM02.ps1 index 21081028b020..f2ad7cb9b27f 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM02.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM02.ps1 @@ -4,7 +4,7 @@ function Invoke-CippTestEIDSCA_AM02 { Checks if Microsoft Authenticator software OATH is disabled #> param($Tenant) - + try { $AuthMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' @@ -14,7 +14,7 @@ function Invoke-CippTestEIDSCA_AM02 { } $MethodConfig = $AuthMethodsPolicy.authenticationMethodConfigurations | Where-Object { $_.id -eq 'MicrosoftAuthenticator' } - + if ($MethodConfig.isSoftwareOathEnabled -eq $false) { $Status = 'Pass' $Result = 'Microsoft Authenticator software OATH is disabled.' diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM03.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM03.ps1 index 8cc830b14674..3bf4b1828e24 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM03.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM03.ps1 @@ -4,7 +4,7 @@ function Invoke-CippTestEIDSCA_AM03 { Checks if Microsoft Authenticator number matching is enabled #> param($Tenant) - + try { $AuthMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' @@ -14,7 +14,7 @@ function Invoke-CippTestEIDSCA_AM03 { } $MethodConfig = $AuthMethodsPolicy.authenticationMethodConfigurations | Where-Object { $_.id -eq 'MicrosoftAuthenticator' } - + if ($MethodConfig.featureSettings.numberMatchingRequiredState.state -eq 'enabled') { $Status = 'Pass' $Result = 'Microsoft Authenticator number matching is enabled.' diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM04.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM04.ps1 index d4d1527f4cd8..c6eceff6aafa 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM04.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM04.ps1 @@ -4,7 +4,7 @@ function Invoke-CippTestEIDSCA_AM04 { Checks if Microsoft Authenticator number matching targets all users #> param($Tenant) - + try { $AuthMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' @@ -14,7 +14,7 @@ function Invoke-CippTestEIDSCA_AM04 { } $MethodConfig = $AuthMethodsPolicy.authenticationMethodConfigurations | Where-Object { $_.id -eq 'MicrosoftAuthenticator' } - + if ($MethodConfig.featureSettings.numberMatchingRequiredState.includeTarget.id -eq 'all_users') { $Status = 'Pass' $Result = 'Microsoft Authenticator number matching targets all users.' diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM06.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM06.ps1 index 6126c9a4b2ae..107fa739a8a8 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM06.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM06.ps1 @@ -4,7 +4,7 @@ function Invoke-CippTestEIDSCA_AM06 { Checks if Microsoft Authenticator app information display is enabled #> param($Tenant) - + try { $AuthMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' @@ -14,7 +14,7 @@ function Invoke-CippTestEIDSCA_AM06 { } $MethodConfig = $AuthMethodsPolicy.authenticationMethodConfigurations | Where-Object { $_.id -eq 'MicrosoftAuthenticator' } - + if ($MethodConfig.featureSettings.displayAppInformationRequiredState.state -eq 'enabled') { $Status = 'Pass' $Result = 'Microsoft Authenticator app information display is enabled.' diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM07.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM07.ps1 index abbe3eae3864..64a5a9605a2b 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM07.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM07.ps1 @@ -4,7 +4,7 @@ function Invoke-CippTestEIDSCA_AM07 { Checks if Microsoft Authenticator app information display targets all users #> param($Tenant) - + try { $AuthMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' @@ -14,7 +14,7 @@ function Invoke-CippTestEIDSCA_AM07 { } $MethodConfig = $AuthMethodsPolicy.authenticationMethodConfigurations | Where-Object { $_.id -eq 'MicrosoftAuthenticator' } - + if ($MethodConfig.featureSettings.displayAppInformationRequiredState.includeTarget.id -eq 'all_users') { $Status = 'Pass' $Result = 'Microsoft Authenticator app information display targets all users.' diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM10.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM10.ps1 index 0271bf0934e5..c4edc34ab89d 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM10.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM10.ps1 @@ -4,7 +4,7 @@ function Invoke-CippTestEIDSCA_AM10 { Checks if Microsoft Authenticator location information display targets all users #> param($Tenant) - + try { $AuthMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' @@ -14,7 +14,7 @@ function Invoke-CippTestEIDSCA_AM10 { } $MethodConfig = $AuthMethodsPolicy.authenticationMethodConfigurations | Where-Object { $_.id -eq 'MicrosoftAuthenticator' } - + if ($MethodConfig.featureSettings.displayLocationInformationRequiredState.includeTarget.id -eq 'all_users') { $Status = 'Pass' $Result = 'Microsoft Authenticator location information display targets all users.' diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP01.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP01.ps1 index 1817bcad5a56..0624cfc89de7 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP01.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP01.ps1 @@ -4,7 +4,7 @@ function Invoke-CippTestEIDSCA_AP01 { Default Authorization Settings - Enabled Self service password reset for administrators #> param($Tenant) - + try { $AuthorizationPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthorizationPolicy' diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP04.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP04.ps1 index dea3b98cb36b..c80a0a46b041 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP04.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP04.ps1 @@ -4,7 +4,7 @@ function Invoke-CippTestEIDSCA_AP04 { Default Authorization Settings - Guest invite restrictions #> param($Tenant) - + try { $AuthorizationPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthorizationPolicy' diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP05.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP05.ps1 index e48a004c97f4..840dc747e769 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP05.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP05.ps1 @@ -4,7 +4,7 @@ function Invoke-CippTestEIDSCA_AP05 { Default Authorization Settings - Sign-up for email based subscription #> param($Tenant) - + try { $AuthorizationPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthorizationPolicy' diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP06.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP06.ps1 index bd2f29c3b8af..cf4fdc63b0d9 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP06.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP06.ps1 @@ -4,7 +4,7 @@ function Invoke-CippTestEIDSCA_AP06 { Default Authorization Settings - User can join the tenant by email validation #> param($Tenant) - + try { $AuthorizationPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthorizationPolicy' diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP07.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP07.ps1 index 426e855505af..3ec9b4a9d289 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP07.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP07.ps1 @@ -4,7 +4,7 @@ function Invoke-CippTestEIDSCA_AP07 { Default Authorization Settings - Guest user access #> param($Tenant) - + try { $AuthorizationPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthorizationPolicy' diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP08.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP08.ps1 index 201186288412..fc04184ca33c 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP08.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP08.ps1 @@ -4,7 +4,7 @@ function Invoke-CippTestEIDSCA_AP08 { Default Authorization Settings - User consent policy assigned for applications #> param($Tenant) - + try { $AuthorizationPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthorizationPolicy' diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP09.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP09.ps1 index a0aca7f273b5..a8e6ae622766 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP09.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP09.ps1 @@ -4,7 +4,7 @@ function Invoke-CippTestEIDSCA_AP09 { Default Authorization Settings - Allow user consent on risk-based apps #> param($Tenant) - + try { $AuthorizationPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthorizationPolicy' diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP10.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP10.ps1 index 47771dc6a4ef..fbc10217707d 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP10.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP10.ps1 @@ -4,7 +4,7 @@ function Invoke-CippTestEIDSCA_AP10 { Default Authorization Settings - Default User Role Permissions - Allowed to create Apps #> param($Tenant) - + try { $AuthorizationPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthorizationPolicy' diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP14.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP14.ps1 index f05c9933958e..b7ff45361eb3 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP14.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP14.ps1 @@ -4,7 +4,7 @@ function Invoke-CippTestEIDSCA_AP14 { Default Authorization Settings - Default User Role Permissions - Allowed to read other users #> param($Tenant) - + try { $AuthorizationPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthorizationPolicy' diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AT01.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AT01.ps1 index 500c3bb6dc26..e6191a184f78 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AT01.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AT01.ps1 @@ -4,7 +4,7 @@ function Invoke-CippTestEIDSCA_AT01 { Temp Access Pass - State #> param($Tenant) - + try { $AuthMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CP01.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CP01.ps1 index a85e0691a3a1..870356a271d5 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CP01.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CP01.ps1 @@ -4,7 +4,7 @@ function Invoke-CippTestEIDSCA_CP01 { Consent Policy Settings - Group owner consent for apps accessing data #> param($Tenant) - + try { $Settings = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Settings' diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CP03.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CP03.ps1 index 3e1dc20c51d1..ae45c78b1fab 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CP03.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CP03.ps1 @@ -4,7 +4,7 @@ function Invoke-CippTestEIDSCA_CP03 { Consent Policy Settings - Block user consent for risky apps #> param($Tenant) - + try { $Settings = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Settings' diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CP04.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CP04.ps1 index f4e13307f89c..bfe0fb5e164d 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CP04.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CP04.ps1 @@ -4,7 +4,7 @@ function Invoke-CippTestEIDSCA_CP04 { Consent Policy Settings - Users can request admin consent #> param($Tenant) - + try { $Settings = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Settings' diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR01.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR01.ps1 index fd8ab7ca6d0f..3ffab6b19e10 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR01.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR01.ps1 @@ -4,7 +4,7 @@ function Invoke-CippTestEIDSCA_CR01 { Admin Consent - Enabled #> param($Tenant) - + try { $AdminConsentPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AdminConsentRequestPolicy' diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR02.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR02.ps1 index a4847f42c38c..070a00e4ccd5 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR02.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR02.ps1 @@ -4,7 +4,7 @@ function Invoke-CippTestEIDSCA_CR02 { Admin Consent - Notify Reviewers #> param($Tenant) - + try { $AdminConsentPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AdminConsentRequestPolicy' diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR03.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR03.ps1 index c9b473b0680d..55777e5f30e5 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR03.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR03.ps1 @@ -4,7 +4,7 @@ function Invoke-CippTestEIDSCA_CR03 { Admin Consent - Reminders #> param($Tenant) - + try { $AdminConsentPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AdminConsentRequestPolicy' diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR04.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR04.ps1 index 356e6a3b31a0..0016700dddf2 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR04.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR04.ps1 @@ -4,7 +4,7 @@ function Invoke-CippTestEIDSCA_CR04 { Admin Consent - Duration #> param($Tenant) - + try { $AdminConsentPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AdminConsentRequestPolicy' diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR02.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR02.ps1 index c68580ba4498..954f8a734216 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR02.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR02.ps1 @@ -4,7 +4,7 @@ function Invoke-CippTestEIDSCA_PR02 { Password Rule Settings - Enable password protection on Windows Server Active Directory #> param($Tenant) - + try { $Settings = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Settings' diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR03.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR03.ps1 index e4e84f7d68c7..698f4f62997e 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR03.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR03.ps1 @@ -4,7 +4,7 @@ function Invoke-CippTestEIDSCA_PR03 { Password Rule Settings - Enforce custom list #> param($Tenant) - + try { $Settings = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Settings' diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR05.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR05.ps1 index 356b1a1436ac..ed4367119b10 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR05.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR05.ps1 @@ -4,7 +4,7 @@ function Invoke-CippTestEIDSCA_PR05 { Password Rule Settings - Lockout duration in seconds #> param($Tenant) - + try { $Settings = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Settings' diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR06.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR06.ps1 index 38eb1d54af22..ad485d415484 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR06.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR06.ps1 @@ -4,7 +4,7 @@ function Invoke-CippTestEIDSCA_PR06 { Password Rule Settings - Lockout threshold #> param($Tenant) - + try { $Settings = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Settings' diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_ST08.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_ST08.ps1 index e3c46cf02bfc..203f9c49d2d4 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_ST08.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_ST08.ps1 @@ -4,7 +4,7 @@ function Invoke-CippTestEIDSCA_ST08 { Classification and M365 Groups - Allow Guests to become Group Owner #> param($Tenant) - + try { $Settings = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Settings' diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_ST09.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_ST09.ps1 index f7510ad2f60e..7ca935c9c8ae 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_ST09.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_ST09.ps1 @@ -4,7 +4,7 @@ function Invoke-CippTestEIDSCA_ST09 { Classification and M365 Groups - Allow Guests to have access to groups content #> param($Tenant) - + try { $Settings = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Settings' From 878e30a2de1f88280abeb044fc1f037c036a6b85 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Sun, 11 Jan 2026 11:49:10 +0100 Subject: [PATCH 104/503] Remove periods --- .../Identity/Invoke-CippTestEIDSCA_AF01.ps1 | 8 +- .../Identity/Invoke-CippTestEIDSCA_AF02.ps1 | 8 +- .../Identity/Invoke-CippTestEIDSCA_AF03.ps1 | 8 +- .../Identity/Invoke-CippTestEIDSCA_AF04.ps1 | 8 +- .../Identity/Invoke-CippTestEIDSCA_AF05.ps1 | 8 +- .../Identity/Invoke-CippTestEIDSCA_AF06.ps1 | 8 +- .../Identity/Invoke-CippTestEIDSCA_AG01.ps1 | 6 +- .../Identity/Invoke-CippTestEIDSCA_AG02.ps1 | 6 +- .../Identity/Invoke-CippTestEIDSCA_AG03.ps1 | 6 +- .../Identity/Invoke-CippTestEIDSCA_AM01.ps1 | 6 +- .../Identity/Invoke-CippTestEIDSCA_AM02.ps1 | 6 +- .../Identity/Invoke-CippTestEIDSCA_AM03.ps1 | 6 +- .../Identity/Invoke-CippTestEIDSCA_AM04.ps1 | 6 +- .../Identity/Invoke-CippTestEIDSCA_AM06.ps1 | 6 +- .../Identity/Invoke-CippTestEIDSCA_AM07.ps1 | 6 +- .../Identity/Invoke-CippTestEIDSCA_AM09.ps1 | 6 +- .../Identity/Invoke-CippTestEIDSCA_AM10.ps1 | 6 +- .../Identity/Invoke-CippTestEIDSCA_AP01.ps1 | 6 +- .../Identity/Invoke-CippTestEIDSCA_AP04.ps1 | 6 +- .../Identity/Invoke-CippTestEIDSCA_AP05.ps1 | 6 +- .../Identity/Invoke-CippTestEIDSCA_AP06.ps1 | 6 +- .../Identity/Invoke-CippTestEIDSCA_AP07.ps1 | 6 +- .../Identity/Invoke-CippTestEIDSCA_AP08.ps1 | 6 +- .../Identity/Invoke-CippTestEIDSCA_AP09.ps1 | 6 +- .../Identity/Invoke-CippTestEIDSCA_AP10.ps1 | 6 +- .../Identity/Invoke-CippTestEIDSCA_AP14.ps1 | 6 +- .../Identity/Invoke-CippTestEIDSCA_AS04.ps1 | 8 +- .../Identity/Invoke-CippTestEIDSCA_AT01.ps1 | 8 +- .../Identity/Invoke-CippTestEIDSCA_AT02.ps1 | 8 +- .../Identity/Invoke-CippTestEIDSCA_AV01.ps1 | 8 +- .../Identity/Invoke-CippTestEIDSCA_CP01.ps1 | 6 +- .../Identity/Invoke-CippTestEIDSCA_CP03.ps1 | 6 +- .../Identity/Invoke-CippTestEIDSCA_CP04.ps1 | 6 +- .../Identity/Invoke-CippTestEIDSCA_CR01.ps1 | 6 +- .../Identity/Invoke-CippTestEIDSCA_CR02.ps1 | 6 +- .../Identity/Invoke-CippTestEIDSCA_CR03.ps1 | 6 +- .../Identity/Invoke-CippTestEIDSCA_CR04.ps1 | 6 +- .../Identity/Invoke-CippTestEIDSCA_PR01.ps1 | 6 +- .../Identity/Invoke-CippTestEIDSCA_PR02.ps1 | 6 +- .../Identity/Invoke-CippTestEIDSCA_PR03.ps1 | 6 +- .../Identity/Invoke-CippTestEIDSCA_PR05.ps1 | 6 +- .../Identity/Invoke-CippTestEIDSCA_PR06.ps1 | 6 +- .../Identity/Invoke-CippTestEIDSCA_ST08.ps1 | 6 +- .../Identity/Invoke-CippTestEIDSCA_ST09.ps1 | 6 +- .../CIPPCore/Public/Tests/EIDSCA/report.json | 88 +++++++++---------- Test-AllZTNATests.ps1 | 2 +- 46 files changed, 187 insertions(+), 187 deletions(-) diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF01.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF01.ps1 index 3c49e4249089..21fd43de3505 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF01.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF01.ps1 @@ -9,14 +9,14 @@ function Invoke-CippTestEIDSCA_AF01 { $AuthMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' if (-not $AuthMethodsPolicy) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AF01' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Low' -Name 'EIDSCA.AF01: FIDO2 - State' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAF01' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Low' -Name 'EIDSCAAF01: FIDO2 - State' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Authentication Methods' return } $Fido2Config = $AuthMethodsPolicy.authenticationMethodConfigurations | Where-Object { $_.id -eq 'Fido2' } if (-not $Fido2Config) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AF01' -TestType 'Identity' -Status 'Failed' -ResultMarkdown 'FIDO2 configuration not found in Authentication Methods Policy.' -Risk 'Low' -Name 'EIDSCA.AF01: FIDO2 - State' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAF01' -TestType 'Identity' -Status 'Failed' -ResultMarkdown 'FIDO2 configuration not found in Authentication Methods Policy.' -Risk 'Low' -Name 'EIDSCAAF01: FIDO2 - State' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Authentication Methods' return } @@ -38,10 +38,10 @@ Enabling FIDO2 provides users with a secure, passwordless authentication option. "@ } - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AF01' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Low' -Name 'EIDSCA.AF01: FIDO2 - State' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAF01' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Low' -Name 'EIDSCAAF01: FIDO2 - State' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Authentication Methods' } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AF01' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Low' -Name 'EIDSCA.AF01: FIDO2 - State' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAF01' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Low' -Name 'EIDSCAAF01: FIDO2 - State' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Authentication Methods' } } diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF02.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF02.ps1 index 173ba9b78a8d..41c61c539bec 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF02.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF02.ps1 @@ -9,14 +9,14 @@ function Invoke-CippTestEIDSCA_AF02 { $AuthMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' if (-not $AuthMethodsPolicy) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AF02' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Low' -Name 'EIDSCA.AF02: FIDO2 - Self-Service' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAF02' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Low' -Name 'EIDSCAAF02: FIDO2 - Self-Service' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' return } $Fido2Config = $AuthMethodsPolicy.authenticationMethodConfigurations | Where-Object { $_.id -eq 'Fido2' } if (-not $Fido2Config) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AF02' -TestType 'Identity' -Status 'Failed' -ResultMarkdown 'FIDO2 configuration not found in Authentication Methods Policy.' -Risk 'Low' -Name 'EIDSCA.AF02: FIDO2 - Self-Service' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAF02' -TestType 'Identity' -Status 'Failed' -ResultMarkdown 'FIDO2 configuration not found in Authentication Methods Policy.' -Risk 'Low' -Name 'EIDSCAAF02: FIDO2 - Self-Service' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' return } @@ -38,10 +38,10 @@ Enabling self-service registration improves user experience and adoption of FIDO "@ } - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AF02' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Low' -Name 'EIDSCA.AF02: FIDO2 - Self-Service' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAF02' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Low' -Name 'EIDSCAAF02: FIDO2 - Self-Service' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AF02' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Low' -Name 'EIDSCA.AF02: FIDO2 - Self-Service' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAF02' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Low' -Name 'EIDSCAAF02: FIDO2 - Self-Service' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' } } diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF03.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF03.ps1 index 0b02f34c7cec..dbc30f48c606 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF03.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF03.ps1 @@ -9,14 +9,14 @@ function Invoke-CippTestEIDSCA_AF03 { $AuthMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' if (-not $AuthMethodsPolicy) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AF03' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'EIDSCA.AF03: FIDO2 - Attestation' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAF03' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'EIDSCAAF03: FIDO2 - Attestation' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Authentication Methods' return } $Fido2Config = $AuthMethodsPolicy.authenticationMethodConfigurations | Where-Object { $_.id -eq 'Fido2' } if (-not $Fido2Config) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AF03' -TestType 'Identity' -Status 'Failed' -ResultMarkdown 'FIDO2 configuration not found in Authentication Methods Policy.' -Risk 'Medium' -Name 'EIDSCA.AF03: FIDO2 - Attestation' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAF03' -TestType 'Identity' -Status 'Failed' -ResultMarkdown 'FIDO2 configuration not found in Authentication Methods Policy.' -Risk 'Medium' -Name 'EIDSCAAF03: FIDO2 - Attestation' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Authentication Methods' return } @@ -38,10 +38,10 @@ Enforcing attestation ensures that only trusted and verified security keys can b "@ } - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AF03' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'EIDSCA.AF03: FIDO2 - Attestation' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAF03' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'EIDSCAAF03: FIDO2 - Attestation' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Authentication Methods' } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AF03' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'EIDSCA.AF03: FIDO2 - Attestation' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAF03' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'EIDSCAAF03: FIDO2 - Attestation' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Authentication Methods' } } diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF04.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF04.ps1 index 367c7752aa61..fc28377a8df5 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF04.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF04.ps1 @@ -9,14 +9,14 @@ function Invoke-CippTestEIDSCA_AF04 { $AuthMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' if (-not $AuthMethodsPolicy) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AF04' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'EIDSCA.AF04: FIDO2 - Key Restrictions' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAF04' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'EIDSCAAF04: FIDO2 - Key Restrictions' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Authentication Methods' return } $Fido2Config = $AuthMethodsPolicy.authenticationMethodConfigurations | Where-Object { $_.id -eq 'Fido2' } if (-not $Fido2Config) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AF04' -TestType 'Identity' -Status 'Failed' -ResultMarkdown 'FIDO2 configuration not found in Authentication Methods Policy.' -Risk 'Medium' -Name 'EIDSCA.AF04: FIDO2 - Key Restrictions' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAF04' -TestType 'Identity' -Status 'Failed' -ResultMarkdown 'FIDO2 configuration not found in Authentication Methods Policy.' -Risk 'Medium' -Name 'EIDSCAAF04: FIDO2 - Key Restrictions' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Authentication Methods' return } @@ -38,10 +38,10 @@ Enforcing key restrictions helps ensure only approved security keys are used in "@ } - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AF04' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'EIDSCA.AF04: FIDO2 - Key Restrictions' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAF04' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'EIDSCAAF04: FIDO2 - Key Restrictions' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Authentication Methods' } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AF04' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'EIDSCA.AF04: FIDO2 - Key Restrictions' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAF04' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'EIDSCAAF04: FIDO2 - Key Restrictions' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Authentication Methods' } } diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF05.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF05.ps1 index aac3f2e780b4..61ffa0605282 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF05.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF05.ps1 @@ -9,14 +9,14 @@ function Invoke-CippTestEIDSCA_AF05 { $AuthMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' if (-not $AuthMethodsPolicy) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AF05' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'EIDSCA.AF05: FIDO2 - Restricted Keys' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAF05' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'EIDSCAAF05: FIDO2 - Restricted Keys' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Authentication Methods' return } $Fido2Config = $AuthMethodsPolicy.authenticationMethodConfigurations | Where-Object { $_.id -eq 'Fido2' } if (-not $Fido2Config) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AF05' -TestType 'Identity' -Status 'Failed' -ResultMarkdown 'FIDO2 configuration not found in Authentication Methods Policy.' -Risk 'Medium' -Name 'EIDSCA.AF05: FIDO2 - Restricted Keys' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAF05' -TestType 'Identity' -Status 'Failed' -ResultMarkdown 'FIDO2 configuration not found in Authentication Methods Policy.' -Risk 'Medium' -Name 'EIDSCAAF05: FIDO2 - Restricted Keys' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Authentication Methods' return } @@ -39,10 +39,10 @@ Specifying AAGuids allows you to restrict registration to specific, approved sec "@ } - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AF05' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'EIDSCA.AF05: FIDO2 - Restricted Keys' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAF05' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'EIDSCAAF05: FIDO2 - Restricted Keys' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Authentication Methods' } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AF05' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'EIDSCA.AF05: FIDO2 - Restricted Keys' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAF05' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'EIDSCAAF05: FIDO2 - Restricted Keys' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Authentication Methods' } } diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF06.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF06.ps1 index 86d2c86eda88..44a21cbb8636 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF06.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF06.ps1 @@ -9,14 +9,14 @@ function Invoke-CippTestEIDSCA_AF06 { $AuthMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' if (-not $AuthMethodsPolicy) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AF06' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'EIDSCA.AF06: FIDO2 - Specific Keys' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAF06' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'EIDSCAAF06: FIDO2 - Specific Keys' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Authentication Methods' return } $Fido2Config = $AuthMethodsPolicy.authenticationMethodConfigurations | Where-Object { $_.id -eq 'Fido2' } if (-not $Fido2Config) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AF06' -TestType 'Identity' -Status 'Failed' -ResultMarkdown 'FIDO2 configuration not found in Authentication Methods Policy.' -Risk 'Medium' -Name 'EIDSCA.AF06: FIDO2 - Specific Keys' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAF06' -TestType 'Identity' -Status 'Failed' -ResultMarkdown 'FIDO2 configuration not found in Authentication Methods Policy.' -Risk 'Medium' -Name 'EIDSCAAF06: FIDO2 - Specific Keys' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Authentication Methods' return } @@ -43,10 +43,10 @@ Proper enforcement type ensures the AAGuids list is actively used to control key "@ } - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AF06' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'EIDSCA.AF06: FIDO2 - Specific Keys' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAF06' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'EIDSCAAF06: FIDO2 - Specific Keys' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Authentication Methods' } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AF06' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'EIDSCA.AF06: FIDO2 - Specific Keys' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAF06' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'EIDSCAAF06: FIDO2 - Specific Keys' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Authentication Methods' } } diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AG01.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AG01.ps1 index 4ee1ec5a52d4..5f1a2ff07fcf 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AG01.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AG01.ps1 @@ -9,7 +9,7 @@ function Invoke-CippTestEIDSCA_AG01 { $AuthMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' if (-not $AuthMethodsPolicy) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AG01' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'EIDSCA.AG01: Authentication Methods - Policy Migration' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAG01' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'EIDSCAAG01: Authentication Methods - Policy Migration' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' return } @@ -33,10 +33,10 @@ Complete the migration to use the modern authentication methods policy. "@ } - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AG01' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'EIDSCA.AG01: Authentication Methods - Policy Migration' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAG01' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'EIDSCAAG01: Authentication Methods - Policy Migration' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AG01' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'EIDSCA.AG01: Authentication Methods - Policy Migration' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAG01' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'EIDSCAAG01: Authentication Methods - Policy Migration' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' } } diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AG02.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AG02.ps1 index 55b9e23167ea..28f7d1f0bef2 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AG02.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AG02.ps1 @@ -9,7 +9,7 @@ function Invoke-CippTestEIDSCA_AG02 { $AuthMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' if (-not $AuthMethodsPolicy) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AG02' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'EIDSCA.AG02: Authentication Methods - Report Suspicious Activity' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAG02' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'EIDSCAAG02: Authentication Methods - Report Suspicious Activity' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' return } @@ -33,10 +33,10 @@ This feature helps detect and prevent unauthorized access attempts. "@ } - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AG02' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'EIDSCA.AG02: Authentication Methods - Report Suspicious Activity' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAG02' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'EIDSCAAG02: Authentication Methods - Report Suspicious Activity' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AG02' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'EIDSCA.AG02: Authentication Methods - Report Suspicious Activity' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAG02' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'EIDSCAAG02: Authentication Methods - Report Suspicious Activity' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' } } diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AG03.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AG03.ps1 index a3e39de3d781..7e916405b7ee 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AG03.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AG03.ps1 @@ -9,7 +9,7 @@ function Invoke-CippTestEIDSCA_AG03 { $AuthMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' if (-not $AuthMethodsPolicy) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AG03' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'EIDSCA.AG03: Authentication Methods - Suspicious Activity Target' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAG03' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'EIDSCAAG03: Authentication Methods - Suspicious Activity Target' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' return } @@ -33,10 +33,10 @@ All users should be able to report suspicious authentication attempts. "@ } - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AG03' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'EIDSCA.AG03: Authentication Methods - Suspicious Activity Target' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAG03' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'EIDSCAAG03: Authentication Methods - Suspicious Activity Target' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AG03' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'EIDSCA.AG03: Authentication Methods - Suspicious Activity Target' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAG03' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'EIDSCAAG03: Authentication Methods - Suspicious Activity Target' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' } } diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM01.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM01.ps1 index a5c9acb5e329..c75f3c7a6a73 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM01.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM01.ps1 @@ -9,7 +9,7 @@ function Invoke-CippTestEIDSCA_AM01 { $AuthMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' if (-not $AuthMethodsPolicy) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AM01' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'EIDSCA.AM01: MS Authenticator - State' -UserImpact 'High' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAM01' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'EIDSCAAM01: MS Authenticator - State' -UserImpact 'High' -ImplementationEffort 'Low' -Category 'Authentication Methods' return } @@ -23,10 +23,10 @@ function Invoke-CippTestEIDSCA_AM01 { $Result = "Microsoft Authenticator authentication method is not enabled. Current state: $($MethodConfig.state)" } - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AM01' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'EIDSCA.AM01: MS Authenticator - State' -UserImpact 'High' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAM01' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'EIDSCAAM01: MS Authenticator - State' -UserImpact 'High' -ImplementationEffort 'Low' -Category 'Authentication Methods' } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AM01' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'EIDSCA.AM01: MS Authenticator - State' -UserImpact 'High' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAM01' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'EIDSCAAM01: MS Authenticator - State' -UserImpact 'High' -ImplementationEffort 'Low' -Category 'Authentication Methods' } } diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM02.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM02.ps1 index f2ad7cb9b27f..ef8cfe242459 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM02.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM02.ps1 @@ -9,7 +9,7 @@ function Invoke-CippTestEIDSCA_AM02 { $AuthMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' if (-not $AuthMethodsPolicy) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AM02' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'EIDSCA.AM02: MS Authenticator - OTP Disabled' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAM02' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'EIDSCAAM02: MS Authenticator - OTP Disabled' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authentication Methods' return } @@ -23,10 +23,10 @@ function Invoke-CippTestEIDSCA_AM02 { $Result = "Microsoft Authenticator software OATH is enabled. It should be disabled." } - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AM02' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'EIDSCA.AM02: MS Authenticator - OTP Disabled' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAM02' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'EIDSCAAM02: MS Authenticator - OTP Disabled' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authentication Methods' } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AM02' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'EIDSCA.AM02: MS Authenticator - OTP Disabled' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAM02' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'EIDSCAAM02: MS Authenticator - OTP Disabled' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authentication Methods' } } diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM03.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM03.ps1 index 3bf4b1828e24..d77f7b9b3a40 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM03.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM03.ps1 @@ -9,7 +9,7 @@ function Invoke-CippTestEIDSCA_AM03 { $AuthMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' if (-not $AuthMethodsPolicy) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AM03' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'EIDSCA.AM03: MS Authenticator - Number Matching' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAM03' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'EIDSCAAM03: MS Authenticator - Number Matching' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authentication Methods' return } @@ -23,10 +23,10 @@ function Invoke-CippTestEIDSCA_AM03 { $Result = "Microsoft Authenticator number matching is not enabled. Current state: $($MethodConfig.featureSettings.numberMatchingRequiredState.state)" } - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AM03' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'EIDSCA.AM03: MS Authenticator - Number Matching' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAM03' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'EIDSCAAM03: MS Authenticator - Number Matching' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authentication Methods' } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AM03' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'EIDSCA.AM03: MS Authenticator - Number Matching' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAM03' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'EIDSCAAM03: MS Authenticator - Number Matching' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authentication Methods' } } diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM04.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM04.ps1 index c6eceff6aafa..fdb24d155943 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM04.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM04.ps1 @@ -9,7 +9,7 @@ function Invoke-CippTestEIDSCA_AM04 { $AuthMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' if (-not $AuthMethodsPolicy) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AM04' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'EIDSCA.AM04: MS Authenticator - Number Matching Target' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAM04' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'EIDSCAAM04: MS Authenticator - Number Matching Target' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' return } @@ -23,10 +23,10 @@ function Invoke-CippTestEIDSCA_AM04 { $Result = "Microsoft Authenticator number matching does not target all users. Current target: $($MethodConfig.featureSettings.numberMatchingRequiredState.includeTarget.id)" } - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AM04' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'EIDSCA.AM04: MS Authenticator - Number Matching Target' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAM04' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'EIDSCAAM04: MS Authenticator - Number Matching Target' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AM04' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'EIDSCA.AM04: MS Authenticator - Number Matching Target' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAM04' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'EIDSCAAM04: MS Authenticator - Number Matching Target' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' } } diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM06.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM06.ps1 index 107fa739a8a8..88a88a6a9b5a 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM06.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM06.ps1 @@ -9,7 +9,7 @@ function Invoke-CippTestEIDSCA_AM06 { $AuthMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' if (-not $AuthMethodsPolicy) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AM06' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'EIDSCA.AM06: MS Authenticator - Show App Name' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAM06' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'EIDSCAAM06: MS Authenticator - Show App Name' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' return } @@ -23,10 +23,10 @@ function Invoke-CippTestEIDSCA_AM06 { $Result = "Microsoft Authenticator app information display is not enabled. Current state: $($MethodConfig.featureSettings.displayAppInformationRequiredState.state)" } - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AM06' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'EIDSCA.AM06: MS Authenticator - Show App Name' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAM06' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'EIDSCAAM06: MS Authenticator - Show App Name' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AM06' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'EIDSCA.AM06: MS Authenticator - Show App Name' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAM06' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'EIDSCAAM06: MS Authenticator - Show App Name' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' } } diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM07.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM07.ps1 index 64a5a9605a2b..1b4ed36398ab 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM07.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM07.ps1 @@ -9,7 +9,7 @@ function Invoke-CippTestEIDSCA_AM07 { $AuthMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' if (-not $AuthMethodsPolicy) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AM07' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'EIDSCA.AM07: MS Authenticator - Show App Name Target' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAM07' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'EIDSCAAM07: MS Authenticator - Show App Name Target' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' return } @@ -23,10 +23,10 @@ function Invoke-CippTestEIDSCA_AM07 { $Result = "Microsoft Authenticator app information display does not target all users. Current target: $($MethodConfig.featureSettings.displayAppInformationRequiredState.includeTarget.id)" } - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AM07' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'EIDSCA.AM07: MS Authenticator - Show App Name Target' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAM07' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'EIDSCAAM07: MS Authenticator - Show App Name Target' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AM07' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'EIDSCA.AM07: MS Authenticator - Show App Name Target' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAM07' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'EIDSCAAM07: MS Authenticator - Show App Name Target' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' } } diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM09.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM09.ps1 index d12332bade23..6a6fd5a06a53 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM09.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM09.ps1 @@ -9,7 +9,7 @@ function Invoke-CippTestEIDSCA_AM09 { $AuthMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' if (-not $AuthMethodsPolicy) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AM09' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'EIDSCA.AM09: MS Authenticator - Show Location' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAM09' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'EIDSCAAM09: MS Authenticator - Show Location' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' return } @@ -23,11 +23,11 @@ function Invoke-CippTestEIDSCA_AM09 { $Result = "Microsoft Authenticator location information display is not enabled. Current state: $($MethodConfig.featureSettings.displayLocationInformationRequiredState.state)" } - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AM09' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'EIDSCA.AM09: MS Authenticator - Show Location' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAM09' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'EIDSCAAM09: MS Authenticator - Show Location' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AM09' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'EIDSCA.AM09: MS Authenticator - Show Location' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAM09' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'EIDSCAAM09: MS Authenticator - Show Location' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' } } diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM10.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM10.ps1 index c4edc34ab89d..3ee0cfd29cbe 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM10.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM10.ps1 @@ -9,7 +9,7 @@ function Invoke-CippTestEIDSCA_AM10 { $AuthMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' if (-not $AuthMethodsPolicy) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AM10' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'EIDSCA.AM10: MS Authenticator - Show Location Target' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAM10' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'EIDSCAAM10: MS Authenticator - Show Location Target' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' return } @@ -23,10 +23,10 @@ function Invoke-CippTestEIDSCA_AM10 { $Result = "Microsoft Authenticator location information display does not target all users. Current target: $($MethodConfig.featureSettings.displayLocationInformationRequiredState.includeTarget.id)" } - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AM10' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'EIDSCA.AM10: MS Authenticator - Show Location Target' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAM10' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'EIDSCAAM10: MS Authenticator - Show Location Target' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AM10' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'EIDSCA.AM10: MS Authenticator - Show Location Target' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAM10' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'EIDSCAAM10: MS Authenticator - Show Location Target' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' } } diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP01.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP01.ps1 index 0624cfc89de7..a860fadf7170 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP01.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP01.ps1 @@ -9,7 +9,7 @@ function Invoke-CippTestEIDSCA_AP01 { $AuthorizationPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthorizationPolicy' if (-not $AuthorizationPolicy) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AP01' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'EIDSCA.AP01: Authorization Policy - Self-Service Password Reset for Admins' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authorization Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAP01' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'EIDSCAAP01: Authorization Policy - Self-Service Password Reset for Admins' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authorization Policy' return } @@ -33,10 +33,10 @@ Administrators should follow more stringent password reset procedures rather tha "@ } - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AP01' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'EIDSCA.AP01: Authorization Policy - Self-Service Password Reset for Admins' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authorization Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAP01' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'EIDSCAAP01: Authorization Policy - Self-Service Password Reset for Admins' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authorization Policy' } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AP01' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'EIDSCA.AP01: Authorization Policy - Self-Service Password Reset for Admins' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authorization Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAP01' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'EIDSCAAP01: Authorization Policy - Self-Service Password Reset for Admins' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authorization Policy' } } diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP04.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP04.ps1 index c80a0a46b041..5fc62c88e6d1 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP04.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP04.ps1 @@ -9,7 +9,7 @@ function Invoke-CippTestEIDSCA_AP04 { $AuthorizationPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthorizationPolicy' if (-not $AuthorizationPolicy) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AP04' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'EIDSCA.AP04: Authorization Policy - Guest Invite Restrictions' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authorization Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAP04' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'EIDSCAAP04: Authorization Policy - Guest Invite Restrictions' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authorization Policy' return } @@ -33,10 +33,10 @@ Restricting guest invitations helps maintain control over external access to you "@ } - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AP04' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'EIDSCA.AP04: Authorization Policy - Guest Invite Restrictions' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authorization Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAP04' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'EIDSCAAP04: Authorization Policy - Guest Invite Restrictions' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authorization Policy' } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AP04' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'EIDSCA.AP04: Authorization Policy - Guest Invite Restrictions' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authorization Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAP04' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'EIDSCAAP04: Authorization Policy - Guest Invite Restrictions' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authorization Policy' } } diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP05.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP05.ps1 index 840dc747e769..fab35cc80467 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP05.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP05.ps1 @@ -9,7 +9,7 @@ function Invoke-CippTestEIDSCA_AP05 { $AuthorizationPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthorizationPolicy' if (-not $AuthorizationPolicy) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AP05' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'EIDSCA.AP05: Authorization Policy - Email-Based Subscription Sign-up' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authorization Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAP05' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'EIDSCAAP05: Authorization Policy - Email-Based Subscription Sign-up' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authorization Policy' return } @@ -33,10 +33,10 @@ Disabling email-based subscriptions helps maintain control over tenant access. "@ } - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AP05' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'EIDSCA.AP05: Authorization Policy - Email-Based Subscription Sign-up' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authorization Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAP05' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'EIDSCAAP05: Authorization Policy - Email-Based Subscription Sign-up' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authorization Policy' } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AP05' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'EIDSCA.AP05: Authorization Policy - Email-Based Subscription Sign-up' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authorization Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAP05' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'EIDSCAAP05: Authorization Policy - Email-Based Subscription Sign-up' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authorization Policy' } } diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP06.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP06.ps1 index cf4fdc63b0d9..7e33f01d5359 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP06.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP06.ps1 @@ -9,7 +9,7 @@ function Invoke-CippTestEIDSCA_AP06 { $AuthorizationPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthorizationPolicy' if (-not $AuthorizationPolicy) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AP06' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'EIDSCA.AP06: Authorization Policy - Email Validation Join' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authorization Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAP06' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'EIDSCAAP06: Authorization Policy - Email Validation Join' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authorization Policy' return } @@ -33,10 +33,10 @@ Disabling this feature prevents unauthorized users from self-registering into yo "@ } - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AP06' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'EIDSCA.AP06: Authorization Policy - Email Validation Join' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authorization Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAP06' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'EIDSCAAP06: Authorization Policy - Email Validation Join' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authorization Policy' } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AP06' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'EIDSCA.AP06: Authorization Policy - Email Validation Join' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authorization Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAP06' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'EIDSCAAP06: Authorization Policy - Email Validation Join' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authorization Policy' } } diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP07.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP07.ps1 index 3ec9b4a9d289..49c188c656b9 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP07.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP07.ps1 @@ -9,7 +9,7 @@ function Invoke-CippTestEIDSCA_AP07 { $AuthorizationPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthorizationPolicy' if (-not $AuthorizationPolicy) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AP07' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'EIDSCA.AP07: Authorization Policy - Guest User Access' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authorization Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAP07' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'EIDSCAAP07: Authorization Policy - Guest User Access' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authorization Policy' return } @@ -34,10 +34,10 @@ This setting limits what guest users can see and do in your directory. "@ } - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AP07' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'EIDSCA.AP07: Authorization Policy - Guest User Access' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authorization Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAP07' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'EIDSCAAP07: Authorization Policy - Guest User Access' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authorization Policy' } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AP07' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'EIDSCA.AP07: Authorization Policy - Guest User Access' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authorization Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAP07' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'EIDSCAAP07: Authorization Policy - Guest User Access' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authorization Policy' } } diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP08.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP08.ps1 index fc04184ca33c..b95e4b675ca2 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP08.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP08.ps1 @@ -9,7 +9,7 @@ function Invoke-CippTestEIDSCA_AP08 { $AuthorizationPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthorizationPolicy' if (-not $AuthorizationPolicy) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AP08' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'EIDSCA.AP08: Authorization Policy - User Consent Policy' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authorization Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAP08' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'EIDSCAAP08: Authorization Policy - User Consent Policy' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authorization Policy' return } @@ -34,10 +34,10 @@ This limits users to only consent to applications with low-risk permissions. "@ } - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AP08' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'EIDSCA.AP08: Authorization Policy - User Consent Policy' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authorization Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAP08' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'EIDSCAAP08: Authorization Policy - User Consent Policy' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authorization Policy' } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AP08' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'EIDSCA.AP08: Authorization Policy - User Consent Policy' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authorization Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAP08' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'EIDSCAAP08: Authorization Policy - User Consent Policy' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authorization Policy' } } diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP09.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP09.ps1 index a8e6ae622766..6c3597a07af2 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP09.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP09.ps1 @@ -9,7 +9,7 @@ function Invoke-CippTestEIDSCA_AP09 { $AuthorizationPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthorizationPolicy' if (-not $AuthorizationPolicy) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AP09' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'EIDSCA.AP09: Authorization Policy - Consent for Risky Apps' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authorization Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAP09' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'EIDSCAAP09: Authorization Policy - Consent for Risky Apps' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authorization Policy' return } @@ -33,10 +33,10 @@ Disabling this prevents users from consenting to apps identified as risky. "@ } - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AP09' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'EIDSCA.AP09: Authorization Policy - Consent for Risky Apps' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authorization Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAP09' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'EIDSCAAP09: Authorization Policy - Consent for Risky Apps' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authorization Policy' } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AP09' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'EIDSCA.AP09: Authorization Policy - Consent for Risky Apps' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authorization Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAP09' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'EIDSCAAP09: Authorization Policy - Consent for Risky Apps' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authorization Policy' } } diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP10.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP10.ps1 index fbc10217707d..542b4ad15755 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP10.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP10.ps1 @@ -9,7 +9,7 @@ function Invoke-CippTestEIDSCA_AP10 { $AuthorizationPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthorizationPolicy' if (-not $AuthorizationPolicy) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AP10' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'EIDSCA.AP10: Authorization Policy - Users Can Create Apps' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authorization Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAP10' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'EIDSCAAP10: Authorization Policy - Users Can Create Apps' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authorization Policy' return } @@ -33,10 +33,10 @@ Only authorized users should be able to register applications in your tenant. "@ } - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AP10' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'EIDSCA.AP10: Authorization Policy - Users Can Create Apps' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authorization Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAP10' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'EIDSCAAP10: Authorization Policy - Users Can Create Apps' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authorization Policy' } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AP10' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'EIDSCA.AP10: Authorization Policy - Users Can Create Apps' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authorization Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAP10' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'EIDSCAAP10: Authorization Policy - Users Can Create Apps' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authorization Policy' } } diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP14.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP14.ps1 index b7ff45361eb3..0bf9df0077fb 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP14.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP14.ps1 @@ -9,7 +9,7 @@ function Invoke-CippTestEIDSCA_AP14 { $AuthorizationPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthorizationPolicy' if (-not $AuthorizationPolicy) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AP14' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Low' -Name 'EIDSCA.AP14: Authorization Policy - Users Can Read Other Users' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authorization Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAP14' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Low' -Name 'EIDSCAAP14: Authorization Policy - Users Can Read Other Users' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authorization Policy' return } @@ -33,10 +33,10 @@ This setting enables basic collaboration features like Teams and SharePoint. "@ } - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AP14' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Low' -Name 'EIDSCA.AP14: Authorization Policy - Users Can Read Other Users' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authorization Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAP14' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Low' -Name 'EIDSCAAP14: Authorization Policy - Users Can Read Other Users' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authorization Policy' } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AP14' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Low' -Name 'EIDSCA.AP14: Authorization Policy - Users Can Read Other Users' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authorization Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAP14' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Low' -Name 'EIDSCAAP14: Authorization Policy - Users Can Read Other Users' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authorization Policy' } } diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AS04.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AS04.ps1 index fc58fe113b94..ae95b95829be 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AS04.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AS04.ps1 @@ -9,14 +9,14 @@ function Invoke-CippTestEIDSCA_AS04 { $AuthMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' if (-not $AuthMethodsPolicy) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AS04' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'EIDSCA.AS04: SMS - No Sign-In' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAS04' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'EIDSCAAS04: SMS - No Sign-In' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authentication Methods' return } $SmsConfig = $AuthMethodsPolicy.authenticationMethodConfigurations | Where-Object { $_.id -eq 'Sms' } if (-not $SmsConfig) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AS04' -TestType 'Identity' -Status 'Failed' -ResultMarkdown 'SMS authentication configuration not found in Authentication Methods Policy.' -Risk 'High' -Name 'EIDSCA.AS04: SMS - No Sign-In' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAS04' -TestType 'Identity' -Status 'Failed' -ResultMarkdown 'SMS authentication configuration not found in Authentication Methods Policy.' -Risk 'High' -Name 'EIDSCAAS04: SMS - No Sign-In' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authentication Methods' return } @@ -47,10 +47,10 @@ Disabling SMS for sign-in while keeping it for MFA provides better security. "@ } - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AS04' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'EIDSCA.AS04: SMS - No Sign-In' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAS04' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'EIDSCAAS04: SMS - No Sign-In' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authentication Methods' } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AS04' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'EIDSCA.AS04: SMS - No Sign-In' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAS04' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'EIDSCAAS04: SMS - No Sign-In' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authentication Methods' } } diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AT01.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AT01.ps1 index e6191a184f78..4fb175f2e443 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AT01.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AT01.ps1 @@ -9,14 +9,14 @@ function Invoke-CippTestEIDSCA_AT01 { $AuthMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' if (-not $AuthMethodsPolicy) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AT01' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'EIDSCA.AT01: Temp Access Pass - State' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAT01' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'EIDSCAAT01: Temp Access Pass - State' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' return } $TAPConfig = $AuthMethodsPolicy.authenticationMethodConfigurations | Where-Object { $_.id -eq 'TemporaryAccessPass' } if (-not $TAPConfig) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AT01' -TestType 'Identity' -Status 'Failed' -ResultMarkdown 'Temporary Access Pass configuration not found in Authentication Methods Policy.' -Risk 'Medium' -Name 'EIDSCA.AT01: Temp Access Pass - State' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAT01' -TestType 'Identity' -Status 'Failed' -ResultMarkdown 'Temporary Access Pass configuration not found in Authentication Methods Policy.' -Risk 'Medium' -Name 'EIDSCAAT01: Temp Access Pass - State' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' return } @@ -38,10 +38,10 @@ Enabling TAP allows administrators to securely onboard users to passwordless aut "@ } - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AT01' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'EIDSCA.AT01: Temp Access Pass - State' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAT01' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'EIDSCAAT01: Temp Access Pass - State' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AT01' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'EIDSCA.AT01: Temp Access Pass - State' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAT01' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'EIDSCAAT01: Temp Access Pass - State' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' } } diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AT02.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AT02.ps1 index 34cfe29861a5..7149c8c91fff 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AT02.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AT02.ps1 @@ -9,14 +9,14 @@ function Invoke-CippTestEIDSCA_AT02 { $AuthMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' if (-not $AuthMethodsPolicy) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AT02' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'EIDSCA.AT02: Temp Access Pass - One-Time' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAT02' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'EIDSCAAT02: Temp Access Pass - One-Time' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' return } $TAPConfig = $AuthMethodsPolicy.authenticationMethodConfigurations | Where-Object { $_.id -eq 'TemporaryAccessPass' } if (-not $TAPConfig) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AT02' -TestType 'Identity' -Status 'Failed' -ResultMarkdown 'Temporary Access Pass configuration not found in Authentication Methods Policy.' -Risk 'Medium' -Name 'EIDSCA.AT02: Temp Access Pass - One-Time' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAT02' -TestType 'Identity' -Status 'Failed' -ResultMarkdown 'Temporary Access Pass configuration not found in Authentication Methods Policy.' -Risk 'Medium' -Name 'EIDSCAAT02: Temp Access Pass - One-Time' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' return } @@ -38,10 +38,10 @@ One-time use reduces the risk of TAP credential theft or misuse. "@ } - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AT02' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'EIDSCA.AT02: Temp Access Pass - One-Time' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAT02' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'EIDSCAAT02: Temp Access Pass - One-Time' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AT02' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'EIDSCA.AT02: Temp Access Pass - One-Time' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAT02' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'EIDSCAAT02: Temp Access Pass - One-Time' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' } } diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AV01.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AV01.ps1 index e079a3c781c4..6ae78ab10a72 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AV01.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AV01.ps1 @@ -9,14 +9,14 @@ function Invoke-CippTestEIDSCA_AV01 { $AuthMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' if (-not $AuthMethodsPolicy) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AV01' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'EIDSCA.AV01: Voice Call - Disabled' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAV01' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'EIDSCAAV01: Voice Call - Disabled' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authentication Methods' return } $VoiceConfig = $AuthMethodsPolicy.authenticationMethodConfigurations | Where-Object { $_.id -eq 'Voice' } if (-not $VoiceConfig) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AV01' -TestType 'Identity' -Status 'Failed' -ResultMarkdown 'Voice authentication configuration not found in Authentication Methods Policy.' -Risk 'Medium' -Name 'EIDSCA.AV01: Voice Call - Disabled' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAV01' -TestType 'Identity' -Status 'Failed' -ResultMarkdown 'Voice authentication configuration not found in Authentication Methods Policy.' -Risk 'Medium' -Name 'EIDSCAAV01: Voice Call - Disabled' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authentication Methods' return } @@ -38,10 +38,10 @@ Disabling voice calls reduces the attack surface by eliminating a less secure au "@ } - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AV01' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'EIDSCA.AV01: Voice Call - Disabled' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAV01' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'EIDSCAAV01: Voice Call - Disabled' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authentication Methods' } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.AV01' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'EIDSCA.AV01: Voice Call - Disabled' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAV01' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'EIDSCAAV01: Voice Call - Disabled' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authentication Methods' } } diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CP01.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CP01.ps1 index 870356a271d5..eee8028f2651 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CP01.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CP01.ps1 @@ -9,7 +9,7 @@ function Invoke-CippTestEIDSCA_CP01 { $Settings = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Settings' if (-not $Settings) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.CP01' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'EIDSCA.CP01: Consent Policy Settings - Group owner consent for apps accessing data' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Consent Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCACP01' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'EIDSCACP01: Consent Policy Settings - Group owner consent for apps accessing data' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Consent Policy' return } @@ -31,10 +31,10 @@ Group owner consent should be disabled to prevent unauthorized app permissions. "@ } - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.CP01' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'EIDSCA.CP01: Consent Policy Settings - Group owner consent for apps accessing data' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Consent Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCACP01' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'EIDSCACP01: Consent Policy Settings - Group owner consent for apps accessing data' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Consent Policy' } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.CP01' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'EIDSCA.CP01: Consent Policy Settings - Group owner consent for apps accessing data' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Consent Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCACP01' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'EIDSCACP01: Consent Policy Settings - Group owner consent for apps accessing data' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Consent Policy' } } diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CP03.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CP03.ps1 index ae45c78b1fab..cccc4fa620a8 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CP03.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CP03.ps1 @@ -9,7 +9,7 @@ function Invoke-CippTestEIDSCA_CP03 { $Settings = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Settings' if (-not $Settings) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.CP03' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'EIDSCA.CP03: Consent Policy Settings - Block user consent for risky apps' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Consent Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCACP03' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'EIDSCACP03: Consent Policy Settings - Block user consent for risky apps' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Consent Policy' return } @@ -31,10 +31,10 @@ User consent for risky apps should be blocked to prevent security risks. "@ } - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.CP03' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'EIDSCA.CP03: Consent Policy Settings - Block user consent for risky apps' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Consent Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCACP03' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'EIDSCACP03: Consent Policy Settings - Block user consent for risky apps' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Consent Policy' } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.CP03' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'EIDSCA.CP03: Consent Policy Settings - Block user consent for risky apps' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Consent Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCACP03' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'EIDSCACP03: Consent Policy Settings - Block user consent for risky apps' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Consent Policy' } } diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CP04.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CP04.ps1 index bfe0fb5e164d..3de484fec745 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CP04.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CP04.ps1 @@ -9,7 +9,7 @@ function Invoke-CippTestEIDSCA_CP04 { $Settings = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Settings' if (-not $Settings) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.CP04' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'EIDSCA.CP04: Consent Policy Settings - Users can request admin consent' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Consent Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCACP04' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'EIDSCACP04: Consent Policy Settings - Users can request admin consent' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Consent Policy' return } @@ -31,10 +31,10 @@ Users should be able to request admin consent to enable proper app approval work "@ } - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.CP04' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'EIDSCA.CP04: Consent Policy Settings - Users can request admin consent' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Consent Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCACP04' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'EIDSCACP04: Consent Policy Settings - Users can request admin consent' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Consent Policy' } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.CP04' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'EIDSCA.CP04: Consent Policy Settings - Users can request admin consent' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Consent Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCACP04' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'EIDSCACP04: Consent Policy Settings - Users can request admin consent' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Consent Policy' } } diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR01.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR01.ps1 index 3ffab6b19e10..04056b5130f6 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR01.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR01.ps1 @@ -9,7 +9,7 @@ function Invoke-CippTestEIDSCA_CR01 { $AdminConsentPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AdminConsentRequestPolicy' if (-not $AdminConsentPolicy) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.CR01' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'EIDSCA.CR01: Admin Consent - Enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Consent Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCACR01' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'EIDSCACR01: Admin Consent - Enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Consent Policy' return } @@ -31,10 +31,10 @@ Enabling this workflow provides a secure process for users to request access to "@ } - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.CR01' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'EIDSCA.CR01: Admin Consent - Enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Consent Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCACR01' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'EIDSCACR01: Admin Consent - Enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Consent Policy' } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.CR01' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'EIDSCA.CR01: Admin Consent - Enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Consent Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCACR01' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'EIDSCACR01: Admin Consent - Enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Consent Policy' } } diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR02.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR02.ps1 index 070a00e4ccd5..0e047797707c 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR02.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR02.ps1 @@ -9,7 +9,7 @@ function Invoke-CippTestEIDSCA_CR02 { $AdminConsentPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AdminConsentRequestPolicy' if (-not $AdminConsentPolicy) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.CR02' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'EIDSCA.CR02: Admin Consent - Notify Reviewers' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Consent Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCACR02' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'EIDSCACR02: Admin Consent - Notify Reviewers' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Consent Policy' return } @@ -31,10 +31,10 @@ Enabling notifications ensures reviewers are promptly informed of pending consen "@ } - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.CR02' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'EIDSCA.CR02: Admin Consent - Notify Reviewers' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Consent Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCACR02' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'EIDSCACR02: Admin Consent - Notify Reviewers' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Consent Policy' } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.CR02' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'EIDSCA.CR02: Admin Consent - Notify Reviewers' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Consent Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCACR02' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'EIDSCACR02: Admin Consent - Notify Reviewers' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Consent Policy' } } diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR03.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR03.ps1 index 55777e5f30e5..dad5cdeacc34 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR03.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR03.ps1 @@ -9,7 +9,7 @@ function Invoke-CippTestEIDSCA_CR03 { $AdminConsentPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AdminConsentRequestPolicy' if (-not $AdminConsentPolicy) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.CR03' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Low' -Name 'EIDSCA.CR03: Admin Consent - Reminders' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Consent Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCACR03' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Low' -Name 'EIDSCACR03: Admin Consent - Reminders' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Consent Policy' return } @@ -31,10 +31,10 @@ Enabling reminders helps prevent consent requests from being overlooked or delay "@ } - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.CR03' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Low' -Name 'EIDSCA.CR03: Admin Consent - Reminders' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Consent Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCACR03' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Low' -Name 'EIDSCACR03: Admin Consent - Reminders' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Consent Policy' } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.CR03' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Low' -Name 'EIDSCA.CR03: Admin Consent - Reminders' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Consent Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCACR03' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Low' -Name 'EIDSCACR03: Admin Consent - Reminders' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Consent Policy' } } diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR04.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR04.ps1 index 0016700dddf2..84152a41bb60 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR04.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR04.ps1 @@ -9,7 +9,7 @@ function Invoke-CippTestEIDSCA_CR04 { $AdminConsentPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AdminConsentRequestPolicy' if (-not $AdminConsentPolicy) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.CR04' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Low' -Name 'EIDSCA.CR04: Admin Consent - Duration' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Consent Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCACR04' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Low' -Name 'EIDSCACR04: Admin Consent - Duration' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Consent Policy' return } @@ -33,10 +33,10 @@ A shorter duration ensures consent requests are reviewed and processed in a time "@ } - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.CR04' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Low' -Name 'EIDSCA.CR04: Admin Consent - Duration' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Consent Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCACR04' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Low' -Name 'EIDSCACR04: Admin Consent - Duration' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Consent Policy' } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.CR04' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Low' -Name 'EIDSCA.CR04: Admin Consent - Duration' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Consent Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCACR04' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Low' -Name 'EIDSCACR04: Admin Consent - Duration' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Consent Policy' } } diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR01.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR01.ps1 index c2fc06d1a5ad..859ea75e1319 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR01.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR01.ps1 @@ -9,7 +9,7 @@ function Invoke-CippTestEIDSCA_PR01 { $Settings = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Settings' if (-not $Settings) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.PR01' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'EIDSCA.PR01: Password Rule Settings - Password Protection Mode' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Password Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAPR01' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'EIDSCAPR01: Password Rule Settings - Password Protection Mode' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Password Policy' return } @@ -31,10 +31,10 @@ Password protection mode should be set to Enforce to prevent weak passwords. "@ } - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.PR01' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'EIDSCA.PR01: Password Rule Settings - Password Protection Mode' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Password Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAPR01' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'EIDSCAPR01: Password Rule Settings - Password Protection Mode' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Password Policy' } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.PR01' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'EIDSCA.PR01: Password Rule Settings - Password Protection Mode' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Password Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAPR01' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'EIDSCAPR01: Password Rule Settings - Password Protection Mode' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Password Policy' } } diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR02.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR02.ps1 index 954f8a734216..e2effe56936b 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR02.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR02.ps1 @@ -9,7 +9,7 @@ function Invoke-CippTestEIDSCA_PR02 { $Settings = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Settings' if (-not $Settings) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.PR02' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'EIDSCA.PR02: Password Rule Settings - Enable password protection on Windows Server Active Directory' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Password Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAPR02' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'EIDSCAPR02: Password Rule Settings - Enable password protection on Windows Server Active Directory' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Password Policy' return } @@ -31,10 +31,10 @@ Password protection should be enabled for on-premises Active Directory to preven "@ } - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.PR02' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'EIDSCA.PR02: Password Rule Settings - Enable password protection on Windows Server Active Directory' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Password Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAPR02' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'EIDSCAPR02: Password Rule Settings - Enable password protection on Windows Server Active Directory' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Password Policy' } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.PR02' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'EIDSCA.PR02: Password Rule Settings - Enable password protection on Windows Server Active Directory' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Password Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAPR02' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'EIDSCAPR02: Password Rule Settings - Enable password protection on Windows Server Active Directory' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Password Policy' } } diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR03.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR03.ps1 index 698f4f62997e..92fe68a0912a 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR03.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR03.ps1 @@ -9,7 +9,7 @@ function Invoke-CippTestEIDSCA_PR03 { $Settings = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Settings' if (-not $Settings) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.PR03' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'EIDSCA.PR03: Password Rule Settings - Enforce custom list' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Password Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAPR03' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'EIDSCAPR03: Password Rule Settings - Enforce custom list' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Password Policy' return } @@ -31,10 +31,10 @@ Custom banned password list should be enforced to prevent common weak passwords. "@ } - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.PR03' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'EIDSCA.PR03: Password Rule Settings - Enforce custom list' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Password Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAPR03' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'EIDSCAPR03: Password Rule Settings - Enforce custom list' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Password Policy' } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.PR03' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'EIDSCA.PR03: Password Rule Settings - Enforce custom list' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Password Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAPR03' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'EIDSCAPR03: Password Rule Settings - Enforce custom list' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Password Policy' } } diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR05.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR05.ps1 index ed4367119b10..63c328b9c7c8 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR05.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR05.ps1 @@ -9,7 +9,7 @@ function Invoke-CippTestEIDSCA_PR05 { $Settings = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Settings' if (-not $Settings) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.PR05' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'EIDSCA.PR05: Password Rule Settings - Lockout duration in seconds' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Password Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAPR05' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'EIDSCAPR05: Password Rule Settings - Lockout duration in seconds' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Password Policy' return } @@ -31,10 +31,10 @@ Lockout duration should be at least 60 seconds to protect against brute force at "@ } - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.PR05' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'EIDSCA.PR05: Password Rule Settings - Lockout duration in seconds' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Password Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAPR05' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'EIDSCAPR05: Password Rule Settings - Lockout duration in seconds' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Password Policy' } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.PR05' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'EIDSCA.PR05: Password Rule Settings - Lockout duration in seconds' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Password Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAPR05' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'EIDSCAPR05: Password Rule Settings - Lockout duration in seconds' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Password Policy' } } diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR06.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR06.ps1 index ad485d415484..7508f08263d8 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR06.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR06.ps1 @@ -9,7 +9,7 @@ function Invoke-CippTestEIDSCA_PR06 { $Settings = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Settings' if (-not $Settings) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.PR06' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'EIDSCA.PR06: Password Rule Settings - Lockout threshold' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Password Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAPR06' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'EIDSCAPR06: Password Rule Settings - Lockout threshold' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Password Policy' return } @@ -31,10 +31,10 @@ Lockout threshold should be 10 or fewer failed attempts to protect against brute "@ } - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.PR06' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'EIDSCA.PR06: Password Rule Settings - Lockout threshold' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Password Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAPR06' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'EIDSCAPR06: Password Rule Settings - Lockout threshold' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Password Policy' } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.PR06' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'EIDSCA.PR06: Password Rule Settings - Lockout threshold' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Password Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAPR06' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'EIDSCAPR06: Password Rule Settings - Lockout threshold' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Password Policy' } } diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_ST08.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_ST08.ps1 index 203f9c49d2d4..017ea2253e68 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_ST08.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_ST08.ps1 @@ -9,7 +9,7 @@ function Invoke-CippTestEIDSCA_ST08 { $Settings = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Settings' if (-not $Settings) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.ST08' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'EIDSCA.ST08: Classification and M365 Groups - Allow Guests to become Group Owner' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Group Settings' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAST08' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'EIDSCAST08: Classification and M365 Groups - Allow Guests to become Group Owner' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Group Settings' return } @@ -31,10 +31,10 @@ Guests should not be allowed to become group owners to maintain proper access co "@ } - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.ST08' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'EIDSCA.ST08: Classification and M365 Groups - Allow Guests to become Group Owner' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Group Settings' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAST08' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'EIDSCAST08: Classification and M365 Groups - Allow Guests to become Group Owner' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Group Settings' } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.ST08' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'EIDSCA.ST08: Classification and M365 Groups - Allow Guests to become Group Owner' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Group Settings' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAST08' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'EIDSCAST08: Classification and M365 Groups - Allow Guests to become Group Owner' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Group Settings' } } diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_ST09.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_ST09.ps1 index 7ca935c9c8ae..33cd7fbeee13 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_ST09.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_ST09.ps1 @@ -9,7 +9,7 @@ function Invoke-CippTestEIDSCA_ST09 { $Settings = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Settings' if (-not $Settings) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.ST09' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Low' -Name 'EIDSCA.ST09: Classification and M365 Groups - Allow Guests to have access to groups content' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Group Settings' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAST09' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Low' -Name 'EIDSCAST09: Classification and M365 Groups - Allow Guests to have access to groups content' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Group Settings' return } @@ -31,10 +31,10 @@ Guests should be allowed to access groups content for proper collaboration. "@ } - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.ST09' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Low' -Name 'EIDSCA.ST09: Classification and M365 Groups - Allow Guests to have access to groups content' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Group Settings' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAST09' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Low' -Name 'EIDSCAST09: Classification and M365 Groups - Allow Guests to have access to groups content' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Group Settings' } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCA.ST09' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Low' -Name 'EIDSCA.ST09: Classification and M365 Groups - Allow Guests to have access to groups content' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Group Settings' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAST09' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Low' -Name 'EIDSCAST09: Classification and M365 Groups - Allow Guests to have access to groups content' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Group Settings' } } diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/report.json b/Modules/CIPPCore/Public/Tests/EIDSCA/report.json index efec36e1280c..2322f3f9597a 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/report.json +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/report.json @@ -5,49 +5,49 @@ "source": "https://github.com/maester365/maester", "category": "Identity Security", "IdentityTests": [ - "EIDSCA_AP01", - "EIDSCA_AP04", - "EIDSCA_AP05", - "EIDSCA_AP06", - "EIDSCA_AP07", - "EIDSCA_AP08", - "EIDSCA_AP09", - "EIDSCA_AP10", - "EIDSCA_AP14", - "EIDSCA_CP01", - "EIDSCA_CP03", - "EIDSCA_CP04", - "EIDSCA_PR01", - "EIDSCA_PR02", - "EIDSCA_PR03", - "EIDSCA_PR05", - "EIDSCA_PR06", - "EIDSCA_ST08", - "EIDSCA_ST09", - "EIDSCA_AG01", - "EIDSCA_AG02", - "EIDSCA_AG03", - "EIDSCA_AM01", - "EIDSCA_AM02", - "EIDSCA_AM03", - "EIDSCA_AM04", - "EIDSCA_AM06", - "EIDSCA_AM07", - "EIDSCA_AM09", - "EIDSCA_AM10", - "EIDSCA_AF01", - "EIDSCA_AF02", - "EIDSCA_AF03", - "EIDSCA_AF04", - "EIDSCA_AF05", - "EIDSCA_AF06", - "EIDSCA_AT01", - "EIDSCA_AT02", - "EIDSCA_AV01", - "EIDSCA_AS04", - "EIDSCA_CR01", - "EIDSCA_CR02", - "EIDSCA_CR03", - "EIDSCA_CR04" + "EIDSCAAP01", + "EIDSCAAP04", + "EIDSCAAP05", + "EIDSCAAP06", + "EIDSCAAP07", + "EIDSCAAP08", + "EIDSCAAP09", + "EIDSCAAP10", + "EIDSCAAP14", + "EIDSCACP01", + "EIDSCACP03", + "EIDSCACP04", + "EIDSCAPR01", + "EIDSCAPR02", + "EIDSCAPR03", + "EIDSCAPR05", + "EIDSCAPR06", + "EIDSCAST08", + "EIDSCAST09", + "EIDSCAAG01", + "EIDSCAAG02", + "EIDSCAAG03", + "EIDSCAAM01", + "EIDSCAAM02", + "EIDSCAAM03", + "EIDSCAAM04", + "EIDSCAAM06", + "EIDSCAAM07", + "EIDSCAAM09", + "EIDSCAAM10", + "EIDSCAAF01", + "EIDSCAAF02", + "EIDSCAAF03", + "EIDSCAAF04", + "EIDSCAAF05", + "EIDSCAAF06", + "EIDSCAAT01", + "EIDSCAAT02", + "EIDSCAAV01", + "EIDSCAAS04", + "EIDSCACR01", + "EIDSCACR02", + "EIDSCACR03", + "EIDSCACR04" ] } diff --git a/Test-AllZTNATests.ps1 b/Test-AllZTNATests.ps1 index b64f12d4ce28..8c371e090854 100644 --- a/Test-AllZTNATests.ps1 +++ b/Test-AllZTNATests.ps1 @@ -1,6 +1,6 @@ $Tenant = '7ngn50.onmicrosoft.com' $item =0 -Get-ChildItem "C:\Github\CIPP-API\Modules\CIPPCore\Public\Tests\Invoke-CippTest*.ps1" | ForEach-Object { +Get-ChildItem -Path 'C:\Github\CIPP-API\Modules\CIPPCore\Public\Tests' -Recurse -Filter 'Invoke-CippTest*.ps1'| ForEach-Object { $item++ write-host "performing test $($_.BaseName) - $($item)" From d5069f60c426b98ab10a86860483e14e97c42aa3 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Sun, 11 Jan 2026 11:56:38 +0100 Subject: [PATCH 105/503] Fix incorrect pass/fail markers --- .../Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM01.ps1 | 4 ++-- .../Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM02.ps1 | 4 ++-- .../Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM03.ps1 | 4 ++-- .../Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM04.ps1 | 4 ++-- .../Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM06.ps1 | 4 ++-- .../Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM07.ps1 | 4 ++-- .../Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM09.ps1 | 4 ++-- .../Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM10.ps1 | 4 ++-- 8 files changed, 16 insertions(+), 16 deletions(-) diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM01.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM01.ps1 index c75f3c7a6a73..cab3b249c2cb 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM01.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM01.ps1 @@ -16,10 +16,10 @@ function Invoke-CippTestEIDSCA_AM01 { $MethodConfig = $AuthMethodsPolicy.authenticationMethodConfigurations | Where-Object { $_.id -eq 'MicrosoftAuthenticator' } if ($MethodConfig.state -eq 'enabled') { - $Status = 'Pass' + $Status = 'Passed' $Result = 'Microsoft Authenticator authentication method is enabled.' } else { - $Status = 'Fail' + $Status = 'Failed' $Result = "Microsoft Authenticator authentication method is not enabled. Current state: $($MethodConfig.state)" } diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM02.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM02.ps1 index ef8cfe242459..06cbde264ed1 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM02.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM02.ps1 @@ -16,10 +16,10 @@ function Invoke-CippTestEIDSCA_AM02 { $MethodConfig = $AuthMethodsPolicy.authenticationMethodConfigurations | Where-Object { $_.id -eq 'MicrosoftAuthenticator' } if ($MethodConfig.isSoftwareOathEnabled -eq $false) { - $Status = 'Pass' + $Status = 'Passed' $Result = 'Microsoft Authenticator software OATH is disabled.' } else { - $Status = 'Fail' + $Status = 'Failed' $Result = "Microsoft Authenticator software OATH is enabled. It should be disabled." } diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM03.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM03.ps1 index d77f7b9b3a40..81e97b24e126 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM03.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM03.ps1 @@ -16,10 +16,10 @@ function Invoke-CippTestEIDSCA_AM03 { $MethodConfig = $AuthMethodsPolicy.authenticationMethodConfigurations | Where-Object { $_.id -eq 'MicrosoftAuthenticator' } if ($MethodConfig.featureSettings.numberMatchingRequiredState.state -eq 'enabled') { - $Status = 'Pass' + $Status = 'Passed' $Result = 'Microsoft Authenticator number matching is enabled.' } else { - $Status = 'Fail' + $Status = 'Failed' $Result = "Microsoft Authenticator number matching is not enabled. Current state: $($MethodConfig.featureSettings.numberMatchingRequiredState.state)" } diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM04.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM04.ps1 index fdb24d155943..3f5f3085637c 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM04.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM04.ps1 @@ -16,10 +16,10 @@ function Invoke-CippTestEIDSCA_AM04 { $MethodConfig = $AuthMethodsPolicy.authenticationMethodConfigurations | Where-Object { $_.id -eq 'MicrosoftAuthenticator' } if ($MethodConfig.featureSettings.numberMatchingRequiredState.includeTarget.id -eq 'all_users') { - $Status = 'Pass' + $Status = 'Passed' $Result = 'Microsoft Authenticator number matching targets all users.' } else { - $Status = 'Fail' + $Status = 'Failed' $Result = "Microsoft Authenticator number matching does not target all users. Current target: $($MethodConfig.featureSettings.numberMatchingRequiredState.includeTarget.id)" } diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM06.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM06.ps1 index 88a88a6a9b5a..0798382c8eb7 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM06.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM06.ps1 @@ -16,10 +16,10 @@ function Invoke-CippTestEIDSCA_AM06 { $MethodConfig = $AuthMethodsPolicy.authenticationMethodConfigurations | Where-Object { $_.id -eq 'MicrosoftAuthenticator' } if ($MethodConfig.featureSettings.displayAppInformationRequiredState.state -eq 'enabled') { - $Status = 'Pass' + $Status = 'Passed' $Result = 'Microsoft Authenticator app information display is enabled.' } else { - $Status = 'Fail' + $Status = 'Failed' $Result = "Microsoft Authenticator app information display is not enabled. Current state: $($MethodConfig.featureSettings.displayAppInformationRequiredState.state)" } diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM07.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM07.ps1 index 1b4ed36398ab..a130b3e27b37 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM07.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM07.ps1 @@ -16,10 +16,10 @@ function Invoke-CippTestEIDSCA_AM07 { $MethodConfig = $AuthMethodsPolicy.authenticationMethodConfigurations | Where-Object { $_.id -eq 'MicrosoftAuthenticator' } if ($MethodConfig.featureSettings.displayAppInformationRequiredState.includeTarget.id -eq 'all_users') { - $Status = 'Pass' + $Status = 'Passed' $Result = 'Microsoft Authenticator app information display targets all users.' } else { - $Status = 'Fail' + $Status = 'Failed' $Result = "Microsoft Authenticator app information display does not target all users. Current target: $($MethodConfig.featureSettings.displayAppInformationRequiredState.includeTarget.id)" } diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM09.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM09.ps1 index 6a6fd5a06a53..bbd2f360ae15 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM09.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM09.ps1 @@ -16,10 +16,10 @@ function Invoke-CippTestEIDSCA_AM09 { $MethodConfig = $AuthMethodsPolicy.authenticationMethodConfigurations | Where-Object { $_.id -eq 'MicrosoftAuthenticator' } if ($MethodConfig.featureSettings.displayLocationInformationRequiredState.state -eq 'enabled') { - $Status = 'Pass' + $Status = 'Passed' $Result = 'Microsoft Authenticator location information display is enabled.' } else { - $Status = 'Fail' + $Status = 'Failed' $Result = "Microsoft Authenticator location information display is not enabled. Current state: $($MethodConfig.featureSettings.displayLocationInformationRequiredState.state)" } diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM10.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM10.ps1 index 3ee0cfd29cbe..102ec058a615 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM10.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM10.ps1 @@ -16,10 +16,10 @@ function Invoke-CippTestEIDSCA_AM10 { $MethodConfig = $AuthMethodsPolicy.authenticationMethodConfigurations | Where-Object { $_.id -eq 'MicrosoftAuthenticator' } if ($MethodConfig.featureSettings.displayLocationInformationRequiredState.includeTarget.id -eq 'all_users') { - $Status = 'Pass' + $Status = 'Passed' $Result = 'Microsoft Authenticator location information display targets all users.' } else { - $Status = 'Fail' + $Status = 'Failed' $Result = "Microsoft Authenticator location information display does not target all users. Current target: $($MethodConfig.featureSettings.displayLocationInformationRequiredState.includeTarget.id)" } From 470597d1f11671227fb35e282bf22898a663bfc8 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Sun, 11 Jan 2026 12:04:23 +0100 Subject: [PATCH 106/503] EIDSCA test names --- .../Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF01.ps1 | 8 ++++---- .../Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF02.ps1 | 8 ++++---- .../Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF03.ps1 | 8 ++++---- .../Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF04.ps1 | 8 ++++---- .../Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF05.ps1 | 8 ++++---- .../Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF06.ps1 | 8 ++++---- .../Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AG01.ps1 | 8 ++++---- .../Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AG02.ps1 | 8 ++++---- .../Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AG03.ps1 | 8 ++++---- .../Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM01.ps1 | 8 ++++---- .../Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM02.ps1 | 8 ++++---- .../Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM03.ps1 | 8 ++++---- .../Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM04.ps1 | 8 ++++---- .../Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM06.ps1 | 8 ++++---- .../Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM07.ps1 | 8 ++++---- .../Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM09.ps1 | 8 ++++---- .../Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM10.ps1 | 8 ++++---- .../Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP01.ps1 | 8 ++++---- .../Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP04.ps1 | 8 ++++---- .../Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP05.ps1 | 8 ++++---- .../Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP06.ps1 | 8 ++++---- .../Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP07.ps1 | 8 ++++---- .../Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP08.ps1 | 8 ++++---- .../Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP09.ps1 | 8 ++++---- .../Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP10.ps1 | 8 ++++---- .../Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP14.ps1 | 8 ++++---- .../Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AS04.ps1 | 8 ++++---- .../Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AT01.ps1 | 8 ++++---- .../Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AT02.ps1 | 8 ++++---- .../Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AV01.ps1 | 8 ++++---- .../Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CP01.ps1 | 6 +++--- .../Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CP03.ps1 | 6 +++--- .../Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CP04.ps1 | 6 +++--- .../Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR01.ps1 | 6 +++--- .../Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR02.ps1 | 6 +++--- .../Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR03.ps1 | 6 +++--- .../Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR04.ps1 | 6 +++--- .../Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR01.ps1 | 6 +++--- .../Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR02.ps1 | 6 +++--- .../Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR03.ps1 | 6 +++--- .../Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR05.ps1 | 6 +++--- .../Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR06.ps1 | 6 +++--- .../Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_ST08.ps1 | 6 +++--- .../Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_ST09.ps1 | 6 +++--- 44 files changed, 162 insertions(+), 162 deletions(-) diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF01.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF01.ps1 index 21fd43de3505..6b75c6486d19 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF01.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF01.ps1 @@ -9,14 +9,14 @@ function Invoke-CippTestEIDSCA_AF01 { $AuthMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' if (-not $AuthMethodsPolicy) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAF01' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Low' -Name 'EIDSCAAF01: FIDO2 - State' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAF01' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Low' -Name 'FIDO2 - State' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Authentication Methods' return } $Fido2Config = $AuthMethodsPolicy.authenticationMethodConfigurations | Where-Object { $_.id -eq 'Fido2' } if (-not $Fido2Config) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAF01' -TestType 'Identity' -Status 'Failed' -ResultMarkdown 'FIDO2 configuration not found in Authentication Methods Policy.' -Risk 'Low' -Name 'EIDSCAAF01: FIDO2 - State' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAF01' -TestType 'Identity' -Status 'Failed' -ResultMarkdown 'FIDO2 configuration not found in Authentication Methods Policy.' -Risk 'Low' -Name 'FIDO2 - State' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Authentication Methods' return } @@ -38,10 +38,10 @@ Enabling FIDO2 provides users with a secure, passwordless authentication option. "@ } - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAF01' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Low' -Name 'EIDSCAAF01: FIDO2 - State' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAF01' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Low' -Name 'FIDO2 - State' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Authentication Methods' } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAF01' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Low' -Name 'EIDSCAAF01: FIDO2 - State' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAF01' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Low' -Name 'FIDO2 - State' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Authentication Methods' } } diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF02.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF02.ps1 index 41c61c539bec..95defc1727d1 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF02.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF02.ps1 @@ -9,14 +9,14 @@ function Invoke-CippTestEIDSCA_AF02 { $AuthMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' if (-not $AuthMethodsPolicy) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAF02' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Low' -Name 'EIDSCAAF02: FIDO2 - Self-Service' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAF02' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Low' -Name 'FIDO2 - Self-Service' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' return } $Fido2Config = $AuthMethodsPolicy.authenticationMethodConfigurations | Where-Object { $_.id -eq 'Fido2' } if (-not $Fido2Config) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAF02' -TestType 'Identity' -Status 'Failed' -ResultMarkdown 'FIDO2 configuration not found in Authentication Methods Policy.' -Risk 'Low' -Name 'EIDSCAAF02: FIDO2 - Self-Service' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAF02' -TestType 'Identity' -Status 'Failed' -ResultMarkdown 'FIDO2 configuration not found in Authentication Methods Policy.' -Risk 'Low' -Name 'FIDO2 - Self-Service' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' return } @@ -38,10 +38,10 @@ Enabling self-service registration improves user experience and adoption of FIDO "@ } - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAF02' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Low' -Name 'EIDSCAAF02: FIDO2 - Self-Service' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAF02' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Low' -Name 'FIDO2 - Self-Service' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAF02' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Low' -Name 'EIDSCAAF02: FIDO2 - Self-Service' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAF02' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Low' -Name 'FIDO2 - Self-Service' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' } } diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF03.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF03.ps1 index dbc30f48c606..eb6f808b71b5 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF03.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF03.ps1 @@ -9,14 +9,14 @@ function Invoke-CippTestEIDSCA_AF03 { $AuthMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' if (-not $AuthMethodsPolicy) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAF03' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'EIDSCAAF03: FIDO2 - Attestation' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAF03' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'FIDO2 - Attestation' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Authentication Methods' return } $Fido2Config = $AuthMethodsPolicy.authenticationMethodConfigurations | Where-Object { $_.id -eq 'Fido2' } if (-not $Fido2Config) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAF03' -TestType 'Identity' -Status 'Failed' -ResultMarkdown 'FIDO2 configuration not found in Authentication Methods Policy.' -Risk 'Medium' -Name 'EIDSCAAF03: FIDO2 - Attestation' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAF03' -TestType 'Identity' -Status 'Failed' -ResultMarkdown 'FIDO2 configuration not found in Authentication Methods Policy.' -Risk 'Medium' -Name 'FIDO2 - Attestation' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Authentication Methods' return } @@ -38,10 +38,10 @@ Enforcing attestation ensures that only trusted and verified security keys can b "@ } - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAF03' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'EIDSCAAF03: FIDO2 - Attestation' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAF03' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'FIDO2 - Attestation' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Authentication Methods' } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAF03' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'EIDSCAAF03: FIDO2 - Attestation' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAF03' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'FIDO2 - Attestation' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Authentication Methods' } } diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF04.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF04.ps1 index fc28377a8df5..aac28ffce9df 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF04.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF04.ps1 @@ -9,14 +9,14 @@ function Invoke-CippTestEIDSCA_AF04 { $AuthMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' if (-not $AuthMethodsPolicy) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAF04' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'EIDSCAAF04: FIDO2 - Key Restrictions' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAF04' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'FIDO2 - Key Restrictions' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Authentication Methods' return } $Fido2Config = $AuthMethodsPolicy.authenticationMethodConfigurations | Where-Object { $_.id -eq 'Fido2' } if (-not $Fido2Config) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAF04' -TestType 'Identity' -Status 'Failed' -ResultMarkdown 'FIDO2 configuration not found in Authentication Methods Policy.' -Risk 'Medium' -Name 'EIDSCAAF04: FIDO2 - Key Restrictions' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAF04' -TestType 'Identity' -Status 'Failed' -ResultMarkdown 'FIDO2 configuration not found in Authentication Methods Policy.' -Risk 'Medium' -Name 'FIDO2 - Key Restrictions' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Authentication Methods' return } @@ -38,10 +38,10 @@ Enforcing key restrictions helps ensure only approved security keys are used in "@ } - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAF04' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'EIDSCAAF04: FIDO2 - Key Restrictions' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAF04' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'FIDO2 - Key Restrictions' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Authentication Methods' } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAF04' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'EIDSCAAF04: FIDO2 - Key Restrictions' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAF04' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'FIDO2 - Key Restrictions' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Authentication Methods' } } diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF05.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF05.ps1 index 61ffa0605282..56647ccee952 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF05.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF05.ps1 @@ -9,14 +9,14 @@ function Invoke-CippTestEIDSCA_AF05 { $AuthMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' if (-not $AuthMethodsPolicy) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAF05' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'EIDSCAAF05: FIDO2 - Restricted Keys' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAF05' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'FIDO2 - Restricted Keys' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Authentication Methods' return } $Fido2Config = $AuthMethodsPolicy.authenticationMethodConfigurations | Where-Object { $_.id -eq 'Fido2' } if (-not $Fido2Config) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAF05' -TestType 'Identity' -Status 'Failed' -ResultMarkdown 'FIDO2 configuration not found in Authentication Methods Policy.' -Risk 'Medium' -Name 'EIDSCAAF05: FIDO2 - Restricted Keys' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAF05' -TestType 'Identity' -Status 'Failed' -ResultMarkdown 'FIDO2 configuration not found in Authentication Methods Policy.' -Risk 'Medium' -Name 'FIDO2 - Restricted Keys' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Authentication Methods' return } @@ -39,10 +39,10 @@ Specifying AAGuids allows you to restrict registration to specific, approved sec "@ } - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAF05' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'EIDSCAAF05: FIDO2 - Restricted Keys' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAF05' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'FIDO2 - Restricted Keys' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Authentication Methods' } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAF05' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'EIDSCAAF05: FIDO2 - Restricted Keys' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAF05' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'FIDO2 - Restricted Keys' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Authentication Methods' } } diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF06.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF06.ps1 index 44a21cbb8636..fc4117c01b43 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF06.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF06.ps1 @@ -9,14 +9,14 @@ function Invoke-CippTestEIDSCA_AF06 { $AuthMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' if (-not $AuthMethodsPolicy) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAF06' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'EIDSCAAF06: FIDO2 - Specific Keys' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAF06' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'FIDO2 - Specific Keys' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Authentication Methods' return } $Fido2Config = $AuthMethodsPolicy.authenticationMethodConfigurations | Where-Object { $_.id -eq 'Fido2' } if (-not $Fido2Config) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAF06' -TestType 'Identity' -Status 'Failed' -ResultMarkdown 'FIDO2 configuration not found in Authentication Methods Policy.' -Risk 'Medium' -Name 'EIDSCAAF06: FIDO2 - Specific Keys' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAF06' -TestType 'Identity' -Status 'Failed' -ResultMarkdown 'FIDO2 configuration not found in Authentication Methods Policy.' -Risk 'Medium' -Name 'FIDO2 - Specific Keys' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Authentication Methods' return } @@ -43,10 +43,10 @@ Proper enforcement type ensures the AAGuids list is actively used to control key "@ } - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAF06' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'EIDSCAAF06: FIDO2 - Specific Keys' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAF06' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'FIDO2 - Specific Keys' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Authentication Methods' } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAF06' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'EIDSCAAF06: FIDO2 - Specific Keys' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAF06' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'FIDO2 - Specific Keys' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Authentication Methods' } } diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AG01.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AG01.ps1 index 5f1a2ff07fcf..b8998cc56529 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AG01.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AG01.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestEIDSCA_AG01 { <# .SYNOPSIS - Authentication Method - General Settings - Manage migration + Authentication Methods - Policy Migration #> param($Tenant) @@ -9,7 +9,7 @@ function Invoke-CippTestEIDSCA_AG01 { $AuthMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' if (-not $AuthMethodsPolicy) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAG01' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'EIDSCAAG01: Authentication Methods - Policy Migration' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAG01' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'Authentication Methods - Policy Migration' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' return } @@ -33,10 +33,10 @@ Complete the migration to use the modern authentication methods policy. "@ } - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAG01' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'EIDSCAAG01: Authentication Methods - Policy Migration' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAG01' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'Authentication Methods - Policy Migration' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAG01' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'EIDSCAAG01: Authentication Methods - Policy Migration' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAG01' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'Authentication Methods - Policy Migration' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' } } diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AG02.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AG02.ps1 index 28f7d1f0bef2..160a8373c972 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AG02.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AG02.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestEIDSCA_AG02 { <# .SYNOPSIS - Authentication Method - General Settings - Report suspicious activity State + Authentication Methods - Report Suspicious Activity #> param($Tenant) @@ -9,7 +9,7 @@ function Invoke-CippTestEIDSCA_AG02 { $AuthMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' if (-not $AuthMethodsPolicy) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAG02' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'EIDSCAAG02: Authentication Methods - Report Suspicious Activity' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAG02' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'Authentication Methods - Report Suspicious Activity' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' return } @@ -33,10 +33,10 @@ This feature helps detect and prevent unauthorized access attempts. "@ } - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAG02' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'EIDSCAAG02: Authentication Methods - Report Suspicious Activity' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAG02' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'Authentication Methods - Report Suspicious Activity' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAG02' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'EIDSCAAG02: Authentication Methods - Report Suspicious Activity' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAG02' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'Authentication Methods - Report Suspicious Activity' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' } } diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AG03.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AG03.ps1 index 7e916405b7ee..881ede11cd79 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AG03.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AG03.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestEIDSCA_AG03 { <# .SYNOPSIS - Authentication Method - General Settings - Report suspicious activity users/groups + Authentication Methods - Suspicious Activity Target #> param($Tenant) @@ -9,7 +9,7 @@ function Invoke-CippTestEIDSCA_AG03 { $AuthMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' if (-not $AuthMethodsPolicy) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAG03' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'EIDSCAAG03: Authentication Methods - Suspicious Activity Target' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAG03' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'Authentication Methods - Suspicious Activity Target' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' return } @@ -33,10 +33,10 @@ All users should be able to report suspicious authentication attempts. "@ } - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAG03' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'EIDSCAAG03: Authentication Methods - Suspicious Activity Target' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAG03' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'Authentication Methods - Suspicious Activity Target' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAG03' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'EIDSCAAG03: Authentication Methods - Suspicious Activity Target' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAG03' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'Authentication Methods - Suspicious Activity Target' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' } } diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM01.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM01.ps1 index cab3b249c2cb..e64f9dcdbf16 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM01.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM01.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestEIDSCA_AM01 { <# .SYNOPSIS - Checks if Microsoft Authenticator authentication method is enabled + MS Authenticator - State #> param($Tenant) @@ -9,7 +9,7 @@ function Invoke-CippTestEIDSCA_AM01 { $AuthMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' if (-not $AuthMethodsPolicy) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAM01' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'EIDSCAAM01: MS Authenticator - State' -UserImpact 'High' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAM01' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'MS Authenticator - State' -UserImpact 'High' -ImplementationEffort 'Low' -Category 'Authentication Methods' return } @@ -23,10 +23,10 @@ function Invoke-CippTestEIDSCA_AM01 { $Result = "Microsoft Authenticator authentication method is not enabled. Current state: $($MethodConfig.state)" } - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAM01' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'EIDSCAAM01: MS Authenticator - State' -UserImpact 'High' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAM01' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'MS Authenticator - State' -UserImpact 'High' -ImplementationEffort 'Low' -Category 'Authentication Methods' } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAM01' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'EIDSCAAM01: MS Authenticator - State' -UserImpact 'High' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAM01' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'MS Authenticator - State' -UserImpact 'High' -ImplementationEffort 'Low' -Category 'Authentication Methods' } } diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM02.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM02.ps1 index 06cbde264ed1..4be6ebe70109 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM02.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM02.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestEIDSCA_AM02 { <# .SYNOPSIS - Checks if Microsoft Authenticator software OATH is disabled + MS Authenticator - OTP Disabled #> param($Tenant) @@ -9,7 +9,7 @@ function Invoke-CippTestEIDSCA_AM02 { $AuthMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' if (-not $AuthMethodsPolicy) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAM02' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'EIDSCAAM02: MS Authenticator - OTP Disabled' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAM02' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'MS Authenticator - OTP Disabled' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authentication Methods' return } @@ -23,10 +23,10 @@ function Invoke-CippTestEIDSCA_AM02 { $Result = "Microsoft Authenticator software OATH is enabled. It should be disabled." } - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAM02' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'EIDSCAAM02: MS Authenticator - OTP Disabled' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAM02' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'MS Authenticator - OTP Disabled' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authentication Methods' } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAM02' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'EIDSCAAM02: MS Authenticator - OTP Disabled' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAM02' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'MS Authenticator - OTP Disabled' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authentication Methods' } } diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM03.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM03.ps1 index 81e97b24e126..f1357097688c 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM03.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM03.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestEIDSCA_AM03 { <# .SYNOPSIS - Checks if Microsoft Authenticator number matching is enabled + MS Authenticator - Number Matching #> param($Tenant) @@ -9,7 +9,7 @@ function Invoke-CippTestEIDSCA_AM03 { $AuthMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' if (-not $AuthMethodsPolicy) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAM03' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'EIDSCAAM03: MS Authenticator - Number Matching' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAM03' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'MS Authenticator - Number Matching' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authentication Methods' return } @@ -23,10 +23,10 @@ function Invoke-CippTestEIDSCA_AM03 { $Result = "Microsoft Authenticator number matching is not enabled. Current state: $($MethodConfig.featureSettings.numberMatchingRequiredState.state)" } - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAM03' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'EIDSCAAM03: MS Authenticator - Number Matching' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAM03' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'MS Authenticator - Number Matching' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authentication Methods' } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAM03' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'EIDSCAAM03: MS Authenticator - Number Matching' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAM03' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'MS Authenticator - Number Matching' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authentication Methods' } } diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM04.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM04.ps1 index 3f5f3085637c..da54722211c9 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM04.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM04.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestEIDSCA_AM04 { <# .SYNOPSIS - Checks if Microsoft Authenticator number matching targets all users + MS Authenticator - Number Matching Target #> param($Tenant) @@ -9,7 +9,7 @@ function Invoke-CippTestEIDSCA_AM04 { $AuthMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' if (-not $AuthMethodsPolicy) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAM04' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'EIDSCAAM04: MS Authenticator - Number Matching Target' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAM04' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'MS Authenticator - Number Matching Target' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' return } @@ -23,10 +23,10 @@ function Invoke-CippTestEIDSCA_AM04 { $Result = "Microsoft Authenticator number matching does not target all users. Current target: $($MethodConfig.featureSettings.numberMatchingRequiredState.includeTarget.id)" } - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAM04' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'EIDSCAAM04: MS Authenticator - Number Matching Target' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAM04' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'MS Authenticator - Number Matching Target' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAM04' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'EIDSCAAM04: MS Authenticator - Number Matching Target' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAM04' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'MS Authenticator - Number Matching Target' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' } } diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM06.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM06.ps1 index 0798382c8eb7..686561718d8f 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM06.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM06.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestEIDSCA_AM06 { <# .SYNOPSIS - Checks if Microsoft Authenticator app information display is enabled + MS Authenticator - Show App Name #> param($Tenant) @@ -9,7 +9,7 @@ function Invoke-CippTestEIDSCA_AM06 { $AuthMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' if (-not $AuthMethodsPolicy) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAM06' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'EIDSCAAM06: MS Authenticator - Show App Name' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAM06' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'MS Authenticator - Show App Name' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' return } @@ -23,10 +23,10 @@ function Invoke-CippTestEIDSCA_AM06 { $Result = "Microsoft Authenticator app information display is not enabled. Current state: $($MethodConfig.featureSettings.displayAppInformationRequiredState.state)" } - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAM06' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'EIDSCAAM06: MS Authenticator - Show App Name' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAM06' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'MS Authenticator - Show App Name' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAM06' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'EIDSCAAM06: MS Authenticator - Show App Name' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAM06' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'MS Authenticator - Show App Name' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' } } diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM07.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM07.ps1 index a130b3e27b37..81799a7d798d 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM07.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM07.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestEIDSCA_AM07 { <# .SYNOPSIS - Checks if Microsoft Authenticator app information display targets all users + MS Authenticator - Show App Name Target #> param($Tenant) @@ -9,7 +9,7 @@ function Invoke-CippTestEIDSCA_AM07 { $AuthMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' if (-not $AuthMethodsPolicy) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAM07' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'EIDSCAAM07: MS Authenticator - Show App Name Target' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAM07' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'MS Authenticator - Show App Name Target' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' return } @@ -23,10 +23,10 @@ function Invoke-CippTestEIDSCA_AM07 { $Result = "Microsoft Authenticator app information display does not target all users. Current target: $($MethodConfig.featureSettings.displayAppInformationRequiredState.includeTarget.id)" } - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAM07' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'EIDSCAAM07: MS Authenticator - Show App Name Target' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAM07' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'MS Authenticator - Show App Name Target' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAM07' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'EIDSCAAM07: MS Authenticator - Show App Name Target' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAM07' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'MS Authenticator - Show App Name Target' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' } } diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM09.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM09.ps1 index bbd2f360ae15..9be8d3f3b57e 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM09.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM09.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestEIDSCA_AM09 { <# .SYNOPSIS - Checks if Microsoft Authenticator location information display is enabled + MS Authenticator - Show Location #> param($Tenant) @@ -9,7 +9,7 @@ function Invoke-CippTestEIDSCA_AM09 { $AuthMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' if (-not $AuthMethodsPolicy) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAM09' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'EIDSCAAM09: MS Authenticator - Show Location' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAM09' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'MS Authenticator - Show Location' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' return } @@ -23,11 +23,11 @@ function Invoke-CippTestEIDSCA_AM09 { $Result = "Microsoft Authenticator location information display is not enabled. Current state: $($MethodConfig.featureSettings.displayLocationInformationRequiredState.state)" } - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAM09' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'EIDSCAAM09: MS Authenticator - Show Location' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAM09' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'MS Authenticator - Show Location' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAM09' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'EIDSCAAM09: MS Authenticator - Show Location' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAM09' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'MS Authenticator - Show Location' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' } } diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM10.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM10.ps1 index 102ec058a615..911bb7d26164 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM10.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM10.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestEIDSCA_AM10 { <# .SYNOPSIS - Checks if Microsoft Authenticator location information display targets all users + MS Authenticator - Show Location Target #> param($Tenant) @@ -9,7 +9,7 @@ function Invoke-CippTestEIDSCA_AM10 { $AuthMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' if (-not $AuthMethodsPolicy) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAM10' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'EIDSCAAM10: MS Authenticator - Show Location Target' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAM10' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'MS Authenticator - Show Location Target' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' return } @@ -23,10 +23,10 @@ function Invoke-CippTestEIDSCA_AM10 { $Result = "Microsoft Authenticator location information display does not target all users. Current target: $($MethodConfig.featureSettings.displayLocationInformationRequiredState.includeTarget.id)" } - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAM10' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'EIDSCAAM10: MS Authenticator - Show Location Target' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAM10' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'MS Authenticator - Show Location Target' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAM10' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'EIDSCAAM10: MS Authenticator - Show Location Target' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAM10' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'MS Authenticator - Show Location Target' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' } } diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP01.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP01.ps1 index a860fadf7170..413ba9d40fc9 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP01.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP01.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestEIDSCA_AP01 { <# .SYNOPSIS - Default Authorization Settings - Enabled Self service password reset for administrators + Authorization Policy - Self-Service Password Reset for Admins #> param($Tenant) @@ -9,7 +9,7 @@ function Invoke-CippTestEIDSCA_AP01 { $AuthorizationPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthorizationPolicy' if (-not $AuthorizationPolicy) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAP01' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'EIDSCAAP01: Authorization Policy - Self-Service Password Reset for Admins' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authorization Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAP01' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'Authorization Policy - Self-Service Password Reset for Admins' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authorization Policy' return } @@ -33,10 +33,10 @@ Administrators should follow more stringent password reset procedures rather tha "@ } - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAP01' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'EIDSCAAP01: Authorization Policy - Self-Service Password Reset for Admins' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authorization Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAP01' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'Authorization Policy - Self-Service Password Reset for Admins' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authorization Policy' } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAP01' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'EIDSCAAP01: Authorization Policy - Self-Service Password Reset for Admins' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authorization Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAP01' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Authorization Policy - Self-Service Password Reset for Admins' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authorization Policy' } } diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP04.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP04.ps1 index 5fc62c88e6d1..1daeea3017eb 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP04.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP04.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestEIDSCA_AP04 { <# .SYNOPSIS - Default Authorization Settings - Guest invite restrictions + Authorization Policy - Guest Invite Restrictions #> param($Tenant) @@ -9,7 +9,7 @@ function Invoke-CippTestEIDSCA_AP04 { $AuthorizationPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthorizationPolicy' if (-not $AuthorizationPolicy) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAP04' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'EIDSCAAP04: Authorization Policy - Guest Invite Restrictions' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authorization Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAP04' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'Authorization Policy - Guest Invite Restrictions' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authorization Policy' return } @@ -33,10 +33,10 @@ Restricting guest invitations helps maintain control over external access to you "@ } - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAP04' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'EIDSCAAP04: Authorization Policy - Guest Invite Restrictions' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authorization Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAP04' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'Authorization Policy - Guest Invite Restrictions' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authorization Policy' } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAP04' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'EIDSCAAP04: Authorization Policy - Guest Invite Restrictions' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authorization Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAP04' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Authorization Policy - Guest Invite Restrictions' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authorization Policy' } } diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP05.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP05.ps1 index fab35cc80467..c9cad439a372 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP05.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP05.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestEIDSCA_AP05 { <# .SYNOPSIS - Default Authorization Settings - Sign-up for email based subscription + Authorization Policy - Email-Based Subscription Sign-up #> param($Tenant) @@ -9,7 +9,7 @@ function Invoke-CippTestEIDSCA_AP05 { $AuthorizationPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthorizationPolicy' if (-not $AuthorizationPolicy) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAP05' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'EIDSCAAP05: Authorization Policy - Email-Based Subscription Sign-up' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authorization Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAP05' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'Authorization Policy - Email-Based Subscription Sign-up' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authorization Policy' return } @@ -33,10 +33,10 @@ Disabling email-based subscriptions helps maintain control over tenant access. "@ } - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAP05' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'EIDSCAAP05: Authorization Policy - Email-Based Subscription Sign-up' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authorization Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAP05' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'Authorization Policy - Email-Based Subscription Sign-up' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authorization Policy' } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAP05' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'EIDSCAAP05: Authorization Policy - Email-Based Subscription Sign-up' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authorization Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAP05' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'Authorization Policy - Email-Based Subscription Sign-up' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authorization Policy' } } diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP06.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP06.ps1 index 7e33f01d5359..8ce6aab74337 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP06.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP06.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestEIDSCA_AP06 { <# .SYNOPSIS - Default Authorization Settings - User can join the tenant by email validation + Authorization Policy - Email Validation Join #> param($Tenant) @@ -9,7 +9,7 @@ function Invoke-CippTestEIDSCA_AP06 { $AuthorizationPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthorizationPolicy' if (-not $AuthorizationPolicy) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAP06' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'EIDSCAAP06: Authorization Policy - Email Validation Join' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authorization Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAP06' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'Authorization Policy - Email Validation Join' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authorization Policy' return } @@ -33,10 +33,10 @@ Disabling this feature prevents unauthorized users from self-registering into yo "@ } - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAP06' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'EIDSCAAP06: Authorization Policy - Email Validation Join' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authorization Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAP06' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'Authorization Policy - Email Validation Join' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authorization Policy' } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAP06' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'EIDSCAAP06: Authorization Policy - Email Validation Join' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authorization Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAP06' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Authorization Policy - Email Validation Join' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authorization Policy' } } diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP07.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP07.ps1 index 49c188c656b9..89b0152d6446 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP07.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP07.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestEIDSCA_AP07 { <# .SYNOPSIS - Default Authorization Settings - Guest user access + Authorization Policy - Guest User Access #> param($Tenant) @@ -9,7 +9,7 @@ function Invoke-CippTestEIDSCA_AP07 { $AuthorizationPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthorizationPolicy' if (-not $AuthorizationPolicy) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAP07' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'EIDSCAAP07: Authorization Policy - Guest User Access' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authorization Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAP07' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'Authorization Policy - Guest User Access' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authorization Policy' return } @@ -34,10 +34,10 @@ This setting limits what guest users can see and do in your directory. "@ } - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAP07' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'EIDSCAAP07: Authorization Policy - Guest User Access' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authorization Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAP07' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'Authorization Policy - Guest User Access' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authorization Policy' } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAP07' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'EIDSCAAP07: Authorization Policy - Guest User Access' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authorization Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAP07' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Authorization Policy - Guest User Access' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authorization Policy' } } diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP08.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP08.ps1 index b95e4b675ca2..2da9571bb90d 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP08.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP08.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestEIDSCA_AP08 { <# .SYNOPSIS - Default Authorization Settings - User consent policy assigned for applications + Authorization Policy - User Consent Policy #> param($Tenant) @@ -9,7 +9,7 @@ function Invoke-CippTestEIDSCA_AP08 { $AuthorizationPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthorizationPolicy' if (-not $AuthorizationPolicy) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAP08' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'EIDSCAAP08: Authorization Policy - User Consent Policy' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authorization Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAP08' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'Authorization Policy - User Consent Policy' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authorization Policy' return } @@ -34,10 +34,10 @@ This limits users to only consent to applications with low-risk permissions. "@ } - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAP08' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'EIDSCAAP08: Authorization Policy - User Consent Policy' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authorization Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAP08' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'Authorization Policy - User Consent Policy' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authorization Policy' } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAP08' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'EIDSCAAP08: Authorization Policy - User Consent Policy' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authorization Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAP08' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Authorization Policy - User Consent Policy' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authorization Policy' } } diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP09.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP09.ps1 index 6c3597a07af2..c22d0408da89 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP09.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP09.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestEIDSCA_AP09 { <# .SYNOPSIS - Default Authorization Settings - Allow user consent on risk-based apps + Authorization Policy - Consent for Risky Apps #> param($Tenant) @@ -9,7 +9,7 @@ function Invoke-CippTestEIDSCA_AP09 { $AuthorizationPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthorizationPolicy' if (-not $AuthorizationPolicy) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAP09' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'EIDSCAAP09: Authorization Policy - Consent for Risky Apps' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authorization Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAP09' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'Authorization Policy - Consent for Risky Apps' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authorization Policy' return } @@ -33,10 +33,10 @@ Disabling this prevents users from consenting to apps identified as risky. "@ } - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAP09' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'EIDSCAAP09: Authorization Policy - Consent for Risky Apps' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authorization Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAP09' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'Authorization Policy - Consent for Risky Apps' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authorization Policy' } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAP09' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'EIDSCAAP09: Authorization Policy - Consent for Risky Apps' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authorization Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAP09' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Authorization Policy - Consent for Risky Apps' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authorization Policy' } } diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP10.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP10.ps1 index 542b4ad15755..51ec0f4ee107 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP10.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP10.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestEIDSCA_AP10 { <# .SYNOPSIS - Default Authorization Settings - Default User Role Permissions - Allowed to create Apps + Authorization Policy - Users Can Create Apps #> param($Tenant) @@ -9,7 +9,7 @@ function Invoke-CippTestEIDSCA_AP10 { $AuthorizationPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthorizationPolicy' if (-not $AuthorizationPolicy) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAP10' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'EIDSCAAP10: Authorization Policy - Users Can Create Apps' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authorization Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAP10' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'Authorization Policy - Users Can Create Apps' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authorization Policy' return } @@ -33,10 +33,10 @@ Only authorized users should be able to register applications in your tenant. "@ } - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAP10' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'EIDSCAAP10: Authorization Policy - Users Can Create Apps' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authorization Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAP10' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'Authorization Policy - Users Can Create Apps' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authorization Policy' } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAP10' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'EIDSCAAP10: Authorization Policy - Users Can Create Apps' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authorization Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAP10' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'Authorization Policy - Users Can Create Apps' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authorization Policy' } } diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP14.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP14.ps1 index 0bf9df0077fb..7270f3a15eef 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP14.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP14.ps1 @@ -1,7 +1,7 @@ function Invoke-CippTestEIDSCA_AP14 { <# .SYNOPSIS - Default Authorization Settings - Default User Role Permissions - Allowed to read other users + Authorization Policy - Users Can Read Other Users #> param($Tenant) @@ -9,7 +9,7 @@ function Invoke-CippTestEIDSCA_AP14 { $AuthorizationPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthorizationPolicy' if (-not $AuthorizationPolicy) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAP14' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Low' -Name 'EIDSCAAP14: Authorization Policy - Users Can Read Other Users' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authorization Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAP14' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Low' -Name 'Authorization Policy - Users Can Read Other Users' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authorization Policy' return } @@ -33,10 +33,10 @@ This setting enables basic collaboration features like Teams and SharePoint. "@ } - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAP14' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Low' -Name 'EIDSCAAP14: Authorization Policy - Users Can Read Other Users' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authorization Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAP14' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Low' -Name 'Authorization Policy - Users Can Read Other Users' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authorization Policy' } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAP14' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Low' -Name 'EIDSCAAP14: Authorization Policy - Users Can Read Other Users' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authorization Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAP14' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Low' -Name 'Authorization Policy - Users Can Read Other Users' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authorization Policy' } } diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AS04.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AS04.ps1 index ae95b95829be..86265496c2e7 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AS04.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AS04.ps1 @@ -9,14 +9,14 @@ function Invoke-CippTestEIDSCA_AS04 { $AuthMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' if (-not $AuthMethodsPolicy) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAS04' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'EIDSCAAS04: SMS - No Sign-In' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAS04' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'SMS - No Sign-In' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authentication Methods' return } $SmsConfig = $AuthMethodsPolicy.authenticationMethodConfigurations | Where-Object { $_.id -eq 'Sms' } if (-not $SmsConfig) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAS04' -TestType 'Identity' -Status 'Failed' -ResultMarkdown 'SMS authentication configuration not found in Authentication Methods Policy.' -Risk 'High' -Name 'EIDSCAAS04: SMS - No Sign-In' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAS04' -TestType 'Identity' -Status 'Failed' -ResultMarkdown 'SMS authentication configuration not found in Authentication Methods Policy.' -Risk 'High' -Name 'SMS - No Sign-In' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authentication Methods' return } @@ -47,10 +47,10 @@ Disabling SMS for sign-in while keeping it for MFA provides better security. "@ } - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAS04' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'EIDSCAAS04: SMS - No Sign-In' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAS04' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'SMS - No Sign-In' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authentication Methods' } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAS04' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'EIDSCAAS04: SMS - No Sign-In' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAS04' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'SMS - No Sign-In' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authentication Methods' } } diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AT01.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AT01.ps1 index 4fb175f2e443..07953fcf6663 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AT01.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AT01.ps1 @@ -9,14 +9,14 @@ function Invoke-CippTestEIDSCA_AT01 { $AuthMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' if (-not $AuthMethodsPolicy) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAT01' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'EIDSCAAT01: Temp Access Pass - State' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAT01' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'Temp Access Pass - State' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' return } $TAPConfig = $AuthMethodsPolicy.authenticationMethodConfigurations | Where-Object { $_.id -eq 'TemporaryAccessPass' } if (-not $TAPConfig) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAT01' -TestType 'Identity' -Status 'Failed' -ResultMarkdown 'Temporary Access Pass configuration not found in Authentication Methods Policy.' -Risk 'Medium' -Name 'EIDSCAAT01: Temp Access Pass - State' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAT01' -TestType 'Identity' -Status 'Failed' -ResultMarkdown 'Temporary Access Pass configuration not found in Authentication Methods Policy.' -Risk 'Medium' -Name 'Temp Access Pass - State' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' return } @@ -38,10 +38,10 @@ Enabling TAP allows administrators to securely onboard users to passwordless aut "@ } - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAT01' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'EIDSCAAT01: Temp Access Pass - State' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAT01' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'Temp Access Pass - State' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAT01' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'EIDSCAAT01: Temp Access Pass - State' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAT01' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'Temp Access Pass - State' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' } } diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AT02.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AT02.ps1 index 7149c8c91fff..c03b590b2440 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AT02.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AT02.ps1 @@ -9,14 +9,14 @@ function Invoke-CippTestEIDSCA_AT02 { $AuthMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' if (-not $AuthMethodsPolicy) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAT02' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'EIDSCAAT02: Temp Access Pass - One-Time' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAT02' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'Temp Access Pass - One-Time' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' return } $TAPConfig = $AuthMethodsPolicy.authenticationMethodConfigurations | Where-Object { $_.id -eq 'TemporaryAccessPass' } if (-not $TAPConfig) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAT02' -TestType 'Identity' -Status 'Failed' -ResultMarkdown 'Temporary Access Pass configuration not found in Authentication Methods Policy.' -Risk 'Medium' -Name 'EIDSCAAT02: Temp Access Pass - One-Time' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAT02' -TestType 'Identity' -Status 'Failed' -ResultMarkdown 'Temporary Access Pass configuration not found in Authentication Methods Policy.' -Risk 'Medium' -Name 'Temp Access Pass - One-Time' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' return } @@ -38,10 +38,10 @@ One-time use reduces the risk of TAP credential theft or misuse. "@ } - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAT02' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'EIDSCAAT02: Temp Access Pass - One-Time' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAT02' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'Temp Access Pass - One-Time' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAT02' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'EIDSCAAT02: Temp Access Pass - One-Time' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAT02' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'Temp Access Pass - One-Time' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Authentication Methods' } } diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AV01.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AV01.ps1 index 6ae78ab10a72..10c666e920b6 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AV01.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AV01.ps1 @@ -9,14 +9,14 @@ function Invoke-CippTestEIDSCA_AV01 { $AuthMethodsPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AuthenticationMethodsPolicy' if (-not $AuthMethodsPolicy) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAV01' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'EIDSCAAV01: Voice Call - Disabled' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAV01' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'Voice Call - Disabled' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authentication Methods' return } $VoiceConfig = $AuthMethodsPolicy.authenticationMethodConfigurations | Where-Object { $_.id -eq 'Voice' } if (-not $VoiceConfig) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAV01' -TestType 'Identity' -Status 'Failed' -ResultMarkdown 'Voice authentication configuration not found in Authentication Methods Policy.' -Risk 'Medium' -Name 'EIDSCAAV01: Voice Call - Disabled' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAV01' -TestType 'Identity' -Status 'Failed' -ResultMarkdown 'Voice authentication configuration not found in Authentication Methods Policy.' -Risk 'Medium' -Name 'Voice Call - Disabled' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authentication Methods' return } @@ -38,10 +38,10 @@ Disabling voice calls reduces the attack surface by eliminating a less secure au "@ } - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAV01' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'EIDSCAAV01: Voice Call - Disabled' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAV01' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'Voice Call - Disabled' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authentication Methods' } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAV01' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'EIDSCAAV01: Voice Call - Disabled' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authentication Methods' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAAV01' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'Voice Call - Disabled' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Authentication Methods' } } diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CP01.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CP01.ps1 index eee8028f2651..52fc5d6ef7ee 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CP01.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CP01.ps1 @@ -9,7 +9,7 @@ function Invoke-CippTestEIDSCA_CP01 { $Settings = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Settings' if (-not $Settings) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCACP01' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'EIDSCACP01: Consent Policy Settings - Group owner consent for apps accessing data' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Consent Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCACP01' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'Consent Policy Settings - Group owner consent for apps accessing data' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Consent Policy' return } @@ -31,10 +31,10 @@ Group owner consent should be disabled to prevent unauthorized app permissions. "@ } - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCACP01' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'EIDSCACP01: Consent Policy Settings - Group owner consent for apps accessing data' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Consent Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCACP01' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'Consent Policy Settings - Group owner consent for apps accessing data' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Consent Policy' } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCACP01' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'EIDSCACP01: Consent Policy Settings - Group owner consent for apps accessing data' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Consent Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCACP01' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Consent Policy Settings - Group owner consent for apps accessing data' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Consent Policy' } } diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CP03.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CP03.ps1 index cccc4fa620a8..a082e0a1d824 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CP03.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CP03.ps1 @@ -9,7 +9,7 @@ function Invoke-CippTestEIDSCA_CP03 { $Settings = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Settings' if (-not $Settings) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCACP03' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'EIDSCACP03: Consent Policy Settings - Block user consent for risky apps' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Consent Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCACP03' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'Consent Policy Settings - Block user consent for risky apps' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Consent Policy' return } @@ -31,10 +31,10 @@ User consent for risky apps should be blocked to prevent security risks. "@ } - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCACP03' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'EIDSCACP03: Consent Policy Settings - Block user consent for risky apps' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Consent Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCACP03' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'Consent Policy Settings - Block user consent for risky apps' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Consent Policy' } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCACP03' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'EIDSCACP03: Consent Policy Settings - Block user consent for risky apps' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Consent Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCACP03' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Consent Policy Settings - Block user consent for risky apps' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Consent Policy' } } diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CP04.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CP04.ps1 index 3de484fec745..34d166359dbe 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CP04.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CP04.ps1 @@ -9,7 +9,7 @@ function Invoke-CippTestEIDSCA_CP04 { $Settings = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Settings' if (-not $Settings) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCACP04' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'EIDSCACP04: Consent Policy Settings - Users can request admin consent' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Consent Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCACP04' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'Consent Policy Settings - Users can request admin consent' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Consent Policy' return } @@ -31,10 +31,10 @@ Users should be able to request admin consent to enable proper app approval work "@ } - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCACP04' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'EIDSCACP04: Consent Policy Settings - Users can request admin consent' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Consent Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCACP04' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'Consent Policy Settings - Users can request admin consent' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Consent Policy' } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCACP04' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'EIDSCACP04: Consent Policy Settings - Users can request admin consent' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Consent Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCACP04' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'Consent Policy Settings - Users can request admin consent' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Consent Policy' } } diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR01.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR01.ps1 index 04056b5130f6..938258752672 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR01.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR01.ps1 @@ -9,7 +9,7 @@ function Invoke-CippTestEIDSCA_CR01 { $AdminConsentPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AdminConsentRequestPolicy' if (-not $AdminConsentPolicy) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCACR01' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'EIDSCACR01: Admin Consent - Enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Consent Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCACR01' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'Admin Consent - Enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Consent Policy' return } @@ -31,10 +31,10 @@ Enabling this workflow provides a secure process for users to request access to "@ } - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCACR01' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'EIDSCACR01: Admin Consent - Enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Consent Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCACR01' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'Admin Consent - Enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Consent Policy' } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCACR01' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'EIDSCACR01: Admin Consent - Enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Consent Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCACR01' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'Admin Consent - Enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Consent Policy' } } diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR02.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR02.ps1 index 0e047797707c..c4077a2304b0 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR02.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR02.ps1 @@ -9,7 +9,7 @@ function Invoke-CippTestEIDSCA_CR02 { $AdminConsentPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AdminConsentRequestPolicy' if (-not $AdminConsentPolicy) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCACR02' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'EIDSCACR02: Admin Consent - Notify Reviewers' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Consent Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCACR02' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'Admin Consent - Notify Reviewers' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Consent Policy' return } @@ -31,10 +31,10 @@ Enabling notifications ensures reviewers are promptly informed of pending consen "@ } - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCACR02' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'EIDSCACR02: Admin Consent - Notify Reviewers' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Consent Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCACR02' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'Admin Consent - Notify Reviewers' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Consent Policy' } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCACR02' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'EIDSCACR02: Admin Consent - Notify Reviewers' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Consent Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCACR02' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'Admin Consent - Notify Reviewers' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Consent Policy' } } diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR03.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR03.ps1 index dad5cdeacc34..489aceb9b503 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR03.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR03.ps1 @@ -9,7 +9,7 @@ function Invoke-CippTestEIDSCA_CR03 { $AdminConsentPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AdminConsentRequestPolicy' if (-not $AdminConsentPolicy) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCACR03' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Low' -Name 'EIDSCACR03: Admin Consent - Reminders' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Consent Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCACR03' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Low' -Name 'Admin Consent - Reminders' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Consent Policy' return } @@ -31,10 +31,10 @@ Enabling reminders helps prevent consent requests from being overlooked or delay "@ } - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCACR03' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Low' -Name 'EIDSCACR03: Admin Consent - Reminders' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Consent Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCACR03' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Low' -Name 'Admin Consent - Reminders' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Consent Policy' } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCACR03' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Low' -Name 'EIDSCACR03: Admin Consent - Reminders' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Consent Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCACR03' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Low' -Name 'Admin Consent - Reminders' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Consent Policy' } } diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR04.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR04.ps1 index 84152a41bb60..5ef6f66fa612 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR04.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR04.ps1 @@ -9,7 +9,7 @@ function Invoke-CippTestEIDSCA_CR04 { $AdminConsentPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'AdminConsentRequestPolicy' if (-not $AdminConsentPolicy) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCACR04' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Low' -Name 'EIDSCACR04: Admin Consent - Duration' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Consent Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCACR04' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Low' -Name 'Admin Consent - Duration' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Consent Policy' return } @@ -33,10 +33,10 @@ A shorter duration ensures consent requests are reviewed and processed in a time "@ } - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCACR04' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Low' -Name 'EIDSCACR04: Admin Consent - Duration' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Consent Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCACR04' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Low' -Name 'Admin Consent - Duration' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Consent Policy' } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCACR04' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Low' -Name 'EIDSCACR04: Admin Consent - Duration' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Consent Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCACR04' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Low' -Name 'Admin Consent - Duration' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Consent Policy' } } diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR01.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR01.ps1 index 859ea75e1319..c6cf610d780e 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR01.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR01.ps1 @@ -9,7 +9,7 @@ function Invoke-CippTestEIDSCA_PR01 { $Settings = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Settings' if (-not $Settings) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAPR01' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'EIDSCAPR01: Password Rule Settings - Password Protection Mode' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Password Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAPR01' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'Password Rule Settings - Password Protection Mode' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Password Policy' return } @@ -31,10 +31,10 @@ Password protection mode should be set to Enforce to prevent weak passwords. "@ } - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAPR01' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'EIDSCAPR01: Password Rule Settings - Password Protection Mode' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Password Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAPR01' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'Password Rule Settings - Password Protection Mode' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Password Policy' } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAPR01' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'EIDSCAPR01: Password Rule Settings - Password Protection Mode' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Password Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAPR01' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Password Rule Settings - Password Protection Mode' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Password Policy' } } diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR02.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR02.ps1 index e2effe56936b..a42fcdbe750b 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR02.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR02.ps1 @@ -9,7 +9,7 @@ function Invoke-CippTestEIDSCA_PR02 { $Settings = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Settings' if (-not $Settings) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAPR02' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'EIDSCAPR02: Password Rule Settings - Enable password protection on Windows Server Active Directory' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Password Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAPR02' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'Password Rule Settings - Enable password protection on Windows Server Active Directory' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Password Policy' return } @@ -31,10 +31,10 @@ Password protection should be enabled for on-premises Active Directory to preven "@ } - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAPR02' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'EIDSCAPR02: Password Rule Settings - Enable password protection on Windows Server Active Directory' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Password Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAPR02' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'Password Rule Settings - Enable password protection on Windows Server Active Directory' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Password Policy' } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAPR02' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'EIDSCAPR02: Password Rule Settings - Enable password protection on Windows Server Active Directory' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Password Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAPR02' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Password Rule Settings - Enable password protection on Windows Server Active Directory' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Password Policy' } } diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR03.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR03.ps1 index 92fe68a0912a..36c2a5bd25fa 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR03.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR03.ps1 @@ -9,7 +9,7 @@ function Invoke-CippTestEIDSCA_PR03 { $Settings = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Settings' if (-not $Settings) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAPR03' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'EIDSCAPR03: Password Rule Settings - Enforce custom list' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Password Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAPR03' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'Password Rule Settings - Enforce custom list' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Password Policy' return } @@ -31,10 +31,10 @@ Custom banned password list should be enforced to prevent common weak passwords. "@ } - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAPR03' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'EIDSCAPR03: Password Rule Settings - Enforce custom list' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Password Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAPR03' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'Password Rule Settings - Enforce custom list' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Password Policy' } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAPR03' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'EIDSCAPR03: Password Rule Settings - Enforce custom list' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Password Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAPR03' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'Password Rule Settings - Enforce custom list' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Password Policy' } } diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR05.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR05.ps1 index 63c328b9c7c8..cf6906468f98 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR05.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR05.ps1 @@ -9,7 +9,7 @@ function Invoke-CippTestEIDSCA_PR05 { $Settings = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Settings' if (-not $Settings) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAPR05' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'EIDSCAPR05: Password Rule Settings - Lockout duration in seconds' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Password Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAPR05' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'Password Rule Settings - Lockout duration in seconds' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Password Policy' return } @@ -31,10 +31,10 @@ Lockout duration should be at least 60 seconds to protect against brute force at "@ } - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAPR05' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'EIDSCAPR05: Password Rule Settings - Lockout duration in seconds' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Password Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAPR05' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'Password Rule Settings - Lockout duration in seconds' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Password Policy' } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAPR05' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'EIDSCAPR05: Password Rule Settings - Lockout duration in seconds' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Password Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAPR05' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'Password Rule Settings - Lockout duration in seconds' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Password Policy' } } diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR06.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR06.ps1 index 7508f08263d8..1a10d67570fb 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR06.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR06.ps1 @@ -9,7 +9,7 @@ function Invoke-CippTestEIDSCA_PR06 { $Settings = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Settings' if (-not $Settings) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAPR06' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'EIDSCAPR06: Password Rule Settings - Lockout threshold' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Password Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAPR06' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'Password Rule Settings - Lockout threshold' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Password Policy' return } @@ -31,10 +31,10 @@ Lockout threshold should be 10 or fewer failed attempts to protect against brute "@ } - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAPR06' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'EIDSCAPR06: Password Rule Settings - Lockout threshold' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Password Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAPR06' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'Password Rule Settings - Lockout threshold' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Password Policy' } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAPR06' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'EIDSCAPR06: Password Rule Settings - Lockout threshold' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Password Policy' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAPR06' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'Password Rule Settings - Lockout threshold' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Password Policy' } } diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_ST08.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_ST08.ps1 index 017ea2253e68..298b94debdaf 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_ST08.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_ST08.ps1 @@ -9,7 +9,7 @@ function Invoke-CippTestEIDSCA_ST08 { $Settings = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Settings' if (-not $Settings) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAST08' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'EIDSCAST08: Classification and M365 Groups - Allow Guests to become Group Owner' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Group Settings' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAST08' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'Classification and M365 Groups - Allow Guests to become Group Owner' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Group Settings' return } @@ -31,10 +31,10 @@ Guests should not be allowed to become group owners to maintain proper access co "@ } - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAST08' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'EIDSCAST08: Classification and M365 Groups - Allow Guests to become Group Owner' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Group Settings' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAST08' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'Classification and M365 Groups - Allow Guests to become Group Owner' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Group Settings' } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAST08' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'EIDSCAST08: Classification and M365 Groups - Allow Guests to become Group Owner' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Group Settings' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAST08' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Classification and M365 Groups - Allow Guests to become Group Owner' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Group Settings' } } diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_ST09.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_ST09.ps1 index 33cd7fbeee13..ca6e9551c342 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_ST09.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_ST09.ps1 @@ -9,7 +9,7 @@ function Invoke-CippTestEIDSCA_ST09 { $Settings = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Settings' if (-not $Settings) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAST09' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Low' -Name 'EIDSCAST09: Classification and M365 Groups - Allow Guests to have access to groups content' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Group Settings' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAST09' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Low' -Name 'Classification and M365 Groups - Allow Guests to have access to groups content' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Group Settings' return } @@ -31,10 +31,10 @@ Guests should be allowed to access groups content for proper collaboration. "@ } - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAST09' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Low' -Name 'EIDSCAST09: Classification and M365 Groups - Allow Guests to have access to groups content' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Group Settings' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAST09' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Low' -Name 'Classification and M365 Groups - Allow Guests to have access to groups content' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Group Settings' } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAST09' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Low' -Name 'EIDSCAST09: Classification and M365 Groups - Allow Guests to have access to groups content' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Group Settings' + Add-CippTestResult -TenantFilter $Tenant -TestId 'EIDSCAST09' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Low' -Name 'Classification and M365 Groups - Allow Guests to have access to groups content' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Group Settings' } } From 4fd3255774c24d36d7d96e642aa54f6c7512687e Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Sun, 11 Jan 2026 13:52:57 +0100 Subject: [PATCH 107/503] ORCA tests start --- .../Push-CIPPDBCacheData.ps1 | 45 +++++++++++ .../Set-CIPPDBCacheExoAntiPhishPolicy.ps1 | 29 +++++++ .../Set-CIPPDBCacheExoAtpPolicyForO365.ps1 | 29 +++++++ ...IPPDBCacheExoHostedContentFilterPolicy.ps1 | 28 +++++++ ...CacheExoHostedOutboundSpamFilterPolicy.ps1 | 29 +++++++ .../Set-CIPPDBCacheExoMalwareFilterPolicy.ps1 | 29 +++++++ .../Set-CIPPDBCacheExoQuarantinePolicy.ps1 | 29 +++++++ .../Public/Set-CIPPDBCacheExoRemoteDomain.ps1 | 29 +++++++ ...Set-CIPPDBCacheExoSafeAttachmentPolicy.ps1 | 29 +++++++ .../Set-CIPPDBCacheExoSafeLinksPolicy.ps1 | 29 +++++++ .../ORCA/Identity/Invoke-CippTestORCA100.ps1 | 28 +++++++ .../ORCA/Identity/Invoke-CippTestORCA104.ps1 | 58 ++++++++++++++ .../ORCA/Identity/Invoke-CippTestORCA108.ps1 | 61 +++++++++++++++ .../ORCA/Identity/Invoke-CippTestORCA113.ps1 | 58 ++++++++++++++ .../CIPPCore/Public/Tests/ORCA/report.json | 77 +++++++++++++++++++ 15 files changed, 587 insertions(+) create mode 100644 Modules/CIPPCore/Public/Set-CIPPDBCacheExoAntiPhishPolicy.ps1 create mode 100644 Modules/CIPPCore/Public/Set-CIPPDBCacheExoAtpPolicyForO365.ps1 create mode 100644 Modules/CIPPCore/Public/Set-CIPPDBCacheExoHostedContentFilterPolicy.ps1 create mode 100644 Modules/CIPPCore/Public/Set-CIPPDBCacheExoHostedOutboundSpamFilterPolicy.ps1 create mode 100644 Modules/CIPPCore/Public/Set-CIPPDBCacheExoMalwareFilterPolicy.ps1 create mode 100644 Modules/CIPPCore/Public/Set-CIPPDBCacheExoQuarantinePolicy.ps1 create mode 100644 Modules/CIPPCore/Public/Set-CIPPDBCacheExoRemoteDomain.ps1 create mode 100644 Modules/CIPPCore/Public/Set-CIPPDBCacheExoSafeAttachmentPolicy.ps1 create mode 100644 Modules/CIPPCore/Public/Set-CIPPDBCacheExoSafeLinksPolicy.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA100.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA104.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA108.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA113.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/report.json diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 index 4dbb7b000423..ef3f7df48952 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 @@ -211,6 +211,51 @@ function Push-CIPPDBCacheData { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoAcceptedDomains collection failed: $($_.Exception.Message)" -sev Error } + Write-Host 'Getting cache for ExoHostedContentFilterPolicy' + try { Set-CIPPDBCacheExoHostedContentFilterPolicy -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoHostedContentFilterPolicy collection failed: $($_.Exception.Message)" -sev Error + } + + Write-Host 'Getting cache for ExoHostedOutboundSpamFilterPolicy' + try { Set-CIPPDBCacheExoHostedOutboundSpamFilterPolicy -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoHostedOutboundSpamFilterPolicy collection failed: $($_.Exception.Message)" -sev Error + } + + Write-Host 'Getting cache for ExoAntiPhishPolicy' + try { Set-CIPPDBCacheExoAntiPhishPolicy -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoAntiPhishPolicy collection failed: $($_.Exception.Message)" -sev Error + } + + Write-Host 'Getting cache for ExoSafeLinksPolicy' + try { Set-CIPPDBCacheExoSafeLinksPolicy -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoSafeLinksPolicy collection failed: $($_.Exception.Message)" -sev Error + } + + Write-Host 'Getting cache for ExoSafeAttachmentPolicy' + try { Set-CIPPDBCacheExoSafeAttachmentPolicy -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoSafeAttachmentPolicy collection failed: $($_.Exception.Message)" -sev Error + } + + Write-Host 'Getting cache for ExoMalwareFilterPolicy' + try { Set-CIPPDBCacheExoMalwareFilterPolicy -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoMalwareFilterPolicy collection failed: $($_.Exception.Message)" -sev Error + } + + Write-Host 'Getting cache for ExoAtpPolicyForO365' + try { Set-CIPPDBCacheExoAtpPolicyForO365 -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoAtpPolicyForO365 collection failed: $($_.Exception.Message)" -sev Error + } + + Write-Host 'Getting cache for ExoQuarantinePolicy' + try { Set-CIPPDBCacheExoQuarantinePolicy -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoQuarantinePolicy collection failed: $($_.Exception.Message)" -sev Error + } + + Write-Host 'Getting cache for ExoRemoteDomain' + try { Set-CIPPDBCacheExoRemoteDomain -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoRemoteDomain collection failed: $($_.Exception.Message)" -sev Error + } + Write-Host 'Getting cache for License Overview' try { Set-CIPPDBCacheLicenseOverview -TenantFilter $TenantFilter } catch { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "License Overview collection failed: $($_.Exception.Message)" -sev Error diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoAntiPhishPolicy.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoAntiPhishPolicy.ps1 new file mode 100644 index 000000000000..ba0a6603efff --- /dev/null +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoAntiPhishPolicy.ps1 @@ -0,0 +1,29 @@ +function Set-CIPPDBCacheExoAntiPhishPolicy { + <# + .SYNOPSIS + Caches Exchange Online Anti-Phish policies (detailed) + + .PARAMETER TenantFilter + The tenant to cache Anti-Phish policy data for + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter + ) + + try { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching Exchange Anti-Phish policies (detailed)' -sev Info + + $AntiPhishPolicies = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-AntiPhishPolicy' + if ($AntiPhishPolicies) { + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoAntiPhishPolicy' -Data $AntiPhishPolicies + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoAntiPhishPolicy' -Data $AntiPhishPolicies -Count + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($AntiPhishPolicies.Count) Anti-Phish policies (detailed)" -sev Info + } + $AntiPhishPolicies = $null + + } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache Anti-Phish policy data: $($_.Exception.Message)" -sev Error + } +} diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoAtpPolicyForO365.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoAtpPolicyForO365.ps1 new file mode 100644 index 000000000000..96912b26c3ae --- /dev/null +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoAtpPolicyForO365.ps1 @@ -0,0 +1,29 @@ +function Set-CIPPDBCacheExoAtpPolicyForO365 { + <# + .SYNOPSIS + Caches Exchange Online ATP policies for Office 365 + + .PARAMETER TenantFilter + The tenant to cache ATP policy data for + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter + ) + + try { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching Exchange ATP policies for Office 365' -sev Info + + $AtpPolicies = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-AtpPolicyForO365' + if ($AtpPolicies) { + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoAtpPolicyForO365' -Data $AtpPolicies + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoAtpPolicyForO365' -Data $AtpPolicies -Count + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($AtpPolicies.Count) ATP policies for Office 365" -sev Info + } + $AtpPolicies = $null + + } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache ATP policy data: $($_.Exception.Message)" -sev Error + } +} diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoHostedContentFilterPolicy.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoHostedContentFilterPolicy.ps1 new file mode 100644 index 000000000000..c8fc088bf333 --- /dev/null +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoHostedContentFilterPolicy.ps1 @@ -0,0 +1,28 @@ +function Set-CIPPDBCacheExoHostedContentFilterPolicy { + <# + .SYNOPSIS + Caches Exchange Online Hosted Content Filter policies + + .PARAMETER TenantFilter + The tenant to cache Hosted Content Filter data for + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter + ) + + try { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching Exchange Hosted Content Filter policies' -sev Info + $HostedContentFilterPolicies = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-HostedContentFilterPolicy' + if ($HostedContentFilterPolicies) { + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoHostedContentFilterPolicy' -Data $HostedContentFilterPolicies + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoHostedContentFilterPolicy' -Data $HostedContentFilterPolicies -Count + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($HostedContentFilterPolicies.Count) Hosted Content Filter policies" -sev Info + } + $HostedContentFilterPolicies = $null + + } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache Hosted Content Filter data: $($_.Exception.Message)" -sev Error + } +} diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoHostedOutboundSpamFilterPolicy.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoHostedOutboundSpamFilterPolicy.ps1 new file mode 100644 index 000000000000..1b9f0319e578 --- /dev/null +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoHostedOutboundSpamFilterPolicy.ps1 @@ -0,0 +1,29 @@ +function Set-CIPPDBCacheExoHostedOutboundSpamFilterPolicy { + <# + .SYNOPSIS + Caches Exchange Online Hosted Outbound Spam Filter policies + + .PARAMETER TenantFilter + The tenant to cache Hosted Outbound Spam Filter data for + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter + ) + + try { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching Exchange Hosted Outbound Spam Filter policies' -sev Info + + $HostedOutboundSpamFilterPolicies = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-HostedOutboundSpamFilterPolicy' + if ($HostedOutboundSpamFilterPolicies) { + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoHostedOutboundSpamFilterPolicy' -Data $HostedOutboundSpamFilterPolicies + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoHostedOutboundSpamFilterPolicy' -Data $HostedOutboundSpamFilterPolicies -Count + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($HostedOutboundSpamFilterPolicies.Count) Hosted Outbound Spam Filter policies" -sev Info + } + $HostedOutboundSpamFilterPolicies = $null + + } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache Hosted Outbound Spam Filter data: $($_.Exception.Message)" -sev Error + } +} diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoMalwareFilterPolicy.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoMalwareFilterPolicy.ps1 new file mode 100644 index 000000000000..3103dbebf29c --- /dev/null +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoMalwareFilterPolicy.ps1 @@ -0,0 +1,29 @@ +function Set-CIPPDBCacheExoMalwareFilterPolicy { + <# + .SYNOPSIS + Caches Exchange Online Malware Filter policies (detailed) + + .PARAMETER TenantFilter + The tenant to cache Malware Filter policy data for + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter + ) + + try { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching Exchange Malware Filter policies (detailed)' -sev Info + + $MalwareFilterPolicies = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-MalwareFilterPolicy' + if ($MalwareFilterPolicies) { + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoMalwareFilterPolicy' -Data $MalwareFilterPolicies + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoMalwareFilterPolicy' -Data $MalwareFilterPolicies -Count + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($MalwareFilterPolicies.Count) Malware Filter policies (detailed)" -sev Info + } + $MalwareFilterPolicies = $null + + } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache Malware Filter policy data: $($_.Exception.Message)" -sev Error + } +} diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoQuarantinePolicy.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoQuarantinePolicy.ps1 new file mode 100644 index 000000000000..894d03f3b1b4 --- /dev/null +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoQuarantinePolicy.ps1 @@ -0,0 +1,29 @@ +function Set-CIPPDBCacheExoQuarantinePolicy { + <# + .SYNOPSIS + Caches Exchange Online Quarantine policies + + .PARAMETER TenantFilter + The tenant to cache Quarantine policy data for + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter + ) + + try { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching Exchange Quarantine policies' -sev Info + + $QuarantinePolicies = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-QuarantinePolicy' + if ($QuarantinePolicies) { + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoQuarantinePolicy' -Data $QuarantinePolicies + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoQuarantinePolicy' -Data $QuarantinePolicies -Count + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($QuarantinePolicies.Count) Quarantine policies" -sev Info + } + $QuarantinePolicies = $null + + } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache Quarantine policy data: $($_.Exception.Message)" -sev Error + } +} diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoRemoteDomain.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoRemoteDomain.ps1 new file mode 100644 index 000000000000..d22c8fd0ccf1 --- /dev/null +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoRemoteDomain.ps1 @@ -0,0 +1,29 @@ +function Set-CIPPDBCacheExoRemoteDomain { + <# + .SYNOPSIS + Caches Exchange Online Remote Domains + + .PARAMETER TenantFilter + The tenant to cache Remote Domain data for + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter + ) + + try { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching Exchange Remote Domains' -sev Info + + $RemoteDomains = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-RemoteDomain' + if ($RemoteDomains) { + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoRemoteDomain' -Data $RemoteDomains + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoRemoteDomain' -Data $RemoteDomains -Count + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($RemoteDomains.Count) Remote Domains" -sev Info + } + $RemoteDomains = $null + + } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache Remote Domain data: $($_.Exception.Message)" -sev Error + } +} diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoSafeAttachmentPolicy.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoSafeAttachmentPolicy.ps1 new file mode 100644 index 000000000000..c6869a72064a --- /dev/null +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoSafeAttachmentPolicy.ps1 @@ -0,0 +1,29 @@ +function Set-CIPPDBCacheExoSafeAttachmentPolicy { + <# + .SYNOPSIS + Caches Exchange Online Safe Attachment policies (detailed) + + .PARAMETER TenantFilter + The tenant to cache Safe Attachment policy data for + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter + ) + + try { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching Exchange Safe Attachment policies (detailed)' -sev Info + + $SafeAttachmentPolicies = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-SafeAttachmentPolicy' + if ($SafeAttachmentPolicies) { + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoSafeAttachmentPolicy' -Data $SafeAttachmentPolicies + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoSafeAttachmentPolicy' -Data $SafeAttachmentPolicies -Count + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($SafeAttachmentPolicies.Count) Safe Attachment policies (detailed)" -sev Info + } + $SafeAttachmentPolicies = $null + + } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache Safe Attachment policy data: $($_.Exception.Message)" -sev Error + } +} diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoSafeLinksPolicy.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoSafeLinksPolicy.ps1 new file mode 100644 index 000000000000..176e3c76f168 --- /dev/null +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoSafeLinksPolicy.ps1 @@ -0,0 +1,29 @@ +function Set-CIPPDBCacheExoSafeLinksPolicy { + <# + .SYNOPSIS + Caches Exchange Online Safe Links policies (detailed) + + .PARAMETER TenantFilter + The tenant to cache Safe Links policy data for + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter + ) + + try { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching Exchange Safe Links policies (detailed)' -sev Info + + $SafeLinksPolicies = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-SafeLinksPolicy' + if ($SafeLinksPolicies) { + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoSafeLinksPolicy' -Data $SafeLinksPolicies + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoSafeLinksPolicy' -Data $SafeLinksPolicies -Count + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($SafeLinksPolicies.Count) Safe Links policies (detailed)" -sev Info + } + $SafeLinksPolicies = $null + + } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache Safe Links policy data: $($_.Exception.Message)" -sev Error + } +} diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA100.ps1 b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA100.ps1 new file mode 100644 index 000000000000..380b04e9886a --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA100.ps1 @@ -0,0 +1,28 @@ +# Cache Missing: ExoHostedContentFilterPolicy or equivalent +# Required Graph API: https://outlook.office365.com/adminapi/beta/$('tenantid')/HostedContentFilterPolicy +# +# This test requires access to the Hosted Content Filter Policy (Anti-Spam Policy) configuration +# to check if the Bulk Complaint Level (BCL) threshold is between 4 and 6. +# +# The BCL threshold determines when bulk email is considered spam. Microsoft recommends +# a value between 4-6 for optimal spam filtering while minimizing false positives. + +function Invoke-CippTestORCA100 { + <# + .SYNOPSIS + Bulk Complaint Level threshold is between 4 and 6 + #> + param($Tenant) + + try { + # TODO: Implement when HostedContentFilterPolicy cache is available + # Expected cache type: 'ExoHostedContentFilterPolicy' or similar + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA100' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'Cache not yet implemented. Required: ExoHostedContentFilterPolicy to check BCL threshold settings.' -Risk 'Medium' -Name 'Bulk Complaint Level threshold is between 4 and 6' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Anti-Spam' + return + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA100' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'Bulk Complaint Level threshold is between 4 and 6' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Anti-Spam' + } +} diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA104.ps1 b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA104.ps1 new file mode 100644 index 000000000000..5f49f7072020 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA104.ps1 @@ -0,0 +1,58 @@ +function Invoke-CippTestORCA104 { + <# + .SYNOPSIS + High Confidence Phish action set to Quarantine message + #> + param($Tenant) + + try { + $AntiPhishPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoAntiPhishPolicies' + + if (-not $AntiPhishPolicies) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA104' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'High Confidence Phish action set to Quarantine message' -UserImpact 'High' -ImplementationEffort 'Low' -Category 'Anti-Phish' + return + } + + $FailedPolicies = @() + $PassedPolicies = @() + + foreach ($Policy in $AntiPhishPolicies) { + # Check if HighConfidencePhishAction is set to Quarantine + if ($Policy.HighConfidencePhishAction -eq 'Quarantine') { + $PassedPolicies += $Policy + } else { + $FailedPolicies += $Policy + } + } + + if ($FailedPolicies.Count -eq 0) { + $Status = 'Passed' + $Result = "All anti-phishing policies have High Confidence Phish action set to Quarantine.`n`n" + $Result += "**Compliant Policies:** $($PassedPolicies.Count)`n`n" + if ($PassedPolicies.Count -gt 0) { + $Result += "| Policy Name | Action |`n" + $Result += "|------------|--------|`n" + foreach ($Policy in $PassedPolicies) { + $Result += "| $($Policy.Identity) | $($Policy.HighConfidencePhishAction) |`n" + } + } + } else { + $Status = 'Failed' + $Result = "Some anti-phishing policies do not have High Confidence Phish action set to Quarantine.`n`n" + $Result += "**Failed Policies:** $($FailedPolicies.Count) | **Passed Policies:** $($PassedPolicies.Count)`n`n" + $Result += "### Non-Compliant Policies`n`n" + $Result += "| Policy Name | Current Action | Recommended Action |`n" + $Result += "|------------|----------------|-------------------|`n" + foreach ($Policy in $FailedPolicies) { + $Result += "| $($Policy.Identity) | $($Policy.HighConfidencePhishAction) | Quarantine |`n" + } + $Result += "`n**Remediation:** Update the HighConfidencePhishAction to 'Quarantine' for enhanced security." + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA104' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'High Confidence Phish action set to Quarantine message' -UserImpact 'High' -ImplementationEffort 'Low' -Category 'Anti-Phish' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA104' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'High Confidence Phish action set to Quarantine message' -UserImpact 'High' -ImplementationEffort 'Low' -Category 'Anti-Phish' + } +} diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA108.ps1 b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA108.ps1 new file mode 100644 index 000000000000..390ab97aa9aa --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA108.ps1 @@ -0,0 +1,61 @@ +function Invoke-CippTestORCA108 { + <# + .SYNOPSIS + DKIM signing is set up for all your custom domains + #> + param($Tenant) + + try { + $DkimConfig = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoDkimSigningConfig' + $AcceptedDomains = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoAcceptedDomains' + + if (-not $DkimConfig -or -not $AcceptedDomains) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA108' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'DKIM signing is set up for all your custom domains' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'DKIM' + return + } + + # Get custom domains (exclude default .onmicrosoft.com domains) + $CustomDomains = $AcceptedDomains | Where-Object { + $_.DomainName -notlike '*.onmicrosoft.com' -and + $_.DomainName -notlike '*.mail.onmicrosoft.com' + } + + if ($CustomDomains.Count -eq 0) { + $Status = 'Passed' + $Result = 'No custom domains configured. DKIM check not applicable for default domains only.' + } else { + $DomainsWithoutDkim = @() + $DomainsWithDkim = @() + + foreach ($Domain in $CustomDomains) { + $DkimForDomain = $DkimConfig | Where-Object { $_.Domain -eq $Domain.DomainName } + + if ($DkimForDomain -and $DkimForDomain.Enabled -eq $true) { + $DomainsWithDkim += $Domain.DomainName + } else { + $DomainsWithoutDkim += $Domain.DomainName + } + } + + if ($DomainsWithoutDkim.Count -eq 0) { + $Status = 'Passed' + $Result = "DKIM signing is enabled for all custom domains ($($DomainsWithDkim.Count) domains).`n`n" + $Result += "**Domains with DKIM enabled:**`n" + $Result += ($DomainsWithDkim | ForEach-Object { "- $_" }) -join "`n" + } else { + $Status = 'Failed' + $Result = "DKIM signing is not configured for all custom domains.`n`n" + $Result += "**Missing DKIM:** $($DomainsWithoutDkim.Count) | **Configured:** $($DomainsWithDkim.Count)`n`n" + $Result += "### Domains without DKIM:`n" + $Result += ($DomainsWithoutDkim | ForEach-Object { "- $_" }) -join "`n" + $Result += "`n`n**Remediation:** Enable DKIM signing for all custom domains to prevent email spoofing." + } + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA108' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'DKIM signing is set up for all your custom domains' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'DKIM' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA108' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'DKIM signing is set up for all your custom domains' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'DKIM' + } +} diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA113.ps1 b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA113.ps1 new file mode 100644 index 000000000000..93cfc03f605a --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA113.ps1 @@ -0,0 +1,58 @@ +function Invoke-CippTestORCA113 { + <# + .SYNOPSIS + AllowClickThrough is disabled in Safe Links policies + #> + param($Tenant) + + try { + $SafeLinksPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoSafeLinksPolicies' + + if (-not $SafeLinksPolicies) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA113' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'AllowClickThrough is disabled in Safe Links policies' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Safe Links' + return + } + + $FailedPolicies = @() + $PassedPolicies = @() + + foreach ($Policy in $SafeLinksPolicies) { + # Check if DoNotAllowClickThrough is set to true (which means AllowClickThrough is disabled) + if ($Policy.DoNotAllowClickThrough -eq $true) { + $PassedPolicies += $Policy + } else { + $FailedPolicies += $Policy + } + } + + if ($FailedPolicies.Count -eq 0) { + $Status = 'Passed' + $Result = "All Safe Links policies have click-through disabled (DoNotAllowClickThrough = true).`n`n" + $Result += "**Compliant Policies:** $($PassedPolicies.Count)`n`n" + if ($PassedPolicies.Count -gt 0) { + $Result += "| Policy Name | DoNotAllowClickThrough |`n" + $Result += "|------------|----------------------|`n" + foreach ($Policy in $PassedPolicies) { + $Result += "| $($Policy.Identity) | $($Policy.DoNotAllowClickThrough) |`n" + } + } + } else { + $Status = 'Failed' + $Result = "Some Safe Links policies allow click-through, which reduces protection.`n`n" + $Result += "**Failed Policies:** $($FailedPolicies.Count) | **Passed Policies:** $($PassedPolicies.Count)`n`n" + $Result += "### Non-Compliant Policies`n`n" + $Result += "| Policy Name | DoNotAllowClickThrough | Recommended |`n" + $Result += "|------------|----------------------|-------------|`n" + foreach ($Policy in $FailedPolicies) { + $Result += "| $($Policy.Identity) | $($Policy.DoNotAllowClickThrough) | true |`n" + } + $Result += "`n**Remediation:** Disable click-through (set DoNotAllowClickThrough to true) to prevent users from bypassing Safe Links protection." + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA113' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'AllowClickThrough is disabled in Safe Links policies' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Safe Links' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA113' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'AllowClickThrough is disabled in Safe Links policies' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Safe Links' + } +} diff --git a/Modules/CIPPCore/Public/Tests/ORCA/report.json b/Modules/CIPPCore/Public/Tests/ORCA/report.json new file mode 100644 index 000000000000..2b223f8e5e69 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/report.json @@ -0,0 +1,77 @@ +{ + "name": "Office 365 Recommended Configuration Analyzer (ORCA) Tests", + "description": "Comprehensive security assessment for Microsoft Exchange Online and Office 365 security configurations. Tests cover anti-spam, anti-phish, anti-malware, safe links, safe attachments, DKIM, transport rules, and other Exchange Online security settings.", + "version": "1.0", + "source": "https://github.com/maester365/maester", + "category": "Exchange Online Security", + "note": "ORCA tests require Exchange Online and Security & Compliance Center data. Many tests require custom ORCA framework classes and detailed implementation. Tests marked as requiring specific caches will be implemented based on available CIPP cache data.", + "IdentityTests": [ + "ORCA100", + "ORCA101", + "ORCA102", + "ORCA103", + "ORCA104", + "ORCA105", + "ORCA106", + "ORCA107", + "ORCA108", + "ORCA1081", + "ORCA109", + "ORCA110", + "ORCA111", + "ORCA112", + "ORCA113", + "ORCA114", + "ORCA115", + "ORCA116", + "ORCA1181", + "ORCA1182", + "ORCA1183", + "ORCA1184", + "ORCA119", + "ORCA120malware", + "ORCA120phish", + "ORCA120spam", + "ORCA121", + "ORCA123", + "ORCA124", + "ORCA139", + "ORCA140", + "ORCA141", + "ORCA142", + "ORCA143", + "ORCA156", + "ORCA158", + "ORCA179", + "ORCA180", + "ORCA189", + "ORCA1892", + "ORCA205", + "ORCA220", + "ORCA221", + "ORCA222", + "ORCA223", + "ORCA224", + "ORCA225", + "ORCA226", + "ORCA227", + "ORCA228", + "ORCA229", + "ORCA230", + "ORCA231", + "ORCA232", + "ORCA233", + "ORCA2331", + "ORCA234", + "ORCA235", + "ORCA236", + "ORCA237", + "ORCA238", + "ORCA239", + "ORCA240", + "ORCA241", + "ORCA242", + "ORCA243", + "ORCA244" + ] +} From cc2b7f0157d7f28b8e619603f9ad0f1dffd2cb63 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Sun, 11 Jan 2026 14:30:37 +0100 Subject: [PATCH 108/503] Introduce ORCA tests --- .../ORCA/Identity/Invoke-CippTestORCA100.ps1 | 48 ++++++++--- .../ORCA/Identity/Invoke-CippTestORCA101.ps1 | 56 +++++++++++++ .../ORCA/Identity/Invoke-CippTestORCA102.ps1 | 74 +++++++++++++++++ .../ORCA/Identity/Invoke-CippTestORCA103.ps1 | 68 +++++++++++++++ .../ORCA/Identity/Invoke-CippTestORCA105.ps1 | 49 +++++++++++ .../ORCA/Identity/Invoke-CippTestORCA106.ps1 | 49 +++++++++++ .../ORCA/Identity/Invoke-CippTestORCA107.ps1 | 57 +++++++++++++ .../Identity/Invoke-CippTestORCA108_1.ps1 | 53 ++++++++++++ .../ORCA/Identity/Invoke-CippTestORCA109.ps1 | 54 ++++++++++++ .../ORCA/Identity/Invoke-CippTestORCA110.ps1 | 49 +++++++++++ .../ORCA/Identity/Invoke-CippTestORCA111.ps1 | 49 +++++++++++ .../ORCA/Identity/Invoke-CippTestORCA112.ps1 | 49 +++++++++++ .../ORCA/Identity/Invoke-CippTestORCA114.ps1 | 52 ++++++++++++ .../ORCA/Identity/Invoke-CippTestORCA115.ps1 | 49 +++++++++++ .../ORCA/Identity/Invoke-CippTestORCA116.ps1 | 49 +++++++++++ .../Identity/Invoke-CippTestORCA118_1.ps1 | 52 ++++++++++++ .../Identity/Invoke-CippTestORCA118_2.ps1 | 48 +++++++++++ .../Identity/Invoke-CippTestORCA118_3.ps1 | 68 +++++++++++++++ .../Identity/Invoke-CippTestORCA118_4.ps1 | 65 +++++++++++++++ .../ORCA/Identity/Invoke-CippTestORCA119.ps1 | 49 +++++++++++ .../Invoke-CippTestORCA120_malware.ps1 | 49 +++++++++++ .../Identity/Invoke-CippTestORCA120_phish.ps1 | 49 +++++++++++ .../Identity/Invoke-CippTestORCA120_spam.ps1 | 49 +++++++++++ .../ORCA/Identity/Invoke-CippTestORCA121.ps1 | 27 ++++++ .../ORCA/Identity/Invoke-CippTestORCA123.ps1 | 49 +++++++++++ .../ORCA/Identity/Invoke-CippTestORCA124.ps1 | 49 +++++++++++ .../ORCA/Identity/Invoke-CippTestORCA139.ps1 | 49 +++++++++++ .../ORCA/Identity/Invoke-CippTestORCA140.ps1 | 49 +++++++++++ .../ORCA/Identity/Invoke-CippTestORCA141.ps1 | 49 +++++++++++ .../ORCA/Identity/Invoke-CippTestORCA142.ps1 | 49 +++++++++++ .../ORCA/Identity/Invoke-CippTestORCA143.ps1 | 49 +++++++++++ .../ORCA/Identity/Invoke-CippTestORCA156.ps1 | 49 +++++++++++ .../ORCA/Identity/Invoke-CippTestORCA158.ps1 | 35 ++++++++ .../ORCA/Identity/Invoke-CippTestORCA179.ps1 | 49 +++++++++++ .../ORCA/Identity/Invoke-CippTestORCA180.ps1 | 49 +++++++++++ .../ORCA/Identity/Invoke-CippTestORCA189.ps1 | 43 ++++++++++ .../Identity/Invoke-CippTestORCA189_2.ps1 | 43 ++++++++++ .../ORCA/Identity/Invoke-CippTestORCA205.ps1 | 49 +++++++++++ .../ORCA/Identity/Invoke-CippTestORCA220.ps1 | 50 +++++++++++ .../ORCA/Identity/Invoke-CippTestORCA221.ps1 | 49 +++++++++++ .../ORCA/Identity/Invoke-CippTestORCA222.ps1 | 49 +++++++++++ .../ORCA/Identity/Invoke-CippTestORCA223.ps1 | 49 +++++++++++ .../ORCA/Identity/Invoke-CippTestORCA224.ps1 | 49 +++++++++++ .../ORCA/Identity/Invoke-CippTestORCA225.ps1 | 35 ++++++++ .../ORCA/Identity/Invoke-CippTestORCA226.ps1 | 60 ++++++++++++++ .../ORCA/Identity/Invoke-CippTestORCA227.ps1 | 60 ++++++++++++++ .../ORCA/Identity/Invoke-CippTestORCA228.ps1 | 52 ++++++++++++ .../ORCA/Identity/Invoke-CippTestORCA229.ps1 | 52 ++++++++++++ .../ORCA/Identity/Invoke-CippTestORCA230.ps1 | 60 ++++++++++++++ .../ORCA/Identity/Invoke-CippTestORCA231.ps1 | 60 ++++++++++++++ .../ORCA/Identity/Invoke-CippTestORCA232.ps1 | 60 ++++++++++++++ .../ORCA/Identity/Invoke-CippTestORCA233.ps1 | 55 ++++++++++++ .../Identity/Invoke-CippTestORCA233_1.ps1 | 44 ++++++++++ .../ORCA/Identity/Invoke-CippTestORCA234.ps1 | 35 ++++++++ .../ORCA/Identity/Invoke-CippTestORCA235.ps1 | 43 ++++++++++ .../ORCA/Identity/Invoke-CippTestORCA236.ps1 | 49 +++++++++++ .../ORCA/Identity/Invoke-CippTestORCA237.ps1 | 49 +++++++++++ .../ORCA/Identity/Invoke-CippTestORCA238.ps1 | 49 +++++++++++ .../ORCA/Identity/Invoke-CippTestORCA239.ps1 | 83 +++++++++++++++++++ .../ORCA/Identity/Invoke-CippTestORCA240.ps1 | 35 ++++++++ .../ORCA/Identity/Invoke-CippTestORCA241.ps1 | 49 +++++++++++ .../ORCA/Identity/Invoke-CippTestORCA242.ps1 | 30 +++++++ .../ORCA/Identity/Invoke-CippTestORCA243.ps1 | 40 +++++++++ .../ORCA/Identity/Invoke-CippTestORCA244.ps1 | 49 +++++++++++ 64 files changed, 3203 insertions(+), 13 deletions(-) create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA101.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA102.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA103.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA105.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA106.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA107.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA108_1.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA109.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA110.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA111.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA112.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA114.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA115.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA116.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA118_1.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA118_2.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA118_3.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA118_4.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA119.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA120_malware.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA120_phish.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA120_spam.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA121.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA123.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA124.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA139.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA140.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA141.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA142.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA143.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA156.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA158.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA179.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA180.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA189.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA189_2.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA205.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA220.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA221.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA222.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA223.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA224.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA225.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA226.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA227.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA228.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA229.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA230.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA231.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA232.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA233.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA233_1.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA234.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA235.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA236.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA237.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA238.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA239.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA240.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA241.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA242.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA243.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA244.ps1 diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA100.ps1 b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA100.ps1 index 380b04e9886a..b6f56d3aec61 100644 --- a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA100.ps1 +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA100.ps1 @@ -1,12 +1,3 @@ -# Cache Missing: ExoHostedContentFilterPolicy or equivalent -# Required Graph API: https://outlook.office365.com/adminapi/beta/$('tenantid')/HostedContentFilterPolicy -# -# This test requires access to the Hosted Content Filter Policy (Anti-Spam Policy) configuration -# to check if the Bulk Complaint Level (BCL) threshold is between 4 and 6. -# -# The BCL threshold determines when bulk email is considered spam. Microsoft recommends -# a value between 4-6 for optimal spam filtering while minimizing false positives. - function Invoke-CippTestORCA100 { <# .SYNOPSIS @@ -15,11 +6,42 @@ function Invoke-CippTestORCA100 { param($Tenant) try { - # TODO: Implement when HostedContentFilterPolicy cache is available - # Expected cache type: 'ExoHostedContentFilterPolicy' or similar + $Policies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoHostedContentFilterPolicy' + + if (-not $Policies) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA100' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'Bulk Complaint Level threshold is between 4 and 6' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Anti-Spam' + return + } + + $FailedPolicies = [System.Collections.Generic.List[object]]::new() + $PassedPolicies = [System.Collections.Generic.List[object]]::new() + + foreach ($Policy in $Policies) { + # Check if BulkThreshold is between 4 and 6 (inclusive) + if ($Policy.BulkThreshold -ge 4 -and $Policy.BulkThreshold -le 6) { + $PassedPolicies.Add($Policy) | Out-Null + } else { + $FailedPolicies.Add($Policy) | Out-Null + } + } + + if ($FailedPolicies.Count -eq 0) { + $Status = 'Passed' + $Result = "All anti-spam policies have appropriate Bulk Complaint Level (BCL) thresholds set between 4 and 6.`n`n" + $Result += "**Compliant Policies:** $($PassedPolicies.Count)" + } else { + $Status = 'Failed' + $Result = "$($FailedPolicies.Count) anti-spam policies have BCL thresholds outside the recommended range (4-6).`n`n" + $Result += "**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n" + $Result += "| Policy Name | Current BCL Threshold |`n" + $Result += "|------------|----------------------|`n" + foreach ($Policy in $FailedPolicies) { + $Result += "| $($Policy.Identity) | $($Policy.BulkThreshold) |`n" + } + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA100' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'Bulk Complaint Level threshold is between 4 and 6' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Anti-Spam' - Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA100' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'Cache not yet implemented. Required: ExoHostedContentFilterPolicy to check BCL threshold settings.' -Risk 'Medium' -Name 'Bulk Complaint Level threshold is between 4 and 6' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Anti-Spam' - return } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA101.ps1 b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA101.ps1 new file mode 100644 index 000000000000..69ec5003be84 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA101.ps1 @@ -0,0 +1,56 @@ +function Invoke-CippTestORCA101 { + <# + .SYNOPSIS + Bulk is marked as spam + #> + param($Tenant) + + try { + $Policies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoHostedContentFilterPolicy' + + if (-not $Policies) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA101' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Low' -Name 'Bulk is marked as spam' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Anti-Spam' + return + } + + $FailedPolicies = [System.Collections.Generic.List[object]]::new() + $PassedPolicies = [System.Collections.Generic.List[object]]::new() + + foreach ($Policy in $Policies) { + if ($Policy.MarkAsSpamBulkMail -eq 'On') { + $PassedPolicies.Add($Policy) | Out-Null + } else { + $FailedPolicies.Add($Policy) | Out-Null + } + } + + if ($FailedPolicies.Count -eq 0) { + $Status = 'Passed' + $Result = "All anti-spam policies are configured to mark bulk mail as spam.`n`n" + $Result += "**Compliant Policies:** $($PassedPolicies.Count)`n`n" + if ($PassedPolicies.Count -gt 0) { + $Result += "| Policy Name | Mark As Spam Bulk Mail |`n" + $Result += "|------------|------------------------|`n" + foreach ($Policy in $PassedPolicies) { + $Result += "| $($Policy.Identity) | $($Policy.MarkAsSpamBulkMail) |`n" + } + } + } else { + $Status = 'Failed' + $Result = "$($FailedPolicies.Count) anti-spam policies are not configured to mark bulk mail as spam.`n`n" + $Result += "**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n" + $Result += "| Policy Name | Mark As Spam Bulk Mail |`n" + $Result += "|------------|------------------------|`n" + foreach ($Policy in $FailedPolicies) { + $Result += "| $($Policy.Identity) | $($Policy.MarkAsSpamBulkMail) |`n" + } + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA101' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Low' -Name 'Bulk is marked as spam' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Anti-Spam' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA101' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Low' -Name 'Bulk is marked as spam' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Anti-Spam' + } +} diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA102.ps1 b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA102.ps1 new file mode 100644 index 000000000000..31da61d73746 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA102.ps1 @@ -0,0 +1,74 @@ +function Invoke-CippTestORCA102 { + <# + .SYNOPSIS + Advanced Spam filter options are turned off + #> + param($Tenant) + + try { + $Policies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoHostedContentFilterPolicy' + + if (-not $Policies) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA102' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Low' -Name 'Advanced Spam filter options are turned off' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Anti-Spam' + return + } + + $FailedPolicies = [System.Collections.Generic.List[object]]::new() + $PassedPolicies = [System.Collections.Generic.List[object]]::new() + + foreach ($Policy in $Policies) { + $ASFSettings = @( + $Policy.IncreaseScoreWithImageLinks, + $Policy.IncreaseScoreWithNumericIps, + $Policy.IncreaseScoreWithRedirectToOtherPort, + $Policy.IncreaseScoreWithBizOrInfoUrls, + $Policy.MarkAsSpamEmptyMessages, + $Policy.MarkAsSpamJavaScriptInHtml, + $Policy.MarkAsSpamFramesInHtml, + $Policy.MarkAsSpamObjectTagsInHtml, + $Policy.MarkAsSpamEmbedTagsInHtml, + $Policy.MarkAsSpamFormTagsInHtml, + $Policy.MarkAsSpamWebBugsInHtml, + $Policy.MarkAsSpamSensitiveWordList, + $Policy.MarkAsSpamFromAddressAuthFail, + $Policy.MarkAsSpamNdrBackscatter, + $Policy.MarkAsSpamSpfRecordHardFail + ) + + $EnabledASF = $ASFSettings | Where-Object { $_ -eq 'On' } + + if ($EnabledASF.Count -eq 0) { + $PassedPolicies.Add($Policy) | Out-Null + } else { + $FailedPolicies.Add($Policy) | Out-Null + } + } + + if ($FailedPolicies.Count -eq 0) { + $Status = 'Passed' + $Result = "All anti-spam policies have Advanced Spam Filter (ASF) options turned off.`n`n" + $Result += "**Compliant Policies:** $($PassedPolicies.Count)" + } else { + $Status = 'Failed' + $Result = "$($FailedPolicies.Count) anti-spam policies have Advanced Spam Filter (ASF) options enabled.`n`n" + $Result += "**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n" + $Result += "| Policy Name | Enabled ASF Options |`n" + $Result += "|------------|---------------------|`n" + foreach ($Policy in $FailedPolicies) { + $EnabledOptions = [System.Collections.Generic.List[string]]::new() + if ($Policy.IncreaseScoreWithImageLinks -eq 'On') { $EnabledOptions.Add('ImageLinks') | Out-Null } + if ($Policy.IncreaseScoreWithNumericIps -eq 'On') { $EnabledOptions.Add('NumericIPs') | Out-Null } + if ($Policy.MarkAsSpamEmptyMessages -eq 'On') { $EnabledOptions.Add('EmptyMessages') | Out-Null } + if ($Policy.MarkAsSpamJavaScriptInHtml -eq 'On') { $EnabledOptions.Add('JavaScript') | Out-Null } + $Result += "| $($Policy.Identity) | $($EnabledOptions -join ', ') |`n" + } + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA102' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Low' -Name 'Advanced Spam filter options are turned off' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Anti-Spam' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA102' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Low' -Name 'Advanced Spam filter options are turned off' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Anti-Spam' + } +} diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA103.ps1 b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA103.ps1 new file mode 100644 index 000000000000..10171890ecec --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA103.ps1 @@ -0,0 +1,68 @@ +function Invoke-CippTestORCA103 { + <# + .SYNOPSIS + Outbound spam filter policy settings configured + #> + param($Tenant) + + try { + $Policies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoHostedOutboundSpamFilterPolicy' + + if (-not $Policies) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA103' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'Outbound spam filter policy settings configured' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Anti-Spam' + return + } + + $FailedPolicies = [System.Collections.Generic.List[object]]::new() + $PassedPolicies = [System.Collections.Generic.List[object]]::new() + + foreach ($Policy in $Policies) { + $IsCompliant = $true + $Issues = [System.Collections.Generic.List[string]]::new() + + if ($Policy.RecipientLimitExternalPerHour -ne 500) { + $IsCompliant = $false + $Issues.Add("RecipientLimitExternalPerHour: $($Policy.RecipientLimitExternalPerHour) (should be 500)") | Out-Null + } + if ($Policy.RecipientLimitInternalPerHour -ne 1000) { + $IsCompliant = $false + $Issues.Add("RecipientLimitInternalPerHour: $($Policy.RecipientLimitInternalPerHour) (should be 1000)") | Out-Null + } + if ($Policy.ActionWhenThresholdReached -ne 'BlockUserForToday') { + $IsCompliant = $false + $Issues.Add("ActionWhenThresholdReached: $($Policy.ActionWhenThresholdReached) (should be BlockUserForToday)") | Out-Null + } + + if ($IsCompliant) { + $PassedPolicies.Add($Policy) | Out-Null + } else { + $FailedPolicies.Add([PSCustomObject]@{ + Policy = $Policy + Issues = $Issues + }) | Out-Null + } + } + + if ($FailedPolicies.Count -eq 0) { + $Status = 'Passed' + $Result = "All outbound spam filter policies are configured correctly.`n`n" + $Result += "**Compliant Policies:** $($PassedPolicies.Count)" + } else { + $Status = 'Failed' + $Result = "$($FailedPolicies.Count) outbound spam filter policies are not configured correctly.`n`n" + $Result += "**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n" + $Result += "| Policy Name | Issues |`n" + $Result += "|------------|--------|`n" + foreach ($Failed in $FailedPolicies) { + $Result += "| $($Failed.Policy.Identity) | $($Failed.Issues -join '
') |`n" + } + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA103' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'Outbound spam filter policy settings configured' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Anti-Spam' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA103' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Outbound spam filter policy settings configured' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Anti-Spam' + } + } diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA105.ps1 b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA105.ps1 new file mode 100644 index 000000000000..84d4a6dd7ab4 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA105.ps1 @@ -0,0 +1,49 @@ +function Invoke-CippTestORCA105 { + <# + .SYNOPSIS + Safe Links Synchronous URL detonation is enabled + #> + param($Tenant) + + try { + $Policies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoSafeLinksPolicies' + + if (-not $Policies) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA105' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'Safe Links Synchronous URL detonation is enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Safe Links' + return + } + + $FailedPolicies = [System.Collections.Generic.List[object]]::new() + $PassedPolicies = [System.Collections.Generic.List[object]]::new() + + foreach ($Policy in $Policies) { + if ($Policy.DeliverMessageAfterScan -eq $true) { + $PassedPolicies.Add($Policy) | Out-Null + } else { + $FailedPolicies.Add($Policy) | Out-Null + } + } + + if ($FailedPolicies.Count -eq 0) { + $Status = 'Passed' + $Result = "All Safe Links policies have synchronous URL detonation enabled.`n`n" + $Result += "**Compliant Policies:** $($PassedPolicies.Count)" + } else { + $Status = 'Failed' + $Result = "$($FailedPolicies.Count) Safe Links policies do not have synchronous URL detonation enabled.`n`n" + $Result += "**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n" + $Result += "| Policy Name | Deliver Message After Scan |`n" + $Result += "|------------|---------------------------|`n" + foreach ($Policy in $FailedPolicies) { + $Result += "| $($Policy.Identity) | $($Policy.DeliverMessageAfterScan) |`n" + } + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA105' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'Safe Links Synchronous URL detonation is enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Safe Links' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA105' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Safe Links Synchronous URL detonation is enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Safe Links' + } +} diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA106.ps1 b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA106.ps1 new file mode 100644 index 000000000000..2058a76fb616 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA106.ps1 @@ -0,0 +1,49 @@ +function Invoke-CippTestORCA106 { + <# + .SYNOPSIS + Quarantine retention period is 30 days + #> + param($Tenant) + + try { + $Policies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoHostedContentFilterPolicy' + + if (-not $Policies) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA106' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Low' -Name 'Quarantine retention period is 30 days' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Anti-Spam' + return + } + + $FailedPolicies = [System.Collections.Generic.List[object]]::new() + $PassedPolicies = [System.Collections.Generic.List[object]]::new() + + foreach ($Policy in $Policies) { + if ($Policy.QuarantineRetentionPeriod -gt 15) { + $PassedPolicies.Add($Policy) | Out-Null + } else { + $FailedPolicies.Add($Policy) | Out-Null + } + } + + if ($FailedPolicies.Count -eq 0) { + $Status = 'Passed' + $Result = "All anti-spam policies have quarantine retention period set to 30 days.`n`n" + $Result += "**Compliant Policies:** $($PassedPolicies.Count)" + } else { + $Status = 'Failed' + $Result = "$($FailedPolicies.Count) anti-spam policies do not have quarantine retention period set to 30 days.`n`n" + $Result += "**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n" + $Result += "| Policy Name | Quarantine Retention Period |`n" + $Result += "|------------|----------------------------|`n" + foreach ($Policy in $FailedPolicies) { + $Result += "| $($Policy.Identity) | $($Policy.QuarantineRetentionPeriod) days |`n" + } + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA106' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Low' -Name 'Quarantine retention period is 30 days' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Anti-Spam' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA106' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Low' -Name 'Quarantine retention period is 30 days' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Anti-Spam' + } +} diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA107.ps1 b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA107.ps1 new file mode 100644 index 000000000000..51f4a1a90e95 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA107.ps1 @@ -0,0 +1,57 @@ +function Invoke-CippTestORCA107 { + <# + .SYNOPSIS + End-user spam notification is enabled + #> + param($Tenant) + + try { + $Policies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoQuarantinePolicy' + + if (-not $Policies) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA107' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Low' -Name 'End-user spam notification is enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Quarantine' + return + } + + $FailedPolicies = [System.Collections.Generic.List[object]]::new() + $PassedPolicies = [System.Collections.Generic.List[object]]::new() + + foreach ($Policy in $Policies) { + if ($Policy.EndUserSpamNotificationFrequency -gt 0) { + $PassedPolicies.Add($Policy) | Out-Null + } else { + $FailedPolicies.Add($Policy) | Out-Null + } + } + + if ($FailedPolicies.Count -eq 0 -and $PassedPolicies.Count -gt 0) { + $Status = 'Passed' + $Result = "All quarantine policies have end-user spam notifications enabled.`n`n" + $Result += "**Compliant Policies:** $($PassedPolicies.Count)`n`n" + $Result += "| Policy Name | Notification Frequency (days) |`n" + $Result += "|------------|-------------------------------|`n" + foreach ($Policy in $PassedPolicies) { + $Result += "| $($Policy.Identity) | $($Policy.EndUserSpamNotificationFrequency) |`n" + } + } elseif ($PassedPolicies.Count -eq 0) { + $Status = 'Failed' + $Result = "No quarantine policies have end-user spam notifications enabled.`n`n" + } else { + $Status = 'Failed' + $Result = "$($FailedPolicies.Count) quarantine policies do not have end-user spam notifications enabled.`n`n" + $Result += "**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n" + $Result += "| Policy Name | Notification Frequency |`n" + $Result += "|------------|----------------------|`n" + foreach ($Policy in $FailedPolicies) { + $Result += "| $($Policy.Identity) | Disabled |`n" + } + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA107' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Low' -Name 'End-user spam notification is enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Quarantine' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA107' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Low' -Name 'End-user spam notification is enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Quarantine' + } +} diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA108_1.ps1 b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA108_1.ps1 new file mode 100644 index 000000000000..6d39c4e1d476 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA108_1.ps1 @@ -0,0 +1,53 @@ +function Invoke-CippTestORCA108_1 { + <# + .SYNOPSIS + DNS Records have been set up to support DKIM + #> + param($Tenant) + + try { + $DkimConfig = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoDkimSigningConfig' + $AcceptedDomains = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoAcceptedDomains' + + if (-not $DkimConfig -or -not $AcceptedDomains) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA108_1' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'DNS Records have been set up to support DKIM' -UserImpact 'High' -ImplementationEffort 'Medium' -Category 'DKIM' + return + } + + $FailedDomains = [System.Collections.Generic.List[object]]::new() + $PassedDomains = [System.Collections.Generic.List[object]]::new() + $CustomDomains = $AcceptedDomains | Where-Object { $_.DomainName -notlike '*onmicrosoft.com' } + + foreach ($Domain in $CustomDomains) { + $DkimRecord = $DkimConfig | Where-Object { $_.Domain -eq $Domain.DomainName } + + if ($DkimRecord -and $DkimRecord.Selector1CNAME -and $DkimRecord.Selector2CNAME) { + $PassedDomains.Add($Domain) | Out-Null + } else { + $FailedDomains.Add($Domain) | Out-Null + } + } + + if ($FailedDomains.Count -eq 0) { + $Status = 'Passed' + $Result = "All custom domains have DKIM DNS records configured.`n`n" + $Result += "**Compliant Domains:** $($PassedDomains.Count)" + } else { + $Status = 'Failed' + $Result = "$($FailedDomains.Count) custom domains do not have DKIM DNS records configured.`n`n" + $Result += "**Non-Compliant Domains:** $($FailedDomains.Count)`n`n" + $Result += "| Domain Name |`n" + $Result += "|------------|`n" + foreach ($Domain in $FailedDomains) { + $Result += "| $($Domain.DomainName) |`n" + } + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA108_1' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'DNS Records have been set up to support DKIM' -UserImpact 'High' -ImplementationEffort 'Medium' -Category 'DKIM' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA108_1' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'DNS Records have been set up to support DKIM' -UserImpact 'High' -ImplementationEffort 'Medium' -Category 'DKIM' + } +} diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA109.ps1 b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA109.ps1 new file mode 100644 index 000000000000..45bbfc43b14d --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA109.ps1 @@ -0,0 +1,54 @@ +function Invoke-CippTestORCA109 { + <# + .SYNOPSIS + Senders are not being allow listed in an unsafe manner + #> + param($Tenant) + + try { + $Policies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoHostedContentFilterPolicy' + + if (-not $Policies) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA109' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'Senders are not being allow listed in an unsafe manner' -UserImpact 'High' -ImplementationEffort 'Low' -Category 'Anti-Spam' + return + } + + $FailedPolicies = [System.Collections.Generic.List[object]]::new() + $PassedPolicies = [System.Collections.Generic.List[object]]::new() + + foreach ($Policy in $Policies) { + $HasAllowedSenders = ($Policy.AllowedSenders -and $Policy.AllowedSenders.Count -gt 0) -or + ($Policy.AllowedSenderDomains -and $Policy.AllowedSenderDomains.Count -gt 0) + + if (-not $HasAllowedSenders) { + $PassedPolicies.Add($Policy) | Out-Null + } else { + $FailedPolicies.Add($Policy) | Out-Null + } + } + + if ($FailedPolicies.Count -eq 0) { + $Status = 'Passed' + $Result = "No anti-spam policies have sender allow lists configured.`n`n" + $Result += "**Compliant Policies:** $($PassedPolicies.Count)" + } else { + $Status = 'Failed' + $Result = "$($FailedPolicies.Count) anti-spam policies have sender allow lists configured.`n`n" + $Result += "**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n" + $Result += "| Policy Name | Allowed Senders | Allowed Sender Domains |`n" + $Result += "|------------|----------------|----------------------|`n" + foreach ($Policy in $FailedPolicies) { + $SenderCount = if ($Policy.AllowedSenders) { $Policy.AllowedSenders.Count } else { 0 } + $DomainCount = if ($Policy.AllowedSenderDomains) { $Policy.AllowedSenderDomains.Count } else { 0 } + $Result += "| $($Policy.Identity) | $SenderCount | $DomainCount |`n" + } + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA109' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'Senders are not being allow listed in an unsafe manner' -UserImpact 'High' -ImplementationEffort 'Low' -Category 'Anti-Spam' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA109' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Senders are not being allow listed in an unsafe manner' -UserImpact 'High' -ImplementationEffort 'Low' -Category 'Anti-Spam' + } +} diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA110.ps1 b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA110.ps1 new file mode 100644 index 000000000000..1894ebd339d7 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA110.ps1 @@ -0,0 +1,49 @@ +function Invoke-CippTestORCA110 { + <# + .SYNOPSIS + Internal Sender notifications are disabled + #> + param($Tenant) + + try { + $Policies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoHostedContentFilterPolicy' + + if (-not $Policies) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA110' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Low' -Name 'Internal Sender notifications are disabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Anti-Spam' + return + } + + $FailedPolicies = [System.Collections.Generic.List[object]]::new() + $PassedPolicies = [System.Collections.Generic.List[object]]::new() + + foreach ($Policy in $Policies) { + if ($Policy.InlineSafetyTipsEnabled -eq $true) { + $PassedPolicies.Add($Policy) | Out-Null + } else { + $FailedPolicies.Add($Policy) | Out-Null + } + } + + if ($FailedPolicies.Count -eq 0) { + $Status = 'Passed' + $Result = "All anti-spam policies have internal sender notifications disabled.`n`n" + $Result += "**Compliant Policies:** $($PassedPolicies.Count)" + } else { + $Status = 'Failed' + $Result = "$($FailedPolicies.Count) anti-spam policies have internal sender notifications enabled.`n`n" + $Result += "**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n" + $Result += "| Policy Name | Inline Safety Tips Enabled |`n" + $Result += "|------------|---------------------------|`n" + foreach ($Policy in $FailedPolicies) { + $Result += "| $($Policy.Identity) | $($Policy.InlineSafetyTipsEnabled) |`n" + } + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA110' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Low' -Name 'Internal Sender notifications are disabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Anti-Spam' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA110' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Low' -Name 'Internal Sender notifications are disabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Anti-Spam' + } +} diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA111.ps1 b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA111.ps1 new file mode 100644 index 000000000000..1fa78226d75b --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA111.ps1 @@ -0,0 +1,49 @@ +function Invoke-CippTestORCA111 { + <# + .SYNOPSIS + Anti-phishing policy exists and EnableUnauthenticatedSender is true + #> + param($Tenant) + + try { + $Policies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoAntiPhishPolicies' + + if (-not $Policies) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA111' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'Unauthenticated Sender tagging enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Anti-Phish' + return + } + + $FailedPolicies = [System.Collections.Generic.List[object]]::new() + $PassedPolicies = [System.Collections.Generic.List[object]]::new() + + foreach ($Policy in $Policies) { + if ($Policy.EnableUnauthenticatedSender -eq $true) { + $PassedPolicies.Add($Policy) | Out-Null + } else { + $FailedPolicies.Add($Policy) | Out-Null + } + } + + if ($FailedPolicies.Count -eq 0) { + $Status = 'Passed' + $Result = "All anti-phishing policies have unauthenticated sender tagging enabled.`n`n" + $Result += "**Compliant Policies:** $($PassedPolicies.Count)" + } else { + $Status = 'Failed' + $Result = "$($FailedPolicies.Count) anti-phishing policies do not have unauthenticated sender tagging enabled.`n`n" + $Result += "**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n" + $Result += "| Policy Name | Enable Unauthenticated Sender |`n" + $Result += "|------------|------------------------------|`n" + foreach ($Policy in $FailedPolicies) { + $Result += "| $($Policy.Identity) | $($Policy.EnableUnauthenticatedSender) |`n" + } + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA111' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'Unauthenticated Sender tagging enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Anti-Phish' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA111' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'Unauthenticated Sender tagging enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Anti-Phish' + } +} diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA112.ps1 b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA112.ps1 new file mode 100644 index 000000000000..84761e1e4d29 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA112.ps1 @@ -0,0 +1,49 @@ +function Invoke-CippTestORCA112 { + <# + .SYNOPSIS + Anti-spoofing protection action is configured to Move message to Junk Email folders + #> + param($Tenant) + + try { + $Policies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoAntiPhishPolicies' + + if (-not $Policies) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA112' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'Anti-spoofing protection action configured' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Anti-Phish' + return + } + + $FailedPolicies = [System.Collections.Generic.List[object]]::new() + $PassedPolicies = [System.Collections.Generic.List[object]]::new() + + foreach ($Policy in $Policies) { + if ($Policy.AuthenticationFailAction -eq 'MoveToJmf') { + $PassedPolicies.Add($Policy) | Out-Null + } else { + $FailedPolicies.Add($Policy) | Out-Null + } + } + + if ($FailedPolicies.Count -eq 0) { + $Status = 'Passed' + $Result = "All anti-phishing policies have anti-spoofing action set to Move to Junk Email folder.`n`n" + $Result += "**Compliant Policies:** $($PassedPolicies.Count)" + } else { + $Status = 'Failed' + $Result = "$($FailedPolicies.Count) anti-phishing policies do not have anti-spoofing action set to Move to Junk Email folder.`n`n" + $Result += "**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n" + $Result += "| Policy Name | Authentication Fail Action |`n" + $Result += "|------------|---------------------------|`n" + foreach ($Policy in $FailedPolicies) { + $Result += "| $($Policy.Identity) | $($Policy.AuthenticationFailAction) |`n" + } + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA112' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'Anti-spoofing protection action configured' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Anti-Phish' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA112' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Anti-spoofing protection action configured' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Anti-Phish' + } +} diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA114.ps1 b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA114.ps1 new file mode 100644 index 000000000000..bcafb040cb04 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA114.ps1 @@ -0,0 +1,52 @@ +function Invoke-CippTestORCA114 { + <# + .SYNOPSIS + No IP Allow Lists have been configured + #> + param($Tenant) + + try { + $Policies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoHostedContentFilterPolicy' + + if (-not $Policies) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA114' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'No IP Allow Lists have been configured' -UserImpact 'High' -ImplementationEffort 'Low' -Category 'Anti-Spam' + return + } + +$FailedPolicies = [System.Collections.Generic.List[object]]::new() + $PassedPolicies = [System.Collections.Generic.List[object]]::new() + + foreach ($Policy in $Policies) { + $HasIPAllowList = ($Policy.IPAllowList -and $Policy.IPAllowList.Count -gt 0) + + if (-not $HasIPAllowList) { + $PassedPolicies.Add($Policy) | Out-Null + } else { + $FailedPolicies.Add($Policy) | Out-Null + } + } + + if ($FailedPolicies.Count -eq 0) { + $Status = 'Passed' + $Result = "No anti-spam policies have IP allow lists configured.`n`n" + $Result += "**Compliant Policies:** $($PassedPolicies.Count)" + } else { + $Status = 'Failed' + $Result = "$($FailedPolicies.Count) anti-spam policies have IP allow lists configured.`n`n" + $Result += "**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n" + $Result += "| Policy Name | IP Allow List Count |`n" + $Result += "|------------|-------------------|`n" + foreach ($Policy in $FailedPolicies) { + $IPCount = if ($Policy.IPAllowList) { $Policy.IPAllowList.Count } else { 0 } + $Result += "| $($Policy.Identity) | $IPCount |`n" + } + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA114' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'No IP Allow Lists have been configured' -UserImpact 'High' -ImplementationEffort 'Low' -Category 'Anti-Spam' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA114' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'No IP Allow Lists have been configured' -UserImpact 'High' -ImplementationEffort 'Low' -Category 'Anti-Spam' + } +} diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA115.ps1 b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA115.ps1 new file mode 100644 index 000000000000..6c9e383ca1d8 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA115.ps1 @@ -0,0 +1,49 @@ +function Invoke-CippTestORCA115 { + <# + .SYNOPSIS + Mailbox intelligence based impersonation protection is enabled + #> + param($Tenant) + + try { + $Policies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoAntiPhishPolicies' + + if (-not $Policies) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA115' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'Mailbox intelligence based impersonation protection is enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Anti-Phish' + return + } + + $FailedPolicies = [System.Collections.Generic.List[object]]::new() + $PassedPolicies = [System.Collections.Generic.List[object]]::new() + + foreach ($Policy in $Policies) { + if ($Policy.EnableMailboxIntelligenceProtection -eq $true) { + $PassedPolicies.Add($Policy) | Out-Null + } else { + $FailedPolicies.Add($Policy) | Out-Null + } + } + + if ($FailedPolicies.Count -eq 0) { + $Status = 'Passed' + $Result = "All anti-phishing policies have mailbox intelligence based impersonation protection enabled.`n`n" + $Result += "**Compliant Policies:** $($PassedPolicies.Count)" + } else { + $Status = 'Failed' + $Result = "$($FailedPolicies.Count) anti-phishing policies do not have mailbox intelligence based impersonation protection enabled.`n`n" + $Result += "**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n" + $Result += "| Policy Name | Enable Mailbox Intelligence Protection |`n" + $Result += "|------------|---------------------------------------|`n" + foreach ($Policy in $FailedPolicies) { + $Result += "| $($Policy.Identity) | $($Policy.EnableMailboxIntelligenceProtection) |`n" + } + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA115' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'Mailbox intelligence based impersonation protection is enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Anti-Phish' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA115' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'Mailbox intelligence based impersonation protection is enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Anti-Phish' + } +} diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA116.ps1 b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA116.ps1 new file mode 100644 index 000000000000..3677473a0684 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA116.ps1 @@ -0,0 +1,49 @@ +function Invoke-CippTestORCA116 { + <# + .SYNOPSIS + Mailbox intelligence based impersonation protection action set to move message to junk mail folder + #> + param($Tenant) + + try { + $Policies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoAntiPhishPolicies' + + if (-not $Policies) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA116' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'Mailbox intelligence impersonation protection action configured' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Anti-Phish' + return + } + + $FailedPolicies = [System.Collections.Generic.List[object]]::new() + $PassedPolicies = [System.Collections.Generic.List[object]]::new() + + foreach ($Policy in $Policies) { + if ($Policy.MailboxIntelligenceProtectionAction -eq 'MoveToJmf') { + $PassedPolicies.Add($Policy) | Out-Null + } else { + $FailedPolicies.Add($Policy) | Out-Null + } + } + + if ($FailedPolicies.Count -eq 0) { + $Status = 'Passed' + $Result = "All anti-phishing policies have mailbox intelligence impersonation protection action set to Move to Junk Email folder.`n`n" + $Result += "**Compliant Policies:** $($PassedPolicies.Count)" + } else { + $Status = 'Failed' + $Result = "$($FailedPolicies.Count) anti-phishing policies do not have mailbox intelligence impersonation protection action set to Move to Junk Email folder.`n`n" + $Result += "**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n" + $Result += "| Policy Name | Mailbox Intelligence Protection Action |`n" + $Result += "|------------|---------------------------------------|`n" + foreach ($Policy in $FailedPolicies) { + $Result += "| $($Policy.Identity) | $($Policy.MailboxIntelligenceProtectionAction) |`n" + } + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA116' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'Mailbox intelligence impersonation protection action configured' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Anti-Phish' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA116' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'Mailbox intelligence impersonation protection action configured' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Anti-Phish' + } +} diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA118_1.ps1 b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA118_1.ps1 new file mode 100644 index 000000000000..eadab025151c --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA118_1.ps1 @@ -0,0 +1,52 @@ +function Invoke-CippTestORCA118_1 { + <# + .SYNOPSIS + Domains not allow listed in Anti-Spam + #> + param($Tenant) + + try { + $Policies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoHostedContentFilterPolicy' + + if (-not $Policies) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA118_1' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'Domains not allow listed in Anti-Spam' -UserImpact 'High' -ImplementationEffort 'Low' -Category 'Anti-Spam' + return + } + + $FailedPolicies = [System.Collections.Generic.List[object]]::new() + $PassedPolicies = [System.Collections.Generic.List[object]]::new() + + foreach ($Policy in $Policies) { + $HasAllowedDomains = ($Policy.AllowedSenderDomains -and $Policy.AllowedSenderDomains.Count -gt 0) + + if (-not $HasAllowedDomains) { + $PassedPolicies.Add($Policy) | Out-Null + } else { + $FailedPolicies.Add($Policy) | Out-Null + } + } + + if ($FailedPolicies.Count -eq 0) { + $Status = 'Passed' + $Result = "No anti-spam policies have allowed sender domains configured.`n`n" + $Result += "**Compliant Policies:** $($PassedPolicies.Count)" + } else { + $Status = 'Failed' + $Result = "$($FailedPolicies.Count) anti-spam policies have allowed sender domains configured.`n`n" + $Result += "**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n" + $Result += "| Policy Name | Allowed Sender Domains Count |`n" + $Result += "|------------|------------------------------|`n" + foreach ($Policy in $FailedPolicies) { + $Count = if ($Policy.AllowedSenderDomains) { $Policy.AllowedSenderDomains.Count } else { 0 } + $Result += "| $($Policy.Identity) | $Count |`n" + } + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA118_1' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'Domains not allow listed in Anti-Spam' -UserImpact 'High' -ImplementationEffort 'Low' -Category 'Anti-Spam' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA118_1' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Domains not allow listed in Anti-Spam' -UserImpact 'High' -ImplementationEffort 'Low' -Category 'Anti-Spam' + } +} diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA118_2.ps1 b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA118_2.ps1 new file mode 100644 index 000000000000..15e795a6b9cf --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA118_2.ps1 @@ -0,0 +1,48 @@ +function Invoke-CippTestORCA118_2 { + <# + .SYNOPSIS + Domains not allow listed in Transport Rules + #> + param($Tenant) + + try { + $TransportRules = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoTransportRules' + + if (-not $TransportRules) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA118_2' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'Domains not allow listed in Transport Rules' -UserImpact 'High' -ImplementationEffort 'Low' -Category 'Transport Rules' + return + } + + $FailedRules = [System.Collections.Generic.List[object]]::new() + + foreach ($Rule in $TransportRules) { + # Check if rule sets SCL to -1 (bypass spam filtering) based on sender domain + if ($Rule.SetSCL -eq -1 -and $Rule.SenderDomainIs) { + $FailedRules.Add($Rule) | Out-Null + } + } + + if ($FailedRules.Count -eq 0) { + $Status = 'Passed' + $Result = "No transport rules allow list domains by setting SCL to -1.`n`n" + $Result += "**Total Transport Rules Checked:** $($TransportRules.Count)" + } else { + $Status = 'Failed' + $Result = "$($FailedRules.Count) transport rules allow list domains by setting SCL to -1.`n`n" + $Result += "**Non-Compliant Rules:** $($FailedRules.Count)`n`n" + $Result += "| Rule Name | Sender Domains |`n" + $Result += "|-----------|---------------|`n" + foreach ($Rule in $FailedRules) { + $Domains = if ($Rule.SenderDomainIs) { ($Rule.SenderDomainIs -join ', ') } else { 'N/A' } + $Result += "| $($Rule.Name) | $Domains |`n" + } + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA118_2' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'Domains not allow listed in Transport Rules' -UserImpact 'High' -ImplementationEffort 'Low' -Category 'Transport Rules' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA118_2' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Domains not allow listed in Transport Rules' -UserImpact 'High' -ImplementationEffort 'Low' -Category 'Transport Rules' + } +} diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA118_3.ps1 b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA118_3.ps1 new file mode 100644 index 000000000000..a01284c51be7 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA118_3.ps1 @@ -0,0 +1,68 @@ +function Invoke-CippTestORCA118_3 { + <# + .SYNOPSIS + Own domains not allow listed in Anti-Spam + #> + param($Tenant) + + try { + $Policies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoHostedContentFilterPolicy' + $AcceptedDomains = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoAcceptedDomains' + + if (-not $Policies) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA118_3' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'Own domains not allow listed in Anti-Spam' -UserImpact 'High' -ImplementationEffort 'Low' -Category 'Anti-Spam' + return + } + + if (-not $AcceptedDomains) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA118_3' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No accepted domains found in database.' -Risk 'High' -Name 'Own domains not allow listed in Anti-Spam' -UserImpact 'High' -ImplementationEffort 'Low' -Category 'Anti-Spam' + return + } + + $OwnDomains = $AcceptedDomains | Select-Object -ExpandProperty DomainName + $FailedPolicies = [System.Collections.Generic.List[object]]::new() + $PassedPolicies = [System.Collections.Generic.List[object]]::new() + + foreach ($Policy in $Policies) { + $HasOwnDomainInAllowList = $false + + if ($Policy.AllowedSenderDomains) { + foreach ($AllowedDomain in $Policy.AllowedSenderDomains) { + if ($OwnDomains -contains $AllowedDomain) { + $HasOwnDomainInAllowList = $true + break + } + } + } + + if ($HasOwnDomainInAllowList) { + $FailedPolicies.Add($Policy) | Out-Null + } else { + $PassedPolicies.Add($Policy) | Out-Null + } + } + + if ($FailedPolicies.Count -eq 0) { + $Status = 'Passed' + $Result = "No anti-spam policies have own domains in the allow list.`n`n" + $Result += "**Compliant Policies:** $($PassedPolicies.Count)" + } else { + $Status = 'Failed' + $Result = "$($FailedPolicies.Count) anti-spam policies have own domains in the allow list.`n`n" + $Result += "**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n" + $Result += "| Policy Name | Own Domains in Allow List |`n" + $Result += "|------------|---------------------------|`n" + foreach ($Policy in $FailedPolicies) { + $OwnDomainsInList = $Policy.AllowedSenderDomains | Where-Object { $OwnDomains -contains $_ } + $Result += "| $($Policy.Identity) | $($OwnDomainsInList -join ', ') |`n" + } + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA118_3' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'Own domains not allow listed in Anti-Spam' -UserImpact 'High' -ImplementationEffort 'Low' -Category 'Anti-Spam' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA118_3' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Own domains not allow listed in Anti-Spam' -UserImpact 'High' -ImplementationEffort 'Low' -Category 'Anti-Spam' + } +} diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA118_4.ps1 b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA118_4.ps1 new file mode 100644 index 000000000000..ace3850208a7 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA118_4.ps1 @@ -0,0 +1,65 @@ +function Invoke-CippTestORCA118_4 { + <# + .SYNOPSIS + Own domains not allow listed in Transport Rules + #> + param($Tenant) + + try { + $TransportRules = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoTransportRules' + $AcceptedDomains = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoAcceptedDomains' + + if (-not $TransportRules) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA118_4' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'Own domains not allow listed in Transport Rules' -UserImpact 'High' -ImplementationEffort 'Low' -Category 'Transport Rules' + return + } + + if (-not $AcceptedDomains) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA118_4' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No accepted domains found in database.' -Risk 'High' -Name 'Own domains not allow listed in Transport Rules' -UserImpact 'High' -ImplementationEffort 'Low' -Category 'Transport Rules' + return + } + + $OwnDomains = $AcceptedDomains | Select-Object -ExpandProperty DomainName + $FailedRules = [System.Collections.Generic.List[object]]::new() + + foreach ($Rule in $TransportRules) { + # Check if rule sets SCL to -1 (bypass spam filtering) based on sender domain + if ($Rule.SetSCL -eq -1 -and $Rule.SenderDomainIs) { + $HasOwnDomain = $false + foreach ($SenderDomain in $Rule.SenderDomainIs) { + if ($OwnDomains -contains $SenderDomain) { + $HasOwnDomain = $true + break + } + } + + if ($HasOwnDomain) { + $FailedRules.Add($Rule) | Out-Null + } + } + } + + if ($FailedRules.Count -eq 0) { + $Status = 'Passed' + $Result = "No transport rules allow list own domains by setting SCL to -1.`n`n" + $Result += "**Total Transport Rules Checked:** $($TransportRules.Count)" + } else { + $Status = 'Failed' + $Result = "$($FailedRules.Count) transport rules allow list own domains by setting SCL to -1.`n`n" + $Result += "**Non-Compliant Rules:** $($FailedRules.Count)`n`n" + $Result += "| Rule Name | Own Domains in Rule |`n" + $Result += "|-----------|-------------------|`n" + foreach ($Rule in $FailedRules) { + $OwnDomainsInRule = $Rule.SenderDomainIs | Where-Object { $OwnDomains -contains $_ } + $Result += "| $($Rule.Name) | $($OwnDomainsInRule -join ', ') |`n" + } + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA118_4' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'Own domains not allow listed in Transport Rules' -UserImpact 'High' -ImplementationEffort 'Low' -Category 'Transport Rules' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA118_4' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Own domains not allow listed in Transport Rules' -UserImpact 'High' -ImplementationEffort 'Low' -Category 'Transport Rules' + } +} diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA119.ps1 b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA119.ps1 new file mode 100644 index 000000000000..8f8cbbca6ece --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA119.ps1 @@ -0,0 +1,49 @@ +function Invoke-CippTestORCA119 { + <# + .SYNOPSIS + Similar Domains Safety Tips is enabled + #> + param($Tenant) + + try { + $Policies = New-CIPPDbRequest -TenantFilter $Tenant -TestType 'ExoAntiPhishPolicies' + + if (-not $Policies) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA119' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'Similar Domains Safety Tips is enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Anti-Phish' + return + } + + $FailedPolicies = [System.Collections.Generic.List[object]]::new() + $PassedPolicies = [System.Collections.Generic.List[object]]::new() + + foreach ($Policy in $Policies) { + if ($Policy.EnableSimilarDomainsSafetyTips -eq $true) { + $PassedPolicies.Add($Policy) | Out-Null + } else { + $FailedPolicies.Add($Policy) | Out-Null + } + } + + if ($FailedPolicies.Count -eq 0) { + $Status = 'Passed' + $Result = "All anti-phishing policies have Similar Domains Safety Tips enabled.`n`n" + $Result += "**Compliant Policies:** $($PassedPolicies.Count)" + } else { + $Status = 'Failed' + $Result = "$($FailedPolicies.Count) anti-phishing policies do not have Similar Domains Safety Tips enabled.`n`n" + $Result += "**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n" + $Result += "| Policy Name | Enable Similar Domains Safety Tips |`n" + $Result += "|------------|-----------------------------------|`n" + foreach ($Policy in $FailedPolicies) { + $Result += "| $($Policy.Identity) | $($Policy.EnableSimilarDomainsSafetyTips) |`n" + } + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA119' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'Similar Domains Safety Tips is enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Anti-Phish' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA119' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'Similar Domains Safety Tips is enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Anti-Phish' + } +} diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA120_malware.ps1 b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA120_malware.ps1 new file mode 100644 index 000000000000..360ddb3b97bd --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA120_malware.ps1 @@ -0,0 +1,49 @@ +function Invoke-CippTestORCA120_malware { + <# + .SYNOPSIS + Zero Hour Autopurge Enabled for Malware + #> + param($Tenant) + + try { + $Policies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoMalwareFilterPolicies' + + if (-not $Policies) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA120_malware' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'Zero Hour Autopurge Enabled for Malware' -UserImpact 'High' -ImplementationEffort 'Low' -Category 'Malware' + return + } + + $FailedPolicies = [System.Collections.Generic.List[object]]::new() + $PassedPolicies = [System.Collections.Generic.List[object]]::new() + + foreach ($Policy in $Policies) { + if ($Policy.ZapEnabled -eq $true) { + $PassedPolicies.Add($Policy) | Out-Null + } else { + $FailedPolicies.Add($Policy) | Out-Null + } + } + + if ($FailedPolicies.Count -eq 0) { + $Status = 'Passed' + $Result = "All malware filter policies have Zero Hour Autopurge enabled.`n`n" + $Result += "**Compliant Policies:** $($PassedPolicies.Count)" + } else { + $Status = 'Failed' + $Result = "$($FailedPolicies.Count) malware filter policies do not have Zero Hour Autopurge enabled.`n`n" + $Result += "**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n" + $Result += "| Policy Name | ZAP Enabled |`n" + $Result += "|------------|------------|`n" + foreach ($Policy in $FailedPolicies) { + $Result += "| $($Policy.Identity) | $($Policy.ZapEnabled) |`n" + } + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA120_malware' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'Zero Hour Autopurge Enabled for Malware' -UserImpact 'High' -ImplementationEffort 'Low' -Category 'Malware' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA120_malware' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Zero Hour Autopurge Enabled for Malware' -UserImpact 'High' -ImplementationEffort 'Low' -Category 'Malware' + } +} diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA120_phish.ps1 b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA120_phish.ps1 new file mode 100644 index 000000000000..b13438033f14 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA120_phish.ps1 @@ -0,0 +1,49 @@ +function Invoke-CippTestORCA120_phish { + <# + .SYNOPSIS + Zero Hour Autopurge Enabled for Phish + #> + param($Tenant) + + try { + $Policies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoHostedContentFilterPolicy' + + if (-not $Policies) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA120_phish' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'Zero Hour Autopurge Enabled for Phish' -UserImpact 'High' -ImplementationEffort 'Low' -Category 'Anti-Spam' + return + } + + $FailedPolicies = [System.Collections.Generic.List[object]]::new() + $PassedPolicies = [System.Collections.Generic.List[object]]::new() + + foreach ($Policy in $Policies) { + if ($Policy.PhishZapEnabled -eq $true) { + $PassedPolicies.Add($Policy) | Out-Null + } else { + $FailedPolicies.Add($Policy) | Out-Null + } + } + + if ($FailedPolicies.Count -eq 0) { + $Status = 'Passed' + $Result = "All anti-spam policies have Zero Hour Autopurge for Phish enabled.`n`n" + $Result += "**Compliant Policies:** $($PassedPolicies.Count)" + } else { + $Status = 'Failed' + $Result = "$($FailedPolicies.Count) anti-spam policies do not have Zero Hour Autopurge for Phish enabled.`n`n" + $Result += "**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n" + $Result += "| Policy Name | Phish ZAP Enabled |`n" + $Result += "|------------|------------------|`n" + foreach ($Policy in $FailedPolicies) { + $Result += "| $($Policy.Identity) | $($Policy.PhishZapEnabled) |`n" + } + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA120_phish' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'Zero Hour Autopurge Enabled for Phish' -UserImpact 'High' -ImplementationEffort 'Low' -Category 'Anti-Spam' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA120_phish' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Zero Hour Autopurge Enabled for Phish' -UserImpact 'High' -ImplementationEffort 'Low' -Category 'Anti-Spam' + } +} diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA120_spam.ps1 b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA120_spam.ps1 new file mode 100644 index 000000000000..974c836d12e0 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA120_spam.ps1 @@ -0,0 +1,49 @@ +function Invoke-CippTestORCA120_spam { + <# + .SYNOPSIS + Zero Hour Autopurge Enabled for Spam + #> + param($Tenant) + + try { + $Policies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoHostedContentFilterPolicy' + + if (-not $Policies) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA120_spam' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'Zero Hour Autopurge Enabled for Spam' -UserImpact 'High' -ImplementationEffort 'Low' -Category 'Anti-Spam' + return + } + + $FailedPolicies = [System.Collections.Generic.List[object]]::new() + $PassedPolicies = [System.Collections.Generic.List[object]]::new() + + foreach ($Policy in $Policies) { + if ($Policy.SpamZapEnabled -eq $true) { + $PassedPolicies.Add($Policy) | Out-Null + } else { + $FailedPolicies.Add($Policy) | Out-Null + } + } + + if ($FailedPolicies.Count -eq 0) { + $Status = 'Passed' + $Result = "All anti-spam policies have Zero Hour Autopurge for Spam enabled.`n`n" + $Result += "**Compliant Policies:** $($PassedPolicies.Count)" + } else { + $Status = 'Failed' + $Result = "$($FailedPolicies.Count) anti-spam policies do not have Zero Hour Autopurge for Spam enabled.`n`n" + $Result += "**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n" + $Result += "| Policy Name | Spam ZAP Enabled |`n" + $Result += "|------------|-----------------|`n" + foreach ($Policy in $FailedPolicies) { + $Result += "| $($Policy.Identity) | $($Policy.SpamZapEnabled) |`n" + } + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA120_spam' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'Zero Hour Autopurge Enabled for Spam' -UserImpact 'High' -ImplementationEffort 'Low' -Category 'Anti-Spam' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA120_spam' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Zero Hour Autopurge Enabled for Spam' -UserImpact 'High' -ImplementationEffort 'Low' -Category 'Anti-Spam' + } +} diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA121.ps1 b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA121.ps1 new file mode 100644 index 000000000000..cbf3d7499d06 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA121.ps1 @@ -0,0 +1,27 @@ +function Invoke-CippTestORCA121 { + <# + .SYNOPSIS + Supported filter policy action used + #> + param($Tenant) + + try { + $Policies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoQuarantinePolicy' + + if (-not $Policies) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA121' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Low' -Name 'Supported filter policy action used' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Quarantine' + return + } + + $Status = 'Passed' + $Result = "Quarantine policies are configured to support Zero Hour Auto Purge.`n`n" + $Result += "**Total Policies:** $($Policies.Count)" + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA121' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Low' -Name 'Supported filter policy action used' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Quarantine' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA121' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Low' -Name 'Supported filter policy action used' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Quarantine' + } +} diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA123.ps1 b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA123.ps1 new file mode 100644 index 000000000000..dd08fdaee61c --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA123.ps1 @@ -0,0 +1,49 @@ +function Invoke-CippTestORCA123 { + <# + .SYNOPSIS + Unusual Characters Safety Tips is enabled + #> + param($Tenant) + + try { + $Policies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoAntiPhishPolicies' + + if (-not $Policies) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA123' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'Unusual Characters Safety Tips is enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Anti-Phish' + return + } + + $FailedPolicies = [System.Collections.Generic.List[object]]::new() + $PassedPolicies = [System.Collections.Generic.List[object]]::new() + + foreach ($Policy in $Policies) { + if ($Policy.EnableUnusualCharactersSafetyTips -eq $true) { + $PassedPolicies.Add($Policy) | Out-Null + } else { + $FailedPolicies.Add($Policy) | Out-Null + } + } + + if ($FailedPolicies.Count -eq 0) { + $Status = 'Passed' + $Result = "All anti-phishing policies have Unusual Characters Safety Tips enabled.`n`n" + $Result += "**Compliant Policies:** $($PassedPolicies.Count)" + } else { + $Status = 'Failed' + $Result = "$($FailedPolicies.Count) anti-phishing policies do not have Unusual Characters Safety Tips enabled.`n`n" + $Result += "**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n" + $Result += "| Policy Name | Enable Unusual Characters Safety Tips |`n" + $Result += "|------------|---------------------------------------|`n" + foreach ($Policy in $FailedPolicies) { + $Result += "| $($Policy.Identity) | $($Policy.EnableUnusualCharactersSafetyTips) |`n" + } + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA123' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'Unusual Characters Safety Tips is enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Anti-Phish' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA123' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'Unusual Characters Safety Tips is enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Anti-Phish' + } +} diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA124.ps1 b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA124.ps1 new file mode 100644 index 000000000000..dcfe7a48b9c8 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA124.ps1 @@ -0,0 +1,49 @@ +function Invoke-CippTestORCA124 { + <# + .SYNOPSIS + Safe attachments unknown malware response set to block messages + #> + param($Tenant) + + try { + $Policies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoSafeAttachmentPolicies' + + if (-not $Policies) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA124' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'Safe attachments unknown malware response set to block messages' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Safe Attachments' + return + } + + $FailedPolicies = [System.Collections.Generic.List[object]]::new() + $PassedPolicies = [System.Collections.Generic.List[object]]::new() + + foreach ($Policy in $Policies) { + if ($Policy.Action -in @('Block', 'Quarantine')) { + $PassedPolicies.Add($Policy) | Out-Null + } else { + $FailedPolicies.Add($Policy) | Out-Null + } + } + + if ($FailedPolicies.Count -eq 0) { + $Status = 'Passed' + $Result = "All Safe Attachments policies have unknown malware response set to Block.`n`n" + $Result += "**Compliant Policies:** $($PassedPolicies.Count)" + } else { + $Status = 'Failed' + $Result = "$($FailedPolicies.Count) Safe Attachments policies do not have unknown malware response set to Block.`n`n" + $Result += "**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n" + $Result += "| Policy Name | Action |`n" + $Result += "|------------|--------|`n" + foreach ($Policy in $FailedPolicies) { + $Result += "| $($Policy.Identity) | $($Policy.Action) |`n" + } + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA124' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'Safe attachments unknown malware response set to block messages' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Safe Attachments' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA124' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Safe attachments unknown malware response set to block messages' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Safe Attachments' + } +} diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA139.ps1 b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA139.ps1 new file mode 100644 index 000000000000..d285fb841375 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA139.ps1 @@ -0,0 +1,49 @@ +function Invoke-CippTestORCA139 { + <# + .SYNOPSIS + Spam action set to move message to junk mail folder or quarantine + #> + param($Tenant) + + try { + $Policies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoHostedContentFilterPolicy' + + if (-not $Policies) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA139' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'Spam action set to move message to junk mail folder or quarantine' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Anti-Spam' + return + } + + $FailedPolicies = [System.Collections.Generic.List[object]]::new() + $PassedPolicies = [System.Collections.Generic.List[object]]::new() + + foreach ($Policy in $Policies) { + if ($Policy.SpamAction -in @('MoveToJmf', 'Quarantine')) { + $PassedPolicies.Add($Policy) | Out-Null + } else { + $FailedPolicies.Add($Policy) | Out-Null + } + } + + if ($FailedPolicies.Count -eq 0) { + $Status = 'Passed' + $Result = "All anti-spam policies have Spam action set to move to Junk Email folder or Quarantine.`n`n" + $Result += "**Compliant Policies:** $($PassedPolicies.Count)" + } else { + $Status = 'Failed' + $Result = "$($FailedPolicies.Count) anti-spam policies do not have Spam action set appropriately.`n`n" + $Result += "**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n" + $Result += "| Policy Name | Spam Action |`n" + $Result += "|------------|------------|`n" + foreach ($Policy in $FailedPolicies) { + $Result += "| $($Policy.Identity) | $($Policy.SpamAction) |`n" + } + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA139' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'Spam action set to move message to junk mail folder or quarantine' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Anti-Spam' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA139' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Spam action set to move message to junk mail folder or quarantine' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Anti-Spam' + } +} diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA140.ps1 b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA140.ps1 new file mode 100644 index 000000000000..df9bc587b330 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA140.ps1 @@ -0,0 +1,49 @@ +function Invoke-CippTestORCA140 { + <# + .SYNOPSIS + High Confidence Spam action set to Quarantine message + #> + param($Tenant) + + try { + $Policies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoHostedContentFilterPolicy' + + if (-not $Policies) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA140' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'High Confidence Spam action set to Quarantine message' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Anti-Spam' + return + } + + $FailedPolicies = [System.Collections.Generic.List[object]]::new() + $PassedPolicies = [System.Collections.Generic.List[object]]::new() + + foreach ($Policy in $Policies) { + if ($Policy.HighConfidenceSpamAction -eq 'Quarantine') { + $PassedPolicies.Add($Policy) | Out-Null + } else { + $FailedPolicies.Add($Policy) | Out-Null + } + } + + if ($FailedPolicies.Count -eq 0) { + $Status = 'Passed' + $Result = "All anti-spam policies have High Confidence Spam action set to Quarantine.`n`n" + $Result += "**Compliant Policies:** $($PassedPolicies.Count)" + } else { + $Status = 'Failed' + $Result = "$($FailedPolicies.Count) anti-spam policies do not have High Confidence Spam action set to Quarantine.`n`n" + $Result += "**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n" + $Result += "| Policy Name | High Confidence Spam Action |`n" + $Result += "|------------|---------------------------|`n" + foreach ($Policy in $FailedPolicies) { + $Result += "| $($Policy.Identity) | $($Policy.HighConfidenceSpamAction) |`n" + } + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA140' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'High Confidence Spam action set to Quarantine message' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Anti-Spam' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA140' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'High Confidence Spam action set to Quarantine message' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Anti-Spam' + } +} diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA141.ps1 b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA141.ps1 new file mode 100644 index 000000000000..678b28afb0ec --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA141.ps1 @@ -0,0 +1,49 @@ +function Invoke-CippTestORCA141 { + <# + .SYNOPSIS + Bulk action set to Move message to Junk Email Folder + #> + param($Tenant) + + try { + $Policies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoHostedContentFilterPolicy' + + if (-not $Policies) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA141' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Low' -Name 'Bulk action set to Move message to Junk Email Folder' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Anti-Spam' + return + } + + $FailedPolicies = [System.Collections.Generic.List[object]]::new() + $PassedPolicies = [System.Collections.Generic.List[object]]::new() + + foreach ($Policy in $Policies) { + if ($Policy.BulkSpamAction -in @('MoveToJmf', 'Quarantine')) { + $PassedPolicies.Add($Policy) | Out-Null + } else { + $FailedPolicies.Add($Policy) | Out-Null + } + } + + if ($FailedPolicies.Count -eq 0) { + $Status = 'Passed' + $Result = "All anti-spam policies have Bulk action set to Move to Junk Email folder.`n`n" + $Result += "**Compliant Policies:** $($PassedPolicies.Count)" + } else { + $Status = 'Failed' + $Result = "$($FailedPolicies.Count) anti-spam policies do not have Bulk action set to Move to Junk Email folder.`n`n" + $Result += "**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n" + $Result += "| Policy Name | Bulk Spam Action |`n" + $Result += "|------------|-----------------|`n" + foreach ($Policy in $FailedPolicies) { + $Result += "| $($Policy.Identity) | $($Policy.BulkSpamAction) |`n" + } + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA141' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Low' -Name 'Bulk action set to Move message to Junk Email Folder' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Anti-Spam' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA141' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Low' -Name 'Bulk action set to Move message to Junk Email Folder' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Anti-Spam' + } +} diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA142.ps1 b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA142.ps1 new file mode 100644 index 000000000000..41c7dcddbb2f --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA142.ps1 @@ -0,0 +1,49 @@ +function Invoke-CippTestORCA142 { + <# + .SYNOPSIS + Phish action set to Quarantine message + #> + param($Tenant) + + try { + $Policies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoHostedContentFilterPolicy' + + if (-not $Policies) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA142' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'Phish action set to Quarantine message' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Anti-Spam' + return + } + + $FailedPolicies = [System.Collections.Generic.List[object]]::new() + $PassedPolicies = [System.Collections.Generic.List[object]]::new() + + foreach ($Policy in $Policies) { + if ($Policy.PhishSpamAction -eq 'Quarantine') { + $PassedPolicies.Add($Policy) | Out-Null + } else { + $FailedPolicies.Add($Policy) | Out-Null + } + } + + if ($FailedPolicies.Count -eq 0) { + $Status = 'Passed' + $Result = "All anti-spam policies have Phish action set to Quarantine.`n`n" + $Result += "**Compliant Policies:** $($PassedPolicies.Count)" + } else { + $Status = 'Failed' + $Result = "$($FailedPolicies.Count) anti-spam policies do not have Phish action set to Quarantine.`n`n" + $Result += "**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n" + $Result += "| Policy Name | Phish Spam Action |`n" + $Result += "|------------|------------------|`n" + foreach ($Policy in $FailedPolicies) { + $Result += "| $($Policy.Identity) | $($Policy.PhishSpamAction) |`n" + } + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA142' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'Phish action set to Quarantine message' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Anti-Spam' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA142' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Phish action set to Quarantine message' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Anti-Spam' + } +} diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA143.ps1 b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA143.ps1 new file mode 100644 index 000000000000..6056b9cc78ad --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA143.ps1 @@ -0,0 +1,49 @@ +function Invoke-CippTestORCA143 { + <# + .SYNOPSIS + Safety Tips are enabled + #> + param($Tenant) + + try { + $Policies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoHostedContentFilterPolicy' + + if (-not $Policies) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA143' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Low' -Name 'Safety Tips are enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Anti-Spam' + return + } + + $FailedPolicies = [System.Collections.Generic.List[object]]::new() + $PassedPolicies = [System.Collections.Generic.List[object]]::new() + + foreach ($Policy in $Policies) { + if ($Policy.InlineSafetyTipsEnabled -eq $true) { + $PassedPolicies.Add($Policy) | Out-Null + } else { + $FailedPolicies.Add($Policy) | Out-Null + } + } + + if ($FailedPolicies.Count -eq 0) { + $Status = 'Passed' + $Result = "All anti-spam policies have Safety Tips enabled.`n`n" + $Result += "**Compliant Policies:** $($PassedPolicies.Count)" + } else { + $Status = 'Failed' + $Result = "$($FailedPolicies.Count) anti-spam policies do not have Safety Tips enabled.`n`n" + $Result += "**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n" + $Result += "| Policy Name | Inline Safety Tips Enabled |`n" + $Result += "|------------|---------------------------|`n" + foreach ($Policy in $FailedPolicies) { + $Result += "| $($Policy.Identity) | $($Policy.InlineSafetyTipsEnabled) |`n" + } + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA143' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Low' -Name 'Safety Tips are enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Anti-Spam' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA143' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Low' -Name 'Safety Tips are enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Anti-Spam' + } +} diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA156.ps1 b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA156.ps1 new file mode 100644 index 000000000000..d73eb7152c02 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA156.ps1 @@ -0,0 +1,49 @@ +function Invoke-CippTestORCA156 { + <# + .SYNOPSIS + Safe Links Policies are tracking when user clicks on safe links + #> + param($Tenant) + + try { + $Policies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoSafeLinksPolicies' + + if (-not $Policies) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA156' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Low' -Name 'Safe Links Policies are tracking user clicks' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Safe Links' + return + } + + $FailedPolicies = [System.Collections.Generic.List[object]]::new() + $PassedPolicies = [System.Collections.Generic.List[object]]::new() + + foreach ($Policy in $Policies) { + if ($Policy.TrackClicks -eq $true) { + $PassedPolicies.Add($Policy) | Out-Null + } else { + $FailedPolicies.Add($Policy) | Out-Null + } + } + + if ($FailedPolicies.Count -eq 0) { + $Status = 'Passed' + $Result = "All Safe Links policies are tracking user clicks.`n`n" + $Result += "**Compliant Policies:** $($PassedPolicies.Count)" + } else { + $Status = 'Failed' + $Result = "$($FailedPolicies.Count) Safe Links policies are not tracking user clicks.`n`n" + $Result += "**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n" + $Result += "| Policy Name | Track Clicks |`n" + $Result += "|------------|-------------|`n" + foreach ($Policy in $FailedPolicies) { + $Result += "| $($Policy.Identity) | $($Policy.TrackClicks) |`n" + } + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA156' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Low' -Name 'Safe Links Policies are tracking user clicks' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Safe Links' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA156' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Low' -Name 'Safe Links Policies are tracking user clicks' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Safe Links' + } +} diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA158.ps1 b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA158.ps1 new file mode 100644 index 000000000000..8f8852090651 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA158.ps1 @@ -0,0 +1,35 @@ +function Invoke-CippTestORCA158 { + <# + .SYNOPSIS + Safe Attachments is enabled for SharePoint and Teams + #> + param($Tenant) + + try { + $AtpPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoAtpPolicyForO365' + + if (-not $AtpPolicy) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA158' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'Safe Attachments enabled for SharePoint and Teams' -UserImpact 'High' -ImplementationEffort 'Low' -Category 'Safe Attachments' + return + } + + $Policy = $AtpPolicy | Select-Object -First 1 + + if ($Policy.EnableATPForSPOTeamsODB -eq $true) { + $Status = 'Passed' + $Result = "Safe Attachments is enabled for SharePoint, OneDrive, and Teams.`n`n" + $Result += "**EnableATPForSPOTeamsODB:** $($Policy.EnableATPForSPOTeamsODB)" + } else { + $Status = 'Failed' + $Result = "Safe Attachments is NOT enabled for SharePoint, OneDrive, and Teams.`n`n" + $Result += "**EnableATPForSPOTeamsODB:** $($Policy.EnableATPForSPOTeamsODB)" + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA158' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'Safe Attachments enabled for SharePoint and Teams' -UserImpact 'High' -ImplementationEffort 'Low' -Category 'Safe Attachments' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA158' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Safe Attachments enabled for SharePoint and Teams' -UserImpact 'High' -ImplementationEffort 'Low' -Category 'Safe Attachments' + } +} diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA179.ps1 b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA179.ps1 new file mode 100644 index 000000000000..e72205f9ee4c --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA179.ps1 @@ -0,0 +1,49 @@ +function Invoke-CippTestORCA179 { + <# + .SYNOPSIS + Safe Links is enabled intra-organization + #> + param($Tenant) + + try { + $Policies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoSafeLinksPolicies' + + if (-not $Policies) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA179' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'Safe Links is enabled intra-organization' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Safe Links' + return + } + + $FailedPolicies = [System.Collections.Generic.List[object]]::new() + $PassedPolicies = [System.Collections.Generic.List[object]]::new() + + foreach ($Policy in $Policies) { + if ($Policy.EnableForInternalSenders -eq $true) { + $PassedPolicies.Add($Policy) | Out-Null + } else { + $FailedPolicies.Add($Policy) | Out-Null + } + } + + if ($FailedPolicies.Count -eq 0) { + $Status = 'Passed' + $Result = "All Safe Links policies are enabled for internal senders.`n`n" + $Result += "**Compliant Policies:** $($PassedPolicies.Count)" + } else { + $Status = 'Failed' + $Result = "$($FailedPolicies.Count) Safe Links policies are not enabled for internal senders.`n`n" + $Result += "**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n" + $Result += "| Policy Name | Enable For Internal Senders |`n" + $Result += "|------------|----------------------------|`n" + foreach ($Policy in $FailedPolicies) { + $Result += "| $($Policy.Identity) | $($Policy.EnableForInternalSenders) |`n" + } + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA179' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'Safe Links is enabled intra-organization' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Safe Links' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA179' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'Safe Links is enabled intra-organization' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Safe Links' + } +} diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA180.ps1 b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA180.ps1 new file mode 100644 index 000000000000..af5b1840cd38 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA180.ps1 @@ -0,0 +1,49 @@ +function Invoke-CippTestORCA180 { + <# + .SYNOPSIS + Anti-phishing policy exists and EnableSpoofIntelligence is true + #> + param($Tenant) + + try { + $Policies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoAntiPhishPolicies' + + if (-not $Policies) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA180' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'Spoof Intelligence is enabled' -UserImpact 'High' -ImplementationEffort 'Low' -Category 'Anti-Phish' + return + } + + $FailedPolicies = [System.Collections.Generic.List[object]]::new() + $PassedPolicies = [System.Collections.Generic.List[object]]::new() + + foreach ($Policy in $Policies) { + if ($Policy.EnableSpoofIntelligence -eq $true) { + $PassedPolicies.Add($Policy) | Out-Null + } else { + $FailedPolicies.Add($Policy) | Out-Null + } + } + + if ($FailedPolicies.Count -eq 0) { + $Status = 'Passed' + $Result = "All anti-phishing policies have Spoof Intelligence enabled.`n`n" + $Result += "**Compliant Policies:** $($PassedPolicies.Count)" + } else { + $Status = 'Failed' + $Result = "$($FailedPolicies.Count) anti-phishing policies do not have Spoof Intelligence enabled.`n`n" + $Result += "**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n" + $Result += "| Policy Name | Enable Spoof Intelligence |`n" + $Result += "|------------|--------------------------|`n" + foreach ($Policy in $FailedPolicies) { + $Result += "| $($Policy.Identity) | $($Policy.EnableSpoofIntelligence) |`n" + } + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA180' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'Spoof Intelligence is enabled' -UserImpact 'High' -ImplementationEffort 'Low' -Category 'Anti-Phish' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA180' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Spoof Intelligence is enabled' -UserImpact 'High' -ImplementationEffort 'Low' -Category 'Anti-Phish' + } +} diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA189.ps1 b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA189.ps1 new file mode 100644 index 000000000000..aa26117ba876 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA189.ps1 @@ -0,0 +1,43 @@ +function Invoke-CippTestORCA189 { + <# + .SYNOPSIS + Safe Attachments is not bypassed + #> + param($Tenant) + + try { + $Rules = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoTransportRules' + + if (-not $Rules) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA189' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'Safe Attachments is not bypassed' -UserImpact 'High' -ImplementationEffort 'Low' -Category 'Safe Attachments' + return + } + + $BypassRules = [System.Collections.Generic.List[object]]::new() + foreach ($Rule in $Rules) { + if ($Rule.SetHeaderName -eq 'X-MS-Exchange-Organization-SkipSafeAttachmentProcessing' -and $Rule.SetHeaderValue -eq '1') { + $BypassRules.Add($Rule) | Out-Null + } + } + + if ($BypassRules.Count -eq 0) { + $Status = 'Passed' + $Result = "No transport rules are bypassing Safe Attachments processing." + } else { + $Status = 'Failed' + $Result = "$($BypassRules.Count) transport rules are bypassing Safe Attachments processing.`n`n" + $Result += "| Rule Name | Priority |`n" + $Result += "|-----------|----------|`n" + foreach ($Rule in $BypassRules) { + $Result += "| $($Rule.Name) | $($Rule.Priority) |`n" + } + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA189' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'Safe Attachments is not bypassed' -UserImpact 'High' -ImplementationEffort 'Low' -Category 'Safe Attachments' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA189' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Safe Attachments is not bypassed' -UserImpact 'High' -ImplementationEffort 'Low' -Category 'Safe Attachments' + } +} diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA189_2.ps1 b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA189_2.ps1 new file mode 100644 index 000000000000..62ee60914a2a --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA189_2.ps1 @@ -0,0 +1,43 @@ +function Invoke-CippTestORCA189_2 { + <# + .SYNOPSIS + Safe Links is not bypassed + #> + param($Tenant) + + try { + $Rules = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoTransportRules' + + if (-not $Rules) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA189_2' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'Safe Links is not bypassed' -UserImpact 'High' -ImplementationEffort 'Low' -Category 'Safe Links' + return + } + + $BypassRules = [System.Collections.Generic.List[object]]::new() + foreach ($Rule in $Rules) { + if ($Rule.SetHeaderName -eq 'X-MS-Exchange-Organization-SkipSafeLinksProcessing' -and $Rule.SetHeaderValue -eq '1') { + $BypassRules.Add($Rule) | Out-Null + } + } + + if ($BypassRules.Count -eq 0) { + $Status = 'Passed' + $Result = "No transport rules are bypassing Safe Links processing." + } else { + $Status = 'Failed' + $Result = "$($BypassRules.Count) transport rules are bypassing Safe Links processing.`n`n" + $Result += "| Rule Name | Priority |`n" + $Result += "|-----------|----------|`n" + foreach ($Rule in $BypassRules) { + $Result += "| $($Rule.Name) | $($Rule.Priority) |`n" + } + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA189_2' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'Safe Links is not bypassed' -UserImpact 'High' -ImplementationEffort 'Low' -Category 'Safe Links' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA189_2' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Safe Links is not bypassed' -UserImpact 'High' -ImplementationEffort 'Low' -Category 'Safe Links' + } +} diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA205.ps1 b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA205.ps1 new file mode 100644 index 000000000000..51ecc2c16d88 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA205.ps1 @@ -0,0 +1,49 @@ +function Invoke-CippTestORCA205 { + <# + .SYNOPSIS + Common attachment type filter is enabled + #> + param($Tenant) + + try { + $Policies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoMalwareFilterPolicies' + + if (-not $Policies) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA205' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'Common attachment type filter is enabled' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Malware' + return + } + + $FailedPolicies = [System.Collections.Generic.List[object]]::new() + $PassedPolicies = [System.Collections.Generic.List[object]]::new() + + foreach ($Policy in $Policies) { + if ($Policy.EnableFileFilter -eq $true) { + $PassedPolicies.Add($Policy) | Out-Null + } else { + $FailedPolicies.Add($Policy) | Out-Null + } + } + + if ($FailedPolicies.Count -eq 0) { + $Status = 'Passed' + $Result = "All malware filter policies have common attachment type filter enabled.`n`n" + $Result += "**Compliant Policies:** $($PassedPolicies.Count)" + } else { + $Status = 'Failed' + $Result = "$($FailedPolicies.Count) malware filter policies do not have common attachment type filter enabled.`n`n" + $Result += "**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n" + $Result += "| Policy Name | Enable File Filter |`n" + $Result += "|------------|-------------------|`n" + foreach ($Policy in $FailedPolicies) { + $Result += "| $($Policy.Identity) | $($Policy.EnableFileFilter) |`n" + } + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA205' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'Common attachment type filter is enabled' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Malware' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA205' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'Common attachment type filter is enabled' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Malware' + } +} diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA220.ps1 b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA220.ps1 new file mode 100644 index 000000000000..d266a8461cd1 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA220.ps1 @@ -0,0 +1,50 @@ +function Invoke-CippTestORCA220 { + <# + .SYNOPSIS + Advanced Phish filter Threshold level is adequate + #> + param($Tenant) + + try { + $Policies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoAntiPhishPolicies' + + if (-not $Policies) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA220' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'Advanced Phish filter Threshold level is adequate' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Anti-Phish' + return + } + + $FailedPolicies = [System.Collections.Generic.List[object]]::new() + $PassedPolicies = [System.Collections.Generic.List[object]]::new() + + foreach ($Policy in $Policies) { + # PhishThresholdLevel: 1=Standard, 2=Aggressive, 3=More Aggressive, 4=Most Aggressive + if ($Policy.PhishThresholdLevel -ge 2) { + $PassedPolicies.Add($Policy) | Out-Null + } else { + $FailedPolicies.Add($Policy) | Out-Null + } + } + + if ($FailedPolicies.Count -eq 0) { + $Status = 'Passed' + $Result = "All anti-phishing policies have adequate phishing threshold levels (2 or higher).`n`n" + $Result += "**Compliant Policies:** $($PassedPolicies.Count)" + } else { + $Status = 'Failed' + $Result = "$($FailedPolicies.Count) anti-phishing policies have inadequate phishing threshold levels.`n`n" + $Result += "**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n" + $Result += "| Policy Name | Phish Threshold Level |`n" + $Result += "|------------|----------------------|`n" + foreach ($Policy in $FailedPolicies) { + $Result += "| $($Policy.Identity) | $($Policy.PhishThresholdLevel) |`n" + } + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA220' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'Advanced Phish filter Threshold level is adequate' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Anti-Phish' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA220' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'Advanced Phish filter Threshold level is adequate' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Anti-Phish' + } +} diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA221.ps1 b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA221.ps1 new file mode 100644 index 000000000000..f76784f3680d --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA221.ps1 @@ -0,0 +1,49 @@ +function Invoke-CippTestORCA221 { + <# + .SYNOPSIS + Mailbox intelligence is enabled in anti-phishing policies + #> + param($Tenant) + + try { + $Policies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoAntiPhishPolicies' + + if (-not $Policies) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA221' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'Mailbox intelligence is enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Anti-Phish' + return + } + + $FailedPolicies = [System.Collections.Generic.List[object]]::new() + $PassedPolicies = [System.Collections.Generic.List[object]]::new() + + foreach ($Policy in $Policies) { + if ($Policy.EnableMailboxIntelligence -eq $true) { + $PassedPolicies.Add($Policy) | Out-Null + } else { + $FailedPolicies.Add($Policy) | Out-Null + } + } + + if ($FailedPolicies.Count -eq 0) { + $Status = 'Passed' + $Result = "All anti-phishing policies have mailbox intelligence enabled.`n`n" + $Result += "**Compliant Policies:** $($PassedPolicies.Count)" + } else { + $Status = 'Failed' + $Result = "$($FailedPolicies.Count) anti-phishing policies do not have mailbox intelligence enabled.`n`n" + $Result += "**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n" + $Result += "| Policy Name | Enable Mailbox Intelligence |`n" + $Result += "|------------|----------------------------|`n" + foreach ($Policy in $FailedPolicies) { + $Result += "| $($Policy.Identity) | $($Policy.EnableMailboxIntelligence) |`n" + } + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA221' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'Mailbox intelligence is enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Anti-Phish' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA221' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'Mailbox intelligence is enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Anti-Phish' + } +} diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA222.ps1 b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA222.ps1 new file mode 100644 index 000000000000..14d2ccc5e552 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA222.ps1 @@ -0,0 +1,49 @@ +function Invoke-CippTestORCA222 { + <# + .SYNOPSIS + Domain Impersonation action is set to move to Quarantine + #> + param($Tenant) + + try { + $Policies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoAntiPhishPolicies' + + if (-not $Policies) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA222' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'Domain Impersonation action set to Quarantine' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Anti-Phish' + return + } + + $FailedPolicies = [System.Collections.Generic.List[object]]::new() + $PassedPolicies = [System.Collections.Generic.List[object]]::new() + + foreach ($Policy in $Policies) { + if ($Policy.TargetedDomainProtectionAction -eq 'Quarantine') { + $PassedPolicies.Add($Policy) | Out-Null + } else { + $FailedPolicies.Add($Policy) | Out-Null + } + } + + if ($FailedPolicies.Count -eq 0) { + $Status = 'Passed' + $Result = "All anti-phishing policies have Domain Impersonation action set to Quarantine.`n`n" + $Result += "**Compliant Policies:** $($PassedPolicies.Count)" + } else { + $Status = 'Failed' + $Result = "$($FailedPolicies.Count) anti-phishing policies do not have Domain Impersonation action set to Quarantine.`n`n" + $Result += "**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n" + $Result += "| Policy Name | Targeted Domain Protection Action |`n" + $Result += "|------------|----------------------------------|`n" + foreach ($Policy in $FailedPolicies) { + $Result += "| $($Policy.Identity) | $($Policy.TargetedDomainProtectionAction) |`n" + } + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA222' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'Domain Impersonation action set to Quarantine' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Anti-Phish' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA222' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Domain Impersonation action set to Quarantine' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Anti-Phish' + } +} diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA223.ps1 b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA223.ps1 new file mode 100644 index 000000000000..3600edeb0723 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA223.ps1 @@ -0,0 +1,49 @@ +function Invoke-CippTestORCA223 { + <# + .SYNOPSIS + User impersonation action is set to move to Quarantine + #> + param($Tenant) + + try { + $Policies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoAntiPhishPolicies' + + if (-not $Policies) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA223' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'User impersonation action set to Quarantine' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Anti-Phish' + return + } + + $FailedPolicies = [System.Collections.Generic.List[object]]::new() + $PassedPolicies = [System.Collections.Generic.List[object]]::new() + + foreach ($Policy in $Policies) { + if ($Policy.TargetedUserProtectionAction -eq 'Quarantine') { + $PassedPolicies.Add($Policy) | Out-Null + } else { + $FailedPolicies.Add($Policy) | Out-Null + } + } + + if ($FailedPolicies.Count -eq 0) { + $Status = 'Passed' + $Result = "All anti-phishing policies have User Impersonation action set to Quarantine.`n`n" + $Result += "**Compliant Policies:** $($PassedPolicies.Count)" + } else { + $Status = 'Failed' + $Result = "$($FailedPolicies.Count) anti-phishing policies do not have User Impersonation action set to Quarantine.`n`n" + $Result += "**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n" + $Result += "| Policy Name | Targeted User Protection Action |`n" + $Result += "|------------|--------------------------------|`n" + foreach ($Policy in $FailedPolicies) { + $Result += "| $($Policy.Identity) | $($Policy.TargetedUserProtectionAction) |`n" + } + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA223' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'User impersonation action set to Quarantine' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Anti-Phish' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA223' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'User impersonation action set to Quarantine' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Anti-Phish' + } +} diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA224.ps1 b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA224.ps1 new file mode 100644 index 000000000000..77e84a0a96cf --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA224.ps1 @@ -0,0 +1,49 @@ +function Invoke-CippTestORCA224 { + <# + .SYNOPSIS + Similar Users Safety Tips is enabled + #> + param($Tenant) + + try { + $Policies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoAntiPhishPolicies' + + if (-not $Policies) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA224' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'Similar Users Safety Tips is enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Anti-Phish' + return + } + + $FailedPolicies = [System.Collections.Generic.List[object]]::new() + $PassedPolicies = [System.Collections.Generic.List[object]]::new() + + foreach ($Policy in $Policies) { + if ($Policy.EnableSimilarUsersSafetyTips -eq $true) { + $PassedPolicies.Add($Policy) | Out-Null + } else { + $FailedPolicies.Add($Policy) | Out-Null + } + } + + if ($FailedPolicies.Count -eq 0) { + $Status = 'Passed' + $Result = "All anti-phishing policies have Similar Users Safety Tips enabled.`n`n" + $Result += "**Compliant Policies:** $($PassedPolicies.Count)" + } else { + $Status = 'Failed' + $Result = "$($FailedPolicies.Count) anti-phishing policies do not have Similar Users Safety Tips enabled.`n`n" + $Result += "**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n" + $Result += "| Policy Name | Enable Similar Users Safety Tips |`n" + $Result += "|------------|----------------------------------|`n" + foreach ($Policy in $FailedPolicies) { + $Result += "| $($Policy.Identity) | $($Policy.EnableSimilarUsersSafetyTips) |`n" + } + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA224' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'Similar Users Safety Tips is enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Anti-Phish' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA224' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'Similar Users Safety Tips is enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Anti-Phish' + } +} diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA225.ps1 b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA225.ps1 new file mode 100644 index 000000000000..9c0fc333de87 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA225.ps1 @@ -0,0 +1,35 @@ +function Invoke-CippTestORCA225 { + <# + .SYNOPSIS + Safe Documents is enabled for Office clients + #> + param($Tenant) + + try { + $AtpPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoAtpPolicyForO365' + + if (-not $AtpPolicy) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA225' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'Safe Documents is enabled for Office clients' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Safe Attachments' + return + } + + $Policy = $AtpPolicy | Select-Object -First 1 + + if ($Policy.EnableSafeDocs -eq $true) { + $Status = 'Passed' + $Result = "Safe Documents is enabled for Office clients.`n`n" + $Result += "**EnableSafeDocs:** $($Policy.EnableSafeDocs)" + } else { + $Status = 'Failed' + $Result = "Safe Documents is NOT enabled for Office clients.`n`n" + $Result += "**EnableSafeDocs:** $($Policy.EnableSafeDocs)" + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA225' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'Safe Documents is enabled for Office clients' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Safe Attachments' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA225' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'Safe Documents is enabled for Office clients' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Safe Attachments' + } +} diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA226.ps1 b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA226.ps1 new file mode 100644 index 000000000000..81c895569cb6 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA226.ps1 @@ -0,0 +1,60 @@ +function Invoke-CippTestORCA226 { + <# + .SYNOPSIS + Each domain has a Safe Links policy + #> + param($Tenant) + + try { + $AcceptedDomains = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoAcceptedDomains' + $SafeLinksPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoSafeLinksPolicies' + + if (-not $AcceptedDomains) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA226' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No accepted domains found in database.' -Risk 'High' -Name 'Each domain has a Safe Links policy' -UserImpact 'High' -ImplementationEffort 'Medium' -Category 'Safe Links' + return + } + + if (-not $SafeLinksPolicies) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA226' -TestType 'Identity' -Status 'Failed' -ResultMarkdown 'No Safe Links policies found. Each domain should have a Safe Links policy.' -Risk 'High' -Name 'Each domain has a Safe Links policy' -UserImpact 'High' -ImplementationEffort 'Medium' -Category 'Safe Links' + return + } + + # Get all recipient domains from policies + $CoveredDomains = [System.Collections.Generic.List[string]]::new() + foreach ($Policy in $SafeLinksPolicies) { + if ($Policy.RecipientDomainIs) { + foreach ($Domain in $Policy.RecipientDomainIs) { + $CoveredDomains.Add($Domain) | Out-Null + } + } + } + + $DomainsWithoutPolicy = [System.Collections.Generic.List[string]]::new() + foreach ($Domain in $AcceptedDomains) { + if ($CoveredDomains -notcontains $Domain.DomainName) { + $DomainsWithoutPolicy.Add($Domain.DomainName) | Out-Null + } + } + + if ($DomainsWithoutPolicy.Count -eq 0) { + $Status = 'Passed' + $Result = "All accepted domains are covered by Safe Links policies.`n`n" + $Result += "**Total Accepted Domains:** $($AcceptedDomains.Count)`n" + $Result += "**Total Safe Links Policies:** $($SafeLinksPolicies.Count)" + } else { + $Status = 'Failed' + $Result = "$($DomainsWithoutPolicy.Count) domains do not have a Safe Links policy.`n`n" + $Result += "**Domains Without Policy:**`n`n" + foreach ($Domain in $DomainsWithoutPolicy) { + $Result += "- $Domain`n" + } + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA226' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'Each domain has a Safe Links policy' -UserImpact 'High' -ImplementationEffort 'Medium' -Category 'Safe Links' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA226' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Each domain has a Safe Links policy' -UserImpact 'High' -ImplementationEffort 'Medium' -Category 'Safe Links' + } +} diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA227.ps1 b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA227.ps1 new file mode 100644 index 000000000000..deb01c9eda53 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA227.ps1 @@ -0,0 +1,60 @@ +function Invoke-CippTestORCA227 { + <# + .SYNOPSIS + Each domain has a Safe Attachments policy + #> + param($Tenant) + + try { + $AcceptedDomains = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoAcceptedDomains' + $SafeAttachmentPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoSafeAttachmentPolicies' + + if (-not $AcceptedDomains) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA227' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No accepted domains found in database.' -Risk 'High' -Name 'Each domain has a Safe Attachments policy' -UserImpact 'High' -ImplementationEffort 'Medium' -Category 'Safe Attachments' + return + } + + if (-not $SafeAttachmentPolicies) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA227' -TestType 'Identity' -Status 'Failed' -ResultMarkdown 'No Safe Attachments policies found. Each domain should have a Safe Attachments policy.' -Risk 'High' -Name 'Each domain has a Safe Attachments policy' -UserImpact 'High' -ImplementationEffort 'Medium' -Category 'Safe Attachments' + return + } + + # Get all recipient domains from policies + $CoveredDomains = [System.Collections.Generic.List[string]]::new() + foreach ($Policy in $SafeAttachmentPolicies) { + if ($Policy.RecipientDomainIs) { + foreach ($Domain in $Policy.RecipientDomainIs) { + $CoveredDomains.Add($Domain) | Out-Null + } + } + } + + $DomainsWithoutPolicy = [System.Collections.Generic.List[string]]::new() + foreach ($Domain in $AcceptedDomains) { + if ($CoveredDomains -notcontains $Domain.DomainName) { + $DomainsWithoutPolicy.Add($Domain.DomainName) | Out-Null + } + } + + if ($DomainsWithoutPolicy.Count -eq 0) { + $Status = 'Passed' + $Result = "All accepted domains are covered by Safe Attachments policies.`n`n" + $Result += "**Total Accepted Domains:** $($AcceptedDomains.Count)`n" + $Result += "**Total Safe Attachments Policies:** $($SafeAttachmentPolicies.Count)" + } else { + $Status = 'Failed' + $Result = "$($DomainsWithoutPolicy.Count) domains do not have a Safe Attachments policy.`n`n" + $Result += "**Domains Without Policy:**`n`n" + foreach ($Domain in $DomainsWithoutPolicy) { + $Result += "- $Domain`n" + } + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA227' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'Each domain has a Safe Attachments policy' -UserImpact 'High' -ImplementationEffort 'Medium' -Category 'Safe Attachments' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA227' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Each domain has a Safe Attachments policy' -UserImpact 'High' -ImplementationEffort 'Medium' -Category 'Safe Attachments' + } +} diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA228.ps1 b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA228.ps1 new file mode 100644 index 000000000000..8a25941105f8 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA228.ps1 @@ -0,0 +1,52 @@ +function Invoke-CippTestORCA228 { + <# + .SYNOPSIS + No trusted senders in Anti-phishing policy + #> + param($Tenant) + + try { + $Policies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoAntiPhishPolicies' + + if (-not $Policies) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA228' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'No trusted senders in Anti-phishing policy' -UserImpact 'High' -ImplementationEffort 'Low' -Category 'Anti-Phish' + return + } + + $FailedPolicies = [System.Collections.Generic.List[object]]::new() + $PassedPolicies = [System.Collections.Generic.List[object]]::new() + + foreach ($Policy in $Policies) { + $HasTrustedSenders = ($Policy.ExcludedSenders -and $Policy.ExcludedSenders.Count -gt 0) + + if (-not $HasTrustedSenders) { + $PassedPolicies.Add($Policy) | Out-Null + } else { + $FailedPolicies.Add($Policy) | Out-Null + } + } + + if ($FailedPolicies.Count -eq 0) { + $Status = 'Passed' + $Result = "No anti-phishing policies have trusted senders configured.`n`n" + $Result += "**Compliant Policies:** $($PassedPolicies.Count)" + } else { + $Status = 'Failed' + $Result = "$($FailedPolicies.Count) anti-phishing policies have trusted senders configured.`n`n" + $Result += "**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n" + $Result += "| Policy Name | Excluded Senders Count |`n" + $Result += "|------------|----------------------|`n" + foreach ($Policy in $FailedPolicies) { + $Count = if ($Policy.ExcludedSenders) { $Policy.ExcludedSenders.Count } else { 0 } + $Result += "| $($Policy.Identity) | $Count |`n" + } + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA228' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'No trusted senders in Anti-phishing policy' -UserImpact 'High' -ImplementationEffort 'Low' -Category 'Anti-Phish' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA228' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'No trusted senders in Anti-phishing policy' -UserImpact 'High' -ImplementationEffort 'Low' -Category 'Anti-Phish' + } +} diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA229.ps1 b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA229.ps1 new file mode 100644 index 000000000000..c9c8ba9fa1cd --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA229.ps1 @@ -0,0 +1,52 @@ +function Invoke-CippTestORCA229 { + <# + .SYNOPSIS + No trusted domains in Anti-phishing policy + #> + param($Tenant) + + try { + $Policies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoAntiPhishPolicies' + + if (-not $Policies) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA229' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'No trusted domains in Anti-phishing policy' -UserImpact 'High' -ImplementationEffort 'Low' -Category 'Anti-Phish' + return + } + + $FailedPolicies = [System.Collections.Generic.List[object]]::new() + $PassedPolicies = [System.Collections.Generic.List[object]]::new() + + foreach ($Policy in $Policies) { + $HasTrustedDomains = ($Policy.ExcludedDomains -and $Policy.ExcludedDomains.Count -gt 0) + + if (-not $HasTrustedDomains) { + $PassedPolicies.Add($Policy) | Out-Null + } else { + $FailedPolicies.Add($Policy) | Out-Null + } + } + + if ($FailedPolicies.Count -eq 0) { + $Status = 'Passed' + $Result = "No anti-phishing policies have trusted domains configured.`n`n" + $Result += "**Compliant Policies:** $($PassedPolicies.Count)" + } else { + $Status = 'Failed' + $Result = "$($FailedPolicies.Count) anti-phishing policies have trusted domains configured.`n`n" + $Result += "**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n" + $Result += "| Policy Name | Excluded Domains Count |`n" + $Result += "|------------|----------------------|`n" + foreach ($Policy in $FailedPolicies) { + $Count = if ($Policy.ExcludedDomains) { $Policy.ExcludedDomains.Count } else { 0 } + $Result += "| $($Policy.Identity) | $Count |`n" + } + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA229' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'No trusted domains in Anti-phishing policy' -UserImpact 'High' -ImplementationEffort 'Low' -Category 'Anti-Phish' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA229' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'No trusted domains in Anti-phishing policy' -UserImpact 'High' -ImplementationEffort 'Low' -Category 'Anti-Phish' + } +} diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA230.ps1 b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA230.ps1 new file mode 100644 index 000000000000..8d72b0ffaccb --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA230.ps1 @@ -0,0 +1,60 @@ +function Invoke-CippTestORCA230 { + <# + .SYNOPSIS + Each domain has an Anti-phishing policy + #> + param($Tenant) + + try { + $AcceptedDomains = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoAcceptedDomains' + $AntiPhishPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoAntiPhishPolicies' + + if (-not $AcceptedDomains) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA230' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No accepted domains found in database.' -Risk 'High' -Name 'Each domain has an Anti-phishing policy' -UserImpact 'High' -ImplementationEffort 'Medium' -Category 'Anti-Phish' + return + } + + if (-not $AntiPhishPolicies) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA230' -TestType 'Identity' -Status 'Failed' -ResultMarkdown 'No Anti-phishing policies found. Each domain should have an Anti-phishing policy.' -Risk 'High' -Name 'Each domain has an Anti-phishing policy' -UserImpact 'High' -ImplementationEffort 'Medium' -Category 'Anti-Phish' + return + } + + # Get all recipient domains from policies + $CoveredDomains = [System.Collections.Generic.List[string]]::new() + foreach ($Policy in $AntiPhishPolicies) { + if ($Policy.RecipientDomainIs) { + foreach ($Domain in $Policy.RecipientDomainIs) { + $CoveredDomains.Add($Domain) | Out-Null + } + } + } + + $DomainsWithoutPolicy = [System.Collections.Generic.List[string]]::new() + foreach ($Domain in $AcceptedDomains) { + if ($CoveredDomains -notcontains $Domain.DomainName) { + $DomainsWithoutPolicy.Add($Domain.DomainName) | Out-Null + } + } + + if ($DomainsWithoutPolicy.Count -eq 0) { + $Status = 'Passed' + $Result = "All accepted domains are covered by Anti-phishing policies.`n`n" + $Result += "**Total Accepted Domains:** $($AcceptedDomains.Count)`n" + $Result += "**Total Anti-phishing Policies:** $($AntiPhishPolicies.Count)" + } else { + $Status = 'Failed' + $Result = "$($DomainsWithoutPolicy.Count) domains do not have an Anti-phishing policy.`n`n" + $Result += "**Domains Without Policy:**`n`n" + foreach ($Domain in $DomainsWithoutPolicy) { + $Result += "- $Domain`n" + } + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA230' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'Each domain has an Anti-phishing policy' -UserImpact 'High' -ImplementationEffort 'Medium' -Category 'Anti-Phish' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA230' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Each domain has an Anti-phishing policy' -UserImpact 'High' -ImplementationEffort 'Medium' -Category 'Anti-Phish' + } +} diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA231.ps1 b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA231.ps1 new file mode 100644 index 000000000000..2dd1c57ef583 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA231.ps1 @@ -0,0 +1,60 @@ +function Invoke-CippTestORCA231 { + <# + .SYNOPSIS + Each domain has an anti-spam policy + #> + param($Tenant) + + try { + $AcceptedDomains = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoAcceptedDomains' + $ContentFilterPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoHostedContentFilterPolicy' + + if (-not $AcceptedDomains) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA231' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No accepted domains found in database.' -Risk 'High' -Name 'Each domain has an anti-spam policy' -UserImpact 'High' -ImplementationEffort 'Medium' -Category 'Anti-Spam' + return + } + + if (-not $ContentFilterPolicies) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA231' -TestType 'Identity' -Status 'Failed' -ResultMarkdown 'No anti-spam policies found. Each domain should have an anti-spam policy.' -Risk 'High' -Name 'Each domain has an anti-spam policy' -UserImpact 'High' -ImplementationEffort 'Medium' -Category 'Anti-Spam' + return + } + + # Get all recipient domains from policies + $CoveredDomains = [System.Collections.Generic.List[string]]::new() + foreach ($Policy in $ContentFilterPolicies) { + if ($Policy.RecipientDomainIs) { + foreach ($Domain in $Policy.RecipientDomainIs) { + $CoveredDomains.Add($Domain) | Out-Null + } + } + } + + $DomainsWithoutPolicy = [System.Collections.Generic.List[string]]::new() + foreach ($Domain in $AcceptedDomains) { + if ($CoveredDomains -notcontains $Domain.DomainName) { + $DomainsWithoutPolicy.Add($Domain.DomainName) | Out-Null + } + } + + if ($DomainsWithoutPolicy.Count -eq 0) { + $Status = 'Passed' + $Result = "All accepted domains are covered by anti-spam policies.`n`n" + $Result += "**Total Accepted Domains:** $($AcceptedDomains.Count)`n" + $Result += "**Total Anti-spam Policies:** $($ContentFilterPolicies.Count)" + } else { + $Status = 'Failed' + $Result = "$($DomainsWithoutPolicy.Count) domains do not have an anti-spam policy.`n`n" + $Result += "**Domains Without Policy:**`n`n" + foreach ($Domain in $DomainsWithoutPolicy) { + $Result += "- $Domain`n" + } + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA231' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'Each domain has an anti-spam policy' -UserImpact 'High' -ImplementationEffort 'Medium' -Category 'Anti-Spam' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA231' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Each domain has an anti-spam policy' -UserImpact 'High' -ImplementationEffort 'Medium' -Category 'Anti-Spam' + } +} diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA232.ps1 b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA232.ps1 new file mode 100644 index 000000000000..f42577954039 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA232.ps1 @@ -0,0 +1,60 @@ +function Invoke-CippTestORCA232 { + <# + .SYNOPSIS + Each domain has a malware filter policy + #> + param($Tenant) + + try { + $AcceptedDomains = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoAcceptedDomains' + $MalwarePolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoMalwareFilterPolicies' + + if (-not $AcceptedDomains) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA232' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No accepted domains found in database.' -Risk 'High' -Name 'Each domain has a malware filter policy' -UserImpact 'High' -ImplementationEffort 'Medium' -Category 'Malware' + return + } + + if (-not $MalwarePolicies) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA232' -TestType 'Identity' -Status 'Failed' -ResultMarkdown 'No malware filter policies found. Each domain should have a malware filter policy.' -Risk 'High' -Name 'Each domain has a malware filter policy' -UserImpact 'High' -ImplementationEffort 'Medium' -Category 'Malware' + return + } + + # Get all recipient domains from policies + $CoveredDomains = [System.Collections.Generic.List[string]]::new() + foreach ($Policy in $MalwarePolicies) { + if ($Policy.RecipientDomainIs) { + foreach ($Domain in $Policy.RecipientDomainIs) { + $CoveredDomains.Add($Domain) | Out-Null + } + } + } + + $DomainsWithoutPolicy = [System.Collections.Generic.List[string]]::new() + foreach ($Domain in $AcceptedDomains) { + if ($CoveredDomains -notcontains $Domain.DomainName) { + $DomainsWithoutPolicy.Add($Domain.DomainName) | Out-Null + } + } + + if ($DomainsWithoutPolicy.Count -eq 0) { + $Status = 'Passed' + $Result = "All accepted domains are covered by malware filter policies.`n`n" + $Result += "**Total Accepted Domains:** $($AcceptedDomains.Count)`n" + $Result += "**Total Malware Filter Policies:** $($MalwarePolicies.Count)" + } else { + $Status = 'Failed' + $Result = "$($DomainsWithoutPolicy.Count) domains do not have a malware filter policy.`n`n" + $Result += "**Domains Without Policy:**`n`n" + foreach ($Domain in $DomainsWithoutPolicy) { + $Result += "- $Domain`n" + } + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA232' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'Each domain has a malware filter policy' -UserImpact 'High' -ImplementationEffort 'Medium' -Category 'Malware' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA232' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Each domain has a malware filter policy' -UserImpact 'High' -ImplementationEffort 'Medium' -Category 'Malware' + } +} diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA233.ps1 b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA233.ps1 new file mode 100644 index 000000000000..71815b0b0e78 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA233.ps1 @@ -0,0 +1,55 @@ +function Invoke-CippTestORCA233 { + <# + .SYNOPSIS + Domains pointed at EOP or enhanced filtering used + #> + param($Tenant) + + try { + $AcceptedDomains = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoAcceptedDomains' + + if (-not $AcceptedDomains) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA233' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No accepted domains found in database.' -Risk 'High' -Name 'Domains pointed at EOP or enhanced filtering used' -UserImpact 'High' -ImplementationEffort 'High' -Category 'Configuration' + return + } + + # This test requires checking MX records and inbound connectors which may not be available + # We'll check if domains are authoritative (pointed at EOP) or use external mail flow + $NonCompliantDomains = [System.Collections.Generic.List[string]]::new() + $CompliantDomains = [System.Collections.Generic.List[string]]::new() + + foreach ($Domain in $AcceptedDomains) { + # Authoritative domains point MX to EOP + # InternalRelay/ExternalRelay domains use inbound connectors with enhanced filtering + if ($Domain.DomainType -eq 'Authoritative') { + $CompliantDomains.Add($Domain.DomainName) | Out-Null + } elseif ($Domain.DomainType -in @('InternalRelay', 'ExternalRelay')) { + # These should have enhanced filtering configured on inbound connectors + # For now, we'll mark these as compliant if they exist + $CompliantDomains.Add($Domain.DomainName) | Out-Null + } else { + $NonCompliantDomains.Add($Domain.DomainName) | Out-Null + } + } + + if ($NonCompliantDomains.Count -eq 0) { + $Status = 'Passed' + $Result = "All domains are properly configured for mail flow.`n`n" + $Result += "**Compliant Domains:** $($CompliantDomains.Count)" + } else { + $Status = 'Failed' + $Result = "$($NonCompliantDomains.Count) domains may not be properly configured for mail flow.`n`n" + $Result += "**Domains Needing Review:**`n`n" + foreach ($Domain in $NonCompliantDomains) { + $Result += "- $Domain`n" + } + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA233' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'Domains pointed at EOP or enhanced filtering used' -UserImpact 'High' -ImplementationEffort 'High' -Category 'Configuration' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA233' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Domains pointed at EOP or enhanced filtering used' -UserImpact 'High' -ImplementationEffort 'High' -Category 'Configuration' + } +} diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA233_1.ps1 b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA233_1.ps1 new file mode 100644 index 000000000000..3243ff67a450 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA233_1.ps1 @@ -0,0 +1,44 @@ +function Invoke-CippTestORCA233_1 { + <# + .SYNOPSIS + Enhanced filtering on default connectors + #> + param($Tenant) + + try { + $OrgConfig = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoOrganizationConfig' + + if (-not $OrgConfig) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA233_1' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No organization config found in database.' -Risk 'Medium' -Name 'Enhanced filtering on default connectors' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Configuration' + return + } + + $Config = $OrgConfig | Select-Object -First 1 + + # Check if enhanced filtering is enabled + # This property may vary depending on Exchange Online version + $EnhancedFilteringEnabled = $false + + # Check various properties that indicate enhanced filtering + if ($Config.PSObject.Properties.Name -contains 'SkipListedFromForging') { + $EnhancedFilteringEnabled = $Config.SkipListedFromForging -eq $false + } + + if ($EnhancedFilteringEnabled) { + $Status = 'Passed' + $Result = "Enhanced filtering appears to be properly configured.`n`n" + $Result += "**Configuration:** Reviewed" + } else { + $Status = 'Informational' + $Result = "Unable to fully determine enhanced filtering status. Manual review recommended.`n`n" + $Result += "**Action Required:** Review inbound connectors for enhanced filtering configuration" + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA233_1' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'Enhanced filtering on default connectors' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Configuration' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA233_1' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'Enhanced filtering on default connectors' -UserImpact 'Medium' -ImplementationEffort 'Medium' -Category 'Configuration' + } +} diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA234.ps1 b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA234.ps1 new file mode 100644 index 000000000000..904c90d5f280 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA234.ps1 @@ -0,0 +1,35 @@ +function Invoke-CippTestORCA234 { + <# + .SYNOPSIS + Click through is disabled for Safe Documents + #> + param($Tenant) + + try { + $AtpPolicy = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoAtpPolicyForO365' + + if (-not $AtpPolicy) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA234' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'Click through is disabled for Safe Documents' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Safe Attachments' + return + } + + $Policy = $AtpPolicy | Select-Object -First 1 + + if ($Policy.AllowSafeDocsOpen -eq $false) { + $Status = 'Passed' + $Result = "Click through is disabled for Safe Documents.`n`n" + $Result += "**AllowSafeDocsOpen:** $($Policy.AllowSafeDocsOpen)" + } else { + $Status = 'Failed' + $Result = "Click through is enabled for Safe Documents.`n`n" + $Result += "**AllowSafeDocsOpen:** $($Policy.AllowSafeDocsOpen)" + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA234' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'Click through is disabled for Safe Documents' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Safe Attachments' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA234' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'Click through is disabled for Safe Documents' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Safe Attachments' + } +} diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA235.ps1 b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA235.ps1 new file mode 100644 index 000000000000..6d72efdf2bfe --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA235.ps1 @@ -0,0 +1,43 @@ +function Invoke-CippTestORCA235 { + <# + .SYNOPSIS + SPF records setup for custom domains + #> + param($Tenant) + + try { + $AcceptedDomains = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoAcceptedDomains' + + if (-not $AcceptedDomains) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA235' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No accepted domains found in database.' -Risk 'High' -Name 'SPF records setup for custom domains' -UserImpact 'High' -ImplementationEffort 'Medium' -Category 'Configuration' + return + } + + # Note: This test would ideally check DNS SPF records + # Since we don't have DNS query capability here, we'll provide informational guidance + + $CustomDomains = $AcceptedDomains | Where-Object { $_.DomainName -notlike '*.onmicrosoft.com' } + + if ($CustomDomains.Count -eq 0) { + $Status = 'Passed' + $Result = "No custom domains found. Only using onmicrosoft.com domain.`n`n" + $Result += "**Total Domains:** $($AcceptedDomains.Count)" + } else { + $Status = 'Informational' + $Result = "Found $($CustomDomains.Count) custom domains that should have SPF records configured.`n`n" + $Result += "**Custom Domains:**`n`n" + foreach ($Domain in $CustomDomains) { + $Result += "- $($Domain.DomainName)`n" + } + $Result += "`n**Action Required:** Verify that each custom domain has an SPF record including Microsoft 365:`n" + $Result += "``v=spf1 include:spf.protection.outlook.com -all``" + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA235' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'SPF records setup for custom domains' -UserImpact 'High' -ImplementationEffort 'Medium' -Category 'Configuration' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA235' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'SPF records setup for custom domains' -UserImpact 'High' -ImplementationEffort 'Medium' -Category 'Configuration' + } +} diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA236.ps1 b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA236.ps1 new file mode 100644 index 000000000000..89f67a40f68f --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA236.ps1 @@ -0,0 +1,49 @@ +function Invoke-CippTestORCA236 { + <# + .SYNOPSIS + Safe Links is enabled for emails + #> + param($Tenant) + + try { + $Policies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoSafeLinksPolicies' + + if (-not $Policies) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA236' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'Safe Links is enabled for emails' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Safe Links' + return + } + + $FailedPolicies = [System.Collections.Generic.List[object]]::new() + $PassedPolicies = [System.Collections.Generic.List[object]]::new() + + foreach ($Policy in $Policies) { + if ($Policy.EnableSafeLinksForEmail -eq $true) { + $PassedPolicies.Add($Policy) | Out-Null + } else { + $FailedPolicies.Add($Policy) | Out-Null + } + } + + if ($FailedPolicies.Count -eq 0) { + $Status = 'Passed' + $Result = "All Safe Links policies have email protection enabled.`n`n" + $Result += "**Compliant Policies:** $($PassedPolicies.Count)" + } else { + $Status = 'Failed' + $Result = "$($FailedPolicies.Count) Safe Links policies do not have email protection enabled.`n`n" + $Result += "**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n" + $Result += "| Policy Name | Enable Safe Links For Email |`n" + $Result += "|------------|----------------------------|`n" + foreach ($Policy in $FailedPolicies) { + $Result += "| $($Policy.Identity) | $($Policy.EnableSafeLinksForEmail) |`n" + } + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA236' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'Safe Links is enabled for emails' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Safe Links' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA236' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Safe Links is enabled for emails' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Safe Links' + } +} diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA237.ps1 b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA237.ps1 new file mode 100644 index 000000000000..5962ac11eb59 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA237.ps1 @@ -0,0 +1,49 @@ +function Invoke-CippTestORCA237 { + <# + .SYNOPSIS + Safe Links is enabled for Teams + #> + param($Tenant) + + try { + $Policies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoSafeLinksPolicies' + + if (-not $Policies) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA237' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'Safe Links is enabled for Teams' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Safe Links' + return + } + + $FailedPolicies = [System.Collections.Generic.List[object]]::new() + $PassedPolicies = [System.Collections.Generic.List[object]]::new() + + foreach ($Policy in $Policies) { + if ($Policy.EnableSafeLinksForTeams -eq $true) { + $PassedPolicies.Add($Policy) | Out-Null + } else { + $FailedPolicies.Add($Policy) | Out-Null + } + } + + if ($FailedPolicies.Count -eq 0) { + $Status = 'Passed' + $Result = "All Safe Links policies have Teams protection enabled.`n`n" + $Result += "**Compliant Policies:** $($PassedPolicies.Count)" + } else { + $Status = 'Failed' + $Result = "$($FailedPolicies.Count) Safe Links policies do not have Teams protection enabled.`n`n" + $Result += "**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n" + $Result += "| Policy Name | Enable Safe Links For Teams |`n" + $Result += "|------------|----------------------------|`n" + foreach ($Policy in $FailedPolicies) { + $Result += "| $($Policy.Identity) | $($Policy.EnableSafeLinksForTeams) |`n" + } + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA237' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'Safe Links is enabled for Teams' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Safe Links' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA237' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Safe Links is enabled for Teams' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Safe Links' + } +} diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA238.ps1 b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA238.ps1 new file mode 100644 index 000000000000..e4f3d35ac306 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA238.ps1 @@ -0,0 +1,49 @@ +function Invoke-CippTestORCA238 { + <# + .SYNOPSIS + Safe Links is enabled for Office documents + #> + param($Tenant) + + try { + $Policies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoSafeLinksPolicies' + + if (-not $Policies) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA238' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'Safe Links is enabled for Office documents' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Safe Links' + return + } + + $FailedPolicies = [System.Collections.Generic.List[object]]::new() + $PassedPolicies = [System.Collections.Generic.List[object]]::new() + + foreach ($Policy in $Policies) { + if ($Policy.EnableSafeLinksForOffice -eq $true) { + $PassedPolicies.Add($Policy) | Out-Null + } else { + $FailedPolicies.Add($Policy) | Out-Null + } + } + + if ($FailedPolicies.Count -eq 0) { + $Status = 'Passed' + $Result = "All Safe Links policies have Office document protection enabled.`n`n" + $Result += "**Compliant Policies:** $($PassedPolicies.Count)" + } else { + $Status = 'Failed' + $Result = "$($FailedPolicies.Count) Safe Links policies do not have Office document protection enabled.`n`n" + $Result += "**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n" + $Result += "| Policy Name | Enable Safe Links For Office |`n" + $Result += "|------------|------------------------------|`n" + foreach ($Policy in $FailedPolicies) { + $Result += "| $($Policy.Identity) | $($Policy.EnableSafeLinksForOffice) |`n" + } + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA238' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'Safe Links is enabled for Office documents' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Safe Links' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA238' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Safe Links is enabled for Office documents' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Safe Links' + } +} diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA239.ps1 b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA239.ps1 new file mode 100644 index 000000000000..24ccf3edcb1d --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA239.ps1 @@ -0,0 +1,83 @@ +function Invoke-CippTestORCA239 { + <# + .SYNOPSIS + No exclusions for built-in protection + #> + param($Tenant) + + try { + $AntiPhishPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoAntiPhishPolicies' + $ContentFilterPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoHostedContentFilterPolicy' + + if (-not $AntiPhishPolicies -and -not $ContentFilterPolicies) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA239' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No policies found in database.' -Risk 'High' -Name 'No exclusions for built-in protection' -UserImpact 'High' -ImplementationEffort 'Low' -Category 'Configuration' + return + } + + $FailedPolicies = @() + $Issues = @() + + # Check Anti-Phish policies for exclusions + if ($AntiPhishPolicies) { + foreach ($Policy in $AntiPhishPolicies) { + $HasExclusions = $false + $ExclusionDetails = @() + + if ($Policy.ExcludedSenders -and $Policy.ExcludedSenders.Count -gt 0) { + $HasExclusions = $true + $ExclusionDetails += "ExcludedSenders: $($Policy.ExcludedSenders.Count)" + } + + if ($Policy.ExcludedDomains -and $Policy.ExcludedDomains.Count -gt 0) { + $HasExclusions = $true + $ExclusionDetails += "ExcludedDomains: $($Policy.ExcludedDomains.Count)" + } + + if ($HasExclusions) { + $Issues += "Anti-Phish Policy '$($Policy.Identity)': $($ExclusionDetails -join ', ')" + } + } + } + + # Check Content Filter policies for exclusions + if ($ContentFilterPolicies) { + foreach ($Policy in $ContentFilterPolicies) { + $HasExclusions = $false + $ExclusionDetails = @() + + if ($Policy.AllowedSenders -and $Policy.AllowedSenders.Count -gt 0) { + $HasExclusions = $true + $ExclusionDetails += "AllowedSenders: $($Policy.AllowedSenders.Count)" + } + + if ($Policy.AllowedSenderDomains -and $Policy.AllowedSenderDomains.Count -gt 0) { + $HasExclusions = $true + $ExclusionDetails += "AllowedSenderDomains: $($Policy.AllowedSenderDomains.Count)" + } + + if ($HasExclusions) { + $Issues += "Anti-Spam Policy '$($Policy.Identity)': $($ExclusionDetails -join ', ')" + } + } + } + + if ($Issues.Count -eq 0) { + $Status = 'Passed' + $Result = "No exclusions found in built-in protection policies." + } else { + $Status = 'Failed' + $Result = "Found $($Issues.Count) policies with exclusions that bypass built-in protection.`n`n" + $Result += "**Issues Found:**`n`n" + foreach ($Issue in $Issues) { + $Result += "- $Issue`n" + } + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA239' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'No exclusions for built-in protection' -UserImpact 'High' -ImplementationEffort 'Low' -Category 'Configuration' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA239' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'No exclusions for built-in protection' -UserImpact 'High' -ImplementationEffort 'Low' -Category 'Configuration' + } +} diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA240.ps1 b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA240.ps1 new file mode 100644 index 000000000000..d283d3a0c45b --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA240.ps1 @@ -0,0 +1,35 @@ +function Invoke-CippTestORCA240 { + <# + .SYNOPSIS + Outlook external tags are configured + #> + param($Tenant) + + try { + $OrgConfig = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoOrganizationConfig' + + if (-not $OrgConfig) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA240' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'Outlook external tags are configured' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Configuration' + return + } + + $Config = $OrgConfig | Select-Object -First 1 + + if ($Config.ExternalInOutlook -ne 'Disabled') { + $Status = 'Passed' + $Result = "Outlook external tags are configured.`n`n" + $Result += "**ExternalInOutlook:** $($Config.ExternalInOutlook)" + } else { + $Status = 'Failed' + $Result = "Outlook external tags are NOT configured.`n`n" + $Result += "**ExternalInOutlook:** $($Config.ExternalInOutlook)" + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA240' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'Outlook external tags are configured' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Configuration' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA240' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'Outlook external tags are configured' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Configuration' + } +} diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA241.ps1 b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA241.ps1 new file mode 100644 index 000000000000..8e5b26129d3e --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA241.ps1 @@ -0,0 +1,49 @@ +function Invoke-CippTestORCA241 { + <# + .SYNOPSIS + First Contact Safety Tips is enabled + #> + param($Tenant) + + try { + $Policies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoAntiPhishPolicies' + + if (-not $Policies) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA241' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'First Contact Safety Tips is enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Anti-Phish' + return + } + + $FailedPolicies = [System.Collections.Generic.List[object]]::new() + $PassedPolicies = [System.Collections.Generic.List[object]]::new() + + foreach ($Policy in $Policies) { + if ($Policy.EnableFirstContactSafetyTips -eq $true) { + $PassedPolicies.Add($Policy) | Out-Null + } else { + $FailedPolicies.Add($Policy) | Out-Null + } + } + + if ($FailedPolicies.Count -eq 0) { + $Status = 'Passed' + $Result = "All anti-phishing policies have First Contact Safety Tips enabled.`n`n" + $Result += "**Compliant Policies:** $($PassedPolicies.Count)" + } else { + $Status = 'Failed' + $Result = "$($FailedPolicies.Count) anti-phishing policies do not have First Contact Safety Tips enabled.`n`n" + $Result += "**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n" + $Result += "| Policy Name | Enable First Contact Safety Tips |`n" + $Result += "|------------|----------------------------------|`n" + foreach ($Policy in $FailedPolicies) { + $Result += "| $($Policy.Identity) | $($Policy.EnableFirstContactSafetyTips) |`n" + } + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA241' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'First Contact Safety Tips is enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Anti-Phish' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA241' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'First Contact Safety Tips is enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Anti-Phish' + } +} diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA242.ps1 b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA242.ps1 new file mode 100644 index 000000000000..ba691ddf939e --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA242.ps1 @@ -0,0 +1,30 @@ +function Invoke-CippTestORCA242 { + <# + .SYNOPSIS + Important protection alerts enabled + #> + param($Tenant) + + try { + # This test would check for alert policies related to ATP/Defender for Office 365 + # Since we don't have an alert policy cache, we'll provide informational guidance + + $Status = 'Informational' + $Result = "Alert policies for protection features should be enabled and monitored.`n`n" + $Result += "**Recommended Alert Policies:**`n`n" + $Result += "- Messages reported by users as malware or phish`n" + $Result += "- Email sending limit exceeded`n" + $Result += "- Suspicious email forwarding activity`n" + $Result += "- Malware campaign detected`n" + $Result += "- Suspicious connector activity`n" + $Result += "- Unusual external user file activity`n" + $Result += "`n**Action Required:** Verify alert policies are configured in Microsoft 365 Security & Compliance Center" + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA242' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'Important protection alerts enabled' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Configuration' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA242' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'Important protection alerts enabled' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Configuration' + } +} diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA243.ps1 b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA243.ps1 new file mode 100644 index 000000000000..28a3717e845b --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA243.ps1 @@ -0,0 +1,40 @@ +function Invoke-CippTestORCA243 { + <# + .SYNOPSIS + Authenticated Receive Chain for non-EOP domains + #> + param($Tenant) + + try { + $AcceptedDomains = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoAcceptedDomains' + + if (-not $AcceptedDomains) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA243' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No accepted domains found in database.' -Risk 'Medium' -Name 'Authenticated Receive Chain for non-EOP domains' -UserImpact 'Medium' -ImplementationEffort 'High' -Category 'Configuration' + return + } + + # Check for non-authoritative domains that would need inbound connectors + $NonAuthDomains = $AcceptedDomains | Where-Object { $_.DomainType -in @('InternalRelay', 'ExternalRelay') } + + if ($NonAuthDomains.Count -eq 0) { + $Status = 'Passed' + $Result = "All domains are authoritative. No inbound connectors needed.`n`n" + $Result += "**Total Domains:** $($AcceptedDomains.Count)" + } else { + $Status = 'Informational' + $Result = "Found $($NonAuthDomains.Count) non-authoritative domains.`n`n" + $Result += "**Domains Requiring Inbound Connectors:**`n`n" + foreach ($Domain in $NonAuthDomains) { + $Result += "- $($Domain.DomainName) (Type: $($Domain.DomainType))`n" + } + $Result += "`n**Action Required:** Verify inbound connectors are configured with proper authentication for these domains" + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA243' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'Authenticated Receive Chain for non-EOP domains' -UserImpact 'Medium' -ImplementationEffort 'High' -Category 'Configuration' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA243' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'Authenticated Receive Chain for non-EOP domains' -UserImpact 'Medium' -ImplementationEffort 'High' -Category 'Configuration' + } +} diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA244.ps1 b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA244.ps1 new file mode 100644 index 000000000000..177731b925e7 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA244.ps1 @@ -0,0 +1,49 @@ +function Invoke-CippTestORCA244 { + <# + .SYNOPSIS + Policies honor sending domain DMARC + #> + param($Tenant) + + try { + $Policies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoHostedContentFilterPolicy' + + if (-not $Policies) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA244' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'Policies honor sending domain DMARC' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Anti-Spam' + return + } + + $FailedPolicies = [System.Collections.Generic.List[object]]::new() + $PassedPolicies = [System.Collections.Generic.List[object]]::new() + + foreach ($Policy in $Policies) { + if ($Policy.HonorDMARCPolicy -eq $true) { + $PassedPolicies.Add($Policy) | Out-Null + } else { + $FailedPolicies.Add($Policy) | Out-Null + } + } + + if ($FailedPolicies.Count -eq 0) { + $Status = 'Passed' + $Result = "All anti-spam policies honor sending domain DMARC.`n`n" + $Result += "**Compliant Policies:** $($PassedPolicies.Count)" + } else { + $Status = 'Failed' + $Result = "$($FailedPolicies.Count) anti-spam policies do not honor sending domain DMARC.`n`n" + $Result += "**Non-Compliant Policies:** $($FailedPolicies.Count)`n`n" + $Result += "| Policy Name | Honor DMARC Policy |`n" + $Result += "|------------|--------------------|`n" + foreach ($Policy in $FailedPolicies) { + $Result += "| $($Policy.Identity) | $($Policy.HonorDMARCPolicy) |`n" + } + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA244' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'Policies honor sending domain DMARC' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Anti-Spam' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA244' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'Policies honor sending domain DMARC' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Anti-Spam' + } +} From d28bdfb9b09775a6eda15ca0de92bc0a5da8545b Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Sun, 11 Jan 2026 16:32:50 +0100 Subject: [PATCH 109/503] reports --- Modules/CIPPCore/Public/Tests/EIDSCA/report.json | 2 +- Modules/CIPPCore/Public/Tests/ORCA/report.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/report.json b/Modules/CIPPCore/Public/Tests/EIDSCA/report.json index 2322f3f9597a..0db980ceada1 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/report.json +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/report.json @@ -1,5 +1,5 @@ { - "name": "Entra ID Security Configuration Analyzer (EIDSCA) Tests", + "name": "EIDSCA (Entra ID Security Configuration Analyzer) Tests", "description": "Comprehensive security assessment for Microsoft Entra ID (formerly Azure AD) covering authorization policies, authentication methods, consent policies, password policies, and group settings. Based on Microsoft's EIDSCA framework for identity security best practices.", "version": "1.0", "source": "https://github.com/maester365/maester", diff --git a/Modules/CIPPCore/Public/Tests/ORCA/report.json b/Modules/CIPPCore/Public/Tests/ORCA/report.json index 2b223f8e5e69..cb19336da0f5 100644 --- a/Modules/CIPPCore/Public/Tests/ORCA/report.json +++ b/Modules/CIPPCore/Public/Tests/ORCA/report.json @@ -1,5 +1,5 @@ { - "name": "Office 365 Recommended Configuration Analyzer (ORCA) Tests", + "name": "ORCA (Office 365 Recommended Configuration Analyzer) Tests", "description": "Comprehensive security assessment for Microsoft Exchange Online and Office 365 security configurations. Tests cover anti-spam, anti-phish, anti-malware, safe links, safe attachments, DKIM, transport rules, and other Exchange Online security settings.", "version": "1.0", "source": "https://github.com/maester365/maester", From c0163b3cc7e863bf9c9b2cf4992d73d4d46fa27d Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Sun, 11 Jan 2026 17:14:55 +0100 Subject: [PATCH 110/503] fixes 5171 --- .../Users/Invoke-ExecBECRemediate.ps1 | 92 +++++++++---------- .../CIPPCore/Public/Set-CIPPMailboxRule.ps1 | 2 +- 2 files changed, 47 insertions(+), 47 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecBECRemediate.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecBECRemediate.ps1 index 9d2db27f7e2a..29bf889d3773 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecBECRemediate.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecBECRemediate.ps1 @@ -28,9 +28,9 @@ function Invoke-ExecBECRemediate { $AllResults.Add($PasswordResult) } catch { $AllResults.Add([pscustomobject]@{ - resultText = "Failed to reset password: $($_.Exception.Message)" - state = 'error' - }) + resultText = "Failed to reset password: $($_.Exception.Message)" + state = 'error' + }) } # Step 2: Disable Account @@ -38,14 +38,14 @@ function Invoke-ExecBECRemediate { try { $DisableResult = Set-CIPPSignInState -userid $Username -AccountEnabled $false -tenantFilter $TenantFilter -APIName $APIName -Headers $Headers $AllResults.Add([pscustomobject]@{ - resultText = $DisableResult - state = if ($DisableResult -like "*WARNING*") { 'warning' } else { 'success' } - }) + resultText = $DisableResult + state = if ($DisableResult -like '*WARNING*') { 'warning' } else { 'success' } + }) } catch { $AllResults.Add([pscustomobject]@{ - resultText = "Failed to disable account: $($_.Exception.Message)" - state = 'error' - }) + resultText = "Failed to disable account: $($_.Exception.Message)" + state = 'error' + }) } # Step 3: Revoke Sessions @@ -53,14 +53,14 @@ function Invoke-ExecBECRemediate { try { $SessionResult = Revoke-CIPPSessions -userid $SuspectUser -username $Username -Headers $Headers -APIName $APIName -tenantFilter $TenantFilter $AllResults.Add([pscustomobject]@{ - resultText = $SessionResult - state = if ($SessionResult -like "*Failed*") { 'error' } else { 'success' } - }) + resultText = $SessionResult + state = if ($SessionResult -like '*Failed*') { 'error' } else { 'success' } + }) } catch { $AllResults.Add([pscustomobject]@{ - resultText = "Failed to revoke sessions: $($_.Exception.Message)" - state = 'error' - }) + resultText = "Failed to revoke sessions: $($_.Exception.Message)" + state = 'error' + }) } # Step 4: Remove MFA methods @@ -68,14 +68,14 @@ function Invoke-ExecBECRemediate { try { $MFAResult = Remove-CIPPUserMFA -UserPrincipalName $Username -TenantFilter $TenantFilter -Headers $Headers $AllResults.Add([pscustomobject]@{ - resultText = $MFAResult - state = if ($MFAResult -like "*No MFA methods*") { 'info' } elseif ($MFAResult -like "*Successfully*") { 'success' } else { 'error' } - }) + resultText = $MFAResult + state = if ($MFAResult -like '*No MFA methods*') { 'info' } elseif ($MFAResult -like '*Successfully*') { 'success' } else { 'error' } + }) } catch { $AllResults.Add([pscustomobject]@{ - resultText = "Failed to remove MFA methods: $($_.Exception.Message)" - state = 'error' - }) + resultText = "Failed to remove MFA methods: $($_.Exception.Message)" + state = 'error' + }) } # Step 5: Disable Inbox Rules @@ -92,9 +92,9 @@ function Invoke-ExecBECRemediate { if (($Rules | Measure-Object).Count -eq 0) { # No rules exist at all $AllResults.Add([pscustomobject]@{ - resultText = "No Inbox Rules found for $Username." - state = 'info' - }) + resultText = "No Inbox Rules found for $Username." + state = 'info' + }) } else { # Rules exist, filter and process them $ProcessableRules = $Rules | Where-Object { @@ -107,9 +107,9 @@ function Invoke-ExecBECRemediate { $SystemRulesCount = ($Rules | Measure-Object).Count - $DelegateRulesSkipped if ($SystemRulesCount -gt 0) { $AllResults.Add([pscustomobject]@{ - resultText = "Found $(($Rules | Measure-Object).Count) inbox rules for $Username, but none require disabling (only system rules found)." - state = 'info' - }) + resultText = "Found $(($Rules | Measure-Object).Count) inbox rules for $Username, but none require disabling (only system rules found)." + state = 'info' + }) } } else { # Process the filterable rules @@ -118,7 +118,7 @@ function Invoke-ExecBECRemediate { Write-LogMessage -headers $Headers -API $APIName -message "Processing rule: Name='$($CurrentRule.Name)', Identity='$($CurrentRule.Identity)'" -Sev 'Info' -tenant $TenantFilter try { - Set-CIPPMailboxRule -Username $Username -TenantFilter $TenantFilter -RuleId $CurrentRule.Identity -RuleName $CurrentRule.Name -Disable -APIName $APIName -Headers $Headers + Set-CIPPMailboxRule -Username $Username -UserId $Username -TenantFilter $TenantFilter -RuleId $CurrentRule.Identity -RuleName $CurrentRule.Name -Disable -APIName $APIName -Headers $Headers Write-LogMessage -headers $Headers -API $APIName -message "Successfully disabled rule: $($CurrentRule.Name)" -Sev 'Info' -tenant $TenantFilter $RuleDisabled++ @@ -140,29 +140,29 @@ function Invoke-ExecBECRemediate { # Report results if ($RuleDisabled -gt 0) { $AllResults.Add([pscustomobject]@{ - resultText = "Successfully disabled $RuleDisabled inbox rules for $Username" - state = 'success' - }) + resultText = "Successfully disabled $RuleDisabled inbox rules for $Username" + state = 'success' + }) } elseif ($DelegateRulesSkipped -gt 0 -and $RuleDisabled -eq 0 -and $RuleFailed -eq 0) { # Only system rules were found, report as no processable rules $AllResults.Add([pscustomobject]@{ - resultText = "No processable inbox rules found for $Username" - state = 'info' - }) + resultText = "No processable inbox rules found for $Username" + state = 'info' + }) } if ($RuleFailed -gt 0) { $AllResults.Add([pscustomobject]@{ - resultText = "Failed to process $RuleFailed inbox rules for $Username" - state = 'warning' - }) + resultText = "Failed to process $RuleFailed inbox rules for $Username" + state = 'warning' + }) # Add individual rule failure messages as objects foreach ($RuleMessage in $RuleMessages) { $AllResults.Add([pscustomobject]@{ - resultText = $RuleMessage - state = 'error' - }) + resultText = $RuleMessage + state = 'error' + }) } } } @@ -175,9 +175,9 @@ function Invoke-ExecBECRemediate { $ErrorMsg = "Failed to process inbox rules: $($_.Exception.Message)" Write-LogMessage -headers $Headers -API $APIName -message $ErrorMsg -Sev 'Error' -tenant $TenantFilter $AllResults.Add([pscustomobject]@{ - resultText = $ErrorMsg - state = 'error' - }) + resultText = $ErrorMsg + state = 'error' + }) } $StatusCode = [HttpStatusCode]::OK @@ -190,9 +190,9 @@ function Invoke-ExecBECRemediate { $ErrorMessage = Get-CippException -Exception $_ $ErrorList = [System.Collections.Generic.List[object]]::new() $ErrorList.Add([pscustomobject]@{ - resultText = "Failed to execute remediation at step '$Step'. $($ErrorMessage.NormalizedError)" - state = 'error' - }) + resultText = "Failed to execute remediation at step '$Step'. $($ErrorMessage.NormalizedError)" + state = 'error' + }) Write-LogMessage -API 'BECRemediate' -tenant $TenantFilter -message "Executed Remediation for $Username failed at the $Step step" -sev 'Error' -LogData $ErrorMessage $StatusCode = [HttpStatusCode]::InternalServerError diff --git a/Modules/CIPPCore/Public/Set-CIPPMailboxRule.ps1 b/Modules/CIPPCore/Public/Set-CIPPMailboxRule.ps1 index cbbd39cd0916..b7b0f4810c15 100644 --- a/Modules/CIPPCore/Public/Set-CIPPMailboxRule.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPMailboxRule.ps1 @@ -22,7 +22,7 @@ } try { - $null = New-ExoRequest -tenantid $TenantFilter -cmdlet "$State-InboxRule" -Anchor $Username -cmdParams @{Identity = $RuleId } + $null = New-ExoRequest -tenantid $TenantFilter -cmdlet "$State-InboxRule" -Anchor $Username -cmdParams @{Identity = $RuleId; mailbox = $UserId } -Headers $Headers Write-LogMessage -headers $Headers -API $APIName -message "Successfully set mailbox rule $($RuleName) for $($Username) to $($State)d" -Sev 'Info' -tenant $TenantFilter return "Successfully set mailbox rule $($RuleName) for $($Username) to $($State)d" } catch { From 3248729c0028ba95c6dadd98b5d5a83cc8ca225a Mon Sep 17 00:00:00 2001 From: John Duprey Date: Sun, 11 Jan 2026 11:18:37 -0500 Subject: [PATCH 111/503] remove extra ip range calculation --- .../CIPP/Settings/Invoke-ListCustomRole.ps1 | 22 +++++-------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ListCustomRole.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ListCustomRole.ps1 index 3c59304b3771..08e84177482a 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ListCustomRole.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ListCustomRole.ps1 @@ -13,7 +13,7 @@ function Invoke-ListCustomRole { $AccessRoleGroupTable = Get-CippTable -tablename 'AccessRoleGroups' $RoleGroups = Get-CIPPAzDataTableEntity @AccessRoleGroupTable - + $AccessIPRangeTable = Get-CippTable -tablename 'AccessIPRanges' $AccessIPRanges = Get-CIPPAzDataTableEntity @AccessIPRangeTable @@ -22,7 +22,7 @@ function Invoke-ListCustomRole { $RoleList = [System.Collections.Generic.List[pscustomobject]]::new() foreach ($Role in $DefaultRoles) { $RoleGroup = $RoleGroups | Where-Object -Property RowKey -EQ $Role - + $IPRangeEntity = $AccessIPRanges | Where-Object -Property RowKey -EQ $Role if ($IPRangeEntity) { try { @@ -33,7 +33,7 @@ function Invoke-ListCustomRole { } else { $IPRanges = @() } - + $RoleList.Add([pscustomobject]@{ RoleName = $Role Type = 'Built-In' @@ -42,7 +42,7 @@ function Invoke-ListCustomRole { BlockedTenants = @() EntraGroup = $RoleGroup.GroupName ?? $null EntraGroupId = $RoleGroup.GroupId ?? $null - IPRange = $IPRanges + IPRange = $IPRanges }) } foreach ($Role in $CustomRoles) { @@ -144,16 +144,4 @@ function Invoke-ListCustomRole { Body = ConvertTo-Json -InputObject $Body -Depth 5 }) } - - $IPRangeEntity = $AccessIPRanges | Where-Object -Property RowKey -EQ $Role.RowKey - if ($IPRangeEntity) { - try { - $IPRanges = @($IPRangeEntity.IPRanges | ConvertFrom-Json) - } catch { - $IPRanges = @() - } - $Role | Add-Member -NotePropertyName IPRange -NotePropertyValue $IPRanges -Force - } else { - $Role | Add-Member -NotePropertyName IPRange -NotePropertyValue @() -Force - } - + From 7b5996cf35e8ee8ebbcf2f27dd2b625bded78837 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Sun, 11 Jan 2026 11:18:46 -0500 Subject: [PATCH 112/503] Replace Get-AzStorageQueue with Get-CIPPAzStorageQueue Updated the queue retrieval command to use Get-CIPPAzStorageQueue instead of Get-AzStorageQueue for consistency with custom module usage. --- .../Public/Entrypoints/Timer Functions/Start-DurableCleanup.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-DurableCleanup.ps1 b/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-DurableCleanup.ps1 index 7abf8bb608f5..5987188d5a10 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-DurableCleanup.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-DurableCleanup.ps1 @@ -33,7 +33,7 @@ function Start-DurableCleanup { $Table = Get-CippTable -TableName $Table $FunctionName = $Table.TableName -replace 'Instances', '' $Orchestrators = Get-CIPPAzDataTableEntity @Table -Filter "RuntimeStatus eq 'Running'" | Select-Object * -ExcludeProperty Input - $Queues = Get-AzStorageQueue -Context $StorageContext -Name ('{0}*' -f $FunctionName) | Select-Object -Property Name, ApproximateMessageCount, QueueClient + $Queues = Get-CIPPAzStorageQueue -Name ('{0}*' -f $FunctionName) | Select-Object -Property Name, ApproximateMessageCount, QueueClient $LongRunningOrchestrators = $Orchestrators | Where-Object { $_.CreatedTime.DateTime -lt $TargetTime } if ($LongRunningOrchestrators.Count -gt 0) { From 8c484e6572691f96fbec719f101ce791b1aa5c70 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Sun, 11 Jan 2026 17:32:59 +0100 Subject: [PATCH 113/503] ORCA MD files --- .../Tests/ORCA/Identity/Invoke-CippTestORCA100.md | 8 ++++++++ .../Tests/ORCA/Identity/Invoke-CippTestORCA101.md | 8 ++++++++ .../Tests/ORCA/Identity/Invoke-CippTestORCA102.md | 8 ++++++++ .../Tests/ORCA/Identity/Invoke-CippTestORCA103.md | 8 ++++++++ .../Tests/ORCA/Identity/Invoke-CippTestORCA104.md | 10 ++++++++++ .../Tests/ORCA/Identity/Invoke-CippTestORCA105.md | 10 ++++++++++ .../Tests/ORCA/Identity/Invoke-CippTestORCA106.md | 10 ++++++++++ .../Tests/ORCA/Identity/Invoke-CippTestORCA107.md | 10 ++++++++++ .../Tests/ORCA/Identity/Invoke-CippTestORCA108.md | 10 ++++++++++ .../Tests/ORCA/Identity/Invoke-CippTestORCA108_1.md | 9 +++++++++ .../Tests/ORCA/Identity/Invoke-CippTestORCA109.md | 10 ++++++++++ .../Tests/ORCA/Identity/Invoke-CippTestORCA110.md | 10 ++++++++++ .../Tests/ORCA/Identity/Invoke-CippTestORCA111.md | 10 ++++++++++ .../Tests/ORCA/Identity/Invoke-CippTestORCA112.md | 10 ++++++++++ .../Tests/ORCA/Identity/Invoke-CippTestORCA113.md | 10 ++++++++++ .../Tests/ORCA/Identity/Invoke-CippTestORCA114.md | 10 ++++++++++ .../Tests/ORCA/Identity/Invoke-CippTestORCA115.md | 10 ++++++++++ .../Tests/ORCA/Identity/Invoke-CippTestORCA116.md | 10 ++++++++++ .../Tests/ORCA/Identity/Invoke-CippTestORCA118_1.md | 10 ++++++++++ .../Tests/ORCA/Identity/Invoke-CippTestORCA118_2.md | 10 ++++++++++ .../Tests/ORCA/Identity/Invoke-CippTestORCA118_3.md | 10 ++++++++++ .../Tests/ORCA/Identity/Invoke-CippTestORCA118_4.md | 10 ++++++++++ .../Tests/ORCA/Identity/Invoke-CippTestORCA119.md | 10 ++++++++++ .../ORCA/Identity/Invoke-CippTestORCA120_malware.md | 10 ++++++++++ .../ORCA/Identity/Invoke-CippTestORCA120_phish.md | 10 ++++++++++ .../Tests/ORCA/Identity/Invoke-CippTestORCA120_spam.md | 10 ++++++++++ .../Tests/ORCA/Identity/Invoke-CippTestORCA121.md | 10 ++++++++++ .../Tests/ORCA/Identity/Invoke-CippTestORCA123.md | 10 ++++++++++ .../Tests/ORCA/Identity/Invoke-CippTestORCA124.md | 10 ++++++++++ .../Tests/ORCA/Identity/Invoke-CippTestORCA139.md | 8 ++++++++ .../Tests/ORCA/Identity/Invoke-CippTestORCA140.md | 8 ++++++++ .../Tests/ORCA/Identity/Invoke-CippTestORCA141.md | 8 ++++++++ .../Tests/ORCA/Identity/Invoke-CippTestORCA142.md | 8 ++++++++ .../Tests/ORCA/Identity/Invoke-CippTestORCA143.md | 8 ++++++++ .../Tests/ORCA/Identity/Invoke-CippTestORCA156.md | 8 ++++++++ .../Tests/ORCA/Identity/Invoke-CippTestORCA158.md | 8 ++++++++ .../Tests/ORCA/Identity/Invoke-CippTestORCA179.md | 8 ++++++++ .../Tests/ORCA/Identity/Invoke-CippTestORCA180.md | 8 ++++++++ .../Tests/ORCA/Identity/Invoke-CippTestORCA189.md | 8 ++++++++ .../Tests/ORCA/Identity/Invoke-CippTestORCA189_2.md | 8 ++++++++ .../Tests/ORCA/Identity/Invoke-CippTestORCA205.md | 8 ++++++++ .../Tests/ORCA/Identity/Invoke-CippTestORCA220.md | 8 ++++++++ .../Tests/ORCA/Identity/Invoke-CippTestORCA221.md | 8 ++++++++ .../Tests/ORCA/Identity/Invoke-CippTestORCA222.md | 8 ++++++++ .../Tests/ORCA/Identity/Invoke-CippTestORCA223.md | 8 ++++++++ .../Tests/ORCA/Identity/Invoke-CippTestORCA224.md | 8 ++++++++ .../Tests/ORCA/Identity/Invoke-CippTestORCA225.md | 8 ++++++++ .../Tests/ORCA/Identity/Invoke-CippTestORCA226.md | 8 ++++++++ .../Tests/ORCA/Identity/Invoke-CippTestORCA227.md | 8 ++++++++ .../Tests/ORCA/Identity/Invoke-CippTestORCA228.md | 8 ++++++++ .../Tests/ORCA/Identity/Invoke-CippTestORCA229.md | 8 ++++++++ .../Tests/ORCA/Identity/Invoke-CippTestORCA230.md | 8 ++++++++ .../Tests/ORCA/Identity/Invoke-CippTestORCA231.md | 8 ++++++++ .../Tests/ORCA/Identity/Invoke-CippTestORCA232.md | 8 ++++++++ .../Tests/ORCA/Identity/Invoke-CippTestORCA233.md | 8 ++++++++ .../Tests/ORCA/Identity/Invoke-CippTestORCA233_1.md | 8 ++++++++ .../Tests/ORCA/Identity/Invoke-CippTestORCA234.md | 8 ++++++++ .../Tests/ORCA/Identity/Invoke-CippTestORCA235.md | 8 ++++++++ .../Tests/ORCA/Identity/Invoke-CippTestORCA236.md | 8 ++++++++ .../Tests/ORCA/Identity/Invoke-CippTestORCA237.md | 8 ++++++++ .../Tests/ORCA/Identity/Invoke-CippTestORCA238.md | 8 ++++++++ .../Tests/ORCA/Identity/Invoke-CippTestORCA239.md | 9 +++++++++ .../Tests/ORCA/Identity/Invoke-CippTestORCA240.md | 8 ++++++++ .../Tests/ORCA/Identity/Invoke-CippTestORCA241.md | 8 ++++++++ .../Tests/ORCA/Identity/Invoke-CippTestORCA242.md | 8 ++++++++ .../Tests/ORCA/Identity/Invoke-CippTestORCA243.md | 8 ++++++++ .../Tests/ORCA/Identity/Invoke-CippTestORCA244.md | 8 ++++++++ 67 files changed, 586 insertions(+) create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA100.md create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA101.md create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA102.md create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA103.md create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA104.md create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA105.md create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA106.md create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA107.md create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA108.md create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA108_1.md create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA109.md create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA110.md create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA111.md create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA112.md create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA113.md create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA114.md create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA115.md create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA116.md create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA118_1.md create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA118_2.md create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA118_3.md create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA118_4.md create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA119.md create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA120_malware.md create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA120_phish.md create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA120_spam.md create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA121.md create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA123.md create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA124.md create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA139.md create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA140.md create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA141.md create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA142.md create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA143.md create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA156.md create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA158.md create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA179.md create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA180.md create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA189.md create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA189_2.md create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA205.md create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA220.md create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA221.md create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA222.md create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA223.md create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA224.md create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA225.md create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA226.md create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA227.md create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA228.md create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA229.md create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA230.md create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA231.md create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA232.md create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA233.md create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA233_1.md create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA234.md create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA235.md create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA236.md create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA237.md create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA238.md create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA239.md create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA240.md create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA241.md create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA242.md create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA243.md create mode 100644 Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA244.md diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA100.md b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA100.md new file mode 100644 index 000000000000..55f82ea10817 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA100.md @@ -0,0 +1,8 @@ +The Bulk Complaint Level (BCL) threshold determines when bulk email messages are treated as spam. Microsoft recommends setting this value between 4 and 6 to achieve a balance between spam protection and minimizing false positives. A threshold that is too high may allow bulk spam through, while one that is too low may quarantine legitimate bulk email. + +**Remediation action** + +- [Configure anti-spam policies in Microsoft 365](https://learn.microsoft.com/microsoft-365/security/office-365-security/anti-spam-policies-configure) +- [Bulk Complaint Level (BCL) in Exchange Online Protection](https://learn.microsoft.com/microsoft-365/security/office-365-security/anti-spam-bulk-complaint-level-bcl-about) + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA101.md b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA101.md new file mode 100644 index 000000000000..6b67729768d4 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA101.md @@ -0,0 +1,8 @@ +Anti-spam policies should enable the option to move spam messages to the Junk Email folder rather than deleting them. This provides users with visibility into what was filtered and allows them to review messages that may have been incorrectly classified, reducing the risk of lost legitimate emails. + +**Remediation action** + +- [Configure anti-spam policies in Microsoft 365](https://learn.microsoft.com/microsoft-365/security/office-365-security/anti-spam-policies-configure) +- [Recommended settings for EOP and Microsoft Defender for Office 365 security](https://learn.microsoft.com/microsoft-365/security/office-365-security/recommended-settings-for-eop-and-office365) + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA102.md b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA102.md new file mode 100644 index 000000000000..4675fbf0fed5 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA102.md @@ -0,0 +1,8 @@ +Advanced Spam Filtering (ASF) options in anti-spam policies provide additional protection against spam. However, Microsoft recommends disabling most ASF options as they can cause false positives. Standard filtering with Defender for Office 365 is more effective. Only specific ASF options should remain enabled when needed for legacy protection scenarios. + +**Remediation action** + +- [Advanced Spam Filter (ASF) settings in Microsoft 365](https://learn.microsoft.com/microsoft-365/security/office-365-security/anti-spam-policies-asf-settings-about) +- [Configure anti-spam policies](https://learn.microsoft.com/microsoft-365/security/office-365-security/anti-spam-policies-configure) + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA103.md b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA103.md new file mode 100644 index 000000000000..bc048fc4c32a --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA103.md @@ -0,0 +1,8 @@ +Outbound spam filter policies should be configured with appropriate limits to prevent compromised accounts from sending large volumes of spam. Microsoft recommends setting RecipientLimitExternalPerHour to 500, RecipientLimitInternalPerHour to 1000, and ActionWhenThresholdReached to BlockUserForToday to protect your organization's reputation and prevent abuse. + +**Remediation action** + +- [Configure outbound spam filtering](https://learn.microsoft.com/microsoft-365/security/office-365-security/outbound-spam-policies-configure) +- [Outbound spam protection in Exchange Online](https://learn.microsoft.com/microsoft-365/security/office-365-security/outbound-spam-protection-about) + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA104.md b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA104.md new file mode 100644 index 000000000000..bf7bb5221b01 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA104.md @@ -0,0 +1,10 @@ +Anti-phishing policies should have High Confidence Phish action set to Quarantine to protect users from sophisticated phishing attacks. Messages identified as high-confidence phishing attempts pose a significant security risk and should be quarantined for review rather than delivered to users. + +**Remediation action** + +- [Configure anti-phishing policies in Microsoft Defender for Office 365](https://learn.microsoft.com/microsoft-365/security/office-365-security/anti-phishing-policies-about) +- [Set up anti-phishing policies in Microsoft 365](https://learn.microsoft.com/microsoft-365/security/office-365-security/anti-phishing-policies-mdo-configure) +- [Recommended settings for EOP and Microsoft Defender for Office 365 security](https://learn.microsoft.com/microsoft-365/security/office-365-security/recommended-settings-for-eop-and-office365) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA105.md b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA105.md new file mode 100644 index 000000000000..3391a38cece9 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA105.md @@ -0,0 +1,10 @@ +Safe Links policies should enable synchronous URL detonation (DeliverMessageAfterScan) to scan links in real-time before delivering messages to users. This ensures that malicious URLs are detected and blocked before they reach user mailboxes, providing enhanced protection against zero-day attacks and sophisticated phishing campaigns. + +**Remediation action** + +- [Safe Links in Microsoft Defender for Office 365](https://learn.microsoft.com/microsoft-365/security/office-365-security/safe-links-about) +- [Set up Safe Links policies in Microsoft Defender for Office 365](https://learn.microsoft.com/microsoft-365/security/office-365-security/safe-links-policies-configure) +- [Recommended settings for EOP and Microsoft Defender for Office 365 security](https://learn.microsoft.com/microsoft-365/security/office-365-security/recommended-settings-for-eop-and-office365) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA106.md b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA106.md new file mode 100644 index 000000000000..8955503864dd --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA106.md @@ -0,0 +1,10 @@ +Anti-spam policies should have a quarantine retention period of at least 30 days to allow sufficient time for administrators to review and release legitimate messages that were incorrectly quarantined. A retention period that is too short may result in legitimate messages being permanently deleted before they can be reviewed. + +**Remediation action** + +- [Configure anti-spam policies in EOP](https://learn.microsoft.com/microsoft-365/security/office-365-security/anti-spam-policies-configure) +- [Anti-spam protection in EOP](https://learn.microsoft.com/microsoft-365/security/office-365-security/anti-spam-protection-about) +- [Quarantine policies in Microsoft 365](https://learn.microsoft.com/microsoft-365/security/office-365-security/quarantine-policies) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA107.md b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA107.md new file mode 100644 index 000000000000..6c0f3601b410 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA107.md @@ -0,0 +1,10 @@ +Quarantine policies should enable end-user spam notifications to inform users about messages that have been quarantined. This allows users to review and release legitimate messages that may have been incorrectly identified as spam, reducing administrative overhead and improving user experience. + +**Remediation action** + +- [Quarantine policies in Microsoft 365](https://learn.microsoft.com/microsoft-365/security/office-365-security/quarantine-policies) +- [End-user spam notifications in Microsoft 365](https://learn.microsoft.com/microsoft-365/security/office-365-security/quarantine-end-user) +- [Configure anti-spam policies in EOP](https://learn.microsoft.com/microsoft-365/security/office-365-security/anti-spam-policies-configure) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA108.md b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA108.md new file mode 100644 index 000000000000..4539f91ff893 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA108.md @@ -0,0 +1,10 @@ +DKIM (DomainKeys Identified Mail) signing should be enabled for all custom domains in your organization. DKIM adds a digital signature to outbound email messages, allowing receiving mail servers to verify that the message was sent from your domain and hasn't been altered in transit. This helps prevent email spoofing and improves email deliverability. + +**Remediation action** + +- [Use DKIM to validate outbound email sent from your custom domain](https://learn.microsoft.com/microsoft-365/security/office-365-security/email-authentication-dkim-configure) +- [How Microsoft 365 uses SPF and DKIM to prevent spoofing](https://learn.microsoft.com/microsoft-365/security/office-365-security/email-authentication-anti-spoofing) +- [Email authentication in Microsoft 365](https://learn.microsoft.com/microsoft-365/security/office-365-security/email-authentication-about) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA108_1.md b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA108_1.md new file mode 100644 index 000000000000..907ad5c4e661 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA108_1.md @@ -0,0 +1,9 @@ +DNS records must be properly configured to support DKIM signing for your custom domains. After enabling DKIM in Microsoft 365, you need to publish the DKIM CNAME records in your domain's DNS zone. Without these DNS records, DKIM signing will not function properly, and your outbound emails will not be signed. + +**Remediation action** + +- [Use DKIM to validate outbound email sent from your custom domain](https://learn.microsoft.com/microsoft-365/security/office-365-security/email-authentication-dkim-configure) +- [Steps to create, enable and disable DKIM from Microsoft 365 Defender portal](https://learn.microsoft.com/microsoft-365/security/office-365-security/email-authentication-dkim-configure#steps-to-create-enable-and-disable-dkim-from-microsoft-365-defender-portal) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA109.md b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA109.md new file mode 100644 index 000000000000..b4fb18706c7f --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA109.md @@ -0,0 +1,10 @@ +Anti-spam policies should not have senders or domains configured in the allow list in an unsafe manner. Allowing senders or entire domains to bypass spam filtering can expose your organization to phishing attacks and malware. Allow lists should be used sparingly and only for trusted senders with a verified business relationship. + +**Remediation action** + +- [Configure anti-spam policies in EOP](https://learn.microsoft.com/microsoft-365/security/office-365-security/anti-spam-policies-configure) +- [Create safe sender lists in EOP](https://learn.microsoft.com/microsoft-365/security/office-365-security/create-safe-sender-lists-in-office-365) +- [Use mail flow rules to filter bulk email in Exchange Online](https://learn.microsoft.com/exchange/security-and-compliance/mail-flow-rules/use-rules-to-filter-bulk-mail) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA110.md b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA110.md new file mode 100644 index 000000000000..70a3607e6be1 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA110.md @@ -0,0 +1,10 @@ +Anti-spam policies should have internal sender notifications disabled to prevent information disclosure. Notifying internal senders when their messages are quarantined can reveal security policy details to potentially compromised accounts and may be used by attackers to understand and bypass your security measures. + +**Remediation action** + +- [Configure anti-spam policies in EOP](https://learn.microsoft.com/microsoft-365/security/office-365-security/anti-spam-policies-configure) +- [Anti-spam protection in EOP](https://learn.microsoft.com/microsoft-365/security/office-365-security/anti-spam-protection-about) +- [Recommended settings for EOP and Microsoft Defender for Office 365 security](https://learn.microsoft.com/microsoft-365/security/office-365-security/recommended-settings-for-eop-and-office365) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA111.md b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA111.md new file mode 100644 index 000000000000..36010802db16 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA111.md @@ -0,0 +1,10 @@ +Anti-phishing policies should enable the unauthenticated sender indicator (EnableUnauthenticatedSender) to display a visual indicator for messages that fail authentication checks. This helps users identify potentially spoofed messages and reduces the risk of falling victim to phishing attacks. + +**Remediation action** + +- [Configure anti-phishing policies in Microsoft Defender for Office 365](https://learn.microsoft.com/microsoft-365/security/office-365-security/anti-phishing-policies-about) +- [Set up anti-phishing policies in Microsoft 365](https://learn.microsoft.com/microsoft-365/security/office-365-security/anti-phishing-policies-mdo-configure) +- [Unauthenticated sender indicators](https://learn.microsoft.com/microsoft-365/security/office-365-security/anti-phishing-policies-about#unauthenticated-sender-indicators) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA112.md b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA112.md new file mode 100644 index 000000000000..fb0bf7059686 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA112.md @@ -0,0 +1,10 @@ +Anti-phishing policies should configure the anti-spoofing protection action to move messages to the Junk Email folder. This provides protection against spoofed messages while allowing users to review them if needed. Messages that appear to be from internal senders but fail authentication checks should be quarantined to prevent impersonation attacks. + +**Remediation action** + +- [Configure anti-phishing policies in Microsoft Defender for Office 365](https://learn.microsoft.com/microsoft-365/security/office-365-security/anti-phishing-policies-about) +- [Spoof intelligence insight in Microsoft 365](https://learn.microsoft.com/microsoft-365/security/office-365-security/anti-spoofing-spoof-intelligence) +- [Anti-spoofing protection in EOP](https://learn.microsoft.com/microsoft-365/security/office-365-security/anti-phishing-protection-spoofing-about) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA113.md b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA113.md new file mode 100644 index 000000000000..5730ae5bc2d9 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA113.md @@ -0,0 +1,10 @@ +Safe Links policies should disable the AllowClickThrough setting to prevent users from clicking through to potentially malicious URLs even after receiving a warning. When AllowClickThrough is enabled, users can bypass the Safe Links warning page and navigate to the original URL, which significantly reduces the effectiveness of Safe Links protection. + +**Remediation action** + +- [Safe Links in Microsoft Defender for Office 365](https://learn.microsoft.com/microsoft-365/security/office-365-security/safe-links-about) +- [Set up Safe Links policies in Microsoft Defender for Office 365](https://learn.microsoft.com/microsoft-365/security/office-365-security/safe-links-policies-configure) +- [Recommended settings for EOP and Microsoft Defender for Office 365 security](https://learn.microsoft.com/microsoft-365/security/office-365-security/recommended-settings-for-eop-and-office365) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA114.md b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA114.md new file mode 100644 index 000000000000..04bf5f2fbbeb --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA114.md @@ -0,0 +1,10 @@ +Anti-spam policies should not have IP addresses configured in the allow list. IP-based allow lists can be exploited by attackers who use compromised or shared infrastructure, and they bypass important security controls. Instead of using IP allow lists, implement proper email authentication (SPF, DKIM, DMARC) and use sender-based allow lists only when absolutely necessary. + +**Remediation action** + +- [Configure anti-spam policies in EOP](https://learn.microsoft.com/microsoft-365/security/office-365-security/anti-spam-policies-configure) +- [Configure the connection filter policy](https://learn.microsoft.com/microsoft-365/security/office-365-security/connection-filter-policies-configure) +- [Create safe sender lists in EOP](https://learn.microsoft.com/microsoft-365/security/office-365-security/create-safe-sender-lists-in-office-365) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA115.md b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA115.md new file mode 100644 index 000000000000..a0485c090e97 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA115.md @@ -0,0 +1,10 @@ +Anti-phishing policies should enable mailbox intelligence-based impersonation protection to leverage machine learning and user behavior patterns for detecting impersonation attempts. This feature analyzes email communication patterns and identifies messages that may be attempting to impersonate trusted contacts or business partners. + +**Remediation action** + +- [Impersonation settings in anti-phishing policies](https://learn.microsoft.com/microsoft-365/security/office-365-security/anti-phishing-policies-about#impersonation-settings-in-anti-phishing-policies-in-microsoft-defender-for-office-365) +- [Configure anti-phishing policies in Microsoft Defender for Office 365](https://learn.microsoft.com/microsoft-365/security/office-365-security/anti-phishing-policies-mdo-configure) +- [Recommended settings for EOP and Microsoft Defender for Office 365 security](https://learn.microsoft.com/microsoft-365/security/office-365-security/recommended-settings-for-eop-and-office365) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA116.md b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA116.md new file mode 100644 index 000000000000..87a80c2c206e --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA116.md @@ -0,0 +1,10 @@ +Anti-phishing policies should configure mailbox intelligence-based impersonation protection to move messages to the Junk Email folder. This setting ensures that messages identified as potential impersonation attempts are quarantined or moved to junk mail, preventing users from being exposed to sophisticated social engineering attacks while still allowing for message review if needed. + +**Remediation action** + +- [Impersonation settings in anti-phishing policies](https://learn.microsoft.com/microsoft-365/security/office-365-security/anti-phishing-policies-about#impersonation-settings-in-anti-phishing-policies-in-microsoft-defender-for-office-365) +- [Configure anti-phishing policies in Microsoft Defender for Office 365](https://learn.microsoft.com/microsoft-365/security/office-365-security/anti-phishing-policies-mdo-configure) +- [Recommended settings for EOP and Microsoft Defender for Office 365 security](https://learn.microsoft.com/microsoft-365/security/office-365-security/recommended-settings-for-eop-and-office365) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA118_1.md b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA118_1.md new file mode 100644 index 000000000000..eb778c08620e --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA118_1.md @@ -0,0 +1,10 @@ +Anti-spam policies should not have entire domains configured in the allow list. Allowing entire domains to bypass spam filtering can expose your organization to phishing attacks and malware, especially if the allowed domain is compromised or used by multiple organizations. Domain-based allow lists should be avoided, and sender-specific allow lists should be used sparingly only for verified business relationships. + +**Remediation action** + +- [Configure anti-spam policies in EOP](https://learn.microsoft.com/microsoft-365/security/office-365-security/anti-spam-policies-configure) +- [Create safe sender lists in EOP](https://learn.microsoft.com/microsoft-365/security/office-365-security/create-safe-sender-lists-in-office-365) +- [Recommended settings for EOP and Microsoft Defender for Office 365 security](https://learn.microsoft.com/microsoft-365/security/office-365-security/recommended-settings-for-eop-and-office365) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA118_2.md b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA118_2.md new file mode 100644 index 000000000000..977cc4a452c8 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA118_2.md @@ -0,0 +1,10 @@ +Transport rules (mail flow rules) should not be configured to allow list entire domains and bypass spam filtering. Transport rules that skip spam filtering for entire domains can be exploited by attackers to deliver malicious content. These rules override anti-spam policies and should only be used in exceptional circumstances with strict conditions. + +**Remediation action** + +- [Mail flow rules (transport rules) in Exchange Online](https://learn.microsoft.com/exchange/security-and-compliance/mail-flow-rules/mail-flow-rules) +- [Use mail flow rules to inspect message attachments](https://learn.microsoft.com/exchange/security-and-compliance/mail-flow-rules/inspect-message-attachments) +- [Recommended settings for EOP and Microsoft Defender for Office 365 security](https://learn.microsoft.com/microsoft-365/security/office-365-security/recommended-settings-for-eop-and-office365) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA118_3.md b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA118_3.md new file mode 100644 index 000000000000..77da8291faed --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA118_3.md @@ -0,0 +1,10 @@ +Anti-spam policies should not have your organization's own domains configured in the allow list. Adding your own domains to the allow list can be exploited by attackers using spoofing techniques to bypass spam filtering. Internal domain protection should be handled through proper email authentication (SPF, DKIM, DMARC) and anti-spoofing policies, not through allow lists. + +**Remediation action** + +- [Configure anti-spam policies in EOP](https://learn.microsoft.com/microsoft-365/security/office-365-security/anti-spam-policies-configure) +- [Anti-spoofing protection in EOP](https://learn.microsoft.com/microsoft-365/security/office-365-security/anti-phishing-protection-spoofing-about) +- [Email authentication in Microsoft 365](https://learn.microsoft.com/microsoft-365/security/office-365-security/email-authentication-about) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA118_4.md b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA118_4.md new file mode 100644 index 000000000000..a08348ebdb46 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA118_4.md @@ -0,0 +1,10 @@ +Transport rules (mail flow rules) should not be configured to allow list your organization's own domains and bypass spam filtering. Allowing your own domains through transport rules can be exploited by attackers using spoofing techniques. These rules override important security controls and should never be used for internal domain protection. + +**Remediation action** + +- [Mail flow rules (transport rules) in Exchange Online](https://learn.microsoft.com/exchange/security-and-compliance/mail-flow-rules/mail-flow-rules) +- [Anti-spoofing protection in EOP](https://learn.microsoft.com/microsoft-365/security/office-365-security/anti-phishing-protection-spoofing-about) +- [Recommended settings for EOP and Microsoft Defender for Office 365 security](https://learn.microsoft.com/microsoft-365/security/office-365-security/recommended-settings-for-eop-and-office365) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA119.md b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA119.md new file mode 100644 index 000000000000..9a7837fa85d0 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA119.md @@ -0,0 +1,10 @@ +Anti-phishing policies should enable similar domains safety tips to warn users when they receive messages from domains that are visually similar to known trusted domains. This feature helps protect users from homograph attacks and typosquatting attempts where attackers register domains that closely resemble legitimate domains to deceive users. + +**Remediation action** + +- [Configure anti-phishing policies in Microsoft Defender for Office 365](https://learn.microsoft.com/microsoft-365/security/office-365-security/anti-phishing-policies-about) +- [Safety tips in email messages](https://learn.microsoft.com/microsoft-365/security/office-365-security/anti-phishing-protection-about#safety-tips-in-email-messages) +- [Set up anti-phishing policies in Microsoft 365](https://learn.microsoft.com/microsoft-365/security/office-365-security/anti-phishing-policies-mdo-configure) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA120_malware.md b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA120_malware.md new file mode 100644 index 000000000000..e6170ac647d5 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA120_malware.md @@ -0,0 +1,10 @@ +Anti-malware policies should enable Zero Hour Autopurge (ZAP) for malware to automatically detect and remediate malicious messages that were delivered before the malware signature was available. ZAP for malware helps protect users from newly identified threats by removing or quarantining malicious messages from mailboxes after delivery. + +**Remediation action** + +- [Zero-hour auto purge (ZAP) in Microsoft Defender for Office 365](https://learn.microsoft.com/microsoft-365/security/office-365-security/zero-hour-auto-purge) +- [Configure anti-malware policies in EOP](https://learn.microsoft.com/microsoft-365/security/office-365-security/anti-malware-policies-configure) +- [Anti-malware protection in EOP](https://learn.microsoft.com/microsoft-365/security/office-365-security/anti-malware-protection-about) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA120_phish.md b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA120_phish.md new file mode 100644 index 000000000000..4d588e4c685f --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA120_phish.md @@ -0,0 +1,10 @@ +Anti-spam policies should enable Zero Hour Autopurge (ZAP) for phishing messages to automatically detect and remediate phishing attacks that were delivered before they were identified as malicious. ZAP for phishing helps protect users from newly identified phishing campaigns by moving or deleting malicious messages from mailboxes after delivery. + +**Remediation action** + +- [Zero-hour auto purge (ZAP) in Microsoft Defender for Office 365](https://learn.microsoft.com/microsoft-365/security/office-365-security/zero-hour-auto-purge) +- [Configure anti-spam policies in EOP](https://learn.microsoft.com/microsoft-365/security/office-365-security/anti-spam-policies-configure) +- [Anti-spam protection in EOP](https://learn.microsoft.com/microsoft-365/security/office-365-security/anti-spam-protection-about) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA120_spam.md b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA120_spam.md new file mode 100644 index 000000000000..3a972e8ceb97 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA120_spam.md @@ -0,0 +1,10 @@ +Anti-spam policies should enable Zero Hour Autopurge (ZAP) for spam messages to automatically detect and move spam messages that were delivered before they were identified as spam. ZAP for spam helps keep user mailboxes clean by moving spam messages to the Junk Email folder after delivery, even if they initially passed spam filtering. + +**Remediation action** + +- [Zero-hour auto purge (ZAP) in Microsoft Defender for Office 365](https://learn.microsoft.com/microsoft-365/security/office-365-security/zero-hour-auto-purge) +- [Configure anti-spam policies in EOP](https://learn.microsoft.com/microsoft-365/security/office-365-security/anti-spam-policies-configure) +- [Anti-spam protection in EOP](https://learn.microsoft.com/microsoft-365/security/office-365-security/anti-spam-protection-about) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA121.md b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA121.md new file mode 100644 index 000000000000..82969121187b --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA121.md @@ -0,0 +1,10 @@ +Quarantine policies should use supported filter policy actions that are appropriate for the threat level. Different types of threats require different quarantine actions - for example, high-confidence phishing should be strictly quarantined with limited user interaction, while spam can be more lenient. Using appropriate quarantine policies ensures effective threat containment while minimizing false positive impact. + +**Remediation action** + +- [Quarantine policies in Microsoft 365](https://learn.microsoft.com/microsoft-365/security/office-365-security/quarantine-policies) +- [Anatomy of a quarantine policy](https://learn.microsoft.com/microsoft-365/security/office-365-security/quarantine-policies#anatomy-of-a-quarantine-policy) +- [Recommended settings for EOP and Microsoft Defender for Office 365 security](https://learn.microsoft.com/microsoft-365/security/office-365-security/recommended-settings-for-eop-and-office365) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA123.md b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA123.md new file mode 100644 index 000000000000..95e74e9e953d --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA123.md @@ -0,0 +1,10 @@ +Anti-phishing policies should enable unusual characters safety tips to warn users when they receive messages containing unusual or suspicious characters. This feature helps protect users from internationalized domain name (IDN) homograph attacks and other obfuscation techniques where attackers use special characters to make malicious URLs or sender addresses appear legitimate. + +**Remediation action** + +- [Configure anti-phishing policies in Microsoft Defender for Office 365](https://learn.microsoft.com/microsoft-365/security/office-365-security/anti-phishing-policies-about) +- [Safety tips in email messages](https://learn.microsoft.com/microsoft-365/security/office-365-security/anti-phishing-protection-about#safety-tips-in-email-messages) +- [Set up anti-phishing policies in Microsoft 365](https://learn.microsoft.com/microsoft-365/security/office-365-security/anti-phishing-policies-mdo-configure) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA124.md b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA124.md new file mode 100644 index 000000000000..f300609ca082 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA124.md @@ -0,0 +1,10 @@ +Safe Attachments policies should configure the unknown malware response to block messages containing potentially malicious attachments. This setting ensures that messages with attachments that cannot be verified as safe are blocked from delivery, preventing zero-day malware attacks and protecting users from unknown threats until they can be properly analyzed. + +**Remediation action** + +- [Safe Attachments in Microsoft Defender for Office 365](https://learn.microsoft.com/microsoft-365/security/office-365-security/safe-attachments-about) +- [Set up Safe Attachments policies in Microsoft Defender for Office 365](https://learn.microsoft.com/microsoft-365/security/office-365-security/safe-attachments-policies-configure) +- [Recommended settings for EOP and Microsoft Defender for Office 365 security](https://learn.microsoft.com/microsoft-365/security/office-365-security/recommended-settings-for-eop-and-office365) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA139.md b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA139.md new file mode 100644 index 000000000000..d79529c834b0 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA139.md @@ -0,0 +1,8 @@ +Anti-spam policies should be configured to move spam messages to the Junk Email folder or quarantine them to protect users from potentially malicious content. Setting the spam action to "Move to Junk Email Folder" or "Quarantine" ensures that spam is properly filtered while still allowing users to review quarantined messages if needed. + +**Remediation action** + +- [Configure anti-spam policies in Microsoft 365](https://learn.microsoft.com/microsoft-365/security/office-365-security/anti-spam-policies-configure) +- [Recommended settings for EOP and Microsoft Defender for Office 365 security](https://learn.microsoft.com/microsoft-365/security/office-365-security/recommended-settings-for-eop-and-office365) + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA140.md b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA140.md new file mode 100644 index 000000000000..180dd069263a --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA140.md @@ -0,0 +1,8 @@ +High confidence spam messages are very likely to be spam and should be quarantined to prevent them from reaching users' inboxes. Setting the high confidence spam action to "Quarantine" provides the strongest protection against spam while maintaining the ability to review and release false positives if necessary. + +**Remediation action** + +- [Configure anti-spam policies in Microsoft 365](https://learn.microsoft.com/microsoft-365/security/office-365-security/anti-spam-policies-configure) +- [Recommended settings for EOP and Microsoft Defender for Office 365 security](https://learn.microsoft.com/microsoft-365/security/office-365-security/recommended-settings-for-eop-and-office365) + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA141.md b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA141.md new file mode 100644 index 000000000000..1b07c3a2e640 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA141.md @@ -0,0 +1,8 @@ +Bulk email messages should be moved to the Junk Email folder to reduce inbox clutter while maintaining access for users who may want to receive such communications. Setting the bulk action to "Move to Junk Email Folder" or "Quarantine" helps filter marketing emails and mass mailings appropriately based on the Bulk Complaint Level (BCL) threshold. + +**Remediation action** + +- [Configure anti-spam policies in Microsoft 365](https://learn.microsoft.com/microsoft-365/security/office-365-security/anti-spam-policies-configure) +- [Bulk Complaint Level (BCL) in Exchange Online Protection](https://learn.microsoft.com/microsoft-365/security/office-365-security/anti-spam-bulk-complaint-level-bcl-about) + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA142.md b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA142.md new file mode 100644 index 000000000000..eb1d0efd8295 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA142.md @@ -0,0 +1,8 @@ +Messages identified as phishing attempts should be quarantined immediately to protect users from credential theft and malicious content. Setting the phishing spam action to "Quarantine" prevents these dangerous messages from reaching user mailboxes while allowing administrators to review and analyze them for security purposes. + +**Remediation action** + +- [Configure anti-spam policies in Microsoft 365](https://learn.microsoft.com/microsoft-365/security/office-365-security/anti-spam-policies-configure) +- [Recommended settings for EOP and Microsoft Defender for Office 365 security](https://learn.microsoft.com/microsoft-365/security/office-365-security/recommended-settings-for-eop-and-office365) + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA143.md b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA143.md new file mode 100644 index 000000000000..467eebcd10a6 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA143.md @@ -0,0 +1,8 @@ +Safety Tips provide inline warnings in email messages to help users identify suspicious or potentially dangerous content. Enabling Safety Tips displays visual indicators for spam, phishing, and spoofing attempts directly in the email client, helping users make informed decisions about email safety. + +**Remediation action** + +- [Configure anti-spam policies in Microsoft 365](https://learn.microsoft.com/microsoft-365/security/office-365-security/anti-spam-policies-configure) +- [Safety tips in email messages in Microsoft 365](https://learn.microsoft.com/microsoft-365/security/office-365-security/anti-spam-safety-tips-about) + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA156.md b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA156.md new file mode 100644 index 000000000000..85ca74b26e19 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA156.md @@ -0,0 +1,8 @@ +Safe Links should track user clicks on protected URLs to provide visibility into potential security threats and user behavior. Enabling click tracking allows administrators to monitor which users are clicking on potentially dangerous links and helps identify targeted attacks or compromised accounts. + +**Remediation action** + +- [Set up Safe Links policies in Microsoft Defender for Office 365](https://learn.microsoft.com/microsoft-365/security/office-365-security/safe-links-policies-configure) +- [Safe Links in Microsoft Defender for Office 365](https://learn.microsoft.com/microsoft-365/security/office-365-security/safe-links-about) + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA158.md b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA158.md new file mode 100644 index 000000000000..ed9aadb907e4 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA158.md @@ -0,0 +1,8 @@ +Safe Attachments protection should be enabled for SharePoint, OneDrive, and Microsoft Teams to scan files for malicious content before users can access them. This protection helps prevent malware from spreading through file sharing and collaboration platforms, providing an additional layer of security beyond email protection. + +**Remediation action** + +- [Turn on Safe Attachments for SharePoint, OneDrive, and Microsoft Teams](https://learn.microsoft.com/microsoft-365/security/office-365-security/safe-attachments-for-spo-odfb-teams-configure) +- [Safe Attachments in Microsoft Defender for Office 365](https://learn.microsoft.com/microsoft-365/security/office-365-security/safe-attachments-about) + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA179.md b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA179.md new file mode 100644 index 000000000000..b880df63edae --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA179.md @@ -0,0 +1,8 @@ +Safe Links should be enabled for internal email messages to protect against threats from compromised accounts within the organization. While external threats are the primary concern, internal accounts can be compromised and used to spread malicious links to other users in the organization. + +**Remediation action** + +- [Set up Safe Links policies in Microsoft Defender for Office 365](https://learn.microsoft.com/microsoft-365/security/office-365-security/safe-links-policies-configure) +- [Safe Links in Microsoft Defender for Office 365](https://learn.microsoft.com/microsoft-365/security/office-365-security/safe-links-about) + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA180.md b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA180.md new file mode 100644 index 000000000000..5f39f69bcc2d --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA180.md @@ -0,0 +1,8 @@ +Spoof Intelligence uses machine learning to distinguish between legitimate and malicious email spoofing attempts. Enabling Spoof Intelligence in anti-phishing policies helps protect users from impersonation attacks by analyzing sender patterns and blocking suspicious spoofed messages while allowing legitimate forwarded or mailing list messages. + +**Remediation action** + +- [Configure anti-phishing policies in Microsoft Defender for Office 365](https://learn.microsoft.com/microsoft-365/security/office-365-security/anti-phishing-policies-mdo-configure) +- [Spoof intelligence insight in EOP](https://learn.microsoft.com/microsoft-365/security/office-365-security/anti-spoofing-spoof-intelligence) + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA189.md b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA189.md new file mode 100644 index 000000000000..5b25d1b0ad4c --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA189.md @@ -0,0 +1,8 @@ +Transport rules should not be configured to bypass Safe Attachments scanning. Bypassing Safe Attachments processing creates a security gap that allows malicious files to reach users without being scanned, potentially leading to malware infections and data breaches. All email messages should go through Safe Attachments scanning to ensure comprehensive protection. + +**Remediation action** + +- [Review and modify transport rules in Exchange Online](https://learn.microsoft.com/exchange/security-and-compliance/mail-flow-rules/manage-mail-flow-rules) +- [Safe Attachments in Microsoft Defender for Office 365](https://learn.microsoft.com/microsoft-365/security/office-365-security/safe-attachments-about) + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA189_2.md b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA189_2.md new file mode 100644 index 000000000000..586b2d1ad64d --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA189_2.md @@ -0,0 +1,8 @@ +Transport rules should not be configured to bypass Safe Links protection. Bypassing Safe Links processing disables URL scanning and protection, allowing potentially malicious links to reach users without being analyzed. This creates a significant security vulnerability that attackers can exploit to deliver phishing and malware attacks. + +**Remediation action** + +- [Review and modify transport rules in Exchange Online](https://learn.microsoft.com/exchange/security-and-compliance/mail-flow-rules/manage-mail-flow-rules) +- [Safe Links in Microsoft Defender for Office 365](https://learn.microsoft.com/microsoft-365/security/office-365-security/safe-links-about) + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA205.md b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA205.md new file mode 100644 index 000000000000..e25749c79d9d --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA205.md @@ -0,0 +1,8 @@ +Common attachment type filtering automatically blocks file types commonly used for malware delivery, such as executable files, scripts, and certain document types. Enabling this feature provides an additional layer of protection by preventing dangerous file types from reaching user mailboxes, even if malware signatures are not yet detected. + +**Remediation action** + +- [Configure anti-malware policies in EOP](https://learn.microsoft.com/microsoft-365/security/office-365-security/anti-malware-policies-configure) +- [Anti-malware protection in EOP](https://learn.microsoft.com/microsoft-365/security/office-365-security/anti-malware-protection-about) + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA220.md b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA220.md new file mode 100644 index 000000000000..963b965b6e75 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA220.md @@ -0,0 +1,8 @@ +The phishing threshold level determines how aggressively anti-phishing policies identify and block phishing attempts. Setting the threshold to level 2 (Aggressive) or higher provides stronger protection against sophisticated phishing attacks by applying more stringent detection criteria, though it may increase the risk of false positives for legitimate messages. + +**Remediation action** + +- [Configure anti-phishing policies in Microsoft Defender for Office 365](https://learn.microsoft.com/microsoft-365/security/office-365-security/anti-phishing-policies-mdo-configure) +- [Recommended settings for EOP and Microsoft Defender for Office 365 security](https://learn.microsoft.com/microsoft-365/security/office-365-security/recommended-settings-for-eop-and-office365) + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA221.md b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA221.md new file mode 100644 index 000000000000..85d5fb232935 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA221.md @@ -0,0 +1,8 @@ +Mailbox intelligence uses machine learning to understand each user's email patterns and communication habits to better detect impersonation attempts. When enabled, this feature analyzes historical email data to identify when someone is attempting to impersonate a user's regular contacts, providing personalized protection against targeted phishing attacks. + +**Remediation action** + +- [Configure anti-phishing policies in Microsoft Defender for Office 365](https://learn.microsoft.com/microsoft-365/security/office-365-security/anti-phishing-policies-mdo-configure) +- [Impersonation insight in Defender for Office 365](https://learn.microsoft.com/microsoft-365/security/office-365-security/anti-phishing-mdo-impersonation-insight) + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA222.md b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA222.md new file mode 100644 index 000000000000..42c43006016e --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA222.md @@ -0,0 +1,8 @@ +Domain impersonation protection should quarantine messages that attempt to impersonate protected domains. When attackers try to spoof your organization's domain or partner domains, quarantining these messages prevents users from interacting with potentially malicious content designed to look like legitimate communication from trusted domains. + +**Remediation action** + +- [Configure anti-phishing policies in Microsoft Defender for Office 365](https://learn.microsoft.com/microsoft-365/security/office-365-security/anti-phishing-policies-mdo-configure) +- [Impersonation protection in anti-phishing policies](https://learn.microsoft.com/microsoft-365/security/office-365-security/anti-phishing-policies-about#impersonation-settings-in-anti-phishing-policies-in-microsoft-defender-for-office-365) + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA223.md b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA223.md new file mode 100644 index 000000000000..2be219b8fdf5 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA223.md @@ -0,0 +1,8 @@ +User impersonation protection should quarantine messages that attempt to impersonate protected users such as executives and other high-value targets. Quarantining these messages prevents sophisticated spear-phishing attacks that target specific individuals by impersonating their trusted contacts, colleagues, or business partners. + +**Remediation action** + +- [Configure anti-phishing policies in Microsoft Defender for Office 365](https://learn.microsoft.com/microsoft-365/security/office-365-security/anti-phishing-policies-mdo-configure) +- [Impersonation protection in anti-phishing policies](https://learn.microsoft.com/microsoft-365/security/office-365-security/anti-phishing-policies-about#impersonation-settings-in-anti-phishing-policies-in-microsoft-defender-for-office-365) + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA224.md b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA224.md new file mode 100644 index 000000000000..05fbf80bfdbe --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA224.md @@ -0,0 +1,8 @@ +Similar Users Safety Tips display warnings when messages appear to come from senders with names similar to protected users in your organization. This visual indicator helps users identify potential impersonation attempts where attackers use slightly misspelled or visually similar names to trick recipients into believing the message is from a trusted colleague. + +**Remediation action** + +- [Configure anti-phishing policies in Microsoft Defender for Office 365](https://learn.microsoft.com/microsoft-365/security/office-365-security/anti-phishing-policies-mdo-configure) +- [Safety tips in email messages in Microsoft 365](https://learn.microsoft.com/microsoft-365/security/office-365-security/anti-spam-safety-tips-about) + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA225.md b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA225.md new file mode 100644 index 000000000000..838e80444335 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA225.md @@ -0,0 +1,8 @@ +Safe Documents uses Microsoft Defender for Endpoint to scan documents opened in Office applications in Protected View. When enabled, this feature provides additional protection by analyzing Office files for malicious content before allowing users to edit them, helping prevent zero-day attacks and advanced threats delivered through documents. + +**Remediation action** + +- [Safe Documents in Microsoft 365 E5](https://learn.microsoft.com/microsoft-365/security/office-365-security/safe-documents-in-e5-plus-security-about) +- [Enable Safe Documents for Microsoft 365](https://learn.microsoft.com/microsoft-365/security/office-365-security/safe-documents-in-e5-plus-security-about#use-the-microsoft-365-defender-portal-to-configure-safe-documents) + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA226.md b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA226.md new file mode 100644 index 000000000000..29973d736834 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA226.md @@ -0,0 +1,8 @@ +Each accepted domain in your organization should be covered by a Safe Links policy to ensure all users receive URL protection. Without domain-specific Safe Links coverage, users in certain domains may not be protected from malicious links, creating security gaps that attackers can exploit to target specific business units or subsidiaries. + +**Remediation action** + +- [Set up Safe Links policies in Microsoft Defender for Office 365](https://learn.microsoft.com/microsoft-365/security/office-365-security/safe-links-policies-configure) +- [Recommended settings for EOP and Microsoft Defender for Office 365 security](https://learn.microsoft.com/microsoft-365/security/office-365-security/recommended-settings-for-eop-and-office365) + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA227.md b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA227.md new file mode 100644 index 000000000000..ad2b56ce49db --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA227.md @@ -0,0 +1,8 @@ +Each accepted domain in your organization should be covered by a Safe Attachments policy to ensure all users receive malware protection. Without domain-specific Safe Attachments coverage, users in certain domains may not be protected from malicious files, creating security vulnerabilities that can be exploited to deliver malware to unprotected segments of your organization. + +**Remediation action** + +- [Set up Safe Attachments policies in Microsoft Defender for Office 365](https://learn.microsoft.com/microsoft-365/security/office-365-security/safe-attachments-policies-configure) +- [Recommended settings for EOP and Microsoft Defender for Office 365 security](https://learn.microsoft.com/microsoft-365/security/office-365-security/recommended-settings-for-eop-and-office365) + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA228.md b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA228.md new file mode 100644 index 000000000000..cf9282fab859 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA228.md @@ -0,0 +1,8 @@ +Anti-phishing policies should not include trusted senders or exclusions, as this creates security gaps that attackers can exploit. Excluding senders from anti-phishing protection means those addresses can be spoofed without detection, allowing attackers to bypass impersonation and spoof protection by using excluded addresses in their phishing campaigns. + +**Remediation action** + +- [Configure anti-phishing policies in Microsoft Defender for Office 365](https://learn.microsoft.com/microsoft-365/security/office-365-security/anti-phishing-policies-mdo-configure) +- [Recommended settings for EOP and Microsoft Defender for Office 365 security](https://learn.microsoft.com/microsoft-365/security/office-365-security/recommended-settings-for-eop-and-office365) + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA229.md b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA229.md new file mode 100644 index 000000000000..3e85642c5d9f --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA229.md @@ -0,0 +1,8 @@ +Anti-phishing policies should not include trusted domains or exclusions, as this weakens domain impersonation protection. Excluding domains from anti-phishing checks allows attackers to register similar-looking domains or spoof excluded domains without triggering security alerts, making it easier to conduct convincing phishing attacks against your users. + +**Remediation action** + +- [Configure anti-phishing policies in Microsoft Defender for Office 365](https://learn.microsoft.com/microsoft-365/security/office-365-security/anti-phishing-policies-mdo-configure) +- [Recommended settings for EOP and Microsoft Defender for Office 365 security](https://learn.microsoft.com/microsoft-365/security/office-365-security/recommended-settings-for-eop-and-office365) + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA230.md b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA230.md new file mode 100644 index 000000000000..1c8dae777e9c --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA230.md @@ -0,0 +1,8 @@ +Each accepted domain in your organization should be covered by an anti-phishing policy to ensure all users receive protection from impersonation and spoofing attacks. Without domain-specific anti-phishing coverage, users in certain domains may be vulnerable to targeted phishing campaigns that exploit the lack of protection for their email addresses. + +**Remediation action** + +- [Configure anti-phishing policies in Microsoft Defender for Office 365](https://learn.microsoft.com/microsoft-365/security/office-365-security/anti-phishing-policies-mdo-configure) +- [Recommended settings for EOP and Microsoft Defender for Office 365 security](https://learn.microsoft.com/microsoft-365/security/office-365-security/recommended-settings-for-eop-and-office365) + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA231.md b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA231.md new file mode 100644 index 000000000000..47e7daa0084e --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA231.md @@ -0,0 +1,8 @@ +Each accepted domain in your organization should be covered by an anti-spam policy to ensure all users receive protection from unwanted and malicious email. Without domain-specific anti-spam coverage, users in certain domains may receive higher volumes of spam and potentially malicious messages, increasing security risks and reducing productivity. + +**Remediation action** + +- [Configure anti-spam policies in Microsoft 365](https://learn.microsoft.com/microsoft-365/security/office-365-security/anti-spam-policies-configure) +- [Recommended settings for EOP and Microsoft Defender for Office 365 security](https://learn.microsoft.com/microsoft-365/security/office-365-security/recommended-settings-for-eop-and-office365) + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA232.md b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA232.md new file mode 100644 index 000000000000..9a2b166214ea --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA232.md @@ -0,0 +1,8 @@ +Each accepted domain in your organization should be covered by a malware filter policy to ensure all users receive protection from malicious attachments and files. Without domain-specific malware filtering, users in certain domains may be exposed to viruses, trojans, and other malware delivered through email, creating potential entry points for security breaches. + +**Remediation action** + +- [Configure anti-malware policies in EOP](https://learn.microsoft.com/microsoft-365/security/office-365-security/anti-malware-policies-configure) +- [Recommended settings for EOP and Microsoft Defender for Office 365 security](https://learn.microsoft.com/microsoft-365/security/office-365-security/recommended-settings-for-eop-and-office365) + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA233.md b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA233.md new file mode 100644 index 000000000000..e946c14da7cf --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA233.md @@ -0,0 +1,8 @@ +Custom domains should have their MX records pointed directly at Exchange Online Protection (EOP) or use enhanced filtering with inbound connectors. This ensures that all email security features function properly, including SPF, DKIM, and DMARC validation. When mail flows through third-party services without proper configuration, important security signals can be lost. + +**Remediation action** + +- [Mail flow best practices for Exchange Online and Microsoft 365](https://learn.microsoft.com/exchange/mail-flow-best-practices/mail-flow-best-practices) +- [Enhanced filtering for connectors in Exchange Online](https://learn.microsoft.com/exchange/mail-flow-best-practices/use-connectors-to-configure-mail-flow/enhanced-filtering-for-connectors) + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA233_1.md b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA233_1.md new file mode 100644 index 000000000000..00f8a4abfbab --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA233_1.md @@ -0,0 +1,8 @@ +Enhanced filtering should be configured on inbound connectors when mail flows through third-party services before reaching Exchange Online. This feature preserves important email authentication signals and source IP information that would otherwise be lost, enabling proper SPF, DKIM, and DMARC validation as well as accurate spam and phishing detection. + +**Remediation action** + +- [Enhanced filtering for connectors in Exchange Online](https://learn.microsoft.com/exchange/mail-flow-best-practices/use-connectors-to-configure-mail-flow/enhanced-filtering-for-connectors) +- [Mail flow best practices for Exchange Online and Microsoft 365](https://learn.microsoft.com/exchange/mail-flow-best-practices/mail-flow-best-practices) + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA234.md b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA234.md new file mode 100644 index 000000000000..0db2874cead6 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA234.md @@ -0,0 +1,8 @@ +Safe Documents click-through protection should be disabled to prevent users from opening potentially dangerous files before they are fully analyzed. When click-through is enabled, users can bypass the security check and open files while they are still being scanned, potentially exposing their systems to malware before the threat is identified. + +**Remediation action** + +- [Safe Documents in Microsoft 365 E5](https://learn.microsoft.com/microsoft-365/security/office-365-security/safe-documents-in-e5-plus-security-about) +- [Enable Safe Documents for Microsoft 365](https://learn.microsoft.com/microsoft-365/security/office-365-security/safe-documents-in-e5-plus-security-about#use-the-microsoft-365-defender-portal-to-configure-safe-documents) + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA235.md b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA235.md new file mode 100644 index 000000000000..a88a52617782 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA235.md @@ -0,0 +1,8 @@ +SPF (Sender Policy Framework) records should be configured for all custom domains to specify which mail servers are authorized to send email on behalf of your domain. Properly configured SPF records help prevent email spoofing and improve email deliverability by allowing receiving servers to verify that messages claiming to be from your domain actually originate from authorized sources. + +**Remediation action** + +- [Set up SPF to help prevent spoofing](https://learn.microsoft.com/microsoft-365/security/office-365-security/email-authentication-spf-configure) +- [Email authentication in Microsoft 365](https://learn.microsoft.com/microsoft-365/security/office-365-security/email-authentication-about) + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA236.md b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA236.md new file mode 100644 index 000000000000..a07d9478c79a --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA236.md @@ -0,0 +1,8 @@ +Safe Links should be enabled for email messages to provide real-time URL scanning and protection against malicious links. When enabled, Safe Links rewrites URLs in email messages and checks them at click-time to detect and block malicious websites, protecting users from phishing attacks and malware delivery even if the link becomes malicious after the email was sent. + +**Remediation action** + +- [Set up Safe Links policies in Microsoft Defender for Office 365](https://learn.microsoft.com/microsoft-365/security/office-365-security/safe-links-policies-configure) +- [Safe Links in Microsoft Defender for Office 365](https://learn.microsoft.com/microsoft-365/security/office-365-security/safe-links-about) + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA237.md b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA237.md new file mode 100644 index 000000000000..2c25114d3dcf --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA237.md @@ -0,0 +1,8 @@ +Safe Links should be enabled for Microsoft Teams to protect users from malicious URLs shared in Teams messages and channels. Enabling this protection ensures that links in Teams conversations are scanned and blocked if they lead to malicious sites, extending the same URL protection available in email to your collaboration platform. + +**Remediation action** + +- [Set up Safe Links policies in Microsoft Defender for Office 365](https://learn.microsoft.com/microsoft-365/security/office-365-security/safe-links-policies-configure) +- [Safe Links in Microsoft Defender for Office 365](https://learn.microsoft.com/microsoft-365/security/office-365-security/safe-links-about) + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA238.md b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA238.md new file mode 100644 index 000000000000..948957dd91bd --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA238.md @@ -0,0 +1,8 @@ +Safe Links should be enabled for Office applications to protect users from malicious URLs in Office documents such as Word, Excel, and PowerPoint files. This protection extends URL scanning to documents opened in Office desktop, mobile, and web applications, helping prevent users from clicking malicious links embedded in documents. + +**Remediation action** + +- [Set up Safe Links policies in Microsoft Defender for Office 365](https://learn.microsoft.com/microsoft-365/security/office-365-security/safe-links-policies-configure) +- [Safe Links in Microsoft Defender for Office 365](https://learn.microsoft.com/microsoft-365/security/office-365-security/safe-links-about) + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA239.md b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA239.md new file mode 100644 index 000000000000..29fde2255510 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA239.md @@ -0,0 +1,9 @@ +Protection policies should not include exclusions or allow lists that bypass security controls. Adding senders or domains to exclusion lists creates security gaps that attackers can exploit to deliver malicious content without being detected. Even trusted partners can be compromised, so all email should go through security scanning. + +**Remediation action** + +- [Configure anti-phishing policies in Microsoft Defender for Office 365](https://learn.microsoft.com/microsoft-365/security/office-365-security/anti-phishing-policies-mdo-configure) +- [Configure anti-spam policies in Microsoft 365](https://learn.microsoft.com/microsoft-365/security/office-365-security/anti-spam-policies-configure) +- [Recommended settings for EOP and Microsoft Defender for Office 365 security](https://learn.microsoft.com/microsoft-365/security/office-365-security/recommended-settings-for-eop-and-office365) + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA240.md b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA240.md new file mode 100644 index 000000000000..09b5ed1ccfc7 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA240.md @@ -0,0 +1,8 @@ +External sender identification should be enabled in Outlook to help users quickly identify messages from external senders. When configured, Outlook displays a visual indicator (such as "External" tags) on messages from outside the organization, helping users exercise appropriate caution when interacting with external content and reducing the risk of phishing attacks. + +**Remediation action** + +- [External sender callouts in Outlook](https://learn.microsoft.com/microsoft-365/security/office-365-security/anti-spam-outbound-spam-about#external-sender-callouts) +- [Configure organization relationships in Exchange Online](https://learn.microsoft.com/exchange/sharing/organization-relationships/organization-relationships) + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA241.md b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA241.md new file mode 100644 index 000000000000..09875bcda233 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA241.md @@ -0,0 +1,8 @@ +First Contact Safety Tips display warnings when users receive email from a sender for the first time. This feature helps users recognize potentially suspicious messages from unfamiliar senders, providing an opportunity to verify the sender's identity before responding or clicking links, especially useful for detecting spear-phishing attacks from new contacts. + +**Remediation action** + +- [Configure anti-phishing policies in Microsoft Defender for Office 365](https://learn.microsoft.com/microsoft-365/security/office-365-security/anti-phishing-policies-mdo-configure) +- [Safety tips in email messages in Microsoft 365](https://learn.microsoft.com/microsoft-365/security/office-365-security/anti-spam-safety-tips-about) + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA242.md b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA242.md new file mode 100644 index 000000000000..19d786b500da --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA242.md @@ -0,0 +1,8 @@ +Security alert policies should be configured to notify administrators of important protection events and potential security incidents. Alerts for events such as malware campaigns, suspicious email patterns, compromised accounts, and policy violations help security teams respond quickly to threats and maintain visibility into the effectiveness of email security controls. + +**Remediation action** + +- [Alert policies in the Microsoft 365 compliance center](https://learn.microsoft.com/microsoft-365/compliance/alert-policies) +- [Get started with alert policies](https://learn.microsoft.com/purview/alert-policies) + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA243.md b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA243.md new file mode 100644 index 000000000000..b24f602a85fd --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA243.md @@ -0,0 +1,8 @@ +Inbound connectors for non-authoritative domains should be configured with proper authentication to maintain the integrity of email security checks. When email flows through third-party services, connectors should specify authenticated source IPs or certificates to ensure that Exchange Online can properly validate sender information and apply appropriate security policies. + +**Remediation action** + +- [Configure mail flow using connectors in Exchange Online](https://learn.microsoft.com/exchange/mail-flow-best-practices/use-connectors-to-configure-mail-flow/use-connectors-to-configure-mail-flow) +- [Enhanced filtering for connectors in Exchange Online](https://learn.microsoft.com/exchange/mail-flow-best-practices/use-connectors-to-configure-mail-flow/enhanced-filtering-for-connectors) + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA244.md b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA244.md new file mode 100644 index 000000000000..51ed9d611bea --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA244.md @@ -0,0 +1,8 @@ +Anti-spam policies should honor DMARC (Domain-based Message Authentication, Reporting, and Conformance) policies published by sending domains. When enabled, this setting respects the sender domain's DMARC policy for handling authentication failures, improving email security by enforcing the sender's preferred treatment of spoofed or forged messages. + +**Remediation action** + +- [Configure anti-spam policies in Microsoft 365](https://learn.microsoft.com/microsoft-365/security/office-365-security/anti-spam-policies-configure) +- [Use DMARC to validate email in Microsoft 365](https://learn.microsoft.com/microsoft-365/security/office-365-security/email-authentication-dmarc-configure) + +%TestResult% From a68c883d3f11ad1d842be0d065739a21594c75a2 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Sun, 11 Jan 2026 17:38:10 +0100 Subject: [PATCH 114/503] added MD files --- .../EIDSCA/Identity/Invoke-CippTestEIDSCA_AF01.md | 13 +++++++++++++ .../EIDSCA/Identity/Invoke-CippTestEIDSCA_AF02.md | 13 +++++++++++++ .../EIDSCA/Identity/Invoke-CippTestEIDSCA_AF03.md | 13 +++++++++++++ .../EIDSCA/Identity/Invoke-CippTestEIDSCA_AF04.md | 13 +++++++++++++ .../EIDSCA/Identity/Invoke-CippTestEIDSCA_AF05.md | 13 +++++++++++++ .../EIDSCA/Identity/Invoke-CippTestEIDSCA_AF06.md | 13 +++++++++++++ .../EIDSCA/Identity/Invoke-CippTestEIDSCA_AG01.md | 13 +++++++++++++ .../EIDSCA/Identity/Invoke-CippTestEIDSCA_AG02.md | 13 +++++++++++++ .../EIDSCA/Identity/Invoke-CippTestEIDSCA_AG03.md | 13 +++++++++++++ .../EIDSCA/Identity/Invoke-CippTestEIDSCA_AM01.md | 13 +++++++++++++ .../EIDSCA/Identity/Invoke-CippTestEIDSCA_AM02.md | 13 +++++++++++++ .../EIDSCA/Identity/Invoke-CippTestEIDSCA_AM03.md | 13 +++++++++++++ .../EIDSCA/Identity/Invoke-CippTestEIDSCA_AM04.md | 13 +++++++++++++ .../EIDSCA/Identity/Invoke-CippTestEIDSCA_AM06.md | 13 +++++++++++++ .../EIDSCA/Identity/Invoke-CippTestEIDSCA_AM07.md | 13 +++++++++++++ .../EIDSCA/Identity/Invoke-CippTestEIDSCA_AM09.md | 13 +++++++++++++ .../EIDSCA/Identity/Invoke-CippTestEIDSCA_AM10.md | 13 +++++++++++++ .../EIDSCA/Identity/Invoke-CippTestEIDSCA_AP01.md | 13 +++++++++++++ .../EIDSCA/Identity/Invoke-CippTestEIDSCA_AP04.md | 13 +++++++++++++ .../EIDSCA/Identity/Invoke-CippTestEIDSCA_AP05.md | 13 +++++++++++++ .../EIDSCA/Identity/Invoke-CippTestEIDSCA_AP06.md | 13 +++++++++++++ .../EIDSCA/Identity/Invoke-CippTestEIDSCA_AP07.md | 13 +++++++++++++ .../EIDSCA/Identity/Invoke-CippTestEIDSCA_AP08.md | 13 +++++++++++++ .../EIDSCA/Identity/Invoke-CippTestEIDSCA_AP09.md | 13 +++++++++++++ .../EIDSCA/Identity/Invoke-CippTestEIDSCA_AP10.md | 13 +++++++++++++ .../EIDSCA/Identity/Invoke-CippTestEIDSCA_AP14.md | 13 +++++++++++++ .../EIDSCA/Identity/Invoke-CippTestEIDSCA_AS04.md | 13 +++++++++++++ .../EIDSCA/Identity/Invoke-CippTestEIDSCA_AT01.md | 13 +++++++++++++ .../EIDSCA/Identity/Invoke-CippTestEIDSCA_AT02.md | 13 +++++++++++++ .../EIDSCA/Identity/Invoke-CippTestEIDSCA_AV01.md | 13 +++++++++++++ .../EIDSCA/Identity/Invoke-CippTestEIDSCA_CP01.md | 13 +++++++++++++ .../EIDSCA/Identity/Invoke-CippTestEIDSCA_CP03.md | 13 +++++++++++++ .../EIDSCA/Identity/Invoke-CippTestEIDSCA_CP04.md | 13 +++++++++++++ .../EIDSCA/Identity/Invoke-CippTestEIDSCA_CR01.md | 13 +++++++++++++ .../EIDSCA/Identity/Invoke-CippTestEIDSCA_CR02.md | 13 +++++++++++++ .../EIDSCA/Identity/Invoke-CippTestEIDSCA_CR03.md | 13 +++++++++++++ .../EIDSCA/Identity/Invoke-CippTestEIDSCA_CR04.md | 13 +++++++++++++ .../EIDSCA/Identity/Invoke-CippTestEIDSCA_PR01.md | 13 +++++++++++++ .../EIDSCA/Identity/Invoke-CippTestEIDSCA_PR02.md | 13 +++++++++++++ .../EIDSCA/Identity/Invoke-CippTestEIDSCA_PR03.md | 13 +++++++++++++ .../EIDSCA/Identity/Invoke-CippTestEIDSCA_PR05.md | 13 +++++++++++++ .../EIDSCA/Identity/Invoke-CippTestEIDSCA_PR06.md | 13 +++++++++++++ .../EIDSCA/Identity/Invoke-CippTestEIDSCA_ST08.md | 13 +++++++++++++ .../EIDSCA/Identity/Invoke-CippTestEIDSCA_ST09.md | 13 +++++++++++++ 44 files changed, 572 insertions(+) create mode 100644 Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF01.md create mode 100644 Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF02.md create mode 100644 Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF03.md create mode 100644 Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF04.md create mode 100644 Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF05.md create mode 100644 Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF06.md create mode 100644 Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AG01.md create mode 100644 Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AG02.md create mode 100644 Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AG03.md create mode 100644 Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM01.md create mode 100644 Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM02.md create mode 100644 Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM03.md create mode 100644 Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM04.md create mode 100644 Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM06.md create mode 100644 Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM07.md create mode 100644 Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM09.md create mode 100644 Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM10.md create mode 100644 Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP01.md create mode 100644 Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP04.md create mode 100644 Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP05.md create mode 100644 Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP06.md create mode 100644 Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP07.md create mode 100644 Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP08.md create mode 100644 Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP09.md create mode 100644 Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP10.md create mode 100644 Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP14.md create mode 100644 Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AS04.md create mode 100644 Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AT01.md create mode 100644 Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AT02.md create mode 100644 Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AV01.md create mode 100644 Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CP01.md create mode 100644 Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CP03.md create mode 100644 Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CP04.md create mode 100644 Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR01.md create mode 100644 Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR02.md create mode 100644 Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR03.md create mode 100644 Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR04.md create mode 100644 Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR01.md create mode 100644 Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR02.md create mode 100644 Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR03.md create mode 100644 Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR05.md create mode 100644 Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR06.md create mode 100644 Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_ST08.md create mode 100644 Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_ST09.md diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF01.md b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF01.md new file mode 100644 index 000000000000..2b4b263f7f43 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF01.md @@ -0,0 +1,13 @@ +# FIDO2 - State + +FIDO2 security keys should be enabled as an authentication method to provide users with the strongest, most phishing-resistant form of authentication available. FIDO2 security keys use public key cryptography and are resistant to phishing, man-in-the-middle attacks, and password database breaches. + +Enabling FIDO2 security keys is a critical step toward passwordless authentication and provides the highest level of security for protecting user accounts, particularly for administrators and high-value targets. + +**Remediation action** +- [Enable passwordless security key sign-in](https://learn.microsoft.com/entra/identity/authentication/howto-authentication-passwordless-security-key) +- [FIDO2 security key authentication method](https://learn.microsoft.com/entra/identity/authentication/concept-authentication-passwordless) +- [Plan a passwordless authentication deployment](https://learn.microsoft.com/entra/identity/authentication/howto-authentication-passwordless-deployment) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF02.md b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF02.md new file mode 100644 index 000000000000..fce0ae0868f6 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF02.md @@ -0,0 +1,13 @@ +# FIDO2 - Self-Service + +Self-service registration for FIDO2 security keys should be enabled to allow users to register their own security keys without requiring administrator intervention. This improves user experience and accelerates the adoption of passwordless authentication while maintaining security. + +Enabling self-service registration empowers users to take control of their authentication security and reduces the administrative burden of manually provisioning security keys for users. + +**Remediation action** +- [Enable passwordless security key sign-in](https://learn.microsoft.com/entra/identity/authentication/howto-authentication-passwordless-security-key) +- [FIDO2 security key authentication method](https://learn.microsoft.com/entra/identity/authentication/concept-authentication-passwordless) +- [Manage authentication methods](https://learn.microsoft.com/entra/identity/authentication/concept-authentication-methods-manage) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF03.md b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF03.md new file mode 100644 index 000000000000..a6747db56708 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF03.md @@ -0,0 +1,13 @@ +# FIDO2 - Attestation + +FIDO2 attestation enforcement should be configured to ensure that only security keys from trusted manufacturers can be registered. Attestation allows Microsoft Entra ID to verify the authenticity and characteristics of FIDO2 security keys during registration, helping prevent the use of compromised or counterfeit devices. + +Enforcing attestation provides an additional layer of security by ensuring that only verified, legitimate FIDO2 security keys are used for authentication in your environment. + +**Remediation action** +- [Enable passwordless security key sign-in](https://learn.microsoft.com/entra/identity/authentication/howto-authentication-passwordless-security-key) +- [FIDO2 security key authentication method](https://learn.microsoft.com/entra/identity/authentication/concept-authentication-passwordless) +- [Manage authentication methods](https://learn.microsoft.com/entra/identity/authentication/concept-authentication-methods-manage) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF04.md b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF04.md new file mode 100644 index 000000000000..7e4573cba30b --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF04.md @@ -0,0 +1,13 @@ +# FIDO2 - Key Restrictions + +FIDO2 key restrictions should be configured to control which security key models and manufacturers are allowed in your environment. Key restrictions help ensure that only approved, tested security keys that meet your organization's security requirements can be used for authentication. + +Organizations can use key restrictions to enforce specific security requirements such as requiring keys with certain certification levels or from approved vendors. + +**Remediation action** +- [Enable passwordless security key sign-in](https://learn.microsoft.com/entra/identity/authentication/howto-authentication-passwordless-security-key) +- [FIDO2 security key authentication method](https://learn.microsoft.com/entra/identity/authentication/concept-authentication-passwordless) +- [Manage FIDO2 security key restrictions](https://learn.microsoft.com/entra/identity/authentication/howto-authentication-passwordless-security-key#key-restrictions) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF05.md b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF05.md new file mode 100644 index 000000000000..63735f222473 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF05.md @@ -0,0 +1,13 @@ +# FIDO2 - Restricted Keys + +FIDO2 security key restrictions should be properly configured to define which specific keys are allowed or restricted in your environment. Organizations may choose to enforce key restrictions to ensure standardization, maintain approved vendor lists, or block specific key models that don't meet security requirements. + +Properly configured key restrictions help maintain consistent security standards across your FIDO2 security key deployment. + +**Remediation action** +- [Enable passwordless security key sign-in](https://learn.microsoft.com/entra/identity/authentication/howto-authentication-passwordless-security-key) +- [FIDO2 security key authentication method](https://learn.microsoft.com/entra/identity/authentication/concept-authentication-passwordless) +- [Manage FIDO2 security key restrictions](https://learn.microsoft.com/entra/identity/authentication/howto-authentication-passwordless-security-key#key-restrictions) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF06.md b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF06.md new file mode 100644 index 000000000000..a0b269b24c97 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF06.md @@ -0,0 +1,13 @@ +# FIDO2 - Specific Keys + +FIDO2 specific key configurations should be reviewed to ensure that key restrictions align with your organization's security policies. Organizations may choose to allow all FIDO2-certified keys or restrict to specific models based on security requirements, user population, or procurement standards. + +The configuration of specific key allowances or restrictions should be based on a thorough assessment of your organization's security needs and risk tolerance. + +**Remediation action** +- [Enable passwordless security key sign-in](https://learn.microsoft.com/entra/identity/authentication/howto-authentication-passwordless-security-key) +- [FIDO2 security key authentication method](https://learn.microsoft.com/entra/identity/authentication/concept-authentication-passwordless) +- [Manage FIDO2 security key restrictions](https://learn.microsoft.com/entra/identity/authentication/howto-authentication-passwordless-security-key#key-restrictions) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AG01.md b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AG01.md new file mode 100644 index 000000000000..05b98d3029e7 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AG01.md @@ -0,0 +1,13 @@ +# Authentication Methods - Policy Migration + +The authentication methods policy migration should be completed to use the modern authentication methods framework in Microsoft Entra ID. The modern policy provides enhanced features, better security controls, and improved management capabilities compared to legacy multi-factor authentication settings. + +Completing the migration ensures you can take advantage of new authentication features such as FIDO2 security keys, certificate-based authentication, and other modern authentication methods that are only available in the new policy framework. + +**Remediation action** +- [Migrate to the authentication methods policy for Microsoft Entra multifactor authentication and SSPR](https://learn.microsoft.com/entra/identity/authentication/how-to-authentication-methods-manage) +- [Authentication methods in Microsoft Entra ID](https://learn.microsoft.com/entra/identity/authentication/concept-authentication-methods) +- [Manage authentication methods](https://learn.microsoft.com/entra/identity/authentication/concept-authentication-methods-manage) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AG02.md b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AG02.md new file mode 100644 index 000000000000..a2412615831b --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AG02.md @@ -0,0 +1,13 @@ +# Authentication Methods - Report Suspicious Activity + +Users should be enabled to report suspicious multifactor authentication prompts they did not initiate. This feature allows users to report fraudulent MFA attempts directly from the Microsoft Authenticator app or via phone call, which helps detect and prevent unauthorized access attempts and credential compromise. + +When users report suspicious activity, Microsoft Entra ID can take automated actions such as blocking the user's account, requiring a password reset, or triggering security alerts for administrators to investigate potential compromises. + +**Remediation action** +- [Enable fraud alerts for Microsoft Entra multifactor authentication](https://learn.microsoft.com/entra/identity/authentication/howto-mfa-mfasettings#fraud-alert) +- [Security defaults in Microsoft Entra ID](https://learn.microsoft.com/entra/fundamentals/security-defaults) +- [What is Identity Protection in Microsoft Entra ID?](https://learn.microsoft.com/entra/id-protection/overview-identity-protection) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AG03.md b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AG03.md new file mode 100644 index 000000000000..0257a5905db5 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AG03.md @@ -0,0 +1,13 @@ +# Authentication Methods - Suspicious Activity Target + +The report suspicious activity feature should target all users or specific groups to ensure comprehensive protection against unauthorized MFA attempts. Properly configuring which users can report suspicious activity ensures that security protections are applied consistently across your organization. + +Organizations should enable this feature for all users who use Microsoft Entra multifactor authentication to maximize the effectiveness of threat detection and response capabilities. + +**Remediation action** +- [Enable fraud alerts for Microsoft Entra multifactor authentication](https://learn.microsoft.com/entra/identity/authentication/howto-mfa-mfasettings#fraud-alert) +- [Authentication methods in Microsoft Entra ID](https://learn.microsoft.com/entra/identity/authentication/concept-authentication-methods) +- [Manage authentication methods](https://learn.microsoft.com/entra/identity/authentication/concept-authentication-methods-manage) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM01.md b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM01.md new file mode 100644 index 000000000000..75f687dea32f --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM01.md @@ -0,0 +1,13 @@ +# MS Authenticator - State + +Microsoft Authenticator should be enabled as an authentication method to provide users with a secure, phishing-resistant way to verify their identity. The Microsoft Authenticator app supports passwordless phone sign-in, push notifications for MFA, and time-based one-time passwords (TOTP). + +Enabling Microsoft Authenticator is a fundamental component of a strong authentication strategy and supports your organization's journey toward passwordless authentication. + +**Remediation action** +- [Enable passwordless security key sign-in](https://learn.microsoft.com/entra/identity/authentication/howto-authentication-passwordless-security-key) +- [Microsoft Authenticator authentication method](https://learn.microsoft.com/entra/identity/authentication/concept-authentication-authenticator-app) +- [Plan a passwordless authentication deployment](https://learn.microsoft.com/entra/identity/authentication/howto-authentication-passwordless-deployment) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM02.md b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM02.md new file mode 100644 index 000000000000..467cc16c7ce4 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM02.md @@ -0,0 +1,13 @@ +# MS Authenticator - OTP Disabled + +Software OATH tokens (time-based one-time passwords) in Microsoft Authenticator should be disabled in favor of push notifications, which provide stronger security and better user experience. Push notifications include additional context about the authentication request and are more resistant to phishing attacks compared to OTP codes that can be phished. + +Disabling OTP while keeping push notifications enabled encourages users to adopt the more secure authentication method while maintaining strong MFA protection. + +**Remediation action** +- [Microsoft Authenticator authentication method](https://learn.microsoft.com/entra/identity/authentication/concept-authentication-authenticator-app) +- [Authentication methods in Microsoft Entra ID](https://learn.microsoft.com/entra/identity/authentication/concept-authentication-methods) +- [Plan a passwordless authentication deployment](https://learn.microsoft.com/entra/identity/authentication/howto-authentication-passwordless-deployment) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM03.md b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM03.md new file mode 100644 index 000000000000..97cb1bfa6252 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM03.md @@ -0,0 +1,13 @@ +# MS Authenticator - Number Matching + +Number matching should be enabled for Microsoft Authenticator push notifications to provide strong protection against MFA fatigue attacks and push notification bombing. With number matching, users must enter a number displayed on their sign-in screen into the Authenticator app, preventing them from accidentally approving fraudulent authentication requests. + +This feature significantly reduces the risk of users approving MFA prompts without proper verification, which is a common technique used by attackers in credential compromise scenarios. + +**Remediation action** +- [How to use number matching in multifactor authentication (MFA) notifications](https://learn.microsoft.com/entra/identity/authentication/how-to-mfa-number-match) +- [Microsoft Authenticator authentication method](https://learn.microsoft.com/entra/identity/authentication/concept-authentication-authenticator-app) +- [System-preferred multifactor authentication](https://learn.microsoft.com/entra/identity/authentication/concept-system-preferred-multifactor-authentication) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM04.md b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM04.md new file mode 100644 index 000000000000..588342c08f87 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM04.md @@ -0,0 +1,13 @@ +# MS Authenticator - Number Matching Target + +Number matching should be configured to target all users or appropriate user groups to ensure consistent security protections across your organization. Selective enablement may be appropriate during initial rollout phases, but the ultimate goal should be to enable number matching for all users performing MFA. + +Proper targeting configuration ensures that security improvements are applied consistently and that all users benefit from enhanced protection against MFA attacks. + +**Remediation action** +- [How to use number matching in multifactor authentication (MFA) notifications](https://learn.microsoft.com/entra/identity/authentication/how-to-mfa-number-match) +- [Authentication methods in Microsoft Entra ID](https://learn.microsoft.com/entra/identity/authentication/concept-authentication-methods) +- [Manage authentication methods](https://learn.microsoft.com/entra/identity/authentication/concept-authentication-methods-manage) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM06.md b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM06.md new file mode 100644 index 000000000000..c2a7016d510e --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM06.md @@ -0,0 +1,13 @@ +# MS Authenticator - Show App Name + +Application name should be displayed in Microsoft Authenticator push notifications to provide additional context that helps users identify legitimate authentication requests. Showing the app name allows users to verify that the authentication request is for the expected application, reducing the risk of approving fraudulent requests. + +This additional context is part of a defense-in-depth strategy that makes it harder for attackers to trick users into approving unauthorized authentication attempts. + +**Remediation action** +- [Microsoft Authenticator authentication method](https://learn.microsoft.com/entra/identity/authentication/concept-authentication-authenticator-app) +- [How to use additional context in Microsoft Authenticator notifications](https://learn.microsoft.com/entra/identity/authentication/how-to-mfa-additional-context) +- [Authentication methods in Microsoft Entra ID](https://learn.microsoft.com/entra/identity/authentication/concept-authentication-methods) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM07.md b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM07.md new file mode 100644 index 000000000000..9d0853f77580 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM07.md @@ -0,0 +1,13 @@ +# MS Authenticator - Show App Name Target + +Application name display should be configured to target appropriate user groups to ensure consistent security context is provided across your organization. Proper targeting ensures that all users receive additional context in their authentication prompts to help them make informed decisions about approving or denying authentication requests. + +Organizations should enable this feature for all users performing MFA to maximize security benefits. + +**Remediation action** +- [How to use additional context in Microsoft Authenticator notifications](https://learn.microsoft.com/entra/identity/authentication/how-to-mfa-additional-context) +- [Microsoft Authenticator authentication method](https://learn.microsoft.com/entra/identity/authentication/concept-authentication-authenticator-app) +- [Manage authentication methods](https://learn.microsoft.com/entra/identity/authentication/concept-authentication-methods-manage) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM09.md b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM09.md new file mode 100644 index 000000000000..61680d77f924 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM09.md @@ -0,0 +1,13 @@ +# MS Authenticator - Show Location + +Geographic location information should be displayed in Microsoft Authenticator push notifications to help users identify potentially suspicious authentication requests from unexpected locations. This additional context allows users to quickly recognize when an authentication attempt is coming from a location that doesn't match their current whereabouts. + +Location information is particularly useful for detecting credential compromise when attackers attempt to authenticate from different geographic regions. + +**Remediation action** +- [How to use additional context in Microsoft Authenticator notifications](https://learn.microsoft.com/entra/identity/authentication/how-to-mfa-additional-context) +- [Microsoft Authenticator authentication method](https://learn.microsoft.com/entra/identity/authentication/concept-authentication-authenticator-app) +- [Authentication methods in Microsoft Entra ID](https://learn.microsoft.com/entra/identity/authentication/concept-authentication-methods) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM10.md b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM10.md new file mode 100644 index 000000000000..53ac29618499 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM10.md @@ -0,0 +1,13 @@ +# MS Authenticator - Show Location Target + +Geographic location display should be configured to target appropriate user groups to ensure users receive location context in their authentication prompts. Proper targeting ensures security protections are applied consistently across your organization. + +Organizations should enable location display for all users performing MFA to maximize the security benefits and help users quickly identify suspicious authentication attempts. + +**Remediation action** +- [How to use additional context in Microsoft Authenticator notifications](https://learn.microsoft.com/entra/identity/authentication/how-to-mfa-additional-context) +- [Microsoft Authenticator authentication method](https://learn.microsoft.com/entra/identity/authentication/concept-authentication-authenticator-app) +- [Manage authentication methods](https://learn.microsoft.com/entra/identity/authentication/concept-authentication-methods-manage) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP01.md b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP01.md new file mode 100644 index 000000000000..5dfb019689e7 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP01.md @@ -0,0 +1,13 @@ +# Authorization Policy - Self-Service Password Reset for Admins + +Administrators should not be allowed to use self-service password reset (SSPR) for enhanced security. Admin accounts require more stringent security controls and should follow formal password reset procedures that involve additional verification steps rather than self-service options. This ensures that administrative account password resets are properly audited and controlled. + +Allowing administrators to use SSPR increases the risk of account compromise, as SSPR methods may be vulnerable to social engineering or other attacks. Administrative accounts have elevated privileges and require the highest level of security protection. + +**Remediation action** +- [Manage user settings for Microsoft Entra multifactor authentication](https://learn.microsoft.com/entra/identity/authentication/howto-mfa-userdevicesettings) +- [Plan a Microsoft Entra self-service password reset deployment](https://learn.microsoft.com/entra/identity/authentication/howto-sspr-deployment) +- [Microsoft Entra built-in roles](https://learn.microsoft.com/entra/identity/role-based-access-control/permissions-reference) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP04.md b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP04.md new file mode 100644 index 000000000000..2a2a26bcc33a --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP04.md @@ -0,0 +1,13 @@ +# Authorization Policy - Guest Invite Restrictions + +Guest invite permissions should be restricted to administrators and designated guest inviters only to maintain proper control over external access to your Microsoft Entra ID tenant. Limiting who can invite guests helps prevent unauthorized external users from accessing organizational resources and reduces the attack surface. + +When all users can invite guests, there is increased risk of data exposure and security incidents, as employees may inadvertently grant access to malicious actors or share sensitive information with unauthorized external parties. + +**Remediation action** +- [Configure external collaboration settings](https://learn.microsoft.com/entra/external-id/external-collaboration-settings-configure) +- [Properties of a Microsoft Entra B2B collaboration user](https://learn.microsoft.com/entra/external-id/user-properties) +- [Authorization policies in Microsoft Entra ID](https://learn.microsoft.com/graph/api/resources/authorizationpolicy) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP05.md b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP05.md new file mode 100644 index 000000000000..58ff64d840a6 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP05.md @@ -0,0 +1,13 @@ +# Authorization Policy - Email-Based Subscription Sign-up + +Users should not be allowed to sign up for email-based subscriptions that may create security risks or lead to unauthorized service usage. Disabling this feature prevents users from self-provisioning trial subscriptions or services that may not comply with organizational policies or security requirements. + +Allowing unrestricted subscription sign-up can lead to shadow IT, data sprawl, and increased security risks as users may store organizational data in unapproved services. + +**Remediation action** +- [What is self-service sign-up for Microsoft Entra ID?](https://learn.microsoft.com/entra/identity/users/directory-self-service-signup) +- [Authorization policies in Microsoft Entra ID](https://learn.microsoft.com/graph/api/resources/authorizationpolicy) +- [Manage app and resource access using Microsoft Entra groups](https://learn.microsoft.com/entra/identity/users/groups-self-service-management) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP06.md b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP06.md new file mode 100644 index 000000000000..734390ded497 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP06.md @@ -0,0 +1,13 @@ +# Authorization Policy - Email Validation Join + +Users should not be allowed to join the tenant using email validation without proper administrative approval. This setting prevents unauthorized users from creating accounts in your tenant simply by validating an email address, which could lead to security breaches and unauthorized access. + +Requiring administrative control over tenant joins ensures that all user accounts are properly vetted and approved before gaining access to organizational resources. + +**Remediation action** +- [What is self-service sign-up for Microsoft Entra ID?](https://learn.microsoft.com/entra/identity/users/directory-self-service-signup) +- [Authorization policies in Microsoft Entra ID](https://learn.microsoft.com/graph/api/resources/authorizationpolicy) +- [Manage external access in Microsoft Entra ID](https://learn.microsoft.com/entra/external-id/external-collaboration-settings-configure) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP07.md b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP07.md new file mode 100644 index 000000000000..0e8db3045137 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP07.md @@ -0,0 +1,13 @@ +# Authorization Policy - Guest User Access + +Guest user access should be restricted to limit what information external users can view and interact with in your tenant. The most restrictive setting ensures that guest users can only access their own profile information and cannot enumerate other users, groups, or organizational resources. + +Unrestricted guest access increases the risk of information disclosure and reconnaissance activities by external parties who may use directory information for targeted attacks. + +**Remediation action** +- [Configure external collaboration settings](https://learn.microsoft.com/entra/external-id/external-collaboration-settings-configure) +- [Properties of a Microsoft Entra B2B collaboration user](https://learn.microsoft.com/entra/external-id/user-properties) +- [Restrict guest access permissions in Microsoft Entra ID](https://learn.microsoft.com/entra/identity/users/users-restrict-guest-permissions) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP08.md b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP08.md new file mode 100644 index 000000000000..41f3fa9eaa81 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP08.md @@ -0,0 +1,13 @@ +# Authorization Policy - User Consent Policy + +User consent for applications should be limited to low-risk, publisher-verified applications only, or disabled entirely to prevent unauthorized data access. Unrestricted user consent allows users to grant applications access to organizational data without proper security review, potentially exposing sensitive information to malicious applications. + +Implementing a restrictive consent policy ensures that only trusted applications can access organizational resources and that high-risk permissions require administrator approval. + +**Remediation action** +- [Configure how users consent to applications](https://learn.microsoft.com/entra/identity/enterprise-apps/configure-user-consent) +- [Manage app consent policies](https://learn.microsoft.com/entra/identity/enterprise-apps/manage-app-consent-policies) +- [Grant tenant-wide admin consent to an application](https://learn.microsoft.com/entra/identity/enterprise-apps/grant-admin-consent) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP09.md b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP09.md new file mode 100644 index 000000000000..356e7e34f6e7 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP09.md @@ -0,0 +1,13 @@ +# Authorization Policy - Consent for Risky Apps + +User consent for risky applications should be blocked to prevent users from granting dangerous permissions to untrusted applications. Risky apps may request excessive permissions or exhibit suspicious behavior that could compromise organizational security. + +Microsoft Entra ID can assess application risk based on various factors, and blocking consent for risky apps provides an additional layer of protection against malicious applications. + +**Remediation action** +- [Configure how users consent to applications](https://learn.microsoft.com/entra/identity/enterprise-apps/configure-user-consent) +- [Manage app consent policies](https://learn.microsoft.com/entra/identity/enterprise-apps/manage-app-consent-policies) +- [Configure the admin consent workflow](https://learn.microsoft.com/entra/identity/enterprise-apps/configure-admin-consent-workflow) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP10.md b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP10.md new file mode 100644 index 000000000000..2af59acc7c38 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP10.md @@ -0,0 +1,13 @@ +# Authorization Policy - Users Can Create Apps + +Regular users should not be allowed to register applications in Microsoft Entra ID. Application registration should be restricted to authorized administrators who can properly assess security implications and configure applications according to organizational policies. + +Allowing unrestricted app registration can lead to shadow IT, misconfigured applications, and potential security vulnerabilities as users may inadvertently create applications with excessive permissions or improper security settings. + +**Remediation action** +- [Restrict who can create applications](https://learn.microsoft.com/entra/identity/role-based-access-control/delegate-app-roles#restrict-who-can-create-applications) +- [Application and service principal objects in Microsoft Entra ID](https://learn.microsoft.com/entra/identity-platform/app-objects-and-service-principals) +- [Authorization policies in Microsoft Entra ID](https://learn.microsoft.com/graph/api/resources/authorizationpolicy) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP14.md b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP14.md new file mode 100644 index 000000000000..0f5105117fa6 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP14.md @@ -0,0 +1,13 @@ +# Authorization Policy - Users Can Read Other Users + +Users should have the ability to read other users' basic profile information to enable collaboration and communication within the organization. However, this should be balanced with privacy and security considerations. This setting controls whether users can discover and view information about other users in the directory. + +Completely restricting user discovery may impact collaboration, while allowing full access enables normal business operations such as looking up colleagues, viewing organizational charts, and facilitating communication. + +**Remediation action** +- [Authorization policies in Microsoft Entra ID](https://learn.microsoft.com/graph/api/resources/authorizationpolicy) +- [Restrict member users default permissions](https://learn.microsoft.com/entra/fundamentals/users-default-permissions#restrict-member-users-default-permissions) +- [What are default user permissions in Microsoft Entra ID?](https://learn.microsoft.com/entra/fundamentals/users-default-permissions) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AS04.md b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AS04.md new file mode 100644 index 000000000000..fedf9dd8d1e1 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AS04.md @@ -0,0 +1,13 @@ +# SMS - No Sign-In + +SMS should not be allowed as a primary authentication method (sign-in), though it may be used for multi-factor authentication verification. SMS is vulnerable to SIM swap attacks and interception, making it unsuitable as a standalone authentication factor. Organizations should enforce stronger authentication methods for sign-in while potentially allowing SMS only as a second factor. + +This configuration prevents users from signing in with SMS alone, which provides better security than allowing SMS-based authentication while still permitting SMS as an MFA option where appropriate. + +**Remediation action** +- [Authentication methods in Microsoft Entra ID](https://learn.microsoft.com/entra/identity/authentication/concept-authentication-methods) +- [SMS-based authentication in Microsoft Entra ID](https://learn.microsoft.com/entra/identity/authentication/concept-authentication-phone-options) +- [Plan a passwordless authentication deployment](https://learn.microsoft.com/entra/identity/authentication/howto-authentication-passwordless-deployment) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AT01.md b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AT01.md new file mode 100644 index 000000000000..9ee13e31e76e --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AT01.md @@ -0,0 +1,13 @@ +# Temp Access Pass - State + +Temporary Access Pass (TAP) should be enabled to facilitate secure onboarding of passwordless authentication methods. TAP provides a time-limited passcode that can be used once or multiple times to register strong authentication methods such as FIDO2 security keys or set up the Microsoft Authenticator app. + +Enabling TAP is particularly important for passwordless authentication rollouts, as it allows administrators to securely bootstrap users into passwordless methods without relying on traditional passwords or less secure alternatives. + +**Remediation action** +- [Enable and configure Temporary Access Pass](https://learn.microsoft.com/entra/identity/authentication/howto-authentication-temporary-access-pass) +- [Temporary Access Pass authentication method](https://learn.microsoft.com/entra/identity/authentication/concept-authentication-temporary-access-pass) +- [Plan a passwordless authentication deployment](https://learn.microsoft.com/entra/identity/authentication/howto-authentication-passwordless-deployment) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AT02.md b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AT02.md new file mode 100644 index 000000000000..cebafc0606cd --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AT02.md @@ -0,0 +1,13 @@ +# Temp Access Pass - One-Time + +Temporary Access Pass should be configured with appropriate usage limits. Setting TAP to one-time use provides the highest security by ensuring the passcode cannot be reused after initial authentication. However, organizations may choose to allow multiple uses based on specific use cases such as registering multiple authentication methods. + +The configuration should balance security requirements with the specific use cases for TAP in your organization, such as user onboarding workflows or authentication method recovery scenarios. + +**Remediation action** +- [Enable and configure Temporary Access Pass](https://learn.microsoft.com/entra/identity/authentication/howto-authentication-temporary-access-pass) +- [Temporary Access Pass authentication method](https://learn.microsoft.com/entra/identity/authentication/concept-authentication-temporary-access-pass) +- [Configure Temporary Access Pass properties](https://learn.microsoft.com/entra/identity/authentication/howto-authentication-temporary-access-pass#configure-temporary-access-pass-settings) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AV01.md b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AV01.md new file mode 100644 index 000000000000..6a462df66c70 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AV01.md @@ -0,0 +1,13 @@ +# Voice Call - Disabled + +Voice call authentication should be disabled as it is vulnerable to social engineering attacks and SIM swap attacks. Voice calls are less secure than modern authentication methods and should be replaced with more secure alternatives such as the Microsoft Authenticator app or FIDO2 security keys. + +Attackers can intercept phone calls through SIM swapping or social engineering telecom providers, making voice calls an unreliable authentication factor. Organizations should migrate users to stronger authentication methods. + +**Remediation action** +- [Authentication methods in Microsoft Entra ID](https://learn.microsoft.com/entra/identity/authentication/concept-authentication-methods) +- [Plan a passwordless authentication deployment](https://learn.microsoft.com/entra/identity/authentication/howto-authentication-passwordless-deployment) +- [Manage authentication methods](https://learn.microsoft.com/entra/identity/authentication/concept-authentication-methods-manage) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CP01.md b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CP01.md new file mode 100644 index 000000000000..2efc4dc29552 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CP01.md @@ -0,0 +1,13 @@ +# Consent Policy Settings - Group owner consent for apps accessing data + +Group owners should not be allowed to consent to applications accessing group data without proper security review. Allowing group owners to consent to apps can lead to unauthorized data access, as group owners may not fully understand the security implications of granting permissions to third-party applications. + +This setting helps prevent data leakage by ensuring that all application consent requests go through proper administrative channels where security assessments can be performed. + +**Remediation action** +- [Configure group owner consent to apps accessing group data](https://learn.microsoft.com/entra/identity/enterprise-apps/configure-user-consent-groups) +- [Manage app consent policies](https://learn.microsoft.com/entra/identity/enterprise-apps/manage-app-consent-policies) +- [Review permissions granted to applications](https://learn.microsoft.com/entra/identity/enterprise-apps/manage-application-permissions) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CP03.md b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CP03.md new file mode 100644 index 000000000000..c3cae6d32ba2 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CP03.md @@ -0,0 +1,13 @@ +# Consent Policy Settings - Block user consent for risky apps + +User consent should be blocked for applications identified as risky by Microsoft to prevent potential security breaches. Microsoft Entra ID assesses application risk based on various signals, including publisher verification status, permissions requested, and application behavior patterns. + +Blocking consent for risky apps provides automatic protection against potentially malicious applications while still allowing users to consent to trusted, low-risk applications (if user consent is enabled). + +**Remediation action** +- [Configure how users consent to applications](https://learn.microsoft.com/entra/identity/enterprise-apps/configure-user-consent) +- [Configure risk-based step-up consent](https://learn.microsoft.com/entra/identity/enterprise-apps/configure-risk-based-step-up-consent) +- [Manage app consent policies](https://learn.microsoft.com/entra/identity/enterprise-apps/manage-app-consent-policies) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CP04.md b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CP04.md new file mode 100644 index 000000000000..8741778896b3 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CP04.md @@ -0,0 +1,13 @@ +# Consent Policy Settings - Users can request admin consent + +Users should be allowed to request administrator consent when they need to use applications that require permissions beyond what they can grant themselves. This creates a formal workflow where users can submit requests for application access, and administrators can review and approve these requests after assessing security implications. + +Enabling this setting provides a balance between security and productivity, as users can request access to needed applications while ensuring proper oversight and approval processes are in place. + +**Remediation action** +- [Configure the admin consent workflow](https://learn.microsoft.com/entra/identity/enterprise-apps/configure-admin-consent-workflow) +- [Manage admin consent requests](https://learn.microsoft.com/entra/identity/enterprise-apps/manage-consent-requests) +- [Review permissions granted to applications](https://learn.microsoft.com/entra/identity/enterprise-apps/manage-application-permissions) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR01.md b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR01.md new file mode 100644 index 000000000000..9add1126ca1c --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR01.md @@ -0,0 +1,13 @@ +# Admin Consent - Enabled + +The admin consent request workflow should be enabled to provide a formal process for users to request administrator approval for applications requiring privileged permissions. This workflow creates visibility and control over application consent requests while allowing users to request access to applications they need for their work. + +Enabling the admin consent workflow provides a balance between security and productivity by ensuring administrators review high-risk permission requests while streamlining the process for users to request access to needed applications. + +**Remediation action** +- [Configure the admin consent workflow](https://learn.microsoft.com/entra/identity/enterprise-apps/configure-admin-consent-workflow) +- [Manage admin consent requests](https://learn.microsoft.com/entra/identity/enterprise-apps/manage-consent-requests) +- [Review permissions granted to applications](https://learn.microsoft.com/entra/identity/enterprise-apps/manage-application-permissions) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR02.md b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR02.md new file mode 100644 index 000000000000..e78c2db619fc --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR02.md @@ -0,0 +1,13 @@ +# Admin Consent - Notify Reviewers + +Reviewers should be notified when new admin consent requests are submitted to ensure timely review and approval of application access requests. Enabling notifications ensures that consent requests don't go unnoticed and that users receive timely responses to their access requests. + +Proper notification configuration helps maintain a responsive admin consent workflow and improves the user experience while maintaining security oversight. + +**Remediation action** +- [Configure the admin consent workflow](https://learn.microsoft.com/entra/identity/enterprise-apps/configure-admin-consent-workflow) +- [Manage admin consent requests](https://learn.microsoft.com/entra/identity/enterprise-apps/manage-consent-requests) +- [Review and take action on admin consent requests](https://learn.microsoft.com/entra/identity/enterprise-apps/manage-consent-requests#review-and-take-action-on-a-request) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR03.md b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR03.md new file mode 100644 index 000000000000..82e9be1b5ac4 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR03.md @@ -0,0 +1,13 @@ +# Admin Consent - Reminders + +Reminders should be enabled for pending admin consent requests to ensure that reviewers don't forget to review and respond to user requests. Regular reminders help maintain an efficient consent workflow and prevent user frustration from delayed responses. + +Configuring appropriate reminder intervals ensures that consent requests are reviewed in a timely manner while not overwhelming reviewers with excessive notifications. + +**Remediation action** +- [Configure the admin consent workflow](https://learn.microsoft.com/entra/identity/enterprise-apps/configure-admin-consent-workflow) +- [Manage admin consent requests](https://learn.microsoft.com/entra/identity/enterprise-apps/manage-consent-requests) +- [Admin consent workflow settings](https://learn.microsoft.com/entra/identity/enterprise-apps/configure-admin-consent-workflow#configure-settings) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR04.md b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR04.md new file mode 100644 index 000000000000..4ad8b36ab867 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR04.md @@ -0,0 +1,13 @@ +# Admin Consent - Duration + +The admin consent request duration should be set to 30 days or less to ensure that consent requests are reviewed and processed in a timely manner. This setting controls how long consent requests remain active before they expire and need to be resubmitted. + +A shorter request duration ensures that pending consent requests don't accumulate indefinitely and encourages prompt review and decision-making by administrators. It also helps keep the consent request queue manageable and relevant. + +**Remediation action** +- [Configure the admin consent workflow](https://learn.microsoft.com/entra/identity/enterprise-apps/configure-admin-consent-workflow) +- [Manage admin consent requests](https://learn.microsoft.com/entra/identity/enterprise-apps/manage-consent-requests) +- [Admin consent workflow settings](https://learn.microsoft.com/entra/identity/enterprise-apps/configure-admin-consent-workflow#configure-settings) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR01.md b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR01.md new file mode 100644 index 000000000000..ab5f6ccb47f9 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR01.md @@ -0,0 +1,13 @@ +# Password Rule Settings - Password Protection Mode + +Password protection mode should be set to "Enforce" to actively block weak passwords and prevent users from setting passwords that appear on Microsoft's banned password list or your custom banned password list. When set to Enforce mode, weak password attempts are blocked in real-time, providing immediate protection against easily compromised credentials. + +Enforce mode applies to both cloud-only users and users whose passwords are synchronized from on-premises Active Directory (when Azure AD Password Protection for Windows Server Active Directory is deployed). + +**Remediation action** +- [Plan and deploy on-premises Azure Active Directory Password Protection](https://learn.microsoft.com/entra/identity/authentication/howto-password-ban-bad-on-premises-deploy) +- [Eliminate bad passwords using Azure Active Directory Password Protection](https://learn.microsoft.com/entra/identity/authentication/concept-password-ban-bad) +- [Configure custom banned password list](https://learn.microsoft.com/entra/identity/authentication/tutorial-configure-custom-password-protection) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR02.md b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR02.md new file mode 100644 index 000000000000..b743add2288f --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR02.md @@ -0,0 +1,13 @@ +# Password Rule Settings - Enable password protection on Windows Server Active Directory + +Password protection should be enabled for on-premises Windows Server Active Directory to extend Microsoft Entra password protection to your hybrid environment. This ensures that weak passwords are blocked not only in the cloud but also when users set or change passwords on domain controllers. + +Enabling this feature requires deploying Azure AD Password Protection DC agents on your domain controllers and proxy services in your on-premises environment. + +**Remediation action** +- [Plan and deploy on-premises Azure Active Directory Password Protection](https://learn.microsoft.com/entra/identity/authentication/howto-password-ban-bad-on-premises-deploy) +- [Enable on-premises Azure Active Directory Password Protection](https://learn.microsoft.com/entra/identity/authentication/howto-password-ban-bad-on-premises-operations) +- [Monitor on-premises Azure Active Directory Password Protection](https://learn.microsoft.com/entra/identity/authentication/howto-password-ban-bad-on-premises-monitor) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR03.md b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR03.md new file mode 100644 index 000000000000..604afe21a263 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR03.md @@ -0,0 +1,13 @@ +# Password Rule Settings - Enforce custom list + +A custom banned password list should be enforced to block passwords that are specific to your organization, such as company names, product names, locations, or industry-specific terms that attackers might use in targeted password attacks. + +The custom banned password list complements Microsoft's global banned password list to provide organization-specific protection against weak passwords. This helps prevent users from choosing passwords that may be easily guessed based on knowledge of your organization. + +**Remediation action** +- [Configure custom banned password list](https://learn.microsoft.com/entra/identity/authentication/tutorial-configure-custom-password-protection) +- [Eliminate bad passwords using Azure Active Directory Password Protection](https://learn.microsoft.com/entra/identity/authentication/concept-password-ban-bad) +- [Password policies and account restrictions in Microsoft Entra ID](https://learn.microsoft.com/entra/identity/authentication/concept-password-policies) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR05.md b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR05.md new file mode 100644 index 000000000000..96657ca3c744 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR05.md @@ -0,0 +1,13 @@ +# Password Rule Settings - Lockout duration in seconds + +Account lockout duration should be configured to automatically unlock accounts after a specified period following too many failed sign-in attempts. A recommended lockout duration is at least 60 seconds to slow down brute-force attacks while balancing user convenience and security. + +The lockout duration determines how long an account remains locked after reaching the lockout threshold, providing temporary protection against automated password guessing attacks. + +**Remediation action** +- [Microsoft Entra smart lockout](https://learn.microsoft.com/entra/identity/authentication/howto-password-smart-lockout) +- [Password policies and account restrictions in Microsoft Entra ID](https://learn.microsoft.com/entra/identity/authentication/concept-password-policies) +- [Configure smart lockout thresholds](https://learn.microsoft.com/entra/identity/authentication/howto-password-smart-lockout#configure-smart-lockout) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR06.md b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR06.md new file mode 100644 index 000000000000..c98e68b5d05e --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR06.md @@ -0,0 +1,13 @@ +# Password Rule Settings - Lockout threshold + +A lockout threshold should be configured to prevent brute-force password attacks by temporarily locking accounts after a specified number of failed sign-in attempts. A recommended threshold is 10 or fewer failed attempts, which provides strong protection against automated attacks while minimizing the impact on legitimate users who may occasionally mistype their passwords. + +Smart lockout in Microsoft Entra ID uses machine learning to distinguish between legitimate users and attackers, helping to prevent legitimate users from being locked out while still protecting against malicious sign-in attempts. + +**Remediation action** +- [Microsoft Entra smart lockout](https://learn.microsoft.com/entra/identity/authentication/howto-password-smart-lockout) +- [Password policies and account restrictions in Microsoft Entra ID](https://learn.microsoft.com/entra/identity/authentication/concept-password-policies) +- [Protect user accounts from attacks with Microsoft Entra ID Protection](https://learn.microsoft.com/entra/id-protection/overview-identity-protection) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_ST08.md b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_ST08.md new file mode 100644 index 000000000000..861f4980d274 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_ST08.md @@ -0,0 +1,13 @@ +# Classification and M365 Groups - Allow Guests to become Group Owner + +Guest users should not be allowed to become group owners to maintain proper access control and governance over Microsoft 365 groups and teams. Group owners have significant privileges including the ability to add or remove members, modify group settings, and control access to group resources. + +Allowing guests to become group owners creates security risks as external users could potentially grant unauthorized access to organizational resources or modify group configurations in ways that conflict with organizational policies. + +**Remediation action** +- [Manage guest access in Microsoft 365 groups](https://learn.microsoft.com/microsoft-365/admin/create-groups/manage-guest-access-in-groups) +- [Microsoft 365 groups and Microsoft Entra access](https://learn.microsoft.com/entra/identity/users/groups-settings-v2-cmdlets) +- [Review and manage guest user access](https://learn.microsoft.com/entra/identity/users/users-restrict-guest-permissions) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_ST09.md b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_ST09.md new file mode 100644 index 000000000000..35def764040b --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_ST09.md @@ -0,0 +1,13 @@ +# Classification and M365 Groups - Allow Guests to have access to groups content + +Guest access to Microsoft 365 groups content should be carefully controlled based on your organization's collaboration requirements and security policies. When enabled, guests can access group resources including SharePoint sites, Teams content, and group files. However, organizations should assess whether guest access is necessary and implement appropriate controls. + +Consider your organization's collaboration needs and data sensitivity when configuring this setting. For highly secure environments, disabling guest access may be appropriate, while collaboration-focused organizations may enable it with proper oversight. + +**Remediation action** +- [Manage guest access in Microsoft 365 groups](https://learn.microsoft.com/microsoft-365/admin/create-groups/manage-guest-access-in-groups) +- [Configure external collaboration settings](https://learn.microsoft.com/entra/external-id/external-collaboration-settings-configure) +- [Secure collaboration with Microsoft 365](https://learn.microsoft.com/microsoft-365/solutions/setup-secure-collaboration-with-teams) + + +%TestResult% From c2db73d9be9c9d9b820a9c29f7b064664201a83e Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Sun, 11 Jan 2026 20:14:08 +0100 Subject: [PATCH 115/503] filename changes --- ...voke-CippTestEIDSCA_AF01.md => Invoke-CippTestEIDSCAAF01.md} | 0 ...ke-CippTestEIDSCA_AF01.ps1 => Invoke-CippTestEIDSCAAF01.ps1} | 2 +- ...voke-CippTestEIDSCA_AF02.md => Invoke-CippTestEIDSCAAF02.md} | 0 ...ke-CippTestEIDSCA_AF02.ps1 => Invoke-CippTestEIDSCAAF02.ps1} | 2 +- ...voke-CippTestEIDSCA_AF03.md => Invoke-CippTestEIDSCAAF03.md} | 0 ...ke-CippTestEIDSCA_AF03.ps1 => Invoke-CippTestEIDSCAAF03.ps1} | 2 +- ...voke-CippTestEIDSCA_AF04.md => Invoke-CippTestEIDSCAAF04.md} | 0 ...ke-CippTestEIDSCA_AF04.ps1 => Invoke-CippTestEIDSCAAF04.ps1} | 2 +- ...voke-CippTestEIDSCA_AF05.md => Invoke-CippTestEIDSCAAF05.md} | 0 ...ke-CippTestEIDSCA_AF05.ps1 => Invoke-CippTestEIDSCAAF05.ps1} | 2 +- ...voke-CippTestEIDSCA_AF06.md => Invoke-CippTestEIDSCAAF06.md} | 0 ...ke-CippTestEIDSCA_AF06.ps1 => Invoke-CippTestEIDSCAAF06.ps1} | 2 +- ...voke-CippTestEIDSCA_AG01.md => Invoke-CippTestEIDSCAAG01.md} | 0 ...ke-CippTestEIDSCA_AG01.ps1 => Invoke-CippTestEIDSCAAG01.ps1} | 2 +- ...voke-CippTestEIDSCA_AG02.md => Invoke-CippTestEIDSCAAG02.md} | 0 ...ke-CippTestEIDSCA_AG02.ps1 => Invoke-CippTestEIDSCAAG02.ps1} | 2 +- ...voke-CippTestEIDSCA_AG03.md => Invoke-CippTestEIDSCAAG03.md} | 0 ...ke-CippTestEIDSCA_AG03.ps1 => Invoke-CippTestEIDSCAAG03.ps1} | 2 +- ...voke-CippTestEIDSCA_AM01.md => Invoke-CippTestEIDSCAAM01.md} | 0 ...ke-CippTestEIDSCA_AM01.ps1 => Invoke-CippTestEIDSCAAM01.ps1} | 2 +- ...voke-CippTestEIDSCA_AM02.md => Invoke-CippTestEIDSCAAM02.md} | 0 ...ke-CippTestEIDSCA_AM02.ps1 => Invoke-CippTestEIDSCAAM02.ps1} | 2 +- ...voke-CippTestEIDSCA_AM03.md => Invoke-CippTestEIDSCAAM03.md} | 0 ...ke-CippTestEIDSCA_AM03.ps1 => Invoke-CippTestEIDSCAAM03.ps1} | 2 +- ...voke-CippTestEIDSCA_AM04.md => Invoke-CippTestEIDSCAAM04.md} | 0 ...ke-CippTestEIDSCA_AM04.ps1 => Invoke-CippTestEIDSCAAM04.ps1} | 2 +- ...voke-CippTestEIDSCA_AM06.md => Invoke-CippTestEIDSCAAM06.md} | 0 ...ke-CippTestEIDSCA_AM06.ps1 => Invoke-CippTestEIDSCAAM06.ps1} | 2 +- ...voke-CippTestEIDSCA_AM07.md => Invoke-CippTestEIDSCAAM07.md} | 0 ...ke-CippTestEIDSCA_AM07.ps1 => Invoke-CippTestEIDSCAAM07.ps1} | 2 +- ...voke-CippTestEIDSCA_AM09.md => Invoke-CippTestEIDSCAAM09.md} | 0 ...ke-CippTestEIDSCA_AM09.ps1 => Invoke-CippTestEIDSCAAM09.ps1} | 2 +- ...voke-CippTestEIDSCA_AM10.md => Invoke-CippTestEIDSCAAM10.md} | 0 ...ke-CippTestEIDSCA_AM10.ps1 => Invoke-CippTestEIDSCAAM10.ps1} | 2 +- ...voke-CippTestEIDSCA_AP01.md => Invoke-CippTestEIDSCAAP01.md} | 0 ...ke-CippTestEIDSCA_AP01.ps1 => Invoke-CippTestEIDSCAAP01.ps1} | 2 +- ...voke-CippTestEIDSCA_AP04.md => Invoke-CippTestEIDSCAAP04.md} | 0 ...ke-CippTestEIDSCA_AP04.ps1 => Invoke-CippTestEIDSCAAP04.ps1} | 2 +- ...voke-CippTestEIDSCA_AP05.md => Invoke-CippTestEIDSCAAP05.md} | 0 ...ke-CippTestEIDSCA_AP05.ps1 => Invoke-CippTestEIDSCAAP05.ps1} | 2 +- ...voke-CippTestEIDSCA_AP06.md => Invoke-CippTestEIDSCAAP06.md} | 0 ...ke-CippTestEIDSCA_AP06.ps1 => Invoke-CippTestEIDSCAAP06.ps1} | 2 +- ...voke-CippTestEIDSCA_AP07.md => Invoke-CippTestEIDSCAAP07.md} | 0 ...ke-CippTestEIDSCA_AP07.ps1 => Invoke-CippTestEIDSCAAP07.ps1} | 2 +- ...voke-CippTestEIDSCA_AP08.md => Invoke-CippTestEIDSCAAP08.md} | 0 ...ke-CippTestEIDSCA_AP08.ps1 => Invoke-CippTestEIDSCAAP08.ps1} | 2 +- ...voke-CippTestEIDSCA_AP09.md => Invoke-CippTestEIDSCAAP09.md} | 0 ...ke-CippTestEIDSCA_AP09.ps1 => Invoke-CippTestEIDSCAAP09.ps1} | 2 +- ...voke-CippTestEIDSCA_AP10.md => Invoke-CippTestEIDSCAAP10.md} | 0 ...ke-CippTestEIDSCA_AP10.ps1 => Invoke-CippTestEIDSCAAP10.ps1} | 2 +- ...voke-CippTestEIDSCA_AP14.md => Invoke-CippTestEIDSCAAP14.md} | 0 ...ke-CippTestEIDSCA_AP14.ps1 => Invoke-CippTestEIDSCAAP14.ps1} | 2 +- ...voke-CippTestEIDSCA_AS04.md => Invoke-CippTestEIDSCAAS04.md} | 0 ...ke-CippTestEIDSCA_AS04.ps1 => Invoke-CippTestEIDSCAAS04.ps1} | 2 +- ...voke-CippTestEIDSCA_AT01.md => Invoke-CippTestEIDSCAAT01.md} | 0 ...ke-CippTestEIDSCA_AT01.ps1 => Invoke-CippTestEIDSCAAT01.ps1} | 2 +- ...voke-CippTestEIDSCA_AT02.md => Invoke-CippTestEIDSCAAT02.md} | 0 ...ke-CippTestEIDSCA_AT02.ps1 => Invoke-CippTestEIDSCAAT02.ps1} | 2 +- ...voke-CippTestEIDSCA_AV01.md => Invoke-CippTestEIDSCAAV01.md} | 0 ...ke-CippTestEIDSCA_AV01.ps1 => Invoke-CippTestEIDSCAAV01.ps1} | 2 +- ...voke-CippTestEIDSCA_CP01.md => Invoke-CippTestEIDSCACP01.md} | 0 ...ke-CippTestEIDSCA_CP01.ps1 => Invoke-CippTestEIDSCACP01.ps1} | 2 +- ...voke-CippTestEIDSCA_CP03.md => Invoke-CippTestEIDSCACP03.md} | 0 ...ke-CippTestEIDSCA_CP03.ps1 => Invoke-CippTestEIDSCACP03.ps1} | 2 +- ...voke-CippTestEIDSCA_CP04.md => Invoke-CippTestEIDSCACP04.md} | 0 ...ke-CippTestEIDSCA_CP04.ps1 => Invoke-CippTestEIDSCACP04.ps1} | 2 +- ...voke-CippTestEIDSCA_CR01.md => Invoke-CippTestEIDSCACR01.md} | 0 ...ke-CippTestEIDSCA_CR01.ps1 => Invoke-CippTestEIDSCACR01.ps1} | 2 +- ...voke-CippTestEIDSCA_CR02.md => Invoke-CippTestEIDSCACR02.md} | 0 ...ke-CippTestEIDSCA_CR02.ps1 => Invoke-CippTestEIDSCACR02.ps1} | 2 +- ...voke-CippTestEIDSCA_CR03.md => Invoke-CippTestEIDSCACR03.md} | 0 ...ke-CippTestEIDSCA_CR03.ps1 => Invoke-CippTestEIDSCACR03.ps1} | 2 +- ...voke-CippTestEIDSCA_CR04.md => Invoke-CippTestEIDSCACR04.md} | 0 ...ke-CippTestEIDSCA_CR04.ps1 => Invoke-CippTestEIDSCACR04.ps1} | 2 +- ...voke-CippTestEIDSCA_PR01.md => Invoke-CippTestEIDSCAPR01.md} | 0 ...ke-CippTestEIDSCA_PR01.ps1 => Invoke-CippTestEIDSCAPR01.ps1} | 2 +- ...voke-CippTestEIDSCA_PR02.md => Invoke-CippTestEIDSCAPR02.md} | 0 ...ke-CippTestEIDSCA_PR02.ps1 => Invoke-CippTestEIDSCAPR02.ps1} | 2 +- ...voke-CippTestEIDSCA_PR03.md => Invoke-CippTestEIDSCAPR03.md} | 0 ...ke-CippTestEIDSCA_PR03.ps1 => Invoke-CippTestEIDSCAPR03.ps1} | 2 +- ...voke-CippTestEIDSCA_PR05.md => Invoke-CippTestEIDSCAPR05.md} | 0 ...ke-CippTestEIDSCA_PR05.ps1 => Invoke-CippTestEIDSCAPR05.ps1} | 2 +- ...voke-CippTestEIDSCA_PR06.md => Invoke-CippTestEIDSCAPR06.md} | 0 ...ke-CippTestEIDSCA_PR06.ps1 => Invoke-CippTestEIDSCAPR06.ps1} | 2 +- ...voke-CippTestEIDSCA_ST08.md => Invoke-CippTestEIDSCAST08.md} | 0 ...ke-CippTestEIDSCA_ST08.ps1 => Invoke-CippTestEIDSCAST08.ps1} | 2 +- ...voke-CippTestEIDSCA_ST09.md => Invoke-CippTestEIDSCAST09.md} | 0 ...ke-CippTestEIDSCA_ST09.ps1 => Invoke-CippTestEIDSCAST09.ps1} | 2 +- 88 files changed, 44 insertions(+), 44 deletions(-) rename Modules/CIPPCore/Public/Tests/EIDSCA/Identity/{Invoke-CippTestEIDSCA_AF01.md => Invoke-CippTestEIDSCAAF01.md} (100%) rename Modules/CIPPCore/Public/Tests/EIDSCA/Identity/{Invoke-CippTestEIDSCA_AF01.ps1 => Invoke-CippTestEIDSCAAF01.ps1} (98%) rename Modules/CIPPCore/Public/Tests/EIDSCA/Identity/{Invoke-CippTestEIDSCA_AF02.md => Invoke-CippTestEIDSCAAF02.md} (100%) rename Modules/CIPPCore/Public/Tests/EIDSCA/Identity/{Invoke-CippTestEIDSCA_AF02.ps1 => Invoke-CippTestEIDSCAAF02.ps1} (98%) rename Modules/CIPPCore/Public/Tests/EIDSCA/Identity/{Invoke-CippTestEIDSCA_AF03.md => Invoke-CippTestEIDSCAAF03.md} (100%) rename Modules/CIPPCore/Public/Tests/EIDSCA/Identity/{Invoke-CippTestEIDSCA_AF03.ps1 => Invoke-CippTestEIDSCAAF03.ps1} (98%) rename Modules/CIPPCore/Public/Tests/EIDSCA/Identity/{Invoke-CippTestEIDSCA_AF04.md => Invoke-CippTestEIDSCAAF04.md} (100%) rename Modules/CIPPCore/Public/Tests/EIDSCA/Identity/{Invoke-CippTestEIDSCA_AF04.ps1 => Invoke-CippTestEIDSCAAF04.ps1} (98%) rename Modules/CIPPCore/Public/Tests/EIDSCA/Identity/{Invoke-CippTestEIDSCA_AF05.md => Invoke-CippTestEIDSCAAF05.md} (100%) rename Modules/CIPPCore/Public/Tests/EIDSCA/Identity/{Invoke-CippTestEIDSCA_AF05.ps1 => Invoke-CippTestEIDSCAAF05.ps1} (98%) rename Modules/CIPPCore/Public/Tests/EIDSCA/Identity/{Invoke-CippTestEIDSCA_AF06.md => Invoke-CippTestEIDSCAAF06.md} (100%) rename Modules/CIPPCore/Public/Tests/EIDSCA/Identity/{Invoke-CippTestEIDSCA_AF06.ps1 => Invoke-CippTestEIDSCAAF06.ps1} (98%) rename Modules/CIPPCore/Public/Tests/EIDSCA/Identity/{Invoke-CippTestEIDSCA_AG01.md => Invoke-CippTestEIDSCAAG01.md} (100%) rename Modules/CIPPCore/Public/Tests/EIDSCA/Identity/{Invoke-CippTestEIDSCA_AG01.ps1 => Invoke-CippTestEIDSCAAG01.ps1} (98%) rename Modules/CIPPCore/Public/Tests/EIDSCA/Identity/{Invoke-CippTestEIDSCA_AG02.md => Invoke-CippTestEIDSCAAG02.md} (100%) rename Modules/CIPPCore/Public/Tests/EIDSCA/Identity/{Invoke-CippTestEIDSCA_AG02.ps1 => Invoke-CippTestEIDSCAAG02.ps1} (98%) rename Modules/CIPPCore/Public/Tests/EIDSCA/Identity/{Invoke-CippTestEIDSCA_AG03.md => Invoke-CippTestEIDSCAAG03.md} (100%) rename Modules/CIPPCore/Public/Tests/EIDSCA/Identity/{Invoke-CippTestEIDSCA_AG03.ps1 => Invoke-CippTestEIDSCAAG03.ps1} (98%) rename Modules/CIPPCore/Public/Tests/EIDSCA/Identity/{Invoke-CippTestEIDSCA_AM01.md => Invoke-CippTestEIDSCAAM01.md} (100%) rename Modules/CIPPCore/Public/Tests/EIDSCA/Identity/{Invoke-CippTestEIDSCA_AM01.ps1 => Invoke-CippTestEIDSCAAM01.ps1} (98%) rename Modules/CIPPCore/Public/Tests/EIDSCA/Identity/{Invoke-CippTestEIDSCA_AM02.md => Invoke-CippTestEIDSCAAM02.md} (100%) rename Modules/CIPPCore/Public/Tests/EIDSCA/Identity/{Invoke-CippTestEIDSCA_AM02.ps1 => Invoke-CippTestEIDSCAAM02.ps1} (98%) rename Modules/CIPPCore/Public/Tests/EIDSCA/Identity/{Invoke-CippTestEIDSCA_AM03.md => Invoke-CippTestEIDSCAAM03.md} (100%) rename Modules/CIPPCore/Public/Tests/EIDSCA/Identity/{Invoke-CippTestEIDSCA_AM03.ps1 => Invoke-CippTestEIDSCAAM03.ps1} (98%) rename Modules/CIPPCore/Public/Tests/EIDSCA/Identity/{Invoke-CippTestEIDSCA_AM04.md => Invoke-CippTestEIDSCAAM04.md} (100%) rename Modules/CIPPCore/Public/Tests/EIDSCA/Identity/{Invoke-CippTestEIDSCA_AM04.ps1 => Invoke-CippTestEIDSCAAM04.ps1} (98%) rename Modules/CIPPCore/Public/Tests/EIDSCA/Identity/{Invoke-CippTestEIDSCA_AM06.md => Invoke-CippTestEIDSCAAM06.md} (100%) rename Modules/CIPPCore/Public/Tests/EIDSCA/Identity/{Invoke-CippTestEIDSCA_AM06.ps1 => Invoke-CippTestEIDSCAAM06.ps1} (98%) rename Modules/CIPPCore/Public/Tests/EIDSCA/Identity/{Invoke-CippTestEIDSCA_AM07.md => Invoke-CippTestEIDSCAAM07.md} (100%) rename Modules/CIPPCore/Public/Tests/EIDSCA/Identity/{Invoke-CippTestEIDSCA_AM07.ps1 => Invoke-CippTestEIDSCAAM07.ps1} (98%) rename Modules/CIPPCore/Public/Tests/EIDSCA/Identity/{Invoke-CippTestEIDSCA_AM09.md => Invoke-CippTestEIDSCAAM09.md} (100%) rename Modules/CIPPCore/Public/Tests/EIDSCA/Identity/{Invoke-CippTestEIDSCA_AM09.ps1 => Invoke-CippTestEIDSCAAM09.ps1} (98%) rename Modules/CIPPCore/Public/Tests/EIDSCA/Identity/{Invoke-CippTestEIDSCA_AM10.md => Invoke-CippTestEIDSCAAM10.md} (100%) rename Modules/CIPPCore/Public/Tests/EIDSCA/Identity/{Invoke-CippTestEIDSCA_AM10.ps1 => Invoke-CippTestEIDSCAAM10.ps1} (98%) rename Modules/CIPPCore/Public/Tests/EIDSCA/Identity/{Invoke-CippTestEIDSCA_AP01.md => Invoke-CippTestEIDSCAAP01.md} (100%) rename Modules/CIPPCore/Public/Tests/EIDSCA/Identity/{Invoke-CippTestEIDSCA_AP01.ps1 => Invoke-CippTestEIDSCAAP01.ps1} (98%) rename Modules/CIPPCore/Public/Tests/EIDSCA/Identity/{Invoke-CippTestEIDSCA_AP04.md => Invoke-CippTestEIDSCAAP04.md} (100%) rename Modules/CIPPCore/Public/Tests/EIDSCA/Identity/{Invoke-CippTestEIDSCA_AP04.ps1 => Invoke-CippTestEIDSCAAP04.ps1} (98%) rename Modules/CIPPCore/Public/Tests/EIDSCA/Identity/{Invoke-CippTestEIDSCA_AP05.md => Invoke-CippTestEIDSCAAP05.md} (100%) rename Modules/CIPPCore/Public/Tests/EIDSCA/Identity/{Invoke-CippTestEIDSCA_AP05.ps1 => Invoke-CippTestEIDSCAAP05.ps1} (98%) rename Modules/CIPPCore/Public/Tests/EIDSCA/Identity/{Invoke-CippTestEIDSCA_AP06.md => Invoke-CippTestEIDSCAAP06.md} (100%) rename Modules/CIPPCore/Public/Tests/EIDSCA/Identity/{Invoke-CippTestEIDSCA_AP06.ps1 => Invoke-CippTestEIDSCAAP06.ps1} (98%) rename Modules/CIPPCore/Public/Tests/EIDSCA/Identity/{Invoke-CippTestEIDSCA_AP07.md => Invoke-CippTestEIDSCAAP07.md} (100%) rename Modules/CIPPCore/Public/Tests/EIDSCA/Identity/{Invoke-CippTestEIDSCA_AP07.ps1 => Invoke-CippTestEIDSCAAP07.ps1} (98%) rename Modules/CIPPCore/Public/Tests/EIDSCA/Identity/{Invoke-CippTestEIDSCA_AP08.md => Invoke-CippTestEIDSCAAP08.md} (100%) rename Modules/CIPPCore/Public/Tests/EIDSCA/Identity/{Invoke-CippTestEIDSCA_AP08.ps1 => Invoke-CippTestEIDSCAAP08.ps1} (98%) rename Modules/CIPPCore/Public/Tests/EIDSCA/Identity/{Invoke-CippTestEIDSCA_AP09.md => Invoke-CippTestEIDSCAAP09.md} (100%) rename Modules/CIPPCore/Public/Tests/EIDSCA/Identity/{Invoke-CippTestEIDSCA_AP09.ps1 => Invoke-CippTestEIDSCAAP09.ps1} (98%) rename Modules/CIPPCore/Public/Tests/EIDSCA/Identity/{Invoke-CippTestEIDSCA_AP10.md => Invoke-CippTestEIDSCAAP10.md} (100%) rename Modules/CIPPCore/Public/Tests/EIDSCA/Identity/{Invoke-CippTestEIDSCA_AP10.ps1 => Invoke-CippTestEIDSCAAP10.ps1} (98%) rename Modules/CIPPCore/Public/Tests/EIDSCA/Identity/{Invoke-CippTestEIDSCA_AP14.md => Invoke-CippTestEIDSCAAP14.md} (100%) rename Modules/CIPPCore/Public/Tests/EIDSCA/Identity/{Invoke-CippTestEIDSCA_AP14.ps1 => Invoke-CippTestEIDSCAAP14.ps1} (98%) rename Modules/CIPPCore/Public/Tests/EIDSCA/Identity/{Invoke-CippTestEIDSCA_AS04.md => Invoke-CippTestEIDSCAAS04.md} (100%) rename Modules/CIPPCore/Public/Tests/EIDSCA/Identity/{Invoke-CippTestEIDSCA_AS04.ps1 => Invoke-CippTestEIDSCAAS04.ps1} (98%) rename Modules/CIPPCore/Public/Tests/EIDSCA/Identity/{Invoke-CippTestEIDSCA_AT01.md => Invoke-CippTestEIDSCAAT01.md} (100%) rename Modules/CIPPCore/Public/Tests/EIDSCA/Identity/{Invoke-CippTestEIDSCA_AT01.ps1 => Invoke-CippTestEIDSCAAT01.ps1} (98%) rename Modules/CIPPCore/Public/Tests/EIDSCA/Identity/{Invoke-CippTestEIDSCA_AT02.md => Invoke-CippTestEIDSCAAT02.md} (100%) rename Modules/CIPPCore/Public/Tests/EIDSCA/Identity/{Invoke-CippTestEIDSCA_AT02.ps1 => Invoke-CippTestEIDSCAAT02.ps1} (98%) rename Modules/CIPPCore/Public/Tests/EIDSCA/Identity/{Invoke-CippTestEIDSCA_AV01.md => Invoke-CippTestEIDSCAAV01.md} (100%) rename Modules/CIPPCore/Public/Tests/EIDSCA/Identity/{Invoke-CippTestEIDSCA_AV01.ps1 => Invoke-CippTestEIDSCAAV01.ps1} (98%) rename Modules/CIPPCore/Public/Tests/EIDSCA/Identity/{Invoke-CippTestEIDSCA_CP01.md => Invoke-CippTestEIDSCACP01.md} (100%) rename Modules/CIPPCore/Public/Tests/EIDSCA/Identity/{Invoke-CippTestEIDSCA_CP01.ps1 => Invoke-CippTestEIDSCACP01.ps1} (98%) rename Modules/CIPPCore/Public/Tests/EIDSCA/Identity/{Invoke-CippTestEIDSCA_CP03.md => Invoke-CippTestEIDSCACP03.md} (100%) rename Modules/CIPPCore/Public/Tests/EIDSCA/Identity/{Invoke-CippTestEIDSCA_CP03.ps1 => Invoke-CippTestEIDSCACP03.ps1} (98%) rename Modules/CIPPCore/Public/Tests/EIDSCA/Identity/{Invoke-CippTestEIDSCA_CP04.md => Invoke-CippTestEIDSCACP04.md} (100%) rename Modules/CIPPCore/Public/Tests/EIDSCA/Identity/{Invoke-CippTestEIDSCA_CP04.ps1 => Invoke-CippTestEIDSCACP04.ps1} (98%) rename Modules/CIPPCore/Public/Tests/EIDSCA/Identity/{Invoke-CippTestEIDSCA_CR01.md => Invoke-CippTestEIDSCACR01.md} (100%) rename Modules/CIPPCore/Public/Tests/EIDSCA/Identity/{Invoke-CippTestEIDSCA_CR01.ps1 => Invoke-CippTestEIDSCACR01.ps1} (98%) rename Modules/CIPPCore/Public/Tests/EIDSCA/Identity/{Invoke-CippTestEIDSCA_CR02.md => Invoke-CippTestEIDSCACR02.md} (100%) rename Modules/CIPPCore/Public/Tests/EIDSCA/Identity/{Invoke-CippTestEIDSCA_CR02.ps1 => Invoke-CippTestEIDSCACR02.ps1} (98%) rename Modules/CIPPCore/Public/Tests/EIDSCA/Identity/{Invoke-CippTestEIDSCA_CR03.md => Invoke-CippTestEIDSCACR03.md} (100%) rename Modules/CIPPCore/Public/Tests/EIDSCA/Identity/{Invoke-CippTestEIDSCA_CR03.ps1 => Invoke-CippTestEIDSCACR03.ps1} (98%) rename Modules/CIPPCore/Public/Tests/EIDSCA/Identity/{Invoke-CippTestEIDSCA_CR04.md => Invoke-CippTestEIDSCACR04.md} (100%) rename Modules/CIPPCore/Public/Tests/EIDSCA/Identity/{Invoke-CippTestEIDSCA_CR04.ps1 => Invoke-CippTestEIDSCACR04.ps1} (98%) rename Modules/CIPPCore/Public/Tests/EIDSCA/Identity/{Invoke-CippTestEIDSCA_PR01.md => Invoke-CippTestEIDSCAPR01.md} (100%) rename Modules/CIPPCore/Public/Tests/EIDSCA/Identity/{Invoke-CippTestEIDSCA_PR01.ps1 => Invoke-CippTestEIDSCAPR01.ps1} (98%) rename Modules/CIPPCore/Public/Tests/EIDSCA/Identity/{Invoke-CippTestEIDSCA_PR02.md => Invoke-CippTestEIDSCAPR02.md} (100%) rename Modules/CIPPCore/Public/Tests/EIDSCA/Identity/{Invoke-CippTestEIDSCA_PR02.ps1 => Invoke-CippTestEIDSCAPR02.ps1} (98%) rename Modules/CIPPCore/Public/Tests/EIDSCA/Identity/{Invoke-CippTestEIDSCA_PR03.md => Invoke-CippTestEIDSCAPR03.md} (100%) rename Modules/CIPPCore/Public/Tests/EIDSCA/Identity/{Invoke-CippTestEIDSCA_PR03.ps1 => Invoke-CippTestEIDSCAPR03.ps1} (98%) rename Modules/CIPPCore/Public/Tests/EIDSCA/Identity/{Invoke-CippTestEIDSCA_PR05.md => Invoke-CippTestEIDSCAPR05.md} (100%) rename Modules/CIPPCore/Public/Tests/EIDSCA/Identity/{Invoke-CippTestEIDSCA_PR05.ps1 => Invoke-CippTestEIDSCAPR05.ps1} (98%) rename Modules/CIPPCore/Public/Tests/EIDSCA/Identity/{Invoke-CippTestEIDSCA_PR06.md => Invoke-CippTestEIDSCAPR06.md} (100%) rename Modules/CIPPCore/Public/Tests/EIDSCA/Identity/{Invoke-CippTestEIDSCA_PR06.ps1 => Invoke-CippTestEIDSCAPR06.ps1} (98%) rename Modules/CIPPCore/Public/Tests/EIDSCA/Identity/{Invoke-CippTestEIDSCA_ST08.md => Invoke-CippTestEIDSCAST08.md} (100%) rename Modules/CIPPCore/Public/Tests/EIDSCA/Identity/{Invoke-CippTestEIDSCA_ST08.ps1 => Invoke-CippTestEIDSCAST08.ps1} (98%) rename Modules/CIPPCore/Public/Tests/EIDSCA/Identity/{Invoke-CippTestEIDSCA_ST09.md => Invoke-CippTestEIDSCAST09.md} (100%) rename Modules/CIPPCore/Public/Tests/EIDSCA/Identity/{Invoke-CippTestEIDSCA_ST09.ps1 => Invoke-CippTestEIDSCAST09.ps1} (98%) diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF01.md b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAF01.md similarity index 100% rename from Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF01.md rename to Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAF01.md diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF01.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAF01.ps1 similarity index 98% rename from Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF01.ps1 rename to Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAF01.ps1 index 6b75c6486d19..9da4913dad9e 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF01.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAF01.ps1 @@ -1,4 +1,4 @@ -function Invoke-CippTestEIDSCA_AF01 { +function Invoke-CippTestEIDSCAAF01 { <# .SYNOPSIS FIDO2 - State diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF02.md b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAF02.md similarity index 100% rename from Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF02.md rename to Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAF02.md diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF02.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAF02.ps1 similarity index 98% rename from Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF02.ps1 rename to Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAF02.ps1 index 95defc1727d1..e10fe6994d4d 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF02.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAF02.ps1 @@ -1,4 +1,4 @@ -function Invoke-CippTestEIDSCA_AF02 { +function Invoke-CippTestEIDSCAAF02 { <# .SYNOPSIS FIDO2 - Self-Service diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF03.md b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAF03.md similarity index 100% rename from Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF03.md rename to Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAF03.md diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF03.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAF03.ps1 similarity index 98% rename from Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF03.ps1 rename to Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAF03.ps1 index eb6f808b71b5..64488cace6d6 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF03.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAF03.ps1 @@ -1,4 +1,4 @@ -function Invoke-CippTestEIDSCA_AF03 { +function Invoke-CippTestEIDSCAAF03 { <# .SYNOPSIS FIDO2 - Attestation diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF04.md b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAF04.md similarity index 100% rename from Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF04.md rename to Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAF04.md diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF04.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAF04.ps1 similarity index 98% rename from Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF04.ps1 rename to Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAF04.ps1 index aac28ffce9df..9732628e81ff 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF04.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAF04.ps1 @@ -1,4 +1,4 @@ -function Invoke-CippTestEIDSCA_AF04 { +function Invoke-CippTestEIDSCAAF04 { <# .SYNOPSIS FIDO2 - Key Restrictions diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF05.md b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAF05.md similarity index 100% rename from Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF05.md rename to Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAF05.md diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF05.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAF05.ps1 similarity index 98% rename from Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF05.ps1 rename to Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAF05.ps1 index 56647ccee952..24ca89ef8203 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF05.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAF05.ps1 @@ -1,4 +1,4 @@ -function Invoke-CippTestEIDSCA_AF05 { +function Invoke-CippTestEIDSCAAF05 { <# .SYNOPSIS FIDO2 - Restricted Keys diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF06.md b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAF06.md similarity index 100% rename from Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF06.md rename to Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAF06.md diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF06.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAF06.ps1 similarity index 98% rename from Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF06.ps1 rename to Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAF06.ps1 index fc4117c01b43..b0f8c58cf1c1 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AF06.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAF06.ps1 @@ -1,4 +1,4 @@ -function Invoke-CippTestEIDSCA_AF06 { +function Invoke-CippTestEIDSCAAF06 { <# .SYNOPSIS FIDO2 - Specific Keys diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AG01.md b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAG01.md similarity index 100% rename from Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AG01.md rename to Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAG01.md diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AG01.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAG01.ps1 similarity index 98% rename from Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AG01.ps1 rename to Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAG01.ps1 index b8998cc56529..63161994f895 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AG01.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAG01.ps1 @@ -1,4 +1,4 @@ -function Invoke-CippTestEIDSCA_AG01 { +function Invoke-CippTestEIDSCAAG01 { <# .SYNOPSIS Authentication Methods - Policy Migration diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AG02.md b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAG02.md similarity index 100% rename from Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AG02.md rename to Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAG02.md diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AG02.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAG02.ps1 similarity index 98% rename from Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AG02.ps1 rename to Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAG02.ps1 index 160a8373c972..77244bc3cd59 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AG02.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAG02.ps1 @@ -1,4 +1,4 @@ -function Invoke-CippTestEIDSCA_AG02 { +function Invoke-CippTestEIDSCAAG02 { <# .SYNOPSIS Authentication Methods - Report Suspicious Activity diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AG03.md b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAG03.md similarity index 100% rename from Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AG03.md rename to Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAG03.md diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AG03.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAG03.ps1 similarity index 98% rename from Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AG03.ps1 rename to Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAG03.ps1 index 881ede11cd79..422f3b15eb38 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AG03.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAG03.ps1 @@ -1,4 +1,4 @@ -function Invoke-CippTestEIDSCA_AG03 { +function Invoke-CippTestEIDSCAAG03 { <# .SYNOPSIS Authentication Methods - Suspicious Activity Target diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM01.md b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAM01.md similarity index 100% rename from Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM01.md rename to Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAM01.md diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM01.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAM01.ps1 similarity index 98% rename from Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM01.ps1 rename to Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAM01.ps1 index e64f9dcdbf16..2202e20fcf9e 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM01.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAM01.ps1 @@ -1,4 +1,4 @@ -function Invoke-CippTestEIDSCA_AM01 { +function Invoke-CippTestEIDSCAAM01 { <# .SYNOPSIS MS Authenticator - State diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM02.md b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAM02.md similarity index 100% rename from Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM02.md rename to Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAM02.md diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM02.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAM02.ps1 similarity index 98% rename from Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM02.ps1 rename to Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAM02.ps1 index 4be6ebe70109..cb55407730d7 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM02.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAM02.ps1 @@ -1,4 +1,4 @@ -function Invoke-CippTestEIDSCA_AM02 { +function Invoke-CippTestEIDSCAAM02 { <# .SYNOPSIS MS Authenticator - OTP Disabled diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM03.md b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAM03.md similarity index 100% rename from Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM03.md rename to Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAM03.md diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM03.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAM03.ps1 similarity index 98% rename from Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM03.ps1 rename to Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAM03.ps1 index f1357097688c..074a1bd30f77 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM03.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAM03.ps1 @@ -1,4 +1,4 @@ -function Invoke-CippTestEIDSCA_AM03 { +function Invoke-CippTestEIDSCAAM03 { <# .SYNOPSIS MS Authenticator - Number Matching diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM04.md b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAM04.md similarity index 100% rename from Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM04.md rename to Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAM04.md diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM04.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAM04.ps1 similarity index 98% rename from Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM04.ps1 rename to Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAM04.ps1 index da54722211c9..aad48456a2c5 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM04.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAM04.ps1 @@ -1,4 +1,4 @@ -function Invoke-CippTestEIDSCA_AM04 { +function Invoke-CippTestEIDSCAAM04 { <# .SYNOPSIS MS Authenticator - Number Matching Target diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM06.md b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAM06.md similarity index 100% rename from Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM06.md rename to Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAM06.md diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM06.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAM06.ps1 similarity index 98% rename from Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM06.ps1 rename to Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAM06.ps1 index 686561718d8f..b9ee3660893a 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM06.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAM06.ps1 @@ -1,4 +1,4 @@ -function Invoke-CippTestEIDSCA_AM06 { +function Invoke-CippTestEIDSCAAM06 { <# .SYNOPSIS MS Authenticator - Show App Name diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM07.md b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAM07.md similarity index 100% rename from Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM07.md rename to Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAM07.md diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM07.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAM07.ps1 similarity index 98% rename from Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM07.ps1 rename to Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAM07.ps1 index 81799a7d798d..89ccd7d9e23b 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM07.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAM07.ps1 @@ -1,4 +1,4 @@ -function Invoke-CippTestEIDSCA_AM07 { +function Invoke-CippTestEIDSCAAM07 { <# .SYNOPSIS MS Authenticator - Show App Name Target diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM09.md b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAM09.md similarity index 100% rename from Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM09.md rename to Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAM09.md diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM09.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAM09.ps1 similarity index 98% rename from Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM09.ps1 rename to Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAM09.ps1 index 9be8d3f3b57e..5a612085f473 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM09.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAM09.ps1 @@ -1,4 +1,4 @@ -function Invoke-CippTestEIDSCA_AM09 { +function Invoke-CippTestEIDSCAAM09 { <# .SYNOPSIS MS Authenticator - Show Location diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM10.md b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAM10.md similarity index 100% rename from Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM10.md rename to Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAM10.md diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM10.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAM10.ps1 similarity index 98% rename from Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM10.ps1 rename to Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAM10.ps1 index 911bb7d26164..cff80ac7015d 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AM10.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAM10.ps1 @@ -1,4 +1,4 @@ -function Invoke-CippTestEIDSCA_AM10 { +function Invoke-CippTestEIDSCAAM10 { <# .SYNOPSIS MS Authenticator - Show Location Target diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP01.md b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAP01.md similarity index 100% rename from Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP01.md rename to Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAP01.md diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP01.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAP01.ps1 similarity index 98% rename from Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP01.ps1 rename to Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAP01.ps1 index 413ba9d40fc9..366d944629ca 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP01.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAP01.ps1 @@ -1,4 +1,4 @@ -function Invoke-CippTestEIDSCA_AP01 { +function Invoke-CippTestEIDSCAAP01 { <# .SYNOPSIS Authorization Policy - Self-Service Password Reset for Admins diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP04.md b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAP04.md similarity index 100% rename from Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP04.md rename to Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAP04.md diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP04.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAP04.ps1 similarity index 98% rename from Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP04.ps1 rename to Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAP04.ps1 index 1daeea3017eb..8d45600068b9 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP04.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAP04.ps1 @@ -1,4 +1,4 @@ -function Invoke-CippTestEIDSCA_AP04 { +function Invoke-CippTestEIDSCAAP04 { <# .SYNOPSIS Authorization Policy - Guest Invite Restrictions diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP05.md b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAP05.md similarity index 100% rename from Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP05.md rename to Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAP05.md diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP05.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAP05.ps1 similarity index 98% rename from Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP05.ps1 rename to Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAP05.ps1 index c9cad439a372..45da15d78a26 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP05.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAP05.ps1 @@ -1,4 +1,4 @@ -function Invoke-CippTestEIDSCA_AP05 { +function Invoke-CippTestEIDSCAAP05 { <# .SYNOPSIS Authorization Policy - Email-Based Subscription Sign-up diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP06.md b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAP06.md similarity index 100% rename from Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP06.md rename to Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAP06.md diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP06.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAP06.ps1 similarity index 98% rename from Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP06.ps1 rename to Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAP06.ps1 index 8ce6aab74337..ffe03f08fd4a 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP06.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAP06.ps1 @@ -1,4 +1,4 @@ -function Invoke-CippTestEIDSCA_AP06 { +function Invoke-CippTestEIDSCAAP06 { <# .SYNOPSIS Authorization Policy - Email Validation Join diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP07.md b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAP07.md similarity index 100% rename from Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP07.md rename to Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAP07.md diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP07.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAP07.ps1 similarity index 98% rename from Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP07.ps1 rename to Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAP07.ps1 index 89b0152d6446..dfbab3fe5197 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP07.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAP07.ps1 @@ -1,4 +1,4 @@ -function Invoke-CippTestEIDSCA_AP07 { +function Invoke-CippTestEIDSCAAP07 { <# .SYNOPSIS Authorization Policy - Guest User Access diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP08.md b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAP08.md similarity index 100% rename from Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP08.md rename to Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAP08.md diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP08.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAP08.ps1 similarity index 98% rename from Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP08.ps1 rename to Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAP08.ps1 index 2da9571bb90d..b727af3c1ba0 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP08.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAP08.ps1 @@ -1,4 +1,4 @@ -function Invoke-CippTestEIDSCA_AP08 { +function Invoke-CippTestEIDSCAAP08 { <# .SYNOPSIS Authorization Policy - User Consent Policy diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP09.md b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAP09.md similarity index 100% rename from Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP09.md rename to Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAP09.md diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP09.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAP09.ps1 similarity index 98% rename from Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP09.ps1 rename to Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAP09.ps1 index c22d0408da89..21919d35321d 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP09.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAP09.ps1 @@ -1,4 +1,4 @@ -function Invoke-CippTestEIDSCA_AP09 { +function Invoke-CippTestEIDSCAAP09 { <# .SYNOPSIS Authorization Policy - Consent for Risky Apps diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP10.md b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAP10.md similarity index 100% rename from Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP10.md rename to Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAP10.md diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP10.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAP10.ps1 similarity index 98% rename from Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP10.ps1 rename to Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAP10.ps1 index 51ec0f4ee107..1de903c42d9d 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP10.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAP10.ps1 @@ -1,4 +1,4 @@ -function Invoke-CippTestEIDSCA_AP10 { +function Invoke-CippTestEIDSCAAP10 { <# .SYNOPSIS Authorization Policy - Users Can Create Apps diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP14.md b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAP14.md similarity index 100% rename from Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP14.md rename to Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAP14.md diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP14.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAP14.ps1 similarity index 98% rename from Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP14.ps1 rename to Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAP14.ps1 index 7270f3a15eef..0e66fb8b39d4 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AP14.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAP14.ps1 @@ -1,4 +1,4 @@ -function Invoke-CippTestEIDSCA_AP14 { +function Invoke-CippTestEIDSCAAP14 { <# .SYNOPSIS Authorization Policy - Users Can Read Other Users diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AS04.md b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAS04.md similarity index 100% rename from Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AS04.md rename to Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAS04.md diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AS04.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAS04.ps1 similarity index 98% rename from Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AS04.ps1 rename to Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAS04.ps1 index 86265496c2e7..e40eacd00391 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AS04.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAS04.ps1 @@ -1,4 +1,4 @@ -function Invoke-CippTestEIDSCA_AS04 { +function Invoke-CippTestEIDSCAAS04 { <# .SYNOPSIS SMS - No Sign-In diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AT01.md b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAT01.md similarity index 100% rename from Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AT01.md rename to Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAT01.md diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AT01.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAT01.ps1 similarity index 98% rename from Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AT01.ps1 rename to Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAT01.ps1 index 07953fcf6663..4cd6c62e1388 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AT01.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAT01.ps1 @@ -1,4 +1,4 @@ -function Invoke-CippTestEIDSCA_AT01 { +function Invoke-CippTestEIDSCAAT01 { <# .SYNOPSIS Temp Access Pass - State diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AT02.md b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAT02.md similarity index 100% rename from Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AT02.md rename to Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAT02.md diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AT02.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAT02.ps1 similarity index 98% rename from Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AT02.ps1 rename to Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAT02.ps1 index c03b590b2440..0d8b91f7bb88 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AT02.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAT02.ps1 @@ -1,4 +1,4 @@ -function Invoke-CippTestEIDSCA_AT02 { +function Invoke-CippTestEIDSCAAT02 { <# .SYNOPSIS Temp Access Pass - One-Time diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AV01.md b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAV01.md similarity index 100% rename from Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AV01.md rename to Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAV01.md diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AV01.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAV01.ps1 similarity index 98% rename from Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AV01.ps1 rename to Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAV01.ps1 index 10c666e920b6..3183898c4494 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_AV01.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAAV01.ps1 @@ -1,4 +1,4 @@ -function Invoke-CippTestEIDSCA_AV01 { +function Invoke-CippTestEIDSCAAV01 { <# .SYNOPSIS Voice Call - Disabled diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CP01.md b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCACP01.md similarity index 100% rename from Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CP01.md rename to Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCACP01.md diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CP01.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCACP01.ps1 similarity index 98% rename from Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CP01.ps1 rename to Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCACP01.ps1 index 52fc5d6ef7ee..00a54c296493 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CP01.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCACP01.ps1 @@ -1,4 +1,4 @@ -function Invoke-CippTestEIDSCA_CP01 { +function Invoke-CippTestEIDSCACP01 { <# .SYNOPSIS Consent Policy Settings - Group owner consent for apps accessing data diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CP03.md b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCACP03.md similarity index 100% rename from Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CP03.md rename to Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCACP03.md diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CP03.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCACP03.ps1 similarity index 98% rename from Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CP03.ps1 rename to Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCACP03.ps1 index a082e0a1d824..4ac4ac664034 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CP03.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCACP03.ps1 @@ -1,4 +1,4 @@ -function Invoke-CippTestEIDSCA_CP03 { +function Invoke-CippTestEIDSCACP03 { <# .SYNOPSIS Consent Policy Settings - Block user consent for risky apps diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CP04.md b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCACP04.md similarity index 100% rename from Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CP04.md rename to Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCACP04.md diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CP04.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCACP04.ps1 similarity index 98% rename from Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CP04.ps1 rename to Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCACP04.ps1 index 34d166359dbe..3b960e183ee1 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CP04.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCACP04.ps1 @@ -1,4 +1,4 @@ -function Invoke-CippTestEIDSCA_CP04 { +function Invoke-CippTestEIDSCACP04 { <# .SYNOPSIS Consent Policy Settings - Users can request admin consent diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR01.md b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCACR01.md similarity index 100% rename from Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR01.md rename to Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCACR01.md diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR01.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCACR01.ps1 similarity index 98% rename from Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR01.ps1 rename to Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCACR01.ps1 index 938258752672..c75af77fc7d2 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR01.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCACR01.ps1 @@ -1,4 +1,4 @@ -function Invoke-CippTestEIDSCA_CR01 { +function Invoke-CippTestEIDSCACR01 { <# .SYNOPSIS Admin Consent - Enabled diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR02.md b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCACR02.md similarity index 100% rename from Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR02.md rename to Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCACR02.md diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR02.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCACR02.ps1 similarity index 98% rename from Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR02.ps1 rename to Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCACR02.ps1 index c4077a2304b0..a99a1c1d3502 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR02.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCACR02.ps1 @@ -1,4 +1,4 @@ -function Invoke-CippTestEIDSCA_CR02 { +function Invoke-CippTestEIDSCACR02 { <# .SYNOPSIS Admin Consent - Notify Reviewers diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR03.md b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCACR03.md similarity index 100% rename from Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR03.md rename to Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCACR03.md diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR03.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCACR03.ps1 similarity index 98% rename from Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR03.ps1 rename to Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCACR03.ps1 index 489aceb9b503..ead722796b07 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR03.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCACR03.ps1 @@ -1,4 +1,4 @@ -function Invoke-CippTestEIDSCA_CR03 { +function Invoke-CippTestEIDSCACR03 { <# .SYNOPSIS Admin Consent - Reminders diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR04.md b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCACR04.md similarity index 100% rename from Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR04.md rename to Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCACR04.md diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR04.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCACR04.ps1 similarity index 98% rename from Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR04.ps1 rename to Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCACR04.ps1 index 5ef6f66fa612..01c555a480e4 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_CR04.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCACR04.ps1 @@ -1,4 +1,4 @@ -function Invoke-CippTestEIDSCA_CR04 { +function Invoke-CippTestEIDSCACR04 { <# .SYNOPSIS Admin Consent - Duration diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR01.md b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAPR01.md similarity index 100% rename from Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR01.md rename to Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAPR01.md diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR01.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAPR01.ps1 similarity index 98% rename from Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR01.ps1 rename to Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAPR01.ps1 index c6cf610d780e..fb00648135f0 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR01.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAPR01.ps1 @@ -1,4 +1,4 @@ -function Invoke-CippTestEIDSCA_PR01 { +function Invoke-CippTestEIDSCAPR01 { <# .SYNOPSIS Password Rule Settings - Password Protection Mode diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR02.md b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAPR02.md similarity index 100% rename from Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR02.md rename to Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAPR02.md diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR02.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAPR02.ps1 similarity index 98% rename from Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR02.ps1 rename to Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAPR02.ps1 index a42fcdbe750b..201e4d78f156 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR02.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAPR02.ps1 @@ -1,4 +1,4 @@ -function Invoke-CippTestEIDSCA_PR02 { +function Invoke-CippTestEIDSCAPR02 { <# .SYNOPSIS Password Rule Settings - Enable password protection on Windows Server Active Directory diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR03.md b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAPR03.md similarity index 100% rename from Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR03.md rename to Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAPR03.md diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR03.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAPR03.ps1 similarity index 98% rename from Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR03.ps1 rename to Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAPR03.ps1 index 36c2a5bd25fa..4efd63038aec 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR03.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAPR03.ps1 @@ -1,4 +1,4 @@ -function Invoke-CippTestEIDSCA_PR03 { +function Invoke-CippTestEIDSCAPR03 { <# .SYNOPSIS Password Rule Settings - Enforce custom list diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR05.md b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAPR05.md similarity index 100% rename from Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR05.md rename to Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAPR05.md diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR05.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAPR05.ps1 similarity index 98% rename from Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR05.ps1 rename to Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAPR05.ps1 index cf6906468f98..f711d13880f5 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR05.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAPR05.ps1 @@ -1,4 +1,4 @@ -function Invoke-CippTestEIDSCA_PR05 { +function Invoke-CippTestEIDSCAPR05 { <# .SYNOPSIS Password Rule Settings - Lockout duration in seconds diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR06.md b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAPR06.md similarity index 100% rename from Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR06.md rename to Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAPR06.md diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR06.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAPR06.ps1 similarity index 98% rename from Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR06.ps1 rename to Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAPR06.ps1 index 1a10d67570fb..24c2de9781d5 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_PR06.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAPR06.ps1 @@ -1,4 +1,4 @@ -function Invoke-CippTestEIDSCA_PR06 { +function Invoke-CippTestEIDSCAPR06 { <# .SYNOPSIS Password Rule Settings - Lockout threshold diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_ST08.md b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAST08.md similarity index 100% rename from Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_ST08.md rename to Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAST08.md diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_ST08.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAST08.ps1 similarity index 98% rename from Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_ST08.ps1 rename to Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAST08.ps1 index 298b94debdaf..eb362a64c07b 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_ST08.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAST08.ps1 @@ -1,4 +1,4 @@ -function Invoke-CippTestEIDSCA_ST08 { +function Invoke-CippTestEIDSCAST08 { <# .SYNOPSIS Classification and M365 Groups - Allow Guests to become Group Owner diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_ST09.md b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAST09.md similarity index 100% rename from Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_ST09.md rename to Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAST09.md diff --git a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_ST09.ps1 b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAST09.ps1 similarity index 98% rename from Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_ST09.ps1 rename to Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAST09.ps1 index ca6e9551c342..161a8ce28821 100644 --- a/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCA_ST09.ps1 +++ b/Modules/CIPPCore/Public/Tests/EIDSCA/Identity/Invoke-CippTestEIDSCAST09.ps1 @@ -1,4 +1,4 @@ -function Invoke-CippTestEIDSCA_ST09 { +function Invoke-CippTestEIDSCAST09 { <# .SYNOPSIS Classification and M365 Groups - Allow Guests to have access to groups content From b6977099d49081e861de73e440dcb1ee9802616a Mon Sep 17 00:00:00 2001 From: John Duprey Date: Sun, 11 Jan 2026 14:40:43 -0500 Subject: [PATCH 116/503] Add Set-CIPPDBCacheMailboxes function Introduces Set-CIPPDBCacheMailboxes to cache mailboxes, CAS mailboxes, and mailbox permissions for a specified tenant. Includes logging and error handling for the caching process. --- .../Public/Set-CIPPDBCacheMailboxes.ps1 | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 Modules/CIPPCore/Public/Set-CIPPDBCacheMailboxes.ps1 diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheMailboxes.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheMailboxes.ps1 new file mode 100644 index 000000000000..3e3d93f776ba --- /dev/null +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheMailboxes.ps1 @@ -0,0 +1,73 @@ +function Set-CIPPDBCacheMailboxes { + <# + .SYNOPSIS + Caches all mailboxes, CAS mailboxes, and mailbox permissions for a tenant + + .PARAMETER TenantFilter + The tenant to cache mailboxes for + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter + ) + + try { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching mailboxes' -sev Info + + # Get mailboxes with select properties + $Select = 'id,ExchangeGuid,ArchiveGuid,UserPrincipalName,DisplayName,PrimarySMTPAddress,RecipientType,RecipientTypeDetails,EmailAddresses,WhenSoftDeleted,IsInactiveMailbox,ForwardingSmtpAddress,DeliverToMailboxAndForward,ForwardingAddress,HiddenFromAddressListsEnabled,ExternalDirectoryObjectId,MessageCopyForSendOnBehalfEnabled,MessageCopyForSentAsEnabled' + $ExoRequest = @{ + tenantid = $TenantFilter + cmdlet = 'Get-Mailbox' + cmdParams = @{} + Select = $Select + } + $Mailboxes = (New-ExoRequest @ExoRequest) | Select-Object id, ExchangeGuid, ArchiveGuid, WhenSoftDeleted, + @{ Name = 'UPN'; Expression = { $_.'UserPrincipalName' } }, + @{ Name = 'displayName'; Expression = { $_.'DisplayName' } }, + @{ Name = 'primarySmtpAddress'; Expression = { $_.'PrimarySMTPAddress' } }, + @{ Name = 'recipientType'; Expression = { $_.'RecipientType' } }, + @{ Name = 'recipientTypeDetails'; Expression = { $_.'RecipientTypeDetails' } }, + @{ Name = 'AdditionalEmailAddresses'; Expression = { ($_.'EmailAddresses' | Where-Object { $_ -clike 'smtp:*' }).Replace('smtp:', '') -join ', ' } }, + @{ Name = 'ForwardingSmtpAddress'; Expression = { $_.'ForwardingSmtpAddress' -replace 'smtp:', '' } }, + @{ Name = 'InternalForwardingAddress'; Expression = { $_.'ForwardingAddress' } }, + DeliverToMailboxAndForward, + HiddenFromAddressListsEnabled, + ExternalDirectoryObjectId, + MessageCopyForSendOnBehalfEnabled, + MessageCopyForSentAsEnabled + + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Mailboxes' -Data $Mailboxes + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Mailboxes' -Data $Mailboxes -Count + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached mailboxes successfully' -sev Info + + # Get CAS mailboxes + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching CAS mailboxes' -sev Info + $CASMailboxes = New-GraphGetRequest -uri "https://outlook.office365.com/adminapi/beta/$($TenantFilter)/CasMailbox" -Tenantid $TenantFilter -scope 'ExchangeOnline' -noPagination $true + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'CASMailbox' -Data $CASMailboxes + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'CASMailbox' -Data $CASMailboxes -Count + $CASMailboxes = $null + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached CAS mailboxes successfully' -sev Info + + # Get mailbox permissions using bulk request + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching mailbox permissions' -sev Info + $ExoBulkRequests = foreach ($Mailbox in $Mailboxes) { + @{ + CmdletInput = @{ + CmdletName = 'Get-MailboxPermission' + Parameters = @{ Identity = $Mailbox.UPN } + } + } + } + $MailboxPermissions = New-ExoBulkRequest -cmdletArray @($ExoBulkRequests) -tenantid $TenantFilter + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'MailboxPermissions' -Data $MailboxPermissions + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'MailboxPermissions' -Data $MailboxPermissions -Count + $MailboxPermissions = $null + $Mailboxes = $null + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached mailbox permissions successfully' -sev Info + + } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache mailboxes: $($_.Exception.Message)" -sev Error + } +} From 5d9ad103eda6fd22850684fbb6137058d9be96f2 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Sun, 11 Jan 2026 15:08:16 -0500 Subject: [PATCH 117/503] Sanitize RowKey values in Add-CIPPDbItem Introduced Format-RowKey helper to remove disallowed characters from RowKey values, ensuring compatibility with Azure Table Storage. Also improved ItemId selection logic for better entity identification. --- Modules/CIPPCore/Public/Add-CIPPDbItem.ps1 | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/Modules/CIPPCore/Public/Add-CIPPDbItem.ps1 b/Modules/CIPPCore/Public/Add-CIPPDbItem.ps1 index 1e15830182fd..913083ba513d 100644 --- a/Modules/CIPPCore/Public/Add-CIPPDbItem.ps1 +++ b/Modules/CIPPCore/Public/Add-CIPPDbItem.ps1 @@ -43,11 +43,20 @@ function Add-CIPPDbItem { try { $Table = Get-CippTable -tablename 'CippReportingDB' + # Helper function to format RowKey values by removing disallowed characters + function Format-RowKey { + param([string]$RowKey) + + # Remove disallowed characters: / \ # ? and control characters (U+0000 to U+001F and U+007F to U+009F) + $sanitized = $RowKey -replace '[/\\#?]', '_' -replace '[\u0000-\u001F\u007F-\u009F]', '' + + return $sanitized + } if ($Count) { $Entity = @{ PartitionKey = $TenantFilter - RowKey = "$Type-Count" + RowKey = Format-RowKey "$Type-Count" DataCount = [int]$Data.Count } @@ -61,10 +70,10 @@ function Add-CIPPDbItem { Remove-AzDataTableEntity @Table -Entity $ExistingEntities -Force | Out-Null } $Entities = foreach ($Item in $Data) { - $ItemId = $Item.id ? $Item.id : $item.skuId + $ItemId = $Item.id ?? $Item.ExternalDirectoryObjectId ?? $Item.Identity ?? $Item.skuId @{ PartitionKey = $TenantFilter - RowKey = "$Type-$ItemId" + RowKey = Format-RowKey "$Type-$ItemId" Data = [string]($Item | ConvertTo-Json -Depth 10 -Compress) Type = $Type } From 91ad846f124d1465f04894040eb2319b61f6f895 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Sun, 11 Jan 2026 15:08:29 -0500 Subject: [PATCH 118/503] Add Exchange license checks and mailbox cache support Introduces Exchange license capability detection and conditional cache collection for Exchange Online features. Refactors cache collection logic to use a switch on $Type, enabling targeted mailbox cache collection and improving modularity for future cache types. --- .../Push-CIPPDBCacheData.ps1 | 631 +++++++++--------- 1 file changed, 327 insertions(+), 304 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 index ef3f7df48952..4ddbbf42238a 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 @@ -13,6 +13,8 @@ function Push-CIPPDBCacheData { param($Item) Write-Host "Starting cache collection for tenant: $($Item.TenantFilter) - Queue: $($Item.QueueName) (ID: $($Item.QueueId))" $TenantFilter = $Item.TenantFilter + $Type = $Item.Type ?? 'Default' + #This collects all data for a tenant and caches it in the CIPP Reporting database. DO NOT ADD PROCESSING OR LOGIC HERE. #The point of this file is to always be <10 minutes execution time. try { @@ -22,313 +24,334 @@ function Push-CIPPDBCacheData { $IntuneCapable = Test-CIPPStandardLicense -StandardName 'IntuneLicenseCheck' -TenantFilter $TenantFilter -RequiredCapabilities @('INTUNE_A', 'MDM_Services', 'EMS', 'SCCM', 'MICROSOFTINTUNEPLAN1') -SkipLog $ConditionalAccessCapable = Test-CIPPStandardLicense -StandardName 'ConditionalAccessLicenseCheck' -TenantFilter $TenantFilter -RequiredCapabilities @('AAD_PREMIUM', 'AAD_PREMIUM_P2') -SkipLog $AzureADPremiumP2Capable = Test-CIPPStandardLicense -StandardName 'AzureADPremiumP2LicenseCheck' -TenantFilter $TenantFilter -RequiredCapabilities @('AAD_PREMIUM_P2') -SkipLog - - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "License capabilities - Intune: $IntuneCapable, Conditional Access: $ConditionalAccessCapable, Azure AD Premium P2: $AzureADPremiumP2Capable" -sev Info - - #region All Licenses - Basic tenant data collection - Write-Host 'Getting cache for Users' - try { Set-CIPPDBCacheUsers -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Users collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for Groups' - try { Set-CIPPDBCacheGroups -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Groups collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for Guests' - try { Set-CIPPDBCacheGuests -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Guests collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for ServicePrincipals' - try { Set-CIPPDBCacheServicePrincipals -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ServicePrincipals collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for Apps' - try { Set-CIPPDBCacheApps -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Apps collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for Devices' - try { Set-CIPPDBCacheDevices -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Devices collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for Organization' - try { Set-CIPPDBCacheOrganization -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Organization collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for Roles' - try { Set-CIPPDBCacheRoles -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Roles collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for AdminConsentRequestPolicy' - try { Set-CIPPDBCacheAdminConsentRequestPolicy -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "AdminConsentRequestPolicy collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for AuthorizationPolicy' - try { Set-CIPPDBCacheAuthorizationPolicy -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "AuthorizationPolicy collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for AuthenticationMethodsPolicy' - try { Set-CIPPDBCacheAuthenticationMethodsPolicy -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "AuthenticationMethodsPolicy collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for DeviceSettings' - try { Set-CIPPDBCacheDeviceSettings -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "DeviceSettings collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for DirectoryRecommendations' - try { Set-CIPPDBCacheDirectoryRecommendations -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "DirectoryRecommendations collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for CrossTenantAccessPolicy' - try { Set-CIPPDBCacheCrossTenantAccessPolicy -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "CrossTenantAccessPolicy collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for DefaultAppManagementPolicy' - try { Set-CIPPDBCacheDefaultAppManagementPolicy -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "DefaultAppManagementPolicy collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for Settings' - try { Set-CIPPDBCacheSettings -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Settings collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for SecureScore' - try { Set-CIPPDBCacheSecureScore -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "SecureScore collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for PIMSettings' - try { Set-CIPPDBCachePIMSettings -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "PIMSettings collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for Domains' - try { Set-CIPPDBCacheDomains -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Domains collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for RoleEligibilitySchedules' - try { Set-CIPPDBCacheRoleEligibilitySchedules -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "RoleEligibilitySchedules collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for RoleManagementPolicies' - try { Set-CIPPDBCacheRoleManagementPolicies -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "RoleManagementPolicies collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for RoleAssignmentScheduleInstances' - try { Set-CIPPDBCacheRoleAssignmentScheduleInstances -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "RoleAssignmentScheduleInstances collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for B2BManagementPolicy' - try { Set-CIPPDBCacheB2BManagementPolicy -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "B2BManagementPolicy collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for AuthenticationFlowsPolicy' - try { Set-CIPPDBCacheAuthenticationFlowsPolicy -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "AuthenticationFlowsPolicy collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for DeviceRegistrationPolicy' - try { Set-CIPPDBCacheDeviceRegistrationPolicy -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "DeviceRegistrationPolicy collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for CredentialUserRegistrationDetails' - try { Set-CIPPDBCacheCredentialUserRegistrationDetails -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "CredentialUserRegistrationDetails collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for UserRegistrationDetails' - try { Set-CIPPDBCacheUserRegistrationDetails -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "UserRegistrationDetails collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for OAuth2PermissionGrants' - try { Set-CIPPDBCacheOAuth2PermissionGrants -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "OAuth2PermissionGrants collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for AppRoleAssignments' - try { Set-CIPPDBCacheAppRoleAssignments -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "AppRoleAssignments collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for ExoAntiPhishPolicies' - try { Set-CIPPDBCacheExoAntiPhishPolicies -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoAntiPhishPolicies collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for ExoMalwareFilterPolicies' - try { Set-CIPPDBCacheExoMalwareFilterPolicies -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoMalwareFilterPolicies collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for ExoSafeLinksPolicies' - try { Set-CIPPDBCacheExoSafeLinksPolicies -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoSafeLinksPolicies collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for ExoSafeAttachmentPolicies' - try { Set-CIPPDBCacheExoSafeAttachmentPolicies -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoSafeAttachmentPolicies collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for ExoTransportRules' - try { Set-CIPPDBCacheExoTransportRules -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoTransportRules collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for ExoDkimSigningConfig' - try { Set-CIPPDBCacheExoDkimSigningConfig -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoDkimSigningConfig collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for ExoOrganizationConfig' - try { Set-CIPPDBCacheExoOrganizationConfig -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoOrganizationConfig collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for ExoAcceptedDomains' - try { Set-CIPPDBCacheExoAcceptedDomains -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoAcceptedDomains collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for ExoHostedContentFilterPolicy' - try { Set-CIPPDBCacheExoHostedContentFilterPolicy -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoHostedContentFilterPolicy collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for ExoHostedOutboundSpamFilterPolicy' - try { Set-CIPPDBCacheExoHostedOutboundSpamFilterPolicy -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoHostedOutboundSpamFilterPolicy collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for ExoAntiPhishPolicy' - try { Set-CIPPDBCacheExoAntiPhishPolicy -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoAntiPhishPolicy collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for ExoSafeLinksPolicy' - try { Set-CIPPDBCacheExoSafeLinksPolicy -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoSafeLinksPolicy collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for ExoSafeAttachmentPolicy' - try { Set-CIPPDBCacheExoSafeAttachmentPolicy -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoSafeAttachmentPolicy collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for ExoMalwareFilterPolicy' - try { Set-CIPPDBCacheExoMalwareFilterPolicy -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoMalwareFilterPolicy collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for ExoAtpPolicyForO365' - try { Set-CIPPDBCacheExoAtpPolicyForO365 -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoAtpPolicyForO365 collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for ExoQuarantinePolicy' - try { Set-CIPPDBCacheExoQuarantinePolicy -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoQuarantinePolicy collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for ExoRemoteDomain' - try { Set-CIPPDBCacheExoRemoteDomain -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoRemoteDomain collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for License Overview' - try { Set-CIPPDBCacheLicenseOverview -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "License Overview collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for MFA State' - try { Set-CIPPDBCacheMFAState -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "MFA State collection failed: $($_.Exception.Message)" -sev Error - } - #endregion All Licenses - - #region Conditional Access Licensed - Azure AD Premium features - if ($ConditionalAccessCapable) { - Write-Host 'Getting cache for ConditionalAccessPolicies' - try { Set-CIPPDBCacheConditionalAccessPolicies -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ConditionalAccessPolicies collection failed: $($_.Exception.Message)" -sev Error - } - } else { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Skipping Conditional Access data collection - tenant does not have required license' -sev Info - } - #endregion Conditional Access Licensed - - #region Azure AD Premium P2 - Identity Protection features - if ($AzureADPremiumP2Capable) { - Write-Host 'Getting cache for RiskyUsers' - try { Set-CIPPDBCacheRiskyUsers -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "RiskyUsers collection failed: $($_.Exception.Message)" -sev Error + $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 + + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "License capabilities - Intune: $IntuneCapable, Conditional Access: $ConditionalAccessCapable, Azure AD Premium P2: $AzureADPremiumP2Capable, Exchange: $ExchangeCapable" -sev Info + + switch ($Type) { + 'Default' { + #region All Licenses - Basic tenant data collection + Write-Host 'Getting cache for Users' + try { Set-CIPPDBCacheUsers -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Users collection failed: $($_.Exception.Message)" -sev Error + } + + Write-Host 'Getting cache for Groups' + try { Set-CIPPDBCacheGroups -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Groups collection failed: $($_.Exception.Message)" -sev Error + } + + Write-Host 'Getting cache for Guests' + try { Set-CIPPDBCacheGuests -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Guests collection failed: $($_.Exception.Message)" -sev Error + } + + Write-Host 'Getting cache for ServicePrincipals' + try { Set-CIPPDBCacheServicePrincipals -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ServicePrincipals collection failed: $($_.Exception.Message)" -sev Error + } + + Write-Host 'Getting cache for Apps' + try { Set-CIPPDBCacheApps -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Apps collection failed: $($_.Exception.Message)" -sev Error + } + + Write-Host 'Getting cache for Devices' + try { Set-CIPPDBCacheDevices -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Devices collection failed: $($_.Exception.Message)" -sev Error + } + + Write-Host 'Getting cache for Organization' + try { Set-CIPPDBCacheOrganization -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Organization collection failed: $($_.Exception.Message)" -sev Error + } + + Write-Host 'Getting cache for Roles' + try { Set-CIPPDBCacheRoles -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Roles collection failed: $($_.Exception.Message)" -sev Error + } + + Write-Host 'Getting cache for AdminConsentRequestPolicy' + try { Set-CIPPDBCacheAdminConsentRequestPolicy -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "AdminConsentRequestPolicy collection failed: $($_.Exception.Message)" -sev Error + } + + Write-Host 'Getting cache for AuthorizationPolicy' + try { Set-CIPPDBCacheAuthorizationPolicy -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "AuthorizationPolicy collection failed: $($_.Exception.Message)" -sev Error + } + + Write-Host 'Getting cache for AuthenticationMethodsPolicy' + try { Set-CIPPDBCacheAuthenticationMethodsPolicy -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "AuthenticationMethodsPolicy collection failed: $($_.Exception.Message)" -sev Error + } + + Write-Host 'Getting cache for DeviceSettings' + try { Set-CIPPDBCacheDeviceSettings -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "DeviceSettings collection failed: $($_.Exception.Message)" -sev Error + } + + Write-Host 'Getting cache for DirectoryRecommendations' + try { Set-CIPPDBCacheDirectoryRecommendations -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "DirectoryRecommendations collection failed: $($_.Exception.Message)" -sev Error + } + + Write-Host 'Getting cache for CrossTenantAccessPolicy' + try { Set-CIPPDBCacheCrossTenantAccessPolicy -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "CrossTenantAccessPolicy collection failed: $($_.Exception.Message)" -sev Error + } + + Write-Host 'Getting cache for DefaultAppManagementPolicy' + try { Set-CIPPDBCacheDefaultAppManagementPolicy -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "DefaultAppManagementPolicy collection failed: $($_.Exception.Message)" -sev Error + } + + Write-Host 'Getting cache for Settings' + try { Set-CIPPDBCacheSettings -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Settings collection failed: $($_.Exception.Message)" -sev Error + } + + Write-Host 'Getting cache for SecureScore' + try { Set-CIPPDBCacheSecureScore -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "SecureScore collection failed: $($_.Exception.Message)" -sev Error + } + + Write-Host 'Getting cache for PIMSettings' + try { Set-CIPPDBCachePIMSettings -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "PIMSettings collection failed: $($_.Exception.Message)" -sev Error + } + + Write-Host 'Getting cache for Domains' + try { Set-CIPPDBCacheDomains -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Domains collection failed: $($_.Exception.Message)" -sev Error + } + + Write-Host 'Getting cache for RoleEligibilitySchedules' + try { Set-CIPPDBCacheRoleEligibilitySchedules -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "RoleEligibilitySchedules collection failed: $($_.Exception.Message)" -sev Error + } + + Write-Host 'Getting cache for RoleManagementPolicies' + try { Set-CIPPDBCacheRoleManagementPolicies -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "RoleManagementPolicies collection failed: $($_.Exception.Message)" -sev Error + } + + Write-Host 'Getting cache for RoleAssignmentScheduleInstances' + try { Set-CIPPDBCacheRoleAssignmentScheduleInstances -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "RoleAssignmentScheduleInstances collection failed: $($_.Exception.Message)" -sev Error + } + + Write-Host 'Getting cache for B2BManagementPolicy' + try { Set-CIPPDBCacheB2BManagementPolicy -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "B2BManagementPolicy collection failed: $($_.Exception.Message)" -sev Error + } + + Write-Host 'Getting cache for AuthenticationFlowsPolicy' + try { Set-CIPPDBCacheAuthenticationFlowsPolicy -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "AuthenticationFlowsPolicy collection failed: $($_.Exception.Message)" -sev Error + } + + Write-Host 'Getting cache for DeviceRegistrationPolicy' + try { Set-CIPPDBCacheDeviceRegistrationPolicy -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "DeviceRegistrationPolicy collection failed: $($_.Exception.Message)" -sev Error + } + + Write-Host 'Getting cache for CredentialUserRegistrationDetails' + try { Set-CIPPDBCacheCredentialUserRegistrationDetails -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "CredentialUserRegistrationDetails collection failed: $($_.Exception.Message)" -sev Error + } + + Write-Host 'Getting cache for UserRegistrationDetails' + try { Set-CIPPDBCacheUserRegistrationDetails -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "UserRegistrationDetails collection failed: $($_.Exception.Message)" -sev Error + } + + Write-Host 'Getting cache for OAuth2PermissionGrants' + try { Set-CIPPDBCacheOAuth2PermissionGrants -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "OAuth2PermissionGrants collection failed: $($_.Exception.Message)" -sev Error + } + + Write-Host 'Getting cache for AppRoleAssignments' + try { Set-CIPPDBCacheAppRoleAssignments -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "AppRoleAssignments collection failed: $($_.Exception.Message)" -sev Error + } + + Write-Host 'Getting cache for License Overview' + try { Set-CIPPDBCacheLicenseOverview -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "License Overview collection failed: $($_.Exception.Message)" -sev Error + } + + Write-Host 'Getting cache for MFA State' + try { Set-CIPPDBCacheMFAState -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "MFA State collection failed: $($_.Exception.Message)" -sev Error + } + #endregion All Licenses + + #region Exchange Licensed - Exchange Online features + if ($ExchangeCapable) { + Write-Host 'Getting cache for ExoAntiPhishPolicies' + try { Set-CIPPDBCacheExoAntiPhishPolicies -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoAntiPhishPolicies collection failed: $($_.Exception.Message)" -sev Error + } + + Write-Host 'Getting cache for ExoMalwareFilterPolicies' + try { Set-CIPPDBCacheExoMalwareFilterPolicies -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoMalwareFilterPolicies collection failed: $($_.Exception.Message)" -sev Error + } + + Write-Host 'Getting cache for ExoSafeLinksPolicies' + try { Set-CIPPDBCacheExoSafeLinksPolicies -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoSafeLinksPolicies collection failed: $($_.Exception.Message)" -sev Error + } + + Write-Host 'Getting cache for ExoSafeAttachmentPolicies' + try { Set-CIPPDBCacheExoSafeAttachmentPolicies -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoSafeAttachmentPolicies collection failed: $($_.Exception.Message)" -sev Error + } + + Write-Host 'Getting cache for ExoTransportRules' + try { Set-CIPPDBCacheExoTransportRules -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoTransportRules collection failed: $($_.Exception.Message)" -sev Error + } + + Write-Host 'Getting cache for ExoDkimSigningConfig' + try { Set-CIPPDBCacheExoDkimSigningConfig -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoDkimSigningConfig collection failed: $($_.Exception.Message)" -sev Error + } + + Write-Host 'Getting cache for ExoOrganizationConfig' + try { Set-CIPPDBCacheExoOrganizationConfig -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoOrganizationConfig collection failed: $($_.Exception.Message)" -sev Error + } + + Write-Host 'Getting cache for ExoAcceptedDomains' + try { Set-CIPPDBCacheExoAcceptedDomains -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoAcceptedDomains collection failed: $($_.Exception.Message)" -sev Error + } + + Write-Host 'Getting cache for ExoHostedContentFilterPolicy' + try { Set-CIPPDBCacheExoHostedContentFilterPolicy -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoHostedContentFilterPolicy collection failed: $($_.Exception.Message)" -sev Error + } + + Write-Host 'Getting cache for ExoHostedOutboundSpamFilterPolicy' + try { Set-CIPPDBCacheExoHostedOutboundSpamFilterPolicy -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoHostedOutboundSpamFilterPolicy collection failed: $($_.Exception.Message)" -sev Error + } + + Write-Host 'Getting cache for ExoAntiPhishPolicy' + try { Set-CIPPDBCacheExoAntiPhishPolicy -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoAntiPhishPolicy collection failed: $($_.Exception.Message)" -sev Error + } + + Write-Host 'Getting cache for ExoSafeLinksPolicy' + try { Set-CIPPDBCacheExoSafeLinksPolicy -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoSafeLinksPolicy collection failed: $($_.Exception.Message)" -sev Error + } + + Write-Host 'Getting cache for ExoSafeAttachmentPolicy' + try { Set-CIPPDBCacheExoSafeAttachmentPolicy -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoSafeAttachmentPolicy collection failed: $($_.Exception.Message)" -sev Error + } + + Write-Host 'Getting cache for ExoMalwareFilterPolicy' + try { Set-CIPPDBCacheExoMalwareFilterPolicy -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoMalwareFilterPolicy collection failed: $($_.Exception.Message)" -sev Error + } + + Write-Host 'Getting cache for ExoAtpPolicyForO365' + try { Set-CIPPDBCacheExoAtpPolicyForO365 -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoAtpPolicyForO365 collection failed: $($_.Exception.Message)" -sev Error + } + + Write-Host 'Getting cache for ExoQuarantinePolicy' + try { Set-CIPPDBCacheExoQuarantinePolicy -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoQuarantinePolicy collection failed: $($_.Exception.Message)" -sev Error + } + + Write-Host 'Getting cache for ExoRemoteDomain' + try { Set-CIPPDBCacheExoRemoteDomain -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoRemoteDomain collection failed: $($_.Exception.Message)" -sev Error + } + } else { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Skipping Exchange Online data collection - tenant does not have required license' -sev Info + } + #endregion Exchange Licensed + + #region Conditional Access Licensed - Azure AD Premium features + if ($ConditionalAccessCapable) { + Write-Host 'Getting cache for ConditionalAccessPolicies' + try { Set-CIPPDBCacheConditionalAccessPolicies -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ConditionalAccessPolicies collection failed: $($_.Exception.Message)" -sev Error + } + } else { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Skipping Conditional Access data collection - tenant does not have required license' -sev Info + } + #endregion Conditional Access Licensed + + #region Azure AD Premium P2 - Identity Protection features + if ($AzureADPremiumP2Capable) { + Write-Host 'Getting cache for RiskyUsers' + try { Set-CIPPDBCacheRiskyUsers -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "RiskyUsers collection failed: $($_.Exception.Message)" -sev Error + } + + Write-Host 'Getting cache for RiskyServicePrincipals' + try { Set-CIPPDBCacheRiskyServicePrincipals -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "RiskyServicePrincipals collection failed: $($_.Exception.Message)" -sev Error + } + + Write-Host 'Getting cache for ServicePrincipalRiskDetections' + try { Set-CIPPDBCacheServicePrincipalRiskDetections -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ServicePrincipalRiskDetections collection failed: $($_.Exception.Message)" -sev Error + } + + Write-Host 'Getting cache for RiskDetections' + try { Set-CIPPDBCacheRiskDetections -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "RiskDetections collection failed: $($_.Exception.Message)" -sev Error + } + } else { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Skipping Azure AD Premium P2 Identity Protection data collection - tenant does not have required license' -sev Info + } + #endregion Azure AD Premium P2 + + #region Intune Licensed - Intune management features + if ($IntuneCapable) { + Write-Host 'Getting cache for ManagedDevices' + try { Set-CIPPDBCacheManagedDevices -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ManagedDevices collection failed: $($_.Exception.Message)" -sev Error + } + + Write-Host 'Getting cache for IntunePolicies' + try { Set-CIPPDBCacheIntunePolicies -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "IntunePolicies collection failed: $($_.Exception.Message)" -sev Error + } + + Write-Host 'Getting cache for ManagedDeviceEncryptionStates' + try { Set-CIPPDBCacheManagedDeviceEncryptionStates -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ManagedDeviceEncryptionStates collection failed: $($_.Exception.Message)" -sev Error + } + + Write-Host 'Getting cache for IntuneAppProtectionPolicies' + try { Set-CIPPDBCacheIntuneAppProtectionPolicies -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "IntuneAppProtectionPolicies collection failed: $($_.Exception.Message)" -sev Error + } + } else { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Skipping Intune data collection - tenant does not have required license' -sev Info + } + #endregion Intune Licensed } - - Write-Host 'Getting cache for RiskyServicePrincipals' - try { Set-CIPPDBCacheRiskyServicePrincipals -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "RiskyServicePrincipals collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for ServicePrincipalRiskDetections' - try { Set-CIPPDBCacheServicePrincipalRiskDetections -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ServicePrincipalRiskDetections collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for RiskDetections' - try { Set-CIPPDBCacheRiskDetections -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "RiskDetections collection failed: $($_.Exception.Message)" -sev Error - } - } else { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Skipping Azure AD Premium P2 Identity Protection data collection - tenant does not have required license' -sev Info - } - #endregion Azure AD Premium P2 - - #region Intune Licensed - Intune management features - if ($IntuneCapable) { - Write-Host 'Getting cache for ManagedDevices' - try { Set-CIPPDBCacheManagedDevices -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ManagedDevices collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for IntunePolicies' - try { Set-CIPPDBCacheIntunePolicies -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "IntunePolicies collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for ManagedDeviceEncryptionStates' - try { Set-CIPPDBCacheManagedDeviceEncryptionStates -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ManagedDeviceEncryptionStates collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for IntuneAppProtectionPolicies' - try { Set-CIPPDBCacheIntuneAppProtectionPolicies -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "IntuneAppProtectionPolicies collection failed: $($_.Exception.Message)" -sev Error + 'Mailboxes' { + if ($ExchangeCapable) { + Write-Host 'Getting cache for Mailboxes' + try { Set-CIPPDBCacheMailboxes -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Mailboxes collection failed: $($_.Exception.Message)" -sev Error + } + } else { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Skipping Mailboxes data collection - tenant does not have required Exchange license' -sev Info + } } - } else { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Skipping Intune data collection - tenant does not have required license' -sev Info } - #endregion Intune Licensed Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Completed database cache collection for tenant' -sev Info From d938047e13a68f0abeac6a9c1dae4d7b7f07d18d Mon Sep 17 00:00:00 2001 From: John Duprey Date: Sun, 11 Jan 2026 15:08:36 -0500 Subject: [PATCH 119/503] Add mailbox cache tasks to DB cache orchestrator The orchestrator now creates two cache collection tasks per tenant: one for general DB cache and one specifically for mailboxes. The total task count and queue entry logic have been updated to reflect this change. --- .../Start-CIPPDBCacheOrchestrator.ps1 | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-CIPPDBCacheOrchestrator.ps1 b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-CIPPDBCacheOrchestrator.ps1 index e6dc730a762c..51e0861ca294 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-CIPPDBCacheOrchestrator.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-CIPPDBCacheOrchestrator.ps1 @@ -22,14 +22,24 @@ function Start-CIPPDBCacheOrchestrator { return } - $Queue = New-CippQueueEntry -Name 'Database Cache Collection' -TotalTasks $TenantList.Count - $Batch = foreach ($Tenant in $TenantList) { - [PSCustomObject]@{ - FunctionName = 'CIPPDBCacheData' - TenantFilter = $Tenant.defaultDomainName - QueueId = $Queue.RowKey - QueueName = "DB Cache - $($Tenant.defaultDomainName)" - } + $TaskCount = $TenantList.Count * 2 + + $Queue = New-CippQueueEntry -Name 'Database Cache Collection' -TotalTasks $TaskCount + $Batch = [system.collections.generic.list[object]]::new() + foreach ($Tenant in $TenantList) { + $Batch.Add([PSCustomObject]@{ + FunctionName = 'CIPPDBCacheData' + TenantFilter = $Tenant.defaultDomainName + QueueId = $Queue.RowKey + QueueName = "DB Cache - $($Tenant.defaultDomainName)" + }) + $Batch.Add([PSCustomObject]@{ + FunctionName = 'CIPPDBCacheData' + TenantFilter = $Tenant.defaultDomainName + QueueId = $Queue.RowKey + Type = 'Mailboxes' + QueueName = "DB Cache Mailboxes - $($Tenant.defaultDomainName)" + }) } Write-Host "Created queue $($Queue.RowKey) for database cache collection of $($TenantList.Count) tenants" Write-Host "Starting batch of $($Batch.Count) cache collection activities" From 8dcee24d17268fb9efc8c8a4f0e1adc5e21c7e34 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Sun, 11 Jan 2026 21:19:11 +0100 Subject: [PATCH 120/503] CISA tests --- .../Push-CIPPDBCacheData.ps1 | 21 ++ .../Set-CIPPDBCacheExoAdminAuditLogConfig.ps1 | 32 +++ ...Set-CIPPDBCacheExoPresetSecurityPolicy.ps1 | 42 ++++ .../Set-CIPPDBCacheExoSharingPolicy.ps1 | 30 +++ ...Set-CIPPDBCacheExoTenantAllowBlockList.ps1 | 52 +++++ .../Public/Tests/CISA-Missing-Caches.md | 195 ++++++++++++++++++ .../Identity/Invoke-CippTestCISAMSEXO101.md | 21 ++ .../Identity/Invoke-CippTestCISAMSEXO101.ps1 | 50 +++++ .../Identity/Invoke-CippTestCISAMSEXO102.md | 23 +++ .../Identity/Invoke-CippTestCISAMSEXO102.ps1 | 54 +++++ .../Identity/Invoke-CippTestCISAMSEXO103.md | 21 ++ .../Identity/Invoke-CippTestCISAMSEXO103.ps1 | 50 +++++ .../Identity/Invoke-CippTestCISAMSEXO11.md | 22 ++ .../Identity/Invoke-CippTestCISAMSEXO11.ps1 | 100 +++++++++ .../Identity/Invoke-CippTestCISAMSEXO111.md | 22 ++ .../Identity/Invoke-CippTestCISAMSEXO111.ps1 | 52 +++++ .../Identity/Invoke-CippTestCISAMSEXO112.md | 26 +++ .../Identity/Invoke-CippTestCISAMSEXO112.ps1 | 57 +++++ .../Identity/Invoke-CippTestCISAMSEXO113.md | 24 +++ .../Identity/Invoke-CippTestCISAMSEXO113.ps1 | 56 +++++ .../Identity/Invoke-CippTestCISAMSEXO121.md | 23 +++ .../Identity/Invoke-CippTestCISAMSEXO121.ps1 | 55 +++++ .../Identity/Invoke-CippTestCISAMSEXO122.md | 22 ++ .../Identity/Invoke-CippTestCISAMSEXO122.ps1 | 50 +++++ .../Identity/Invoke-CippTestCISAMSEXO131.md | 19 ++ .../Identity/Invoke-CippTestCISAMSEXO131.ps1 | 89 ++++++++ .../Identity/Invoke-CippTestCISAMSEXO141.md | 21 ++ .../Identity/Invoke-CippTestCISAMSEXO141.ps1 | 51 +++++ .../Identity/Invoke-CippTestCISAMSEXO142.md | 23 +++ .../Identity/Invoke-CippTestCISAMSEXO142.ps1 | 54 +++++ .../Identity/Invoke-CippTestCISAMSEXO143.md | 22 ++ .../Identity/Invoke-CippTestCISAMSEXO143.ps1 | 57 +++++ .../Identity/Invoke-CippTestCISAMSEXO151.md | 21 ++ .../Identity/Invoke-CippTestCISAMSEXO151.ps1 | 50 +++++ .../Identity/Invoke-CippTestCISAMSEXO152.md | 22 ++ .../Identity/Invoke-CippTestCISAMSEXO152.ps1 | 50 +++++ .../Identity/Invoke-CippTestCISAMSEXO153.md | 21 ++ .../Identity/Invoke-CippTestCISAMSEXO153.ps1 | 50 +++++ .../Identity/Invoke-CippTestCISAMSEXO171.md | 19 ++ .../Identity/Invoke-CippTestCISAMSEXO171.ps1 | 46 +++++ .../Identity/Invoke-CippTestCISAMSEXO173.md | 22 ++ .../Identity/Invoke-CippTestCISAMSEXO173.ps1 | 46 +++++ .../Identity/Invoke-CippTestCISAMSEXO31.md | 27 +++ .../Identity/Invoke-CippTestCISAMSEXO31.ps1 | 64 ++++++ .../Identity/Invoke-CippTestCISAMSEXO51.md | 23 +++ .../Identity/Invoke-CippTestCISAMSEXO51.ps1 | 141 +++++++++++++ .../Identity/Invoke-CippTestCISAMSEXO61.md | 20 ++ .../Identity/Invoke-CippTestCISAMSEXO61.ps1 | 98 +++++++++ .../Identity/Invoke-CippTestCISAMSEXO62.md | 24 +++ .../Identity/Invoke-CippTestCISAMSEXO62.ps1 | 58 ++++++ .../Identity/Invoke-CippTestCISAMSEXO71.md | 20 ++ .../Identity/Invoke-CippTestCISAMSEXO71.ps1 | 103 +++++++++ .../Identity/Invoke-CippTestCISAMSEXO95.md | 22 ++ .../Identity/Invoke-CippTestCISAMSEXO95.ps1 | 68 ++++++ .../CIPPCore/Public/Tests/CISA/report.json | 33 +++ 55 files changed, 2484 insertions(+) create mode 100644 Modules/CIPPCore/Public/Set-CIPPDBCacheExoAdminAuditLogConfig.ps1 create mode 100644 Modules/CIPPCore/Public/Set-CIPPDBCacheExoPresetSecurityPolicy.ps1 create mode 100644 Modules/CIPPCore/Public/Set-CIPPDBCacheExoSharingPolicy.ps1 create mode 100644 Modules/CIPPCore/Public/Set-CIPPDBCacheExoTenantAllowBlockList.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/CISA-Missing-Caches.md create mode 100644 Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO101.md create mode 100644 Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO101.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO102.md create mode 100644 Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO102.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO103.md create mode 100644 Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO103.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO11.md create mode 100644 Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO11.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO111.md create mode 100644 Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO111.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO112.md create mode 100644 Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO112.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO113.md create mode 100644 Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO113.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO121.md create mode 100644 Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO121.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO122.md create mode 100644 Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO122.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO131.md create mode 100644 Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO131.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO141.md create mode 100644 Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO141.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO142.md create mode 100644 Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO142.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO143.md create mode 100644 Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO143.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO151.md create mode 100644 Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO151.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO152.md create mode 100644 Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO152.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO153.md create mode 100644 Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO153.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO171.md create mode 100644 Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO171.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO173.md create mode 100644 Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO173.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO31.md create mode 100644 Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO31.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO51.md create mode 100644 Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO51.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO61.md create mode 100644 Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO61.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO62.md create mode 100644 Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO62.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO71.md create mode 100644 Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO71.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO95.md create mode 100644 Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO95.ps1 create mode 100644 Modules/CIPPCore/Public/Tests/CISA/report.json diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 index ef3f7df48952..4ff5a9ea7d55 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 @@ -256,6 +256,27 @@ function Push-CIPPDBCacheData { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoRemoteDomain collection failed: $($_.Exception.Message)" -sev Error } + + Write-Host 'Getting cache for ExoSharingPolicy' + try { Set-CIPPDBCacheExoSharingPolicy -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoSharingPolicy collection failed: $($_.Exception.Message)" -sev Error + } + + Write-Host 'Getting cache for ExoAdminAuditLogConfig' + try { Set-CIPPDBCacheExoAdminAuditLogConfig -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoAdminAuditLogConfig collection failed: $($_.Exception.Message)" -sev Error + } + + Write-Host 'Getting cache for ExoPresetSecurityPolicy' + try { Set-CIPPDBCacheExoPresetSecurityPolicy -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoPresetSecurityPolicy collection failed: $($_.Exception.Message)" -sev Error + } + + Write-Host 'Getting cache for ExoTenantAllowBlockList' + try { Set-CIPPDBCacheExoTenantAllowBlockList -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoTenantAllowBlockList collection failed: $($_.Exception.Message)" -sev Error + } + Write-Host 'Getting cache for License Overview' try { Set-CIPPDBCacheLicenseOverview -TenantFilter $TenantFilter } catch { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "License Overview collection failed: $($_.Exception.Message)" -sev Error diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoAdminAuditLogConfig.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoAdminAuditLogConfig.ps1 new file mode 100644 index 000000000000..9ee38f5e1185 --- /dev/null +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoAdminAuditLogConfig.ps1 @@ -0,0 +1,32 @@ +function Set-CIPPDBCacheExoAdminAuditLogConfig { + <# + .SYNOPSIS + Caches Exchange Online Admin Audit Log Configuration + + .PARAMETER TenantFilter + The tenant to cache admin audit log config for + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter + ) + + try { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching Exchange Admin Audit Log configuration' -sev Info + + $AuditConfig = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-AdminAuditLogConfig' + + if ($AuditConfig) { + # AdminAuditLogConfig returns a single object, wrap in array for consistency + $AuditConfigArray = @($AuditConfig) + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoAdminAuditLogConfig' -Data $AuditConfigArray + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoAdminAuditLogConfig' -Data $AuditConfigArray -Count + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached Exchange Admin Audit Log configuration' -sev Info + } + $AuditConfig = $null + + } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache Admin Audit Log configuration: $($_.Exception.Message)" -sev Error + } +} diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoPresetSecurityPolicy.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoPresetSecurityPolicy.ps1 new file mode 100644 index 000000000000..9d227da4122e --- /dev/null +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoPresetSecurityPolicy.ps1 @@ -0,0 +1,42 @@ +function Set-CIPPDBCacheExoPresetSecurityPolicy { + <# + .SYNOPSIS + Caches Exchange Online Preset Security Policies (EOP and ATP Protection Policy Rules) + + .PARAMETER TenantFilter + The tenant to cache preset security policies for + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter + ) + + try { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching Exchange Preset Security Policies' -sev Info + + $EOPRules = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-EOPProtectionPolicyRule' + $ATPRules = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-ATPProtectionPolicyRule' + + # Combine both rule types into a single collection + $AllRules = @() + if ($EOPRules) { + $AllRules += $EOPRules + } + if ($ATPRules) { + $AllRules += $ATPRules + } + + if ($AllRules.Count -gt 0) { + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoPresetSecurityPolicy' -Data $AllRules + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoPresetSecurityPolicy' -Data $AllRules -Count + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($AllRules.Count) Preset Security Policy rules" -sev Info + } + $EOPRules = $null + $ATPRules = $null + $AllRules = $null + + } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache Preset Security Policies: $($_.Exception.Message)" -sev Error + } +} diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoSharingPolicy.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoSharingPolicy.ps1 new file mode 100644 index 000000000000..4b59088adfa0 --- /dev/null +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoSharingPolicy.ps1 @@ -0,0 +1,30 @@ +function Set-CIPPDBCacheExoSharingPolicy { + <# + .SYNOPSIS + Caches Exchange Online Sharing Policies + + .PARAMETER TenantFilter + The tenant to cache sharing policies for + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter + ) + + try { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching Exchange Sharing Policies' -sev Info + + $SharingPolicies = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-SharingPolicy' + + if ($SharingPolicies) { + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoSharingPolicy' -Data $SharingPolicies + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoSharingPolicy' -Data $SharingPolicies -Count + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($SharingPolicies.Count) Sharing Policies" -sev Info + } + $SharingPolicies = $null + + } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache Sharing Policies: $($_.Exception.Message)" -sev Error + } +} diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoTenantAllowBlockList.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoTenantAllowBlockList.ps1 new file mode 100644 index 000000000000..fe6c1ee1f9b4 --- /dev/null +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoTenantAllowBlockList.ps1 @@ -0,0 +1,52 @@ +function Set-CIPPDBCacheExoTenantAllowBlockList { + <# + .SYNOPSIS + Caches Exchange Online Tenant Allow/Block List items + + .PARAMETER TenantFilter + The tenant to cache tenant allow/block list for + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter + ) + + try { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching Tenant Allow/Block List items' -sev Info + + $SenderItems = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-TenantAllowBlockListItems' -cmdParams @{ListType = 'Sender' } + $UrlItems = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-TenantAllowBlockListItems' -cmdParams @{ListType = 'Url' } + $FileHashItems = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-TenantAllowBlockListItems' -cmdParams @{ListType = 'FileHash' } + + # Combine all list types into a single collection + $AllItems = @() + if ($SenderItems) { + $AllItems += $SenderItems + } + if ($UrlItems) { + $AllItems += $UrlItems + } + if ($FileHashItems) { + $AllItems += $FileHashItems + } + + if ($AllItems.Count -gt 0) { + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoTenantAllowBlockList' -Data $AllItems + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoTenantAllowBlockList' -Data $AllItems -Count + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($AllItems.Count) Tenant Allow/Block List items" -sev Info + } else { + # Even if empty, store an empty array so test knows cache was populated + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoTenantAllowBlockList' -Data @() + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoTenantAllowBlockList' -Data @() -Count + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached empty Tenant Allow/Block List' -sev Info + } + $SenderItems = $null + $UrlItems = $null + $FileHashItems = $null + $AllItems = $null + + } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache Tenant Allow/Block List: $($_.Exception.Message)" -sev Error + } +} diff --git a/Modules/CIPPCore/Public/Tests/CISA-Missing-Caches.md b/Modules/CIPPCore/Public/Tests/CISA-Missing-Caches.md new file mode 100644 index 000000000000..5b7bd652195a --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/CISA-Missing-Caches.md @@ -0,0 +1,195 @@ +# Missing CIPP Caches for CISA Tests + +This document lists the caches that need to be created to support the remaining CISA tests that cannot currently be implemented. + +## ✅ Implemented Cache Functions + +### 1. ✅ CASMailbox Cache +**Required For:** +- ✅ MS.EXO.5.1 - SMTP Authentication + +**Status**: ✅ IMPLEMENTED in Set-CIPPDBCacheCASMailbox.ps1 + +--- + +### 2. ✅ ExoSharingPolicy Cache +**Required For:** +- ✅ MS.EXO.6.1 - Contact Sharing +- ✅ MS.EXO.6.2 - Calendar Sharing + +**Status**: ✅ IMPLEMENTED in Set-CIPPDBCacheExoSharingPolicy.ps1 + +--- + +### 3. ✅ ExoAdminAuditLogConfig Cache +**Required For:** +- ✅ MS.EXO.17.1 - Audit Log +- ✅ MS.EXO.17.3 - Audit Log Retention + +**Status**: ✅ IMPLEMENTED in Set-CIPPDBCacheExoAdminAuditLogConfig.ps1 + +--- + +### 4. ✅ ExoPresetSecurityPolicy Cache +**Required For:** +- ✅ MS.EXO.11.1 - Impersonation +- ✅ MS.EXO.11.2 - Impersonation Tips +- ✅ MS.EXO.11.3 - Mailbox Intelligence + +**Status**: ✅ IMPLEMENTED in Set-CIPPDBCacheExoPresetSecurityPolicy.ps1 + +--- + +### 5. ✅ ExoTenantAllowBlockList Cache +**Required For:** +- ✅ MS.EXO.12.1 - Anti-Spam Allow List + +**Status**: ✅ IMPLEMENTED in Set-CIPPDBCacheExoTenantAllowBlockList.ps1 + +--- + +## Required New Cache Functions +**Required For:** +- MS.EXO.8.1 - DLP Solution +- MS.EXO.8.2 - DLP PII +- MS.EXO.8.4 - DLP Baseline Rules + +**SecurityCompliance Command:** +```powershell +Get-DlpCompliancePolicy | Select-Object Name, Enabled, Mode +Get-DlpComplianceRule | Select-Object Name, ParentPolicyName, ContentContainsSensitiveInformation, Disabled +``` +1 +**Cache Function Names:** +- `Set-CIPPDBCacheSccDlpPolicy` +- `Set-CIPPDBCacheSccDlpRule` + +**Properties Needed:** +- Policy: Name, Enabled, Mode +- Rule: Name, ParentPolicyName, ContentContainsSensitiveInformation, Disabled + +**Note:** Requires SecurityCompliance PowerShell connection + +--- + +### 2. SecurityCompliance ProtectionAlert Cache +**Required For:** +- MS.EXO.16.1 - Alerts + +**SecurityCompliance Command:** +```powershell +Get-ProtectionAlert | Select-Object Name, Disabled +``` + +**Cache Function Name:** `Set-CIPPDBCacheSccProtectionAlert` + +**Properties Needed:** +- Name +- Disabled + +**Note:** Requires SecurityCompliance PowerShell connection + +--- + +### 3. SecurityCompliance ActivityAlert Cache +**Required For:** +- MS.EXO.16.2 - Alert SIEM + +**SecurityCompliance Command:** +```powershell +Get-ActivityAlert | Select-Object Name, Disabled, NotificationEnabled, Type +``` + +**Cache Function Name:** `Set-CIPPDBCacheSccActivityAlert` + +**Properties Needed:** +- Name +- Disabled +- NotificationEnabled +- Type + +**Note:** Requires SecurityCompliance PowerShell connection + +--- + +## DNS-Based Tests (Cannot Be Cached) + +These tests require external DNS lookups and cannot be implemented with cached Exchange data: + +### MS.EXO.2.1 - SPF Restriction +**Requires:** DNS TXT record lookup for SPF +**Query:** `nslookup -type=txt ` + +### MS.EXO.2.2 - SPF Directive +**Requires:** DNS TXT record parsing for SPF policy +**Query:** Parse SPF record for `~all` or `-all` + +### MS.EXO.4.1 - DMARC Record Exists +**Requires:** DNS TXT record lookup for DMARC +**Query:** `nslookup -type=txt _dmarc.` + +### MS.EXO.4.2 - DMARC Reject Policy +**Requires:** DNS TXT record parsing for DMARC policy +**Query:** Parse DMARC record for `p=reject` or `p=quarantine` + +### MS.EXO.4.3 - DMARC Aggregate Reports +**Requires:** DNS TXT record parsing for DMARC rua tags +**Query:** Parse DMARC record for `rua=` email addresses + +### MS.EXO.4.4 - DMARC Reports +**Requires:** DNS TXT record parsing for DMARC report configuration +**Query:** Parse DMARC record for report targets + +### MS.EXO.7.1 - External Sender Warning +**Requires:** ExoOrganizationConfig.ExternalInOutlook property +**Note:** May already be in ExoOrganizationConfig cache - needs verification + +### MS.EXO.13.1 - Mailbox Auditing +**Requires:** ExoOrganizationConfig.AuditDisabled property +**Note:** May already be in ExoOrganizationConfig cache - needs verification + +## Manual Assessment Tests (Cannot Be Automated) + +### MS.EXO.8.3 - DLP Alternate Solution +**Reason:** Requires manual assessment of 3rd party DLP solutions + +### MS.EXO.9.4 - Email Filter Alternative +**Reason:** Requires manual assessment of 3rd party email filtering solutions + +### MS.EXO.14.4 - Spam Alternative Solution +**Reason:** Requires manual assessment of 3rd party anti-spam solutions + +### MS.EXO.17.2 - Audit Log Premium +**Reason:** Requires license validation and advanced audit policy checks beyond cached data + +--- + +## Implementation Priority + +### High Priority (Core Security Controls): +1. CASMailbox - SMTP Auth control +2. ExoAdminAuditLogConfig - Audit logging +3. ExoTenantAllowBlockList - Allow list bypass prevention + +### Medium Priority (DLP and Alerts): +4. SecurityCompliance DLP caches - Data loss prevention +5. SecurityCompliance Alert caches - Security monitoring + +### Low Priority (Advanced Features): +6. ExoSharingPolicy - External sharing controls +7. ExoPresetSecurityPolicy - Preset security policies + +--- + +## Notes on Implementation + +1. **Graph API Alternative**: Some Exchange Online cmdlets may have equivalent Graph API endpoints that could be used instead. + +## Summary + +- **New Caches Required**: 8 cache functions +- **DNS Tests**: 6 tests (architectural limitation) +- **Manual Tests**: 4 tests (cannot be automated) +- **Implementable After New Caches**: 15 additional tests +- **Current Implementation**: 13 tests +- **Total Possible with New Caches**: 28 tests (68% coverage) diff --git a/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO101.md b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO101.md new file mode 100644 index 000000000000..47d0249a0689 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO101.md @@ -0,0 +1,21 @@ +Emails SHALL be filtered by attachment file types. + +Email attachment filtering helps prevent malicious files from reaching users' inboxes. By blocking or quarantining emails with potentially dangerous file types, organizations can significantly reduce the risk of malware infections and data breaches. + +**Remediation Action:** + +1. Navigate to Microsoft 365 Defender portal > Email & collaboration > Policies & rules > Threat policies > Anti-malware +2. Select each malware filter policy +3. Under "Protection settings": + - Enable "Enable the common attachments filter" +4. Or use PowerShell: +```powershell +Set-MalwareFilterPolicy -Identity "Default" -EnableFileFilter $true +``` + +**Links:** +- [CISA SCubaGear EXO Baseline - MS.EXO.10.1](https://github.com/cisagov/ScubaGear/blob/main/PowerShell/ScubaGear/baselines/exo.md#msexo101v1) +- [Configure anti-malware policies](https://learn.microsoft.com/microsoft-365/security/office-365-security/anti-malware-protection-configure) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO101.ps1 b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO101.ps1 new file mode 100644 index 000000000000..fa1fc49c2e3b --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO101.ps1 @@ -0,0 +1,50 @@ +function Invoke-CippTestCISAMSEXO101 { + <# + .SYNOPSIS + Tests MS.EXO.10.1 - Emails SHALL be filtered by attachment file types + + .DESCRIPTION + Checks if malware filter policies have file filtering enabled + + .FUNCTIONALITY + Internal + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Tenant + ) + + try { + $MalwarePolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoMalwareFilterPolicy' + + if (-not $MalwarePolicies) { + Add-CippTestResult -Status 'Skipped' -ResultMarkdown 'ExoMalwareFilterPolicy cache not found. Please refresh the cache for this tenant.' -Risk 'High' -Category 'Exchange Online' -TestId 'CISAMSEXO101' -TenantFilter $Tenant + return + } + + $FailedPolicies = $MalwarePolicies | Where-Object { -not $_.EnableFileFilter } + + if ($FailedPolicies.Count -eq 0) { + $Result = "✅ **Pass**: All $($MalwarePolicies.Count) malware filter policy/policies have file filtering enabled." + $Status = 'Pass' + } else { + $ResultTable = foreach ($Policy in $FailedPolicies) { + [PSCustomObject]@{ + 'Policy Name' = $Policy.Name + 'File Filter Enabled' = $Policy.EnableFileFilter + } + } + + $Result = "❌ **Fail**: $($FailedPolicies.Count) of $($MalwarePolicies.Count) malware filter policy/policies do not have file filtering enabled:`n`n" + $Result += ($ResultTable | ConvertTo-Html -Fragment | Out-String) + $Status = 'Fail' + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO101' -Status $Status -ResultMarkdown $Result -Risk 'High' -Category 'Exchange Online' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Add-CippTestResult -Status 'Failed' -ResultMarkdown "Test execution failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Category 'Exchange Online' -TestId 'CISAMSEXO101' -TenantFilter $Tenant + } +} diff --git a/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO102.md b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO102.md new file mode 100644 index 000000000000..438d6636be83 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO102.md @@ -0,0 +1,23 @@ +Emails identified as containing malware SHALL be quarantined or dropped. + +Ensuring that emails containing malware are immediately quarantined or deleted prevents malicious content from reaching users' mailboxes. This is a critical security control that stops malware distribution at the email gateway level. + +**Remediation Action:** + +1. Navigate to Microsoft 365 Defender portal > Email & collaboration > Policies & rules > Threat policies > Anti-malware +2. Select each malware filter policy +3. Under "Protection settings": + - Set "Malware detection response" to either "Delete entire message" or "Quarantine message" +4. Or use PowerShell: +```powershell +Set-MalwareFilterPolicy -Identity "Default" -Action Quarantine +# Or +Set-MalwareFilterPolicy -Identity "Default" -Action DeleteMessage +``` + +**Links:** +- [CISA SCubaGear EXO Baseline - MS.EXO.10.2](https://github.com/cisagov/ScubaGear/blob/main/PowerShell/ScubaGear/baselines/exo.md#msexo102v1) +- [Configure anti-malware policies](https://learn.microsoft.com/microsoft-365/security/office-365-security/anti-malware-protection-configure) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO102.ps1 b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO102.ps1 new file mode 100644 index 000000000000..2d09dde64278 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO102.ps1 @@ -0,0 +1,54 @@ +function Invoke-CippTestCISAMSEXO102 { + <# + .SYNOPSIS + Tests MS.EXO.10.2 - Emails identified as malware SHALL be quarantined or dropped + + .DESCRIPTION + Checks if malware filter policies quarantine or delete emails with malware + + .FUNCTIONALITY + Internal + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Tenant + ) + + try { + $MalwarePolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoMalwareFilterPolicy' + + if (-not $MalwarePolicies) { + Add-CippTestResult -Status 'Skipped' -ResultMarkdown 'ExoMalwareFilterPolicy cache not found. Please refresh the cache for this tenant.' -Risk 'High' -Category 'Exchange Online' -TestId 'CISAMSEXO102' -TenantFilter $Tenant + return + } + + $AcceptableActions = @('DeleteMessage', 'Quarantine') + $FailedPolicies = [System.Collections.Generic.List[object]]::new() + + foreach ($Policy in $MalwarePolicies) { + if ($Policy.Action -notin $AcceptableActions) { + $FailedPolicies.Add([PSCustomObject]@{ + 'Policy Name' = $Policy.Name + 'Current Action' = $Policy.Action + 'Expected' = 'DeleteMessage or Quarantine' + }) + } + } + + if ($FailedPolicies.Count -eq 0) { + $Result = "✅ **Pass**: All $($MalwarePolicies.Count) malware filter policy/policies quarantine or delete emails with malware." + $Status = 'Pass' + } else { + $Result = "❌ **Fail**: $($FailedPolicies.Count) of $($MalwarePolicies.Count) malware filter policy/policies do not quarantine or delete malware:`n`n" + $Result += ($FailedPolicies | ConvertTo-Html -Fragment | Out-String) + $Status = 'Fail' + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO102' -Status $Status -ResultMarkdown $Result -Risk 'High' -Category 'Exchange Online' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Add-CippTestResult -Status 'Failed' -ResultMarkdown "Test execution failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Category 'Exchange Online' -TestId 'CISAMSEXO102' -TenantFilter $Tenant + } +} diff --git a/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO103.md b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO103.md new file mode 100644 index 000000000000..17f11b64670f --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO103.md @@ -0,0 +1,21 @@ +Email scanning SHALL be capable of reviewing emails after delivery. + +Zero-hour Auto Purge (ZAP) provides post-delivery protection by retroactively detecting and removing malicious emails that were initially deemed safe. This is crucial because malware signatures and threat intelligence are constantly updated, and emails that were safe at delivery time may later be identified as malicious. + +**Remediation Action:** + +1. Navigate to Microsoft 365 Defender portal > Email & collaboration > Policies & rules > Threat policies > Anti-malware +2. Select each malware filter policy +3. Under "Protection settings": + - Enable "Enable zero-hour auto purge (ZAP) for malware" +4. Or use PowerShell: +```powershell +Set-MalwareFilterPolicy -Identity "Default" -ZapEnabled $true +``` + +**Links:** +- [CISA SCubaGear EXO Baseline - MS.EXO.10.3](https://github.com/cisagov/ScubaGear/blob/main/PowerShell/ScubaGear/baselines/exo.md#msexo103v1) +- [Zero-hour auto purge (ZAP) in Microsoft Defender for Office 365](https://learn.microsoft.com/microsoft-365/security/office-365-security/zero-hour-auto-purge) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO103.ps1 b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO103.ps1 new file mode 100644 index 000000000000..f857c1aa419e --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO103.ps1 @@ -0,0 +1,50 @@ +function Invoke-CippTestCISAMSEXO103 { + <# + .SYNOPSIS + Tests MS.EXO.10.3 - Email scanning SHALL be capable of reviewing emails after delivery (ZAP) + + .DESCRIPTION + Checks if Zero-hour Auto Purge (ZAP) is enabled for malware protection + + .FUNCTIONALITY + Internal + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Tenant + ) + + try { + $MalwarePolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoMalwareFilterPolicy' + + if (-not $MalwarePolicies) { + Add-CippTestResult -Status 'Skipped' -ResultMarkdown 'ExoMalwareFilterPolicy cache not found. Please refresh the cache for this tenant.' -Risk 'High' -Category 'Exchange Online' -TestId 'CISAMSEXO103' -TenantFilter $Tenant + return + } + + $FailedPolicies = $MalwarePolicies | Where-Object { -not $_.ZapEnabled } + + if ($FailedPolicies.Count -eq 0) { + $Result = "✅ **Pass**: All $($MalwarePolicies.Count) malware filter policy/policies have ZAP (Zero-hour Auto Purge) enabled." + $Status = 'Pass' + } else { + $ResultTable = foreach ($Policy in $FailedPolicies) { + [PSCustomObject]@{ + 'Policy Name' = $Policy.Name + 'ZAP Enabled' = $Policy.ZapEnabled + } + } + + $Result = "❌ **Fail**: $($FailedPolicies.Count) of $($MalwarePolicies.Count) malware filter policy/policies do not have ZAP enabled:`n`n" + $Result += ($ResultTable | ConvertTo-Html -Fragment | Out-String) + $Status = 'Fail' + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO103' -Status $Status -ResultMarkdown $Result -Risk 'High' -Category 'Exchange Online' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Add-CippTestResult -Status 'Failed' -ResultMarkdown "Test execution failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Category 'Exchange Online' -TestId 'CISAMSEXO103' -TenantFilter $Tenant + } +} diff --git a/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO11.md b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO11.md new file mode 100644 index 000000000000..be33a98245e3 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO11.md @@ -0,0 +1,22 @@ +Automatic forwarding to external domains SHALL be disabled. + +Disabling automatic forwarding prevents potential data exfiltration scenarios where malicious actors could set up forwarding rules to steal sensitive information. This control ensures that emails cannot be automatically forwarded outside the organization without proper oversight. + +**Remediation Action:** + +1. Navigate to Exchange Admin Center > Mail flow > Remote domains +2. For each remote domain, disable automatic forwarding: + - Select the domain + - Click Edit + - Set "Allow automatic forwarding" to Off +3. Or use PowerShell: +```powershell +Get-RemoteDomain | Set-RemoteDomain -AutoForwardEnabled $false +``` + +**Links:** +- [CISA SCubaGear EXO Baseline - MS.EXO.1.1](https://github.com/cisagov/ScubaGear/blob/main/PowerShell/ScubaGear/baselines/exo.md#msexo11v1) +- [Configure remote domain settings](https://learn.microsoft.com/exchange/mail-flow-best-practices/remote-domains/remote-domains) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO11.ps1 b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO11.ps1 new file mode 100644 index 000000000000..5ad9bdf843f1 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO11.ps1 @@ -0,0 +1,100 @@ +function Invoke-CippTestCISAMSEXO11 { + <# + .SYNOPSIS + Tests MS.EXO.1.1 - Automatic forwarding to external domains SHALL be disabled + + .DESCRIPTION + Checks if automatic forwarding to external domains is disabled across all remote domains + + .FUNCTIONALITY + Internal + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Tenant + ) + + try { + $RemoteDomains = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoRemoteDomain' + + if (-not $RemoteDomains) { + Add-CippTestResult -Status 'Skipped' -ResultMarkdown 'ExoRemoteDomain cache not found. Please refresh the cache for this tenant.' -Risk 'High' -Category 'Exchange Online' -TestId 'CISAMSEXO11' -TenantFilter $Tenant + return + } + + $ForwardingEnabledDomains = $RemoteDomains | Where-Object { $_.AutoForwardEnabled -eq $true } + + if (($ForwardingEnabledDomains | Measure-Object).Count -eq 0) { + $Result = '✅ **Pass**: Automatic forwarding to external domains is disabled for all remote domains.' + $Status = 'Pass' + } else { + $ResultTable = foreach ($Domain in $ForwardingEnabledDomains) { + [PSCustomObject]@{ + 'Domain Name' = $Domain.DomainName + 'Auto Forward' = $Domain.AutoForwardEnabled + } + } + + $Result = "❌ **Fail**: $($ForwardingEnabledDomains.Count) domain(s) have automatic forwarding enabled:`n`n" + $Result += ($ResultTable | ConvertTo-Html -Fragment | Out-String) + $Status = 'Fail' + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO11' -Status $Status -ResultMarkdown $Result -Risk 'High' -Category 'Exchange Online' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Add-CippTestResult -Status 'Failed' -ResultMarkdown "Test execution failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Category 'Exchange Online' -TestId 'CISAMSEXO11' -TenantFilter $Tenant + } +} +function Invoke-CippTestCISAMSEXO11 { + <# + .SYNOPSIS + MS.EXO.1.1 - Automatic forwarding to external domains SHALL be disabled + + .DESCRIPTION + Tests if automatic forwarding to external domains is disabled in Exchange Online + + .LINK + https://github.com/cisagov/ScubaGear + #> + param($Tenant) + + try { + $RemoteDomains = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoRemoteDomain' + + if (-not $RemoteDomains) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO11' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'ExoRemoteDomain cache not found. Please ensure cache data is available.' -Risk 'High' -Name 'Auto-forwarding to external domains is disabled' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Exchange Online' + return + } + + $ForwardingEnabledDomains = $RemoteDomains | Where-Object { $_.AutoForwardEnabled -eq $true } + + if ($ForwardingEnabledDomains.Count -eq 0) { + $Status = 'Passed' + $Result = "✅ Well done. Your tenant has automatic forwarding disabled for all remote domains.`n`n" + $Result += "| Domain Name | Auto Forward Enabled |`n" + $Result += "| --- | --- |`n" + foreach ($domain in $RemoteDomains) { + $Result += "| $($domain.DomainName) | ❌ Disabled |`n" + } + } else { + $Status = 'Failed' + $Result = "❌ Your tenant has automatic forwarding enabled for some remote domains.`n`n" + $Result += "| Domain Name | Auto Forward Enabled | Result |`n" + $Result += "| --- | --- | --- |`n" + foreach ($domain in $RemoteDomains) { + $enabled = if ($domain.AutoForwardEnabled) { '✅ Enabled' } else { '❌ Disabled' } + $testResult = if ($domain.AutoForwardEnabled) { '❌ Fail' } else { '✅ Pass' } + $Result += "| $($domain.DomainName) | $enabled | $testResult |`n" + } + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO11' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'Auto-forwarding to external domains is disabled' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Exchange Online' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run CISA test CISAMSEXO11: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO11' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test execution failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Auto-forwarding to external domains is disabled' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Exchange Online' + } +} diff --git a/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO111.md b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO111.md new file mode 100644 index 000000000000..c4979f85630e --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO111.md @@ -0,0 +1,22 @@ +Impersonation protection checks SHOULD be used. + +Impersonation protection defends against phishing attacks where attackers impersonate trusted users or domains. These checks analyze sender patterns, domain similarities, and user behavior to identify and block sophisticated impersonation attempts before they reach users. + +**Remediation Action:** + +1. Navigate to Microsoft 365 Defender portal > Email & collaboration > Policies & rules > Threat policies > Preset security policies +2. Enable either Standard or Strict preset security policy +3. Ensure policies include appropriate user and domain protection +4. Or use PowerShell: +```powershell +# Enable standard preset security policy +Enable-EOPProtectionPolicyRule -Identity "Standard Preset Security Policy" +Enable-ATPProtectionPolicyRule -Identity "Standard Preset Security Policy" +``` + +**Links:** +- [CISA SCubaGear EXO Baseline - MS.EXO.11.1](https://github.com/cisagov/ScubaGear/blob/main/PowerShell/ScubaGear/baselines/exo.md#msexo111v1) +- [Preset security policies](https://learn.microsoft.com/microsoft-365/security/office-365-security/preset-security-policies) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO111.ps1 b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO111.ps1 new file mode 100644 index 000000000000..d8b89ea1fc03 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO111.ps1 @@ -0,0 +1,52 @@ +function Invoke-CippTestCISAMSEXO111 { + <# + .SYNOPSIS + Tests MS.EXO.11.1 - Impersonation protection checks SHOULD be used + + .DESCRIPTION + Checks if both standard and strict EOP/ATP preset security policies are enabled + + .FUNCTIONALITY + Internal + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Tenant + ) + + try { + $PresetPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoPresetSecurityPolicy' + + if (-not $PresetPolicies) { + Add-CippTestResult -Status 'Skipped' -ResultMarkdown 'ExoPresetSecurityPolicy cache not found. Please refresh the cache for this tenant.' -Risk 'High' -Category 'Exchange Online' -TestId 'CISAMSEXO111' -TenantFilter $Tenant + return + } + + $StandardEOP = $PresetPolicies | Where-Object { $_.Identity -eq 'Standard Preset Security Policy' -and $_.State -eq 'Enabled' } + $StrictEOP = $PresetPolicies | Where-Object { $_.Identity -eq 'Strict Preset Security Policy' -and $_.State -eq 'Enabled' } + + $StandardATP = $PresetPolicies | Where-Object { $_.Identity -like '*Preset Security Policy*' -and $_.ImpersonationProtectionState -eq 'Enabled' } + + $EnabledPolicies = @() + if ($StandardEOP) { $EnabledPolicies += 'Standard EOP' } + if ($StrictEOP) { $EnabledPolicies += 'Strict EOP' } + if ($StandardATP) { $EnabledPolicies += "$($StandardATP.Count) ATP policy/policies with impersonation protection" } + + if ($EnabledPolicies.Count -gt 0) { + $Result = "✅ **Pass**: Preset security policies with impersonation protection are enabled:`n`n" + $Result += ($EnabledPolicies | ForEach-Object { "- $_" }) -join "`n" + $Status = 'Pass' + } else { + $Result = "❌ **Fail**: No preset security policies with impersonation protection enabled.`n`n" + $Result += "Enable Standard or Strict preset security policies to provide impersonation protection." + $Status = 'Fail' + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO111' -Status $Status -ResultMarkdown $Result -Risk 'High' -Category 'Exchange Online' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Add-CippTestResult -Status 'Failed' -ResultMarkdown "Test execution failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Category 'Exchange Online' -TestId 'CISAMSEXO111' -TenantFilter $Tenant + } +} diff --git a/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO112.md b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO112.md new file mode 100644 index 000000000000..6ef65e55b934 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO112.md @@ -0,0 +1,26 @@ +User warnings, comparable to the user safety tips included with EOP, SHOULD be displayed. + +Safety tips provide visual warnings to users when emails contain indicators of impersonation attempts, such as similar display names, lookalike domains, or unusual character patterns. These warnings help users recognize and avoid phishing attacks. + +**Remediation Action:** + +1. Navigate to Microsoft 365 Defender portal > Email & collaboration > Policies & rules > Threat policies > Anti-phishing +2. Edit preset security policies or custom anti-phishing policies +3. Under Impersonation section, enable: + - Show user impersonation safety tip + - Show domain impersonation safety tip + - Show unusual characters impersonation safety tip +4. Or use PowerShell: +```powershell +Set-AntiPhishPolicy -Identity "Standard Preset Security Policy" ` + -EnableSimilarUsersSafetyTips $true ` + -EnableSimilarDomainsSafetyTips $true ` + -EnableUnusualCharactersSafetyTips $true +``` + +**Links:** +- [CISA SCubaGear EXO Baseline - MS.EXO.11.2](https://github.com/cisagov/ScubaGear/blob/main/PowerShell/ScubaGear/baselines/exo.md#msexo112v1) +- [Safety tips in email messages](https://learn.microsoft.com/microsoft-365/security/office-365-security/anti-phishing-protection-about#safety-tips-in-email-messages) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO112.ps1 b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO112.ps1 new file mode 100644 index 000000000000..5c720b3cf64d --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO112.ps1 @@ -0,0 +1,57 @@ +function Invoke-CippTestCISAMSEXO112 { + <# + .SYNOPSIS + Tests MS.EXO.11.2 - User warnings, comparable to the user safety tips included with EOP, SHOULD be displayed + + .DESCRIPTION + Checks if impersonation safety tips are enabled in preset security policies + + .FUNCTIONALITY + Internal + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Tenant + ) + + try { + $PresetPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoPresetSecurityPolicy' + + if (-not $PresetPolicies) { + Add-CippTestResult -Status 'Skipped' -ResultMarkdown 'ExoPresetSecurityPolicy cache not found. Please refresh the cache for this tenant.' -Risk 'Medium' -Category 'Exchange Online' -TestId 'CISAMSEXO112' -TenantFilter $Tenant + return + } + + $PoliciesWithTips = $PresetPolicies | Where-Object { + ($_.EnableSimilarUsersSafetyTips -eq $true) -or + ($_.EnableSimilarDomainsSafetyTips -eq $true) -or + ($_.EnableUnusualCharactersSafetyTips -eq $true) + } + + if ($PoliciesWithTips.Count -gt 0) { + $ResultTable = $PoliciesWithTips | ForEach-Object { + [PSCustomObject]@{ + 'Policy' = $_.Identity + 'Similar Users Tips' = $_.EnableSimilarUsersSafetyTips + 'Similar Domains Tips' = $_.EnableSimilarDomainsSafetyTips + 'Unusual Characters Tips' = $_.EnableUnusualCharactersSafetyTips + } + } + + $Result = "✅ **Pass**: $($PoliciesWithTips.Count) policy/policies have impersonation safety tips enabled:`n`n" + $Result += ($ResultTable | ConvertTo-Html -Fragment | Out-String) + $Status = 'Pass' + } else { + $Result = "❌ **Fail**: No policies found with impersonation safety tips enabled.`n`n" + $Result += "Enable safety tips in preset security policies to warn users about potential impersonation." + $Status = 'Fail' + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO112' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Category 'Exchange Online' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Add-CippTestResult -Status 'Failed' -ResultMarkdown "Test execution failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Category 'Exchange Online' -TestId 'CISAMSEXO112' -TenantFilter $Tenant + } +} diff --git a/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO113.md b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO113.md new file mode 100644 index 000000000000..0a5894c4c237 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO113.md @@ -0,0 +1,24 @@ +Mailbox intelligence SHALL be enabled. + +Mailbox intelligence uses machine learning to analyze user email patterns and relationships, identifying anomalous sender behavior that may indicate impersonation attempts. This AI-powered protection adapts to each user's communication patterns for more accurate threat detection. + +**Remediation Action:** + +1. Navigate to Microsoft 365 Defender portal > Email & collaboration > Policies & rules > Threat policies > Anti-phishing +2. Edit preset security policies or custom anti-phishing policies +3. Under Impersonation section, enable: + - Enable mailbox intelligence + - Enable intelligence for impersonation protection +4. Or use PowerShell: +```powershell +Set-AntiPhishPolicy -Identity "Standard Preset Security Policy" ` + -EnableMailboxIntelligence $true ` + -EnableMailboxIntelligenceProtection $true +``` + +**Links:** +- [CISA SCubaGear EXO Baseline - MS.EXO.11.3](https://github.com/cisagov/ScubaGear/blob/main/PowerShell/ScubaGear/baselines/exo.md#msexo113v1) +- [Mailbox intelligence in anti-phishing policies](https://learn.microsoft.com/microsoft-365/security/office-365-security/anti-phishing-policies-about#impersonation-settings-in-anti-phishing-policies-in-microsoft-defender-for-office-365) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO113.ps1 b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO113.ps1 new file mode 100644 index 000000000000..1879bf42a3b2 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO113.ps1 @@ -0,0 +1,56 @@ +function Invoke-CippTestCISAMSEXO113 { + <# + .SYNOPSIS + Tests MS.EXO.11.3 - Mailbox intelligence SHALL be enabled + + .DESCRIPTION + Checks if mailbox intelligence and impersonation protection are enabled in preset security policies + + .FUNCTIONALITY + Internal + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Tenant + ) + + try { + $PresetPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoPresetSecurityPolicy' + + if (-not $PresetPolicies) { + Add-CippTestResult -Status 'Skipped' -ResultMarkdown 'ExoPresetSecurityPolicy cache not found. Please refresh the cache for this tenant.' -Risk 'High' -Category 'Exchange Online' -TestId 'CISAMSEXO113' -TenantFilter $Tenant + return + } + + $PoliciesWithIntelligence = $PresetPolicies | Where-Object { + ($_.EnableMailboxIntelligence -eq $true) -and + ($_.EnableMailboxIntelligenceProtection -eq $true) + } + + if ($PoliciesWithIntelligence.Count -gt 0) { + $ResultTable = $PoliciesWithIntelligence | ForEach-Object { + [PSCustomObject]@{ + 'Policy' = $_.Identity + 'Mailbox Intelligence' = $_.EnableMailboxIntelligence + 'Intelligence Protection' = $_.EnableMailboxIntelligenceProtection + 'State' = $_.State + } + } + + $Result = "✅ **Pass**: $($PoliciesWithIntelligence.Count) policy/policies have mailbox intelligence enabled:`n`n" + $Result += ($ResultTable | ConvertTo-Html -Fragment | Out-String) + $Status = 'Pass' + } else { + $Result = "❌ **Fail**: No policies found with mailbox intelligence enabled.`n`n" + $Result += "Enable mailbox intelligence in preset security policies for AI-powered impersonation protection." + $Status = 'Fail' + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO113' -Status $Status -ResultMarkdown $Result -Risk 'High' -Category 'Exchange Online' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Add-CippTestResult -Status 'Failed' -ResultMarkdown "Test execution failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Category 'Exchange Online' -TestId 'CISAMSEXO113' -TenantFilter $Tenant + } +} diff --git a/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO121.md b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO121.md new file mode 100644 index 000000000000..4af85199efc5 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO121.md @@ -0,0 +1,23 @@ +Allowed sender lists SHOULD NOT be used. + +Adding senders to the tenant allow list bypasses all spam, phishing, and spoofing protection. Compromised or spoofed allowed senders can be used to deliver malicious content directly to users' inboxes without any filtering. + +**Remediation Action:** + +1. Navigate to Microsoft 365 Defender portal > Policies & rules > Threat policies > Tenant Allow/Block Lists +2. Review and remove entries from the "Allow" list under "Senders" +3. Or use PowerShell: +```powershell +# List all allowed senders +Get-TenantAllowBlockListItems -ListType Sender -Action Allow + +# Remove specific allowed sender +Remove-TenantAllowBlockListItems -ListType Sender -Ids +``` + +**Links:** +- [CISA SCubaGear EXO Baseline - MS.EXO.12.1](https://github.com/cisagov/ScubaGear/blob/main/PowerShell/ScubaGear/baselines/exo.md#msexo121v1) +- [Manage the Tenant Allow/Block List](https://learn.microsoft.com/microsoft-365/security/office-365-security/tenant-allow-block-list-about) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO121.ps1 b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO121.ps1 new file mode 100644 index 000000000000..0c8b3bd978d3 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO121.ps1 @@ -0,0 +1,55 @@ +function Invoke-CippTestCISAMSEXO121 { + <# + .SYNOPSIS + Tests MS.EXO.12.1 - Allowed senders list SHOULD NOT be used + + .DESCRIPTION + Checks if tenant allow/block list has allowed senders configured + + .FUNCTIONALITY + Internal + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Tenant + ) + + try { + $AllowBlockList = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoTenantAllowBlockList' + + if ($null -eq $AllowBlockList) { + Add-CippTestResult -Status 'Skipped' -ResultMarkdown 'ExoTenantAllowBlockList cache not found. Please refresh the cache for this tenant.' -Risk 'Medium' -Category 'Exchange Online' -TestId 'CISAMSEXO121' -TenantFilter $Tenant + return + } + + $AllowedSenders = $AllowBlockList | Where-Object { $_.Action -eq 'Allow' -and $_.ListType -eq 'Sender' } + + if ($AllowedSenders.Count -eq 0) { + $Result = "✅ **Pass**: No allowed senders configured in tenant allow/block list." + $Status = 'Pass' + } else { + $ResultTable = $AllowedSenders | Select-Object -First 10 | ForEach-Object { + [PSCustomObject]@{ + 'Value' = $_.Value + 'Action' = $_.Action + 'List Type' = $_.ListType + } + } + + $Result = "❌ **Fail**: $($AllowedSenders.Count) allowed sender(s) configured in tenant allow/block list" + if ($AllowedSenders.Count -gt 10) { + $Result += " (showing first 10)" + } + $Result += ":`n`n" + $Result += ($ResultTable | ConvertTo-Html -Fragment | Out-String) + $Status = 'Fail' + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO121' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Category 'Exchange Online' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Add-CippTestResult -Status 'Failed' -ResultMarkdown "Test execution failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Category 'Exchange Online' -TestId 'CISAMSEXO121' -TenantFilter $Tenant + } +} diff --git a/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO122.md b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO122.md new file mode 100644 index 000000000000..32352dc39224 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO122.md @@ -0,0 +1,22 @@ +Safe lists SHOULD NOT be enabled. + +Safe lists in Outlook bypass Exchange Online Protection (EOP) spam filtering, which can allow malicious emails from compromised accounts or domains on users' safe senders lists to reach their inboxes. This creates a security risk that attackers can exploit through social engineering. + +**Remediation Action:** + +1. Navigate to Microsoft 365 Defender portal > Email & collaboration > Policies & rules > Threat policies > Anti-spam +2. Select each anti-spam policy +3. Under "Actions": + - Disable "Enable end-user spam notifications" + - Or ensure "On" for safe lists is disabled +4. Or use PowerShell: +```powershell +Set-HostedContentFilterPolicy -Identity "Default" -EnableSafeList $false +``` + +**Links:** +- [CISA SCubaGear EXO Baseline - MS.EXO.12.2](https://github.com/cisagov/ScubaGear/blob/main/PowerShell/ScubaGear/baselines/exo.md#msexo122v1) +- [Configure anti-spam policies](https://learn.microsoft.com/microsoft-365/security/office-365-security/anti-spam-policies-configure) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO122.ps1 b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO122.ps1 new file mode 100644 index 000000000000..6a55b8c6f401 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO122.ps1 @@ -0,0 +1,50 @@ +function Invoke-CippTestCISAMSEXO122 { + <# + .SYNOPSIS + Tests MS.EXO.12.2 - Safe lists SHOULD NOT be enabled + + .DESCRIPTION + Checks if anti-spam policies have safe lists disabled + + .FUNCTIONALITY + Internal + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Tenant + ) + + try { + $SpamPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoHostedContentFilterPolicy' + + if (-not $SpamPolicies) { + Add-CippTestResult -Status 'Skipped' -ResultMarkdown 'ExoHostedContentFilterPolicy cache not found. Please refresh the cache for this tenant.' -Risk 'Medium' -Category 'Exchange Online' -TestId 'CISAMSEXO122' -TenantFilter $Tenant + return + } + + $FailedPolicies = $SpamPolicies | Where-Object { $_.EnableSafeList -eq $true } + + if ($FailedPolicies.Count -eq 0) { + $Result = "✅ **Pass**: All $($SpamPolicies.Count) anti-spam policy/policies have safe lists disabled." + $Status = 'Pass' + } else { + $ResultTable = foreach ($Policy in $FailedPolicies) { + [PSCustomObject]@{ + 'Policy Name' = $Policy.Name + 'Safe List Enabled' = $Policy.EnableSafeList + } + } + + $Result = "❌ **Fail**: $($FailedPolicies.Count) of $($SpamPolicies.Count) anti-spam policy/policies have safe lists enabled:`n`n" + $Result += ($ResultTable | ConvertTo-Html -Fragment | Out-String) + $Status = 'Fail' + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO122' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Category 'Exchange Online' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Add-CippTestResult -Status 'Failed' -ResultMarkdown "Test execution failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Category 'Exchange Online' -TestId 'CISAMSEXO122' -TenantFilter $Tenant + } +} diff --git a/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO131.md b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO131.md new file mode 100644 index 000000000000..26ea16f7bbb2 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO131.md @@ -0,0 +1,19 @@ +Mailbox auditing SHALL be enabled. + +Mailbox auditing logs user and administrator actions in mailboxes, providing critical forensic data for security investigations and compliance requirements. This enables detection of unauthorized access and data exfiltration attempts. + +**Remediation Action:** + +1. Navigate to Microsoft Purview compliance portal > Audit +2. Ensure mailbox auditing is turned on +3. Or use PowerShell: +```powershell +Set-OrganizationConfig -AuditDisabled $false +``` + +**Links:** +- [CISA SCubaGear EXO Baseline - MS.EXO.13.1](https://github.com/cisagov/ScubaGear/blob/main/PowerShell/ScubaGear/baselines/exo.md#msexo131v1) +- [Manage mailbox auditing](https://learn.microsoft.com/purview/audit-mailboxes) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO131.ps1 b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO131.ps1 new file mode 100644 index 000000000000..ae763c155326 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO131.ps1 @@ -0,0 +1,89 @@ +function Invoke-CippTestCISAMSEXO131 { + <# + .SYNOPSIS + Tests MS.EXO.13.1 - Mailbox auditing SHALL be enabled + + .DESCRIPTION + Checks if mailbox auditing is enabled in Exchange Online organization config + + .FUNCTIONALITY + Internal + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Tenant + ) + + try { + $OrgConfig = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoOrganizationConfig' + + if (-not $OrgConfig) { + Add-CippTestResult -Status 'Skipped' -ResultMarkdown 'ExoOrganizationConfig cache not found. Please refresh the cache for this tenant.' -Risk 'High' -Category 'Exchange Online' -TestId 'CISAMSEXO131' -TenantFilter $Tenant + return + } + + $OrgConfigObject = $OrgConfig | Select-Object -First 1 + + if ($OrgConfigObject.AuditDisabled -eq $false) { + $Result = '✅ **Pass**: Mailbox auditing is enabled for the organization.' + $Status = 'Pass' + } else { + $Result = "❌ **Fail**: Mailbox auditing is disabled for the organization.`n`n" + $Result += "**Current Setting:**`n" + $Result += "- AuditDisabled: $($OrgConfigObject.AuditDisabled)" + $Status = 'Fail' + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO131' -Status $Status -ResultMarkdown $Result -Risk 'High' -Category 'Exchange Online' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Add-CippTestResult -Status 'Failed' -ResultMarkdown "Test execution failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Category 'Exchange Online' -TestId 'CISAMSEXO131' -TenantFilter $Tenant + } +} +function Invoke-CippTestCISAMSEXO131 { + <# + .SYNOPSIS + MS.EXO.13.1 - Mailbox auditing SHALL be enabled + + .DESCRIPTION + Tests if mailbox auditing is enabled organization-wide + + .LINK + https://github.com/cisagov/ScubaGear + #> + param($Tenant) + + try { + $OrgConfig = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoOrganizationConfig' + + if (-not $OrgConfig) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO131' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'ExoOrganizationConfig cache not found. Please ensure cache data is available.' -Risk 'High' -Name 'Mailbox auditing enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Exchange Online' + return + } + + $AuditDisabled = $OrgConfig.AuditDisabled + + # AuditDisabled should be False (meaning auditing is enabled) + if ($AuditDisabled -eq $false) { + $Status = 'Passed' + $Result = "✅ Well done. Mailbox auditing is enabled organization-wide.`n`n" + $Result += "**Current Configuration:**`n" + $Result += "- Mailbox Auditing: **Enabled** ✅`n`n" + $Result += 'Mailbox auditing helps track and log mailbox access and modifications for security and compliance purposes.' + } else { + $Status = 'Failed' + $Result = "❌ Mailbox auditing is disabled organization-wide.`n`n" + $Result += "**Current Configuration:**`n" + $Result += "- Mailbox Auditing: **Disabled** ❌`n`n" + $Result += '**Recommendation:** Enable mailbox auditing to track mailbox access and modifications. This is critical for security investigations and compliance requirements.' + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO131' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'Mailbox auditing enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Exchange Online' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run CISA test CISAMSEXO131: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO131' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test execution failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Mailbox auditing enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Exchange Online' + } +} diff --git a/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO141.md b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO141.md new file mode 100644 index 000000000000..8b7ff1d73025 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO141.md @@ -0,0 +1,21 @@ +High confidence spam SHALL be quarantined. + +High confidence spam represents emails that Microsoft's filtering systems are very confident are spam. Quarantining these messages rather than delivering them to junk mail folders provides better protection and allows administrators to review and release legitimate emails if needed. + +**Remediation Action:** + +1. Navigate to Microsoft 365 Defender portal > Email & collaboration > Policies & rules > Threat policies > Anti-spam +2. Select each anti-spam policy +3. Under "Actions": + - Set "High confidence spam" action to "Quarantine message" +4. Or use PowerShell: +```powershell +Set-HostedContentFilterPolicy -Identity "Default" -HighConfidenceSpamAction Quarantine +``` + +**Links:** +- [CISA SCubaGear EXO Baseline - MS.EXO.14.1](https://github.com/cisagov/ScubaGear/blob/main/PowerShell/ScubaGear/baselines/exo.md#msexo141v2) +- [Configure anti-spam policies](https://learn.microsoft.com/microsoft-365/security/office-365-security/anti-spam-policies-configure) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO141.ps1 b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO141.ps1 new file mode 100644 index 000000000000..051518cad861 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO141.ps1 @@ -0,0 +1,51 @@ +function Invoke-CippTestCISAMSEXO141 { + <# + .SYNOPSIS + Tests MS.EXO.14.1 - High confidence spam SHALL be quarantined + + .DESCRIPTION + Checks if high confidence spam action is set to Quarantine in anti-spam policies + + .FUNCTIONALITY + Internal + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Tenant + ) + + try { + $SpamPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoHostedContentFilterPolicy' + + if (-not $SpamPolicies) { + Add-CippTestResult -Status 'Skipped' -ResultMarkdown 'ExoHostedContentFilterPolicy cache not found. Please refresh the cache for this tenant.' -Risk 'High' -Category 'Exchange Online' -TestId 'CISAMSEXO141' -TenantFilter $Tenant + return + } + + $FailedPolicies = $SpamPolicies | Where-Object { $_.HighConfidenceSpamAction -ne 'Quarantine' } + + if ($FailedPolicies.Count -eq 0) { + $Result = "✅ **Pass**: All $($SpamPolicies.Count) anti-spam policy/policies quarantine high confidence spam." + $Status = 'Pass' + } else { + $ResultTable = foreach ($Policy in $FailedPolicies) { + [PSCustomObject]@{ + 'Policy Name' = $Policy.Name + 'Current Action' = $Policy.HighConfidenceSpamAction + 'Expected' = 'Quarantine' + } + } + + $Result = "❌ **Fail**: $($FailedPolicies.Count) of $($SpamPolicies.Count) anti-spam policy/policies do not quarantine high confidence spam:`n`n" + $Result += ($ResultTable | ConvertTo-Html -Fragment | Out-String) + $Status = 'Fail' + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO141' -Status $Status -ResultMarkdown $Result -Risk 'High' -Category 'Exchange Online' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Add-CippTestResult -Status 'Failed' -ResultMarkdown "Test execution failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Category 'Exchange Online' -TestId 'CISAMSEXO141' -TenantFilter $Tenant + } +} diff --git a/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO142.md b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO142.md new file mode 100644 index 000000000000..ec3b2ce5e871 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO142.md @@ -0,0 +1,23 @@ +Spam and high confidence spam SHALL be moved to either the junk email folder or the quarantine folder. + +Properly handling spam emails prevents users from being exposed to potentially malicious content while still allowing recovery of false positives. Moving spam to junk folders or quarantine provides a balance between security and usability. + +**Remediation Action:** + +1. Navigate to Microsoft 365 Defender portal > Email & collaboration > Policies & rules > Threat policies > Anti-spam +2. Select each anti-spam policy +3. Under "Actions": + - Set "Spam" action to "Move message to Junk Email folder" or "Quarantine message" +4. Or use PowerShell: +```powershell +Set-HostedContentFilterPolicy -Identity "Default" -SpamAction MoveToJmf +# Or +Set-HostedContentFilterPolicy -Identity "Default" -SpamAction Quarantine +``` + +**Links:** +- [CISA SCubaGear EXO Baseline - MS.EXO.14.2](https://github.com/cisagov/ScubaGear/blob/main/PowerShell/ScubaGear/baselines/exo.md#msexo142v1) +- [Configure anti-spam policies](https://learn.microsoft.com/microsoft-365/security/office-365-security/anti-spam-policies-configure) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO142.ps1 b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO142.ps1 new file mode 100644 index 000000000000..f31995c16325 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO142.ps1 @@ -0,0 +1,54 @@ +function Invoke-CippTestCISAMSEXO142 { + <# + .SYNOPSIS + Tests MS.EXO.14.2 - Spam SHALL be moved to junk email or quarantine + + .DESCRIPTION + Checks if spam action is set to MoveToJmf or Quarantine in anti-spam policies + + .FUNCTIONALITY + Internal + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Tenant + ) + + try { + $SpamPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoHostedContentFilterPolicy' + + if (-not $SpamPolicies) { + Add-CippTestResult -Status 'Skipped' -ResultMarkdown 'ExoHostedContentFilterPolicy cache not found. Please refresh the cache for this tenant.' -Risk 'Medium' -Category 'Exchange Online' -TestId 'CISAMSEXO142' -TenantFilter $Tenant + return + } + + $AcceptableActions = @('MoveToJmf', 'Quarantine') + $FailedPolicies = [System.Collections.Generic.List[object]]::new() + + foreach ($Policy in $SpamPolicies) { + if ($Policy.SpamAction -notin $AcceptableActions) { + $FailedPolicies.Add([PSCustomObject]@{ + 'Policy Name' = $Policy.Name + 'Current Action' = $Policy.SpamAction + 'Expected' = 'MoveToJmf or Quarantine' + }) + } + } + + if ($FailedPolicies.Count -eq 0) { + $Result = "✅ **Pass**: All $($SpamPolicies.Count) anti-spam policy/policies move spam to junk folder or quarantine." + $Status = 'Pass' + } else { + $Result = "❌ **Fail**: $($FailedPolicies.Count) of $($SpamPolicies.Count) anti-spam policy/policies do not properly handle spam:`n`n" + $Result += ($FailedPolicies | ConvertTo-Html -Fragment | Out-String) + $Status = 'Fail' + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO142' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Category 'Exchange Online' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Add-CippTestResult -Status 'Failed' -ResultMarkdown "Test execution failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Category 'Exchange Online' -TestId 'CISAMSEXO142' -TenantFilter $Tenant + } +} diff --git a/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO143.md b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO143.md new file mode 100644 index 000000000000..4958c0a1627e --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO143.md @@ -0,0 +1,22 @@ +Allowed senders and domains SHOULD NOT be added to the anti-spam filter. + +Adding senders or domains to the allowed list bypasses spam filtering, which can be exploited by attackers. Compromised accounts or spoofed emails from allowed domains will bypass security controls and reach users' inboxes unchecked. + +**Remediation Action:** + +1. Navigate to Microsoft 365 Defender portal > Email & collaboration > Policies & rules > Threat policies > Anti-spam +2. Select each anti-spam policy +3. Under "Allowed and blocked senders and domains": + - Review and remove entries from "Allowed senders" list + - Review and remove entries from "Allowed domains" list +4. Or use PowerShell: +```powershell +Set-HostedContentFilterPolicy -Identity "Default" -AllowedSenders @() -AllowedSenderDomains @() +``` + +**Links:** +- [CISA SCubaGear EXO Baseline - MS.EXO.14.3](https://github.com/cisagov/ScubaGear/blob/main/PowerShell/ScubaGear/baselines/exo.md#msexo143v1) +- [Configure allowed and blocked senders](https://learn.microsoft.com/microsoft-365/security/office-365-security/create-safe-sender-lists-in-office-365) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO143.ps1 b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO143.ps1 new file mode 100644 index 000000000000..1824f85822cc --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO143.ps1 @@ -0,0 +1,57 @@ +function Invoke-CippTestCISAMSEXO143 { + <# + .SYNOPSIS + Tests MS.EXO.14.3 - Spam filter bypass SHALL be disabled + + .DESCRIPTION + Checks if anti-spam policies have empty allowed senders and domains lists + + .FUNCTIONALITY + Internal + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Tenant + ) + + try { + $SpamPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoHostedContentFilterPolicy' + + if (-not $SpamPolicies) { + Add-CippTestResult -Status 'Skipped' -ResultMarkdown 'ExoHostedContentFilterPolicy cache not found. Please refresh the cache for this tenant.' -Risk 'High' -Category 'Exchange Online' -TestId 'CISAMSEXO143' -TenantFilter $Tenant + return + } + + $FailedPolicies = [System.Collections.Generic.List[object]]::new() + + foreach ($Policy in $SpamPolicies) { + $AllowedSenders = if ($Policy.AllowedSenders) { $Policy.AllowedSenders.Count } else { 0 } + $AllowedSenderDomains = if ($Policy.AllowedSenderDomains) { $Policy.AllowedSenderDomains.Count } else { 0 } + + if ($AllowedSenders -gt 0 -or $AllowedSenderDomains -gt 0) { + $FailedPolicies.Add([PSCustomObject]@{ + 'Policy Name' = $Policy.Name + 'Allowed Senders' = $AllowedSenders + 'Allowed Domains' = $AllowedSenderDomains + 'Issue' = 'Has allowed senders/domains that bypass spam filtering' + }) + } + } + + if ($FailedPolicies.Count -eq 0) { + $Result = "✅ **Pass**: All $($SpamPolicies.Count) anti-spam policy/policies have no spam filter bypasses configured." + $Status = 'Pass' + } else { + $Result = "❌ **Fail**: $($FailedPolicies.Count) of $($SpamPolicies.Count) anti-spam policy/policies have spam filter bypasses configured:`n`n" + $Result += ($FailedPolicies | ConvertTo-Html -Fragment | Out-String) + $Status = 'Fail' + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO143' -Status $Status -ResultMarkdown $Result -Risk 'High' -Category 'Exchange Online' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Add-CippTestResult -Status 'Failed' -ResultMarkdown "Test execution failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Category 'Exchange Online' -TestId 'CISAMSEXO143' -TenantFilter $Tenant + } +} diff --git a/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO151.md b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO151.md new file mode 100644 index 000000000000..5fbbde6b6bca --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO151.md @@ -0,0 +1,21 @@ +URL comparison with a block-list SHOULD be enabled. + +Safe Links provides time-of-click verification of URLs in email messages and Office documents. This protection helps prevent users from clicking on malicious links by checking URLs against a dynamically updated block-list of known malicious websites. + +**Remediation Action:** + +1. Navigate to Microsoft 365 Defender portal > Email & collaboration > Policies & rules > Threat policies > Safe Links +2. Select each Safe Links policy +3. Under "URL & click protection settings": + - Enable "On: Safe Links checks a list of known, malicious links when users click links in email" +4. Or use PowerShell: +```powershell +Set-SafeLinksPolicy -Identity "Default" -EnableSafeLinksForEmail $true +``` + +**Links:** +- [CISA SCubaGear EXO Baseline - MS.EXO.15.1](https://github.com/cisagov/ScubaGear/blob/main/PowerShell/ScubaGear/baselines/exo.md#msexo151v1) +- [Set up Safe Links policies](https://learn.microsoft.com/microsoft-365/security/office-365-security/safe-links-policies-configure) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO151.ps1 b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO151.ps1 new file mode 100644 index 000000000000..ec2878ef8263 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO151.ps1 @@ -0,0 +1,50 @@ +function Invoke-CippTestCISAMSEXO151 { + <# + .SYNOPSIS + Tests MS.EXO.15.1 - URL comparison with a block-list SHOULD be enabled + + .DESCRIPTION + Checks if Safe Links policies have URL scanning enabled + + .FUNCTIONALITY + Internal + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Tenant + ) + + try { + $SafeLinksPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoSafeLinksPolicy' + + if (-not $SafeLinksPolicies) { + Add-CippTestResult -Status 'Skipped' -ResultMarkdown 'ExoSafeLinksPolicy cache not found. Please refresh the cache for this tenant.' -Risk 'Medium' -Category 'Exchange Online' -TestId 'CISAMSEXO151' -TenantFilter $Tenant + return + } + + $FailedPolicies = $SafeLinksPolicies | Where-Object { -not $_.EnableSafeLinksForEmail } + + if ($FailedPolicies.Count -eq 0) { + $Result = "✅ **Pass**: All $($SafeLinksPolicies.Count) Safe Links policy/policies have URL comparison with block-list enabled." + $Status = 'Pass' + } else { + $ResultTable = foreach ($Policy in $FailedPolicies) { + [PSCustomObject]@{ + 'Policy Name' = $Policy.Name + 'Safe Links for Email' = $Policy.EnableSafeLinksForEmail + } + } + + $Result = "❌ **Fail**: $($FailedPolicies.Count) of $($SafeLinksPolicies.Count) Safe Links policy/policies do not have URL scanning enabled:`n`n" + $Result += ($ResultTable | ConvertTo-Html -Fragment | Out-String) + $Status = 'Fail' + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO151' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Category 'Exchange Online' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Add-CippTestResult -Status 'Failed' -ResultMarkdown "Test execution failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Category 'Exchange Online' -TestId 'CISAMSEXO151' -TenantFilter $Tenant + } +} diff --git a/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO152.md b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO152.md new file mode 100644 index 000000000000..db04d60a9e24 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO152.md @@ -0,0 +1,22 @@ +Real-time suspicious URL and file-link scanning SHOULD be enabled. + +Real-time scanning checks suspicious URLs at the time of click, even if the URL wasn't initially identified as malicious. This provides additional protection against rapidly evolving threats and newly created malicious websites. + +**Remediation Action:** + +1. Navigate to Microsoft 365 Defender portal > Email & collaboration > Policies & rules > Threat policies > Safe Links +2. Select each Safe Links policy +3. Under "URL & click protection settings": + - Enable "Apply Safe Links to email messages sent within the organization" + - Enable "Apply real-time URL scanning for suspicious links and links that point to files" +4. Or use PowerShell: +```powershell +Set-SafeLinksPolicy -Identity "Default" -ScanUrls $true +``` + +**Links:** +- [CISA SCubaGear EXO Baseline - MS.EXO.15.2](https://github.com/cisagov/ScubaGear/blob/main/PowerShell/ScubaGear/baselines/exo.md#msexo152v1) +- [Set up Safe Links policies](https://learn.microsoft.com/microsoft-365/security/office-365-security/safe-links-policies-configure) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO152.ps1 b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO152.ps1 new file mode 100644 index 000000000000..fe7d29c7047d --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO152.ps1 @@ -0,0 +1,50 @@ +function Invoke-CippTestCISAMSEXO152 { + <# + .SYNOPSIS + Tests MS.EXO.15.2 - Real-time suspicious URL and file-link scanning SHOULD be enabled + + .DESCRIPTION + Checks if Safe Links policies have real-time link scanning enabled + + .FUNCTIONALITY + Internal + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Tenant + ) + + try { + $SafeLinksPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoSafeLinksPolicy' + + if (-not $SafeLinksPolicies) { + Add-CippTestResult -Status 'Skipped' -ResultMarkdown 'ExoSafeLinksPolicy cache not found. Please refresh the cache for this tenant.' -Risk 'Medium' -Category 'Exchange Online' -TestId 'CISAMSEXO152' -TenantFilter $Tenant + return + } + + $FailedPolicies = $SafeLinksPolicies | Where-Object { -not $_.ScanUrls } + + if ($FailedPolicies.Count -eq 0) { + $Result = "✅ **Pass**: All $($SafeLinksPolicies.Count) Safe Links policy/policies have real-time URL scanning enabled." + $Status = 'Pass' + } else { + $ResultTable = foreach ($Policy in $FailedPolicies) { + [PSCustomObject]@{ + 'Policy Name' = $Policy.Name + 'Scan URLs' = $Policy.ScanUrls + } + } + + $Result = "❌ **Fail**: $($FailedPolicies.Count) of $($SafeLinksPolicies.Count) Safe Links policy/policies do not have real-time URL scanning enabled:`n`n" + $Result += ($ResultTable | ConvertTo-Html -Fragment | Out-String) + $Status = 'Fail' + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO152' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Category 'Exchange Online' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Add-CippTestResult -Status 'Failed' -ResultMarkdown "Test execution failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Category 'Exchange Online' -TestId 'CISAMSEXO152' -TenantFilter $Tenant + } +} diff --git a/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO153.md b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO153.md new file mode 100644 index 000000000000..f8c99f40da39 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO153.md @@ -0,0 +1,21 @@ +User click tracking SHOULD be disabled. + +Click tracking in Safe Links can collect information about which URLs users click, which may raise privacy concerns. CISA recommends disabling this feature to protect user privacy while still maintaining URL protection capabilities. + +**Remediation Action:** + +1. Navigate to Microsoft 365 Defender portal > Email & collaboration > Policies & rules > Threat policies > Safe Links +2. Select each Safe Links policy +3. Under "URL & click protection settings": + - Disable "Track user clicks" +4. Or use PowerShell: +```powershell +Set-SafeLinksPolicy -Identity "Default" -TrackUserClicks $false +``` + +**Links:** +- [CISA SCubaGear EXO Baseline - MS.EXO.15.3](https://github.com/cisagov/ScubaGear/blob/main/PowerShell/ScubaGear/baselines/exo.md#msexo153v1) +- [Set up Safe Links policies](https://learn.microsoft.com/microsoft-365/security/office-365-security/safe-links-policies-configure) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO153.ps1 b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO153.ps1 new file mode 100644 index 000000000000..4538a13b0643 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO153.ps1 @@ -0,0 +1,50 @@ +function Invoke-CippTestCISAMSEXO153 { + <# + .SYNOPSIS + Tests MS.EXO.15.3 - User click tracking SHOULD be disabled + + .DESCRIPTION + Checks if Safe Links policies have click tracking disabled for privacy + + .FUNCTIONALITY + Internal + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Tenant + ) + + try { + $SafeLinksPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoSafeLinksPolicy' + + if (-not $SafeLinksPolicies) { + Add-CippTestResult -Status 'Skipped' -ResultMarkdown 'ExoSafeLinksPolicy cache not found. Please refresh the cache for this tenant.' -Risk 'Low' -Category 'Exchange Online' -TestId 'CISAMSEXO153' -TenantFilter $Tenant + return + } + + $FailedPolicies = $SafeLinksPolicies | Where-Object { $_.TrackUserClicks -eq $true } + + if ($FailedPolicies.Count -eq 0) { + $Result = "✅ **Pass**: All $($SafeLinksPolicies.Count) Safe Links policy/policies have click tracking disabled." + $Status = 'Pass' + } else { + $ResultTable = foreach ($Policy in $FailedPolicies) { + [PSCustomObject]@{ + 'Policy Name' = $Policy.Name + 'Track User Clicks' = $Policy.TrackUserClicks + } + } + + $Result = "❌ **Fail**: $($FailedPolicies.Count) of $($SafeLinksPolicies.Count) Safe Links policy/policies have click tracking enabled:`n`n" + $Result += ($ResultTable | ConvertTo-Html -Fragment | Out-String) + $Status = 'Fail' + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO153' -Status $Status -ResultMarkdown $Result -Risk 'Low' -Category 'Exchange Online' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Add-CippTestResult -Status 'Failed' -ResultMarkdown "Test execution failed: $($ErrorMessage.NormalizedError)" -Risk 'Low' -Category 'Exchange Online' -TestId 'CISAMSEXO153' -TenantFilter $Tenant + } +} diff --git a/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO171.md b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO171.md new file mode 100644 index 000000000000..677b8a837248 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO171.md @@ -0,0 +1,19 @@ +Microsoft Purview Audit (Standard) logging SHALL be enabled. + +Audit logging captures user and administrator activities across Microsoft 365 services, providing essential forensic data for security investigations, compliance requirements, and detecting unauthorized access or data breaches. + +**Remediation Action:** + +1. Navigate to Microsoft Purview compliance portal > Audit +2. Turn on audit log search +3. Or use PowerShell: +```powershell +Set-AdminAuditLogConfig -UnifiedAuditLogIngestionEnabled $true +``` + +**Links:** +- [CISA SCubaGear EXO Baseline - MS.EXO.17.1](https://github.com/cisagov/ScubaGear/blob/main/PowerShell/ScubaGear/baselines/exo.md#msexo171v1) +- [Turn audit log search on or off](https://learn.microsoft.com/purview/audit-log-enable-disable) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO171.ps1 b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO171.ps1 new file mode 100644 index 000000000000..cab8cf17243e --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO171.ps1 @@ -0,0 +1,46 @@ +function Invoke-CippTestCISAMSEXO171 { + <# + .SYNOPSIS + Tests MS.EXO.17.1 - Microsoft Purview Audit (Standard) logging SHALL be enabled + + .DESCRIPTION + Checks if unified audit log ingestion is enabled + + .FUNCTIONALITY + Internal + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Tenant + ) + + try { + $AuditConfig = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoAdminAuditLogConfig' + + if (-not $AuditConfig) { + Add-CippTestResult -Status 'Skipped' -ResultMarkdown 'ExoAdminAuditLogConfig cache not found. Please refresh the cache for this tenant.' -Risk 'High' -Category 'Exchange Online' -TestId 'CISAMSEXO171' -TenantFilter $Tenant + return + } + + $AuditConfigObject = $AuditConfig | Select-Object -First 1 + + if ($AuditConfigObject.UnifiedAuditLogIngestionEnabled -eq $true) { + $Result = "✅ **Pass**: Microsoft Purview Audit (Standard) logging is enabled.`n`n" + $Result += "**Current Settings:**`n" + $Result += "- UnifiedAuditLogIngestionEnabled: $($AuditConfigObject.UnifiedAuditLogIngestionEnabled)" + $Status = 'Pass' + } else { + $Result = "❌ **Fail**: Microsoft Purview Audit (Standard) logging is not enabled.`n`n" + $Result += "**Current Settings:**`n" + $Result += "- UnifiedAuditLogIngestionEnabled: $($AuditConfigObject.UnifiedAuditLogIngestionEnabled)" + $Status = 'Fail' + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO171' -Status $Status -ResultMarkdown $Result -Risk 'High' -Category 'Exchange Online' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Add-CippTestResult -Status 'Failed' -ResultMarkdown "Test execution failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Category 'Exchange Online' -TestId 'CISAMSEXO171' -TenantFilter $Tenant + } +} diff --git a/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO173.md b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO173.md new file mode 100644 index 000000000000..32dcbc6e2f79 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO173.md @@ -0,0 +1,22 @@ +Audit logs SHALL be maintained for at least the minimum duration dictated by OMB M-21-31 (1 year). + +Maintaining audit logs for an adequate retention period is essential for security investigations, compliance audits, and meeting federal record-keeping requirements. A minimum of one year retention allows organizations to investigate incidents and establish historical baselines. + +**Remediation Action:** + +1. Navigate to Microsoft Purview compliance portal > Data lifecycle management > Microsoft 365 retention > Retention policies +2. Create or modify retention policy for audit logs +3. Or use PowerShell: +```powershell +# Enable admin audit logging (provides 1 year retention) +Set-AdminAuditLogConfig -AdminAuditLogEnabled $true + +# For longer retention, configure retention policies in Purview +``` + +**Links:** +- [CISA SCubaGear EXO Baseline - MS.EXO.17.3](https://github.com/cisagov/ScubaGear/blob/main/PowerShell/ScubaGear/baselines/exo.md#msexo173v1) +- [Audit log retention policies](https://learn.microsoft.com/purview/audit-log-retention-policies) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO173.ps1 b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO173.ps1 new file mode 100644 index 000000000000..80aa93c17623 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO173.ps1 @@ -0,0 +1,46 @@ +function Invoke-CippTestCISAMSEXO173 { + <# + .SYNOPSIS + Tests MS.EXO.17.3 - Audit logs SHALL be maintained for at least the minimum duration + + .DESCRIPTION + Checks if admin audit log is enabled (provides 1 year retention) + + .FUNCTIONALITY + Internal + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Tenant + ) + + try { + $AuditConfig = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoAdminAuditLogConfig' + + if (-not $AuditConfig) { + Add-CippTestResult -Status 'Skipped' -ResultMarkdown 'ExoAdminAuditLogConfig cache not found. Please refresh the cache for this tenant.' -Risk 'Medium' -Category 'Exchange Online' -TestId 'CISAMSEXO173' -TenantFilter $Tenant + return + } + + $AuditConfigObject = $AuditConfig | Select-Object -First 1 + + if ($AuditConfigObject.AdminAuditLogEnabled -eq $true) { + $Result = "✅ **Pass**: Admin audit log is enabled (provides 1 year retention).`n`n" + $Result += "**Current Settings:**`n" + $Result += "- AdminAuditLogEnabled: $($AuditConfigObject.AdminAuditLogEnabled)" + $Status = 'Pass' + } else { + $Result = "❌ **Fail**: Admin audit log is not enabled.`n`n" + $Result += "**Current Settings:**`n" + $Result += "- AdminAuditLogEnabled: $($AuditConfigObject.AdminAuditLogEnabled)" + $Status = 'Fail' + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO173' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Category 'Exchange Online' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Add-CippTestResult -Status 'Failed' -ResultMarkdown "Test execution failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Category 'Exchange Online' -TestId 'CISAMSEXO173' -TenantFilter $Tenant + } +} diff --git a/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO31.md b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO31.md new file mode 100644 index 000000000000..4cb5dc494993 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO31.md @@ -0,0 +1,27 @@ +DKIM SHOULD be enabled for all domains. + +DomainKeys Identified Mail (DKIM) adds a digital signature to outgoing email messages, allowing receiving mail servers to verify that the email actually came from your domain and wasn't altered in transit. This helps prevent email spoofing and improves email deliverability. + +**Remediation Action:** + +1. Navigate to Microsoft 365 Defender portal > Email & collaboration > Policies & rules > Threat policies > DKIM +2. For each domain: + - Select the domain + - Click "Create DKIM keys" if not already created + - Publish the CNAME records to DNS + - Enable DKIM signing +3. Or use PowerShell: +```powershell +# Create DKIM signing configuration +New-DkimSigningConfig -DomainName "contoso.com" -Enabled $true + +# Enable existing DKIM configuration +Set-DkimSigningConfig -Identity "contoso.com" -Enabled $true +``` + +**Links:** +- [CISA SCubaGear EXO Baseline - MS.EXO.3.1](https://github.com/cisagov/ScubaGear/blob/main/PowerShell/ScubaGear/baselines/exo.md#msexo31v1) +- [Use DKIM to validate outbound email](https://learn.microsoft.com/microsoft-365/security/office-365-security/email-authentication-dkim-configure) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO31.ps1 b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO31.ps1 new file mode 100644 index 000000000000..f6559852e341 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO31.ps1 @@ -0,0 +1,64 @@ +function Invoke-CippTestCISAMSEXO31 { + <# + .SYNOPSIS + Tests MS.EXO.3.1 - DKIM SHOULD be enabled for all domains + + .DESCRIPTION + Checks if DKIM (DomainKeys Identified Mail) signing is enabled for all accepted domains + + .FUNCTIONALITY + Internal + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Tenant + ) + + try { + $DkimConfigs = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoDkimSigningConfig' + $AcceptedDomains = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoAcceptedDomains' + + if (-not $DkimConfigs -or -not $AcceptedDomains) { + Add-CippTestResult -Status 'Skipped' -ResultMarkdown 'Required cache (ExoDkimSigningConfig or ExoAcceptedDomains) not found. Please refresh the cache for this tenant.' -Risk 'Medium' -Category 'Exchange Online' -TestId 'CISAMSEXO31' -TenantFilter $Tenant + return + } + + # Filter to non-internal accepted domains + $SendingDomains = $AcceptedDomains | Where-Object { -not $_.SendingFromDomainDisabled } + + if (($SendingDomains | Measure-Object).Count -eq 0) { + Add-CippTestResult -Status 'Pass' -ResultMarkdown '✅ **Pass**: No sending domains found to check DKIM configuration.' -Risk 'Medium' -Category 'Exchange Online' -TestId 'CISAMSEXO31' -TenantFilter $Tenant + return + } + + $FailedDomains = [System.Collections.Generic.List[object]]::new() + + foreach ($Domain in $SendingDomains) { + $DkimConfig = $DkimConfigs | Where-Object { $_.Domain -eq $Domain.DomainName } + + if (-not $DkimConfig -or -not $DkimConfig.Enabled) { + $FailedDomains.Add([PSCustomObject]@{ + 'Domain' = $Domain.DomainName + 'DKIM Enabled' = if ($DkimConfig) { $DkimConfig.Enabled } else { 'Not Configured' } + 'Status' = if (-not $DkimConfig) { 'No DKIM config found' } else { 'DKIM disabled' } + }) + } + } + + if ($FailedDomains.Count -eq 0) { + $Result = "✅ **Pass**: DKIM is enabled for all $($SendingDomains.Count) sending domain(s)." + $Status = 'Pass' + } else { + $Result = "❌ **Fail**: $($FailedDomains.Count) of $($SendingDomains.Count) domain(s) do not have DKIM properly enabled:`n`n" + $Result += ($FailedDomains | ConvertTo-Html -Fragment | Out-String) + $Status = 'Fail' + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO31' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Category 'Exchange Online' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Add-CippTestResult -Status 'Failed' -ResultMarkdown "Test execution failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Category 'Exchange Online' -TestId 'CISAMSEXO31' -TenantFilter $Tenant + } +} diff --git a/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO51.md b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO51.md new file mode 100644 index 000000000000..4df1e83a5c56 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO51.md @@ -0,0 +1,23 @@ +SMTP AUTH SHALL be disabled in Exchange Online. + +SMTP AUTH is a legacy authentication protocol that doesn't support modern security features like multi-factor authentication. Disabling SMTP AUTH reduces the attack surface and forces applications to use more secure authentication methods like OAuth 2.0. + +**Remediation Action:** + +1. Navigate to Exchange Admin Center > Mail flow > SMTP AUTH +2. Disable SMTP AUTH for all users or specific users +3. Or use PowerShell to disable organization-wide: +```powershell +Set-TransportConfig -SmtpClientAuthenticationDisabled $true +``` +4. Or disable per-mailbox: +```powershell +Set-CASMailbox -Identity user@domain.com -SmtpClientAuthenticationDisabled $true +``` + +**Links:** +- [CISA SCubaGear EXO Baseline - MS.EXO.5.1](https://github.com/cisagov/ScubaGear/blob/main/PowerShell/ScubaGear/baselines/exo.md#msexo51v1) +- [Disable SMTP AUTH](https://learn.microsoft.com/exchange/clients-and-mobile-in-exchange-online/authenticated-client-smtp-submission) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO51.ps1 b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO51.ps1 new file mode 100644 index 000000000000..f4f6458081df --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO51.ps1 @@ -0,0 +1,141 @@ +function Invoke-CippTestCISAMSEXO51 { + <# + .SYNOPSIS + Tests MS.EXO.5.1 - SMTP AUTH SHALL be disabled for all users + + .DESCRIPTION + Checks if SMTP authentication is disabled in CAS Mailbox settings + + .FUNCTIONALITY + Internal + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Tenant + ) + + try { + $CASMailboxes = New-CIPPDbRequest -TenantFilter $Tenant -Type 'CASMailbox' + + if (-not $CASMailboxes) { + Add-CippTestResult -Status 'Skipped' -ResultMarkdown 'CASMailbox cache not found. Please refresh the cache for this tenant.' -Risk 'High' -Category 'Exchange Online' -TestId 'CISAMSEXO51' -TenantFilter $Tenant + return + } + + $FailedMailboxes = $CASMailboxes | Where-Object { $_.SmtpClientAuthenticationDisabled -eq $false } + + if ($FailedMailboxes.Count -eq 0) { + $Result = "✅ **Pass**: SMTP authentication is disabled for all $($CASMailboxes.Count) mailbox(es)." + $Status = 'Pass' + } else { + $ResultTable = $FailedMailboxes | Select-Object -First 10 | ForEach-Object { + [PSCustomObject]@{ + 'Display Name' = $_.DisplayName + 'Identity' = $_.Identity + 'SMTP Auth Disabled' = $_.SmtpClientAuthenticationDisabled + } + } + + $Result = "❌ **Fail**: $($FailedMailboxes.Count) of $($CASMailboxes.Count) mailbox(es) have SMTP authentication enabled" + if ($FailedMailboxes.Count -gt 10) { + $Result += ' (showing first 10)' + } + $Result += ":`n`n" + $Result += ($ResultTable | ConvertTo-Html -Fragment | Out-String) + $Status = 'Fail' + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO51' -Status $Status -ResultMarkdown $Result -Risk 'High' -Category 'Exchange Online' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Add-CippTestResult -Status 'Failed' -ResultMarkdown "Test execution failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Category 'Exchange Online' -TestId 'CISAMSEXO51' -TenantFilter $Tenant + } +} +function Invoke-CippTestCISAMSEXO51 { + <# + .SYNOPSIS + MS.EXO.5.1 - SMTP authentication SHALL be disabled + + .DESCRIPTION + Tests if SMTP AUTH is disabled in Exchange Online organization config + + .LINK + https://github.com/cisagov/ScubaGear + #> + param($Tenant) + + try { + $OrgConfig = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoOrganizationConfig' + + if (-not $OrgConfig) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO51' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'ExoOrganizationConfig cache not found. Please ensure cache data is available.' -Risk 'High' -Name 'SMTP authentication disabled' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Exchange Online' + return + } + + $SmtpAuthDisabled = $OrgConfig.SmtpClientAuthenticationDisabled + + if ($SmtpAuthDisabled -eq $true) { + $Status = 'Passed' + $Result = "✅ Well done. Your tenant has SMTP Authentication disabled organization-wide.`n`n" + $Result += "**Current Configuration:**`n" + $Result += "- SMTP Client Authentication: **Disabled** ✅`n" + } else { + $Status = 'Failed' + $Result = "❌ Your tenant has SMTP Authentication enabled.`n`n" + $Result += "**Current Configuration:**`n" + $Result += "- SMTP Client Authentication: **Enabled** ❌`n`n" + $Result += "**Recommendation:** Disable SMTP AUTH to prevent legacy authentication attacks. Users should use modern authentication methods.`n" + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO51' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'SMTP authentication disabled' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Exchange Online' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run CISA test CISAMSEXO51: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO51' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test execution failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'SMTP authentication disabled' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Exchange Online' + } +} +function Invoke-CippTestCISAMSEXO51 { + <# + .SYNOPSIS + MS.EXO.5.1 - SMTP authentication SHALL be disabled + + .DESCRIPTION + Tests if SMTP AUTH is disabled in Exchange Online organization config + + .LINK + https://github.com/cisagov/ScubaGear + #> + param($Tenant) + + try { + $OrgConfig = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoOrganizationConfig' + + if (-not $OrgConfig) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO51' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'ExoOrganizationConfig cache not found. Please ensure cache data is available.' -Risk 'High' -Name 'SMTP authentication disabled' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Exchange Online' + return + } + + $SmtpAuthDisabled = $OrgConfig.SmtpClientAuthenticationDisabled + + if ($SmtpAuthDisabled -eq $true) { + $Status = 'Passed' + $Result = "✅ Well done. Your tenant has SMTP Authentication disabled organization-wide.`n`n" + $Result += "**Current Configuration:**`n" + $Result += "- SMTP Client Authentication: **Disabled** ✅`n" + } else { + $Status = 'Failed' + $Result = "❌ Your tenant has SMTP Authentication enabled.`n`n" + $Result += "**Current Configuration:**`n" + $Result += "- SMTP Client Authentication: **Enabled** ❌`n`n" + $Result += "**Recommendation:** Disable SMTP AUTH to prevent legacy authentication attacks. Users should use modern authentication methods.`n" + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO51' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'SMTP authentication disabled' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Exchange Online' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run CISA test CISAMSEXO51: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO51' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test execution failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'SMTP authentication disabled' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Exchange Online' + } +} diff --git a/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO61.md b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO61.md new file mode 100644 index 000000000000..94c2efba5523 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO61.md @@ -0,0 +1,20 @@ +Contact folders SHALL NOT be shared with all domains, although they MAY be shared with specific domains. + +Sharing contact folders with external domains can expose sensitive organizational information. Limiting contact sharing to specific approved domains reduces the risk of information disclosure. + +**Remediation Action:** + +1. Navigate to Exchange Admin Center > Organization > Sharing +2. Review sharing policies +3. Remove or modify policies that allow contact sharing with all domains +4. Or use PowerShell: +```powershell +Set-SharingPolicy -Identity "Default Sharing Policy" -Domains @{Remove="*:ContactsSharing"} +``` + +**Links:** +- [CISA SCubaGear EXO Baseline - MS.EXO.6.1](https://github.com/cisagov/ScubaGear/blob/main/PowerShell/ScubaGear/baselines/exo.md#msexo61v1) +- [Sharing policies in Exchange Online](https://learn.microsoft.com/exchange/sharing/sharing-policies/sharing-policies) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO61.ps1 b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO61.ps1 new file mode 100644 index 000000000000..9f0ab9e412d7 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO61.ps1 @@ -0,0 +1,98 @@ +function Invoke-CippTestCISAMSEXO61 { + <# + .SYNOPSIS + Tests MS.EXO.6.1 - Contact folders SHALL NOT be shared with all domains + + .DESCRIPTION + Checks if sharing policies allow sharing contact folders with external domains + + .FUNCTIONALITY + Internal + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Tenant + ) + + try { + $SharingPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoSharingPolicy' + + if (-not $SharingPolicies) { + Add-CippTestResult -Status 'Skipped' -ResultMarkdown 'ExoSharingPolicy cache not found. Please refresh the cache for this tenant.' -Risk 'Medium' -Category 'Exchange Online' -TestId 'CISAMSEXO61' -TenantFilter $Tenant + return + } + + $FailedPolicies = [System.Collections.Generic.List[object]]::new() + + foreach ($Policy in $SharingPolicies) { + if ($Policy.Enabled) { + # Check if any domain allows contact sharing (ContactsSharing capability) + $ContactSharingDomains = $Policy.Domains | Where-Object { $_ -match 'ContactsSharing' } + if ($ContactSharingDomains) { + $FailedPolicies.Add([PSCustomObject]@{ + 'Policy Name' = $Policy.Name + 'Enabled' = $Policy.Enabled + 'Issue' = 'Allows contact sharing with external domains' + }) + } + } + } + + if ($FailedPolicies.Count -eq 0) { + $Result = '✅ **Pass**: No sharing policies allow contact folder sharing with external domains.' + $Status = 'Pass' + } else { + $Result = "❌ **Fail**: $($FailedPolicies.Count) sharing policy/policies allow contact folder sharing:`n`n" + $Result += ($FailedPolicies | ConvertTo-Html -Fragment | Out-String) + $Status = 'Fail' + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO61' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Category 'Exchange Online' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Add-CippTestResult -Status 'Failed' -ResultMarkdown "Test execution failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Category 'Exchange Online' -TestId 'CISAMSEXO61' -TenantFilter $Tenant + } +} +function Invoke-CippTestCISAMSEXO61 { + <# + .SYNOPSIS + MS.EXO.6.1 - Contact folder sharing SHALL be restricted + + .DESCRIPTION + Tests if contact folder sharing with external users is restricted + + .LINK + https://github.com/cisagov/ScubaGear + #> + param($Tenant) + + try { + $OrgConfig = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoOrganizationConfig' + + if (-not $OrgConfig) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO61' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'ExoOrganizationConfig cache not found. Please ensure cache data is available.' -Risk 'Medium' -Name 'Contact folder sharing restricted' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Exchange Online' + return + } + + # Check if external sharing of contacts is disabled + $SharingPolicy = $OrgConfig.DefaultSharingPolicy + + $Status = 'Skipped' + $Result = "⚠️ **Additional Data Required**`n`n" + $Result += "This test requires sharing policy details to verify contact folder sharing restrictions.`n`n" + $Result += "**Current Organization Configuration:**`n" + $Result += "- Default Sharing Policy: $($SharingPolicy)`n`n" + $Result += "**Manual verification recommended:**`n" + $Result += "1. Navigate to Exchange Admin Center > Organization > Sharing`n" + $Result += "2. Verify that contact folder sharing with external domains is disabled or limited`n" + $Result += "3. Check that the default policy does not allow 'ContactsSharing' for external domains`n" + + Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO61' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'Contact folder sharing restricted' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Exchange Online' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run CISA test CISAMSEXO61: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO61' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test execution failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'Contact folder sharing restricted' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Exchange Online' + } +} diff --git a/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO62.md b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO62.md new file mode 100644 index 000000000000..22e7bfad4310 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO62.md @@ -0,0 +1,24 @@ +Calendar details SHALL NOT be shared with all domains, although they MAY be shared with specific domains. + +Sharing detailed calendar information (including meeting subjects, locations, and attendees) with all external domains can expose sensitive business information. Limiting detailed calendar sharing to specific approved domains protects organizational privacy. + +**Remediation Action:** + +1. Navigate to Exchange Admin Center > Organization > Sharing +2. Review sharing policies +3. Ensure wildcard (*) domains only allow free/busy time, not detailed information +4. Or use PowerShell: +```powershell +# Allow only free/busy with all domains +Set-SharingPolicy -Identity "Default Sharing Policy" -Domains "*:CalendarSharingFreeBusySimple" + +# For specific domains, you can allow details +Set-SharingPolicy -Identity "Default Sharing Policy" -Domains @{Add="partner.com:CalendarSharingFreeBusyDetail"} +``` + +**Links:** +- [CISA SCubaGear EXO Baseline - MS.EXO.6.2](https://github.com/cisagov/ScubaGear/blob/main/PowerShell/ScubaGear/baselines/exo.md#msexo62v1) +- [Sharing policies in Exchange Online](https://learn.microsoft.com/exchange/sharing/sharing-policies/sharing-policies) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO62.ps1 b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO62.ps1 new file mode 100644 index 000000000000..1154f7bb7fe0 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO62.ps1 @@ -0,0 +1,58 @@ +function Invoke-CippTestCISAMSEXO62 { + <# + .SYNOPSIS + Tests MS.EXO.6.2 - Calendar details SHALL NOT be shared with all domains + + .DESCRIPTION + Checks if sharing policies allow sharing calendar details with external domains + + .FUNCTIONALITY + Internal + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Tenant + ) + + try { + $SharingPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoSharingPolicy' + + if (-not $SharingPolicies) { + Add-CippTestResult -Status 'Skipped' -ResultMarkdown 'ExoSharingPolicy cache not found. Please refresh the cache for this tenant.' -Risk 'Medium' -Category 'Exchange Online' -TestId 'CISAMSEXO62' -TenantFilter $Tenant + return + } + + $FailedPolicies = [System.Collections.Generic.List[object]]::new() + + foreach ($Policy in $SharingPolicies) { + if ($Policy.Enabled) { + # Check if wildcard domain (*) allows detailed calendar sharing + $WildcardDomains = $Policy.Domains | Where-Object { $_ -match '^\*:' -and $_ -match 'CalendarSharing(FreeBusyDetail|All)' } + if ($WildcardDomains) { + $FailedPolicies.Add([PSCustomObject]@{ + 'Policy Name' = $Policy.Name + 'Enabled' = $Policy.Enabled + 'Issue' = 'Allows detailed calendar sharing with all domains' + 'Domains' = ($WildcardDomains -join ', ') + }) + } + } + } + + if ($FailedPolicies.Count -eq 0) { + $Result = "✅ **Pass**: No sharing policies allow detailed calendar sharing with all domains." + $Status = 'Pass' + } else { + $Result = "❌ **Fail**: $($FailedPolicies.Count) sharing policy/policies allow detailed calendar sharing with all domains:`n`n" + $Result += ($FailedPolicies | ConvertTo-Html -Fragment | Out-String) + $Status = 'Fail' + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO62' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Category 'Exchange Online' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Add-CippTestResult -Status 'Failed' -ResultMarkdown "Test execution failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Category 'Exchange Online' -TestId 'CISAMSEXO62' -TenantFilter $Tenant + } +} diff --git a/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO71.md b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO71.md new file mode 100644 index 000000000000..1bda7cbc9a3d --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO71.md @@ -0,0 +1,20 @@ +External sender warnings SHALL be implemented. + +External sender warnings help users identify emails from outside the organization, reducing the risk of phishing and social engineering attacks. This visual indicator alerts users to exercise caution when interacting with external emails. + +**Remediation Action:** + +1. Navigate to Microsoft 365 Defender portal > Email & collaboration > Policies & rules > Threat policies +2. Under "Rules", select "External sender" +3. Enable external sender warnings +4. Or use PowerShell: +```powershell +Set-ExternalInOutlook -Enabled $true +``` + +**Links:** +- [CISA SCubaGear EXO Baseline - MS.EXO.7.1](https://github.com/cisagov/ScubaGear/blob/main/PowerShell/ScubaGear/baselines/exo.md#msexo71v1) +- [External sender warnings](https://learn.microsoft.com/microsoft-365/security/office-365-security/external-email-forwarding) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO71.ps1 b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO71.ps1 new file mode 100644 index 000000000000..fbd774e2871f --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO71.ps1 @@ -0,0 +1,103 @@ +function Invoke-CippTestCISAMSEXO71 { + <# + .SYNOPSIS + Tests MS.EXO.7.1 - External sender warnings SHALL be implemented + + .DESCRIPTION + Checks if external sender warnings are enabled in Exchange Online organization config + + .FUNCTIONALITY + Internal + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Tenant + ) + + try { + $OrgConfig = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoOrganizationConfig' + + if (-not $OrgConfig) { + Add-CippTestResult -Status 'Skipped' -ResultMarkdown 'ExoOrganizationConfig cache not found. Please refresh the cache for this tenant.' -Risk 'Medium' -Category 'Exchange Online' -TestId 'CISAMSEXO71' -TenantFilter $Tenant + return + } + + $OrgConfigObject = $OrgConfig | Select-Object -First 1 + + if ($OrgConfigObject.ExternalInOutlook -eq $true) { + $Result = '✅ **Pass**: External sender warnings are enabled in Outlook.' + $Status = 'Pass' + } else { + $Result = "❌ **Fail**: External sender warnings are not enabled in Outlook.`n`n" + $Result += "**Current Setting:**`n" + $Result += "- ExternalInOutlook: $($OrgConfigObject.ExternalInOutlook)" + $Status = 'Fail' + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO71' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Category 'Exchange Online' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Add-CippTestResult -Status 'Failed' -ResultMarkdown "Test execution failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Category 'Exchange Online' -TestId 'CISAMSEXO71' -TenantFilter $Tenant + } +} +function Invoke-CippTestCISAMSEXO71 { + <# + .SYNOPSIS + MS.EXO.7.1 - External sender warnings SHALL be implemented + + .DESCRIPTION + Tests if external sender warning is configured in Exchange transport rules + + .LINK + https://github.com/cisagov/ScubaGear + #> + param($Tenant) + + try { + $TransportRules = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoTransportRules' + + if (-not $TransportRules) { + Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO71' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'ExoTransportRules cache not found. Please ensure cache data is available.' -Risk 'Medium' -Name 'External sender warnings implemented' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Exchange Online' + return + } + + # Look for external sender warning rules + $ExternalSenderRules = $TransportRules | Where-Object { + ($_.FromScope -eq 'NotInOrganization') -and + ($_.PrependSubject -or $_.SetHeaderName -or $_.ApplyHtmlDisclaimerText) -and + ($_.State -eq 'Enabled') + } + + if ($ExternalSenderRules.Count -gt 0) { + $Status = 'Passed' + $Result = "✅ Well done. Your tenant has external sender warning rules configured.`n`n" + $Result += "**Active External Sender Warning Rules:**`n`n" + $Result += "| Rule Name | Priority | Action |`n" + $Result += "| --- | --- | --- |`n" + + foreach ($rule in $ExternalSenderRules | Sort-Object Priority) { + $action = if ($rule.PrependSubject) { 'Prepend Subject' } + elseif ($rule.SetHeaderName) { 'Set Header' } + elseif ($rule.ApplyHtmlDisclaimerText) { 'Add Disclaimer' } + else { 'Modified Message' } + $Result += "| $($rule.Name) | $($rule.Priority) | $action |`n" + } + } else { + $Status = 'Failed' + $Result = "❌ No external sender warning rules found.`n`n" + $Result += "**Recommendation:** Create a transport rule to warn users about external senders.`n`n" + $Result += "**Example configurations:**`n" + $Result += "- Add '[EXTERNAL]' prefix to subject line for emails from outside organization`n" + $Result += "- Add HTML disclaimer banner warning users the email is from external sender`n" + $Result += "- Add custom header for client-side filtering`n" + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO71' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'External sender warnings implemented' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Exchange Online' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run CISA test CISAMSEXO71: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO71' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test execution failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'External sender warnings implemented' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Exchange Online' + } +} diff --git a/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO95.md b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO95.md new file mode 100644 index 000000000000..1b8d550197e9 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO95.md @@ -0,0 +1,22 @@ +At a minimum, click-to-run files SHOULD be blocked (e.g., .exe, .cmd, and .vbe). + +Blocking executable file types prevents users from receiving and potentially executing malicious files through email. These file types are commonly used in malware attacks and social engineering campaigns. + +**Remediation Action:** + +1. Navigate to Microsoft 365 Defender portal > Email & collaboration > Policies & rules > Threat policies > Anti-malware +2. Select the malware filter policy to edit +3. Under "Protection settings": + - Enable "Enable the common attachments filter" + - Ensure the blocked file types include at minimum: cmd, exe, vbe +4. Or use PowerShell: +```powershell +Set-MalwareFilterPolicy -Identity "Default" -EnableFileFilter $true -FileTypes @("ace","ani","app","cab","docm","exe","jar","reg","scr","vbe","vbs","cmd","bat","com","cpl","dll","exe","hta","inf","ins","isp","js","jse","lib","lnk","mde","msc","msp","mst","pif","scr","sct","shb","sys","vb","vbe","vbs","vxd","wsc","wsf","wsh") +``` + +**Links:** +- [CISA SCubaGear EXO Baseline - MS.EXO.9.5](https://github.com/cisagov/ScubaGear/blob/main/PowerShell/ScubaGear/baselines/exo.md#msexo95v1) +- [Configure anti-malware policies](https://learn.microsoft.com/microsoft-365/security/office-365-security/anti-malware-protection-configure) + + +%TestResult% diff --git a/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO95.ps1 b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO95.ps1 new file mode 100644 index 000000000000..79cf19e43f8c --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO95.ps1 @@ -0,0 +1,68 @@ +function Invoke-CippTestCISAMSEXO95 { + <# + .SYNOPSIS + Tests MS.EXO.9.5 - At a minimum, click-to-run files SHOULD be blocked + + .DESCRIPTION + Checks if malware filter policies block click-to-run executables (.exe, .cmd, .vbe) + + .FUNCTIONALITY + Internal + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Tenant + ) + + try { + $MalwarePolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoMalwareFilterPolicy' + + if (-not $MalwarePolicies) { + Add-CippTestResult -Status 'Skipped' -ResultMarkdown 'ExoMalwareFilterPolicy cache not found. Please refresh the cache for this tenant.' -Risk 'High' -Category 'Exchange Online' -TestId 'CISAMSEXO95' -TenantFilter $Tenant + return + } + + $RequiredBlockedTypes = @('cmd', 'exe', 'vbe') + $FailedPolicies = [System.Collections.Generic.List[object]]::new() + + foreach ($Policy in $MalwarePolicies) { + if (-not $Policy.EnableFileFilter) { + # Policy doesn't have file filtering enabled at all + $FailedPolicies.Add([PSCustomObject]@{ + 'Policy Name' = $Policy.Name + 'File Filter Enabled' = $false + 'Issue' = 'File filtering not enabled' + }) + continue + } + + # Check if required types are blocked + $BlockedTypes = $Policy.FileTypes + $MissingTypes = $RequiredBlockedTypes | Where-Object { $_ -notin $BlockedTypes } + + if ($MissingTypes) { + $FailedPolicies.Add([PSCustomObject]@{ + 'Policy Name' = $Policy.Name + 'File Filter Enabled' = $true + 'Missing Blocked Types' = ($MissingTypes -join ', ') + }) + } + } + + if ($FailedPolicies.Count -eq 0) { + $Result = "✅ **Pass**: All malware filter policies block click-to-run files (.exe, .cmd, .vbe)." + $Status = 'Pass' + } else { + $Result = "❌ **Fail**: $($FailedPolicies.Count) malware filter policy/policies do not properly block click-to-run executables:`n`n" + $Result += ($FailedPolicies | ConvertTo-Html -Fragment | Out-String) + $Status = 'Fail' + } + + Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO95' -Status $Status -ResultMarkdown $Result -Risk 'High' -Category 'Exchange Online' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Add-CippTestResult -Status 'Failed' -ResultMarkdown "Test execution failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Category 'Exchange Online' -TestId 'CISAMSEXO95' -TenantFilter $Tenant + } +} diff --git a/Modules/CIPPCore/Public/Tests/CISA/report.json b/Modules/CIPPCore/Public/Tests/CISA/report.json new file mode 100644 index 000000000000..75ac2ab1bb69 --- /dev/null +++ b/Modules/CIPPCore/Public/Tests/CISA/report.json @@ -0,0 +1,33 @@ +{ + "name": "CISA SCubaGear Tests for Exchange Online", + "description": "Security configuration assessment tests based on CISA's Secure Cloud Business Applications (SCubaGear) project for Microsoft Exchange Online. These tests validate compliance with federal security baselines.", + "version": "1.0", + "source": "https://github.com/cisagov/ScubaGear", + "category": "CISA Security Baselines", + "IdentityTests": [ + "CISAMSEXO11", + "CISAMSEXO31", + "CISAMSEXO51", + "CISAMSEXO61", + "CISAMSEXO62", + "CISAMSEXO71", + "CISAMSEXO95", + "CISAMSEXO101", + "CISAMSEXO102", + "CISAMSEXO103", + "CISAMSEXO111", + "CISAMSEXO112", + "CISAMSEXO113", + "CISAMSEXO121", + "CISAMSEXO122", + "CISAMSEXO131", + "CISAMSEXO141", + "CISAMSEXO142", + "CISAMSEXO143", + "CISAMSEXO151", + "CISAMSEXO152", + "CISAMSEXO153", + "CISAMSEXO171", + "CISAMSEXO173" + ] +} From d870bd7ac790d69c65735e4b99f1789f6c500943 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Sun, 11 Jan 2026 21:30:58 +0100 Subject: [PATCH 121/503] Fix issue with conflict --- .../Push-CIPPDBCacheData.ps1 | 56 ++++++++----------- .../Identity/Invoke-CippTestCISAMSEXO113.ps1 | 8 +-- 2 files changed, 28 insertions(+), 36 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 index bc9ba2e24e0e..07b0fc9c5f7b 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 @@ -269,42 +269,34 @@ function Push-CIPPDBCacheData { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoQuarantinePolicy collection failed: $($_.Exception.Message)" -sev Error } - Write-Host 'Getting cache for ExoRemoteDomain' - try { Set-CIPPDBCacheExoRemoteDomain -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoRemoteDomain collection failed: $($_.Exception.Message)" -sev Error - } - - - Write-Host 'Getting cache for ExoSharingPolicy' - try { Set-CIPPDBCacheExoSharingPolicy -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoSharingPolicy collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for ExoAdminAuditLogConfig' - try { Set-CIPPDBCacheExoAdminAuditLogConfig -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoAdminAuditLogConfig collection failed: $($_.Exception.Message)" -sev Error - } + Write-Host 'Getting cache for ExoRemoteDomain' + try { Set-CIPPDBCacheExoRemoteDomain -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoRemoteDomain collection failed: $($_.Exception.Message)" -sev Error + } - Write-Host 'Getting cache for ExoPresetSecurityPolicy' - try { Set-CIPPDBCacheExoPresetSecurityPolicy -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoPresetSecurityPolicy collection failed: $($_.Exception.Message)" -sev Error - } + Write-Host 'Getting cache for ExoSharingPolicy' + try { Set-CIPPDBCacheExoSharingPolicy -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoSharingPolicy collection failed: $($_.Exception.Message)" -sev Error + } - Write-Host 'Getting cache for ExoTenantAllowBlockList' - try { Set-CIPPDBCacheExoTenantAllowBlockList -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoTenantAllowBlockList collection failed: $($_.Exception.Message)" -sev Error - } + Write-Host 'Getting cache for ExoAdminAuditLogConfig' + try { Set-CIPPDBCacheExoAdminAuditLogConfig -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoAdminAuditLogConfig collection failed: $($_.Exception.Message)" -sev Error + } - Write-Host 'Getting cache for License Overview' - try { Set-CIPPDBCacheLicenseOverview -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "License Overview collection failed: $($_.Exception.Message)" -sev Error - } + Write-Host 'Getting cache for ExoPresetSecurityPolicy' + try { Set-CIPPDBCacheExoPresetSecurityPolicy -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoPresetSecurityPolicy collection failed: $($_.Exception.Message)" -sev Error + } - Write-Host 'Getting cache for MFA State' - try { Set-CIPPDBCacheMFAState -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "MFA State collection failed: $($_.Exception.Message)" -sev Error - } - #endregion All Licenses + Write-Host 'Getting cache for ExoTenantAllowBlockList' + try { Set-CIPPDBCacheExoTenantAllowBlockList -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoTenantAllowBlockList collection failed: $($_.Exception.Message)" -sev Error + } + } else { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Skipping Exchange Online data collection - tenant does not have required license' -sev Info + } + #endregion Exchange Licensed #region Conditional Access Licensed - Azure AD Premium features if ($ConditionalAccessCapable) { diff --git a/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO113.ps1 b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO113.ps1 index 1879bf42a3b2..bbe33c8d04fd 100644 --- a/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO113.ps1 +++ b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO113.ps1 @@ -31,10 +31,10 @@ function Invoke-CippTestCISAMSEXO113 { if ($PoliciesWithIntelligence.Count -gt 0) { $ResultTable = $PoliciesWithIntelligence | ForEach-Object { [PSCustomObject]@{ - 'Policy' = $_.Identity - 'Mailbox Intelligence' = $_.EnableMailboxIntelligence + 'Policy' = $_.Identity + 'Mailbox Intelligence' = $_.EnableMailboxIntelligence 'Intelligence Protection' = $_.EnableMailboxIntelligenceProtection - 'State' = $_.State + 'State' = $_.State } } @@ -43,7 +43,7 @@ function Invoke-CippTestCISAMSEXO113 { $Status = 'Pass' } else { $Result = "❌ **Fail**: No policies found with mailbox intelligence enabled.`n`n" - $Result += "Enable mailbox intelligence in preset security policies for AI-powered impersonation protection." + $Result += 'Enable mailbox intelligence in preset security policies for AI-powered impersonation protection.' $Status = 'Fail' } From ffbf9a4dc37554053c36872690b7201e4d06d46d Mon Sep 17 00:00:00 2001 From: John Duprey Date: Sun, 11 Jan 2026 15:59:12 -0500 Subject: [PATCH 122/503] Optimize group and role member caching with bulk requests Refactored Set-CIPPDBCacheGroups and Set-CIPPDBCacheRoles to use Microsoft Graph bulk requests for fetching group and role members, improving performance and efficiency. Updated logic to attach member data to each group and role object before caching. --- .../CIPPCore/Public/Set-CIPPDBCacheGroups.ps1 | 37 +++++++++++++-- .../CIPPCore/Public/Set-CIPPDBCacheRoles.ps1 | 47 +++++++++++-------- 2 files changed, 60 insertions(+), 24 deletions(-) diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheGroups.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheGroups.ps1 index 84d1d971ed9a..2725887c2fa9 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheGroups.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheGroups.ps1 @@ -16,11 +16,40 @@ function Set-CIPPDBCacheGroups { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching groups' -sev Info $Groups = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/groups?$top=999&$select=id,displayName,groupTypes,mail,mailEnabled,securityEnabled,membershipRule,onPremisesSyncEnabled' -tenantid $TenantFilter - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Groups' -Data $Groups - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Groups' -Data $Groups -Count - $Groups = $null - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached groups successfully' -sev Info + # Build bulk request for group members + $MemberRequests = $Groups | ForEach-Object { + if ($_.id) { + [PSCustomObject]@{ + id = $_.id + method = 'GET' + url = "/groups/$($_.id)/members?`$select=id,displayName,userPrincipalName" + } + } + } + + if ($MemberRequests) { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Fetching group members' -sev Info + $MemberResults = New-GraphBulkRequest -Requests @($MemberRequests) -tenantid $TenantFilter + + # Add members to each group object + $GroupsWithMembers = foreach ($Group in $Groups) { + $Members = ($MemberResults | Where-Object { $_.id -eq $Group.id }).body.value + $Group | Add-Member -NotePropertyName 'members' -NotePropertyValue $Members -Force + $Group + } + + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Groups' -Data $GroupsWithMembers + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Groups' -Data $GroupsWithMembers -Count + $Groups = $null + $GroupsWithMembers = $null + } else { + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Groups' -Data $Groups + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Groups' -Data $Groups -Count + $Groups = $null + } + + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached groups with members successfully' -sev Info } catch { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter ` diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheRoles.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheRoles.ps1 index 1c76f2eac12e..4d926c1c722c 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheRoles.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheRoles.ps1 @@ -17,36 +17,43 @@ function Set-CIPPDBCacheRoles { $Roles = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/directoryRoles' -tenantid $TenantFilter - $RolesWithMembers = foreach ($Role in $Roles) { - try { - $Members = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/directoryRoles/$($Role.id)/members?&`$select=id,displayName,userPrincipalName" -tenantid $TenantFilter + # Build bulk request for role members + $MemberRequests = $Roles | ForEach-Object { + if ($_.id) { [PSCustomObject]@{ - id = $Role.id - displayName = $Role.displayName - description = $Role.description - roleTemplateId = $Role.roleTemplateId - members = $Members - memberCount = $Members.Count + id = $_.id + method = 'GET' + url = "/directoryRoles/$($_.id)/members?`$select=id,displayName,userPrincipalName" } - } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to get members for role $($Role.displayName): $($_.Exception.Message)" -sev Warning + } + } + + if ($MemberRequests) { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Fetching role members' -sev Info + $MemberResults = New-GraphBulkRequest -Requests @($MemberRequests) -tenantid $TenantFilter + + # Add members to each role object + $RolesWithMembers = foreach ($Role in $Roles) { + $Members = ($MemberResults | Where-Object { $_.id -eq $Role.id }).body.value [PSCustomObject]@{ id = $Role.id displayName = $Role.displayName description = $Role.description roleTemplateId = $Role.roleTemplateId - members = @() - memberCount = 0 + members = $Members + memberCount = ($Members | Measure-Object).Count } } - } - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Roles' -Data $RolesWithMembers - - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Roles' -Data $RolesWithMembers -Count - - $Roles = $null - $RolesWithMembers = $null + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Roles' -Data $RolesWithMembers + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Roles' -Data $RolesWithMembers -Count + $Roles = $null + $RolesWithMembers = $null + } else { + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Roles' -Data $Roles + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Roles' -Data $Roles -Count + $Roles = $null + } Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached directory roles successfully' -sev Info From 1c303894fb341b0616a35c6cae675104edd40f66 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Mon, 12 Jan 2026 11:51:09 +0100 Subject: [PATCH 123/503] update Identity --- .../CIPPCore/Public/Add-CippTestResult.ps1 | 4 +- .../Identity/Invoke-CippTestCISAMSEXO101.ps1 | 23 ++-- .../Identity/Invoke-CippTestCISAMSEXO102.ps1 | 16 ++- .../Identity/Invoke-CippTestCISAMSEXO103.ps1 | 23 ++-- .../Identity/Invoke-CippTestCISAMSEXO11.ps1 | 73 ++---------- .../Identity/Invoke-CippTestCISAMSEXO111.ps1 | 10 +- .../Identity/Invoke-CippTestCISAMSEXO112.ps1 | 25 ++-- .../Identity/Invoke-CippTestCISAMSEXO113.ps1 | 25 ++-- .../Identity/Invoke-CippTestCISAMSEXO121.ps1 | 28 ++--- .../Identity/Invoke-CippTestCISAMSEXO122.ps1 | 23 ++-- .../Identity/Invoke-CippTestCISAMSEXO131.ps1 | 55 +-------- .../Identity/Invoke-CippTestCISAMSEXO141.ps1 | 24 ++-- .../Identity/Invoke-CippTestCISAMSEXO142.ps1 | 24 ++-- .../Identity/Invoke-CippTestCISAMSEXO143.ps1 | 26 +++-- .../Identity/Invoke-CippTestCISAMSEXO151.ps1 | 23 ++-- .../Identity/Invoke-CippTestCISAMSEXO152.ps1 | 23 ++-- .../Identity/Invoke-CippTestCISAMSEXO153.ps1 | 23 ++-- .../Identity/Invoke-CippTestCISAMSEXO171.ps1 | 10 +- .../Identity/Invoke-CippTestCISAMSEXO173.ps1 | 10 +- .../Identity/Invoke-CippTestCISAMSEXO31.ps1 | 18 +-- .../Identity/Invoke-CippTestCISAMSEXO51.ps1 | 110 ++---------------- .../Identity/Invoke-CippTestCISAMSEXO61.ps1 | 57 ++------- .../Identity/Invoke-CippTestCISAMSEXO62.ps1 | 16 ++- .../Identity/Invoke-CippTestCISAMSEXO71.ps1 | 69 +---------- .../Identity/Invoke-CippTestCISAMSEXO95.ps1 | 36 +++--- 25 files changed, 240 insertions(+), 534 deletions(-) diff --git a/Modules/CIPPCore/Public/Add-CippTestResult.ps1 b/Modules/CIPPCore/Public/Add-CippTestResult.ps1 index 4d8bc95bc0f2..a4bee90dae78 100644 --- a/Modules/CIPPCore/Public/Add-CippTestResult.ps1 +++ b/Modules/CIPPCore/Public/Add-CippTestResult.ps1 @@ -48,7 +48,7 @@ function Add-CippTestResult { [string]$TestId, [Parameter(Mandatory = $false)] - [string]$testType = 'identity', + [string]$testType = 'Identity', [Parameter(Mandatory = $true)] [string]$Status, @@ -93,7 +93,7 @@ function Add-CippTestResult { } Add-CIPPAzDataTableEntity @Table -Entity $Entity -Force - Write-LogMessage -API 'CIPPTestResults' -tenant $TenantFilter -message "Added test result: $TestId - $Status" -sev Info + Write-LogMessage -API 'CIPPTestResults' -tenant $TenantFilter -message "Added test result: $TestId - $Status" -sev Debug } catch { Write-LogMessage -API 'CIPPTestResults' -tenant $TenantFilter -message "Failed to add test result: $($_.Exception.Message)" -sev Error throw diff --git a/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO101.ps1 b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO101.ps1 index fa1fc49c2e3b..cda775b98b25 100644 --- a/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO101.ps1 +++ b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO101.ps1 @@ -19,7 +19,7 @@ function Invoke-CippTestCISAMSEXO101 { $MalwarePolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoMalwareFilterPolicy' if (-not $MalwarePolicies) { - Add-CippTestResult -Status 'Skipped' -ResultMarkdown 'ExoMalwareFilterPolicy cache not found. Please refresh the cache for this tenant.' -Risk 'High' -Category 'Exchange Online' -TestId 'CISAMSEXO101' -TenantFilter $Tenant + Add-CippTestResult -Status 'Skipped' -ResultMarkdown 'ExoMalwareFilterPolicy cache not found. Please refresh the cache for this tenant.' -Risk 'High' -Name 'Emails SHALL be filtered by attachment file types' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Email Protection' -TestId 'CISAMSEXO101' -TenantFilter $Tenant return } @@ -27,24 +27,21 @@ function Invoke-CippTestCISAMSEXO101 { if ($FailedPolicies.Count -eq 0) { $Result = "✅ **Pass**: All $($MalwarePolicies.Count) malware filter policy/policies have file filtering enabled." - $Status = 'Pass' + $Status = 'Passed' } else { - $ResultTable = foreach ($Policy in $FailedPolicies) { - [PSCustomObject]@{ - 'Policy Name' = $Policy.Name - 'File Filter Enabled' = $Policy.EnableFileFilter - } - } - $Result = "❌ **Fail**: $($FailedPolicies.Count) of $($MalwarePolicies.Count) malware filter policy/policies do not have file filtering enabled:`n`n" - $Result += ($ResultTable | ConvertTo-Html -Fragment | Out-String) - $Status = 'Fail' + $Result += "| Policy Name | File Filter Enabled |`n" + $Result += "| :---------- | :------------------ |`n" + foreach ($Policy in $FailedPolicies) { + $Result += "| $($Policy.Name) | $($Policy.EnableFileFilter) |`n" + } + $Status = 'Failed' } - Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO101' -Status $Status -ResultMarkdown $Result -Risk 'High' -Category 'Exchange Online' + Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO101' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'Emails SHALL be filtered by attachment file types' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Email Protection' } catch { $ErrorMessage = Get-CippException -Exception $_ - Add-CippTestResult -Status 'Failed' -ResultMarkdown "Test execution failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Category 'Exchange Online' -TestId 'CISAMSEXO101' -TenantFilter $Tenant + Add-CippTestResult -Status 'Failed' -ResultMarkdown "Test execution failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Emails SHALL be filtered by attachment file types' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Email Protection' -TestId 'CISAMSEXO101' -TenantFilter $Tenant } } diff --git a/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO102.ps1 b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO102.ps1 index 2d09dde64278..5ced966d198e 100644 --- a/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO102.ps1 +++ b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO102.ps1 @@ -19,7 +19,7 @@ function Invoke-CippTestCISAMSEXO102 { $MalwarePolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoMalwareFilterPolicy' if (-not $MalwarePolicies) { - Add-CippTestResult -Status 'Skipped' -ResultMarkdown 'ExoMalwareFilterPolicy cache not found. Please refresh the cache for this tenant.' -Risk 'High' -Category 'Exchange Online' -TestId 'CISAMSEXO102' -TenantFilter $Tenant + Add-CippTestResult -Status 'Skipped' -ResultMarkdown 'ExoMalwareFilterPolicy cache not found. Please refresh the cache for this tenant.' -Risk 'High' -Name 'Emails identified as malware SHALL be quarantined or dropped' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Email Protection' -TestId 'CISAMSEXO102' -TenantFilter $Tenant return } @@ -38,17 +38,21 @@ function Invoke-CippTestCISAMSEXO102 { if ($FailedPolicies.Count -eq 0) { $Result = "✅ **Pass**: All $($MalwarePolicies.Count) malware filter policy/policies quarantine or delete emails with malware." - $Status = 'Pass' + $Status = 'Passed' } else { $Result = "❌ **Fail**: $($FailedPolicies.Count) of $($MalwarePolicies.Count) malware filter policy/policies do not quarantine or delete malware:`n`n" - $Result += ($FailedPolicies | ConvertTo-Html -Fragment | Out-String) - $Status = 'Fail' + $Result += "| Policy Name | Current Action | Expected |`n" + $Result += "| :---------- | :------------- | :------- |`n" + foreach ($Policy in $FailedPolicies) { + $Result += "| $($Policy.'Policy Name') | $($Policy.'Current Action') | $($Policy.Expected) |`n" + } + $Status = 'Failed' } - Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO102' -Status $Status -ResultMarkdown $Result -Risk 'High' -Category 'Exchange Online' + Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO102' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'Emails identified as malware SHALL be quarantined or dropped' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Email Protection' } catch { $ErrorMessage = Get-CippException -Exception $_ - Add-CippTestResult -Status 'Failed' -ResultMarkdown "Test execution failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Category 'Exchange Online' -TestId 'CISAMSEXO102' -TenantFilter $Tenant + Add-CippTestResult -Status 'Failed' -ResultMarkdown "Test execution failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Emails identified as malware SHALL be quarantined or dropped' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Email Protection' -TestId 'CISAMSEXO102' -TenantFilter $Tenant } } diff --git a/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO103.ps1 b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO103.ps1 index f857c1aa419e..41f1e92f6385 100644 --- a/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO103.ps1 +++ b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO103.ps1 @@ -19,7 +19,7 @@ function Invoke-CippTestCISAMSEXO103 { $MalwarePolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoMalwareFilterPolicy' if (-not $MalwarePolicies) { - Add-CippTestResult -Status 'Skipped' -ResultMarkdown 'ExoMalwareFilterPolicy cache not found. Please refresh the cache for this tenant.' -Risk 'High' -Category 'Exchange Online' -TestId 'CISAMSEXO103' -TenantFilter $Tenant + Add-CippTestResult -Status 'Skipped' -ResultMarkdown 'ExoMalwareFilterPolicy cache not found. Please refresh the cache for this tenant.' -Risk 'High' -Name 'Email scanning SHALL be capable of reviewing emails after delivery' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Email Protection' -TestId 'CISAMSEXO103' -TenantFilter $Tenant return } @@ -27,24 +27,21 @@ function Invoke-CippTestCISAMSEXO103 { if ($FailedPolicies.Count -eq 0) { $Result = "✅ **Pass**: All $($MalwarePolicies.Count) malware filter policy/policies have ZAP (Zero-hour Auto Purge) enabled." - $Status = 'Pass' + $Status = 'Passed' } else { - $ResultTable = foreach ($Policy in $FailedPolicies) { - [PSCustomObject]@{ - 'Policy Name' = $Policy.Name - 'ZAP Enabled' = $Policy.ZapEnabled - } - } - $Result = "❌ **Fail**: $($FailedPolicies.Count) of $($MalwarePolicies.Count) malware filter policy/policies do not have ZAP enabled:`n`n" - $Result += ($ResultTable | ConvertTo-Html -Fragment | Out-String) - $Status = 'Fail' + $Result += "| Policy Name | ZAP Enabled |`n" + $Result += "| :---------- | :---------- |`n" + foreach ($Policy in $FailedPolicies) { + $Result += "| $($Policy.Name) | $($Policy.ZapEnabled) |`n" + } + $Status = 'Failed' } - Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO103' -Status $Status -ResultMarkdown $Result -Risk 'High' -Category 'Exchange Online' + Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO103' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'Email scanning SHALL be capable of reviewing emails after delivery' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Email Protection' } catch { $ErrorMessage = Get-CippException -Exception $_ - Add-CippTestResult -Status 'Failed' -ResultMarkdown "Test execution failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Category 'Exchange Online' -TestId 'CISAMSEXO103' -TenantFilter $Tenant + Add-CippTestResult -Status 'Failed' -ResultMarkdown "Test execution failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Email scanning SHALL be capable of reviewing emails after delivery' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Email Protection' -TestId 'CISAMSEXO103' -TenantFilter $Tenant } } diff --git a/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO11.ps1 b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO11.ps1 index 5ad9bdf843f1..96651f05b743 100644 --- a/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO11.ps1 +++ b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO11.ps1 @@ -19,7 +19,7 @@ function Invoke-CippTestCISAMSEXO11 { $RemoteDomains = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoRemoteDomain' if (-not $RemoteDomains) { - Add-CippTestResult -Status 'Skipped' -ResultMarkdown 'ExoRemoteDomain cache not found. Please refresh the cache for this tenant.' -Risk 'High' -Category 'Exchange Online' -TestId 'CISAMSEXO11' -TenantFilter $Tenant + Add-CippTestResult -Status 'Skipped' -ResultMarkdown 'ExoRemoteDomain cache not found. Please refresh the cache for this tenant.' -Risk 'High' -Name 'Automatic forwarding to external domains SHALL be disabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Email Protection' -TestId 'CISAMSEXO11' -TestType 'Identity' -TenantFilter $Tenant return } @@ -27,74 +27,21 @@ function Invoke-CippTestCISAMSEXO11 { if (($ForwardingEnabledDomains | Measure-Object).Count -eq 0) { $Result = '✅ **Pass**: Automatic forwarding to external domains is disabled for all remote domains.' - $Status = 'Pass' + $Status = 'Passed' } else { - $ResultTable = foreach ($Domain in $ForwardingEnabledDomains) { - [PSCustomObject]@{ - 'Domain Name' = $Domain.DomainName - 'Auto Forward' = $Domain.AutoForwardEnabled - } - } - $Result = "❌ **Fail**: $($ForwardingEnabledDomains.Count) domain(s) have automatic forwarding enabled:`n`n" - $Result += ($ResultTable | ConvertTo-Html -Fragment | Out-String) - $Status = 'Fail' - } - - Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO11' -Status $Status -ResultMarkdown $Result -Risk 'High' -Category 'Exchange Online' - - } catch { - $ErrorMessage = Get-CippException -Exception $_ - Add-CippTestResult -Status 'Failed' -ResultMarkdown "Test execution failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Category 'Exchange Online' -TestId 'CISAMSEXO11' -TenantFilter $Tenant - } -} -function Invoke-CippTestCISAMSEXO11 { - <# - .SYNOPSIS - MS.EXO.1.1 - Automatic forwarding to external domains SHALL be disabled - - .DESCRIPTION - Tests if automatic forwarding to external domains is disabled in Exchange Online - - .LINK - https://github.com/cisagov/ScubaGear - #> - param($Tenant) - - try { - $RemoteDomains = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoRemoteDomain' - - if (-not $RemoteDomains) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO11' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'ExoRemoteDomain cache not found. Please ensure cache data is available.' -Risk 'High' -Name 'Auto-forwarding to external domains is disabled' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Exchange Online' - return - } - - $ForwardingEnabledDomains = $RemoteDomains | Where-Object { $_.AutoForwardEnabled -eq $true } - - if ($ForwardingEnabledDomains.Count -eq 0) { - $Status = 'Passed' - $Result = "✅ Well done. Your tenant has automatic forwarding disabled for all remote domains.`n`n" - $Result += "| Domain Name | Auto Forward Enabled |`n" - $Result += "| --- | --- |`n" - foreach ($domain in $RemoteDomains) { - $Result += "| $($domain.DomainName) | ❌ Disabled |`n" + $Result += "| Domain Name | Auto Forward |`n" + $Result += "| :---------- | :----------- |`n" + foreach ($Domain in $ForwardingEnabledDomains) { + $Result += "| $($Domain.DomainName) | $($Domain.AutoForwardEnabled) |`n" } - } else { $Status = 'Failed' - $Result = "❌ Your tenant has automatic forwarding enabled for some remote domains.`n`n" - $Result += "| Domain Name | Auto Forward Enabled | Result |`n" - $Result += "| --- | --- | --- |`n" - foreach ($domain in $RemoteDomains) { - $enabled = if ($domain.AutoForwardEnabled) { '✅ Enabled' } else { '❌ Disabled' } - $testResult = if ($domain.AutoForwardEnabled) { '❌ Fail' } else { '✅ Pass' } - $Result += "| $($domain.DomainName) | $enabled | $testResult |`n" - } } - - Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO11' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'Auto-forwarding to external domains is disabled' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Exchange Online' + + Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO11' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'Automatic forwarding to external domains SHALL be disabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Email Protection' + } catch { $ErrorMessage = Get-CippException -Exception $_ - Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run CISA test CISAMSEXO11: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO11' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test execution failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Auto-forwarding to external domains is disabled' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Exchange Online' + Add-CippTestResult -Status 'Failed' -ResultMarkdown "Test execution failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Automatic forwarding to external domains SHALL be disabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Email Protection' -TestId 'CISAMSEXO11' -TestType 'Identity' -TenantFilter $Tenant } } diff --git a/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO111.ps1 b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO111.ps1 index d8b89ea1fc03..4bfd427b47d4 100644 --- a/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO111.ps1 +++ b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO111.ps1 @@ -19,7 +19,7 @@ function Invoke-CippTestCISAMSEXO111 { $PresetPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoPresetSecurityPolicy' if (-not $PresetPolicies) { - Add-CippTestResult -Status 'Skipped' -ResultMarkdown 'ExoPresetSecurityPolicy cache not found. Please refresh the cache for this tenant.' -Risk 'High' -Category 'Exchange Online' -TestId 'CISAMSEXO111' -TenantFilter $Tenant + Add-CippTestResult -Status 'Skipped' -ResultMarkdown 'ExoPresetSecurityPolicy cache not found. Please refresh the cache for this tenant.' -Risk 'High' -Name 'Impersonation protection checks SHOULD be used' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Email Protection' -TestId 'CISAMSEXO111' -TenantFilter $Tenant return } @@ -36,17 +36,17 @@ function Invoke-CippTestCISAMSEXO111 { if ($EnabledPolicies.Count -gt 0) { $Result = "✅ **Pass**: Preset security policies with impersonation protection are enabled:`n`n" $Result += ($EnabledPolicies | ForEach-Object { "- $_" }) -join "`n" - $Status = 'Pass' + $Status = 'Passed' } else { $Result = "❌ **Fail**: No preset security policies with impersonation protection enabled.`n`n" $Result += "Enable Standard or Strict preset security policies to provide impersonation protection." - $Status = 'Fail' + $Status = 'Failed' } - Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO111' -Status $Status -ResultMarkdown $Result -Risk 'High' -Category 'Exchange Online' + Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO111' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'Impersonation protection checks SHOULD be used' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Email Protection' } catch { $ErrorMessage = Get-CippException -Exception $_ - Add-CippTestResult -Status 'Failed' -ResultMarkdown "Test execution failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Category 'Exchange Online' -TestId 'CISAMSEXO111' -TenantFilter $Tenant + Add-CippTestResult -Status 'Failed' -ResultMarkdown "Test execution failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Impersonation protection checks SHOULD be used' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Email Protection' -TestId 'CISAMSEXO111' -TenantFilter $Tenant } } diff --git a/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO112.ps1 b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO112.ps1 index 5c720b3cf64d..94d3c617cad5 100644 --- a/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO112.ps1 +++ b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO112.ps1 @@ -19,7 +19,7 @@ function Invoke-CippTestCISAMSEXO112 { $PresetPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoPresetSecurityPolicy' if (-not $PresetPolicies) { - Add-CippTestResult -Status 'Skipped' -ResultMarkdown 'ExoPresetSecurityPolicy cache not found. Please refresh the cache for this tenant.' -Risk 'Medium' -Category 'Exchange Online' -TestId 'CISAMSEXO112' -TenantFilter $Tenant + Add-CippTestResult -Status 'Skipped' -ResultMarkdown 'ExoPresetSecurityPolicy cache not found. Please refresh the cache for this tenant.' -Risk 'High' -Name 'User warnings comparable to EOP safety tips SHOULD be displayed' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Email Protection' -TestId 'CISAMSEXO112' -TenantFilter $Tenant return } @@ -30,28 +30,23 @@ function Invoke-CippTestCISAMSEXO112 { } if ($PoliciesWithTips.Count -gt 0) { - $ResultTable = $PoliciesWithTips | ForEach-Object { - [PSCustomObject]@{ - 'Policy' = $_.Identity - 'Similar Users Tips' = $_.EnableSimilarUsersSafetyTips - 'Similar Domains Tips' = $_.EnableSimilarDomainsSafetyTips - 'Unusual Characters Tips' = $_.EnableUnusualCharactersSafetyTips - } - } - $Result = "✅ **Pass**: $($PoliciesWithTips.Count) policy/policies have impersonation safety tips enabled:`n`n" - $Result += ($ResultTable | ConvertTo-Html -Fragment | Out-String) - $Status = 'Pass' + $Result += "| Policy | Similar Users Tips | Similar Domains Tips | Unusual Characters Tips |`n" + $Result += "| :----- | :----------------- | :------------------- | :---------------------- |`n" + foreach ($Policy in $PoliciesWithTips) { + $Result += "| $($Policy.Identity) | $($Policy.EnableSimilarUsersSafetyTips) | $($Policy.EnableSimilarDomainsSafetyTips) | $($Policy.EnableUnusualCharactersSafetyTips) |`n" + } + $Status = 'Passed' } else { $Result = "❌ **Fail**: No policies found with impersonation safety tips enabled.`n`n" $Result += "Enable safety tips in preset security policies to warn users about potential impersonation." - $Status = 'Fail' + $Status = 'Failed' } - Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO112' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Category 'Exchange Online' + Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO112' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'User warnings comparable to EOP safety tips SHOULD be displayed' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Email Protection' } catch { $ErrorMessage = Get-CippException -Exception $_ - Add-CippTestResult -Status 'Failed' -ResultMarkdown "Test execution failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Category 'Exchange Online' -TestId 'CISAMSEXO112' -TenantFilter $Tenant + Add-CippTestResult -Status 'Failed' -ResultMarkdown "Test execution failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'User warnings comparable to EOP safety tips SHOULD be displayed' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Email Protection' -TestId 'CISAMSEXO112' -TenantFilter $Tenant } } diff --git a/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO113.ps1 b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO113.ps1 index bbe33c8d04fd..97541e383c37 100644 --- a/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO113.ps1 +++ b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO113.ps1 @@ -19,7 +19,7 @@ function Invoke-CippTestCISAMSEXO113 { $PresetPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoPresetSecurityPolicy' if (-not $PresetPolicies) { - Add-CippTestResult -Status 'Skipped' -ResultMarkdown 'ExoPresetSecurityPolicy cache not found. Please refresh the cache for this tenant.' -Risk 'High' -Category 'Exchange Online' -TestId 'CISAMSEXO113' -TenantFilter $Tenant + Add-CippTestResult -Status 'Skipped' -ResultMarkdown 'ExoPresetSecurityPolicy cache not found. Please refresh the cache for this tenant.' -Risk 'High' -Name 'Mailbox intelligence SHALL be enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Email Protection' -TestId 'CISAMSEXO113' -TenantFilter $Tenant return } @@ -29,28 +29,23 @@ function Invoke-CippTestCISAMSEXO113 { } if ($PoliciesWithIntelligence.Count -gt 0) { - $ResultTable = $PoliciesWithIntelligence | ForEach-Object { - [PSCustomObject]@{ - 'Policy' = $_.Identity - 'Mailbox Intelligence' = $_.EnableMailboxIntelligence - 'Intelligence Protection' = $_.EnableMailboxIntelligenceProtection - 'State' = $_.State - } - } - $Result = "✅ **Pass**: $($PoliciesWithIntelligence.Count) policy/policies have mailbox intelligence enabled:`n`n" - $Result += ($ResultTable | ConvertTo-Html -Fragment | Out-String) - $Status = 'Pass' + $Result += "| Policy | Mailbox Intelligence | Intelligence Protection | State |`n" + $Result += "| :----- | :------------------- | :---------------------- | :---- |`n" + foreach ($Policy in $PoliciesWithIntelligence) { + $Result += "| $($Policy.Identity) | $($Policy.EnableMailboxIntelligence) | $($Policy.EnableMailboxIntelligenceProtection) | $($Policy.State) |`n" + } + $Status = 'Passed' } else { $Result = "❌ **Fail**: No policies found with mailbox intelligence enabled.`n`n" $Result += 'Enable mailbox intelligence in preset security policies for AI-powered impersonation protection.' - $Status = 'Fail' + $Status = 'Failed' } - Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO113' -Status $Status -ResultMarkdown $Result -Risk 'High' -Category 'Exchange Online' + Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO113' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'Mailbox intelligence SHALL be enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Email Protection' } catch { $ErrorMessage = Get-CippException -Exception $_ - Add-CippTestResult -Status 'Failed' -ResultMarkdown "Test execution failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Category 'Exchange Online' -TestId 'CISAMSEXO113' -TenantFilter $Tenant + Add-CippTestResult -Status 'Failed' -ResultMarkdown "Test execution failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Mailbox intelligence SHALL be enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Email Protection' -TestId 'CISAMSEXO113' -TenantFilter $Tenant } } diff --git a/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO121.ps1 b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO121.ps1 index 0c8b3bd978d3..f9bb69153fa4 100644 --- a/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO121.ps1 +++ b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO121.ps1 @@ -19,37 +19,33 @@ function Invoke-CippTestCISAMSEXO121 { $AllowBlockList = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoTenantAllowBlockList' if ($null -eq $AllowBlockList) { - Add-CippTestResult -Status 'Skipped' -ResultMarkdown 'ExoTenantAllowBlockList cache not found. Please refresh the cache for this tenant.' -Risk 'Medium' -Category 'Exchange Online' -TestId 'CISAMSEXO121' -TenantFilter $Tenant + Add-CippTestResult -Status 'Skipped' -ResultMarkdown 'ExoTenantAllowBlockList cache not found. Please refresh the cache for this tenant.' -Risk 'Medium' -Name 'Allowed sender lists SHOULD NOT be used' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Email Protection' -TestId 'CISAMSEXO121' -TenantFilter $Tenant return } $AllowedSenders = $AllowBlockList | Where-Object { $_.Action -eq 'Allow' -and $_.ListType -eq 'Sender' } if ($AllowedSenders.Count -eq 0) { - $Result = "✅ **Pass**: No allowed senders configured in tenant allow/block list." - $Status = 'Pass' + $Result = '✅ **Pass**: No allowed senders configured in tenant allow/block list.' + $Status = 'Passed' } else { - $ResultTable = $AllowedSenders | Select-Object -First 10 | ForEach-Object { - [PSCustomObject]@{ - 'Value' = $_.Value - 'Action' = $_.Action - 'List Type' = $_.ListType - } - } - $Result = "❌ **Fail**: $($AllowedSenders.Count) allowed sender(s) configured in tenant allow/block list" if ($AllowedSenders.Count -gt 10) { - $Result += " (showing first 10)" + $Result += ' (showing first 10)' } $Result += ":`n`n" - $Result += ($ResultTable | ConvertTo-Html -Fragment | Out-String) - $Status = 'Fail' + $Result += "| Value | Action | List Type |`n" + $Result += "| :---- | :----- | :-------- |`n" + foreach ($Sender in ($AllowedSenders | Select-Object -First 10)) { + $Result += "| $($Sender.Value) | $($Sender.Action) | $($Sender.ListType) |`n" + } + $Status = 'Failed' } - Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO121' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Category 'Exchange Online' + Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO121' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'Allowed sender lists SHOULD NOT be used' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Email Protection' } catch { $ErrorMessage = Get-CippException -Exception $_ - Add-CippTestResult -Status 'Failed' -ResultMarkdown "Test execution failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Category 'Exchange Online' -TestId 'CISAMSEXO121' -TenantFilter $Tenant + Add-CippTestResult -Status 'Failed' -ResultMarkdown "Test execution failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'Allowed sender lists SHOULD NOT be used' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Email Protection' -TestId 'CISAMSEXO121' -TenantFilter $Tenant } } diff --git a/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO122.ps1 b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO122.ps1 index 6a55b8c6f401..bd50e4688c76 100644 --- a/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO122.ps1 +++ b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO122.ps1 @@ -19,7 +19,7 @@ function Invoke-CippTestCISAMSEXO122 { $SpamPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoHostedContentFilterPolicy' if (-not $SpamPolicies) { - Add-CippTestResult -Status 'Skipped' -ResultMarkdown 'ExoHostedContentFilterPolicy cache not found. Please refresh the cache for this tenant.' -Risk 'Medium' -Category 'Exchange Online' -TestId 'CISAMSEXO122' -TenantFilter $Tenant + Add-CippTestResult -Status 'Skipped' -ResultMarkdown 'ExoHostedContentFilterPolicy cache not found. Please refresh the cache for this tenant.' -Risk 'Medium' -Name 'Safe lists SHOULD NOT be enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Email Protection' -TestId 'CISAMSEXO122' -TenantFilter $Tenant return } @@ -27,24 +27,21 @@ function Invoke-CippTestCISAMSEXO122 { if ($FailedPolicies.Count -eq 0) { $Result = "✅ **Pass**: All $($SpamPolicies.Count) anti-spam policy/policies have safe lists disabled." - $Status = 'Pass' + $Status = 'Passed' } else { - $ResultTable = foreach ($Policy in $FailedPolicies) { - [PSCustomObject]@{ - 'Policy Name' = $Policy.Name - 'Safe List Enabled' = $Policy.EnableSafeList - } - } - $Result = "❌ **Fail**: $($FailedPolicies.Count) of $($SpamPolicies.Count) anti-spam policy/policies have safe lists enabled:`n`n" - $Result += ($ResultTable | ConvertTo-Html -Fragment | Out-String) - $Status = 'Fail' + $Result += "| Policy Name | Safe List Enabled |`n" + $Result += "| :---------- | :---------------- |`n" + foreach ($Policy in $FailedPolicies) { + $Result += "| $($Policy.Name) | $($Policy.EnableSafeList) |`n" + } + $Status = 'Failed' } - Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO122' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Category 'Exchange Online' + Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO122' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'Safe lists SHOULD NOT be enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Email Protection' } catch { $ErrorMessage = Get-CippException -Exception $_ - Add-CippTestResult -Status 'Failed' -ResultMarkdown "Test execution failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Category 'Exchange Online' -TestId 'CISAMSEXO122' -TenantFilter $Tenant + Add-CippTestResult -Status 'Failed' -ResultMarkdown "Test execution failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'Safe lists SHOULD NOT be enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Email Protection' -TestId 'CISAMSEXO122' -TenantFilter $Tenant } } diff --git a/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO131.ps1 b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO131.ps1 index ae763c155326..e3513310346d 100644 --- a/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO131.ps1 +++ b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO131.ps1 @@ -19,7 +19,7 @@ function Invoke-CippTestCISAMSEXO131 { $OrgConfig = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoOrganizationConfig' if (-not $OrgConfig) { - Add-CippTestResult -Status 'Skipped' -ResultMarkdown 'ExoOrganizationConfig cache not found. Please refresh the cache for this tenant.' -Risk 'High' -Category 'Exchange Online' -TestId 'CISAMSEXO131' -TenantFilter $Tenant + Add-CippTestResult -Status 'Skipped' -ResultMarkdown 'ExoOrganizationConfig cache not found. Please refresh the cache for this tenant.' -Risk 'High' -Name 'Mailbox auditing SHALL be enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Audit & Compliance' -TestId 'CISAMSEXO131' -TenantFilter $Tenant return } @@ -27,63 +27,18 @@ function Invoke-CippTestCISAMSEXO131 { if ($OrgConfigObject.AuditDisabled -eq $false) { $Result = '✅ **Pass**: Mailbox auditing is enabled for the organization.' - $Status = 'Pass' + $Status = 'Passed' } else { $Result = "❌ **Fail**: Mailbox auditing is disabled for the organization.`n`n" $Result += "**Current Setting:**`n" $Result += "- AuditDisabled: $($OrgConfigObject.AuditDisabled)" - $Status = 'Fail' + $Status = 'Failed' } - Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO131' -Status $Status -ResultMarkdown $Result -Risk 'High' -Category 'Exchange Online' + Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO131' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'Mailbox auditing SHALL be enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Audit & Compliance' } catch { $ErrorMessage = Get-CippException -Exception $_ - Add-CippTestResult -Status 'Failed' -ResultMarkdown "Test execution failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Category 'Exchange Online' -TestId 'CISAMSEXO131' -TenantFilter $Tenant - } -} -function Invoke-CippTestCISAMSEXO131 { - <# - .SYNOPSIS - MS.EXO.13.1 - Mailbox auditing SHALL be enabled - - .DESCRIPTION - Tests if mailbox auditing is enabled organization-wide - - .LINK - https://github.com/cisagov/ScubaGear - #> - param($Tenant) - - try { - $OrgConfig = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoOrganizationConfig' - - if (-not $OrgConfig) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO131' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'ExoOrganizationConfig cache not found. Please ensure cache data is available.' -Risk 'High' -Name 'Mailbox auditing enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Exchange Online' - return - } - - $AuditDisabled = $OrgConfig.AuditDisabled - - # AuditDisabled should be False (meaning auditing is enabled) - if ($AuditDisabled -eq $false) { - $Status = 'Passed' - $Result = "✅ Well done. Mailbox auditing is enabled organization-wide.`n`n" - $Result += "**Current Configuration:**`n" - $Result += "- Mailbox Auditing: **Enabled** ✅`n`n" - $Result += 'Mailbox auditing helps track and log mailbox access and modifications for security and compliance purposes.' - } else { - $Status = 'Failed' - $Result = "❌ Mailbox auditing is disabled organization-wide.`n`n" - $Result += "**Current Configuration:**`n" - $Result += "- Mailbox Auditing: **Disabled** ❌`n`n" - $Result += '**Recommendation:** Enable mailbox auditing to track mailbox access and modifications. This is critical for security investigations and compliance requirements.' - } - - Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO131' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'Mailbox auditing enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Exchange Online' - } catch { - $ErrorMessage = Get-CippException -Exception $_ - Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run CISA test CISAMSEXO131: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO131' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test execution failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Mailbox auditing enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Exchange Online' + Add-CippTestResult -Status 'Failed' -ResultMarkdown "Test execution failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Mailbox auditing SHALL be enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Audit & Compliance' -TestId 'CISAMSEXO131' -TenantFilter $Tenant } } diff --git a/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO141.ps1 b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO141.ps1 index 051518cad861..9e332811ea10 100644 --- a/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO141.ps1 +++ b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO141.ps1 @@ -19,7 +19,7 @@ function Invoke-CippTestCISAMSEXO141 { $SpamPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoHostedContentFilterPolicy' if (-not $SpamPolicies) { - Add-CippTestResult -Status 'Skipped' -ResultMarkdown 'ExoHostedContentFilterPolicy cache not found. Please refresh the cache for this tenant.' -Risk 'High' -Category 'Exchange Online' -TestId 'CISAMSEXO141' -TenantFilter $Tenant + Add-CippTestResult -Status 'Skipped' -ResultMarkdown 'ExoHostedContentFilterPolicy cache not found. Please refresh the cache for this tenant.' -Risk 'High' -Name 'High confidence spam SHALL be quarantined' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Email Protection' -TestId 'CISAMSEXO141' -TenantFilter $Tenant return } @@ -27,25 +27,21 @@ function Invoke-CippTestCISAMSEXO141 { if ($FailedPolicies.Count -eq 0) { $Result = "✅ **Pass**: All $($SpamPolicies.Count) anti-spam policy/policies quarantine high confidence spam." - $Status = 'Pass' + $Status = 'Passed' } else { - $ResultTable = foreach ($Policy in $FailedPolicies) { - [PSCustomObject]@{ - 'Policy Name' = $Policy.Name - 'Current Action' = $Policy.HighConfidenceSpamAction - 'Expected' = 'Quarantine' - } - } - $Result = "❌ **Fail**: $($FailedPolicies.Count) of $($SpamPolicies.Count) anti-spam policy/policies do not quarantine high confidence spam:`n`n" - $Result += ($ResultTable | ConvertTo-Html -Fragment | Out-String) - $Status = 'Fail' + $Result += "| Policy Name | Current Action | Expected |`n" + $Result += "| :---------- | :------------- | :------- |`n" + foreach ($Policy in $FailedPolicies) { + $Result += "| $($Policy.'Policy Name') | $($Policy.'Current Action') | $($Policy.Expected) |`n" + } + $Status = 'Failed' } - Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO141' -Status $Status -ResultMarkdown $Result -Risk 'High' -Category 'Exchange Online' + Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO141' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'High confidence spam SHALL be quarantined' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Email Protection' } catch { $ErrorMessage = Get-CippException -Exception $_ - Add-CippTestResult -Status 'Failed' -ResultMarkdown "Test execution failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Category 'Exchange Online' -TestId 'CISAMSEXO141' -TenantFilter $Tenant + Add-CippTestResult -Status 'Failed' -ResultMarkdown "Test execution failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'High confidence spam SHALL be quarantined' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Email Protection' -TestId 'CISAMSEXO141' -TenantFilter $Tenant } } diff --git a/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO142.ps1 b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO142.ps1 index f31995c16325..d9d2f8a2ca4e 100644 --- a/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO142.ps1 +++ b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO142.ps1 @@ -19,7 +19,7 @@ function Invoke-CippTestCISAMSEXO142 { $SpamPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoHostedContentFilterPolicy' if (-not $SpamPolicies) { - Add-CippTestResult -Status 'Skipped' -ResultMarkdown 'ExoHostedContentFilterPolicy cache not found. Please refresh the cache for this tenant.' -Risk 'Medium' -Category 'Exchange Online' -TestId 'CISAMSEXO142' -TenantFilter $Tenant + Add-CippTestResult -Status 'Skipped' -ResultMarkdown 'ExoHostedContentFilterPolicy cache not found. Please refresh the cache for this tenant.' -Risk 'Medium' -Name 'Spam SHALL be moved to junk email or quarantine' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Email Protection' -TestId 'CISAMSEXO142' -TenantFilter $Tenant return } @@ -29,26 +29,30 @@ function Invoke-CippTestCISAMSEXO142 { foreach ($Policy in $SpamPolicies) { if ($Policy.SpamAction -notin $AcceptableActions) { $FailedPolicies.Add([PSCustomObject]@{ - 'Policy Name' = $Policy.Name - 'Current Action' = $Policy.SpamAction - 'Expected' = 'MoveToJmf or Quarantine' - }) + 'Policy Name' = $Policy.Name + 'Current Action' = $Policy.SpamAction + 'Expected' = 'MoveToJmf or Quarantine' + }) } } if ($FailedPolicies.Count -eq 0) { $Result = "✅ **Pass**: All $($SpamPolicies.Count) anti-spam policy/policies move spam to junk folder or quarantine." - $Status = 'Pass' + $Status = 'Passed' } else { $Result = "❌ **Fail**: $($FailedPolicies.Count) of $($SpamPolicies.Count) anti-spam policy/policies do not properly handle spam:`n`n" - $Result += ($FailedPolicies | ConvertTo-Html -Fragment | Out-String) - $Status = 'Fail' + $Result += "| Policy Name | Current Action | Expected |`n" + $Result += "| :---------- | :------------- | :------- |`n" + foreach ($Policy in $FailedPolicies) { + $Result += "| $($Policy.'Policy Name') | $($Policy.'Current Action') | $($Policy.Expected) |`n" + } + $Status = 'Failed' } - Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO142' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Category 'Exchange Online' + Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO142' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'Spam SHALL be moved to junk email or quarantine' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Email Protection' } catch { $ErrorMessage = Get-CippException -Exception $_ - Add-CippTestResult -Status 'Failed' -ResultMarkdown "Test execution failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Category 'Exchange Online' -TestId 'CISAMSEXO142' -TenantFilter $Tenant + Add-CippTestResult -Status 'Failed' -ResultMarkdown "Test execution failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Spam SHALL be moved to junk email or quarantine' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Email Protection' -TestId 'CISAMSEXO142' -TenantFilter $Tenant } } diff --git a/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO143.ps1 b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO143.ps1 index 1824f85822cc..a5882bb96120 100644 --- a/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO143.ps1 +++ b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO143.ps1 @@ -19,7 +19,7 @@ function Invoke-CippTestCISAMSEXO143 { $SpamPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoHostedContentFilterPolicy' if (-not $SpamPolicies) { - Add-CippTestResult -Status 'Skipped' -ResultMarkdown 'ExoHostedContentFilterPolicy cache not found. Please refresh the cache for this tenant.' -Risk 'High' -Category 'Exchange Online' -TestId 'CISAMSEXO143' -TenantFilter $Tenant + Add-CippTestResult -Status 'Skipped' -ResultMarkdown 'ExoHostedContentFilterPolicy cache not found. Please refresh the cache for this tenant.' -Risk 'High' -Name 'Allowed senders SHOULD NOT be added to anti-spam filter' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Email Protection' -TestId 'CISAMSEXO143' -TenantFilter $Tenant return } @@ -31,27 +31,31 @@ function Invoke-CippTestCISAMSEXO143 { if ($AllowedSenders -gt 0 -or $AllowedSenderDomains -gt 0) { $FailedPolicies.Add([PSCustomObject]@{ - 'Policy Name' = $Policy.Name - 'Allowed Senders' = $AllowedSenders - 'Allowed Domains' = $AllowedSenderDomains - 'Issue' = 'Has allowed senders/domains that bypass spam filtering' - }) + 'Policy Name' = $Policy.Name + 'Allowed Senders' = $AllowedSenders + 'Allowed Domains' = $AllowedSenderDomains + 'Issue' = 'Has allowed senders/domains that bypass spam filtering' + }) } } if ($FailedPolicies.Count -eq 0) { $Result = "✅ **Pass**: All $($SpamPolicies.Count) anti-spam policy/policies have no spam filter bypasses configured." - $Status = 'Pass' + $Status = 'Passed' } else { $Result = "❌ **Fail**: $($FailedPolicies.Count) of $($SpamPolicies.Count) anti-spam policy/policies have spam filter bypasses configured:`n`n" - $Result += ($FailedPolicies | ConvertTo-Html -Fragment | Out-String) - $Status = 'Fail' + $Result += "| Policy Name | Allowed Senders | Allowed Domains | Issue |`n" + $Result += "| :---------- | :-------------- | :-------------- | :---- |`n" + foreach ($Policy in $FailedPolicies) { + $Result += "| $($Policy.'Policy Name') | $($Policy.'Allowed Senders') | $($Policy.'Allowed Domains') | $($Policy.Issue) |`n" + } + $Status = 'Failed' } - Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO143' -Status $Status -ResultMarkdown $Result -Risk 'High' -Category 'Exchange Online' + Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO143' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'Allowed senders SHOULD NOT be added to anti-spam filter' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Email Protection' } catch { $ErrorMessage = Get-CippException -Exception $_ - Add-CippTestResult -Status 'Failed' -ResultMarkdown "Test execution failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Category 'Exchange Online' -TestId 'CISAMSEXO143' -TenantFilter $Tenant + Add-CippTestResult -Status 'Failed' -ResultMarkdown "Test execution failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'Allowed senders SHOULD NOT be added to anti-spam filter' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Email Protection' -TestId 'CISAMSEXO143' -TenantFilter $Tenant } } diff --git a/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO151.ps1 b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO151.ps1 index ec2878ef8263..1b63ba30f031 100644 --- a/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO151.ps1 +++ b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO151.ps1 @@ -19,7 +19,7 @@ function Invoke-CippTestCISAMSEXO151 { $SafeLinksPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoSafeLinksPolicy' if (-not $SafeLinksPolicies) { - Add-CippTestResult -Status 'Skipped' -ResultMarkdown 'ExoSafeLinksPolicy cache not found. Please refresh the cache for this tenant.' -Risk 'Medium' -Category 'Exchange Online' -TestId 'CISAMSEXO151' -TenantFilter $Tenant + Add-CippTestResult -Status 'Skipped' -ResultMarkdown 'ExoSafeLinksPolicy cache not found. Please refresh the cache for this tenant.' -Risk 'High' -Name 'URL comparison with block-list SHOULD be enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Email Protection' -TestId 'CISAMSEXO151' -TenantFilter $Tenant return } @@ -27,24 +27,21 @@ function Invoke-CippTestCISAMSEXO151 { if ($FailedPolicies.Count -eq 0) { $Result = "✅ **Pass**: All $($SafeLinksPolicies.Count) Safe Links policy/policies have URL comparison with block-list enabled." - $Status = 'Pass' + $Status = 'Passed' } else { - $ResultTable = foreach ($Policy in $FailedPolicies) { - [PSCustomObject]@{ - 'Policy Name' = $Policy.Name - 'Safe Links for Email' = $Policy.EnableSafeLinksForEmail - } - } - $Result = "❌ **Fail**: $($FailedPolicies.Count) of $($SafeLinksPolicies.Count) Safe Links policy/policies do not have URL scanning enabled:`n`n" - $Result += ($ResultTable | ConvertTo-Html -Fragment | Out-String) - $Status = 'Fail' + $Result += "| Policy Name | Safe Links for Email |`n" + $Result += "| :---------- | :------------------- |`n" + foreach ($Policy in $FailedPolicies) { + $Result += "| $($Policy.Name) | $($Policy.EnableSafeLinksForEmail) |`n" + } + $Status = 'Failed' } - Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO151' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Category 'Exchange Online' + Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO151' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'URL comparison with block-list SHOULD be enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Email Protection' } catch { $ErrorMessage = Get-CippException -Exception $_ - Add-CippTestResult -Status 'Failed' -ResultMarkdown "Test execution failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Category 'Exchange Online' -TestId 'CISAMSEXO151' -TenantFilter $Tenant + Add-CippTestResult -Status 'Failed' -ResultMarkdown "Test execution failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'URL comparison with block-list SHOULD be enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Email Protection' -TestId 'CISAMSEXO151' -TenantFilter $Tenant } } diff --git a/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO152.ps1 b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO152.ps1 index fe7d29c7047d..2239e1c116d5 100644 --- a/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO152.ps1 +++ b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO152.ps1 @@ -19,7 +19,7 @@ function Invoke-CippTestCISAMSEXO152 { $SafeLinksPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoSafeLinksPolicy' if (-not $SafeLinksPolicies) { - Add-CippTestResult -Status 'Skipped' -ResultMarkdown 'ExoSafeLinksPolicy cache not found. Please refresh the cache for this tenant.' -Risk 'Medium' -Category 'Exchange Online' -TestId 'CISAMSEXO152' -TenantFilter $Tenant + Add-CippTestResult -Status 'Skipped' -ResultMarkdown 'ExoSafeLinksPolicy cache not found. Please refresh the cache for this tenant.' -Risk 'High' -Name 'Real-time suspicious URL scanning SHOULD be enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Email Protection' -TestId 'CISAMSEXO152' -TenantFilter $Tenant return } @@ -27,24 +27,21 @@ function Invoke-CippTestCISAMSEXO152 { if ($FailedPolicies.Count -eq 0) { $Result = "✅ **Pass**: All $($SafeLinksPolicies.Count) Safe Links policy/policies have real-time URL scanning enabled." - $Status = 'Pass' + $Status = 'Passed' } else { - $ResultTable = foreach ($Policy in $FailedPolicies) { - [PSCustomObject]@{ - 'Policy Name' = $Policy.Name - 'Scan URLs' = $Policy.ScanUrls - } - } - $Result = "❌ **Fail**: $($FailedPolicies.Count) of $($SafeLinksPolicies.Count) Safe Links policy/policies do not have real-time URL scanning enabled:`n`n" - $Result += ($ResultTable | ConvertTo-Html -Fragment | Out-String) - $Status = 'Fail' + $Result += "| Policy Name | Scan URLs |`n" + $Result += "| :---------- | :-------- |`n" + foreach ($Policy in $FailedPolicies) { + $Result += "| $($Policy.Name) | $($Policy.ScanUrls) |`n" + } + $Status = 'Failed' } - Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO152' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Category 'Exchange Online' + Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO152' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'Real-time suspicious URL scanning SHOULD be enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Email Protection' } catch { $ErrorMessage = Get-CippException -Exception $_ - Add-CippTestResult -Status 'Failed' -ResultMarkdown "Test execution failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Category 'Exchange Online' -TestId 'CISAMSEXO152' -TenantFilter $Tenant + Add-CippTestResult -Status 'Failed' -ResultMarkdown "Test execution failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Real-time suspicious URL scanning SHOULD be enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Email Protection' -TestId 'CISAMSEXO152' -TenantFilter $Tenant } } diff --git a/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO153.ps1 b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO153.ps1 index 4538a13b0643..bd23744fabca 100644 --- a/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO153.ps1 +++ b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO153.ps1 @@ -19,7 +19,7 @@ function Invoke-CippTestCISAMSEXO153 { $SafeLinksPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoSafeLinksPolicy' if (-not $SafeLinksPolicies) { - Add-CippTestResult -Status 'Skipped' -ResultMarkdown 'ExoSafeLinksPolicy cache not found. Please refresh the cache for this tenant.' -Risk 'Low' -Category 'Exchange Online' -TestId 'CISAMSEXO153' -TenantFilter $Tenant + Add-CippTestResult -Status 'Skipped' -ResultMarkdown 'ExoSafeLinksPolicy cache not found. Please refresh the cache for this tenant.' -Risk 'Medium' -Name 'User click tracking SHOULD be disabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Email Protection' -TestId 'CISAMSEXO153' -TenantFilter $Tenant return } @@ -27,24 +27,21 @@ function Invoke-CippTestCISAMSEXO153 { if ($FailedPolicies.Count -eq 0) { $Result = "✅ **Pass**: All $($SafeLinksPolicies.Count) Safe Links policy/policies have click tracking disabled." - $Status = 'Pass' + $Status = 'Passed' } else { - $ResultTable = foreach ($Policy in $FailedPolicies) { - [PSCustomObject]@{ - 'Policy Name' = $Policy.Name - 'Track User Clicks' = $Policy.TrackUserClicks - } - } - $Result = "❌ **Fail**: $($FailedPolicies.Count) of $($SafeLinksPolicies.Count) Safe Links policy/policies have click tracking enabled:`n`n" - $Result += ($ResultTable | ConvertTo-Html -Fragment | Out-String) - $Status = 'Fail' + $Result += "| Policy Name | Track User Clicks |`n" + $Result += "| :---------- | :---------------- |`n" + foreach ($Policy in $FailedPolicies) { + $Result += "| $($Policy.Name) | $($Policy.TrackUserClicks) |`n" + } + $Status = 'Failed' } - Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO153' -Status $Status -ResultMarkdown $Result -Risk 'Low' -Category 'Exchange Online' + Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO153' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'User click tracking SHOULD be disabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Email Protection' } catch { $ErrorMessage = Get-CippException -Exception $_ - Add-CippTestResult -Status 'Failed' -ResultMarkdown "Test execution failed: $($ErrorMessage.NormalizedError)" -Risk 'Low' -Category 'Exchange Online' -TestId 'CISAMSEXO153' -TenantFilter $Tenant + Add-CippTestResult -Status 'Failed' -ResultMarkdown "Test execution failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'User click tracking SHOULD be disabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Email Protection' -TestId 'CISAMSEXO153' -TenantFilter $Tenant } } diff --git a/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO171.ps1 b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO171.ps1 index cab8cf17243e..f4f0dfa93e3a 100644 --- a/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO171.ps1 +++ b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO171.ps1 @@ -19,7 +19,7 @@ function Invoke-CippTestCISAMSEXO171 { $AuditConfig = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoAdminAuditLogConfig' if (-not $AuditConfig) { - Add-CippTestResult -Status 'Skipped' -ResultMarkdown 'ExoAdminAuditLogConfig cache not found. Please refresh the cache for this tenant.' -Risk 'High' -Category 'Exchange Online' -TestId 'CISAMSEXO171' -TenantFilter $Tenant + Add-CippTestResult -Status 'Skipped' -ResultMarkdown 'ExoAdminAuditLogConfig cache not found. Please refresh the cache for this tenant.' -Risk 'High' -Name 'Microsoft Purview Audit logging SHALL be enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Audit & Compliance' -TestId 'CISAMSEXO171' -TenantFilter $Tenant return } @@ -29,18 +29,18 @@ function Invoke-CippTestCISAMSEXO171 { $Result = "✅ **Pass**: Microsoft Purview Audit (Standard) logging is enabled.`n`n" $Result += "**Current Settings:**`n" $Result += "- UnifiedAuditLogIngestionEnabled: $($AuditConfigObject.UnifiedAuditLogIngestionEnabled)" - $Status = 'Pass' + $Status = 'Passed' } else { $Result = "❌ **Fail**: Microsoft Purview Audit (Standard) logging is not enabled.`n`n" $Result += "**Current Settings:**`n" $Result += "- UnifiedAuditLogIngestionEnabled: $($AuditConfigObject.UnifiedAuditLogIngestionEnabled)" - $Status = 'Fail' + $Status = 'Failed' } - Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO171' -Status $Status -ResultMarkdown $Result -Risk 'High' -Category 'Exchange Online' + Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO171' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'Microsoft Purview Audit logging SHALL be enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Audit & Compliance' } catch { $ErrorMessage = Get-CippException -Exception $_ - Add-CippTestResult -Status 'Failed' -ResultMarkdown "Test execution failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Category 'Exchange Online' -TestId 'CISAMSEXO171' -TenantFilter $Tenant + Add-CippTestResult -Status 'Failed' -ResultMarkdown "Test execution failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Microsoft Purview Audit logging SHALL be enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Audit & Compliance' -TestId 'CISAMSEXO171' -TenantFilter $Tenant } } diff --git a/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO173.ps1 b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO173.ps1 index 80aa93c17623..e42677ae6d3a 100644 --- a/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO173.ps1 +++ b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO173.ps1 @@ -19,7 +19,7 @@ function Invoke-CippTestCISAMSEXO173 { $AuditConfig = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoAdminAuditLogConfig' if (-not $AuditConfig) { - Add-CippTestResult -Status 'Skipped' -ResultMarkdown 'ExoAdminAuditLogConfig cache not found. Please refresh the cache for this tenant.' -Risk 'Medium' -Category 'Exchange Online' -TestId 'CISAMSEXO173' -TenantFilter $Tenant + Add-CippTestResult -Status 'Skipped' -ResultMarkdown 'ExoAdminAuditLogConfig cache not found. Please refresh the cache for this tenant.' -Risk 'Medium' -Name 'Audit logs SHALL be maintained for at least 1 year' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Audit & Compliance' -TestId 'CISAMSEXO173' -TenantFilter $Tenant return } @@ -29,18 +29,18 @@ function Invoke-CippTestCISAMSEXO173 { $Result = "✅ **Pass**: Admin audit log is enabled (provides 1 year retention).`n`n" $Result += "**Current Settings:**`n" $Result += "- AdminAuditLogEnabled: $($AuditConfigObject.AdminAuditLogEnabled)" - $Status = 'Pass' + $Status = 'Passed' } else { $Result = "❌ **Fail**: Admin audit log is not enabled.`n`n" $Result += "**Current Settings:**`n" $Result += "- AdminAuditLogEnabled: $($AuditConfigObject.AdminAuditLogEnabled)" - $Status = 'Fail' + $Status = 'Failed' } - Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO173' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Category 'Exchange Online' + Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO173' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'Audit logs SHALL be maintained for at least 1 year' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Audit & Compliance' } catch { $ErrorMessage = Get-CippException -Exception $_ - Add-CippTestResult -Status 'Failed' -ResultMarkdown "Test execution failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Category 'Exchange Online' -TestId 'CISAMSEXO173' -TenantFilter $Tenant + Add-CippTestResult -Status 'Failed' -ResultMarkdown "Test execution failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Audit logs SHALL be maintained for at least 1 year' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Audit & Compliance' -TestId 'CISAMSEXO173' -TenantFilter $Tenant } } diff --git a/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO31.ps1 b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO31.ps1 index f6559852e341..5b2af1f957d1 100644 --- a/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO31.ps1 +++ b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO31.ps1 @@ -20,7 +20,7 @@ function Invoke-CippTestCISAMSEXO31 { $AcceptedDomains = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoAcceptedDomains' if (-not $DkimConfigs -or -not $AcceptedDomains) { - Add-CippTestResult -Status 'Skipped' -ResultMarkdown 'Required cache (ExoDkimSigningConfig or ExoAcceptedDomains) not found. Please refresh the cache for this tenant.' -Risk 'Medium' -Category 'Exchange Online' -TestId 'CISAMSEXO31' -TenantFilter $Tenant + Add-CippTestResult -Status 'Skipped' -ResultMarkdown 'Required cache (ExoDkimSigningConfig or ExoAcceptedDomains) not found. Please refresh the cache for this tenant.' -Risk 'Medium' -Name 'DKIM SHOULD be enabled for all domains' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Email Authentication' -TestId 'CISAMSEXO31' -TenantFilter $Tenant return } @@ -28,7 +28,7 @@ function Invoke-CippTestCISAMSEXO31 { $SendingDomains = $AcceptedDomains | Where-Object { -not $_.SendingFromDomainDisabled } if (($SendingDomains | Measure-Object).Count -eq 0) { - Add-CippTestResult -Status 'Pass' -ResultMarkdown '✅ **Pass**: No sending domains found to check DKIM configuration.' -Risk 'Medium' -Category 'Exchange Online' -TestId 'CISAMSEXO31' -TenantFilter $Tenant + Add-CippTestResult -Status 'Passed' -ResultMarkdown '✅ **Pass**: No sending domains found to check DKIM configuration.' -Risk 'Medium' -Name 'DKIM SHOULD be enabled for all domains' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Email Authentication' -TestId 'CISAMSEXO31' -TenantFilter $Tenant return } @@ -48,17 +48,21 @@ function Invoke-CippTestCISAMSEXO31 { if ($FailedDomains.Count -eq 0) { $Result = "✅ **Pass**: DKIM is enabled for all $($SendingDomains.Count) sending domain(s)." - $Status = 'Pass' + $Status = 'Passed' } else { $Result = "❌ **Fail**: $($FailedDomains.Count) of $($SendingDomains.Count) domain(s) do not have DKIM properly enabled:`n`n" - $Result += ($FailedDomains | ConvertTo-Html -Fragment | Out-String) - $Status = 'Fail' + $Result += "| Domain | DKIM Enabled | Status |`n" + $Result += "| :----- | :----------- | :----- |`n" + foreach ($Domain in $FailedDomains) { + $Result += "| $($Domain.Domain) | $($Domain.'DKIM Enabled') | $($Domain.Status) |`n" + } + $Status = 'Failed' } - Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO31' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Category 'Exchange Online' + Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO31' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'DKIM SHOULD be enabled for all domains' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Email Authentication' } catch { $ErrorMessage = Get-CippException -Exception $_ - Add-CippTestResult -Status 'Failed' -ResultMarkdown "Test execution failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Category 'Exchange Online' -TestId 'CISAMSEXO31' -TenantFilter $Tenant + Add-CippTestResult -Status 'Failed' -ResultMarkdown "Test execution failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'DKIM SHOULD be enabled for all domains' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Email Authentication' -TestId 'CISAMSEXO31' -TenantFilter $Tenant } } diff --git a/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO51.ps1 b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO51.ps1 index f4f6458081df..91e6390de0a8 100644 --- a/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO51.ps1 +++ b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO51.ps1 @@ -19,7 +19,7 @@ function Invoke-CippTestCISAMSEXO51 { $CASMailboxes = New-CIPPDbRequest -TenantFilter $Tenant -Type 'CASMailbox' if (-not $CASMailboxes) { - Add-CippTestResult -Status 'Skipped' -ResultMarkdown 'CASMailbox cache not found. Please refresh the cache for this tenant.' -Risk 'High' -Category 'Exchange Online' -TestId 'CISAMSEXO51' -TenantFilter $Tenant + Add-CippTestResult -Status 'Skipped' -ResultMarkdown 'CASMailbox cache not found. Please refresh the cache for this tenant.' -Risk 'High' -Name 'SMTP AUTH SHALL be disabled in Exchange Online' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Email Authentication' -TestId 'CISAMSEXO51' -TenantFilter $Tenant return } @@ -27,115 +27,25 @@ function Invoke-CippTestCISAMSEXO51 { if ($FailedMailboxes.Count -eq 0) { $Result = "✅ **Pass**: SMTP authentication is disabled for all $($CASMailboxes.Count) mailbox(es)." - $Status = 'Pass' + $Status = 'Passed' } else { - $ResultTable = $FailedMailboxes | Select-Object -First 10 | ForEach-Object { - [PSCustomObject]@{ - 'Display Name' = $_.DisplayName - 'Identity' = $_.Identity - 'SMTP Auth Disabled' = $_.SmtpClientAuthenticationDisabled - } - } - $Result = "❌ **Fail**: $($FailedMailboxes.Count) of $($CASMailboxes.Count) mailbox(es) have SMTP authentication enabled" if ($FailedMailboxes.Count -gt 10) { $Result += ' (showing first 10)' } $Result += ":`n`n" - $Result += ($ResultTable | ConvertTo-Html -Fragment | Out-String) - $Status = 'Fail' - } - - Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO51' -Status $Status -ResultMarkdown $Result -Risk 'High' -Category 'Exchange Online' - - } catch { - $ErrorMessage = Get-CippException -Exception $_ - Add-CippTestResult -Status 'Failed' -ResultMarkdown "Test execution failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Category 'Exchange Online' -TestId 'CISAMSEXO51' -TenantFilter $Tenant - } -} -function Invoke-CippTestCISAMSEXO51 { - <# - .SYNOPSIS - MS.EXO.5.1 - SMTP authentication SHALL be disabled - - .DESCRIPTION - Tests if SMTP AUTH is disabled in Exchange Online organization config - - .LINK - https://github.com/cisagov/ScubaGear - #> - param($Tenant) - - try { - $OrgConfig = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoOrganizationConfig' - - if (-not $OrgConfig) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO51' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'ExoOrganizationConfig cache not found. Please ensure cache data is available.' -Risk 'High' -Name 'SMTP authentication disabled' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Exchange Online' - return - } - - $SmtpAuthDisabled = $OrgConfig.SmtpClientAuthenticationDisabled - - if ($SmtpAuthDisabled -eq $true) { - $Status = 'Passed' - $Result = "✅ Well done. Your tenant has SMTP Authentication disabled organization-wide.`n`n" - $Result += "**Current Configuration:**`n" - $Result += "- SMTP Client Authentication: **Disabled** ✅`n" - } else { + $Result += "| Display Name | Identity | SMTP Auth Disabled |`n" + $Result += "| :----------- | :------- | :----------------- |`n" + foreach ($Mailbox in ($FailedMailboxes | Select-Object -First 10)) { + $Result += "| $($Mailbox.DisplayName) | $($Mailbox.Identity) | $($Mailbox.SmtpClientAuthenticationDisabled) |`n" + } $Status = 'Failed' - $Result = "❌ Your tenant has SMTP Authentication enabled.`n`n" - $Result += "**Current Configuration:**`n" - $Result += "- SMTP Client Authentication: **Enabled** ❌`n`n" - $Result += "**Recommendation:** Disable SMTP AUTH to prevent legacy authentication attacks. Users should use modern authentication methods.`n" } - Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO51' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'SMTP authentication disabled' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Exchange Online' - } catch { - $ErrorMessage = Get-CippException -Exception $_ - Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run CISA test CISAMSEXO51: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO51' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test execution failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'SMTP authentication disabled' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Exchange Online' - } -} -function Invoke-CippTestCISAMSEXO51 { - <# - .SYNOPSIS - MS.EXO.5.1 - SMTP authentication SHALL be disabled - - .DESCRIPTION - Tests if SMTP AUTH is disabled in Exchange Online organization config - - .LINK - https://github.com/cisagov/ScubaGear - #> - param($Tenant) - - try { - $OrgConfig = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoOrganizationConfig' - - if (-not $OrgConfig) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO51' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'ExoOrganizationConfig cache not found. Please ensure cache data is available.' -Risk 'High' -Name 'SMTP authentication disabled' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Exchange Online' - return - } - - $SmtpAuthDisabled = $OrgConfig.SmtpClientAuthenticationDisabled - - if ($SmtpAuthDisabled -eq $true) { - $Status = 'Passed' - $Result = "✅ Well done. Your tenant has SMTP Authentication disabled organization-wide.`n`n" - $Result += "**Current Configuration:**`n" - $Result += "- SMTP Client Authentication: **Disabled** ✅`n" - } else { - $Status = 'Failed' - $Result = "❌ Your tenant has SMTP Authentication enabled.`n`n" - $Result += "**Current Configuration:**`n" - $Result += "- SMTP Client Authentication: **Enabled** ❌`n`n" - $Result += "**Recommendation:** Disable SMTP AUTH to prevent legacy authentication attacks. Users should use modern authentication methods.`n" - } - - Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO51' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'SMTP authentication disabled' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Exchange Online' + Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO51' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'SMTP AUTH SHALL be disabled in Exchange Online' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Email Authentication' + } catch { $ErrorMessage = Get-CippException -Exception $_ - Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run CISA test CISAMSEXO51: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO51' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test execution failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'SMTP authentication disabled' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Exchange Online' + Add-CippTestResult -Status 'Failed' -ResultMarkdown "Test execution failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'SMTP AUTH SHALL be disabled in Exchange Online' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Email Authentication' -TestId 'CISAMSEXO51' -TenantFilter $Tenant } } diff --git a/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO61.ps1 b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO61.ps1 index 9f0ab9e412d7..cbcdc07b90b0 100644 --- a/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO61.ps1 +++ b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO61.ps1 @@ -19,7 +19,7 @@ function Invoke-CippTestCISAMSEXO61 { $SharingPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoSharingPolicy' if (-not $SharingPolicies) { - Add-CippTestResult -Status 'Skipped' -ResultMarkdown 'ExoSharingPolicy cache not found. Please refresh the cache for this tenant.' -Risk 'Medium' -Category 'Exchange Online' -TestId 'CISAMSEXO61' -TenantFilter $Tenant + Add-CippTestResult -Status 'Skipped' -ResultMarkdown 'ExoSharingPolicy cache not found. Please refresh the cache for this tenant.' -Risk 'Medium' -Name 'Contact folders SHALL NOT be shared with all domains' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Data Protection' -TestId 'CISAMSEXO61' -TenantFilter $Tenant return } @@ -41,58 +41,21 @@ function Invoke-CippTestCISAMSEXO61 { if ($FailedPolicies.Count -eq 0) { $Result = '✅ **Pass**: No sharing policies allow contact folder sharing with external domains.' - $Status = 'Pass' + $Status = 'Passed' } else { $Result = "❌ **Fail**: $($FailedPolicies.Count) sharing policy/policies allow contact folder sharing:`n`n" - $Result += ($FailedPolicies | ConvertTo-Html -Fragment | Out-String) - $Status = 'Fail' + $Result += "| Policy Name | Enabled | Issue |`n" + $Result += "| :---------- | :------ | :---- |`n" + foreach ($Policy in $FailedPolicies) { + $Result += "| $($Policy.'Policy Name') | $($Policy.Enabled) | $($Policy.Issue) |`n" + } + $Status = 'Failed' } - Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO61' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Category 'Exchange Online' + Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO61' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'Contact folders SHALL NOT be shared with all domains' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Data Protection' } catch { $ErrorMessage = Get-CippException -Exception $_ - Add-CippTestResult -Status 'Failed' -ResultMarkdown "Test execution failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Category 'Exchange Online' -TestId 'CISAMSEXO61' -TenantFilter $Tenant - } -} -function Invoke-CippTestCISAMSEXO61 { - <# - .SYNOPSIS - MS.EXO.6.1 - Contact folder sharing SHALL be restricted - - .DESCRIPTION - Tests if contact folder sharing with external users is restricted - - .LINK - https://github.com/cisagov/ScubaGear - #> - param($Tenant) - - try { - $OrgConfig = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoOrganizationConfig' - - if (-not $OrgConfig) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO61' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'ExoOrganizationConfig cache not found. Please ensure cache data is available.' -Risk 'Medium' -Name 'Contact folder sharing restricted' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Exchange Online' - return - } - - # Check if external sharing of contacts is disabled - $SharingPolicy = $OrgConfig.DefaultSharingPolicy - - $Status = 'Skipped' - $Result = "⚠️ **Additional Data Required**`n`n" - $Result += "This test requires sharing policy details to verify contact folder sharing restrictions.`n`n" - $Result += "**Current Organization Configuration:**`n" - $Result += "- Default Sharing Policy: $($SharingPolicy)`n`n" - $Result += "**Manual verification recommended:**`n" - $Result += "1. Navigate to Exchange Admin Center > Organization > Sharing`n" - $Result += "2. Verify that contact folder sharing with external domains is disabled or limited`n" - $Result += "3. Check that the default policy does not allow 'ContactsSharing' for external domains`n" - - Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO61' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'Contact folder sharing restricted' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Exchange Online' - } catch { - $ErrorMessage = Get-CippException -Exception $_ - Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run CISA test CISAMSEXO61: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO61' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test execution failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'Contact folder sharing restricted' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Exchange Online' + Add-CippTestResult -Status 'Failed' -ResultMarkdown "Test execution failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'Contact folders SHALL NOT be shared with all domains' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Data Protection' -TestId 'CISAMSEXO61' -TenantFilter $Tenant } } diff --git a/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO62.ps1 b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO62.ps1 index 1154f7bb7fe0..378995d9e126 100644 --- a/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO62.ps1 +++ b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO62.ps1 @@ -19,7 +19,7 @@ function Invoke-CippTestCISAMSEXO62 { $SharingPolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoSharingPolicy' if (-not $SharingPolicies) { - Add-CippTestResult -Status 'Skipped' -ResultMarkdown 'ExoSharingPolicy cache not found. Please refresh the cache for this tenant.' -Risk 'Medium' -Category 'Exchange Online' -TestId 'CISAMSEXO62' -TenantFilter $Tenant + Add-CippTestResult -Status 'Skipped' -ResultMarkdown 'ExoSharingPolicy cache not found. Please refresh the cache for this tenant.' -Risk 'Medium' -Name 'Calendar details SHALL NOT be shared with all domains' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Data Protection' -TestId 'CISAMSEXO62' -TenantFilter $Tenant return } @@ -42,17 +42,21 @@ function Invoke-CippTestCISAMSEXO62 { if ($FailedPolicies.Count -eq 0) { $Result = "✅ **Pass**: No sharing policies allow detailed calendar sharing with all domains." - $Status = 'Pass' + $Status = 'Passed' } else { $Result = "❌ **Fail**: $($FailedPolicies.Count) sharing policy/policies allow detailed calendar sharing with all domains:`n`n" - $Result += ($FailedPolicies | ConvertTo-Html -Fragment | Out-String) - $Status = 'Fail' + $Result += "| Policy Name | Enabled | Issue |`n" + $Result += "| :---------- | :------ | :---- |`n" + foreach ($Policy in $FailedPolicies) { + $Result += "| $($Policy.'Policy Name') | $($Policy.Enabled) | $($Policy.Issue) |`n" + } + $Status = 'Failed' } - Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO62' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Category 'Exchange Online' + Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO62' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'Calendar details SHALL NOT be shared with all domains' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Data Protection' } catch { $ErrorMessage = Get-CippException -Exception $_ - Add-CippTestResult -Status 'Failed' -ResultMarkdown "Test execution failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Category 'Exchange Online' -TestId 'CISAMSEXO62' -TenantFilter $Tenant + Add-CippTestResult -Status 'Failed' -ResultMarkdown "Test execution failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'Calendar details SHALL NOT be shared with all domains' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Data Protection' -TestId 'CISAMSEXO62' -TenantFilter $Tenant } } diff --git a/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO71.ps1 b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO71.ps1 index fbd774e2871f..e45af03bb0e7 100644 --- a/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO71.ps1 +++ b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO71.ps1 @@ -19,7 +19,7 @@ function Invoke-CippTestCISAMSEXO71 { $OrgConfig = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoOrganizationConfig' if (-not $OrgConfig) { - Add-CippTestResult -Status 'Skipped' -ResultMarkdown 'ExoOrganizationConfig cache not found. Please refresh the cache for this tenant.' -Risk 'Medium' -Category 'Exchange Online' -TestId 'CISAMSEXO71' -TenantFilter $Tenant + Add-CippTestResult -Status 'Skipped' -ResultMarkdown 'ExoOrganizationConfig cache not found. Please refresh the cache for this tenant.' -Risk 'Medium' -Name 'External sender warnings SHALL be implemented' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Email Protection' -TestId 'CISAMSEXO71' -TenantFilter $Tenant return } @@ -27,77 +27,18 @@ function Invoke-CippTestCISAMSEXO71 { if ($OrgConfigObject.ExternalInOutlook -eq $true) { $Result = '✅ **Pass**: External sender warnings are enabled in Outlook.' - $Status = 'Pass' + $Status = 'Passed' } else { $Result = "❌ **Fail**: External sender warnings are not enabled in Outlook.`n`n" $Result += "**Current Setting:**`n" $Result += "- ExternalInOutlook: $($OrgConfigObject.ExternalInOutlook)" - $Status = 'Fail' + $Status = 'Failed' } - Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO71' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Category 'Exchange Online' + Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO71' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'External sender warnings SHALL be implemented' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Email Protection' } catch { $ErrorMessage = Get-CippException -Exception $_ - Add-CippTestResult -Status 'Failed' -ResultMarkdown "Test execution failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Category 'Exchange Online' -TestId 'CISAMSEXO71' -TenantFilter $Tenant - } -} -function Invoke-CippTestCISAMSEXO71 { - <# - .SYNOPSIS - MS.EXO.7.1 - External sender warnings SHALL be implemented - - .DESCRIPTION - Tests if external sender warning is configured in Exchange transport rules - - .LINK - https://github.com/cisagov/ScubaGear - #> - param($Tenant) - - try { - $TransportRules = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoTransportRules' - - if (-not $TransportRules) { - Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO71' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'ExoTransportRules cache not found. Please ensure cache data is available.' -Risk 'Medium' -Name 'External sender warnings implemented' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Exchange Online' - return - } - - # Look for external sender warning rules - $ExternalSenderRules = $TransportRules | Where-Object { - ($_.FromScope -eq 'NotInOrganization') -and - ($_.PrependSubject -or $_.SetHeaderName -or $_.ApplyHtmlDisclaimerText) -and - ($_.State -eq 'Enabled') - } - - if ($ExternalSenderRules.Count -gt 0) { - $Status = 'Passed' - $Result = "✅ Well done. Your tenant has external sender warning rules configured.`n`n" - $Result += "**Active External Sender Warning Rules:**`n`n" - $Result += "| Rule Name | Priority | Action |`n" - $Result += "| --- | --- | --- |`n" - - foreach ($rule in $ExternalSenderRules | Sort-Object Priority) { - $action = if ($rule.PrependSubject) { 'Prepend Subject' } - elseif ($rule.SetHeaderName) { 'Set Header' } - elseif ($rule.ApplyHtmlDisclaimerText) { 'Add Disclaimer' } - else { 'Modified Message' } - $Result += "| $($rule.Name) | $($rule.Priority) | $action |`n" - } - } else { - $Status = 'Failed' - $Result = "❌ No external sender warning rules found.`n`n" - $Result += "**Recommendation:** Create a transport rule to warn users about external senders.`n`n" - $Result += "**Example configurations:**`n" - $Result += "- Add '[EXTERNAL]' prefix to subject line for emails from outside organization`n" - $Result += "- Add HTML disclaimer banner warning users the email is from external sender`n" - $Result += "- Add custom header for client-side filtering`n" - } - - Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO71' -TestType 'Identity' -Status $Status -ResultMarkdown $Result -Risk 'Medium' -Name 'External sender warnings implemented' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Exchange Online' - } catch { - $ErrorMessage = Get-CippException -Exception $_ - Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run CISA test CISAMSEXO71: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO71' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test execution failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'External sender warnings implemented' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Exchange Online' + Add-CippTestResult -Status 'Failed' -ResultMarkdown "Test execution failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'External sender warnings SHALL be implemented' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Email Protection' -TestId 'CISAMSEXO71' -TenantFilter $Tenant } } diff --git a/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO95.ps1 b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO95.ps1 index 79cf19e43f8c..ebdb715ac779 100644 --- a/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO95.ps1 +++ b/Modules/CIPPCore/Public/Tests/CISA/Identity/Invoke-CippTestCISAMSEXO95.ps1 @@ -19,7 +19,7 @@ function Invoke-CippTestCISAMSEXO95 { $MalwarePolicies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoMalwareFilterPolicy' if (-not $MalwarePolicies) { - Add-CippTestResult -Status 'Skipped' -ResultMarkdown 'ExoMalwareFilterPolicy cache not found. Please refresh the cache for this tenant.' -Risk 'High' -Category 'Exchange Online' -TestId 'CISAMSEXO95' -TenantFilter $Tenant + Add-CippTestResult -Status 'Skipped' -ResultMarkdown 'ExoMalwareFilterPolicy cache not found. Please refresh the cache for this tenant.' -Risk 'High' -Name 'Click-to-run files SHOULD be blocked' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Email Protection' -TestId 'CISAMSEXO95' -TenantFilter $Tenant return } @@ -30,10 +30,10 @@ function Invoke-CippTestCISAMSEXO95 { if (-not $Policy.EnableFileFilter) { # Policy doesn't have file filtering enabled at all $FailedPolicies.Add([PSCustomObject]@{ - 'Policy Name' = $Policy.Name - 'File Filter Enabled' = $false - 'Issue' = 'File filtering not enabled' - }) + 'Policy Name' = $Policy.Name + 'File Filter Enabled' = $false + 'Issue' = 'File filtering not enabled' + }) continue } @@ -43,26 +43,32 @@ function Invoke-CippTestCISAMSEXO95 { if ($MissingTypes) { $FailedPolicies.Add([PSCustomObject]@{ - 'Policy Name' = $Policy.Name - 'File Filter Enabled' = $true - 'Missing Blocked Types' = ($MissingTypes -join ', ') - }) + 'Policy Name' = $Policy.Name + 'File Filter Enabled' = $true + 'Missing Blocked Types' = ($MissingTypes -join ', ') + }) } } if ($FailedPolicies.Count -eq 0) { - $Result = "✅ **Pass**: All malware filter policies block click-to-run files (.exe, .cmd, .vbe)." - $Status = 'Pass' + $Result = '✅ **Pass**: All malware filter policies block click-to-run files (.exe, .cmd, .vbe).' + $Status = 'Passed' } else { $Result = "❌ **Fail**: $($FailedPolicies.Count) malware filter policy/policies do not properly block click-to-run executables:`n`n" - $Result += ($FailedPolicies | ConvertTo-Html -Fragment | Out-String) - $Status = 'Fail' + $Result += "| Policy Name | File Filter Enabled | Missing Blocked Types |`n" + $Result += "| :---------- | :------------------ | :-------------------- |`n" + foreach ($Policy in $FailedPolicies) { + $fileFilterValue = if ($Policy.'File Filter Enabled') { $Policy.'File Filter Enabled' } else { $Policy.'Issue' } + $missingTypes = if ($Policy.'Missing Blocked Types') { $Policy.'Missing Blocked Types' } else { 'N/A' } + $Result += "| $($Policy.'Policy Name') | $fileFilterValue | $missingTypes |`n" + } + $Status = 'Failed' } - Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO95' -Status $Status -ResultMarkdown $Result -Risk 'High' -Category 'Exchange Online' + Add-CippTestResult -TenantFilter $Tenant -TestId 'CISAMSEXO95' -Status $Status -ResultMarkdown $Result -Risk 'High' -Name 'Click-to-run files SHOULD be blocked' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Email Protection' } catch { $ErrorMessage = Get-CippException -Exception $_ - Add-CippTestResult -Status 'Failed' -ResultMarkdown "Test execution failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Category 'Exchange Online' -TestId 'CISAMSEXO95' -TenantFilter $Tenant + Add-CippTestResult -Status 'Failed' -ResultMarkdown "Test execution failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'Click-to-run files SHOULD be blocked' -UserImpact 'Medium' -ImplementationEffort 'Low' -Category 'Email Protection' -TestId 'CISAMSEXO95' -TenantFilter $Tenant } } From acaab5179f4d8fbdb728ec59ae77c6ba4ffa2b21 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Mon, 12 Jan 2026 14:38:52 +0100 Subject: [PATCH 124/503] fixes bug --- .../CIPP/Settings/Invoke-ExecBackendURLs.ps1 | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecBackendURLs.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecBackendURLs.ps1 index 043b12fea790..699550f4cf69 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecBackendURLs.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecBackendURLs.ps1 @@ -21,13 +21,13 @@ function Invoke-ExecBackendURLs { } $results = [PSCustomObject]@{ - ResourceGroup = "https://portal.azure.com/#@Go/resource/subscriptions/$Subscription/resourceGroups/$RGName/overview" - KeyVault = "https://portal.azure.com/#@Go/resource/subscriptions/$Subscription/resourceGroups/$RGName/providers/Microsoft.KeyVault/vaults/$($env:WEBSITE_SITE_NAME)/secrets" - FunctionApp = "https://portal.azure.com/#@Go/resource/subscriptions/$Subscription/resourceGroups/$RGName/providers/Microsoft.Web/sites/$($env:WEBSITE_SITE_NAME)/appServices" - FunctionConfig = "https://portal.azure.com/#@Go/resource/subscriptions/$Subscription/resourceGroups/$RGName/providers/Microsoft.Web/sites/$($env:WEBSITE_SITE_NAME)/configuration" - FunctionDeployment = "https://portal.azure.com/#@Go/resource/subscriptions/$Subscription/resourceGroups/$RGName/providers/Microsoft.Web/sites/$($env:WEBSITE_SITE_NAME)/vstscd" - SWADomains = "https://portal.azure.com/#@Go/resource/subscriptions/$Subscription/resourceGroups/$RGName/providers/Microsoft.Web/staticSites/$SWAName/customDomains" - SWARoles = "https://portal.azure.com/#@Go/resource/subscriptions/$Subscription/resourceGroups/$RGName/providers/Microsoft.Web/staticSites/$SWAName/roleManagement" + ResourceGroup = "https://portal.azure.com/#@/resource/subscriptions/$Subscription/resourceGroups/$RGName/overview" + KeyVault = "https://portal.azure.com/#@/resource/subscriptions/$Subscription/resourceGroups/$RGName/providers/Microsoft.KeyVault/vaults/$($env:WEBSITE_SITE_NAME)/secrets" + FunctionApp = "https://portal.azure.com/#@/resource/subscriptions/$Subscription/resourceGroups/$RGName/providers/Microsoft.Web/sites/$($env:WEBSITE_SITE_NAME)/appServices" + FunctionConfig = "https://portal.azure.com/#@/resource/subscriptions/$Subscription/resourceGroups/$RGName/providers/Microsoft.Web/sites/$($env:WEBSITE_SITE_NAME)/configuration" + FunctionDeployment = "https://portal.azure.com/#@/resource/subscriptions/$Subscription/resourceGroups/$RGName/providers/Microsoft.Web/sites/$($env:WEBSITE_SITE_NAME)/vstscd" + SWADomains = "https://portal.azure.com/#@/resource/subscriptions/$Subscription/resourceGroups/$RGName/providers/Microsoft.Web/staticSites/$SWAName/customDomains" + SWARoles = "https://portal.azure.com/#@/resource/subscriptions/$Subscription/resourceGroups/$RGName/providers/Microsoft.Web/staticSites/$SWAName/roleManagement" Subscription = $Subscription RGName = $RGName FunctionName = $env:WEBSITE_SITE_NAME From 0dee9b1b7e4ade406b9a0ae7c3572a88dff43fdb Mon Sep 17 00:00:00 2001 From: Logan Cook <2997336+MWG-Logan@users.noreply.github.com> Date: Sat, 3 Jan 2026 20:01:44 -0500 Subject: [PATCH 125/503] feat(api): add Intune reusable settings endpoints and tests feat(api): add reusable settings template endpoints and tests feat(api): add reusable settings template standard and tests feat(intune): enhance reusable settings handling in templates adds reusable setting template reference from within intune templates. Attempts to acquire a template match by reusable setting disaplayname and references the discovered template if found. if not, creates a new template and references that. This also enhances the standards experience to allow for simply deploying your intune policy. Everything else is automatic. fix: Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> fix(standards): change impact from High to Low refactor(api): extract reusable setting sync to helper Update Tests/Standards/Invoke-CIPPStandardReusableSettingsTemplate.Tests.ps1 Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> test(tests): update assertions for New-GraphPOSTRequest feat(api): add support for reusable settings in Intune policy refactor(api): extract metadata removal function into helper refactor(api): implement reusable settings discovery helper chore(api): update added date for reusable settings template refactor(api): remove package field from reusable setting templates fix(api): undo over-zealous changes on existing file refactor(api): enhance reusable settings discovery logic refactor(api): remove unused reusable settings assignment refactor(api): rename normalization function to approved verb fix(api): change impact level from high to low test(api): add metadata cleanup function to tests refactor(api): ensure string serialization for RawJSON refactor(api): optimize ReusableSettings initialization fix(api): move helper functions into public moving helper functions into public as that seems to be where the bulk of existing ones actually live fix(api): clean up spacing and foreach childResults refactor(api): optimize array handling in metadata removal refactor(api): replace Write-Information with Write-Verbose for ReusableSettings logging fix(tests): remove unused package field from test data Update Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardIntuneTemplate.ps1 Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Config/standards.json | 40 ++++ .../Public/Compare-CIPPIntuneObject.ps1 | 12 +- .../MEM/Invoke-AddIntuneReusableSetting.ps1 | 108 +++++++++++ ...nvoke-AddIntuneReusableSettingTemplate.ps1 | 92 +++++++++ .../Endpoint/MEM/Invoke-AddIntuneTemplate.ps1 | 40 ++-- .../Endpoint/MEM/Invoke-AddPolicy.ps1 | 51 ++++- ...oke-ListIntuneReusableSettingTemplates.ps1 | 44 +++++ .../MEM/Invoke-ListIntuneReusableSettings.ps1 | 72 ++++++++ .../MEM/Invoke-ListIntuneTemplates.ps1 | 2 + .../Invoke-RemoveIntuneReusableSetting.ps1 | 51 +++++ ...ke-RemoveIntuneReusableSettingTemplate.ps1 | 38 ++++ .../Get-CIPPReusableSettingsFromPolicy.ps1 | 157 ++++++++++++++++ .../Remove-CIPPReusableSettingMetadata.ps1 | 23 +++ .../CIPPCore/Public/Set-CIPPIntunePolicy.ps1 | 11 +- .../Invoke-CIPPStandardIntuneTemplate.ps1 | 13 +- ...e-CIPPStandardReusableSettingsTemplate.ps1 | 174 ++++++++++++++++++ .../Sync-CIPPReusablePolicySettings.ps1 | 78 ++++++++ .../Invoke-AddIntuneReusableSetting.Tests.ps1 | 86 +++++++++ ...AddIntuneReusableSettingTemplate.Tests.ps1 | 75 ++++++++ ...stIntuneReusableSettingTemplates.Tests.ps1 | 66 +++++++ ...nvoke-ListIntuneReusableSettings.Tests.ps1 | 66 +++++++ ...voke-RemoveIntuneReusableSetting.Tests.ps1 | 64 +++++++ ...oveIntuneReusableSettingTemplate.Tests.ps1 | 53 ++++++ ...StandardReusableSettingsTemplate.Tests.ps1 | 152 +++++++++++++++ 24 files changed, 1535 insertions(+), 33 deletions(-) create mode 100644 Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-AddIntuneReusableSetting.ps1 create mode 100644 Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-AddIntuneReusableSettingTemplate.ps1 create mode 100644 Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListIntuneReusableSettingTemplates.ps1 create mode 100644 Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListIntuneReusableSettings.ps1 create mode 100644 Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-RemoveIntuneReusableSetting.ps1 create mode 100644 Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-RemoveIntuneReusableSettingTemplate.ps1 create mode 100644 Modules/CIPPCore/Public/Get-CIPPReusableSettingsFromPolicy.ps1 create mode 100644 Modules/CIPPCore/Public/Remove-CIPPReusableSettingMetadata.ps1 create mode 100644 Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardReusableSettingsTemplate.ps1 create mode 100644 Modules/CIPPCore/Public/Sync-CIPPReusablePolicySettings.ps1 create mode 100644 Tests/Endpoint/Invoke-AddIntuneReusableSetting.Tests.ps1 create mode 100644 Tests/Endpoint/Invoke-AddIntuneReusableSettingTemplate.Tests.ps1 create mode 100644 Tests/Endpoint/Invoke-ListIntuneReusableSettingTemplates.Tests.ps1 create mode 100644 Tests/Endpoint/Invoke-ListIntuneReusableSettings.Tests.ps1 create mode 100644 Tests/Endpoint/Invoke-RemoveIntuneReusableSetting.Tests.ps1 create mode 100644 Tests/Endpoint/Invoke-RemoveIntuneReusableSettingTemplate.Tests.ps1 create mode 100644 Tests/Standards/Invoke-CIPPStandardReusableSettingsTemplate.Tests.ps1 diff --git a/Config/standards.json b/Config/standards.json index 3c40463ff676..245f7bae2466 100644 --- a/Config/standards.json +++ b/Config/standards.json @@ -4835,6 +4835,46 @@ } ] }, + { + "name": "standards.ReusableSettingsTemplate", + "cat": "Templates", + "label": "Reusable Settings Template", + "multiple": true, + "disabledFeatures": { + "report": false, + "warn": false, + "remediate": false + }, + "impact": "Low Impact", + "impactColour": "info", + "addedDate": "2026-01-11", + "helpText": "Deploy and maintain Intune reusable settings templates that can be referenced by multiple policies.", + "executiveText": "Creates and keeps reusable Intune settings templates consistent so common firewall and configuration blocks can be reused across many policies.", + "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": [] + }, { "name": "standards.TransportRuleTemplate", "label": "Transport Rule Template", diff --git a/Modules/CIPPCore/Public/Compare-CIPPIntuneObject.ps1 b/Modules/CIPPCore/Public/Compare-CIPPIntuneObject.ps1 index 20f5464d1bfa..b61e0dd093c2 100644 --- a/Modules/CIPPCore/Public/Compare-CIPPIntuneObject.ps1 +++ b/Modules/CIPPCore/Public/Compare-CIPPIntuneObject.ps1 @@ -166,7 +166,7 @@ function Compare-CIPPIntuneObject { if ($isObj1Array -or $isObj2Array) { return } - + # Safely get property names - ensure objects are not arrays before accessing PSObject.Properties $allPropertyNames = @() try { @@ -202,7 +202,7 @@ function Compare-CIPPIntuneObject { if ($prop1Exists -and $prop2Exists) { try { # Double-check arrays before accessing properties - if (($Object1 -is [Array] -or $Object1 -is [System.Collections.IList]) -or + if (($Object1 -is [Array] -or $Object1 -is [System.Collections.IList]) -or ($Object2 -is [Array] -or $Object2 -is [System.Collections.IList])) { continue } @@ -297,7 +297,7 @@ function Compare-CIPPIntuneObject { foreach ($groupValue in $child.groupSettingCollectionValue) { if ($groupValue.children) { $nestedResults = Process-GroupSettingChildren -Children $groupValue.children -Source $Source -IntuneCollection $IntuneCollection - $results.AddRange($nestedResults) + foreach ($nr in $nestedResults) { $results.Add($nr) } } } } @@ -383,7 +383,7 @@ function Compare-CIPPIntuneObject { # Also process any children within choice setting values if ($child.choiceSettingValue?.children) { $nestedResults = Process-GroupSettingChildren -Children $child.choiceSettingValue.children -Source $Source -IntuneCollection $IntuneCollection - $results.AddRange($nestedResults) + foreach ($nr in $nestedResults) { $results.Add($nr) } } } @@ -401,7 +401,7 @@ function Compare-CIPPIntuneObject { foreach ($groupValue in $settingInstance.groupSettingCollectionValue) { if ($groupValue.children -is [System.Array]) { $childResults = Process-GroupSettingChildren -Children $groupValue.children -Source 'Reference' -IntuneCollection $intuneCollection - $groupResults.AddRange($childResults) + foreach ($cr in $childResults) { $groupResults.Add($cr) } } } # Return the results from the recursive processing @@ -473,7 +473,7 @@ function Compare-CIPPIntuneObject { foreach ($groupValue in $settingInstance.groupSettingCollectionValue) { if ($groupValue.children -is [System.Array]) { $childResults = Process-GroupSettingChildren -Children $groupValue.children -Source 'Difference' -IntuneCollection $intuneCollection - $groupResults.AddRange($childResults) + foreach ($cr in $childResults) { $groupResults.Add($cr) } } } # Return the results from the recursive processing diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-AddIntuneReusableSetting.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-AddIntuneReusableSetting.ps1 new file mode 100644 index 000000000000..e13b0d5039a3 --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-AddIntuneReusableSetting.ps1 @@ -0,0 +1,108 @@ +function Invoke-AddIntuneReusableSetting { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Endpoint.MEM.ReadWrite + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + + $Tenant = $Request.Query.tenantFilter ?? $Request.Body.tenantFilter + $TemplateId = $Request.Body.TemplateId ?? $Request.Body.TemplateList?.value ?? $Request.Body.TemplateList ?? $Request.Query.TemplateId + + # Normalize tenant filter (UI sends an array of objects with value/defaultDomainName) + if ($Tenant -is [System.Collections.IEnumerable] -and -not ($Tenant -is [string])) { + $Tenant = @($Tenant)[0] + } + + $Tenant = $Tenant.value ?? $Tenant.addedFields?.defaultDomainName ?? $Tenant + + if (-not $Tenant) { + return ([HttpResponseContext]@{ + StatusCode = [System.Net.HttpStatusCode]::BadRequest + Body = @{ Results = 'tenantFilter is required' } + }) + } + + if (-not $TemplateId) { + return ([HttpResponseContext]@{ + StatusCode = [System.Net.HttpStatusCode]::BadRequest + Body = @{ Results = 'TemplateId is required' } + }) + } + + try { + $Table = Get-CippTable -tablename 'templates' + $Filter = "PartitionKey eq 'IntuneReusableSettingTemplate' and RowKey eq '$TemplateId'" + $TemplateEntity = Get-CIPPAzDataTableEntity @Table -Filter $Filter + if (-not $TemplateEntity) { + return ([HttpResponseContext]@{ + StatusCode = [System.Net.HttpStatusCode]::NotFound + Body = @{ Results = "Template $TemplateId not found" } + }) + } + + $TemplateJson = $TemplateEntity.RawJSON + if (-not $TemplateJson) { + $ParsedEntity = $TemplateEntity.JSON | ConvertFrom-Json -Depth 200 -ErrorAction SilentlyContinue + $TemplateJson = $ParsedEntity.RawJSON + } + if (-not $TemplateJson) { throw "Template $TemplateId has no RawJSON" } + + try { + $BodyObject = $TemplateJson | ConvertFrom-Json -ErrorAction Stop + } catch { + throw "Template JSON is invalid: $($_.Exception.Message)" + } + + $displayName = $BodyObject.displayName ?? $TemplateId + + $ExistingSettings = New-GraphGETRequest -Uri 'https://graph.microsoft.com/beta/deviceManagement/reusablePolicySettings' -tenantid $Tenant + $ExistingMatch = @($ExistingSettings) | Where-Object { $_.displayName -eq $displayName } | Select-Object -First 1 + + $compare = $null + if ($ExistingMatch) { + try { + $ExistingSanitized = $ExistingMatch | Select-Object -Property * -ExcludeProperty id, createdDateTime, lastModifiedDateTime, version, '@odata.context' + $compare = Compare-CIPPIntuneObject -ReferenceObject $BodyObject -DifferenceObject $ExistingSanitized -compareType 'ReusablePolicySetting' -ErrorAction SilentlyContinue + } catch { + $compare = $null + } + } + + if ($ExistingMatch -and -not $compare) { + $message = "Reusable setting '$displayName' is already compliant." + Write-LogMessage -headers $Headers -API $APIName -message $message -Sev 'Info' + return ([HttpResponseContext]@{ + StatusCode = [System.Net.HttpStatusCode]::OK + Body = @{ Results = $message; Id = $ExistingMatch.id } + }) + } + + if ($ExistingMatch) { + $null = New-GraphPOSTRequest -Uri "https://graph.microsoft.com/beta/deviceManagement/reusablePolicySettings/$($ExistingMatch.id)" -tenantid $Tenant -type PUT -body $TemplateJson + $Result = "Updated reusable setting '$displayName' in tenant $Tenant" + } else { + $Create = New-GraphPOSTRequest -Uri 'https://graph.microsoft.com/beta/deviceManagement/reusablePolicySettings' -tenantid $Tenant -type POST -body $TemplateJson + $Result = "Created reusable setting '$displayName' in tenant $Tenant" + } + + Write-LogMessage -headers $Headers -API $APIName -message $Result -Sev 'Info' + return ([HttpResponseContext]@{ + StatusCode = [System.Net.HttpStatusCode]::OK + Body = @{ Results = $Result } + }) + } catch { + $ErrorMessage = Get-CippException -Exception $_ + $logMessage = "Reusable settings deployment failed: $($ErrorMessage.NormalizedError)" + Write-LogMessage -headers $Headers -API $APIName -message $logMessage -Sev Error -LogData $ErrorMessage + return ([HttpResponseContext]@{ + StatusCode = [System.Net.HttpStatusCode]::InternalServerError + Body = @{ Results = $logMessage } + }) + } +} diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-AddIntuneReusableSettingTemplate.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-AddIntuneReusableSettingTemplate.ps1 new file mode 100644 index 000000000000..c5d96910299a --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-AddIntuneReusableSettingTemplate.ps1 @@ -0,0 +1,92 @@ +function Invoke-AddIntuneReusableSettingTemplate { + <# + .FUNCTIONALITY + Entrypoint,AnyTenant + .ROLE + Endpoint.MEM.ReadWrite + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + + $GUID = $Request.Body.GUID ?? (New-Guid).GUID + + function Format-ReusableSettingCollections { + param($InputObject) + + if ($null -eq $InputObject) { return } + + if ($InputObject -is [System.Collections.IEnumerable] -and -not ($InputObject -is [string])) { + foreach ($item in $InputObject) { Format-ReusableSettingCollections -InputObject $item } + return + } + + if ($InputObject -is [psobject]) { + foreach ($prop in $InputObject.PSObject.Properties) { + if ($prop.Name -ieq 'children' -and $null -eq $prop.Value) { + # Graph requires children to be an array; null collections must be normalized. + $prop.Value = @() + continue + } + + Format-ReusableSettingCollections -InputObject $prop.Value + } + } + } + + try { + $displayName = $Request.Body.displayName ?? $Request.Body.DisplayName ?? $Request.Body.displayname + if (-not $displayName) { throw 'You must enter a displayName' } + + $description = $Request.Body.description ?? $Request.Body.Description + $rawJsonInput = $Request.Body.rawJSON ?? $Request.Body.RawJSON ?? $Request.Body.json + + if (-not $rawJsonInput) { throw 'You must provide RawJSON for the reusable setting' } + + try { + $parsed = $rawJsonInput | ConvertFrom-Json -Depth 100 -ErrorAction Stop + } catch { + throw "RawJSON is not valid JSON: $($_.Exception.Message)" + } + + # Normalize required collections and deep-clean Graph metadata/nulls before storing + Format-ReusableSettingCollections -InputObject $parsed + $cleanParsed = Remove-CIPPReusableSettingMetadata -InputObject $parsed + $sanitizedJson = ($cleanParsed | ConvertTo-Json -Depth 100 -Compress) + + $entity = [pscustomobject]@{ + DisplayName = $displayName + Description = $description + RawJSON = $sanitizedJson + GUID = $GUID + } | ConvertTo-Json -Depth 100 -Compress + + $Table = Get-CippTable -tablename 'templates' + $Table.Force = $true + Add-CIPPAzDataTableEntity @Table -Force -Entity @{ + JSON = "$entity" + RowKey = "$GUID" + PartitionKey = 'IntuneReusableSettingTemplate' + GUID = "$GUID" + DisplayName = $displayName + Description = $description + RawJSON = "$sanitizedJson" # ensure string serialization for table storage + } + + Write-LogMessage -headers $Headers -API $APINAME -message "Created Intune reusable setting template named $displayName with GUID $GUID" -Sev 'Debug' + $body = [pscustomobject]@{ Results = 'Successfully added reusable setting template' } + $StatusCode = [System.Net.HttpStatusCode]::OK + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -headers $Headers -API $APINAME -message "Reusable Settings Template creation failed: $($ErrorMessage.NormalizedError)" -Sev 'Error' -LogData $ErrorMessage + $body = [pscustomobject]@{ Results = "Reusable Settings Template creation failed: $($ErrorMessage.NormalizedError)" } + $StatusCode = [System.Net.HttpStatusCode]::InternalServerError + } + + return ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = $body + }) +} diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-AddIntuneTemplate.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-AddIntuneTemplate.ps1 index 22c029fb5b16..ad2c5d7c5466 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-AddIntuneTemplate.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-AddIntuneTemplate.ps1 @@ -17,20 +17,23 @@ function Invoke-AddIntuneTemplate { if (!$Request.Body.displayName) { throw 'You must enter a displayName' } if ($null -eq ($Request.Body.RawJSON | ConvertFrom-Json)) { throw 'the JSON is invalid' } - + $reusableTemplateRefs = @() $object = [PSCustomObject]@{ - Displayname = $Request.Body.displayName - Description = $Request.Body.description - RAWJson = $Request.Body.RawJSON - Type = $Request.Body.TemplateType - GUID = $GUID + Displayname = $Request.Body.displayName + Description = $Request.Body.description + RAWJson = $Request.Body.RawJSON + Type = $Request.Body.TemplateType + GUID = $GUID + ReusableSettings = $reusableTemplateRefs } | ConvertTo-Json $Table = Get-CippTable -tablename 'templates' $Table.Force = $true Add-CIPPAzDataTableEntity @Table -Entity @{ - JSON = "$object" - RowKey = "$GUID" - PartitionKey = 'IntuneTemplate' + JSON = "$object" + ReusableSettingsCount = $reusableTemplateRefs.Count + RowKey = "$GUID" + PartitionKey = 'IntuneTemplate' + GUID = "$GUID" } Write-LogMessage -headers $Headers -API $APIName -message "Created intune policy template named $($Request.Body.displayName) with GUID $GUID" -Sev 'Debug' @@ -42,14 +45,19 @@ function Invoke-AddIntuneTemplate { $ID = $Request.Body.ID ?? $Request.Query.ID $ODataType = $Request.Body.ODataType ?? $Request.Query.ODataType $Template = New-CIPPIntuneTemplate -TenantFilter $TenantFilter -URLName $URLName -ID $ID -ODataType $ODataType - Write-Host "Template: $Template" + + $reusableResult = Get-CIPPReusableSettingsFromPolicy -PolicyJson $Template.TemplateJson -Tenant $TenantFilter -Headers $Headers -APIName $APIName + $reusableTemplateRefs = $reusableResult.ReusableSettings + $object = [PSCustomObject]@{ - Displayname = $Template.DisplayName - Description = $Template.Description - RAWJson = $Template.TemplateJson - Type = $Template.Type - GUID = $GUID - } | ConvertTo-Json + Displayname = $Template.DisplayName + Description = $Template.Description + RAWJson = $Template.TemplateJson + Type = $Template.Type + GUID = $GUID + ReusableSettings = $reusableTemplateRefs + } + $Table = Get-CippTable -tablename 'templates' $Table.Force = $true Add-CIPPAzDataTableEntity @Table -Entity @{ diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-AddPolicy.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-AddPolicy.ps1 index 5aaf4692faac..92dfb409d341 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-AddPolicy.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-AddPolicy.ps1 @@ -24,18 +24,51 @@ function Invoke-AddPolicy { if ($Request.Body.replacemap.$Tenant) { ([pscustomobject]$Request.Body.replacemap.$Tenant).PSObject.Properties | ForEach-Object { $RawJSON = $RawJSON -replace $_.name, $_.value } } + + $reusableSettings = $Request.Body.ReusableSettings ?? $Request.Body.reusableSettings + if (-not $reusableSettings -or $reusableSettings.Count -eq 0) { + try { + $templatesTable = Get-CippTable -tablename 'templates' + $templateEntity = Get-CIPPAzDataTableEntity @templatesTable -Filter "PartitionKey eq 'IntuneTemplate' and RowKey eq '$($Request.Body.TemplateID ?? $Request.Body.TemplateId ?? $Request.Body.TemplateGuid ?? $Request.Body.TemplateGUID)'" | Select-Object -First 1 + if (-not $templateEntity -and $DisplayName) { + $templateEntity = Get-CIPPAzDataTableEntity @templatesTable -Filter "PartitionKey eq 'IntuneTemplate'" | Where-Object { ($_.JSON | ConvertFrom-Json -ErrorAction SilentlyContinue).Displayname -eq $DisplayName } | Select-Object -First 1 + } + if ($templateEntity) { + $templateObj = $templateEntity.JSON | ConvertFrom-Json -ErrorAction SilentlyContinue + if ($templateObj.ReusableSettings) { $reusableSettings = $templateObj.ReusableSettings } + if ($templateObj.RAWJson) { $RawJSON = $templateObj.RAWJson } + } + } catch {} + } + + if (-not $reusableSettings -and $RawJSON) { + try { + # Discover referenced reusable settings from the policy JSON when none were supplied + $reusableResult = Get-CIPPReusableSettingsFromPolicy -PolicyJson $RawJSON -Tenant $Tenant -Headers $Headers -APIName $APIName + if ($reusableResult.ReusableSettings) { $reusableSettings = $reusableResult.ReusableSettings } + } catch {} + } + + $reusableSettingsForSet = $reusableSettings + if ($Request.Body.TemplateType -eq 'Catalog') { + $syncResult = Sync-CIPPReusablePolicySettings -TemplateInfo ([pscustomobject]@{ RawJSON = $RawJSON; ReusableSettings = $reusableSettings }) -Tenant $Tenant + if ($syncResult.RawJSON) { $RawJSON = $syncResult.RawJSON } + $reusableSettingsForSet = $null # helper already created/updated reusable settings and rewrote JSON + } + try { Write-Host 'Calling Adding policy' $params = @{ - TemplateType = $Request.Body.TemplateType - Description = $description - DisplayName = $DisplayName - RawJSON = $RawJSON - AssignTo = $AssignTo - ExcludeGroup = $ExcludeGroup - tenantFilter = $Tenant - Headers = $Headers - APIName = $APIName + TemplateType = $Request.Body.TemplateType + Description = $description + DisplayName = $DisplayName + RawJSON = $RawJSON + ReusableSettings = $reusableSettingsForSet + AssignTo = $AssignTo + ExcludeGroup = $ExcludeGroup + tenantFilter = $Tenant + Headers = $Headers + APIName = $APIName } Set-CIPPIntunePolicy @params } catch { diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListIntuneReusableSettingTemplates.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListIntuneReusableSettingTemplates.ps1 new file mode 100644 index 000000000000..941142e43fc7 --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListIntuneReusableSettingTemplates.ps1 @@ -0,0 +1,44 @@ +function Invoke-ListIntuneReusableSettingTemplates { + <# + .FUNCTIONALITY + Entrypoint,AnyTenant + .ROLE + Endpoint.MEM.Read + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $Table = Get-CippTable -tablename 'templates' + $Filter = "PartitionKey eq 'IntuneReusableSettingTemplate'" + $RawTemplates = Get-CIPPAzDataTableEntity @Table -Filter $Filter + + $Templates = foreach ($Item in $RawTemplates) { + $Parsed = $null + if ($Item.JSON) { + $Parsed = $Item.JSON | ConvertFrom-Json -Depth 100 -ErrorAction SilentlyContinue + } + + $DisplayName = $Parsed.DisplayName ?? $Parsed.displayName ?? $Item.DisplayName ?? $Item.RowKey + $Description = $Parsed.Description ?? $Parsed.description ?? $Item.Description + $RawJSON = $Parsed.RawJSON ?? $Item.RawJSON + [PSCustomObject]@{ + displayName = $DisplayName + description = $Description + GUID = $Item.RowKey + RawJSON = $RawJSON + isSynced = -not [string]::IsNullOrEmpty($Item.SHA) + } + } + + $Templates = $Templates | Sort-Object -Property displayName + + if ($Request.query.ID) { + $Templates = $Templates | Where-Object -Property GUID -EQ $Request.query.ID + } + + return ([HttpResponseContext]@{ + StatusCode = [System.Net.HttpStatusCode]::OK + Body = @($Templates) + }) + +} diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListIntuneReusableSettings.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListIntuneReusableSettings.ps1 new file mode 100644 index 000000000000..319a79b8956c --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListIntuneReusableSettings.ps1 @@ -0,0 +1,72 @@ +function Invoke-ListIntuneReusableSettings { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Endpoint.MEM.Read + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + + $TenantFilter = $Request.Query.tenantFilter + $SettingId = $Request.Query.ID + + if (-not $TenantFilter) { + return ([HttpResponseContext]@{ + StatusCode = [System.Net.HttpStatusCode]::BadRequest + Body = @{ Results = 'tenantFilter is required' } + }) + } + + try { + $baseUri = 'https://graph.microsoft.com/beta/deviceManagement/reusablePolicySettings' + $selectFields = @( + 'id' + 'settingInstance' + 'displayName' + 'description' + 'settingDefinitionId' + 'version' + 'referencingConfigurationPolicyCount' + 'createdDateTime' + 'lastModifiedDateTime' + ) + $selectQuery = '?$select=' + ($selectFields -join ',') + $uri = if ($SettingId) { "$baseUri/$SettingId$selectQuery" } else { "$baseUri$selectQuery" } + + $Settings = New-GraphGetRequest -uri $uri -tenantid $TenantFilter + if (-not $Settings) { $Settings = @() } + + $Settings = @($Settings) | Where-Object { $_ } | ForEach-Object { + $setting = $_ + + $rawJson = $null + try { + $rawJson = $setting | ConvertTo-Json -Depth 50 -Compress -ErrorAction Stop + } catch { + $rawJson = $null + } + + $setting | Add-Member -NotePropertyName 'RawJSON' -NotePropertyValue $rawJson -Force -PassThru + } + $StatusCode = [System.Net.HttpStatusCode]::OK + } catch { + $ErrorMessage = Get-CippException -Exception $_ + $logMessage = "Failed to retrieve reusable policy settings: $($ErrorMessage.NormalizedError)" + Write-LogMessage -headers $Headers -API $APIName -message $logMessage -Sev Error -LogData $ErrorMessage + $Settings = @() + $StatusCode = [System.Net.HttpStatusCode]::InternalServerError + return ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = @{ Results = $logMessage } + }) + } + + return ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = @($Settings) + }) +} diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListIntuneTemplates.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListIntuneTemplates.ps1 index a64228f7c2f6..f1f117dfe453 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListIntuneTemplates.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListIntuneTemplates.ps1 @@ -41,6 +41,7 @@ function Invoke-ListIntuneTemplates { $data | Add-Member -NotePropertyName 'package' -NotePropertyValue $_.Package -Force $data | Add-Member -NotePropertyName 'isSynced' -NotePropertyValue (![string]::IsNullOrEmpty($_.SHA)) -Force $data | Add-Member -NotePropertyName 'source' -NotePropertyValue $_.Source -Force + $data | Add-Member -NotePropertyName 'reusableSettings' -NotePropertyValue $JSONData.ReusableSettings -Force $data } catch { @@ -68,6 +69,7 @@ function Invoke-ListIntuneTemplates { $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 { diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-RemoveIntuneReusableSetting.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-RemoveIntuneReusableSetting.ps1 new file mode 100644 index 000000000000..03b42a4c6c2a --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-RemoveIntuneReusableSetting.ps1 @@ -0,0 +1,51 @@ +function Invoke-RemoveIntuneReusableSetting { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Endpoint.MEM.ReadWrite + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + + $TenantFilter = $Request.Body.tenantFilter ?? $Request.Query.tenantFilter + $ID = $Request.Body.ID ?? $Request.Query.ID + $DisplayName = $Request.Body.DisplayName ?? $Request.Query.DisplayName + + if (-not $TenantFilter) { + return ([HttpResponseContext]@{ + StatusCode = [System.Net.HttpStatusCode]::BadRequest + Body = @{ Results = 'tenantFilter is required' } + }) + } + + if (-not $ID) { + return ([HttpResponseContext]@{ + StatusCode = [System.Net.HttpStatusCode]::BadRequest + Body = @{ Results = 'ID is required' } + }) + } + + try { + $uri = "https://graph.microsoft.com/beta/deviceManagement/reusablePolicySettings/$ID" + $null = New-GraphPOSTRequest -uri $uri -type DELETE -tenantid $TenantFilter + + $name = if ($DisplayName) { $DisplayName } else { $ID } + $Result = "Deleted Intune reusable setting '$name' ($ID)" + Write-LogMessage -headers $Headers -API $APIName -message $Result -Sev 'Info' + $StatusCode = [System.Net.HttpStatusCode]::OK + } catch { + $ErrorMessage = Get-CippException -Exception $_ + $Result = "Failed to delete Intune reusable setting $($ID): $($ErrorMessage.NormalizedError)" + Write-LogMessage -headers $Headers -API $APIName -message $Result -Sev 'Error' -LogData $ErrorMessage + $StatusCode = [System.Net.HttpStatusCode]::InternalServerError + } + + return ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = @{ Results = $Result } + }) +} diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-RemoveIntuneReusableSettingTemplate.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-RemoveIntuneReusableSettingTemplate.ps1 new file mode 100644 index 000000000000..50cf3154e2ea --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-RemoveIntuneReusableSettingTemplate.ps1 @@ -0,0 +1,38 @@ +function Invoke-RemoveIntuneReusableSettingTemplate { + <# + .FUNCTIONALITY + Entrypoint,AnyTenant + .ROLE + Endpoint.MEM.ReadWrite + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + + $ID = $Request.Query.ID ?? $Request.Body.ID + + try { + if (-not $ID) { throw 'You must supply an ID' } + + $Table = Get-CippTable -tablename 'templates' + $Filter = "PartitionKey eq 'IntuneReusableSettingTemplate' and RowKey eq '$ID'" + $Row = Get-CIPPAzDataTableEntity @Table -Filter $Filter -Property PartitionKey, RowKey + Remove-AzDataTableEntity -Force @Table -Entity $Row + + $Result = "Removed Intune reusable setting template with ID $ID" + Write-LogMessage -Headers $Headers -API $APIName -message $Result -Sev 'Info' + $StatusCode = [System.Net.HttpStatusCode]::OK + } catch { + $ErrorMessage = Get-CippException -Exception $_ + $Result = "Failed to remove Intune reusable setting template $($ID): $($ErrorMessage.NormalizedError)" + Write-LogMessage -Headers $Headers -API $APIName -message $Result -Sev 'Error' -LogData $ErrorMessage + $StatusCode = [System.Net.HttpStatusCode]::InternalServerError + } + + return ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = @{'Results' = $Result } + }) +} diff --git a/Modules/CIPPCore/Public/Get-CIPPReusableSettingsFromPolicy.ps1 b/Modules/CIPPCore/Public/Get-CIPPReusableSettingsFromPolicy.ps1 new file mode 100644 index 000000000000..8e331a484f00 --- /dev/null +++ b/Modules/CIPPCore/Public/Get-CIPPReusableSettingsFromPolicy.ps1 @@ -0,0 +1,157 @@ +function Get-CIPPReusableSettingsFromPolicy { + param( + [string]$PolicyJson, + [string]$Tenant, + $Headers, + [string]$APIName + ) + + $result = [pscustomobject]@{ + ReusableSettings = [System.Collections.Generic.List[psobject]]::new() + } + + if (-not $PolicyJson) { return $result } + + try { + $policyObject = $PolicyJson | ConvertFrom-Json -Depth 300 -ErrorAction Stop + } catch { + Write-LogMessage -headers $Headers -API $APIName -message "Reusable settings discovery failed: policy JSON invalid ($($_.Exception.Message))" -Sev 'Warn' + return $result + } + + function Get-ReusableSettingIds { + param( + [Parameter(Mandatory = $true)] + $PolicyObject + ) + + $ids = [System.Collections.Generic.List[string]]::new() + + function Get-ReusableSettingIdsFromValue { + param( + $Value, + [string]$ParentName = '' + ) + + if ($null -eq $Value) { return } + + if ($Value -is [System.Collections.IEnumerable] -and $Value -isnot [string]) { + foreach ($item in $Value) { Get-ReusableSettingIdsFromValue -Value $item -ParentName $ParentName } + return + } + + if ($Value -is [psobject]) { + if ($Value.'@odata.type' -like '*ReferenceSettingValue' -and $Value.value -match '^[0-9a-fA-F-]{36}$') { + $ids.Add($Value.value) + } + + if ($ParentName -eq 'simpleSettingCollectionValue' -and $Value.value -is [string] -and $Value.value -match '^[0-9a-fA-F-]{36}$') { + $ids.Add($Value.value) + } + + foreach ($prop in $Value.PSObject.Properties) { + $name = $prop.Name + $propValue = $prop.Value + + if ($name -match 'reusableSetting') { + if ($propValue -is [string] -and $propValue -match '^[0-9a-fA-F-]{36}$') { $ids.Add($propValue) } + elseif ($propValue -is [psobject] -and $propValue.id -match '^[0-9a-fA-F-]{36}$') { $ids.Add($propValue.id) } + elseif ($propValue -is [System.Collections.IEnumerable]) { + foreach ($entry in $propValue) { + if ($entry -is [string] -and $entry -match '^[0-9a-fA-F-]{36}$') { $ids.Add($entry) } + elseif ($entry -is [psobject] -and $entry.id -match '^[0-9a-fA-F-]{36}$') { $ids.Add($entry.id) } + } + } + } + + Get-ReusableSettingIdsFromValue -Value $propValue -ParentName $name + } + } + } + + Get-ReusableSettingIdsFromValue -Value $PolicyObject + return $ids | Select-Object -Unique + } + + $referencedReusableIds = Get-ReusableSettingIds -PolicyObject $policyObject + Write-Information "ReusableSettings discovery: found $($referencedReusableIds.Count) ids -> $($referencedReusableIds -join ',')" + + if (-not $referencedReusableIds) { return $result } + + $templatesTable = Get-CippTable -tablename 'templates' + $templatesTableForAdd = @{} + $templatesTable + $templatesTableForAdd.Force = $true + + $existingReusableTemplates = @(Get-CIPPAzDataTableEntity @templatesTable -Filter "PartitionKey eq 'IntuneReusableSettingTemplate'") + $existingReusableByName = @{} + foreach ($templateEntry in $existingReusableTemplates) { + $name = $templateEntry.DisplayName + if (-not $name) { + $parsed = $templateEntry.JSON | ConvertFrom-Json -ErrorAction SilentlyContinue + $name = $parsed.DisplayName + } + if ($name -and -not $existingReusableByName.ContainsKey($name)) { + $existingReusableByName[$name] = $templateEntry + } + } + + foreach ($settingId in $referencedReusableIds) { + try { + $setting = New-GraphGETRequest -Uri "https://graph.microsoft.com/beta/deviceManagement/reusablePolicySettings/$settingId" -tenantid $Tenant + if (-not $setting) { + Write-LogMessage -headers $Headers -API $APIName -message "Reusable setting $settingId not returned from Graph" -Sev 'Warn' + continue + } + + $settingDisplayName = $setting.displayName + if (-not $settingDisplayName) { + Write-LogMessage -headers $Headers -API $APIName -message "Reusable setting $settingId missing displayName" -Sev 'Warn' + continue + } + + $matchedTemplate = $existingReusableByName[$settingDisplayName] + $templateGuid = $matchedTemplate.RowKey + + if (-not $templateGuid) { + $cleanSetting = Remove-CIPPReusableSettingMetadata -InputObject $setting + $sanitizedJson = $cleanSetting | ConvertTo-Json -Depth 100 -Compress + $templateGuid = (New-Guid).Guid + $reusableEntity = [pscustomobject]@{ + DisplayName = $settingDisplayName + Description = $setting.description + RawJSON = $sanitizedJson + GUID = $templateGuid + } | ConvertTo-Json -Depth 100 -Compress + + Add-CIPPAzDataTableEntity @templatesTableForAdd -Entity @{ + JSON = "$reusableEntity" + RowKey = "$templateGuid" + PartitionKey = 'IntuneReusableSettingTemplate' + GUID = "$templateGuid" + DisplayName = $settingDisplayName + } + + $existingReusableByName[$settingDisplayName] = [pscustomobject]@{ + RowKey = $templateGuid + DisplayName = $settingDisplayName + JSON = $reusableEntity + } + + Write-LogMessage -headers $Headers -API $APIName -message "Created reusable setting template $templateGuid for '$settingDisplayName'" -Sev 'Info' + } else { + Write-LogMessage -headers $Headers -API $APIName -message "Reusing existing reusable setting template $templateGuid for '$settingDisplayName'" -Sev 'Info' + } + + $result.ReusableSettings.Add([pscustomobject]@{ + displayName = $settingDisplayName + templateId = $templateGuid + sourceId = $settingId + }) + } catch { + Write-LogMessage -headers $Headers -API $APIName -message "Failed to link reusable setting $settingId for template creation: $($_.Exception.Message)" -Sev 'Warn' + } + } + + Write-LogMessage -headers $Headers -API $APIName -message "Reusable settings mapped: $($result.ReusableSettings.Count) -> $($result.ReusableSettings.displayName -join ', ')" -Sev 'Info' + return $result +} diff --git a/Modules/CIPPCore/Public/Remove-CIPPReusableSettingMetadata.ps1 b/Modules/CIPPCore/Public/Remove-CIPPReusableSettingMetadata.ps1 new file mode 100644 index 000000000000..3a6fcba20866 --- /dev/null +++ b/Modules/CIPPCore/Public/Remove-CIPPReusableSettingMetadata.ps1 @@ -0,0 +1,23 @@ +function Remove-CIPPReusableSettingMetadata { + param($InputObject) + + if ($null -eq $InputObject) { return $null } + + if ($InputObject -is [System.Collections.IEnumerable] -and $InputObject -isnot [string]) { + $cleanArray = [System.Collections.Generic.List[object]]::new() + foreach ($item in $InputObject) { $cleanArray.Add((Remove-CIPPReusableSettingMetadata -InputObject $item)) } + return $cleanArray + } + + if ($InputObject -is [psobject]) { + $output = [ordered]@{} + foreach ($prop in $InputObject.PSObject.Properties) { + if ($null -eq $prop.Value) { continue } + if ($prop.Name -in @('id','createdDateTime','lastModifiedDateTime','version','@odata.context','@odata.etag','referencingConfigurationPolicyCount','settingInstanceTemplateReference','settingValueTemplateReference','auditRuleInformation')) { continue } + $output[$prop.Name] = Remove-CIPPReusableSettingMetadata -InputObject $prop.Value + } + return [pscustomobject]$output + } + + return $InputObject +} diff --git a/Modules/CIPPCore/Public/Set-CIPPIntunePolicy.ps1 b/Modules/CIPPCore/Public/Set-CIPPIntunePolicy.ps1 index b0518e1d5d5a..63895588ee49 100644 --- a/Modules/CIPPCore/Public/Set-CIPPIntunePolicy.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPIntunePolicy.ps1 @@ -11,7 +11,8 @@ function Set-CIPPIntunePolicy { $APIName = 'Set-CIPPIntunePolicy', $TenantFilter, $AssignmentFilterName, - $AssignmentFilterType = 'include' + $AssignmentFilterType = 'include', + [array]$ReusableSettings ) $RawJSON = Get-CIPPTextReplacement -TenantFilter $TenantFilter -Text $RawJSON @@ -133,6 +134,14 @@ function Set-CIPPIntunePolicy { $PlatformType = 'deviceManagement' $TemplateTypeURL = 'configurationPolicies' $DisplayName = ($RawJSON | ConvertFrom-Json).Name + if ($ReusableSettings) { + Write-Verbose "Catalog: ReusableSettings count $($ReusableSettings.Count)" + Write-Verbose ("Catalog: ReusableSettings detail " + ($ReusableSettings | ConvertTo-Json -Depth 5 -Compress)) + $syncResult = Sync-CIPPReusablePolicySettings -TemplateInfo ([pscustomobject]@{ RawJSON = $RawJSON; ReusableSettings = $ReusableSettings }) -Tenant $TenantFilter + if ($syncResult.RawJSON) { $RawJSON = $syncResult.RawJSON } + } else { + Write-Verbose "Catalog: No ReusableSettings provided" + } $Template = $RawJSON | ConvertFrom-Json if ($Template.templateReference.templateId) { diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardIntuneTemplate.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardIntuneTemplate.ps1 index 4ba8ebbc353d..595e536c0714 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardIntuneTemplate.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardIntuneTemplate.ps1 @@ -36,6 +36,7 @@ function Invoke-CIPPStandardIntuneTemplate { https://docs.cipp.app/user-documentation/tenant/standards/list-standards #> param($Tenant, $Settings) + $TestResult = Test-CIPPStandardLicense -StandardName 'IntuneTemplate_general' -TenantFilter $Tenant -RequiredCapabilities @('INTUNE_A', 'MDM_Services', 'EMS', 'SCCM', 'MICROSOFTINTUNEPLAN1') ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'intuneTemplate' @@ -58,6 +59,16 @@ function Invoke-CIPPStandardIntuneTemplate { Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to find template $($Template.TemplateList.value). Has this Intune Template been deleted?" -sev 'Error' continue } + try { + $reusableSync = Sync-CIPPReusablePolicySettings -TemplateInfo $Request.body -Tenant $Tenant -ErrorAction Stop + if ($null -ne $reusableSync -and $reusableSync.PSObject.Properties.Name -contains 'RawJSON' -and $reusableSync.RawJSON) { + $Request.body.RawJSON = $reusableSync.RawJSON + } + } catch { + Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to sync reusable policy settings for template $($Template.TemplateList.value): $($_.Exception.Message)" -sev 'Error' + Write-Host "IntuneTemplate: $($Template.TemplateList.value) - Failed to sync reusable policy settings. Skipping this template." + continue + } Write-Host "IntuneTemplate: $($Template.TemplateList.value) - Got template." $displayname = $request.body.Displayname @@ -140,7 +151,7 @@ function Invoke-CIPPStandardIntuneTemplate { Write-Host "working on template deploy: $($TemplateFile.displayname)" try { $TemplateFile.customGroup ? ($TemplateFile.AssignTo = $TemplateFile.customGroup) : $null - + $PolicyParams = @{ TemplateType = $TemplateFile.body.Type Description = $TemplateFile.description diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardReusableSettingsTemplate.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardReusableSettingsTemplate.ps1 new file mode 100644 index 000000000000..3960ddb4281c --- /dev/null +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardReusableSettingsTemplate.ps1 @@ -0,0 +1,174 @@ +function Invoke-CIPPStandardReusableSettingsTemplate { + <# + .FUNCTIONALITY + Internal + .COMPONENT + (APIName) ReusableSettingsTemplate + .SYNOPSIS + (Label) Reusable Settings Template + .DESCRIPTION + (Helptext) Deploy and manage Intune reusable settings templates for reuse across multiple policies. + (DocsDescription) Deploy and manage Intune reusable settings templates for reuse across multiple policies. + .NOTES + CAT + Templates + MULTIPLE + True + DISABLEDFEATURES + {"report":false,"warn":false,"remediate":false} + IMPACT + Low Impact + ADDEDDATE + 2026-01-11 + EXECUTIVETEXT + Creates and maintains reusable Intune settings templates that can be referenced by multiple policies, ensuring consistent firewall and configuration rule blocks are centrally managed and updated. + 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"}}} + UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block + .LINK + https://docs.cipp.app/user-documentation/tenant/standards/list-standards + #> + param($Tenant, $Settings) + + function Remove-CIPPNullProperties { + param($InputObject) + + if ($null -eq $InputObject) { + return $null + } + + if ($InputObject -is [System.Collections.IEnumerable] -and $InputObject -isnot [string]) { + $CleanArray = [System.Collections.Generic.List[object]]::new() + foreach ($item in $InputObject) { + $CleanArray.Add((Remove-CIPPNullProperties -InputObject $item)) + } + return $CleanArray + } + + if ($InputObject -is [psobject]) { + $Output = [ordered]@{} + foreach ($prop in $InputObject.PSObject.Properties) { + if ($null -ne $prop.Value) { + $Output[$prop.Name] = Remove-CIPPNullProperties -InputObject $prop.Value + } + } + return [pscustomobject]$Output + } + + return $InputObject + } + + $RequiredCapabilities = @('INTUNE_A', 'MDM_Services', 'EMS', 'SCCM', 'MICROSOFTINTUNEPLAN1') + $TestResult = Test-CIPPStandardLicense -StandardName 'ReusableSettingsTemplate_general' -TenantFilter $Tenant -RequiredCapabilities $RequiredCapabilities + if ($TestResult -eq $false) { + $settings.TemplateList | ForEach-Object { + $MissingLicenseMessage = "This tenant is missing one or more required licenses for this standard: $($RequiredCapabilities -join ', ')." + Set-CIPPStandardsCompareField -FieldName "standards.ReusableSettingsTemplate.$($_.value)" -FieldValue $MissingLicenseMessage -Tenant $Tenant + } + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Exiting as the correct license is not present for this standard. Missing: $($RequiredCapabilities -join ', ')" -sev 'Warn' + return $true + } + + $Table = Get-CippTable -tablename 'templates' + $ExistingReusableSettings = New-GraphGETRequest -Uri 'https://graph.microsoft.com/beta/deviceManagement/reusablePolicySettings?$top=999' -tenantid $Tenant + + # Align with other template standards by resolving all selected templates upfront + $SelectedTemplateIds = @($Settings.TemplateList.value) + if (-not $SelectedTemplateIds) { + Write-LogMessage -API 'Standards' -tenant $Tenant -message 'No reusable settings templates were selected.' -sev 'Warn' + return $true + } + + $AllTemplateEntities = Get-CIPPAzDataTableEntity @Table -Filter "PartitionKey eq 'IntuneReusableSettingTemplate'" + $TemplateEntities = $AllTemplateEntities | + Where-Object { ($_.RowKey -in $SelectedTemplateIds) -and (-not [string]::IsNullOrWhiteSpace($_.JSON)) } | + ForEach-Object { $_.JSON } | + ConvertFrom-Json -ErrorAction SilentlyContinue + if (-not $TemplateEntities) { + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to resolve reusable settings templates: $($SelectedTemplateIds -join ', ')" -sev 'Error' + return $true + } + + $CompareList = foreach ($TemplateEntity in $TemplateEntities) { + $Compare = $null + $displayName = $TemplateEntity.DisplayName ?? $TemplateEntity.Name + $RawJSON = $TemplateEntity.RawJSON ?? $TemplateEntity.JSON + $BodyObject = $RawJSON | ConvertFrom-Json -ErrorAction SilentlyContinue + $BodyObjectClean = Remove-CIPPNullProperties -InputObject $BodyObject + $Existing = $ExistingReusableSettings | Where-Object -Property displayName -EQ $displayName | Select-Object -First 1 + + if ($Existing) { + try { + $ExistingSanitized = $Existing | Select-Object -Property * -ExcludeProperty id, createdDateTime, lastModifiedDateTime, version, '@odata.context' + $ExistingClean = Remove-CIPPNullProperties -InputObject $ExistingSanitized + $Compare = Compare-CIPPIntuneObject -ReferenceObject $BodyObjectClean -DifferenceObject $ExistingClean -compareType 'ReusablePolicySetting' -ErrorAction SilentlyContinue + } catch { + Write-LogMessage -API 'Standards' -tenant $Tenant -message "ReusableSettingsTemplate: compare failed for $displayName. $($_.Exception.Message)" -sev 'Error' + } + } else { + $Compare = [pscustomobject]@{ + MatchFailed = $true + Difference = 'Reusable setting is missing in this tenant.' + } + } + + $CompareClean = if ($Compare) { Remove-CIPPNullProperties -InputObject $Compare } else { $Compare } + + [pscustomobject]@{ + MatchFailed = [bool]$Compare + displayname = $displayName + compare = $CompareClean + rawJSON = $RawJSON + remediate = $Settings.remediate + alert = $Settings.alert + report = $Settings.report + templateId = $TemplateEntity.GUID + existingId = $Existing.id + } + } + + if ($true -in $Settings.remediate) { + foreach ($Template in $CompareList | Where-Object -Property remediate -EQ $true) { + $Body = $Template.rawJSON + + if ($Template.existingId) { + try { + $null = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/deviceManagement/reusablePolicySettings/$($Template.existingId)" -tenantid $Tenant -type PUT -body $Body + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Updated reusable setting $($Template.displayName)" -sev 'Info' + } catch { + $errorMessage = Get-NormalizedError -Message $_.Exception.Message + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to update reusable setting $($Template.displayName). Error: $errorMessage" -sev 'Error' + } + } else { + try { + $CreateRequest = New-GraphPOSTRequest -uri 'https://graph.microsoft.com/beta/deviceManagement/reusablePolicySettings' -tenantid $Tenant -type POST -body $Body + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Created reusable setting $($Template.displayName)" -sev 'Info' + } catch { + $createError = Get-NormalizedError -Message $_.Exception.Message + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to create reusable setting $($Template.displayName). Error: $createError" -sev 'Error' + } + } + } + } + + if ($true -in $Settings.alert) { + foreach ($Template in $CompareList | Where-Object -Property alert -EQ $true) { + $AlertObj = $Template | Select-Object -Property displayName, compare, existingId + if ($Template.compare) { + Write-StandardsAlert -message "Reusable setting $($Template.displayName) does not match the expected configuration." -object $AlertObj -tenant $Tenant -standardName 'ReusableSettingsTemplate' -standardId $Template.templateId + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Reusable setting $($Template.displayName) is out of compliance." -sev info + } else { + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Reusable setting $($Template.displayName) is compliant." -sev Info + } + } + } + + if ($true -in $Settings.report) { + foreach ($Template in $CompareList | Where-Object { $_.report -eq $true -or $_.remediate -eq $true }) { + $id = $Template.templateId + $state = $Template.compare ? $Template.compare : $true + Set-CIPPStandardsCompareField -FieldName "standards.ReusableSettingsTemplate.$id" -FieldValue $state -TenantFilter $Tenant + } + } +} diff --git a/Modules/CIPPCore/Public/Sync-CIPPReusablePolicySettings.ps1 b/Modules/CIPPCore/Public/Sync-CIPPReusablePolicySettings.ps1 new file mode 100644 index 000000000000..6d1313157f03 --- /dev/null +++ b/Modules/CIPPCore/Public/Sync-CIPPReusablePolicySettings.ps1 @@ -0,0 +1,78 @@ +function Sync-CIPPReusablePolicySettings { + param( + [psobject]$TemplateInfo, + [string]$Tenant + ) + + $result = [pscustomobject]@{ + RawJSON = $TemplateInfo.RawJSON + Map = @{} + } + + $reusableRefs = @($TemplateInfo.ReusableSettings) + if (-not $reusableRefs) { return $result } + + $existingReusableSettings = New-GraphGETRequest -Uri 'https://graph.microsoft.com/beta/deviceManagement/reusablePolicySettings?$top=999' -tenantid $Tenant + $table = Get-CippTable -tablename 'templates' + $templateEntities = Get-CIPPAzDataTableEntity @table -Filter "PartitionKey eq 'IntuneReusableSettingTemplate'" + + foreach ($ref in $reusableRefs) { + $templateId = $ref.templateId ?? $ref.templateID ?? $ref.GUID ?? $ref.RowKey + $sourceId = $ref.sourceId ?? $ref.sourceReusableSettingId ?? $ref.sourceGuid ?? $ref.id + $displayName = $ref.displayName ?? $ref.DisplayName + + if (-not $templateId -or -not $displayName) { continue } + + $templateEntity = $templateEntities | Where-Object { $_.RowKey -eq $templateId } | Select-Object -First 1 + if (-not $templateEntity) { continue } + + $templateData = $templateEntity.JSON | ConvertFrom-Json -Depth 200 -ErrorAction SilentlyContinue + $templateRaw = $templateData.RawJSON + if ($templateRaw -is [string] -and $templateRaw -match '"children"\s*:\s*null') { + try { + $templateRaw = [regex]::Replace($templateRaw, '"children"\s*:\s*null', '"children":[]', [System.Text.RegularExpressions.RegexOptions]::IgnoreCase) + } catch {} + } + $templateBody = $templateRaw | ConvertFrom-Json -Depth 200 -ErrorAction SilentlyContinue + if (-not $templateRaw -or -not $templateBody) { continue } + $existingMatch = $existingReusableSettings | Where-Object -Property displayName -EQ $displayName | Select-Object -First 1 + $targetId = $existingMatch.id + $needsUpdate = $false + + if ($existingMatch) { + try { + $existingClean = $existingMatch | Select-Object -Property * -ExcludeProperty id, createdDateTime, lastModifiedDateTime, version, '@odata.context', '@odata.etag' + $compare = Compare-CIPPIntuneObject -ReferenceObject $templateBody -DifferenceObject $existingClean -compareType 'ReusablePolicySetting' -ErrorAction SilentlyContinue + if ($compare) { $needsUpdate = $true } + } catch { + $needsUpdate = $true + } + } else { + $needsUpdate = $true + } + + if ($needsUpdate) { + try { + if ($targetId) { + $updated = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/deviceManagement/reusablePolicySettings/$targetId" -tenantid $Tenant -type PUT -body $templateRaw + $targetId = $updated.id ?? $targetId + } else { + $created = New-GraphPOSTRequest -uri 'https://graph.microsoft.com/beta/deviceManagement/reusablePolicySettings' -tenantid $Tenant -type POST -body $templateRaw + $targetId = $created.id ?? $targetId + } + } catch { + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to deploy reusable setting $($displayName): $($_.Exception.Message)" -sev 'Error' + } + } + + if ($sourceId -and $targetId) { $result.Map[$sourceId] = $targetId } + } + + $updatedJson = $result.RawJSON + foreach ($pair in $result.Map.GetEnumerator()) { + $updatedJson = $updatedJson -replace [regex]::Escape($pair.Key), $pair.Value + } + $result.RawJSON = $updatedJson + + return $result +} diff --git a/Tests/Endpoint/Invoke-AddIntuneReusableSetting.Tests.ps1 b/Tests/Endpoint/Invoke-AddIntuneReusableSetting.Tests.ps1 new file mode 100644 index 000000000000..555008547f99 --- /dev/null +++ b/Tests/Endpoint/Invoke-AddIntuneReusableSetting.Tests.ps1 @@ -0,0 +1,86 @@ +# Pester tests for Invoke-AddIntuneReusableSetting +# Validates create path, compliance short-circuit, and validation + +BeforeAll { + $RepoRoot = Split-Path -Parent (Split-Path -Parent (Split-Path -Parent $PSCommandPath)) + $FunctionPath = Join-Path $RepoRoot 'Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-AddIntuneReusableSetting.ps1' + + class HttpResponseContext { + [int]$StatusCode + [object]$Body + } + + function Get-CippTable { param($tablename) @{} } + function Get-CIPPAzDataTableEntity { param($Filter) $script:lastFilter = $Filter; return $script:templateRow } + function New-GraphGETRequest { param($Uri, $tenantid) return $script:existingSettings } + function Compare-CIPPIntuneObject { param($ReferenceObject, $DifferenceObject, $compareType) return $script:compareResult } + function New-GraphPOSTRequest { param($Uri, $tenantid, $type, $body) $script:lastPost = @{ Uri = $Uri; Type = $type; Body = $body } } + function Write-LogMessage { param($headers, $API, $message, $sev, $LogData) $script:logs += $message } + function Get-CippException { param($Exception) $Exception } + + . $FunctionPath +} + +Describe 'Invoke-AddIntuneReusableSetting' { + BeforeEach { + $script:lastFilter = $null + $script:templateRow = [pscustomobject]@{ + RawJSON = '{"displayName":"Reusable One","setting":"value"}' + DisplayName = 'Reusable One' + } + $script:existingSettings = @() + $script:compareResult = $null + $script:lastPost = $null + $script:logs = @() + } + + It 'creates a new reusable setting when none exist' { + $request = [pscustomobject]@{ + Params = @{ CIPPEndpoint = 'AddIntuneReusableSetting' } + Headers = @{ Authorization = 'token' } + Body = [pscustomobject]@{ + tenantFilter = 'contoso.onmicrosoft.com' + TemplateId = 'template-1' + } + } + + $response = Invoke-AddIntuneReusableSetting -Request $request -TriggerMetadata $null + + $response.StatusCode | Should -Be ([System.Net.HttpStatusCode]::OK) + $response.Body.Results | Should -Match 'Created reusable setting' + $lastPost.Type | Should -Be 'POST' + $lastPost.Uri | Should -Match '/reusablePolicySettings$' + $lastPost.Body | Should -Match 'displayName":"Reusable One"' + $logs | Should -Not -BeNullOrEmpty + } + + It 'returns OK and does not post when the setting is already compliant' { + $script:existingSettings = @([pscustomobject]@{ id = 'existing'; displayName = 'Reusable One'; version = 1 }) + $script:compareResult = $null + + $request = [pscustomobject]@{ + Params = @{ CIPPEndpoint = 'AddIntuneReusableSetting' } + Headers = @{} + Body = [pscustomobject]@{ + tenantFilter = 'contoso.onmicrosoft.com' + TemplateId = 'template-1' + } + } + + $response = Invoke-AddIntuneReusableSetting -Request $request -TriggerMetadata $null + + $response.StatusCode | Should -Be ([System.Net.HttpStatusCode]::OK) + $response.Body.Id | Should -Be 'existing' + $response.Body.Results | Should -Match 'already compliant' + $lastPost | Should -BeNullOrEmpty + } + + It 'returns BadRequest when tenantFilter is missing' { + $request = [pscustomobject]@{ Params = @{}; Body = [pscustomobject]@{ TemplateId = 'template-1' } } + + $response = Invoke-AddIntuneReusableSetting -Request $request -TriggerMetadata $null + + $response.StatusCode | Should -Be ([System.Net.HttpStatusCode]::BadRequest) + $response.Body.Results | Should -Match 'tenantFilter is required' + } +} diff --git a/Tests/Endpoint/Invoke-AddIntuneReusableSettingTemplate.Tests.ps1 b/Tests/Endpoint/Invoke-AddIntuneReusableSettingTemplate.Tests.ps1 new file mode 100644 index 000000000000..14a79de3fca1 --- /dev/null +++ b/Tests/Endpoint/Invoke-AddIntuneReusableSettingTemplate.Tests.ps1 @@ -0,0 +1,75 @@ +# Pester tests for Invoke-AddIntuneReusableSettingTemplate +# Validates template creation and validation + +BeforeAll { + $RepoRoot = Split-Path -Parent (Split-Path -Parent (Split-Path -Parent $PSCommandPath)) + $FunctionPath = Join-Path $RepoRoot 'Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-AddIntuneReusableSettingTemplate.ps1' + + class HttpResponseContext { + [int]$StatusCode + [object]$Body + } + + function Get-CippTable { param($tablename) @{} } + function Add-CIPPAzDataTableEntity { param([switch]$Force, $Entity) $script:lastEntity = $Entity; $script:lastForce = $Force } + function Write-LogMessage { param($headers, $API, $message, $sev, $LogData) $script:logs += $message } + function Get-CippException { + param($Exception) + # Mimic normalized error structure returned in prod code + [pscustomobject]@{ NormalizedError = $Exception } + } + + # Pass-through for metadata cleanup used in the function + function Remove-CIPPReusableSettingMetadata { param($InputObject) $InputObject } + + . $FunctionPath +} + +Describe 'Invoke-AddIntuneReusableSettingTemplate' { + BeforeEach { + $script:lastEntity = $null + $script:lastForce = $false + $script:logs = @() + } + + It 'creates a reusable setting template with stored metadata' { + $request = [pscustomobject]@{ + Params = @{ CIPPEndpoint = 'AddIntuneReusableSettingTemplate' } + Headers = @{ Authorization = 'Bearer token' } + Body = [pscustomobject]@{ + displayName = 'Template A' + description = 'Template description' + rawJSON = '{"displayName":"Template A"}' + GUID = 'template-a' + } + } + + $response = Invoke-AddIntuneReusableSettingTemplate -Request $request -TriggerMetadata $null + + $response.StatusCode | Should -Be ([System.Net.HttpStatusCode]::OK) + $response.Body.Results | Should -Match 'Successfully added reusable setting template' + $lastEntity.PartitionKey | Should -Be 'IntuneReusableSettingTemplate' + $lastEntity.RowKey | Should -Be 'template-a' + $lastEntity.DisplayName | Should -Be 'Template A' + $lastEntity.Description | Should -Be 'Template description' + $lastEntity.RawJSON | Should -Match '"displayName":"Template A"' + $lastForce | Should -BeTrue + $logs | Should -Not -BeNullOrEmpty + } + + It 'returns InternalServerError when raw JSON is invalid' { + $request = [pscustomobject]@{ + Params = @{ CIPPEndpoint = 'AddIntuneReusableSettingTemplate' } + Headers = @{} + Body = [pscustomobject]@{ + displayName = 'Broken Template' + rawJSON = '{not-json}' + } + } + + $response = Invoke-AddIntuneReusableSettingTemplate -Request $request -TriggerMetadata $null + + $response.StatusCode | Should -Be ([System.Net.HttpStatusCode]::InternalServerError) + $response.Body.Results | Should -Match 'RawJSON is not valid JSON' + } +} diff --git a/Tests/Endpoint/Invoke-ListIntuneReusableSettingTemplates.Tests.ps1 b/Tests/Endpoint/Invoke-ListIntuneReusableSettingTemplates.Tests.ps1 new file mode 100644 index 000000000000..3813c7de946b --- /dev/null +++ b/Tests/Endpoint/Invoke-ListIntuneReusableSettingTemplates.Tests.ps1 @@ -0,0 +1,66 @@ +# Pester tests for Invoke-ListIntuneReusableSettingTemplates +# Validates sorting, parsing, filtering, and sync flags + +BeforeAll { + $RepoRoot = Split-Path -Parent (Split-Path -Parent (Split-Path -Parent $PSCommandPath)) + $FunctionPath = Join-Path $RepoRoot 'Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListIntuneReusableSettingTemplates.ps1' + + class HttpResponseContext { + [int]$StatusCode + [object]$Body + } + + function Get-CippTable { param($tablename) @{} } + function Get-CIPPAzDataTableEntity { param($Filter) $script:lastFilter = $Filter; return $script:tableRows } + + . $FunctionPath +} + +Describe 'Invoke-ListIntuneReusableSettingTemplates' { + BeforeEach { + $script:lastFilter = $null + $script:tableRows = @( + [pscustomobject]@{ + RowKey = 'b-guid' + JSON = '{"DisplayName":"B","RawJSON":"{\"b\":1}","Description":"B desc"}' + Source = 'sync' + SHA = 'abc123' + }, + [pscustomobject]@{ + RowKey = 'a-guid' + RawJSON = '{"displayName":"A"}' + DisplayName = 'A' + Description = 'Entity desc' + } + ) + } + + It 'returns sorted templates with parsed metadata and sync flag' { + $request = [pscustomobject]@{ query = @{} } + + $response = Invoke-ListIntuneReusableSettingTemplates -Request $request -TriggerMetadata $null + + $response.StatusCode | Should -Be ([System.Net.HttpStatusCode]::OK) + $response.Body | Should -HaveCount 2 + $response.Body[0].displayName | Should -Be 'A' + $response.Body[0].description | Should -Be 'Entity desc' + $response.Body[0].GUID | Should -Be 'a-guid' + $response.Body[0].RawJSON | Should -Match '"displayName":"A"' + $response.Body[0].isSynced | Should -BeFalse + + $response.Body[1].displayName | Should -Be 'B' + $response.Body[1].description | Should -Be 'B desc' + $response.Body[1].GUID | Should -Be 'b-guid' + $response.Body[1].isSynced | Should -BeTrue + $lastFilter | Should -Be "PartitionKey eq 'IntuneReusableSettingTemplate'" + } + + It 'filters by ID when provided' { + $request = [pscustomobject]@{ query = @{ ID = 'b-guid' } } + + $response = Invoke-ListIntuneReusableSettingTemplates -Request $request -TriggerMetadata $null + + $response.Body | Should -HaveCount 1 + $response.Body[0].GUID | Should -Be 'b-guid' + } +} diff --git a/Tests/Endpoint/Invoke-ListIntuneReusableSettings.Tests.ps1 b/Tests/Endpoint/Invoke-ListIntuneReusableSettings.Tests.ps1 new file mode 100644 index 000000000000..5a66f62a43a4 --- /dev/null +++ b/Tests/Endpoint/Invoke-ListIntuneReusableSettings.Tests.ps1 @@ -0,0 +1,66 @@ +# Pester tests for Invoke-ListIntuneReusableSettings +# Validates listing and filtering of live reusable settings + +BeforeAll { + $RepoRoot = Split-Path -Parent (Split-Path -Parent (Split-Path -Parent $PSCommandPath)) + $FunctionPath = Join-Path $RepoRoot 'Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListIntuneReusableSettings.ps1' + + class HttpResponseContext { + [int]$StatusCode + [object]$Body + } + + Add-Type -AssemblyName System.Net.Http + + function Write-LogMessage { param($headers, $API, $message, $sev, $LogData) } + function Get-CippException { param($Exception) $Exception } + function New-GraphGETRequest { param($uri, $tenantid) } + + . $FunctionPath +} + +Describe 'Invoke-ListIntuneReusableSettings' { + BeforeEach { + $script:lastUri = $null + } + + It 'returns reusable settings with raw JSON when tenantFilter is provided' { + Mock -CommandName New-GraphGETRequest -MockWith { + @( + [pscustomobject]@{ id = 'one'; displayName = 'A Item'; description = 'A description'; version = 1 }, + [pscustomobject]@{ id = 'two'; displayName = 'Z Item'; description = 'Z description'; version = 2 } + ) + } + + $request = [pscustomobject]@{ query = @{ tenantFilter = 'contoso.onmicrosoft.com' } } + $response = Invoke-ListIntuneReusableSettings -Request $request -TriggerMetadata $null + + $response.StatusCode | Should -Be ([System.Net.HttpStatusCode]::OK) + $response.Body.Count | Should -Be 2 + $response.Body[0].displayName | Should -Be 'A Item' + $response.Body[0].RawJSON | Should -Not -BeNullOrEmpty + } + + It 'requests a specific setting when ID is provided' { + Mock -CommandName New-GraphGETRequest -MockWith { + param($uri, $tenantid) + $script:lastUri = $uri + @([pscustomobject]@{ id = 'beta'; displayName = 'Beta' }) + } + + $request = [pscustomobject]@{ query = @{ tenantFilter = 'contoso.onmicrosoft.com'; ID = 'beta' } } + $response = Invoke-ListIntuneReusableSettings -Request $request -TriggerMetadata $null + + $lastUri | Should -Match '/reusablePolicySettings/beta' + $response.Body.Count | Should -Be 1 + $response.Body[0].displayName | Should -Be 'Beta' + $response.Body[0].RawJSON | Should -Match '"id":"beta"' + } + + It 'returns BadRequest when tenantFilter is missing' { + $request = [pscustomobject]@{ query = @{} } + $response = Invoke-ListIntuneReusableSettings -Request $request -TriggerMetadata $null + + $response.StatusCode | Should -Be ([System.Net.HttpStatusCode]::BadRequest) + } +} diff --git a/Tests/Endpoint/Invoke-RemoveIntuneReusableSetting.Tests.ps1 b/Tests/Endpoint/Invoke-RemoveIntuneReusableSetting.Tests.ps1 new file mode 100644 index 000000000000..da1acacfab22 --- /dev/null +++ b/Tests/Endpoint/Invoke-RemoveIntuneReusableSetting.Tests.ps1 @@ -0,0 +1,64 @@ +# Pester tests for Invoke-RemoveIntuneReusableSetting +# Validates deletion and required parameters + +BeforeAll { + $RepoRoot = Split-Path -Parent (Split-Path -Parent (Split-Path -Parent $PSCommandPath)) + $FunctionPath = Join-Path $RepoRoot 'Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-RemoveIntuneReusableSetting.ps1' + + class HttpResponseContext { + [int]$StatusCode + [object]$Body + } + + function New-GraphPOSTRequest { param($uri, $type, $tenantid) $script:lastDelete = @{ Uri = $uri; Type = $type; Tenant = $tenantid } } + function Write-LogMessage { param($headers, $API, $message, $sev, $LogData) $script:logs += $message } + function Get-CippException { param($Exception) $Exception } + + . $FunctionPath +} + +Describe 'Invoke-RemoveIntuneReusableSetting' { + BeforeEach { + $script:lastDelete = $null + $script:logs = @() + } + + It 'deletes a reusable setting when tenant and ID are provided' { + $request = [pscustomobject]@{ + Params = @{ CIPPEndpoint = 'RemoveIntuneReusableSetting' } + Headers = @{ Authorization = 'token' } + Body = [pscustomobject]@{ + tenantFilter = 'contoso.onmicrosoft.com' + ID = 'setting-1' + DisplayName = 'Setting One' + } + } + + $response = Invoke-RemoveIntuneReusableSetting -Request $request -TriggerMetadata $null + + $response.StatusCode | Should -Be ([System.Net.HttpStatusCode]::OK) + $response.Body.Results | Should -Match 'Deleted Intune reusable setting' + $lastDelete.Type | Should -Be 'DELETE' + $lastDelete.Uri | Should -Match '/reusablePolicySettings/setting-1' + $lastDelete.Tenant | Should -Be 'contoso.onmicrosoft.com' + $logs | Should -Not -BeNullOrEmpty + } + + It 'returns BadRequest when tenantFilter is missing' { + $request = [pscustomobject]@{ Body = [pscustomobject]@{ ID = 'missing-tenant' } } + + $response = Invoke-RemoveIntuneReusableSetting -Request $request -TriggerMetadata $null + + $response.StatusCode | Should -Be ([System.Net.HttpStatusCode]::BadRequest) + $response.Body.Results | Should -Match 'tenantFilter is required' + } + + It 'returns BadRequest when ID is missing' { + $request = [pscustomobject]@{ Body = [pscustomobject]@{ tenantFilter = 'contoso.onmicrosoft.com' } } + + $response = Invoke-RemoveIntuneReusableSetting -Request $request -TriggerMetadata $null + + $response.StatusCode | Should -Be ([System.Net.HttpStatusCode]::BadRequest) + $response.Body.Results | Should -Match 'ID is required' + } +} diff --git a/Tests/Endpoint/Invoke-RemoveIntuneReusableSettingTemplate.Tests.ps1 b/Tests/Endpoint/Invoke-RemoveIntuneReusableSettingTemplate.Tests.ps1 new file mode 100644 index 000000000000..e39bb1dfa0b7 --- /dev/null +++ b/Tests/Endpoint/Invoke-RemoveIntuneReusableSettingTemplate.Tests.ps1 @@ -0,0 +1,53 @@ +# Pester tests for Invoke-RemoveIntuneReusableSettingTemplate +# Validates template removal and error handling + +BeforeAll { + $RepoRoot = Split-Path -Parent (Split-Path -Parent (Split-Path -Parent $PSCommandPath)) + $FunctionPath = Join-Path $RepoRoot 'Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-RemoveIntuneReusableSettingTemplate.ps1' + + class HttpResponseContext { + [int]$StatusCode + [object]$Body + } + + function Get-CippTable { param($tablename) @{} } + function Get-CIPPAzDataTableEntity { param($Filter, $Property) return [pscustomobject]@{ PartitionKey = 'IntuneReusableSettingTemplate'; RowKey = 'template-x' } } + function Remove-AzDataTableEntity { param([switch]$Force, $Entity) $script:lastRemoved = $Entity; $script:lastForce = $Force } + function Write-LogMessage { param($Headers, $API, $message, $sev, $LogData) $script:logs += $message } + function Get-CippException { param($Exception) [pscustomobject]@{ NormalizedError = $Exception } } + + . $FunctionPath +} + +Describe 'Invoke-RemoveIntuneReusableSettingTemplate' { + BeforeEach { + $script:lastRemoved = $null + $script:lastForce = $false + $script:logs = @() + } + + It 'removes a reusable setting template when ID is provided' { + $request = [pscustomobject]@{ + Params = @{ CIPPEndpoint = 'RemoveIntuneReusableSettingTemplate' } + Headers = @{ Authorization = 'token' } + Query = @{ ID = 'template-x' } + } + + $response = Invoke-RemoveIntuneReusableSettingTemplate -Request $request -TriggerMetadata $null + + $response.StatusCode | Should -Be ([System.Net.HttpStatusCode]::OK) + $response.Body.Results | Should -Match 'Removed Intune reusable setting template with ID template-x' + $lastRemoved.RowKey | Should -Be 'template-x' + $lastForce | Should -BeTrue + $logs | Should -Not -BeNullOrEmpty + } + + It 'returns InternalServerError when ID is missing' { + $request = [pscustomobject]@{ Params = @{}; Query = @{}; Body = [pscustomobject]@{} } + + $response = Invoke-RemoveIntuneReusableSettingTemplate -Request $request -TriggerMetadata $null + + $response.StatusCode | Should -Be ([System.Net.HttpStatusCode]::InternalServerError) + $response.Body.Results | Should -Match 'You must supply an ID' + } +} diff --git a/Tests/Standards/Invoke-CIPPStandardReusableSettingsTemplate.Tests.ps1 b/Tests/Standards/Invoke-CIPPStandardReusableSettingsTemplate.Tests.ps1 new file mode 100644 index 000000000000..9777672690c5 --- /dev/null +++ b/Tests/Standards/Invoke-CIPPStandardReusableSettingsTemplate.Tests.ps1 @@ -0,0 +1,152 @@ +# Pester tests for Invoke-CIPPStandardReusableSettingsTemplate +# Validates licensing guard, remediation flows, alerting, and reporting + +BeforeAll { + $RepoRoot = Split-Path -Parent (Split-Path -Parent (Split-Path -Parent $PSCommandPath)) + $StandardPath = Join-Path $RepoRoot 'Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardReusableSettingsTemplate.ps1' + + function Test-CIPPStandardLicense { param($StandardName, $TenantFilter, $RequiredCapabilities) } + function Get-CippTable { param($tablename) } + function New-GraphGETRequest { param($uri, $tenantid) } + function Get-CippAzDataTableEntity { param($Table, $Filter) } + function Compare-CIPPIntuneObject { param($ReferenceObject, $DifferenceObject, $compareType) } + function New-GraphPOSTRequest { param($uri, $tenantid, $type, $body) } + function Write-LogMessage { param($API, $tenant, $message, $sev) } + function Write-StandardsAlert { param($message, $object, $tenant, $standardName, $standardId) } + function Set-CIPPStandardsCompareField { param($FieldName, $FieldValue, $TenantFilter) } + function Get-NormalizedError { param($Message) $Message } + + . $StandardPath +} + +Describe 'Invoke-CIPPStandardReusableSettingsTemplate' { + $tenant = 'contoso.onmicrosoft.com' + + BeforeEach { + $script:compareFields = @() + $script:alerts = @() + $script:logs = @() + $script:updateCalls = 0 + $script:createCalls = 0 + + Mock -CommandName Test-CIPPStandardLicense -MockWith { $true } + Mock -CommandName Get-CippTable -MockWith { @{ Table = 'templates' } } + Mock -CommandName New-GraphGETRequest -MockWith { @() } + Mock -CommandName Get-CippAzDataTableEntity -MockWith { + @([pscustomobject]@{ + RowKey = 'template-existing' + JSON = '{"DisplayName":"Reusable A","RawJSON":"{\"displayName\":\"Reusable A\"}"}' + RawJSON = '{"displayName":"Reusable A"}' + DisplayName = 'Reusable A' + }) + } + Mock -CommandName Compare-CIPPIntuneObject -MockWith { $null } + Mock -CommandName New-GraphPOSTRequest -MockWith { + param($uri, $tenantid, $type, $body) + if ($type -eq 'PUT') { $script:updateCalls++ } else { $script:createCalls++ } + } + Mock -CommandName Write-LogMessage -MockWith { + param($API, $tenant, $message, $sev) + $script:logs += @{ Message = $message; Sev = $sev } + } + Mock -CommandName Write-StandardsAlert -MockWith { + param($message, $object, $tenant, $standardName, $standardId) + $script:alerts += @{ Message = $message; Object = $object; Standard = $standardName; Id = $standardId } + } + Mock -CommandName Set-CIPPStandardsCompareField -MockWith { + param($FieldName, $FieldValue, $TenantFilter) + $script:compareFields += @{ Field = $FieldName; Value = $FieldValue; Tenant = $TenantFilter } + } + } + + It 'sets compare fields and exits when license requirement fails' { + Mock -CommandName Test-CIPPStandardLicense -MockWith { $false } + + $settings = @( + [pscustomobject]@{ TemplateList = [pscustomobject]@{ value = 'template-one' } }, + [pscustomobject]@{ TemplateList = [pscustomobject]@{ value = 'template-two' } } + ) + + $result = Invoke-CIPPStandardReusableSettingsTemplate -Tenant $tenant -Settings $settings + + $result | Should -BeTrue + $compareFields.Field | Should -Contain 'standards.ReusableSettingsTemplate.template-one' + $compareFields.Field | Should -Contain 'standards.ReusableSettingsTemplate.template-two' + Should -Invoke Get-CippAzDataTableEntity -Times 0 + Should -Invoke New-GraphGETRequest -Times 0 + } + + It 'creates missing reusable settings when remediate is enabled' { + Mock -CommandName Get-CippAzDataTableEntity -MockWith { + @([pscustomobject]@{ + RowKey = 'template-create' + JSON = '{"DisplayName":"Reusable Create","RawJSON":"{\"displayName\":\"Reusable Create\"}"}' + RawJSON = '{"displayName":"Reusable Create"}' + DisplayName = 'Reusable Create' + }) + } + + $settings = @( + [pscustomobject]@{ TemplateList = [pscustomobject]@{ value = 'template-create' }; remediate = $true; alert = $false; report = $false } + ) + + Invoke-CIPPStandardReusableSettingsTemplate -Tenant $tenant -Settings $settings + + $createCalls | Should -Be 1 + Should -Invoke New-GraphPOSTRequest -ParameterFilter { $type -eq 'POST' -and $uri -like '*reusablePolicySettings' } -Times 1 + $compareFields | Should -BeNullOrEmpty + } + + It 'updates existing reusable settings when a mismatch is found' { + Mock -CommandName New-GraphGETRequest -MockWith { + @([pscustomobject]@{ id = 'existing-1'; displayName = 'Reusable A'; version = 1 }) + } + Mock -CommandName Compare-CIPPIntuneObject -MockWith { [pscustomobject]@{ Difference = 'changed' } } + + $settings = @( + [pscustomobject]@{ TemplateList = [pscustomobject]@{ value = 'template-existing' }; remediate = $true; alert = $false; report = $false } + ) + + Invoke-CIPPStandardReusableSettingsTemplate -Tenant $tenant -Settings $settings + + $updateCalls | Should -Be 1 + Should -Invoke New-GraphPOSTRequest -ParameterFilter { $type -eq 'PUT' -and $uri -like '*reusablePolicySettings/existing-1' } -Times 1 + Should -Invoke New-GraphPOSTRequest -ParameterFilter { $type -eq 'POST' } -Times 0 + } + + It 'writes standards alerts when alerting is enabled and drift exists' { + Mock -CommandName New-GraphGETRequest -MockWith { + @([pscustomobject]@{ id = 'existing-2'; displayName = 'Reusable Alert' }) + } + Mock -CommandName Compare-CIPPIntuneObject -MockWith { @{ Difference = 'drift' } } + + $settings = @( + [pscustomobject]@{ TemplateList = [pscustomobject]@{ value = 'template-existing' }; remediate = $false; alert = $true; report = $false } + ) + + Invoke-CIPPStandardReusableSettingsTemplate -Tenant $tenant -Settings $settings + + $alerts | Should -HaveCount 1 + $alerts[0].Message | Should -Match 'Reusable setting Reusable A does not match' + $alerts[0].Standard | Should -Be 'ReusableSettingsTemplate' + $logs.Where({ $_.Message -like '*out of compliance*' }).Count | Should -Be 1 + } + + It 'logs compliance and reports true when no differences are found' { + Mock -CommandName New-GraphGETRequest -MockWith { + @([pscustomobject]@{ id = 'existing-3'; displayName = 'Reusable A' }) + } + Mock -CommandName Compare-CIPPIntuneObject -MockWith { $null } + + $settings = @( + [pscustomobject]@{ TemplateList = [pscustomobject]@{ value = 'template-existing' }; remediate = $false; alert = $true; report = $true } + ) + + Invoke-CIPPStandardReusableSettingsTemplate -Tenant $tenant -Settings $settings + + $logs.Where({ $_.Message -like '*is compliant.*' }).Count | Should -Be 1 + $compareFields | Should -HaveCount 1 + $compareFields[0].Value | Should -BeTrue + Should -Invoke -CommandName Write-StandardsAlert -Times 0 + } +} From 8e69843ff99d358b67d9ecdf9c4a11e3a1d5bd63 Mon Sep 17 00:00:00 2001 From: Logan Cook <2997336+MWG-Logan@users.noreply.github.com> Date: Mon, 12 Jan 2026 16:00:38 -0500 Subject: [PATCH 126/503] fix(api): boost BRR on ID filtering in ListIntuneReusableSettingTemplates --- .../MEM/Invoke-ListIntuneReusableSettingTemplates.ps1 | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListIntuneReusableSettingTemplates.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListIntuneReusableSettingTemplates.ps1 index 941142e43fc7..93dd3b986ce3 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListIntuneReusableSettingTemplates.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListIntuneReusableSettingTemplates.ps1 @@ -10,6 +10,12 @@ function Invoke-ListIntuneReusableSettingTemplates { $Table = Get-CippTable -tablename 'templates' $Filter = "PartitionKey eq 'IntuneReusableSettingTemplate'" + + if ($Request.query.ID) { + $EscapedId = $Request.query.ID -replace "'", "''" # escape OData quotes + $Filter = "PartitionKey eq 'IntuneReusableSettingTemplate' and RowKey eq '$EscapedId'" + } + $RawTemplates = Get-CIPPAzDataTableEntity @Table -Filter $Filter $Templates = foreach ($Item in $RawTemplates) { @@ -32,10 +38,6 @@ function Invoke-ListIntuneReusableSettingTemplates { $Templates = $Templates | Sort-Object -Property displayName - if ($Request.query.ID) { - $Templates = $Templates | Where-Object -Property GUID -EQ $Request.query.ID - } - return ([HttpResponseContext]@{ StatusCode = [System.Net.HttpStatusCode]::OK Body = @($Templates) From f88cad05854e5e8133f3bda45f0a4764947dc6cf Mon Sep 17 00:00:00 2001 From: Logan Cook <2997336+MWG-Logan@users.noreply.github.com> Date: Mon, 12 Jan 2026 16:02:53 -0500 Subject: [PATCH 127/503] fix(api): remove unnecessary initialization of Settings array --- .../Endpoint/MEM/Invoke-ListIntuneReusableSettings.ps1 | 1 - 1 file changed, 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListIntuneReusableSettings.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListIntuneReusableSettings.ps1 index 319a79b8956c..20f2712ae3c0 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListIntuneReusableSettings.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListIntuneReusableSettings.ps1 @@ -57,7 +57,6 @@ function Invoke-ListIntuneReusableSettings { $ErrorMessage = Get-CippException -Exception $_ $logMessage = "Failed to retrieve reusable policy settings: $($ErrorMessage.NormalizedError)" Write-LogMessage -headers $Headers -API $APIName -message $logMessage -Sev Error -LogData $ErrorMessage - $Settings = @() $StatusCode = [System.Net.HttpStatusCode]::InternalServerError return ([HttpResponseContext]@{ StatusCode = $StatusCode From 4b4018d2f659c1aeb89d1e7e4c6aa237246e0e8b Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Mon, 12 Jan 2026 22:46:24 +0100 Subject: [PATCH 128/503] SecDefaultsDisabled --- .../Get-CIPPAlertSecDefaultsDisabled.ps1 | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 Modules/CIPPCore/Public/Alerts/Get-CIPPAlertSecDefaultsDisabled.ps1 diff --git a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertSecDefaultsDisabled.ps1 b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertSecDefaultsDisabled.ps1 new file mode 100644 index 000000000000..104cf17e03fa --- /dev/null +++ b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertSecDefaultsDisabled.ps1 @@ -0,0 +1,35 @@ +function Get-CIPPAlertSecDefaultsDisabled { + <# + .FUNCTIONALITY + Entrypoint + #> + [CmdletBinding()] + param ( + [Parameter(Mandatory = $false)] + [Alias('input')] + $InputValue, + $TenantFilter + ) + + try { + # Check if Security Defaults is disabled + $SecDefaults = (New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/policies/identitySecurityDefaultsEnforcementPolicy' -tenantid $TenantFilter) + + if ($SecDefaults.isEnabled -eq $false) { + # Security Defaults is disabled, now check if there are any CA policies + $CAPolicies = (New-GraphGetRequest -uri 'https://graph.microsoft.com/v1.0/identity/conditionalAccess/policies' -tenantid $TenantFilter) + + if (!$CAPolicies -or $CAPolicies.Count -eq 0) { + # Security Defaults is off AND no CA policies exist + $AlertData = [PSCustomObject]@{ + Message = 'Security Defaults is disabled and no Conditional Access policies are configured. This tenant has no baseline security protection.' + Tenant = $TenantFilter + } + + Write-AlertTrace -cmdletName $MyInvocation.MyCommand -tenantFilter $TenantFilter -data $AlertData + } + } + } catch { + Write-AlertMessage -tenant $($TenantFilter) -message "Security Defaults Disabled Alert: Error occurred: $(Get-NormalizedError -message $_.Exception.message)" + } +} From 93ba2882bd0c6fe1103a8a367c855c56af24b72e Mon Sep 17 00:00:00 2001 From: John Duprey Date: Mon, 12 Jan 2026 18:03:21 -0500 Subject: [PATCH 129/503] ActivityBasedTimeout to DefaultPlatformRestrictions Updated multiple Invoke-CIPPStandard* scripts to include CurrentValue and ExpectedValue objects when calling Set-CIPPStandardsCompareField. This enhances standards reporting by providing more detailed information about the current and expected configuration states for each standard. --- ...nvoke-CIPPStandardActivityBasedTimeout.ps1 | 6 +- .../Standards/Invoke-CIPPStandardAddDKIM.ps1 | 13 ++- .../Invoke-CIPPStandardAddDMARCToMOERA.ps1 | 5 +- .../Invoke-CIPPStandardAnonReportDisable.ps1 | 5 +- .../Invoke-CIPPStandardAntiPhishPolicy.ps1 | 38 ++++++++ .../Invoke-CIPPStandardAntiSpamSafeList.ps1 | 8 +- .../Invoke-CIPPStandardAppDeploy.ps1 | 7 +- ...e-CIPPStandardAssignmentFilterTemplate.ps1 | 17 ++-- .../Invoke-CIPPStandardAtpPolicyForO365.ps1 | 9 +- .../Standards/Invoke-CIPPStandardAuditLog.ps1 | 9 +- ...CIPPStandardAuthMethodsPolicyMigration.ps1 | 7 +- ...Invoke-CIPPStandardAuthMethodsSettings.ps1 | 13 ++- .../Invoke-CIPPStandardAutoAddProxy.ps1 | 10 +- .../Invoke-CIPPStandardAutoArchive.ps1 | 9 +- .../Invoke-CIPPStandardAutoExpandArchive.ps1 | 10 +- .../Invoke-CIPPStandardAutopilotProfile.ps1 | 29 +++++- ...Invoke-CIPPStandardAutopilotStatusPage.ps1 | 16 +++- ...IPPStandardBitLockerKeysForOwnedDevice.ps1 | 17 +++- .../Standards/Invoke-CIPPStandardBookings.ps1 | 9 +- .../Standards/Invoke-CIPPStandardBranding.ps1 | 22 ++++- .../Invoke-CIPPStandardCloudMessageRecall.ps1 | 20 ++-- ...e-CIPPStandardCustomBannedPasswordList.ps1 | 10 +- ...IPPStandardDefaultPlatformRestrictions.ps1 | 95 +++++++++++-------- .../Invoke-CIPPStandardallowOAuthTokens.ps1 | 4 +- .../Invoke-CIPPStandardallowOTPTokens.ps1 | 5 +- .../Invoke-CIPPStandardcalDefault.ps1 | 5 +- 26 files changed, 305 insertions(+), 93 deletions(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardActivityBasedTimeout.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardActivityBasedTimeout.ps1 index 0bdf7e92a3eb..d89d9804bc15 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardActivityBasedTimeout.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardActivityBasedTimeout.ps1 @@ -53,7 +53,9 @@ function Invoke-CIPPStandardActivityBasedTimeout { Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the ActivityBasedTimeout state for $Tenant. Error: $($ErrorMessage.NormalizedError)" -Sev Error -LogData $ErrorMessage return } - $StateIsCorrect = if ($CurrentState.definition -like "*$timeout*") { $true } else { $false } + $CurrentValue = ($CurrentState.definition | ConvertFrom-Json -ErrorAction SilentlyContinue).activitybasedtimeoutpolicy.ApplicationPolicies | Select-Object -First 1 -Property WebSessionIdleTimeout + $StateIsCorrect = if ($CurrentValue.WebSessionIdleTimeout -eq $timeout) { $true } else { $false } + $ExpectedValue = [PSCustomObject]@{WebSessionIdleTimeout = $timeout } if ($Settings.remediate -eq $true) { try { @@ -97,7 +99,7 @@ function Invoke-CIPPStandardActivityBasedTimeout { } if ($Settings.report -eq $true) { - Set-CIPPStandardsCompareField -FieldName 'standards.ActivityBasedTimeout' -FieldValue $StateIsCorrect -TenantFilter $Tenant + Set-CIPPStandardsCompareField -FieldName 'standards.ActivityBasedTimeout' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant Add-CIPPBPAField -FieldName 'ActivityBasedTimeout' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $Tenant } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAddDKIM.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAddDKIM.ps1 index 4485efc83044..fbaf8ce3251a 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAddDKIM.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAddDKIM.ps1 @@ -112,6 +112,17 @@ function Invoke-CIPPStandardAddDKIM { $NewDomains = $AllDomains | Where-Object { $DKIM.Domain -notcontains $_ } $SetDomains = $DKIM | Where-Object { $AllDomains -contains $_.Domain -and $_.Enabled -eq $false } + $MissingDKIM = [System.Collections.Generic.List[string]]::new() + if ($null -ne $NewDomains) { + $MissingDKIM.AddRange($NewDomains) + } + if ($null -ne $SetDomains) { + $MissingDKIM.AddRange($SetDomains.Domain) + } + + $CurrentValue = if ($MissingDKIM.Count -eq 0) { [PSCustomObject]@{'state' = 'Configured correctly' } } else { [PSCustomObject]@{'MissingDKIM' = $MissingDKIM } } + $ExpectedValue = [PSCustomObject]@{'state' = 'Configured correctly' } + if ($Settings.remediate -eq $true) { if ($null -eq $NewDomains -and $null -eq $SetDomains) { @@ -179,7 +190,7 @@ function Invoke-CIPPStandardAddDKIM { if ($Settings.report -eq $true) { $DKIMState = if ($null -eq $NewDomains -and $null -eq $SetDomains) { $true } else { $SetDomains, $NewDomains } - Set-CIPPStandardsCompareField -FieldName 'standards.AddDKIM' -FieldValue $DKIMState -TenantFilter $tenant + Set-CIPPStandardsCompareField -FieldName 'standards.AddDKIM' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $tenant Add-CIPPBPAField -FieldName 'DKIM' -FieldValue $DKIMState -StoreAs bool -Tenant $tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAddDMARCToMOERA.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAddDMARCToMOERA.ps1 index 1b7e98d18c06..288053375351 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAddDMARCToMOERA.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAddDMARCToMOERA.ps1 @@ -92,6 +92,9 @@ function Invoke-CIPPStandardAddDMARCToMOERA { } # Check if match is true and there is only one DMARC record for each domain $StateIsCorrect = $false -notin $CurrentInfo.Match -and $CurrentInfo.Count -eq $Domains.Count + + $CurrentValue = if ($StateIsCorrect) { [PSCustomObject]@{'state' = 'Configured correctly' } } else { [PSCustomObject]@{'MissingDMARC' = @($CurrentInfo | Where-Object -Property Match -EQ $false | Select-Object -ExpandProperty DomainName) } } + $ExpectedValue = [PSCustomObject]@{'state' = 'Configured correctly' } } catch { $ErrorMessage = Get-CippException -Exception $_ if ($_.Exception.Message -like '*403*') { @@ -156,7 +159,7 @@ function Invoke-CIPPStandardAddDMARCToMOERA { } if ($Settings.report -eq $true) { - set-CIPPStandardsCompareField -FieldName 'standards.AddDMARCToMOERA' -FieldValue $StateIsCorrect -TenantFilter $Tenant + Set-CIPPStandardsCompareField -FieldName 'standards.AddDMARCToMOERA' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant Add-CIPPBPAField -FieldName 'AddDMARCToMOERA' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAnonReportDisable.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAnonReportDisable.ps1 index 05d16fd877de..6d58254efa8d 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAnonReportDisable.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAnonReportDisable.ps1 @@ -41,6 +41,9 @@ function Invoke-CIPPStandardAnonReportDisable { return } + $CurrentValue = $CurrentInfo | Select-Object -Property displayConcealedNames + $ExpectedValue = [PSCustomObject]@{displayConcealedNames = $false } + if ($Settings.remediate -eq $true) { if ($CurrentInfo.displayConcealedNames -eq $false) { @@ -66,7 +69,7 @@ function Invoke-CIPPStandardAnonReportDisable { } if ($Settings.report -eq $true) { $StateIsCorrect = $CurrentInfo.displayConcealedNames ? $false : $true - Set-CIPPStandardsCompareField -FieldName 'standards.AnonReportDisable' -FieldValue $StateIsCorrect -TenantFilter $tenant + Set-CIPPStandardsCompareField -FieldName 'standards.AnonReportDisable' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $tenant Add-CIPPBPAField -FieldName 'AnonReport' -FieldValue $CurrentInfo.displayConcealedNames -StoreAs bool -Tenant $tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAntiPhishPolicy.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAntiPhishPolicy.ps1 index 34a1fa83cdd9..79f02f28e15f 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAntiPhishPolicy.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAntiPhishPolicy.ps1 @@ -115,6 +115,33 @@ function Invoke-CIPPStandardAntiPhishPolicy { ($CurrentState.EnableTargetedDomainsProtection -eq $true) -and ($CurrentState.EnableTargetedUserProtection -eq $true) -and ($CurrentState.EnableOrganizationDomainsProtection -eq $true) + + $CurrentValue = $CurrentState | Select-Object Name, Enabled, PhishThresholdLevel, EnableMailboxIntelligence, EnableMailboxIntelligenceProtection, EnableSpoofIntelligence, EnableFirstContactSafetyTips, EnableSimilarUsersSafetyTips, EnableSimilarDomainsSafetyTips, EnableUnusualCharactersSafetyTips, EnableUnauthenticatedSender, EnableViaTag, AuthenticationFailAction, SpoofQuarantineTag, MailboxIntelligenceProtectionAction, MailboxIntelligenceQuarantineTag, TargetedUserProtectionAction, TargetedUserQuarantineTag, TargetedDomainProtectionAction, TargetedDomainQuarantineTag, EnableOrganizationDomainsProtection, EnableTargetedDomainsProtection, EnableTargetedUserProtection + $ExpectedValue = [PSCustomObject]@{ + Name = $PolicyName + Enabled = $true + PhishThresholdLevel = $Settings.PhishThresholdLevel + EnableMailboxIntelligence = $true + EnableMailboxIntelligenceProtection = $true + EnableSpoofIntelligence = $true + EnableFirstContactSafetyTips = $Settings.EnableFirstContactSafetyTips + EnableSimilarUsersSafetyTips = $Settings.EnableSimilarUsersSafetyTips + EnableSimilarDomainsSafetyTips = $Settings.EnableSimilarDomainsSafetyTips + EnableUnusualCharactersSafetyTips = $Settings.EnableUnusualCharactersSafetyTips + EnableUnauthenticatedSender = $true + EnableViaTag = $true + AuthenticationFailAction = $Settings.AuthenticationFailAction + SpoofQuarantineTag = $Settings.SpoofQuarantineTag + MailboxIntelligenceProtectionAction = $Settings.MailboxIntelligenceProtectionAction + MailboxIntelligenceQuarantineTag = $Settings.MailboxIntelligenceQuarantineTag + TargetedUserProtectionAction = $Settings.TargetedUserProtectionAction + TargetedUserQuarantineTag = $Settings.TargetedUserQuarantineTag + TargetedDomainProtectionAction = $Settings.TargetedDomainProtectionAction + TargetedDomainQuarantineTag = $Settings.TargetedDomainQuarantineTag + EnableTargetedDomainsProtection = $true + EnableTargetedUserProtection = $true + EnableOrganizationDomainsProtection = $true + } } else { $StateIsCorrect = ($CurrentState.Name -eq $PolicyName) -and ($CurrentState.Enabled -eq $true) -and @@ -124,6 +151,17 @@ function Invoke-CIPPStandardAntiPhishPolicy { ($CurrentState.EnableViaTag -eq $true) -and ($CurrentState.AuthenticationFailAction -eq $Settings.AuthenticationFailAction) -and ($CurrentState.SpoofQuarantineTag -eq $Settings.SpoofQuarantineTag) + $CurrentValue = $CurrentState | Select-Object Name, Enabled, EnableSpoofIntelligence, EnableFirstContactSafetyTips, EnableUnauthenticatedSender, EnableViaTag, AuthenticationFailAction, SpoofQuarantineTag + $ExpectedValue = [PSCustomObject]@{ + Name = $PolicyName + Enabled = $true + EnableSpoofIntelligence = $true + EnableFirstContactSafetyTips= $Settings.EnableFirstContactSafetyTips + EnableUnauthenticatedSender = $true + EnableViaTag = $true + AuthenticationFailAction = $Settings.AuthenticationFailAction + SpoofQuarantineTag = $Settings.SpoofQuarantineTag + } } $AcceptedDomains = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-AcceptedDomain' diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAntiSpamSafeList.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAntiSpamSafeList.ps1 index 768dd1c9628c..614a1a7f036c 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAntiSpamSafeList.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAntiSpamSafeList.ps1 @@ -56,9 +56,15 @@ function Invoke-CIPPStandardAntiSpamSafeList { } $WantedState = $State -eq $true ? $true : $false $StateIsCorrect = if ($CurrentState -eq $WantedState) { $true } else { $false } + $CurrentValue = [PSCustomObject]@{ + EnableSafeList = $CurrentState + } + $ExpectedValue = [PSCustomObject]@{ + EnableSafeList = $WantedState + } if ($Settings.report -eq $true) { - Set-CIPPStandardsCompareField -FieldName 'standards.AntiSpamSafeList' -FieldValue $StateIsCorrect -TenantFilter $Tenant + Set-CIPPStandardsCompareField -FieldName 'standards.AntiSpamSafeList' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant Add-CIPPBPAField -FieldName 'AntiSpamSafeList' -FieldValue $CurrentState -StoreAs bool -Tenant $Tenant } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAppDeploy.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAppDeploy.ps1 index af96e1e1ab7c..c01aade26629 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAppDeploy.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAppDeploy.ps1 @@ -39,6 +39,8 @@ function Invoke-CIPPStandardAppDeploy { $AppExists = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/servicePrincipals?$top=999' -tenantid $Tenant $Mode = $Settings.mode ?? 'copy' + $ExpectedValue = [PSCustomObject]@{ state = 'Configured correctly' } + if ($Mode -eq 'template') { # For template mode, we need to check each template individually # since Gallery Templates and Enterprise Apps have different deployment methods @@ -105,6 +107,9 @@ function Invoke-CIPPStandardAppDeploy { } } } + + $CurrentValue = if ($MissingApps.Count -eq 0) { [PSCustomObject]@{'state' = 'Configured correctly' } } else { [PSCustomObject]@{'MissingApps' = $MissingApps } } + if ($Settings.remediate -eq $true) { if ($Mode -eq 'copy') { foreach ($App in $AppsToAdd) { @@ -279,7 +284,7 @@ function Invoke-CIPPStandardAppDeploy { if ($Settings.report -eq $true) { $StateIsCorrect = $MissingApps.Count -eq 0 ? $true : @{ 'Missing Apps' = $MissingApps -join ',' } - Set-CIPPStandardsCompareField -FieldName 'standards.AppDeploy' -FieldValue $StateIsCorrect -TenantFilter $tenant + Set-CIPPStandardsCompareField -FieldName 'standards.AppDeploy' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $tenant Add-CIPPBPAField -FieldName 'AppDeploy' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $tenant } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAssignmentFilterTemplate.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAssignmentFilterTemplate.ps1 index 5cececa9417b..8d371752daa4 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAssignmentFilterTemplate.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAssignmentFilterTemplate.ps1 @@ -40,6 +40,15 @@ function Invoke-CIPPStandardAssignmentFilterTemplate { $Filter = "PartitionKey eq 'AssignmentFilterTemplate' and (RowKey eq '$($Settings.TemplateList.value -join "' or RowKey eq '")')" $AssignmentFilterTemplates = (Get-CIPPAzDataTableEntity @Table -Filter $Filter).JSON | ConvertFrom-Json + $ExpectedValue = [PSCustomObject]@{ state = 'Configured correctly' } + $MissingFilters = $AssignmentFilterTemplates | Where-Object { + $CheckExisting = $existingFilters | Where-Object { $_.displayName -eq $_.displayName } + if (!$CheckExisting) { + $_.displayName + } + } + $CurrentValue = if ($MissingFilters.Count -eq 0) { [PSCustomObject]@{'state' = 'Configured correctly' } } else { [PSCustomObject]@{'MissingFilters' = @($MissingFilters) } } + if ($Settings.remediate -eq $true) { Write-Host "Settings: $($Settings.TemplateList | ConvertTo-Json)" foreach ($Template in $AssignmentFilterTemplates) { @@ -115,12 +124,6 @@ function Invoke-CIPPStandardAssignmentFilterTemplate { } } - if ($MissingFilters.Count -eq 0) { - $fieldValue = $true - } else { - $fieldValue = $MissingFilters -join ', ' - } - - Set-CIPPStandardsCompareField -FieldName 'standards.AssignmentFilterTemplate' -FieldValue $fieldValue -Tenant $Tenant + Set-CIPPStandardsCompareField -FieldName 'standards.AssignmentFilterTemplate' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -Tenant $Tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAtpPolicyForO365.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAtpPolicyForO365.ps1 index 4eb08c0cf3fa..3bf5fd8ba0d7 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAtpPolicyForO365.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAtpPolicyForO365.ps1 @@ -51,6 +51,13 @@ function Invoke-CIPPStandardAtpPolicyForO365 { ($CurrentState.EnableSafeDocs -eq $true) -and ($CurrentState.AllowSafeDocsOpen -eq $Settings.AllowSafeDocsOpen) + $CurrentValue = $CurrentState | Select-Object EnableATPForSPOTeamsODB, EnableSafeDocs, AllowSafeDocsOpen + $ExpectedValue = [PSCustomObject]@{ + EnableATPForSPOTeamsODB = $true + EnableSafeDocs = $true + AllowSafeDocsOpen = $Settings.AllowSafeDocsOpen + } + if ($Settings.remediate -eq $true) { if ($StateIsCorrect -eq $true) { Write-LogMessage -API 'Standards' -tenant $Tenant -message 'Atp Policy For O365 already set.' -sev Info @@ -83,7 +90,7 @@ function Invoke-CIPPStandardAtpPolicyForO365 { if ($Settings.report -eq $true) { $state = $StateIsCorrect -eq $true ? $true : $CurrentState - Set-CIPPStandardsCompareField -FieldName 'standards.AtpPolicyForO365' -FieldValue $state -TenantFilter $tenant + Set-CIPPStandardsCompareField -FieldName 'standards.AtpPolicyForO365' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $tenant Add-CIPPBPAField -FieldName 'AtpPolicyForO365' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $tenant } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAuditLog.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAuditLog.ps1 index 3d8ef1325f2e..4d1c5a4d1b68 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAuditLog.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAuditLog.ps1 @@ -46,6 +46,13 @@ function Invoke-CIPPStandardAuditLog { Write-Host ($Settings | ConvertTo-Json) $AuditLogEnabled = [bool](New-ExoRequest -tenantid $Tenant -cmdlet 'Get-AdminAuditLogConfig' -Select UnifiedAuditLogIngestionEnabled).UnifiedAuditLogIngestionEnabled + $CurrentValue = [PSCustomObject]@{ + UnifiedAuditLogIngestionEnabled = $AuditLogEnabled + } + $ExpectedValue = [PSCustomObject]@{ + UnifiedAuditLogIngestionEnabled = $true + } + if ($Settings.remediate -eq $true) { Write-Host 'Time to remediate' @@ -85,7 +92,7 @@ function Invoke-CIPPStandardAuditLog { if ($Settings.report -eq $true) { $state = $AuditLogEnabled -eq $true ? $true : $AuditLogEnabled - Set-CIPPStandardsCompareField -FieldName 'standards.AuditLog' -FieldValue $state -TenantFilter $Tenant + Set-CIPPStandardsCompareField -FieldName 'standards.AuditLog' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant Add-CIPPBPAField -FieldName 'AuditLog' -FieldValue $AuditLogEnabled -StoreAs bool -Tenant $tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAuthMethodsPolicyMigration.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAuthMethodsPolicyMigration.ps1 index 733571d2ceba..cd3f5126a43d 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAuthMethodsPolicyMigration.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAuthMethodsPolicyMigration.ps1 @@ -43,6 +43,11 @@ function Invoke-CIPPStandardAuthMethodsPolicyMigration { throw 'Failed to retrieve current authentication methods policy information' } + $CurrentValue = $CurrentInfo | Select-Object policyMigrationState + $ExpectedValue = [PSCustomObject]@{ + policyMigrationState = 'migrationComplete' + } + if ($Settings.remediate -eq $true) { if ($CurrentInfo.policyMigrationState -eq 'migrationComplete' -or $null -eq $CurrentInfo.policyMigrationState) { Write-LogMessage -API 'Standards' -tenant $tenant -message 'Authentication methods policy migration is already complete.' -sev Info @@ -66,7 +71,7 @@ function Invoke-CIPPStandardAuthMethodsPolicyMigration { if ($Settings.report -eq $true) { $migrationComplete = $CurrentInfo.policyMigrationState -eq 'migrationComplete' -or $null -eq $CurrentInfo.policyMigrationState - Set-CIPPStandardsCompareField -FieldName 'standards.AuthMethodsPolicyMigration' -FieldValue $migrationComplete -TenantFilter $tenant + Set-CIPPStandardsCompareField -FieldName 'standards.AuthMethodsPolicyMigration' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $tenant Add-CIPPBPAField -FieldName 'AuthMethodsPolicyMigration' -FieldValue $migrationComplete -StoreAs bool -Tenant $tenant } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAuthMethodsSettings.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAuthMethodsSettings.ps1 index da292eff3ae1..cd0332008211 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAuthMethodsSettings.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAuthMethodsSettings.ps1 @@ -36,7 +36,6 @@ function Invoke-CIPPStandardAuthMethodsSettings { param($Tenant, $Settings) - Write-Host 'Time to run' # Get current authentication methods policy try { $CurrentPolicy = New-GraphGetRequest -Uri 'https://graph.microsoft.com/beta/policies/authenticationMethodsPolicy' -tenantid $Tenant -AsApp $true @@ -61,7 +60,14 @@ function Invoke-CIPPStandardAuthMethodsSettings { return } - + $CurrentValue = [PSCustomObject]@{ + reportSuspiciousActivitySettings = $CurrentPolicy.reportSuspiciousActivitySettings.state + systemCredentialPreferences = $CurrentPolicy.systemCredentialPreferences.state + } + $ExpectedValue = [PSCustomObject]@{ + reportSuspiciousActivitySettings = $ReportSuspiciousActivityState + systemCredentialPreferences = $SystemCredentialState + } # Check if states are set correctly $ReportSuspiciousActivityCorrect = if ($CurrentPolicy.reportSuspiciousActivitySettings.state -eq $ReportSuspiciousActivityState) { $true } else { $false } @@ -93,8 +99,7 @@ function Invoke-CIPPStandardAuthMethodsSettings { } if ($Settings.report -eq $true) { - $state = $StateSetCorrectly ? $true : @{CurrentReportState = $CurrentReportState; CurrentSystemState = $CurrentSystemState; WantedReportState = $ReportSuspiciousActivityState; WantedSystemState = $SystemCredentialState } - Set-CIPPStandardsCompareField -FieldName 'standards.AuthMethodsSettings' -FieldValue $state -TenantFilter $tenant + Set-CIPPStandardsCompareField -FieldName 'standards.AuthMethodsSettings' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $tenant Add-CIPPBPAField -FieldName 'ReportSuspiciousActivity' -FieldValue $CurrentPolicy.reportSuspiciousActivitySettings.state -StoreAs string -Tenant $tenant Add-CIPPBPAField -FieldName 'SystemCredential' -FieldValue $CurrentPolicy.systemCredentialPreferences.state -StoreAs string -Tenant $tenant } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAutoAddProxy.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAutoAddProxy.ps1 index 06313eebbf53..43b3a943ecf3 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAutoAddProxy.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAutoAddProxy.ps1 @@ -56,10 +56,16 @@ function Invoke-CIPPStandardAutoAddProxy { } $StateIsCorrect = $MissingProxies -eq 0 + $ExpectedValue = [PSCustomObject]@{ + MissingProxies = 0 + } + $CurrentValue = [PSCustomObject]@{ + MissingProxies = $MissingProxies + } + if ($Settings.report -eq $true) { - $state = $StateIsCorrect ? $true : $MissingProxies - Set-CIPPStandardsCompareField -FieldName 'standards.AutoAddProxy' -FieldValue $state -TenantFilter $Tenant + Set-CIPPStandardsCompareField -FieldName 'standards.AutoAddProxy' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant Add-CIPPBPAField -FieldName 'AutoAddProxy' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $Tenant } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAutoArchive.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAutoArchive.ps1 index c11d2f661262..971c4bf55e56 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAutoArchive.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAutoArchive.ps1 @@ -57,6 +57,13 @@ function Invoke-CIPPStandardAutoArchive { $CorrectState = $CurrentState -eq $DesiredThreshold + $ExpectedValue = [PSCustomObject]@{ + AutoArchivingThresholdPercentage = $DesiredThreshold + } + $CurrentValue = [PSCustomObject]@{ + AutoArchivingThresholdPercentage = $CurrentState + } + if ($Settings.remediate -eq $true) { Write-Host 'Time to remediate' @@ -91,6 +98,6 @@ function Invoke-CIPPStandardAutoArchive { } else { $FieldValue = @{ CurrentThreshold = $CurrentState; DesiredThreshold = $DesiredThreshold } } - Set-CIPPStandardsCompareField -FieldName 'standards.AutoArchive' -FieldValue $FieldValue -Tenant $Tenant + Set-CIPPStandardsCompareField -FieldName 'standards.AutoArchive' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -Tenant $Tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAutoExpandArchive.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAutoExpandArchive.ps1 index 6a9f9d80cc8d..72b4552da9bb 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAutoExpandArchive.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAutoExpandArchive.ps1 @@ -46,6 +46,13 @@ function Invoke-CIPPStandardAutoExpandArchive { return } + $ExpectedValue = [PSCustomObject]@{ + AutoExpandingArchive = $true + } + $CurrentValue = [PSCustomObject]@{ + AutoExpandingArchive = $CurrentState + } + if ($Settings.remediate -eq $true) { Write-Host 'Time to remediate' @@ -73,8 +80,7 @@ function Invoke-CIPPStandardAutoExpandArchive { } if ($Settings.report -eq $true) { - $state = $CurrentState -eq $true ? $true : $CurrentState - Set-CIPPStandardsCompareField -FieldName 'standards.AutoExpandArchive' -FieldValue $state -TenantFilter $tenant + Set-CIPPStandardsCompareField -FieldName 'standards.AutoExpandArchive' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $tenant Add-CIPPBPAField -FieldName 'AutoExpandingArchive' -FieldValue $CurrentState -StoreAs bool -Tenant $tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAutopilotProfile.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAutopilotProfile.ps1 index 4db07c9b94b8..98cda9ca282b 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAutopilotProfile.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAutopilotProfile.ps1 @@ -78,6 +78,32 @@ function Invoke-CIPPStandardAutopilotProfile { $StateIsCorrect = $false } + $CurrentValue = $CurrentConfig | Select-Object -Property displayName, description, deviceNameTemplate, locale, preprovisioningAllowed, hardwareHashExtractionEnabled, @{Name = 'outOfBoxExperienceSetting'; Expression = { + [PSCustomObject]@{ + deviceUsageType = $_.outOfBoxExperienceSetting.deviceUsageType + privacySettingsHidden = $_.outOfBoxExperienceSetting.privacySettingsHidden + eulaHidden = $_.outOfBoxExperienceSetting.eulaHidden + userType = $_.outOfBoxExperienceSetting.userType + keyboardSelectionPageSkipped = $_.outOfBoxExperienceSetting.keyboardSelectionPageSkipped + } + } + } + $ExpectedValue = [PSCustomObject]@{ + displayName = $Settings.DisplayName + description = $Settings.Description + deviceNameTemplate = $Settings.DeviceNameTemplate + locale = $Settings.Languages.value + preprovisioningAllowed = $Settings.AllowWhiteGlove + hardwareHashExtractionEnabled = $Settings.CollectHash + outOfBoxExperienceSetting = [PSCustomObject]@{ + deviceUsageType = $DeploymentMode + privacySettingsHidden = $Settings.HidePrivacy + eulaHidden = $Settings.HideTerms + userType = $userType + keyboardSelectionPageSkipped = $Settings.AutoKeyboard + } + } + # Remediate if the state is not correct if ($Settings.remediate -eq $true) { if ($StateIsCorrect -eq $true) { @@ -117,8 +143,7 @@ function Invoke-CIPPStandardAutopilotProfile { # Report if ($Settings.report -eq $true) { - $FieldValue = $StateIsCorrect -eq $true ? $true : $CurrentConfig - Set-CIPPStandardsCompareField -FieldName 'standards.AutopilotProfile' -FieldValue $FieldValue -TenantFilter $Tenant + Set-CIPPStandardsCompareField -FieldName 'standards.AutopilotProfile' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant Add-CIPPBPAField -FieldName 'AutopilotProfile' -FieldValue [bool]$StateIsCorrect -StoreAs bool -Tenant $Tenant } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAutopilotStatusPage.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAutopilotStatusPage.ps1 index e765ec90013e..17ac191280b5 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAutopilotStatusPage.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAutopilotStatusPage.ps1 @@ -68,6 +68,19 @@ function Invoke-CIPPStandardAutopilotStatusPage { $StateIsCorrect = $false } + $CurrentValue = $CurrentConfig | Select-Object -Property id, displayName, priority, showInstallationProgress, blockDeviceSetupRetryByUser, allowDeviceResetOnInstallFailure, allowLogCollectionOnInstallFailure, customErrorMessage, installProgressTimeoutInMinutes, allowDeviceUseOnInstallFailure, trackInstallProgressForAutopilotOnly, installQualityUpdates + $ExpectedValue = [PSCustomObject]@{ + installProgressTimeoutInMinutes = $Settings.TimeOutInMinutes + customErrorMessage = $Settings.ErrorMessage + showInstallationProgress = $Settings.ShowProgress + allowLogCollectionOnInstallFailure = $Settings.EnableLog + trackInstallProgressForAutopilotOnly = $Settings.OBEEOnly + blockDeviceSetupRetryByUser = !$Settings.BlockDevice + installQualityUpdates = $InstallWindowsUpdates + allowDeviceResetOnInstallFailure = $Settings.AllowReset + allowDeviceUseOnInstallFailure = $Settings.AllowFail + } + # Remediate if the state is not correct if ($Settings.remediate -eq $true) { try { @@ -91,8 +104,7 @@ function Invoke-CIPPStandardAutopilotStatusPage { # Report if ($Settings.report -eq $true) { - $FieldValue = $StateIsCorrect -eq $true ? $true : $CurrentConfig - Set-CIPPStandardsCompareField -FieldName 'standards.AutopilotStatusPage' -FieldValue $FieldValue -TenantFilter $Tenant + Set-CIPPStandardsCompareField -FieldName 'standards.AutopilotStatusPage' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant Add-CIPPBPAField -FieldName 'AutopilotStatusPage' -FieldValue [bool]$StateIsCorrect -StoreAs bool -Tenant $Tenant } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardBitLockerKeysForOwnedDevice.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardBitLockerKeysForOwnedDevice.ps1 index e063f6a05f9e..ffd3062882a1 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardBitLockerKeysForOwnedDevice.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardBitLockerKeysForOwnedDevice.ps1 @@ -55,8 +55,15 @@ function Invoke-CIPPStandardBitLockerKeysForOwnedDevice { Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the BitLockerKeysForOwnedDevice state for $Tenant. Error: $($ErrorMessage.NormalizedError)" -Sev Error -LogData $ErrorMessage return } - $CurrentValue = [bool]$CurrentState.defaultUserRolePermissions.allowedToReadBitLockerKeysForOwnedDevice - $StateIsCorrect = ($CurrentValue -eq $DesiredValue) + $CurrentStateValue = [bool]$CurrentState.defaultUserRolePermissions.allowedToReadBitLockerKeysForOwnedDevice + $StateIsCorrect = ($CurrentStateValue -eq $DesiredValue) + + $CurrentValue = [PSCustomObject]@{ + allowedToReadBitLockerKeysForOwnedDevice = $CurrentStateValue + } + $ExpectedValue = [PSCustomObject]@{ + allowedToReadBitLockerKeysForOwnedDevice = $DesiredValue + } if ($Settings.remediate -eq $true) { if ($StateIsCorrect -eq $true) { @@ -72,7 +79,7 @@ function Invoke-CIPPStandardBitLockerKeysForOwnedDevice { # Update current state variables to reflect the change immediately if running remediate and report/alert together $CurrentState.defaultUserRolePermissions.allowedToReadBitLockerKeysForOwnedDevice = $DesiredValue - $CurrentValue = $DesiredValue + $CurrentStateValue = $DesiredValue $StateIsCorrect = $true } catch { $ErrorMessage = Get-CippException -Exception $_ @@ -85,7 +92,7 @@ function Invoke-CIPPStandardBitLockerKeysForOwnedDevice { if ($StateIsCorrect -eq $true) { Write-LogMessage -API 'Standards' -tenant $tenant -message "Users are $DesiredLabel to recover BitLocker keys for their owned devices as configured." -sev Info } else { - $CurrentLabel = if ($CurrentValue) { 'allowed' } else { 'restricted' } + $CurrentLabel = if ($CurrentStateValue) { 'allowed' } else { 'restricted' } $AlertMessage = "Users are $CurrentLabel to recover BitLocker keys for their owned devices but should be $DesiredLabel." Write-StandardsAlert -message $AlertMessage -object $CurrentState -tenant $tenant -standardName 'BitLockerKeysForOwnedDevice' -standardId $Settings.standardId Write-LogMessage -API 'Standards' -tenant $tenant -message $AlertMessage -sev Info @@ -93,7 +100,7 @@ function Invoke-CIPPStandardBitLockerKeysForOwnedDevice { } if ($Settings.report -eq $true) { - Set-CIPPStandardsCompareField -FieldName 'standards.BitLockerKeysForOwnedDevice' -FieldValue $StateIsCorrect -Tenant $tenant + Set-CIPPStandardsCompareField -FieldName 'standards.BitLockerKeysForOwnedDevice' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -Tenant $tenant Add-CIPPBPAField -FieldName 'BitLockerKeysForOwnedDevice' -FieldValue $CurrentValue -StoreAs bool -Tenant $tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardBookings.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardBookings.ps1 index a9d2d9633ffe..608aadf0e58c 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardBookings.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardBookings.ps1 @@ -52,9 +52,16 @@ function Invoke-CIPPStandardBookings { $WantedState = if ($state -eq 'true') { $true } else { $false } $StateIsCorrect = if ($CurrentState -eq $WantedState) { $true } else { $false } + $CurrentValue = [PSCustomObject]@{ + BookingsEnabled = $CurrentState + } + $ExpectedValue = [PSCustomObject]@{ + BookingsEnabled = $WantedState + } + if ($Settings.report -eq $true) { $state = $StateIsCorrect ? $true : $CurrentState - Set-CIPPStandardsCompareField -FieldName 'standards.Bookings' -FieldValue $state -TenantFilter $Tenant + Set-CIPPStandardsCompareField -FieldName 'standards.Bookings' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant if ($null -eq $CurrentState ) { $CurrentState = $true } Add-CIPPBPAField -FieldName 'BookingsState' -FieldValue $CurrentState -StoreAs bool -Tenant $Tenant } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardBranding.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardBranding.ps1 index c6b8f80a97a6..467279f5838d 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardBranding.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardBranding.ps1 @@ -88,6 +88,25 @@ function Invoke-CIPPStandardBranding { ($CurrentState.loginPageLayoutConfiguration.isHeaderShown -eq $Settings.isHeaderShown) -and ($CurrentState.loginPageLayoutConfiguration.isFooterShown -eq $Settings.isFooterShown) + $CurrentValue = [PSCustomObject]@{ + signInPageText = $CurrentState.signInPageText + usernameHintText = $CurrentState.usernameHintText + loginPageTextVisibilitySettings = $CurrentState.loginPageTextVisibilitySettings | Select-Object -Property hideAccountResetCredentials + loginPageLayoutConfiguration = $CurrentState.loginPageLayoutConfiguration | Select-Object -Property layoutTemplateType, isHeaderShown, isFooterShown + } + $ExpectedValue = [PSCustomObject]@{ + signInPageText = $Settings.signInPageText + usernameHintText = $Settings.usernameHintText + loginPageTextVisibilitySettings = [pscustomobject]@{ + hideAccountResetCredentials = $Settings.hideAccountResetCredentials + } + loginPageLayoutConfiguration = [pscustomobject]@{ + layoutTemplateType = $layoutTemplateType + isHeaderShown = $Settings.isHeaderShown + isFooterShown = $Settings.isFooterShown + } + } + if ($Settings.remediate -eq $true) { if ($StateIsCorrect -eq $true) { Write-LogMessage -API 'Standards' -Tenant $Tenant -Message 'Branding is already applied correctly.' -Sev Info @@ -133,8 +152,7 @@ function Invoke-CIPPStandardBranding { } if ($Settings.report -eq $true) { - $state = $StateIsCorrect -eq $true ? $true : ($CurrentState | Select-Object -Property signInPageText, usernameHintText, loginPageTextVisibilitySettings, loginPageLayoutConfiguration) - Set-CIPPStandardsCompareField -FieldName 'standards.Branding' -FieldValue $state -TenantFilter $Tenant + Set-CIPPStandardsCompareField -FieldName 'standards.Branding' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant Add-CIPPBPAField -FieldName 'Branding' -FieldValue [bool]$StateIsCorrect -StoreAs bool -Tenant $Tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardCloudMessageRecall.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardCloudMessageRecall.ps1 index 8a61d794eac0..48feb07b2c45 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardCloudMessageRecall.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardCloudMessageRecall.ps1 @@ -52,18 +52,26 @@ function Invoke-CIPPStandardCloudMessageRecall { $WantedState = if ($state -eq 'true') { $true } else { $false } $StateIsCorrect = if ($CurrentState -eq $WantedState) { $true } else { $false } + # Input validation + if (([string]::IsNullOrWhiteSpace($state) -or $state -eq 'Select a value') -and ($Settings.remediate -eq $true -or $Settings.alert -eq $true)) { + Write-LogMessage -API 'Standards' -tenant $Tenant -message 'CloudMessageRecall: Invalid state parameter set' -sev Error + return + } + + $CurrentValue = [PSCustomObject]@{ + MessageRecallEnabled = $CurrentState + } + $ExpectedValue = [PSCustomObject]@{ + MessageRecallEnabled = $WantedState + } + if ($Settings.report -eq $true) { # Default is not set, not set means it's enabled if ($null -eq $CurrentState ) { $CurrentState = $true } - Set-CIPPStandardsCompareField -FieldName 'standards.CloudMessageRecall' -FieldValue $StateIsCorrect -TenantFilter $Tenant + Set-CIPPStandardsCompareField -FieldName 'standards.CloudMessageRecall' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant Add-CIPPBPAField -FieldName 'MessageRecall' -FieldValue $CurrentState -StoreAs bool -Tenant $Tenant } - # Input validation - if (([string]::IsNullOrWhiteSpace($state) -or $state -eq 'Select a value') -and ($Settings.remediate -eq $true -or $Settings.alert -eq $true)) { - Write-LogMessage -API 'Standards' -tenant $Tenant -message 'CloudMessageRecall: Invalid state parameter set' -sev Error - return - } if ($Settings.remediate -eq $true) { Write-Host 'Time to remediate' diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardCustomBannedPasswordList.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardCustomBannedPasswordList.ps1 index 6c5009d324a9..1eb6f3370fcd 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardCustomBannedPasswordList.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardCustomBannedPasswordList.ps1 @@ -203,6 +203,14 @@ function Invoke-CIPPStandardCustomBannedPasswordList { } if ($Settings.report -eq $true) { + $ExpectedValue = @{ + Status = 'Configured' + Enabled = $true + WordCount = $BannedWordsList.Count + Compliant = $true + MissingInputWords = @() + } + if ($null -eq $ExistingSettings) { $BannedPasswordState = @{ Status = 'Not Configured' @@ -229,7 +237,7 @@ function Invoke-CIPPStandardCustomBannedPasswordList { } } - Add-CIPPBPAField -FieldName 'CustomBannedPasswordList' -FieldValue $BannedPasswordState -StoreAs json -Tenant $tenant + Add-CIPPBPAField -FieldName 'CustomBannedPasswordList' -CurrentValue $BannedPasswordState -ExpectedValue $ExpectedValue -StoreAs json -Tenant $tenant Set-CIPPStandardsCompareField -FieldName 'standards.CustomBannedPasswordList' -FieldValue $BannedPasswordState.Compliant -Tenant $tenant } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDefaultPlatformRestrictions.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDefaultPlatformRestrictions.ps1 index 1dc00ce739d0..23945c2b628b 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDefaultPlatformRestrictions.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDefaultPlatformRestrictions.ps1 @@ -50,72 +50,84 @@ function Invoke-CIPPStandardDefaultPlatformRestrictions { try { $CurrentState = New-GraphGetRequest -Uri "https://graph.microsoft.com/beta/deviceManagement/deviceEnrollmentConfigurations?`$expand=assignments&orderBy=priority&`$filter=deviceEnrollmentConfigurationType eq 'SinglePlatformRestriction'" -tenantID $Tenant -AsApp $true | - Select-Object -Property id, androidForWorkRestriction, androidRestriction, iosRestriction, macOSRestriction, windowsRestriction - } - catch { + Select-Object -Property id, androidForWorkRestriction, androidRestriction, iosRestriction, macOSRestriction, windowsRestriction + } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the DefaultPlatformRestrictions state for $Tenant. Error: $ErrorMessage" -Sev Error return } $StateIsCorrect = ($CurrentState.androidForWorkRestriction.platformBlocked -eq $Settings.platformAndroidForWorkBlocked) -and - ($CurrentState.androidForWorkRestriction.personalDeviceEnrollmentBlocked -eq $Settings.personalAndroidForWorkBlocked) -and - ($CurrentState.androidRestriction.platformBlocked -eq $Settings.platformAndroidBlocked) -and - ($CurrentState.androidRestriction.personalDeviceEnrollmentBlocked -eq $Settings.personalAndroidBlocked) -and - ($CurrentState.iosRestriction.platformBlocked -eq $Settings.platformiOSBlocked) -and - ($CurrentState.iosRestriction.personalDeviceEnrollmentBlocked -eq $Settings.personaliOSBlocked) -and - ($CurrentState.macOSRestriction.platformBlocked -eq $Settings.platformMacOSBlocked) -and - ($CurrentState.macOSRestriction.personalDeviceEnrollmentBlocked -eq $Settings.personalMacOSBlocked) -and - ($CurrentState.windowsRestriction.platformBlocked -eq $Settings.platformWindowsBlocked) -and - ($CurrentState.windowsRestriction.personalDeviceEnrollmentBlocked -eq $Settings.personalWindowsBlocked) + ($CurrentState.androidForWorkRestriction.personalDeviceEnrollmentBlocked -eq $Settings.personalAndroidForWorkBlocked) -and + ($CurrentState.androidRestriction.platformBlocked -eq $Settings.platformAndroidBlocked) -and + ($CurrentState.androidRestriction.personalDeviceEnrollmentBlocked -eq $Settings.personalAndroidBlocked) -and + ($CurrentState.iosRestriction.platformBlocked -eq $Settings.platformiOSBlocked) -and + ($CurrentState.iosRestriction.personalDeviceEnrollmentBlocked -eq $Settings.personaliOSBlocked) -and + ($CurrentState.macOSRestriction.platformBlocked -eq $Settings.platformMacOSBlocked) -and + ($CurrentState.macOSRestriction.personalDeviceEnrollmentBlocked -eq $Settings.personalMacOSBlocked) -and + ($CurrentState.windowsRestriction.platformBlocked -eq $Settings.platformWindowsBlocked) -and + ($CurrentState.windowsRestriction.personalDeviceEnrollmentBlocked -eq $Settings.personalWindowsBlocked) $CompareField = [PSCustomObject]@{ - platformAndroidForWorkBlocked = $CurrentState.androidForWorkRestriction.platformBlocked - personalAndroidForWorkBlocked = $CurrentState.androidForWorkRestriction.personalDeviceEnrollmentBlocked - platformAndroidBlocked = $CurrentState.androidRestriction.platformBlocked - personalAndroidBlocked = $CurrentState.androidRestriction.personalDeviceEnrollmentBlocked - platformiOSBlocked = $CurrentState.iosRestriction.platformBlocked - personaliOSBlocked = $CurrentState.iosRestriction.personalDeviceEnrollmentBlocked - platformMacOSBlocked = $CurrentState.macOSRestriction.platformBlocked - personalMacOSBlocked = $CurrentState.macOSRestriction.personalDeviceEnrollmentBlocked - platformWindowsBlocked = $CurrentState.windowsRestriction.platformBlocked - personalWindowsBlocked = $CurrentState.windowsRestriction.personalDeviceEnrollmentBlocked + platformAndroidForWorkBlocked = $CurrentState.androidForWorkRestriction.platformBlocked + personalAndroidForWorkBlocked = $CurrentState.androidForWorkRestriction.personalDeviceEnrollmentBlocked + platformAndroidBlocked = $CurrentState.androidRestriction.platformBlocked + personalAndroidBlocked = $CurrentState.androidRestriction.personalDeviceEnrollmentBlocked + platformiOSBlocked = $CurrentState.iosRestriction.platformBlocked + personaliOSBlocked = $CurrentState.iosRestriction.personalDeviceEnrollmentBlocked + platformMacOSBlocked = $CurrentState.macOSRestriction.platformBlocked + personalMacOSBlocked = $CurrentState.macOSRestriction.personalDeviceEnrollmentBlocked + platformWindowsBlocked = $CurrentState.windowsRestriction.platformBlocked + personalWindowsBlocked = $CurrentState.windowsRestriction.personalDeviceEnrollmentBlocked + } + + $ExpectedValue = [PSCustomObject]@{ + platformAndroidForWorkBlocked = $Settings.platformAndroidForWorkBlocked + personalAndroidForWorkBlocked = $Settings.personalAndroidForWorkBlocked + platformAndroidBlocked = $Settings.platformAndroidBlocked + personalAndroidBlocked = $Settings.personalAndroidBlocked + platformiOSBlocked = $Settings.platformiOSBlocked + personaliOSBlocked = $Settings.personaliOSBlocked + platformMacOSBlocked = $Settings.platformMacOSBlocked + personalMacOSBlocked = $Settings.personalMacOSBlocked + platformWindowsBlocked = $Settings.platformWindowsBlocked + personalWindowsBlocked = $Settings.personalWindowsBlocked } - If ($Settings.remediate -eq $true) { + if ($Settings.remediate -eq $true) { if ($StateIsCorrect -eq $true) { Write-LogMessage -API 'Standards' -Tenant $Tenant -Message 'DefaultPlatformRestrictions is already applied correctly.' -Sev Info } else { $cmdParam = @{ - tenantid = $Tenant - uri = "https://graph.microsoft.com/beta/deviceManagement/deviceEnrollmentConfigurations/$($CurrentState.id)" - AsApp = $false - Type = 'PATCH' + tenantid = $Tenant + uri = "https://graph.microsoft.com/beta/deviceManagement/deviceEnrollmentConfigurations/$($CurrentState.id)" + AsApp = $false + Type = 'PATCH' ContentType = 'application/json; charset=utf-8' - Body = [PSCustomObject]@{ - "@odata.type" = "#microsoft.graph.deviceEnrollmentPlatformRestrictionsConfiguration" + Body = [PSCustomObject]@{ + '@odata.type' = '#microsoft.graph.deviceEnrollmentPlatformRestrictionsConfiguration' androidForWorkRestriction = [PSCustomObject]@{ - "@odata.type" = "microsoft.graph.deviceEnrollmentPlatformRestriction" + '@odata.type' = 'microsoft.graph.deviceEnrollmentPlatformRestriction' platformBlocked = $Settings.platformAndroidForWorkBlocked personalDeviceEnrollmentBlocked = $Settings.personalAndroidForWorkBlocked } - androidRestriction = [PSCustomObject]@{ - "@odata.type" = "microsoft.graph.deviceEnrollmentPlatformRestriction" + androidRestriction = [PSCustomObject]@{ + '@odata.type' = 'microsoft.graph.deviceEnrollmentPlatformRestriction' platformBlocked = $Settings.platformAndroidBlocked personalDeviceEnrollmentBlocked = $Settings.personalAndroidBlocked } - iosRestriction = [PSCustomObject]@{ - "@odata.type" = "microsoft.graph.deviceEnrollmentPlatformRestriction" + iosRestriction = [PSCustomObject]@{ + '@odata.type' = 'microsoft.graph.deviceEnrollmentPlatformRestriction' platformBlocked = $Settings.platformiOSBlocked personalDeviceEnrollmentBlocked = $Settings.personaliOSBlocked } - macOSRestriction = [PSCustomObject]@{ - "@odata.type" = "microsoft.graph.deviceEnrollmentPlatformRestriction" + macOSRestriction = [PSCustomObject]@{ + '@odata.type' = 'microsoft.graph.deviceEnrollmentPlatformRestriction' platformBlocked = $Settings.platformMacOSBlocked personalDeviceEnrollmentBlocked = $Settings.personalMacOSBlocked } - windowsRestriction = [PSCustomObject]@{ - "@odata.type" = "microsoft.graph.deviceEnrollmentPlatformRestriction" + windowsRestriction = [PSCustomObject]@{ + '@odata.type' = 'microsoft.graph.deviceEnrollmentPlatformRestriction' platformBlocked = $Settings.platformWindowsBlocked personalDeviceEnrollmentBlocked = $Settings.personalWindowsBlocked } @@ -132,7 +144,7 @@ function Invoke-CIPPStandardDefaultPlatformRestrictions { } - If ($Settings.alert -eq $true) { + if ($Settings.alert -eq $true) { if ($StateIsCorrect -eq $true) { Write-LogMessage -API 'Standards' -Tenant $Tenant -Message 'DefaultPlatformRestrictions is correctly set.' -Sev Info } else { @@ -141,9 +153,8 @@ function Invoke-CIPPStandardDefaultPlatformRestrictions { } } - If ($Settings.report -eq $true) { - $FieldValue = $StateIsCorrect ? $true : $CompareField - Set-CIPPStandardsCompareField -FieldName 'standards.DefaultPlatformRestrictions' -FieldValue $FieldValue -TenantFilter $Tenant + if ($Settings.report -eq $true) { + Set-CIPPStandardsCompareField -FieldName 'standards.DefaultPlatformRestrictions' -CurrentValue $CompareField -ExpectedValue $ExpectedValue -TenantFilter $Tenant Add-CIPPBPAField -FieldName 'DefaultPlatformRestrictions' -FieldValue [bool]$StateIsCorrect -StoreAs bool -Tenant $Tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardallowOAuthTokens.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardallowOAuthTokens.ps1 index 748567ac2da1..d2022ada13c4 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardallowOAuthTokens.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardallowOAuthTokens.ps1 @@ -42,6 +42,8 @@ function Invoke-CIPPStandardallowOAuthTokens { return } $StateIsCorrect = ($CurrentState.state -eq 'enabled') + $CurrentValue = $CurrentState | Select-Object -Property state + $ExpectedValue = [PSCustomObject]@{state = 'enabled' } if ($Settings.remediate -eq $true) { if ($StateIsCorrect -eq $true) { @@ -65,6 +67,6 @@ function Invoke-CIPPStandardallowOAuthTokens { if ($Settings.report -eq $true) { Add-CIPPBPAField -FieldName 'softwareOath' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $tenant - Set-CIPPStandardsCompareField -FieldName 'standards.allowOAuthTokens' -FieldValue $StateIsCorrect -TenantFilter $tenant + Set-CIPPStandardsCompareField -FieldName 'standards.allowOAuthTokens' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardallowOTPTokens.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardallowOTPTokens.ps1 index a8a7037a8b84..acfdcc29a911 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardallowOTPTokens.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardallowOTPTokens.ps1 @@ -41,6 +41,9 @@ function Invoke-CIPPStandardallowOTPTokens { return } + $CurrentValue = $CurrentInfo | Select-Object -Property isSoftwareOathEnabled + $ExpectedValue = [PSCustomObject]@{isSoftwareOathEnabled = $true } + if ($Settings.remediate -eq $true) { if ($CurrentInfo.isSoftwareOathEnabled) { Write-LogMessage -API 'Standards' -tenant $tenant -message 'MS authenticator OTP/oAuth tokens is already enabled.' -sev Info @@ -62,7 +65,7 @@ function Invoke-CIPPStandardallowOTPTokens { } if ($Settings.report -eq $true) { - Set-CIPPStandardsCompareField -FieldName 'standards.allowOTPTokens' -FieldValue $CurrentInfo.isSoftwareOathEnabled -TenantFilter $tenant + Set-CIPPStandardsCompareField -FieldName 'standards.allowOTPTokens' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $tenant Add-CIPPBPAField -FieldName 'MSAuthenticator' -FieldValue $CurrentInfo.isSoftwareOathEnabled -StoreAs bool -Tenant $tenant } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardcalDefault.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardcalDefault.ps1 index 5926d094622f..193addccbdc1 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardcalDefault.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardcalDefault.ps1 @@ -115,8 +115,5 @@ function Invoke-CIPPStandardcalDefault { Write-LogMessage -API 'Standards' -tenant $Tenant -message "Successfully set default calendar permissions for $SuccessCounter out of $TotalMailboxes mailboxes." -sev Info } - if ($Settings.report -eq $true) { - #This script always returns true, as it only disables the Safe Senders list - Set-CIPPStandardsCompareField -FieldName 'standards.SafeSendersDisable' -FieldValue $true -Tenant $Tenant - } + } From 124a8eb2230f0d33eca47a38774810b0c49cfdcb Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Tue, 13 Jan 2026 13:05:33 +0800 Subject: [PATCH 130/503] Add JIT Admin template management and settings Introduces new PowerShell functions for managing JIT Admin templates: add, edit, list, and remove operations. Adds support for JIT Admin settings, including a configurable maximum duration, and enforces this limit in JIT Admin execution. Enhances template uniqueness checks, default template handling, and audit logging. --- .../Settings/Invoke-ExecJITAdminSettings.ps1 | 94 ++++++++++ .../Users/Invoke-AddJITAdminTemplate.ps1 | 150 ++++++++++++++++ .../Users/Invoke-EditJITAdminTemplate.ps1 | 164 ++++++++++++++++++ .../Users/Invoke-ExecJITAdmin.ps1 | 33 ++++ .../Users/Invoke-ListJITAdminTemplates.ps1 | 74 ++++++++ .../Users/Invoke-RemoveJITAdminTemplate.ps1 | 47 +++++ 6 files changed, 562 insertions(+) create mode 100644 Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecJITAdminSettings.ps1 create mode 100644 Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-AddJITAdminTemplate.ps1 create mode 100644 Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-EditJITAdminTemplate.ps1 create mode 100644 Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListJITAdminTemplates.ps1 create mode 100644 Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-RemoveJITAdminTemplate.ps1 diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecJITAdminSettings.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecJITAdminSettings.ps1 new file mode 100644 index 000000000000..9098562f5870 --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecJITAdminSettings.ps1 @@ -0,0 +1,94 @@ +Function Invoke-ExecJITAdminSettings { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + CIPP.AppSettings.ReadWrite + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + $StatusCode = [HttpStatusCode]::OK + + try { + $Table = Get-CIPPTable -TableName Config + $Filter = "PartitionKey eq 'JITAdminSettings' and RowKey eq 'JITAdminSettings'" + $JITAdminConfig = Get-CIPPAzDataTableEntity @Table -Filter $Filter + + if (-not $JITAdminConfig) { + $JITAdminConfig = @{ + PartitionKey = 'JITAdminSettings' + RowKey = 'JITAdminSettings' + MaxDuration = $null # null means no limit + } + } + + $Action = if ($Request.Body.Action) { $Request.Body.Action } else { $Request.Query.Action } + + $Results = switch ($Action) { + 'Get' { + @{ + MaxDuration = $JITAdminConfig.MaxDuration + } + } + 'Set' { + $MaxDuration = $Request.Body.MaxDuration.value + + # Validate ISO 8601 duration format if provided + if (![string]::IsNullOrWhiteSpace($MaxDuration)) { + try { + # Test if it's a valid ISO 8601 duration + $null = [System.Xml.XmlConvert]::ToTimeSpan($MaxDuration) + $JITAdminConfig.MaxDuration = $MaxDuration + } catch { + $StatusCode = [HttpStatusCode]::BadRequest + @{ + Results = "Error: Invalid ISO 8601 duration format. Expected format like PT4H, P1D, P4W, etc." + } + break + } + } else { + # Empty or null means no limit + $JITAdminConfig.MaxDuration = $null + } + + $JITAdminConfig.PartitionKey = 'JITAdminSettings' + $JITAdminConfig.RowKey = 'JITAdminSettings' + + Add-CIPPAzDataTableEntity @Table -Entity $JITAdminConfig -Force | Out-Null + + $Message = if ($JITAdminConfig.MaxDuration) { + "Successfully set JIT Admin maximum duration to $($JITAdminConfig.MaxDuration)" + } else { + "Successfully removed JIT Admin maximum duration limit" + } + + Write-LogMessage -headers $Headers -API $APIName -message $Message -Sev 'Info' + + @{ + Results = $Message + } + } + default { + $StatusCode = [HttpStatusCode]::BadRequest + @{ + Results = 'Error: Invalid action. Use Get or Set.' + } + } + } + } catch { + $ErrorMessage = Get-CippException -Exception $_ + $StatusCode = [HttpStatusCode]::InternalServerError + $Results = @{ + Results = "Error: $($ErrorMessage.NormalizedError)" + } + Write-LogMessage -headers $Headers -API $APIName -message "Failed to process JIT Admin settings: $($ErrorMessage.NormalizedError)" -Sev 'Error' -LogData $ErrorMessage + } + + return ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = $Results + }) +} diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-AddJITAdminTemplate.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-AddJITAdminTemplate.ps1 new file mode 100644 index 000000000000..dd2de9d523e0 --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-AddJITAdminTemplate.ps1 @@ -0,0 +1,150 @@ +function Invoke-AddJITAdminTemplate { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Identity.Role.ReadWrite + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + + try { + # Extract data from request body + $TenantFilter = $Request.Body.tenantFilter + $TemplateName = $Request.Body.templateName + + # Validate required fields + if ([string]::IsNullOrWhiteSpace($TenantFilter)) { + throw 'tenantFilter is required' + } + if ([string]::IsNullOrWhiteSpace($TemplateName)) { + throw 'templateName is required' + } + + Write-LogMessage -headers $Headers -API $APIName -message "Creating JIT Admin template '$TemplateName' for tenant: $TenantFilter" -Sev 'Info' + + # Get user info for audit + $UserDetails = ([System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($Headers.'x-ms-client-principal')) | ConvertFrom-Json).userDetails + + # Check if template name already exists for this tenant + $Table = Get-CippTable -tablename 'templates' + $ExistingTemplates = Get-CIPPAzDataTableEntity @Table -Filter "PartitionKey eq 'JITAdminTemplate'" + $ExistingNames = $ExistingTemplates | ForEach-Object { + try { + $data = $_.JSON | ConvertFrom-Json -Depth 100 -ErrorAction Stop + if ($data.tenantFilter -eq $TenantFilter -and $data.templateName -eq $TemplateName) { + $data + } + } catch {} + } + + if ($ExistingNames) { + throw "A template with name '$TemplateName' already exists for tenant '$TenantFilter'" + } + + $DefaultForTenant = [bool]$Request.Body.defaultForTenant + + # If this template is set as default, unset other defaults for this tenant + if ($DefaultForTenant) { + $ExistingTemplates | ForEach-Object { + try { + $row = $_ + $data = $row.JSON | ConvertFrom-Json -Depth 100 -ErrorAction Stop + if ($data.tenantFilter -eq $TenantFilter -and $data.defaultForTenant -eq $true) { + # Unset the default flag + $data.defaultForTenant = $false + $row.JSON = ($data | ConvertTo-Json -Depth 100 -Compress) + Add-CIPPAzDataTableEntity @Table -Entity $row -Force + Write-LogMessage -headers $Headers -API $APIName -message "Unset default flag for existing template: $($data.templateName)" -Sev 'Info' + } + } catch { + Write-LogMessage -headers $Headers -API $APIName -message "Failed to update existing template: $($_.Exception.Message)" -Sev 'Warning' + } + } + } + + # Validate user action fields + $DefaultUserAction = $Request.Body.defaultUserAction + if ($TenantFilter -eq 'AllTenants' -and $DefaultUserAction -eq 'select') { + throw 'defaultUserAction cannot be "select" when tenantFilter is "AllTenants"' + } + + # Create template object + $TemplateObject = @{ + tenantFilter = $TenantFilter + templateName = $TemplateName + defaultForTenant = $DefaultForTenant + defaultRoles = $Request.Body.defaultRoles + defaultDuration = $Request.Body.defaultDuration + defaultExpireAction = $Request.Body.defaultExpireAction + defaultNotificationActions = $Request.Body.defaultNotificationActions + generateTAPByDefault = [bool]$Request.Body.generateTAPByDefault + reasonTemplate = $Request.Body.reasonTemplate + createdBy = $UserDetails + createdDate = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ') + } + + # Add defaultUserAction if provided + if (![string]::IsNullOrWhiteSpace($DefaultUserAction)) { + $TemplateObject.defaultUserAction = $DefaultUserAction + } + + # Add user detail fields when "create" action is specified + if ($DefaultUserAction -eq 'create') { + # These fields can be saved for both AllTenants and specific tenant templates + if (![string]::IsNullOrWhiteSpace($Request.Body.defaultFirstName)) { + $TemplateObject.defaultFirstName = $Request.Body.defaultFirstName + } + if (![string]::IsNullOrWhiteSpace($Request.Body.defaultLastName)) { + $TemplateObject.defaultLastName = $Request.Body.defaultLastName + } + if (![string]::IsNullOrWhiteSpace($Request.Body.defaultUserName)) { + $TemplateObject.defaultUserName = $Request.Body.defaultUserName + } + + # defaultDomain is only saved for specific tenant templates (not AllTenants) + if ($TenantFilter -ne 'AllTenants' -and $Request.Body.defaultDomain) { + if ($Request.Body.defaultDomain -is [string]) { + if (![string]::IsNullOrWhiteSpace($Request.Body.defaultDomain)) { + $TemplateObject.defaultDomain = $Request.Body.defaultDomain + } + } else { + $TemplateObject.defaultDomain = $Request.Body.defaultDomain + } + } + } + + # Generate GUID for the template + $GUID = (New-Guid).GUID + + # Convert to JSON + $JSON = ConvertTo-Json -InputObject $TemplateObject -Depth 100 -Compress + + # Store in table + $Table.Force = $true + Add-CIPPAzDataTableEntity @Table -Entity @{ + JSON = "$JSON" + RowKey = "$GUID" + PartitionKey = 'JITAdminTemplate' + GUID = "$GUID" + } + + $Result = "Created JIT Admin Template '$($TemplateName)' 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 JIT Admin 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/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-EditJITAdminTemplate.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-EditJITAdminTemplate.ps1 new file mode 100644 index 000000000000..35bbd95139ca --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-EditJITAdminTemplate.ps1 @@ -0,0 +1,164 @@ +function Invoke-EditJITAdminTemplate { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Identity.Role.ReadWrite + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + + try { + # Extract data from request body + $GUID = $Request.Body.GUID + $TenantFilter = $Request.Body.tenantFilter + $TemplateName = $Request.Body.templateName + + # Validate required fields + if ([string]::IsNullOrWhiteSpace($GUID)) { + throw 'GUID is required' + } + if ([string]::IsNullOrWhiteSpace($TenantFilter)) { + throw 'tenantFilter is required' + } + if ([string]::IsNullOrWhiteSpace($TemplateName)) { + throw 'templateName is required' + } + + Write-LogMessage -headers $Headers -API $APIName -message "Editing JIT Admin template '$GUID' for tenant: $TenantFilter" -Sev 'Info' + + # Get user info for audit + $UserDetails = ([System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($Headers.'x-ms-client-principal')) | ConvertFrom-Json).userDetails + + # Get the existing template + $Table = Get-CippTable -tablename 'templates' + $Filter = "PartitionKey eq 'JITAdminTemplate' and RowKey eq '$GUID'" + $ExistingTemplate = Get-CIPPAzDataTableEntity @Table -Filter $Filter + + if (!$ExistingTemplate) { + throw "Template with GUID '$GUID' not found" + } + + # Parse existing template data + $ExistingData = $ExistingTemplate.JSON | ConvertFrom-Json -Depth 100 + + # Check if template name is unique (excluding current template) + $AllTemplates = Get-CIPPAzDataTableEntity @Table -Filter "PartitionKey eq 'JITAdminTemplate'" + $DuplicateName = $AllTemplates | Where-Object { $_.RowKey -ne $GUID } | ForEach-Object { + try { + $data = $_.JSON | ConvertFrom-Json -Depth 100 -ErrorAction Stop + if ($data.tenantFilter -eq $TenantFilter -and $data.templateName -eq $TemplateName) { + $data + } + } catch {} + } + + if ($DuplicateName) { + throw "A template with name '$TemplateName' already exists for tenant '$TenantFilter'" + } + + $DefaultForTenant = [bool]$Request.Body.defaultForTenant + + # If this template is being set as default, unset other defaults for this tenant + if ($DefaultForTenant) { + $AllTemplates | Where-Object { $_.RowKey -ne $GUID } | ForEach-Object { + try { + $row = $_ + $data = $row.JSON | ConvertFrom-Json -Depth 100 -ErrorAction Stop + if ($data.tenantFilter -eq $TenantFilter -and $data.defaultForTenant -eq $true) { + # Unset the default flag + $data.defaultForTenant = $false + $row.JSON = ($data | ConvertTo-Json -Depth 100 -Compress) + Add-CIPPAzDataTableEntity @Table -Entity $row -Force + Write-LogMessage -headers $Headers -API $APIName -message "Unset default flag for existing template: $($data.templateName)" -Sev 'Info' + } + } catch { + Write-LogMessage -headers $Headers -API $APIName -message "Failed to update existing template: $($_.Exception.Message)" -Sev 'Warning' + } + } + } + + # Validate user action fields + $DefaultUserAction = $Request.Body.defaultUserAction + if ($TenantFilter -eq 'AllTenants' -and $DefaultUserAction -eq 'select') { + throw 'defaultUserAction cannot be "select" when tenantFilter is "AllTenants"' + } + + # Update template object (preserve creation metadata) + $TemplateObject = @{ + tenantFilter = $TenantFilter + templateName = $TemplateName + defaultForTenant = $DefaultForTenant + defaultRoles = $Request.Body.defaultRoles + defaultDuration = $Request.Body.defaultDuration + defaultExpireAction = $Request.Body.defaultExpireAction + defaultNotificationActions = $Request.Body.defaultNotificationActions + generateTAPByDefault = [bool]$Request.Body.generateTAPByDefault + reasonTemplate = $Request.Body.reasonTemplate + createdBy = $ExistingData.createdBy + createdDate = $ExistingData.createdDate + modifiedBy = $UserDetails + modifiedDate = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ') + } + + # Add defaultUserAction if provided + if (![string]::IsNullOrWhiteSpace($DefaultUserAction)) { + $TemplateObject.defaultUserAction = $DefaultUserAction + } + + # Add user detail fields when "create" action is specified + if ($DefaultUserAction -eq 'create') { + # These fields can be saved for both AllTenants and specific tenant templates + if (![string]::IsNullOrWhiteSpace($Request.Body.defaultFirstName)) { + $TemplateObject.defaultFirstName = $Request.Body.defaultFirstName + } + if (![string]::IsNullOrWhiteSpace($Request.Body.defaultLastName)) { + $TemplateObject.defaultLastName = $Request.Body.defaultLastName + } + if (![string]::IsNullOrWhiteSpace($Request.Body.defaultUserName)) { + $TemplateObject.defaultUserName = $Request.Body.defaultUserName + } + + # defaultDomain is only saved for specific tenant templates (not AllTenants) + if ($TenantFilter -ne 'AllTenants' -and $Request.Body.defaultDomain) { + if ($Request.Body.defaultDomain -is [string]) { + if (![string]::IsNullOrWhiteSpace($Request.Body.defaultDomain)) { + $TemplateObject.defaultDomain = $Request.Body.defaultDomain + } + } else { + $TemplateObject.defaultDomain = $Request.Body.defaultDomain + } + } + } + + # Convert to JSON + $JSON = ConvertTo-Json -InputObject $TemplateObject -Depth 100 -Compress + + # Update in table + $Table.Force = $true + Add-CIPPAzDataTableEntity @Table -Entity @{ + JSON = "$JSON" + RowKey = "$GUID" + PartitionKey = 'JITAdminTemplate' + GUID = "$GUID" + } + + $Result = "Updated JIT Admin Template '$($TemplateName)' (GUID: $GUID)" + Write-LogMessage -headers $Headers -API $APIName -message $Result -Sev 'Info' + $StatusCode = [HttpStatusCode]::OK + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + $Result = "Failed to update JIT Admin 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/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecJITAdmin.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecJITAdmin.ps1 index 3a054bc4f0f7..4eca5bd0cbe8 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecJITAdmin.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecJITAdmin.ps1 @@ -20,6 +20,39 @@ function Invoke-ExecJITAdmin { $Expiration = ([System.DateTimeOffset]::FromUnixTimeSeconds($Request.Body.EndDate)).DateTime.ToLocalTime() $Results = [System.Collections.Generic.List[object]]::new() + # Check maximum duration setting + try { + $ConfigTable = Get-CIPPTable -TableName Config + $Filter = "PartitionKey eq 'JITAdminSettings' and RowKey eq 'JITAdminSettings'" + $JITAdminConfig = Get-CIPPAzDataTableEntity @ConfigTable -Filter $Filter + + if ($JITAdminConfig -and ![string]::IsNullOrWhiteSpace($JITAdminConfig.MaxDuration)) { + # Calculate the duration between start and expiration + $RequestedDuration = $Expiration - $Start + + # Parse the max duration from ISO 8601 format + try { + $MaxDurationTimeSpan = [System.Xml.XmlConvert]::ToTimeSpan($JITAdminConfig.MaxDuration) + + if ($RequestedDuration -gt $MaxDurationTimeSpan) { + $RequestedDays = $RequestedDuration.TotalDays.ToString('0.00') + $MaxDays = $MaxDurationTimeSpan.TotalDays.ToString('0.00') + $ErrorMessage = "Requested JIT Admin duration ($RequestedDays days) exceeds the maximum allowed duration of $($JITAdminConfig.MaxDuration) ($MaxDays days)" + Write-LogMessage -headers $Headers -API $APIName -message $ErrorMessage -Sev 'Error' + return ([HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::BadRequest + Body = @{'Results' = @($ErrorMessage) } + }) + } + } catch { + Write-Warning "Failed to parse MaxDuration setting: $($_.Exception.Message)" + } + } + } catch { + Write-Warning "Failed to check JIT Admin max duration setting: $($_.Exception.Message)" + # Continue execution if we can't check the setting + } + if ($Request.Body.userAction -eq 'create') { $Domain = $Request.Body.Domain.value ? $Request.Body.Domain.value : $Request.Body.Domain $Username = "$($Request.Body.Username)@$($Domain)" diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListJITAdminTemplates.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListJITAdminTemplates.ps1 new file mode 100644 index 000000000000..bfe8dc5eb820 --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListJITAdminTemplates.ps1 @@ -0,0 +1,74 @@ +function Invoke-ListJITAdminTemplates { + <# + .FUNCTIONALITY + Entrypoint,AnyTenant + .ROLE + Identity.Role.Read + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + + Write-LogMessage -headers $Headers -API $APIName -message 'Listing JIT Admin Templates' -Sev 'Info' + + # Get the TenantFilter from query parameters + $TenantFilter = $Request.Query.TenantFilter + + # Get the includeAllTenants flag from query or body parameters (defaults to true) + $IncludeAllTenants = if ($Request.Query.includeAllTenants -eq 'false' -or $Request.Body.includeAllTenants -eq 'false') { + $false + } else { + $true + } + + # Get the templates table + $Table = Get-CippTable -tablename 'templates' + $Filter = "PartitionKey eq 'JITAdminTemplate'" + + # Retrieve all JIT Admin templates + $Templates = (Get-CIPPAzDataTableEntity @Table -Filter $Filter) | ForEach-Object { + try { + $row = $_ + $data = $row.JSON | ConvertFrom-Json -Depth 100 -ErrorAction Stop + $data | Add-Member -NotePropertyName 'GUID' -NotePropertyValue $row.GUID -Force + $data | Add-Member -NotePropertyName 'RowKey' -NotePropertyValue $row.RowKey -Force + $data + } catch { + Write-LogMessage -headers $Headers -API $APIName -message "Failed to process JIT Admin template: $($row.RowKey) - $($_.Exception.Message)" -Sev 'Warning' + } + } + + # Filter by tenant if TenantFilter is provided + if ($TenantFilter) { + if ($TenantFilter -eq 'AllTenants') { + # When requesting AllTenants, return only templates stored under AllTenants + $Templates = $Templates | Where-Object -Property tenantFilter -eq 'AllTenants' + } else { + # When requesting a specific tenant + if ($IncludeAllTenants) { + # Include both tenant-specific and AllTenants templates + $Templates = $Templates | Where-Object { $_.tenantFilter -eq $TenantFilter -or $_.tenantFilter -eq 'AllTenants' } + } else { + # Return only tenant-specific templates (exclude AllTenants) + $Templates = $Templates | Where-Object -Property tenantFilter -eq $TenantFilter + } + } + } + + # Sort by template name + $Templates = $Templates | Sort-Object -Property templateName + + # If a specific GUID is requested, filter to that template + if ($Request.query.GUID) { + $Templates = $Templates | Where-Object -Property GUID -eq $Request.query.GUID + } + + $Templates = ConvertTo-Json -InputObject @($Templates) -Depth 100 + + return ([HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::OK + Body = $Templates + }) +} diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-RemoveJITAdminTemplate.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-RemoveJITAdminTemplate.ps1 new file mode 100644 index 000000000000..e0ac56a8f294 --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-RemoveJITAdminTemplate.ps1 @@ -0,0 +1,47 @@ +function Invoke-RemoveJITAdminTemplate { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Identity.Role.ReadWrite + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + + try { + $ID = $Request.Query.ID ?? $Request.Body.ID + + if ([string]::IsNullOrWhiteSpace($ID)) { + throw 'ID is required' + } + + $Table = Get-CippTable -tablename 'templates' + $Filter = "PartitionKey eq 'JITAdminTemplate' and RowKey eq '$ID'" + $Template = Get-CIPPAzDataTableEntity @Table -Filter $Filter + + if ($Template) { + Remove-AzDataTableEntity @Table -Entity $Template + $Result = "Successfully deleted JIT Admin Template with ID: $ID" + Write-LogMessage -headers $Headers -API $APIName -message $Result -Sev 'Info' + $StatusCode = [HttpStatusCode]::OK + } else { + $Result = "JIT Admin Template with ID $ID not found" + Write-LogMessage -headers $Headers -API $APIName -message $Result -Sev 'Warning' + $StatusCode = [HttpStatusCode]::NotFound + } + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + $Result = "Failed to delete JIT Admin 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" } + }) +} From 5ae674d6acac8918d79d6dc3082f150b0c4bf370 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Tue, 13 Jan 2026 13:38:12 +0100 Subject: [PATCH 131/503] fix report creation --- .../Entrypoints/HTTP Functions/Invoke-AddTestReport.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-AddTestReport.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-AddTestReport.ps1 index 534dc7ed2896..c0038feec179 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-AddTestReport.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-AddTestReport.ps1 @@ -21,8 +21,8 @@ function Invoke-AddTestReport { # Generate a unique ID $ReportId = New-Guid - $IdentityTests = $Body.IdentityTests ? ($Body.IdentityTests.value | ConvertTo-Json) : '[]' - $DevicesTests = $Body.DevicesTests ? ($Body.DevicesTests.value | ConvertTo-Json) : '[]' + $IdentityTests = $Body.IdentityTests ? ($Body.IdentityTests | ConvertTo-Json -Compress) : '[]' + $DevicesTests = $Body.DevicesTests ? ($Body.DevicesTests | ConvertTo-Json -Compress) : '[]' # Create report object $Report = [PSCustomObject]@{ From e98244644b6cfb35c34fe5f426b79f2321499c07 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Tue, 13 Jan 2026 15:13:59 +0100 Subject: [PATCH 132/503] add-member incase object does not yet exist. --- .../Settings/Invoke-ExecJITAdminSettings.ps1 | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecJITAdminSettings.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecJITAdminSettings.ps1 index 9098562f5870..416c118bd238 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecJITAdminSettings.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecJITAdminSettings.ps1 @@ -1,4 +1,4 @@ -Function Invoke-ExecJITAdminSettings { +function Invoke-ExecJITAdminSettings { <# .FUNCTIONALITY Entrypoint @@ -19,9 +19,9 @@ Function Invoke-ExecJITAdminSettings { if (-not $JITAdminConfig) { $JITAdminConfig = @{ - PartitionKey = 'JITAdminSettings' - RowKey = 'JITAdminSettings' - MaxDuration = $null # null means no limit + PartitionKey = 'JITAdminSettings' + RowKey = 'JITAdminSettings' + MaxDuration = $null # null means no limit } } @@ -35,17 +35,17 @@ Function Invoke-ExecJITAdminSettings { } 'Set' { $MaxDuration = $Request.Body.MaxDuration.value - + Write-Host "MAx dur: $($MaxDuration)" # Validate ISO 8601 duration format if provided if (![string]::IsNullOrWhiteSpace($MaxDuration)) { try { # Test if it's a valid ISO 8601 duration $null = [System.Xml.XmlConvert]::ToTimeSpan($MaxDuration) - $JITAdminConfig.MaxDuration = $MaxDuration + $JITAdminConfig | Add-Member -NotePropertyName MaxDuration -NotePropertyValue $MaxDuration -Force } catch { $StatusCode = [HttpStatusCode]::BadRequest @{ - Results = "Error: Invalid ISO 8601 duration format. Expected format like PT4H, P1D, P4W, etc." + Results = 'Error: Invalid ISO 8601 duration format. Expected format like PT4H, P1D, P4W, etc.' } break } @@ -62,7 +62,7 @@ Function Invoke-ExecJITAdminSettings { $Message = if ($JITAdminConfig.MaxDuration) { "Successfully set JIT Admin maximum duration to $($JITAdminConfig.MaxDuration)" } else { - "Successfully removed JIT Admin maximum duration limit" + 'Successfully removed JIT Admin maximum duration limit' } Write-LogMessage -headers $Headers -API $APIName -message $Message -Sev 'Info' From 9d634a5725210e1a3690a03bf4401a244ccc756f Mon Sep 17 00:00:00 2001 From: John Duprey Date: Tue, 13 Jan 2026 15:58:48 -0500 Subject: [PATCH 133/503] AnonReportDisable to EnrollmentWindowsHelloForBusinessConfiguration Updated multiple standards scripts to use 'CurrentValue' and 'ExpectedValue' objects in Set-CIPPStandardsCompareField for improved reporting consistency. Also fixed minor formatting, error handling, and parameter validation issues across several scripts. --- .../Invoke-CIPPStandardAnonReportDisable.ps1 | 1 - .../Invoke-CIPPStandardDefaultSharingLink.ps1 | 28 +++++- .../Invoke-CIPPStandardDelegateSentItems.ps1 | 17 +++- ...voke-CIPPStandardDeletedUserRentention.ps1 | 19 ++-- ...CIPPStandardDeployCheckChromeExtension.ps1 | 20 +++- ...oke-CIPPStandardDeployContactTemplates.ps1 | 98 ++++++++++--------- .../Invoke-CIPPStandardDeployMailContact.ps1 | 18 ++-- ...PStandardDisableAddShortcutsToOneDrive.ps1 | 20 ++-- ...ndardDisableAdditionalStorageProviders.ps1 | 13 ++- .../Invoke-CIPPStandardDisableAppCreation.ps1 | 12 ++- ...nvoke-CIPPStandardDisableBasicAuthSMTP.ps1 | 21 ++-- .../Invoke-CIPPStandardDisableEmail.ps1 | 16 +-- .../Invoke-CIPPStandardDisableEntraPortal.ps1 | 1 - ...tandardDisableExchangeOnlinePowerShell.ps1 | 10 +- ...StandardDisableExternalCalendarSharing.ps1 | 20 ++-- ...voke-CIPPStandardDisableGuestDirectory.ps1 | 22 +++-- .../Invoke-CIPPStandardDisableGuests.ps1 | 16 ++- ...voke-CIPPStandardDisableM365GroupUsers.ps1 | 20 ++-- ...nvoke-CIPPStandardDisableOutlookAddins.ps1 | 17 ++-- .../Invoke-CIPPStandardDisableQRCodePin.ps1 | 10 +- .../Invoke-CIPPStandardDisableReshare.ps1 | 18 ++-- ...oke-CIPPStandardDisableResourceMailbox.ps1 | 21 ++-- .../Invoke-CIPPStandardDisableSMS.ps1 | 16 ++- ...-CIPPStandardDisableSecurityGroupUsers.ps1 | 16 +-- ...CIPPStandardDisableSelfServiceLicenses.ps1 | 1 - ...IPPStandardDisableSharePointLegacyAuth.ps1 | 18 ++-- ...nvoke-CIPPStandardDisableSharedMailbox.ps1 | 20 ++-- .../Invoke-CIPPStandardDisableTNEF.ps1 | 14 ++- ...voke-CIPPStandardDisableTenantCreation.ps1 | 15 ++- ...voke-CIPPStandardDisableUserSiteCreate.ps1 | 18 ++-- .../Invoke-CIPPStandardDisableViva.ps1 | 15 ++- .../Invoke-CIPPStandardDisableVoice.ps1 | 15 ++- ...oke-CIPPStandardDisablex509Certificate.ps1 | 16 +-- ...e-CIPPStandardEnableAppConsentRequests.ps1 | 19 ++-- ...voke-CIPPStandardEnableCustomerLockbox.ps1 | 14 ++- .../Invoke-CIPPStandardEnableFIDO2.ps1 | 16 +-- ...Invoke-CIPPStandardEnableHardwareOAuth.ps1 | 18 ++-- ...nvoke-CIPPStandardEnableLitigationHold.ps1 | 13 ++- .../Invoke-CIPPStandardEnableMailTips.ps1 | 15 ++- ...voke-CIPPStandardEnableMailboxAuditing.ps1 | 11 ++- ...ke-CIPPStandardEnableNamePronunciation.ps1 | 11 ++- ...voke-CIPPStandardEnableOnlineArchiving.ps1 | 15 ++- .../Invoke-CIPPStandardEnablePronouns.ps1 | 12 ++- ...ntWindowsHelloForBusinessConfiguration.ps1 | 44 +++++---- .../Invoke-CIPPStandarddisableMacSync.ps1 | 22 +++-- 45 files changed, 534 insertions(+), 278 deletions(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAnonReportDisable.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAnonReportDisable.ps1 index 6d58254efa8d..6d924f41cdef 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAnonReportDisable.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAnonReportDisable.ps1 @@ -68,7 +68,6 @@ function Invoke-CIPPStandardAnonReportDisable { } } if ($Settings.report -eq $true) { - $StateIsCorrect = $CurrentInfo.displayConcealedNames ? $false : $true Set-CIPPStandardsCompareField -FieldName 'standards.AnonReportDisable' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $tenant Add-CIPPBPAField -FieldName 'AnonReport' -FieldValue $CurrentInfo.displayConcealedNames -StoreAs bool -Tenant $tenant } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDefaultSharingLink.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDefaultSharingLink.ps1 index 409ad97db6bb..d3397e85af50 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDefaultSharingLink.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDefaultSharingLink.ps1 @@ -36,7 +36,7 @@ function Invoke-CIPPStandardDefaultSharingLink { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'DefaultSharingLink' -TenantFilter $Tenant -RequiredCapabilities @('SHAREPOINTWAC', 'SHAREPOINTSTANDARD', 'SHAREPOINTENTERPRISE', 'SHAREPOINTENTERPRISE_EDU','ONEDRIVE_BASIC', 'ONEDRIVE_ENTERPRISE') + $TestResult = Test-CIPPStandardLicense -StandardName 'DefaultSharingLink' -TenantFilter $Tenant -RequiredCapabilities @('SHAREPOINTWAC', 'SHAREPOINTSTANDARD', 'SHAREPOINTENTERPRISE', 'SHAREPOINTENTERPRISE_EDU', 'ONEDRIVE_BASIC', 'ONEDRIVE_ENTERPRISE') # Determine the desired sharing link type (default to Internal if not specified) @@ -56,14 +56,32 @@ function Invoke-CIPPStandardDefaultSharingLink { try { $CurrentState = Get-CIPPSPOTenant -TenantFilter $Tenant | - Select-Object -Property _ObjectIdentity_, TenantFilter, DefaultSharingLinkType, DefaultLinkPermission - } - catch { + Select-Object -Property _ObjectIdentity_, TenantFilter, DefaultSharingLinkType, DefaultLinkPermission + } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the DefaultSharingLink state for $Tenant. Error: $ErrorMessage" -Sev Error return } + $CurrentValue = [PSCustomObject]@{ + DefaultSharingLinkType = switch ($CurrentState.DefaultSharingLinkType) { + 1 { 'Direct' } + 2 { 'Internal' } + 3 { 'Anyone' } + default { 'Unknown' } + } + DefaultLinkPermission = switch ($CurrentState.DefaultLinkPermission) { + 0 { 'Edit' } + 1 { 'View' } + 2 { 'Edit' } + default { 'Unknown' } + } + } + $ExpectedValue = [PSCustomObject]@{ + DefaultSharingLinkType = $DesiredSharingLinkType + DefaultLinkPermission = 'View' + } + # Check if the current state matches the desired configuration $StateIsCorrect = ($CurrentState.DefaultSharingLinkType -eq $DesiredSharingLinkTypeValue) -and ($CurrentState.DefaultLinkPermission -eq 1) Write-Host "currentstate: $($CurrentState.DefaultSharingLinkType), $($CurrentState.DefaultLinkPermission). Desired: $DesiredSharingLinkTypeValue, 1" @@ -117,6 +135,6 @@ function Invoke-CIPPStandardDefaultSharingLink { } else { $FieldValue = $CurrentState } - Set-CIPPStandardsCompareField -FieldName 'standards.DefaultSharingLink' -FieldValue $FieldValue -Tenant $Tenant + Set-CIPPStandardsCompareField -FieldName 'standards.DefaultSharingLink' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -Tenant $Tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDelegateSentItems.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDelegateSentItems.ps1 index 14c3d735e600..ac95dcd2b1fd 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDelegateSentItems.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDelegateSentItems.ps1 @@ -47,15 +47,23 @@ function Invoke-CIPPStandardDelegateSentItems { if ($Settings.IncludeUserMailboxes -eq $true) { $Mailboxes = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-Mailbox' -cmdParams @{ RecipientTypeDetails = @('UserMailbox', 'SharedMailbox') } -Select 'Identity,UserPrincipalName,MessageCopyForSendOnBehalfEnabled,MessageCopyForSentAsEnabled' | - Where-Object { $_.MessageCopyForSendOnBehalfEnabled -eq $false -or $_.MessageCopyForSentAsEnabled -eq $false } + Where-Object { $_.MessageCopyForSendOnBehalfEnabled -eq $false -or $_.MessageCopyForSentAsEnabled -eq $false } } else { $Mailboxes = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-Mailbox' -cmdParams @{ RecipientTypeDetails = @('SharedMailbox') } -Select 'Identity,UserPrincipalName,MessageCopyForSendOnBehalfEnabled,MessageCopyForSentAsEnabled' | - Where-Object { $_.MessageCopyForSendOnBehalfEnabled -eq $false -or $_.MessageCopyForSentAsEnabled -eq $false } + Where-Object { $_.MessageCopyForSendOnBehalfEnabled -eq $false -or $_.MessageCopyForSentAsEnabled -eq $false } } + $CurrentValue = if (!$Mailboxes) { + [PSCustomObject]@{ state = 'Configured correctly' } + } else { + [PSCustomObject]@{ NonCompliantMailboxes = $Mailboxes | Select-Object -Property UserPrincipalName, MessageCopyForSendOnBehalfEnabled, MessageCopyForSentAsEnabled } + } + $ExpectedValue = [PSCustomObject]@{ + state = 'Configured correctly' + } Write-Host "Mailboxes: $($Mailboxes.Count)" - If ($Settings.remediate -eq $true) { + if ($Settings.remediate -eq $true) { Write-Host 'Time to remediate' if ($Mailboxes) { @@ -97,8 +105,7 @@ function Invoke-CIPPStandardDelegateSentItems { if ($Settings.report -eq $true) { $Filtered = $Mailboxes | Select-Object -Property UserPrincipalName, MessageCopyForSendOnBehalfEnabled, MessageCopyForSentAsEnabled - $CurrentState = if ($null -eq $Mailboxes) { $true } else { $Filtered } - Set-CIPPStandardsCompareField -FieldName 'standards.DelegateSentItems' -FieldValue $CurrentState -TenantFilter $Tenant + Set-CIPPStandardsCompareField -FieldName 'standards.DelegateSentItems' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant Add-CIPPBPAField -FieldName 'DelegateSentItems' -FieldValue $Filtered -StoreAs json -Tenant $Tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeletedUserRentention.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeletedUserRentention.ps1 index 41172643c22e..ccb08c900fe3 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeletedUserRentention.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeletedUserRentention.ps1 @@ -31,7 +31,7 @@ function Invoke-CIPPStandardDeletedUserRentention { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'DeletedUserRentention' -TenantFilter $Tenant -RequiredCapabilities @('SHAREPOINTWAC', 'SHAREPOINTSTANDARD', 'SHAREPOINTENTERPRISE', 'SHAREPOINTENTERPRISE_EDU','ONEDRIVE_BASIC', 'ONEDRIVE_ENTERPRISE') + $TestResult = Test-CIPPStandardLicense -StandardName 'DeletedUserRentention' -TenantFilter $Tenant -RequiredCapabilities @('SHAREPOINTWAC', 'SHAREPOINTSTANDARD', 'SHAREPOINTENTERPRISE', 'SHAREPOINTENTERPRISE_EDU', 'ONEDRIVE_BASIC', 'ONEDRIVE_ENTERPRISE') ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'DeletedUserRetention' if ($TestResult -eq $false) { @@ -41,17 +41,22 @@ function Invoke-CIPPStandardDeletedUserRentention { try { $CurrentInfo = New-GraphGetRequest -Uri 'https://graph.microsoft.com/beta/admin/sharepoint/settings' -tenantid $Tenant -AsApp $true - } - catch { + } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the DeletedUserRetention state for $Tenant. Error: $ErrorMessage" -Sev Error return } $Days = $Settings.Days.value ?? $Settings.Days + $ExpectedValue = [PSCustomObject]@{ + DeletedUserRententionDays = [int]$Days + } + $CurrentValue = [PSCustomObject]@{ + DeletedUserRententionDays = $CurrentInfo.deletedUserPersonalSiteRetentionPeriodInDays + } + if ($Settings.report -eq $true) { - $CurrentState = $CurrentInfo.deletedUserPersonalSiteRetentionPeriodInDays -eq $Days ? $true : ($CurrentInfo | Select-Object deletedUserPersonalSiteRetentionPeriodInDays) - Set-CIPPStandardsCompareField -FieldName 'standards.DeletedUserRentention' -FieldValue $CurrentState -TenantFilter $Tenant + Set-CIPPStandardsCompareField -FieldName 'standards.DeletedUserRentention' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant Add-CIPPBPAField -FieldName 'DeletedUserRentention' -FieldValue $CurrentInfo.deletedUserPersonalSiteRetentionPeriodInDays -StoreAs string -Tenant $Tenant } @@ -60,7 +65,7 @@ function Invoke-CIPPStandardDeletedUserRentention { # Input validation if (($Days -eq 'Select a value') -and ($Settings.remediate -eq $true -or $Settings.alert -eq $true)) { Write-LogMessage -API 'Standards' -tenant $Tenant -message 'DeletedUserRentention: Invalid Days parameter set' -sev Error - Return + return } # Backwards compatibility for v5.9.4 and back @@ -72,7 +77,7 @@ function Invoke-CIPPStandardDeletedUserRentention { $StateSetCorrectly = if ($CurrentInfo.deletedUserPersonalSiteRetentionPeriodInDays -eq $WantedState) { $true } else { $false } - If ($Settings.remediate -eq $true) { + if ($Settings.remediate -eq $true) { Write-Host 'Time to remediate' if ($StateSetCorrectly -eq $false) { diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployCheckChromeExtension.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployCheckChromeExtension.ps1 index 664e27fec20c..921fe6d5db0f 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployCheckChromeExtension.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployCheckChromeExtension.ps1 @@ -65,13 +65,18 @@ function Invoke-CIPPStandardDeployCheckChromeExtension { $ChromePolicyName = 'Deploy Check Chrome Extension (Chrome)' $EdgePolicyName = 'Deploy Check Chrome Extension (Edge)' + # CIPP Url + $CippConfigTable = Get-CippTable -tablename Config + $CippConfig = Get-CIPPAzDataTableEntity @CippConfigTable -Filter "PartitionKey eq 'InstanceProperties' and RowKey eq 'CIPPURL'" + $CIPPURL = 'https://{0}' -f $CippConfig.Value + # Get configuration values with defaults $ShowNotifications = $Settings.showNotifications ?? $true $EnableValidPageBadge = $Settings.enableValidPageBadge ?? $true $EnablePageBlocking = $Settings.enablePageBlocking ?? $true $EnableCippReporting = $Settings.enableCippReporting ?? $true - $CippServerUrl = $Settings.cippServerUrl - $CippTenantId = $Settings.cippTenantId + $CippServerUrl = $CIPPURL + $CippTenantId = $Tenant $CustomRulesUrl = $Settings.customRulesUrl $UpdateInterval = $Settings.updateInterval ?? 24 $EnableDebugLogging = $Settings.enableDebugLogging ?? $false @@ -212,7 +217,16 @@ function Invoke-CIPPStandardDeployCheckChromeExtension { if ($Settings.report -eq $true) { $StateIsCorrect = $ChromePolicyExists -and $EdgePolicyExists - Set-CIPPStandardsCompareField -FieldName 'standards.DeployCheckChromeExtension' -FieldValue $StateIsCorrect -TenantFilter $Tenant + + $ExpectedValue = [PSCustomObject]@{ + ChromePolicyDeployed = $true + EdgePolicyDeployed = $true + } + $CurrentValue = [PSCustomObject]@{ + ChromePolicyDeployed = $ChromePolicyExists + EdgePolicyDeployed = $EdgePolicyExists + } + Set-CIPPStandardsCompareField -FieldName 'standards.DeployCheckChromeExtension' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant Add-CIPPBPAField -FieldName 'DeployCheckChromeExtension' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $Tenant } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployContactTemplates.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployContactTemplates.ps1 index c5a6ce29d1f6..067feb5c567d 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployContactTemplates.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployContactTemplates.ps1 @@ -58,8 +58,7 @@ function Invoke-CIPPStandardDeployContactTemplates { } return $StoredTemplate.JSON | ConvertFrom-Json - } - catch { + } catch { Write-LogMessage -API $APIName -tenant $Tenant -message "Failed to retrieve template $TemplateGUID. Error: $($_.Exception.Message)" -sev Error return $null } @@ -75,8 +74,8 @@ function Invoke-CIPPStandardDeployContactTemplates { # Get templateIds array if (-not $Settings.templateIds -or $Settings.templateIds.Count -eq 0) { - Write-LogMessage -API $APIName -tenant $Tenant -message "DeployContactTemplate: No template IDs found in settings" -sev Error - return "No template IDs found in settings" + Write-LogMessage -API $APIName -tenant $Tenant -message 'DeployContactTemplate: No template IDs found in settings' -sev Error + return 'No template IDs found in settings' } Write-LogMessage -API $APIName -tenant $Tenant -message "DeployContactTemplate: Processing $($Settings.templateIds.Count) template(s)" -sev Info @@ -91,7 +90,7 @@ function Invoke-CIPPStandardDeployContactTemplates { $TemplateGUID = $TemplateItem.value if ([string]::IsNullOrWhiteSpace($TemplateGUID)) { - Write-LogMessage -API $APIName -tenant $Tenant -message "DeployContactTemplate: TemplateGUID cannot be empty." -sev Error + Write-LogMessage -API $APIName -tenant $Tenant -message 'DeployContactTemplate: TemplateGUID cannot be empty.' -sev Error continue } @@ -115,8 +114,7 @@ function Invoke-CIPPStandardDeployContactTemplates { # Validate email address format try { $null = [System.Net.Mail.MailAddress]::new($Template.email) - } - catch { + } catch { Write-LogMessage -API $APIName -tenant $Tenant -message "DeployContactTemplate: Invalid email address format: $($Template.email)" -sev Error continue } @@ -127,39 +125,37 @@ function Invoke-CIPPStandardDeployContactTemplates { # If the contact exists, we'll overwrite it; if not, we'll create it if ($ExistingContact) { $StateIsCorrect = $false # Always update existing contacts to match template - $Action = "Update" + $Action = 'Update' $Missing = $false - } - else { + } else { # Contact doesn't exist, needs to be created $StateIsCorrect = $false - $Action = "Create" + $Action = 'Create' $Missing = $true } [PSCustomObject]@{ - missing = $Missing - StateIsCorrect = $StateIsCorrect - Action = $Action - Template = $Template - TemplateGUID = $TemplateGUID + missing = $Missing + StateIsCorrect = $StateIsCorrect + Action = $Action + Template = $Template + TemplateGUID = $TemplateGUID } - } - catch { + } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message $Message = "Failed to process template $TemplateGUID, Error: $ErrorMessage" Write-LogMessage -API $APIName -tenant $tenant -message $Message -sev 'Error' - Return $Message + return $Message } } # Remediate each contact which needs to be created or updated - If ($RemediateEnabled) { + if ($RemediateEnabled) { $ContactsToProcess = $CompareList | Where-Object { $_.StateIsCorrect -eq $false } if ($ContactsToProcess.Count -gt 0) { - $ContactsToCreate = $ContactsToProcess | Where-Object { $_.Action -eq "Create" } - $ContactsToUpdate = $ContactsToProcess | Where-Object { $_.Action -eq "Update" } + $ContactsToCreate = $ContactsToProcess | Where-Object { $_.Action -eq 'Create' } + $ContactsToUpdate = $ContactsToProcess | Where-Object { $_.Action -eq 'Update' } Write-LogMessage -API $APIName -tenant $Tenant -message "DeployContactTemplate: Processing $($ContactsToCreate.Count) new contacts, $($ContactsToUpdate.Count) existing contacts" -sev Info @@ -192,13 +188,12 @@ function Invoke-CIPPStandardDeployContactTemplates { # Store contact info for second pass $ProcessedContacts.Add([PSCustomObject]@{ - Contact = $Contact - ContactObject = $NewContact - Template = $Template - IsNew = $true - }) - } - catch { + Contact = $Contact + ContactObject = $NewContact + Template = $Template + IsNew = $true + }) + } catch { $ProcessingFailures++ $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API $APIName -tenant $tenant -message "Failed to create contact $($Template.displayName): $ErrorMessage" -sev 'Error' @@ -213,7 +208,7 @@ function Invoke-CIPPStandardDeployContactTemplates { # Update MailContact properties (email address) $UpdateMailContactParams = @{ - Identity = $ExistingContact.Identity + Identity = $ExistingContact.Identity ExternalEmailAddress = $Template.email } @@ -242,13 +237,12 @@ function Invoke-CIPPStandardDeployContactTemplates { # Store contact info for second pass $ProcessedContacts.Add([PSCustomObject]@{ - Contact = $Contact - ContactObject = $ExistingContact - Template = $Template - IsNew = $false - }) - } - catch { + Contact = $Contact + ContactObject = $ExistingContact + Template = $Template + IsNew = $false + }) + } catch { $ProcessingFailures++ $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API $APIName -tenant $tenant -message "Failed to update contact $($Template.displayName): $ErrorMessage" -sev 'Error' @@ -326,8 +320,7 @@ function Invoke-CIPPStandardDeployContactTemplates { $null = New-ExoRequest -tenantid $Tenant -cmdlet 'Set-MailContact' -cmdParams $MailContactParams -UseSystemMailbox $true } } - } - catch { + } catch { $UpdateFailures++ $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API $APIName -tenant $tenant -message "Failed to update additional fields for contact $($Template.displayName): $ErrorMessage" -sev 'Error' @@ -357,25 +350,36 @@ function Invoke-CIPPStandardDeployContactTemplates { if ($Contact.missing) { $CurrentInfo = $Contact.Template | Select-Object -Property displayName, email, missing Write-StandardsAlert -message "Mail contact $($Contact.Template.displayName) from template $($Contact.TemplateGUID) is missing." -object $CurrentInfo -tenant $Tenant -standardName 'DeployContactTemplate' - } - else { - $CurrentInfo = $CurrentContacts | Where-Object -Property DisplayName -eq $Contact.Template.displayName | Select-Object -Property DisplayName, ExternalEmailAddress, FirstName, LastName + } else { + $CurrentInfo = $CurrentContacts | Where-Object -Property DisplayName -EQ $Contact.Template.displayName | Select-Object -Property DisplayName, ExternalEmailAddress, FirstName, LastName Write-StandardsAlert -message "Mail contact $($Contact.Template.displayName) from template $($Contact.TemplateGUID) will be updated to match template." -object $CurrentInfo -tenant $Tenant -standardName 'DeployContactTemplate' } } Write-LogMessage -API $APIName -tenant $Tenant -message "DeployContactTemplate: $MissingContacts missing, $ExistingContacts to update" -sev Info } else { - Write-LogMessage -API $APIName -tenant $Tenant -message "DeployContactTemplate: No contacts need processing" -sev Info + Write-LogMessage -API $APIName -tenant $Tenant -message 'DeployContactTemplate: No contacts need processing' -sev Info } } if ($ReportEnabled) { - foreach ($Contact in $CompareList) { - Set-CIPPStandardsCompareField -FieldName "standards.DeployContactTemplate" -FieldValue $Contact.StateIsCorrect -TenantFilter $Tenant + $ExpectedValue = [PSCustomObject]@{ + state = 'Correctly configured' + } + $CurrentValue = if ($CompareList.StateIsCorrect -eq $true) { + [PSCustomObject]@{ state = 'Correctly configured' } + } else { + [PSCustomObject]@{ + MissingContacts = $CompareList | Where-Object { $_.missing } | ForEach-Object { + $_.Template | Select-Object -Property displayName, Email + } + ContactsToUpdate = $CompareList | Where-Object { -not $_.missing } | ForEach-Object { + $_.Template | Select-Object -Property displayName, Email + } + } } + Set-CIPPStandardsCompareField -FieldName 'standards.DeployContactTemplate' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant } - } - catch { + } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API $APIName -tenant $tenant -message "Failed to create or update mail contact(s) from templates, Error: $ErrorMessage" -sev 'Error' } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployMailContact.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployMailContact.ps1 index 01b2c06a8ae6..4ee810e1f85a 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployMailContact.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployMailContact.ps1 @@ -50,8 +50,7 @@ function Invoke-CIPPStandardDeployMailContact { try { $null = [System.Net.Mail.MailAddress]::new($Settings.ExternalEmailAddress) - } - catch { + } catch { Write-LogMessage -API 'Standards' -tenant $Tenant -message "DeployMailContact: Invalid email address format: $($Settings.ExternalEmailAddress)" -sev Error return } @@ -70,12 +69,10 @@ function Invoke-CIPPStandardDeployMailContact { Identity = $Settings.ExternalEmailAddress ErrorAction = 'Stop' } - } - catch { + } catch { if ($_.Exception.Message -like "*couldn't be found*") { $ExistingContact = $null - } - else { + } else { Write-LogMessage -API 'Standards' -tenant $Tenant -message "Error checking for existing mail contact: $(Get-CippException -Exception $_).NormalizedError" -sev Error return } @@ -88,8 +85,7 @@ function Invoke-CIPPStandardDeployMailContact { $NewContactParams.Name = $Settings.DisplayName $null = New-ExoRequest -tenantid $Tenant -cmdlet 'New-MailContact' -cmdParams $NewContactParams Write-LogMessage -API 'Standards' -tenant $Tenant -message "Successfully created mail contact $($Settings.DisplayName) with email $($Settings.ExternalEmailAddress)" -sev Info - } - catch { + } catch { Write-LogMessage -API 'Standards' -tenant $Tenant -message "Could not create mail contact. $(Get-CippException -Exception $_).NormalizedError" -sev Error } } @@ -98,8 +94,7 @@ function Invoke-CIPPStandardDeployMailContact { if ($Settings.alert -eq $true) { if ($ExistingContact) { Write-LogMessage -API 'Standards' -tenant $Tenant -message "Mail contact $($Settings.DisplayName) already exists" -sev Info - } - else { + } else { Write-StandardsAlert -message "Mail contact $($Settings.DisplayName) needs to be created" -object $ContactData -tenant $Tenant -standardName 'DeployMailContact' -standardId $Settings.standardId Write-LogMessage -API 'Standards' -tenant $Tenant -message "Mail contact $($Settings.DisplayName) needs to be created" -sev Info } @@ -108,8 +103,9 @@ function Invoke-CIPPStandardDeployMailContact { # Report if ($Settings.report -eq $true) { $ReportData = $ContactData.Clone() + $CurrentValue = $ExistingContact | Select-Object DisplayName, ExternalEmailAddress, FirstName, LastName $ReportData.Exists = [bool]$ExistingContact Add-CIPPBPAField -FieldName 'DeployMailContact' -FieldValue $ReportData -StoreAs json -Tenant $Tenant - Set-CIPPStandardsCompareField -FieldName 'standards.DeployMailContact' -FieldValue $($ExistingContact ? $true : $ReportData) -Tenant $Tenant + Set-CIPPStandardsCompareField -FieldName 'standards.DeployMailContact' -CurrentValue $CurrentValue -ExpectedValue $ReportData -Tenant $Tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableAddShortcutsToOneDrive.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableAddShortcutsToOneDrive.ps1 index 248c250d6b6f..5b43e27dbee0 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableAddShortcutsToOneDrive.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableAddShortcutsToOneDrive.ps1 @@ -31,7 +31,7 @@ function Invoke-CIPPStandardDisableAddShortcutsToOneDrive { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'DisableAddShortcutsToOneDrive' -TenantFilter $Tenant -RequiredCapabilities @('SHAREPOINTWAC', 'SHAREPOINTSTANDARD', 'SHAREPOINTENTERPRISE', 'SHAREPOINTENTERPRISE_EDU','ONEDRIVE_BASIC', 'ONEDRIVE_ENTERPRISE') + $TestResult = Test-CIPPStandardLicense -StandardName 'DisableAddShortcutsToOneDrive' -TenantFilter $Tenant -RequiredCapabilities @('SHAREPOINTWAC', 'SHAREPOINTSTANDARD', 'SHAREPOINTENTERPRISE', 'SHAREPOINTENTERPRISE_EDU', 'ONEDRIVE_BASIC', 'ONEDRIVE_ENTERPRISE') ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'DisableAddShortcutsToOneDrive' if ($TestResult -eq $false) { @@ -41,9 +41,8 @@ function Invoke-CIPPStandardDisableAddShortcutsToOneDrive { try { $CurrentState = Get-CIPPSPOTenant -TenantFilter $Tenant | - Select-Object _ObjectIdentity_, TenantFilter, DisableAddToOneDrive - } - catch { + Select-Object _ObjectIdentity_, TenantFilter, DisableAddToOneDrive + } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the DisableAddShortcutsToOneDrive state for $Tenant. Error: $ErrorMessage" -Sev Error return @@ -53,7 +52,14 @@ function Invoke-CIPPStandardDisableAddShortcutsToOneDrive { $StateValue = $Settings.state.value ?? $Settings.state if (([string]::IsNullOrWhiteSpace($StateValue) -or $StateValue -eq 'Select a value') -and ($Settings.remediate -eq $true -or $Settings.alert -eq $true)) { Write-LogMessage -API 'Standards' -tenant $tenant -message 'DisableAddShortcutsToOneDrive: Invalid state parameter set' -sev Error - Return + return + } + + $CurrentValue = [PSCustomObject]@{ + DisableAddShortcutsToOneDrive = $CurrentState.DisableAddToOneDrive + } + $ExpectedValue = [PSCustomObject]@{ + DisableAddShortcutsToOneDrive = [System.Convert]::ToBoolean($StateValue) } $WantedState = [System.Convert]::ToBoolean($StateValue) @@ -66,11 +72,11 @@ function Invoke-CIPPStandardDisableAddShortcutsToOneDrive { } else { $FieldValue = $CurrentState | Select-Object -Property DisableAddToOneDrive } - Set-CIPPStandardsCompareField -FieldName 'standards.DisableAddShortcutsToOneDrive' -FieldValue $FieldValue -TenantFilter $Tenant + Set-CIPPStandardsCompareField -FieldName 'standards.DisableAddShortcutsToOneDrive' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant Add-CIPPBPAField -FieldName 'OneDriveAddShortcutButtonDisabled' -FieldValue $CurrentState.DisableAddToOneDrive -StoreAs bool -Tenant $Tenant } - If ($Settings.remediate -eq $true) { + if ($Settings.remediate -eq $true) { Write-Host 'Time to remediate' if ($StateIsCorrect -eq $false) { diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableAdditionalStorageProviders.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableAdditionalStorageProviders.ps1 index e3768e11b933..5a83612fc3b9 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableAdditionalStorageProviders.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableAdditionalStorageProviders.ps1 @@ -50,6 +50,13 @@ function Invoke-CIPPStandardDisableAdditionalStorageProviders { return } + $CurrentValue = [PSCustomObject]@{ + AdditionalStorageProvidersAvailable = $AdditionalStorageProvidersState.AdditionalStorageProvidersAvailable + } + $ExpectedValue = [PSCustomObject]@{ + AdditionalStorageProvidersAvailable = $false + } + if ($Settings.remediate -eq $true) { try { @@ -78,8 +85,8 @@ function Invoke-CIPPStandardDisableAdditionalStorageProviders { } if ($Settings.report -eq $true) { - $State = $AdditionalStorageProvidersState.AdditionalStorageProvidersEnabled ? $false : $true - Set-CIPPStandardsCompareField -FieldName 'standards.DisableAdditionalStorageProviders' -FieldValue $State -TenantFilter $Tenant - Add-CIPPBPAField -FieldName 'AdditionalStorageProvidersEnabled' -FieldValue $State -StoreAs bool -Tenant $Tenant + $State = $AdditionalStorageProvidersState.AdditionalStorageProvidersAvailable ? $false : $true + Set-CIPPStandardsCompareField -FieldName 'standards.DisableAdditionalStorageProviders' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant + Add-CIPPBPAField -FieldName 'AdditionalStorageProvidersAvailable' -FieldValue $State -StoreAs bool -Tenant $Tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableAppCreation.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableAppCreation.ps1 index a95bb94a6278..f0620e414878 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableAppCreation.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableAppCreation.ps1 @@ -42,14 +42,18 @@ function Invoke-CIPPStandardDisableAppCreation { try { $CurrentInfo = New-GraphGetRequest -Uri 'https://graph.microsoft.com/beta/policies/authorizationPolicy/authorizationPolicy?$select=defaultUserRolePermissions' -tenantid $Tenant - } - catch { + } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the DisableAppCreation state for $Tenant. Error: $ErrorMessage" -Sev Error return } - If ($Settings.remediate -eq $true) { + $CurrentValue = $CurrentInfo.defaultUserRolePermissions | Select-Object -Property allowedToCreateApps + $ExpectedValue = [PSCustomObject]@{ + allowedToCreateApps = $false + } + + if ($Settings.remediate -eq $true) { if ($CurrentInfo.defaultUserRolePermissions.allowedToCreateApps -eq $false) { Write-LogMessage -API 'Standards' -tenant $tenant -message 'Users are already not allowed to create App registrations.' -sev Info } else { @@ -77,7 +81,7 @@ function Invoke-CIPPStandardDisableAppCreation { if ($Settings.report -eq $true) { $State = -not $CurrentInfo.defaultUserRolePermissions.allowedToCreateApps - Set-CIPPStandardsCompareField -FieldName 'standards.DisableAppCreation' -FieldValue $State -TenantFilter $Tenant + Set-CIPPStandardsCompareField -FieldName 'standards.DisableAppCreation' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant Add-CIPPBPAField -FieldName 'UserAppCreationDisabled' -FieldValue $State -StoreAs bool -Tenant $tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableBasicAuthSMTP.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableBasicAuthSMTP.ps1 index d2a75761de75..5d5e4b55f0c3 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableBasicAuthSMTP.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableBasicAuthSMTP.ps1 @@ -46,15 +46,14 @@ function Invoke-CIPPStandardDisableBasicAuthSMTP { $CurrentInfo = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-TransportConfig' $SMTPusers = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-CASMailbox' -cmdParams @{ ResultSize = 'Unlimited' } | - Where-Object { ($_.SmtpClientAuthenticationDisabled -eq $false) } - } - catch { + Where-Object { ($_.SmtpClientAuthenticationDisabled -eq $false) } + } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the DisableBasicAuthSMTP state for $Tenant. Error: $ErrorMessage" -Sev Error return } - If ($Settings.remediate -eq $true) { + if ($Settings.remediate -eq $true) { Write-Host 'Time to remediate' if ($CurrentInfo.SmtpClientAuthenticationDisabled -and $SMTPusers.Count -eq 0) { @@ -108,12 +107,22 @@ function Invoke-CIPPStandardDisableBasicAuthSMTP { } if ($Settings.report -eq $true) { + + $CurrentValue = [PSCustomObject]@{ + SmtpClientAuthenticationDisabled = $CurrentInfo.SmtpClientAuthenticationDisabled + UsersWithSmtpAuthEnabled = @($SMTPusers.PrimarySmtpAddress) + } + $ExpectedValue = [PSCustomObject]@{ + SmtpClientAuthenticationDisabled = $true + UsersWithSmtpAuthEnabled = @() + } + + Set-CIPPStandardsCompareField -FieldName 'standards.DisableBasicAuthSMTP' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant + if ($CurrentInfo.SmtpClientAuthenticationDisabled -and $SMTPusers.Count -eq 0) { - Set-CIPPStandardsCompareField -FieldName 'standards.DisableBasicAuthSMTP' -FieldValue $true -TenantFilter $Tenant Add-CIPPBPAField -FieldName 'DisableBasicAuthSMTP' -FieldValue $CurrentInfo.SmtpClientAuthenticationDisabled -StoreAs bool -Tenant $tenant } else { $Logs = $LogMessage | Select-Object @{n = 'Message'; e = { $_ } } - Set-CIPPStandardsCompareField -FieldName 'standards.DisableBasicAuthSMTP' -FieldValue $logs -TenantFilter $Tenant Add-CIPPBPAField -FieldName 'DisableBasicAuthSMTP' -FieldValue $Logs -StoreAs json -Tenant $tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableEmail.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableEmail.ps1 index 58c0c89e84db..31729038577c 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableEmail.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableEmail.ps1 @@ -32,19 +32,17 @@ function Invoke-CIPPStandardDisableEmail { #> param($Tenant, $Settings) - ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'DisableEmail' try { $CurrentState = New-GraphGetRequest -Uri 'https://graph.microsoft.com/beta/policies/authenticationmethodspolicy/authenticationMethodConfigurations/Email' -tenantid $Tenant - } - catch { + } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the DisableEmail state for $Tenant. Error: $ErrorMessage" -Sev Error return } $StateIsCorrect = ($CurrentState.state -eq 'disabled') - If ($Settings.remediate -eq $true) { + if ($Settings.remediate -eq $true) { if ($StateIsCorrect -eq $true) { Write-LogMessage -API 'Standards' -tenant $tenant -message 'Email authentication method is already disabled.' -sev Info } else { @@ -65,8 +63,14 @@ function Invoke-CIPPStandardDisableEmail { } if ($Settings.report -eq $true) { - $state = $StateIsCorrect -eq $true ? $true : $CurrentState - Set-CIPPStandardsCompareField -FieldName 'standards.DisableEmail' -FieldValue $state -TenantFilter $Tenant + $CurrentValue = [PSCustomObject]@{ + DisableEmail = $CurrentState.state -eq 'disabled' + } + $ExpectedValue = [PSCustomObject]@{ + DisableEmail = $true + } + + Set-CIPPStandardsCompareField -FieldName 'standards.DisableEmail' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant Add-CIPPBPAField -FieldName 'DisableEmail' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableEntraPortal.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableEntraPortal.ps1 index 1f791cbd4b26..bd774e10251c 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableEntraPortal.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableEntraPortal.ps1 @@ -10,7 +10,6 @@ function Invoke-CIPPStandardDisableEntraPortal { #> param($Tenant, $Settings) - #$Rerun -Type Standard -Tenant $Tenant -API 'allowOTPTokens' -Settings $Settings #This standard is still unlisted due to MS fixing some permissions. This will be added to the list once it is fixed. try { $CurrentInfo = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/admin/entra/uxSetting' -tenantid $Tenant diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableExchangeOnlinePowerShell.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableExchangeOnlinePowerShell.ps1 index 016557aa7124..ff49eefefeef 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableExchangeOnlinePowerShell.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableExchangeOnlinePowerShell.ps1 @@ -95,8 +95,14 @@ function Invoke-CIPPStandardDisableExchangeOnlinePowerShell { } if ($Settings.report -eq $true) { - $state = $StateIsCorrect ?? @{UsersWithPowerShellEnabled = $PowerShellEnabledCount } - Set-CIPPStandardsCompareField -FieldName 'standards.DisableExchangeOnlinePowerShell' -FieldValue $state -TenantFilter $Tenant + $CurrentValue = [PSCustomObject]@{ + UsersWithPowerShellEnabled = $PowerShellEnabledCount + } + $ExpectedValue = [PSCustomObject]@{ + UsersWithPowerShellEnabled = 0 + } + + Set-CIPPStandardsCompareField -FieldName 'standards.DisableExchangeOnlinePowerShell' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant Add-CIPPBPAField -FieldName 'ExchangeOnlinePowerShellDisabled' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableExternalCalendarSharing.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableExternalCalendarSharing.ps1 index fca63098a8cc..d8bf42551e92 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableExternalCalendarSharing.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableExternalCalendarSharing.ps1 @@ -39,13 +39,11 @@ function Invoke-CIPPStandardDisableExternalCalendarSharing { Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. - ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'DisableExternalCalendarSharing' try { $CurrentInfo = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-SharingPolicy' | - Where-Object { $_.Default -eq $true } - } - catch { + Where-Object { $_.Default -eq $true } + } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the DisableExternalCalendarSharing state for $Tenant. Error: $ErrorMessage" -Sev Error return @@ -79,8 +77,16 @@ function Invoke-CIPPStandardDisableExternalCalendarSharing { } if ($Settings.report -eq $true) { - $CurrentInfo.Enabled = -not $CurrentInfo.Enabled - Set-CIPPStandardsCompareField -FieldName 'standards.DisableExternalCalendarSharing' -FieldValue $CurrentInfo.Enabled -TenantFilter $Tenant - Add-CIPPBPAField -FieldName 'ExternalCalendarSharingDisabled' -FieldValue $CurrentInfo.Enabled -StoreAs bool -Tenant $tenant + $CurrentStatus = -not $CurrentInfo.Enabled + + $CurrentValue = [PSCustomObject]@{ + ExternalCalendarSharingDisabled = $CurrentStatus + } + $ExpectedValue = [PSCustomObject]@{ + ExternalCalendarSharingDisabled = $true + } + + Set-CIPPStandardsCompareField -FieldName 'standards.DisableExternalCalendarSharing' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant + Add-CIPPBPAField -FieldName 'ExternalCalendarSharingDisabled' -FieldValue $CurrentStatus -StoreAs bool -Tenant $tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableGuestDirectory.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableGuestDirectory.ps1 index 83179a83634b..f4ec96f9f33d 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableGuestDirectory.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableGuestDirectory.ps1 @@ -37,19 +37,16 @@ function Invoke-CIPPStandardDisableGuestDirectory { #> param($Tenant, $Settings) - ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'DisableGuestDirectory' try { $CurrentInfo = New-GraphGetRequest -Uri 'https://graph.microsoft.com/beta/policies/authorizationPolicy/authorizationPolicy' -tenantid $Tenant - } - catch { + } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the DisableGuestDirectory state for $Tenant. Error: $ErrorMessage" -Sev Error return } - If ($Settings.remediate -eq $true) { - + if ($Settings.remediate -eq $true) { if ($CurrentInfo.guestUserRoleId -eq '2af84b1e-32c8-42b7-82bc-daa82404023b') { Write-LogMessage -API 'Standards' -tenant $tenant -message 'Guest access to directory information is already disabled.' -sev Info } else { @@ -65,7 +62,6 @@ function Invoke-CIPPStandardDisableGuestDirectory { } if ($Settings.alert -eq $true) { - if ($CurrentInfo.guestUserRoleId -eq '2af84b1e-32c8-42b7-82bc-daa82404023b') { Write-LogMessage -API 'Standards' -tenant $tenant -message 'Guest access to directory information is disabled.' -sev Info } else { @@ -75,8 +71,16 @@ function Invoke-CIPPStandardDisableGuestDirectory { } if ($Settings.report -eq $true) { - if ($CurrentInfo.guestUserRoleId -eq '2af84b1e-32c8-42b7-82bc-daa82404023b') { $CurrentInfo.guestUserRoleId = $true } else { $CurrentInfo.guestUserRoleId = $false } - Set-CIPPStandardsCompareField -FieldName 'standards.DisableGuestDirectory' -FieldValue $CurrentInfo.guestUserRoleId -TenantFilter $Tenant - Add-CIPPBPAField -FieldName 'DisableGuestDirectory' -FieldValue $CurrentInfo.guestUserRoleId -StoreAs bool -Tenant $tenant + if ($CurrentInfo.guestUserRoleId -eq '2af84b1e-32c8-42b7-82bc-daa82404023b') { $CurrentStatus = $true } else { $CurrentStatus = $false } + + $CurrentValue = [PSCustomObject]@{ + GuestDirectoryAccessDisabled = $CurrentStatus + } + $ExpectedValue = [PSCustomObject]@{ + GuestDirectoryAccessDisabled = $true + } + + Set-CIPPStandardsCompareField -FieldName 'standards.DisableGuestDirectory' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant + Add-CIPPBPAField -FieldName 'DisableGuestDirectory' -FieldValue $CurrentStatus -StoreAs bool -Tenant $tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableGuests.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableGuests.ps1 index a4169524521d..de4789652b77 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableGuests.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableGuests.ps1 @@ -91,8 +91,20 @@ function Invoke-CIPPStandardDisableGuests { } if ($Settings.report -eq $true) { $Filtered = $GraphRequest | Select-Object -Property UserPrincipalName, id, signInActivity, mail, userType, accountEnabled - $State = $Filtered ? $Filtered : $true - Set-CIPPStandardsCompareField -FieldName 'standards.DisableGuests' -FieldValue $State -TenantFilter $Tenant + + $CurrentValue = [PSCustomObject]@{ + GuestsDisabledAfterDays = $checkDays + GuestsDisabledAccountCount = $Filtered.Count + GuestsDisabledAccountDetails = @($Filtered) + } + + $ExpectedValue = [PSCustomObject]@{ + GuestsDisabledAfterDays = $checkDays + GuestsDisabledAccountCount = 0 + GuestsDisabledAccountDetails = @() + } + + Set-CIPPStandardsCompareField -FieldName 'standards.DisableGuests' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant Add-CIPPBPAField -FieldName 'DisableGuests' -FieldValue $Filtered -StoreAs json -Tenant $tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableM365GroupUsers.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableM365GroupUsers.ps1 index 174eeef2fd89..4919d502721f 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableM365GroupUsers.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableM365GroupUsers.ps1 @@ -31,8 +31,7 @@ function Invoke-CIPPStandardDisableM365GroupUsers { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'DisableM365GroupUsers' -TenantFilter $Tenant -RequiredCapabilities @('SHAREPOINTWAC', 'SHAREPOINTSTANDARD', 'SHAREPOINTENTERPRISE', 'SHAREPOINTENTERPRISE_EDU','ONEDRIVE_BASIC', 'ONEDRIVE_ENTERPRISE') - ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'DisableM365GroupUsers' + $TestResult = Test-CIPPStandardLicense -StandardName 'DisableM365GroupUsers' -TenantFilter $Tenant -RequiredCapabilities @('SHAREPOINTWAC', 'SHAREPOINTSTANDARD', 'SHAREPOINTENTERPRISE', 'SHAREPOINTENTERPRISE_EDU', 'ONEDRIVE_BASIC', 'ONEDRIVE_ENTERPRISE') if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." @@ -41,15 +40,14 @@ function Invoke-CIPPStandardDisableM365GroupUsers { try { $CurrentState = (New-GraphGetRequest -Uri 'https://graph.microsoft.com/beta/settings' -tenantid $tenant) | - Where-Object -Property displayname -EQ 'Group.unified' - } - catch { + Where-Object -Property displayname -EQ 'Group.unified' + } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the DisableM365GroupUsers state for $Tenant. Error: $ErrorMessage" -Sev Error return } - If ($Settings.remediate -eq $true) { + if ($Settings.remediate -eq $true) { if (($CurrentState.values | Where-Object { $_.name -eq 'EnableGroupCreation' }).value -eq 'false') { Write-LogMessage -API 'Standards' -tenant $tenant -message 'Users are already disabled from creating M365 Groups.' -sev Info } else { @@ -94,7 +92,15 @@ function Invoke-CIPPStandardDisableM365GroupUsers { } else { $CurrentState = $false } - Set-CIPPStandardsCompareField -FieldName 'standards.DisableM365GroupUsers' -FieldValue $CurrentState -TenantFilter $Tenant + + $CurrentValue = [PSCustomObject]@{ + M365GroupUserCreationDisabled = $CurrentState + } + $ExpectedValue = [PSCustomObject]@{ + M365GroupUserCreationDisabled = $true + } + + Set-CIPPStandardsCompareField -FieldName 'standards.DisableM365GroupUsers' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant Add-CIPPBPAField -FieldName 'DisableM365GroupUsers' -FieldValue $CurrentState -StoreAs bool -Tenant $tenant } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableOutlookAddins.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableOutlookAddins.ps1 index 483d7b3c1219..42c955f5186f 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableOutlookAddins.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableOutlookAddins.ps1 @@ -41,13 +41,11 @@ function Invoke-CIPPStandardDisableOutlookAddins { Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. - ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'DisableOutlookAddins' try { $CurrentInfo = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-RoleAssignmentPolicy' | - Where-Object { $_.IsDefault -eq $true } - } - catch { + Where-Object { $_.IsDefault -eq $true } + } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the DisableOutlookAddins state for $Tenant. Error: $ErrorMessage" -Sev Error return @@ -97,8 +95,15 @@ function Invoke-CIPPStandardDisableOutlookAddins { } if ($Settings.report -eq $true) { $State = if ($RolesToRemove) { $false } else { $true } - $StateForCompare = if ($RolesToRemove) { @{ AllowedApps = $RolesToRemove } } else { $true } - Set-CIPPStandardsCompareField -FieldName 'standards.DisableOutlookAddins' -FieldValue $StateForCompare -TenantFilter $Tenant + + $CurrentValue = [PSCustomObject]@{ + DisabledOutlookAddins = $State + } + $ExpectedValue = [PSCustomObject]@{ + DisabledOutlookAddins = $true + } + + Set-CIPPStandardsCompareField -FieldName 'standards.DisableOutlookAddins' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant Add-CIPPBPAField -FieldName 'DisabledOutlookAddins' -FieldValue $State -StoreAs bool -Tenant $tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableQRCodePin.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableQRCodePin.ps1 index cd3ad4ca11e5..244a867a3d3e 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableQRCodePin.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableQRCodePin.ps1 @@ -63,7 +63,15 @@ function Invoke-CIPPStandardDisableQRCodePin { if ($Settings.report -eq $true) { $state = $StateIsCorrect -eq $true ? $true : $CurrentState - Set-CIPPStandardsCompareField -FieldName 'standards.DisableQRCodePin' -FieldValue $state -TenantFilter $Tenant + + $CurrentValue = [PSCustomObject]@{ + DisableQRCodePin = $state + } + $ExpectedValue = [PSCustomObject]@{ + DisableQRCodePin = $true + } + + Set-CIPPStandardsCompareField -FieldName 'standards.DisableQRCodePin' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant Add-CIPPBPAField -FieldName 'DisableQRCodePin' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableReshare.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableReshare.ps1 index 00907b647f35..3e29a5d81a3b 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableReshare.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableReshare.ps1 @@ -35,8 +35,7 @@ function Invoke-CIPPStandardDisableReshare { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'DisableReshare' -TenantFilter $Tenant -RequiredCapabilities @('SHAREPOINTWAC', 'SHAREPOINTSTANDARD', 'SHAREPOINTENTERPRISE', 'SHAREPOINTENTERPRISE_EDU','ONEDRIVE_BASIC', 'ONEDRIVE_ENTERPRISE') - ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'DisableReshare' + $TestResult = Test-CIPPStandardLicense -StandardName 'DisableReshare' -TenantFilter $Tenant -RequiredCapabilities @('SHAREPOINTWAC', 'SHAREPOINTSTANDARD', 'SHAREPOINTENTERPRISE', 'SHAREPOINTENTERPRISE_EDU', 'ONEDRIVE_BASIC', 'ONEDRIVE_ENTERPRISE') if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." @@ -45,14 +44,13 @@ function Invoke-CIPPStandardDisableReshare { try { $CurrentInfo = New-GraphGetRequest -Uri 'https://graph.microsoft.com/beta/admin/sharepoint/settings' -tenantid $Tenant -AsApp $true - } - catch { + } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the DisableReshare state for $Tenant. Error: $ErrorMessage" -Sev Error return } - If ($Settings.remediate -eq $true) { + if ($Settings.remediate -eq $true) { if ($CurrentInfo.isResharingByExternalUsersEnabled) { try { @@ -79,7 +77,15 @@ function Invoke-CIPPStandardDisableReshare { if ($Settings.report -eq $true) { $state = $CurrentInfo.isResharingByExternalUsersEnabled ? ($CurrentInfo | Select-Object isResharingByExternalUsersEnabled) : $true - Set-CIPPStandardsCompareField -FieldName 'standards.DisableReshare' -FieldValue $state -TenantFilter $Tenant + + $CurrentValue = [PSCustomObject]@{ + DisableReshare = $state + } + $ExpectedValue = [PSCustomObject]@{ + DisableReshare = $true + } + + Set-CIPPStandardsCompareField -FieldName 'standards.DisableReshare' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant Add-CIPPBPAField -FieldName 'DisableReshare' -FieldValue $CurrentInfo.isResharingByExternalUsersEnabled -StoreAs bool -Tenant $tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableResourceMailbox.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableResourceMailbox.ps1 index 5f6509f7baa1..ca70a1330275 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableResourceMailbox.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableResourceMailbox.ps1 @@ -39,22 +39,20 @@ function Invoke-CIPPStandardDisableResourceMailbox { Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. - ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'DisableResourceMailbox' # Get all users that are able to be try { $UserList = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/users?$top=999&$filter=accountEnabled eq true and onPremisesSyncEnabled ne true and assignedLicenses/$count eq 0&$count=true' -Tenantid $Tenant -ComplexFilter | - Where-Object { $_.userType -eq 'Member' } + Where-Object { $_.userType -eq 'Member' } $ResourceMailboxList = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-Mailbox' -cmdParams @{ Filter = "RecipientTypeDetails -eq 'RoomMailbox' -or RecipientTypeDetails -eq 'EquipmentMailbox'" } -Select 'UserPrincipalName,DisplayName,RecipientTypeDetails,ExternalDirectoryObjectId' | - Where-Object { $_.ExternalDirectoryObjectId -in $UserList.id } - } - catch { + Where-Object { $_.ExternalDirectoryObjectId -in $UserList.id } + } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the DisableResourceMailbox state for $Tenant. Error: $ErrorMessage" -Sev Error return } - If ($Settings.remediate -eq $true) { + if ($Settings.remediate -eq $true) { Write-Host 'Time to remediate' @@ -85,9 +83,14 @@ function Invoke-CIPPStandardDisableResourceMailbox { } if ($Settings.report -eq $true) { - # If there are no resource mailboxes, we set the state to true, so that the standard reports as compliant. - $State = $ResourceMailboxList ? $ResourceMailboxList : $true - Set-CIPPStandardsCompareField -FieldName 'standards.DisableResourceMailbox' -FieldValue $State -Tenant $Tenant + $CurrentValue = [PSCustomObject]@{ + ResourceMailboxesToDisable = @($ResourceMailboxList) + } + $ExpectedValue = [PSCustomObject]@{ + ResourceMailboxesToDisable = @() + } + + Set-CIPPStandardsCompareField -FieldName 'standards.DisableResourceMailbox' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant Add-CIPPBPAField -FieldName 'DisableResourceMailbox' -FieldValue $ResourceMailboxList -StoreAs json -Tenant $Tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSMS.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSMS.ps1 index 9e31319e9e74..f793a36cad48 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSMS.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSMS.ps1 @@ -34,19 +34,17 @@ function Invoke-CIPPStandardDisableSMS { #> param($Tenant, $Settings) - ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'DisableSMS' try { $CurrentState = New-GraphGetRequest -Uri 'https://graph.microsoft.com/beta/policies/authenticationmethodspolicy/authenticationMethodConfigurations/SMS' -tenantid $Tenant - } - catch { + } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the DisableSMS state for $Tenant. Error: $ErrorMessage" -Sev Error return } $StateIsCorrect = ($CurrentState.state -eq 'disabled') - If ($Settings.remediate -eq $true) { + if ($Settings.remediate -eq $true) { if ($StateIsCorrect -eq $true) { Write-LogMessage -API 'Standards' -tenant $tenant -message 'SMS authentication method is already disabled.' -sev Info } else { @@ -67,7 +65,15 @@ function Invoke-CIPPStandardDisableSMS { } if ($Settings.report -eq $true) { - Set-CIPPStandardsCompareField -FieldName 'standards.DisableSMS' -FieldValue $StateIsCorrect -TenantFilter $Tenant + + $CurrentValue = [PSCustomObject]@{ + DisableSMS = $StateIsCorrect + } + $ExpectedValue = [PSCustomObject]@{ + DisableSMS = $true + } + + Set-CIPPStandardsCompareField -FieldName 'standards.DisableSMS' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant Add-CIPPBPAField -FieldName 'DisableSMS' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSecurityGroupUsers.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSecurityGroupUsers.ps1 index 2ebc31ab174f..a114b59134bd 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSecurityGroupUsers.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSecurityGroupUsers.ps1 @@ -32,18 +32,16 @@ function Invoke-CIPPStandardDisableSecurityGroupUsers { #> param($Tenant, $Settings) - ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'DisableSecurityGroupUsers' try { $CurrentInfo = New-GraphGetRequest -Uri 'https://graph.microsoft.com/beta/policies/authorizationPolicy/authorizationPolicy' -tenantid $Tenant - } - catch { + } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the DisableSecurityGroupUsers state for $Tenant. Error: $ErrorMessage" -Sev Error return } - If ($Settings.remediate -eq $true) { + if ($Settings.remediate -eq $true) { if ($CurrentInfo.defaultUserRolePermissions.allowedToCreateSecurityGroups -eq $false) { Write-LogMessage -API 'Standards' -tenant $tenant -message 'Users are already not allowed to create Security Groups.' -sev Info } else { @@ -70,8 +68,14 @@ function Invoke-CIPPStandardDisableSecurityGroupUsers { } if ($Settings.report -eq $true) { - $state = $CurrentInfo.defaultUserRolePermissions.allowedToCreateSecurityGroups -eq $false ? $true : ($currentInfo.defaultUserRolePermissions | Select-Object allowedToCreateSecurityGroups) - Set-CIPPStandardsCompareField -FieldName 'standards.DisableSecurityGroupUsers' -FieldValue $state -Tenant $tenant + $CurrentValue = [PSCustomObject]@{ + DisableSecurityGroupUsers = $CurrentInfo.defaultUserRolePermissions.allowedToCreateSecurityGroups -eq $false + } + $ExpectedValue = [PSCustomObject]@{ + DisableSecurityGroupUsers = $true + } + + Set-CIPPStandardsCompareField -FieldName 'standards.DisableSecurityGroupUsers' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $tenant Add-CIPPBPAField -FieldName 'DisableSecurityGroupUsers' -FieldValue $CurrentInfo.defaultUserRolePermissions.allowedToCreateSecurityGroups -StoreAs bool -Tenant $tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSelfServiceLicenses.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSelfServiceLicenses.ps1 index 84aa8b9e166d..41dc31365e43 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSelfServiceLicenses.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSelfServiceLicenses.ps1 @@ -31,7 +31,6 @@ function Invoke-CIPPStandardDisableSelfServiceLicenses { #> param($Tenant, $Settings) - ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'DisableSelfServiceLicenses' try { $selfServiceItems = (New-GraphGETRequest -scope 'aeb86249-8ea3-49e2-900b-54cc8e308f85/.default' -uri 'https://licensing.m365.microsoft.com/v1.0/policies/AllowSelfServicePurchase/products' -tenantid $Tenant).items diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSharePointLegacyAuth.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSharePointLegacyAuth.ps1 index 3f3ad436c31a..88b113d8d554 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSharePointLegacyAuth.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSharePointLegacyAuth.ps1 @@ -37,8 +37,7 @@ function Invoke-CIPPStandardDisableSharePointLegacyAuth { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'DisableSharePointLegacyAuth' -TenantFilter $Tenant -RequiredCapabilities @('SHAREPOINTWAC', 'SHAREPOINTSTANDARD', 'SHAREPOINTENTERPRISE', 'SHAREPOINTENTERPRISE_EDU','ONEDRIVE_BASIC', 'ONEDRIVE_ENTERPRISE') - ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'DisableSharePointLegacyAuth' + $TestResult = Test-CIPPStandardLicense -StandardName 'DisableSharePointLegacyAuth' -TenantFilter $Tenant -RequiredCapabilities @('SHAREPOINTWAC', 'SHAREPOINTSTANDARD', 'SHAREPOINTENTERPRISE', 'SHAREPOINTENTERPRISE_EDU', 'ONEDRIVE_BASIC', 'ONEDRIVE_ENTERPRISE') if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." @@ -47,14 +46,13 @@ function Invoke-CIPPStandardDisableSharePointLegacyAuth { try { $CurrentInfo = New-GraphGetRequest -Uri 'https://graph.microsoft.com/beta/admin/sharepoint/settings?$select=isLegacyAuthProtocolsEnabled' -tenantid $Tenant -AsApp $true - } - catch { + } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the DisableSharePointLegacyAuth state for $Tenant. Error: $ErrorMessage" -Sev Error return } - If ($Settings.remediate -eq $true) { + if ($Settings.remediate -eq $true) { if ($CurrentInfo.isLegacyAuthProtocolsEnabled) { try { @@ -80,8 +78,14 @@ function Invoke-CIPPStandardDisableSharePointLegacyAuth { } } if ($Settings.report -eq $true) { - $state = $CurrentInfo.isLegacyAuthProtocolsEnabled ? ($CurrentInfo | Select-Object isLegacyAuthProtocolsEnabled) : $true - Set-CIPPStandardsCompareField -FieldName 'standards.DisableSharePointLegacyAuth' -FieldValue $state -TenantFilter $Tenant + $CurrentValue = [PSCustomObject]@{ + DisableSharePointLegacyAuth = $CurrentInfo.isLegacyAuthProtocolsEnabled -eq $false + } + $ExpectedValue = [PSCustomObject]@{ + DisableSharePointLegacyAuth = $true + } + + Set-CIPPStandardsCompareField -FieldName 'standards.DisableSharePointLegacyAuth' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant Add-CIPPBPAField -FieldName 'SharePointLegacyAuthEnabled' -FieldValue $CurrentInfo.isLegacyAuthProtocolsEnabled -StoreAs bool -Tenant $tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSharedMailbox.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSharedMailbox.ps1 index ce4ee3ecd59a..01b501aec319 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSharedMailbox.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSharedMailbox.ps1 @@ -35,19 +35,17 @@ function Invoke-CIPPStandardDisableSharedMailbox { #> param($Tenant, $Settings) - ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'DisableSharedMailbox' try { $UserList = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/users?$top=999&$filter=accountEnabled eq true and onPremisesSyncEnabled ne true&$count=true' -Tenantid $Tenant -ComplexFilter - $SharedMailboxList = (New-GraphGetRequest -uri "https://outlook.office365.com/adminapi/beta/$($Tenant)/Mailbox" -Tenantid $Tenant -scope ExchangeOnline | Where-Object { $_.RecipientTypeDetails -EQ 'SharedMailbox' -or $_.RecipientTypeDetails -eq 'SchedulingMailbox' -and $_.UserPrincipalName -in $UserList.UserPrincipalName }) - } - catch { + $SharedMailboxList = (New-GraphGetRequest -uri "https://outlook.office365.com/adminapi/beta/$($Tenant)/Mailbox" -Tenantid $Tenant -scope ExchangeOnline | Where-Object { $_.RecipientTypeDetails -eq 'SharedMailbox' -or $_.RecipientTypeDetails -eq 'SchedulingMailbox' -and $_.UserPrincipalName -in $UserList.UserPrincipalName }) + } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the DisableSharedMailbox state for $Tenant. Error: $ErrorMessage" -Sev Error return } - If ($Settings.remediate -eq $true) { + if ($Settings.remediate -eq $true) { if ($SharedMailboxList) { $SharedMailboxList | ForEach-Object { @@ -75,8 +73,16 @@ function Invoke-CIPPStandardDisableSharedMailbox { } if ($Settings.report -eq $true) { - $State = $SharedMailboxList ? $SharedMailboxList : $true - Set-CIPPStandardsCompareField -FieldName 'standards.DisableSharedMailbox' -FieldValue $State -Tenant $Tenant + $State = $SharedMailboxList ? $SharedMailboxList : @() + + $CurrentValue = [PSCustomObject]@{ + DisableSharedMailbox = @($State) + } + $ExpectedValue = [PSCustomObject]@{ + DisableSharedMailbox = @() + } + + Set-CIPPStandardsCompareField -FieldName 'standards.DisableSharedMailbox' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -Tenant $Tenant Add-CIPPBPAField -FieldName 'DisableSharedMailbox' -FieldValue $SharedMailboxList -StoreAs json -Tenant $Tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableTNEF.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableTNEF.ps1 index 37453584a858..e82a30d9de1a 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableTNEF.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableTNEF.ps1 @@ -31,7 +31,6 @@ function Invoke-CIPPStandardDisableTNEF { #> param ($Tenant, $Settings) - ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'DisableTNEF' $TestResult = Test-CIPPStandardLicense -StandardName 'DisableTNEF' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { @@ -41,8 +40,7 @@ function Invoke-CIPPStandardDisableTNEF { try { $CurrentState = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-RemoteDomain' -cmdParams @{Identity = 'Default' } - } - catch { + } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the DisableTNEF state for $Tenant. Error: $ErrorMessage" -Sev Error return @@ -76,7 +74,15 @@ function Invoke-CIPPStandardDisableTNEF { if ($Settings.report -eq $true) { $State = if ($CurrentState.TNEFEnabled -ne $false) { $false } else { $true } - Set-CIPPStandardsCompareField -FieldName 'standards.DisableTNEF' -FieldValue $State -Tenant $tenant + + $CurrentValue = [PSCustomObject]@{ + DisableTNEF = $State + } + $ExpectedValue = [PSCustomObject]@{ + DisableTNEF = $true + } + + Set-CIPPStandardsCompareField -FieldName 'standards.DisableTNEF' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant Add-CIPPBPAField -FieldName 'TNEFDisabled' -FieldValue $State -StoreAs bool -Tenant $tenant } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableTenantCreation.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableTenantCreation.ps1 index f5023093a594..c68199c54554 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableTenantCreation.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableTenantCreation.ps1 @@ -34,19 +34,17 @@ function Invoke-CIPPStandardDisableTenantCreation { #> param($Tenant, $Settings) - ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'DisableTenantCreation' try { $CurrentState = New-GraphGetRequest -Uri 'https://graph.microsoft.com/beta/policies/authorizationPolicy/authorizationPolicy' -tenantid $Tenant - } - catch { + } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the DisableTenantCreation state for $Tenant. Error: $ErrorMessage" -Sev Error return } $StateIsCorrect = ($CurrentState.defaultUserRolePermissions.allowedToCreateTenants -eq $false) - If ($Settings.remediate -eq $true) { + if ($Settings.remediate -eq $true) { Write-Host "Time to remediate DisableTenantCreation standard for tenant $Tenant" if ($StateIsCorrect -eq $true) { Write-LogMessage -API 'Standards' -tenant $Tenant -message 'Users are already disabled from creating tenants.' -sev Info @@ -77,7 +75,14 @@ function Invoke-CIPPStandardDisableTenantCreation { } if ($Settings.report -eq $true) { - Set-CIPPStandardsCompareField -FieldName 'standards.DisableTenantCreation' -FieldValue $StateIsCorrect -TenantFilter $Tenant + $CurrentValue = [PSCustomObject]@{ + DisableTenantCreation = $StateIsCorrect + } + $ExpectedValue = [PSCustomObject]@{ + DisableTenantCreation = $true + } + + Set-CIPPStandardsCompareField -FieldName 'standards.DisableTenantCreation' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant Add-CIPPBPAField -FieldName 'DisableTenantCreation' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $Tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableUserSiteCreate.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableUserSiteCreate.ps1 index 20c91e310e65..2eeda69ca0f1 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableUserSiteCreate.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableUserSiteCreate.ps1 @@ -30,8 +30,7 @@ function Invoke-CIPPStandardDisableUserSiteCreate { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'DisableUserSiteCreate' -TenantFilter $Tenant -RequiredCapabilities @('SHAREPOINTWAC', 'SHAREPOINTSTANDARD', 'SHAREPOINTENTERPRISE', 'SHAREPOINTENTERPRISE_EDU','ONEDRIVE_BASIC', 'ONEDRIVE_ENTERPRISE') - ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'DisableUserSiteCreate' + $TestResult = Test-CIPPStandardLicense -StandardName 'DisableUserSiteCreate' -TenantFilter $Tenant -RequiredCapabilities @('SHAREPOINTWAC', 'SHAREPOINTSTANDARD', 'SHAREPOINTENTERPRISE', 'SHAREPOINTENTERPRISE_EDU', 'ONEDRIVE_BASIC', 'ONEDRIVE_ENTERPRISE') if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." @@ -40,14 +39,13 @@ function Invoke-CIPPStandardDisableUserSiteCreate { try { $CurrentInfo = New-GraphGetRequest -Uri 'https://graph.microsoft.com/beta/admin/sharepoint/settings' -tenantid $Tenant -AsApp $true - } - catch { + } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the DisableUserSiteCreate state for $Tenant. Error: $ErrorMessage" -Sev Error return } - If ($Settings.remediate -eq $true) { + if ($Settings.remediate -eq $true) { if ($CurrentInfo.isSiteCreationEnabled -or $CurrentInfo.isSiteCreationUIEnabled) { try { @@ -76,7 +74,15 @@ function Invoke-CIPPStandardDisableUserSiteCreate { if ($Settings.report -eq $true) { $state = $CurrentInfo.isSiteCreationEnabled -and $CurrentInfo.isSiteCreationUIEnabled ? ($CurrentInfo | Select-Object isSiteCreationEnabled, isSiteCreationUIEnabled) : $true - Set-CIPPStandardsCompareField -FieldName 'standards.DisableUserSiteCreate' -FieldValue $State -Tenant $tenant + + $CurrentValue = [PSCustomObject]@{ + DisableUserSiteCreate = $state + } + $ExpectedValue = [PSCustomObject]@{ + DisableUserSiteCreate = $true + } + + Set-CIPPStandardsCompareField -FieldName 'standards.DisableUserSiteCreate' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant Add-CIPPBPAField -FieldName 'DisableUserSiteCreate' -FieldValue $CurrentInfo.isSiteCreationEnabled -StoreAs bool -Tenant $tenant Add-CIPPBPAField -FieldName 'DisableUserSiteCreateUI' -FieldValue $CurrentInfo.isSiteCreationUIEnabled -StoreAs bool -Tenant $tenant } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableViva.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableViva.ps1 index 5cae497f3b8d..4ae2623e8e1b 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableViva.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableViva.ps1 @@ -30,7 +30,6 @@ function Invoke-CIPPStandardDisableViva { #> param($Tenant, $Settings) - ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'DisableViva' try { # TODO This does not work without Global Admin permissions for some reason. Throws an "EXCEPTION: Tenant admin role is required" error. -Bobby @@ -38,10 +37,10 @@ function Invoke-CIPPStandardDisableViva { } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to get Viva insights settings. Error: $ErrorMessage" -sev Error - Return + return } - If ($Settings.remediate -eq $true) { + if ($Settings.remediate -eq $true) { Write-Host 'Time to remediate' if ($CurrentSetting.isEnabledInOrganization -eq $false) { @@ -68,8 +67,14 @@ function Invoke-CIPPStandardDisableViva { } if ($Settings.report -eq $true) { - $state = $CurrentSetting.isEnabledInOrganization ? $true : ($CurrentSetting | Select-Object isEnabledInOrganization) - Set-CIPPStandardsCompareField -FieldName 'standards.DisableViva' -FieldValue $State -Tenant $Tenant + $CurrentValue = [PSCustomObject]@{ + DisableViva = -not $CurrentSetting.isEnabledInOrganization + } + $ExpectedValue = [PSCustomObject]@{ + DisableViva = $true + } + + Set-CIPPStandardsCompareField -FieldName 'standards.DisableViva' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant Add-CIPPBPAField -FieldName 'DisableViva' -FieldValue $CurrentSetting.isEnabledInOrganization -StoreAs bool -Tenant $Tenant } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableVoice.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableVoice.ps1 index 4e53abdff35a..254bb83e0d4e 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableVoice.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableVoice.ps1 @@ -34,19 +34,17 @@ function Invoke-CIPPStandardDisableVoice { #> param($Tenant, $Settings) - ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'DisableVoice' try { $CurrentState = New-GraphGetRequest -Uri 'https://graph.microsoft.com/beta/policies/authenticationmethodspolicy/authenticationMethodConfigurations/Voice' -tenantid $Tenant - } - catch { + } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the DisableVoice state for $Tenant. Error: $ErrorMessage" -Sev Error return } $StateIsCorrect = ($CurrentState.state -eq 'disabled') - If ($Settings.remediate -eq $true) { + if ($Settings.remediate -eq $true) { if ($StateIsCorrect -eq $true) { Write-LogMessage -API 'Standards' -tenant $tenant -message 'Voice authentication method is already disabled.' -sev Info } else { @@ -67,7 +65,14 @@ function Invoke-CIPPStandardDisableVoice { } if ($Settings.report -eq $true) { - Set-CIPPStandardsCompareField -FieldName 'standards.DisableVoice' -FieldValue $StateIsCorrect -TenantFilter $Tenant + $CurrentValue = [PSCustomObject]@{ + DisableVoice = $StateIsCorrect + } + $ExpectedValue = [PSCustomObject]@{ + DisableVoice = $true + } + + Set-CIPPStandardsCompareField -FieldName 'standards.DisableVoice' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant Add-CIPPBPAField -FieldName 'DisableVoice' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisablex509Certificate.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisablex509Certificate.ps1 index 3424827b8efa..5ea05a264e08 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisablex509Certificate.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisablex509Certificate.ps1 @@ -30,19 +30,17 @@ function Invoke-CIPPStandardDisablex509Certificate { #> param($Tenant, $Settings) - ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'Disablex509Certificate' try { $CurrentState = New-GraphGetRequest -Uri 'https://graph.microsoft.com/beta/policies/authenticationmethodspolicy/authenticationMethodConfigurations/x509Certificate' -tenantid $Tenant - } - catch { + } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the Disablex509Certificate state for $Tenant. Error: $ErrorMessage" -Sev Error return } $StateIsCorrect = ($CurrentState.state -eq 'disabled') - If ($Settings.remediate -eq $true) { + if ($Settings.remediate -eq $true) { if ($StateIsCorrect -eq $true) { Write-LogMessage -API 'Standards' -tenant $tenant -message 'x509Certificate authentication method is already disabled.' -sev Info } else { @@ -63,8 +61,14 @@ function Invoke-CIPPStandardDisablex509Certificate { } if ($Settings.report -eq $true) { - $state = $StateIsCorrect ? $true : $CurrentState - Set-CIPPStandardsCompareField -FieldName 'standards.Disablex509Certificate' -FieldValue $state -TenantFilter $Tenant + $CurrentValue = [PSCustomObject]@{ + Disablex509Certificate = $StateIsCorrect + } + $ExpectedValue = [PSCustomObject]@{ + Disablex509Certificate = $true + } + + Set-CIPPStandardsCompareField -FieldName 'standards.Disablex509Certificate' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant Add-CIPPBPAField -FieldName 'Disablex509Certificate' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $tenant } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableAppConsentRequests.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableAppConsentRequests.ps1 index 93c488c8637d..21a52e52dda7 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableAppConsentRequests.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableAppConsentRequests.ps1 @@ -41,18 +41,16 @@ function Invoke-CIPPStandardEnableAppConsentRequests { #> param($Tenant, $Settings) - ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'EnableAppConsentRequests' try { $CurrentInfo = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/policies/adminConsentRequestPolicy' -tenantid $Tenant - } - catch { + } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the EnableAppConsentRequests state for $Tenant. Error: $ErrorMessage" -Sev Error return } - If ($Settings.remediate -eq $true) { + if ($Settings.remediate -eq $true) { try { # Get current state @@ -121,8 +119,17 @@ function Invoke-CIPPStandardEnableAppConsentRequests { } } if ($Settings.report -eq $true) { - $state = $CurrentInfo.isEnabled ? $true : $CurrentInfo - Set-CIPPStandardsCompareField -FieldName 'standards.EnableAppConsentRequests' -FieldValue $state -TenantFilter $Tenant + + $CurrentValue = [PSCustomObject]@{ + EnableAppConsentRequests = $CurrentInfo.isEnabled + ReviewerCount = $CurrentInfo.reviewers.count + } + $ExpectedValue = [PSCustomObject]@{ + EnableAppConsentRequests = $true + ReviewerCount = ($Settings.ReviewerRoles.value).count + } + + Set-CIPPStandardsCompareField -FieldName 'standards.EnableAppConsentRequests' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant Add-CIPPBPAField -FieldName 'EnableAppConsentAdminRequests' -FieldValue $CurrentInfo.isEnabled -StoreAs bool -Tenant $tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableCustomerLockbox.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableCustomerLockbox.ps1 index 60c87aaa5359..f790392524ba 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableCustomerLockbox.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableCustomerLockbox.ps1 @@ -33,7 +33,6 @@ function Invoke-CIPPStandardEnableCustomerLockbox { #> param($Tenant, $Settings) - ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'EnableCustomerLockbox' $TestResult = Test-CIPPStandardLicense -StandardName 'EnableCustomerLockbox' -TenantFilter $Tenant -RequiredCapabilities @('CustomerLockbox') if ($TestResult -eq $false) { @@ -43,8 +42,7 @@ function Invoke-CIPPStandardEnableCustomerLockbox { try { $CustomerLockboxStatus = (New-ExoRequest -tenantid $Tenant -cmdlet 'Get-OrganizationConfig').CustomerLockboxEnabled - } - catch { + } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the EnableCustomerLockbox state for $Tenant. Error: $ErrorMessage" -Sev Error return @@ -81,7 +79,15 @@ function Invoke-CIPPStandardEnableCustomerLockbox { if ($Settings.report -eq $true) { $state = $CustomerLockboxStatus ? $true : $false - Set-CIPPStandardsCompareField -FieldName 'standards.EnableCustomerLockbox' -FieldValue $state -Tenant $tenant + + $CurrentValue = [PSCustomObject]@{ + EnableCustomerLockbox = $CustomerLockboxStatus + } + $ExpectedValue = [PSCustomObject]@{ + EnableCustomerLockbox = $true + } + + Set-CIPPStandardsCompareField -FieldName 'standards.EnableCustomerLockbox' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -Tenant $tenant Add-CIPPBPAField -FieldName 'CustomerLockboxEnabled' -FieldValue $CustomerLockboxStatus -StoreAs bool -Tenant $tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableFIDO2.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableFIDO2.ps1 index 873b902eca0b..f137fd8d8534 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableFIDO2.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableFIDO2.ps1 @@ -38,19 +38,17 @@ function Invoke-CIPPStandardEnableFIDO2 { #> param($Tenant, $Settings) - ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'EnableFIDO2' try { $CurrentState = New-GraphGetRequest -Uri 'https://graph.microsoft.com/beta/policies/authenticationmethodspolicy/authenticationMethodConfigurations/Fido2' -tenantid $Tenant - } - catch { + } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the EnableFIDO2 state for $Tenant. Error: $ErrorMessage" -Sev Error return } $StateIsCorrect = ($CurrentState.state -eq 'enabled') - If ($Settings.remediate -eq $true) { + if ($Settings.remediate -eq $true) { if ($StateIsCorrect -eq $true) { Write-LogMessage -API 'Standards' -tenant $tenant -message 'FIDO2 Support is already enabled.' -sev Info } else { @@ -65,14 +63,20 @@ function Invoke-CIPPStandardEnableFIDO2 { if ($StateIsCorrect -eq $true) { Write-LogMessage -API 'Standards' -tenant $tenant -message 'FIDO2 Support is enabled' -sev Info } else { - Write-StandardsAlert -message "FIDO2 Support is not enabled" -object $CurrentState -tenant $tenant -standardName 'EnableFIDO2' -standardId $Settings.standardId + Write-StandardsAlert -message 'FIDO2 Support is not enabled' -object $CurrentState -tenant $tenant -standardName 'EnableFIDO2' -standardId $Settings.standardId Write-LogMessage -API 'Standards' -tenant $tenant -message 'FIDO2 Support is not enabled' -sev Info } } if ($Settings.report -eq $true) { $state = $StateIsCorrect ? $true : $CurrentState - Set-CIPPStandardsCompareField -FieldName 'standards.EnableFIDO2' -FieldValue $state -TenantFilter $Tenant + $CurrentValue = [PSCustomObject]@{ + EnableFIDO2 = $state + } + $ExpectedValue = [PSCustomObject]@{ + EnableFIDO2 = $true + } + Set-CIPPStandardsCompareField -FieldName 'standards.EnableFIDO2' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant Add-CIPPBPAField -FieldName 'EnableFIDO2' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableHardwareOAuth.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableHardwareOAuth.ps1 index 2f72f66f0136..2977733b7fc1 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableHardwareOAuth.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableHardwareOAuth.ps1 @@ -30,19 +30,17 @@ function Invoke-CIPPStandardEnableHardwareOAuth { #> param($Tenant, $Settings) - ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'EnableHardwareOAuth' try { $CurrentState = New-GraphGetRequest -Uri 'https://graph.microsoft.com/beta/policies/authenticationmethodspolicy/authenticationMethodConfigurations/HardwareOath' -tenantid $Tenant - } - catch { + } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the EnableHardwareOAuth state for $Tenant. Error: $ErrorMessage" -Sev Error return } $StateIsCorrect = ($CurrentState.state -eq 'enabled') - If ($Settings.remediate -eq $true) { + if ($Settings.remediate -eq $true) { if ($StateIsCorrect -eq $true) { Write-LogMessage -API 'Standards' -tenant $tenant -message 'HardwareOAuth Support is already enabled.' -sev Info } else { @@ -57,14 +55,22 @@ function Invoke-CIPPStandardEnableHardwareOAuth { if ($StateIsCorrect -eq $true) { Write-LogMessage -API 'Standards' -tenant $tenant -message 'HardwareOAuth Support is enabled' -sev Info } else { - Write-StandardsAlert -message "HardwareOAuth Support is not enabled" -object $CurrentState -tenant $tenant -standardName 'EnableHardwareOAuth' -standardId $Settings.standardId + Write-StandardsAlert -message 'HardwareOAuth Support is not enabled' -object $CurrentState -tenant $tenant -standardName 'EnableHardwareOAuth' -standardId $Settings.standardId Write-LogMessage -API 'Standards' -tenant $tenant -message 'HardwareOAuth Support is not enabled' -sev Info } } if ($Settings.report -eq $true) { $state = $StateIsCorrect ? $true : $CurrentState - Set-CIPPStandardsCompareField -FieldName 'standards.EnableHardwareOAuth' -FieldValue $state -TenantFilter $Tenant + + $CurrentValue = [PSCustomObject]@{ + EnableHardwareOAuth = $state + } + $ExpectedValue = [PSCustomObject]@{ + EnableHardwareOAuth = $true + } + + Set-CIPPStandardsCompareField -FieldName 'standards.EnableHardwareOAuth' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant Add-CIPPBPAField -FieldName 'EnableHardwareOAuth' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableLitigationHold.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableLitigationHold.ps1 index 1476f7eab82d..0b7f49ac521e 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableLitigationHold.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableLitigationHold.ps1 @@ -37,7 +37,6 @@ function Invoke-CIPPStandardEnableLitigationHold { Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. - ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'EnableLitigationHold' try { $MailboxesNoLitHold = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-Mailbox' -cmdParams @{ Filter = 'LitigationHoldEnabled -eq "False"' } -Select 'UserPrincipalName,PersistedCapabilities,LitigationHoldEnabled' | @@ -94,8 +93,16 @@ function Invoke-CIPPStandardEnableLitigationHold { if ($Settings.report -eq $true) { $filtered = $MailboxesNoLitHold | Select-Object -Property UserPrincipalName - $state = $filtered ? $MailboxesNoLitHold : $true - Set-CIPPStandardsCompareField -FieldName 'standards.EnableLitigationHold' -FieldValue $state -Tenant $Tenant + $state = $filtered ? $MailboxesNoLitHold : @() + + $CurrentValue = [PSCustomObject]@{ + EnableLitigationHold = @($state) + } + $ExpectedValue = [PSCustomObject]@{ + EnableLitigationHold = @() + } + + Set-CIPPStandardsCompareField -FieldName 'standards.EnableLitigationHold' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -Tenant $Tenant Add-CIPPBPAField -FieldName 'EnableLitHold' -FieldValue $filtered -StoreAs json -Tenant $Tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableMailTips.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableMailTips.ps1 index 0ab55ef1a2c9..c76f81dd6e3f 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableMailTips.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableMailTips.ps1 @@ -41,12 +41,10 @@ function Invoke-CIPPStandardEnableMailTips { Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. - ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'EnableMailTips' try { $MailTipsState = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-OrganizationConfig' | Select-Object MailTipsAllTipsEnabled, MailTipsExternalRecipientsTipsEnabled, MailTipsGroupMetricsEnabled, MailTipsLargeAudienceThreshold - } - catch { + } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the EnableMailTips state for $Tenant. Error: $ErrorMessage" -Sev Error return @@ -79,8 +77,15 @@ function Invoke-CIPPStandardEnableMailTips { } if ($Settings.report -eq $true) { - $state = $StateIsCorrect ? $true : $MailTipsState - Set-CIPPStandardsCompareField -FieldName 'standards.EnableMailTips' -FieldValue $State -Tenant $tenant + $CurrentValue = $MailTipsState + $ExpectedValue = [PSCustomObject]@{ + MailTipsAllTipsEnabled = $true + MailTipsExternalRecipientsTipsEnabled = $true + MailTipsGroupMetricsEnabled = $true + MailTipsLargeAudienceThreshold = $Settings.MailTipsLargeAudienceThreshold + } + + Set-CIPPStandardsCompareField -FieldName 'standards.EnableMailTips' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -Tenant $tenant Add-CIPPBPAField -FieldName 'MailTipsEnabled' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $tenant } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableMailboxAuditing.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableMailboxAuditing.ps1 index 7f5edd9b41c6..5bfab677b1d9 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableMailboxAuditing.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableMailboxAuditing.ps1 @@ -45,7 +45,6 @@ function Invoke-CIPPStandardEnableMailboxAuditing { Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. - ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'EnableMailboxAuditing' try { $AuditState = (New-ExoRequest -tenantid $Tenant -cmdlet 'Get-OrganizationConfig').AuditDisabled @@ -142,7 +141,15 @@ function Invoke-CIPPStandardEnableMailboxAuditing { if ($Settings.report -eq $true) { $AuditState = -not $AuditState - Set-CIPPStandardsCompareField -FieldName 'standards.EnableMailboxAuditing' -FieldValue $AuditState -Tenant $Tenant + + $CurrentValue = [PSCustomObject]@{ + EnableMailboxAuditing = $AuditState + } + $ExpectedValue = [PSCustomObject]@{ + EnableMailboxAuditing = $true + } + + Set-CIPPStandardsCompareField -FieldName 'standards.EnableMailboxAuditing' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -Tenant $Tenant Add-CIPPBPAField -FieldName 'MailboxAuditingEnabled' -FieldValue $AuditState -StoreAs bool -Tenant $Tenant } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableNamePronunciation.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableNamePronunciation.ps1 index c24373226143..21d676aa2253 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableNamePronunciation.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableNamePronunciation.ps1 @@ -36,7 +36,7 @@ function Invoke-CIPPStandardEnableNamePronunciation { } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Standards' -tenant $Tenant -message "Could not get CurrentState for Name Pronunciation. Error: $($ErrorMessage.NormalizedError)" -sev Error - Return + return } Write-Host $CurrentState @@ -69,7 +69,14 @@ function Invoke-CIPPStandardEnableNamePronunciation { } if ($Settings.report -eq $true) { - Set-CIPPStandardsCompareField -FieldName 'standards.EnableNamePronunciation' -FieldValue $CurrentState.isEnabledInOrganization -Tenant $tenant + $CurrentValue = [PSCustomObject]@{ + EnableNamePronunciation = $CurrentState.isEnabledInOrganization + } + $ExpectedValue = [PSCustomObject]@{ + EnableNamePronunciation = $true + } + + Set-CIPPStandardsCompareField -FieldName 'standards.EnableNamePronunciation' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -Tenant $tenant Add-CIPPBPAField -FieldName 'NamePronunciationEnabled' -FieldValue $CurrentState.isEnabledInOrganization -StoreAs bool -Tenant $tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableOnlineArchiving.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableOnlineArchiving.ps1 index bb0389b0988d..b69034289ed8 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableOnlineArchiving.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableOnlineArchiving.ps1 @@ -38,7 +38,6 @@ function Invoke-CIPPStandardEnableOnlineArchiving { Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. - ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'EnableOnlineArchiving' $MailboxPlans = @( 'ExchangeOnline', 'ExchangeOnlineEnterprise' ) $MailboxesNoArchive = $MailboxPlans | ForEach-Object { @@ -46,7 +45,7 @@ function Invoke-CIPPStandardEnableOnlineArchiving { Write-Host "Getting mailboxes without Online Archiving for plan $_" } - If ($Settings.remediate -eq $true) { + if ($Settings.remediate -eq $true) { if ($null -eq $MailboxesNoArchive) { Write-LogMessage -API 'Standards' -tenant $Tenant -message 'Online Archiving already enabled for all accounts' -sev Info @@ -90,8 +89,16 @@ function Invoke-CIPPStandardEnableOnlineArchiving { if ($Settings.report -eq $true) { $filtered = $MailboxesNoArchive | Select-Object -Property UserPrincipalName, ArchiveGuid - $stateReport = $filtered ? $filtered : $true - Set-CIPPStandardsCompareField -FieldName 'standards.EnableOnlineArchiving' -FieldValue $stateReport -TenantFilter $Tenant + $stateReport = $filtered ? $filtered : @() + + $CurrentValue = [PSCustomObject]@{ + ArchiveNotEnabled = @($stateReport) + } + $ExpectedValue = [PSCustomObject]@{ + ArchiveNotEnabled = @() + } + + Set-CIPPStandardsCompareField -FieldName 'standards.EnableOnlineArchiving' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant Add-CIPPBPAField -FieldName 'EnableOnlineArchiving' -FieldValue $filtered -StoreAs json -Tenant $Tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnablePronouns.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnablePronouns.ps1 index 99df96d1ca0c..1288147b8448 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnablePronouns.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnablePronouns.ps1 @@ -30,7 +30,6 @@ function Invoke-CIPPStandardEnablePronouns { #> param ($Tenant, $Settings) - ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'EnablePronouns' $Uri = 'https://graph.microsoft.com/v1.0/admin/people/pronouns' try { @@ -38,7 +37,7 @@ function Invoke-CIPPStandardEnablePronouns { } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Standards' -tenant $Tenant -message "Could not get CurrentState for Pronouns. Error: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Return + return } if ($Settings.remediate -eq $true) { @@ -70,7 +69,14 @@ function Invoke-CIPPStandardEnablePronouns { } if ($Settings.report -eq $true) { - Set-CIPPStandardsCompareField -FieldName 'standards.EnablePronouns' -FieldValue $CurrentState.isEnabledInOrganization -Tenant $tenant + $CurrentValue = [PSCustomObject]@{ + EnablePronouns = $CurrentState.isEnabledInOrganization + } + $ExpectedValue = [PSCustomObject]@{ + EnablePronouns = $true + } + + Set-CIPPStandardsCompareField -FieldName 'standards.EnablePronouns' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant Add-CIPPBPAField -FieldName 'PronounsEnabled' -FieldValue $CurrentState.isEnabledInOrganization -StoreAs bool -Tenant $tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnrollmentWindowsHelloForBusinessConfiguration.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnrollmentWindowsHelloForBusinessConfiguration.ps1 index dc8ab4838108..5115ee7463b8 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnrollmentWindowsHelloForBusinessConfiguration.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnrollmentWindowsHelloForBusinessConfiguration.ps1 @@ -51,9 +51,8 @@ function Invoke-CIPPStandardEnrollmentWindowsHelloForBusinessConfiguration { try { $CurrentState = New-GraphGetRequest -Uri "https://graph.microsoft.com/beta/deviceManagement/deviceEnrollmentConfigurations?`$expand=assignments&orderBy=priority&`$filter=deviceEnrollmentConfigurationType eq 'WindowsHelloForBusiness'" -tenantID $Tenant -AsApp $true | - Select-Object -Property id, pinMinimumLength, pinMaximumLength, pinUppercaseCharactersUsage, pinLowercaseCharactersUsage, pinSpecialCharactersUsage, state, securityDeviceRequired, unlockWithBiometricsEnabled, remotePassportEnabled, pinPreviousBlockCount, pinExpirationInDays, enhancedBiometricsState - } - catch { + Select-Object -Property id, pinMinimumLength, pinMaximumLength, pinUppercaseCharactersUsage, pinLowercaseCharactersUsage, pinSpecialCharactersUsage, state, securityDeviceRequired, unlockWithBiometricsEnabled, remotePassportEnabled, pinPreviousBlockCount, pinExpirationInDays, enhancedBiometricsState + } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the EnrollmentWindowsHelloForBusinessConfiguration state for $Tenant. Error: $ErrorMessage" -Sev Error return @@ -87,11 +86,25 @@ function Invoke-CIPPStandardEnrollmentWindowsHelloForBusinessConfiguration { enhancedBiometricsState = $CurrentState.enhancedBiometricsState } - If ($Settings.remediate -eq $true) { + $ExpectedValue = [PSCustomObject]@{ + pinMinimumLength = $Settings.pinMinimumLength + pinMaximumLength = $Settings.pinMaximumLength + pinUppercaseCharactersUsage = $Settings.pinUppercaseCharactersUsage.value + pinLowercaseCharactersUsage = $Settings.pinLowercaseCharactersUsage.value + pinSpecialCharactersUsage = $Settings.pinSpecialCharactersUsage.value + state = $Settings.state.value + securityDeviceRequired = $Settings.securityDeviceRequired + unlockWithBiometricsEnabled = $Settings.unlockWithBiometricsEnabled + remotePassportEnabled = $Settings.remotePassportEnabled + pinPreviousBlockCount = $Settings.pinPreviousBlockCount + pinExpirationInDays = $Settings.pinExpirationInDays + enhancedBiometricsState = $Settings.enhancedBiometricsState.value + } + + if ($Settings.remediate -eq $true) { if ($StateIsCorrect -eq $true) { Write-LogMessage -API 'Standards' -Tenant $Tenant -Message 'EnrollmentWindowsHelloForBusinessConfiguration is already applied correctly.' -Sev Info - } - else { + } else { $cmdParam = @{ tenantid = $Tenant uri = "https://graph.microsoft.com/beta/deviceManagement/deviceEnrollmentConfigurations/$($CurrentState.id)" @@ -99,9 +112,9 @@ function Invoke-CIPPStandardEnrollmentWindowsHelloForBusinessConfiguration { Type = 'PATCH' ContentType = 'application/json; charset=utf-8' Body = [PSCustomObject]@{ - "@odata.type" = "#microsoft.graph.deviceEnrollmentWindowsHelloForBusinessConfiguration" - pinMinimumLength = $Settings.pinMinimumLength - pinMaximumLength = $Settings.pinMaximumLength + '@odata.type' = '#microsoft.graph.deviceEnrollmentWindowsHelloForBusinessConfiguration' + pinMinimumLength = $Settings.pinMinimumLength + pinMaximumLength = $Settings.pinMaximumLength pinUppercaseCharactersUsage = $Settings.pinUppercaseCharactersUsage.value pinLowercaseCharactersUsage = $Settings.pinLowercaseCharactersUsage.value pinSpecialCharactersUsage = $Settings.pinSpecialCharactersUsage.value @@ -117,8 +130,7 @@ function Invoke-CIPPStandardEnrollmentWindowsHelloForBusinessConfiguration { try { $null = New-GraphPostRequest @cmdParam Write-LogMessage -API 'Standards' -Tenant $Tenant -Message 'Successfully updated EnrollmentWindowsHelloForBusinessConfiguration.' -Sev Info - } - catch { + } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Failed to update EnrollmentWindowsHelloForBusinessConfiguration. Error: $($ErrorMessage.NormalizedError)" -Sev Error } @@ -126,18 +138,16 @@ function Invoke-CIPPStandardEnrollmentWindowsHelloForBusinessConfiguration { } - If ($Settings.alert -eq $true) { + if ($Settings.alert -eq $true) { if ($StateIsCorrect -eq $true) { Write-LogMessage -API 'Standards' -Tenant $Tenant -Message 'EnrollmentWindowsHelloForBusinessConfiguration is correctly set.' -Sev Info - } - else { + } else { Write-StandardsAlert -message 'EnrollmentWindowsHelloForBusinessConfiguration is incorrectly set.' -object $CompareField -tenant $Tenant -standardName 'EnrollmentWindowsHelloForBusinessConfiguration' -standardId $Settings.standardId Write-LogMessage -API 'Standards' -Tenant $Tenant -Message 'EnrollmentWindowsHelloForBusinessConfiguration is incorrectly set.' -Sev Info } } - If ($Settings.report -eq $true) { - $FieldValue = $StateIsCorrect ? $true : $CompareField - Set-CIPPStandardsCompareField -FieldName 'standards.EnrollmentWindowsHelloForBusinessConfiguration' -FieldValue $FieldValue -TenantFilter $Tenant + if ($Settings.report -eq $true) { + Set-CIPPStandardsCompareField -FieldName 'standards.EnrollmentWindowsHelloForBusinessConfiguration' -CurrentValue $CompareField -ExpectedValue $ExpectedValue -TenantFilter $Tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandarddisableMacSync.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandarddisableMacSync.ps1 index d5a024756ccc..df8b04ecfdeb 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandarddisableMacSync.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandarddisableMacSync.ps1 @@ -30,8 +30,7 @@ function Invoke-CIPPStandarddisableMacSync { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'disableMacSync' -TenantFilter $Tenant -RequiredCapabilities @('SHAREPOINTWAC', 'SHAREPOINTSTANDARD', 'SHAREPOINTENTERPRISE', 'SHAREPOINTENTERPRISE_EDU','ONEDRIVE_BASIC', 'ONEDRIVE_ENTERPRISE') - ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'disableMacSync' + $TestResult = Test-CIPPStandardLicense -StandardName 'disableMacSync' -TenantFilter $Tenant -RequiredCapabilities @('SHAREPOINTWAC', 'SHAREPOINTSTANDARD', 'SHAREPOINTENTERPRISE', 'SHAREPOINTENTERPRISE_EDU', 'ONEDRIVE_BASIC', 'ONEDRIVE_ENTERPRISE') if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." @@ -40,14 +39,13 @@ function Invoke-CIPPStandarddisableMacSync { try { $CurrentInfo = New-GraphGetRequest -Uri 'https://graph.microsoft.com/beta/admin/sharepoint/settings' -tenantid $Tenant -AsApp $true - } - catch { + } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the DisableMacSync state for $Tenant. Error: $ErrorMessage" -Sev Error return } - If ($Settings.remediate -eq $true) { + if ($Settings.remediate -eq $true) { if ($CurrentInfo.isMacSyncAppEnabled -eq $true) { try { @@ -74,8 +72,16 @@ function Invoke-CIPPStandarddisableMacSync { } if ($Settings.report -eq $true) { - $CurrentInfo.isMacSyncAppEnabled = -not $CurrentInfo.isMacSyncAppEnabled - Set-CIPPStandardsCompareField -FieldName 'standards.disableMacSync' -FieldValue $CurrentInfo.isMacSyncAppEnabled -TenantFilter $Tenant - Add-CIPPBPAField -FieldName 'MacSync' -FieldValue $CurrentInfo.isMacSyncAppEnabled -StoreAs bool -Tenant $tenant + $CurrentState = -not $CurrentInfo.isMacSyncAppEnabled + + $CurrentValue = [PSCustomObject]@{ + MacSyncDisabled = $CurrentState + } + $ExpectedValue = [PSCustomObject]@{ + MacSyncDisabled = $true + } + + Set-CIPPStandardsCompareField -FieldName 'standards.disableMacSync' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant + Add-CIPPBPAField -FieldName 'MacSync' -FieldValue $CurrentState -StoreAs bool -Tenant $tenant } } From 05151f80d8e2396beca9bbb76ec6da868350a5ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Wed, 14 Jan 2026 00:01:04 +0100 Subject: [PATCH 134/503] feat(users): enhance user creation task handling - Improve error handling for scheduled user creation. - Ensure detailed error messages are thrown for user creation failures. --- .../Administration/Users/Invoke-AddUser.ps1 | 80 +++++++++++-------- Modules/CIPPCore/Public/New-CIPPUserTask.ps1 | 8 +- 2 files changed, 51 insertions(+), 37 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-AddUser.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-AddUser.ps1 index 8d679686c945..f1792c18adf9 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-AddUser.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-AddUser.ps1 @@ -15,48 +15,62 @@ function Invoke-AddUser { $UserObj = $Request.Body if ($UserObj.Scheduled.Enabled) { - $Username = $UserObj.username ?? $UserObj.mailNickname - $TaskBody = [pscustomobject]@{ - TenantFilter = $UserObj.tenantFilter - Name = "New user creation: $($Username)@$($UserObj.PrimDomain.value)" - Command = @{ - value = 'New-CIPPUserTask' - label = 'New-CIPPUserTask' + try { + $Username = $UserObj.username ?? $UserObj.mailNickname + $TaskBody = [pscustomobject]@{ + TenantFilter = $UserObj.tenantFilter + Name = "New user creation: $($Username)@$($UserObj.PrimDomain.value)" + Command = @{ + value = 'New-CIPPUserTask' + label = 'New-CIPPUserTask' + } + Parameters = [pscustomobject]@{ UserObj = $UserObj } + ScheduledTime = $UserObj.Scheduled.date + Reference = $UserObj.reference ?? $null + PostExecution = @{ + Webhook = [bool]$Request.Body.PostExecution.Webhook + Email = [bool]$Request.Body.PostExecution.Email + PSA = [bool]$Request.Body.PostExecution.PSA + } } - Parameters = [pscustomobject]@{ UserObj = $UserObj } - ScheduledTime = $UserObj.Scheduled.date - Reference = $UserObj.reference ?? $null - PostExecution = @{ - Webhook = [bool]$Request.Body.PostExecution.Webhook - Email = [bool]$Request.Body.PostExecution.Email - PSA = [bool]$Request.Body.PostExecution.PSA + Add-CIPPScheduledTask -Task $TaskBody -hidden $false -DisallowDuplicateName $true -Headers $Headers + $body = [pscustomobject] @{ + 'Results' = @("Successfully created scheduled task to create user $($UserObj.DisplayName)") } - } - Add-CIPPScheduledTask -Task $TaskBody -hidden $false -DisallowDuplicateName $true -Headers $Headers - $body = [pscustomobject] @{ - 'Results' = @("Successfully created scheduled task to create user $($UserObj.DisplayName)") + } catch { + $body = [pscustomobject] @{ + 'Results' = @("Failed to create scheduled task to create user $($UserObj.DisplayName): $($_.Exception.Message)") + } + $StatusCode = [HttpStatusCode]::InternalServerError } } else { - $CreationResults = New-CIPPUserTask -UserObj $UserObj -APIName $APIName -Headers $Headers - $body = [pscustomobject] @{ - 'Results' = @( - $CreationResults.Results[0], - $CreationResults.Results[1], - @{ - 'resultText' = $CreationResults.Results[2] - 'copyField' = $CreationResults.password - 'state' = 'success' + try { + $CreationResults = New-CIPPUserTask -UserObj $UserObj -APIName $APIName -Headers $Headers + $body = [pscustomobject] @{ + 'Results' = @( + $CreationResults.Results[0], + $CreationResults.Results[1], + @{ + 'resultText' = $CreationResults.Results[2] + 'copyField' = $CreationResults.password + 'state' = 'success' + } + ) + 'CopyFrom' = @{ + 'Success' = $CreationResults.CopyFrom.Success + 'Error' = $CreationResults.CopyFrom.Error } - ) - 'CopyFrom' = @{ - 'Success' = $CreationResults.CopyFrom.Success - 'Error' = $CreationResults.CopyFrom.Error + 'User' = $CreationResults.User + } + } catch { + $body = [pscustomobject] @{ + 'Results' = @("$($_.Exception.Message)") } - 'User' = $CreationResults.User + $StatusCode = [HttpStatusCode]::InternalServerError } } return ([HttpResponseContext]@{ - StatusCode = [HttpStatusCode]::OK + StatusCode = $StatusCode ? $StatusCode : [HttpStatusCode]::OK Body = $Body }) diff --git a/Modules/CIPPCore/Public/New-CIPPUserTask.ps1 b/Modules/CIPPCore/Public/New-CIPPUserTask.ps1 index ed21070cefa5..fb831949ccaa 100644 --- a/Modules/CIPPCore/Public/New-CIPPUserTask.ps1 +++ b/Modules/CIPPCore/Public/New-CIPPUserTask.ps1 @@ -14,15 +14,15 @@ function New-CIPPUserTask { $Results.Add("Username: $($CreationResults.Username)") $Results.Add("Password: $($CreationResults.Password)") } catch { - $Results.Add("Failed to create user. $($_.Exception.Message)" ) - return @{'Results' = $Results } + $Results.Add("$($_.Exception.Message)" ) + throw @{'Results' = $Results } } try { if ($UserObj.licenses.value) { if ($UserObj.sherwebLicense.value) { - $License = Set-SherwebSubscription -Headers $Headers -TenantFilter $UserObj.tenantFilter -SKU $UserObj.sherwebLicense.value -Add 1 - $null = $results.Add('Added Sherweb License, scheduling assignment') + $null = Set-SherwebSubscription -Headers $Headers -TenantFilter $UserObj.tenantFilter -SKU $UserObj.sherwebLicense.value -Add 1 + $null = $Results.Add('Added Sherweb License, scheduling assignment') $taskObject = [PSCustomObject]@{ TenantFilter = $UserObj.tenantFilter Name = "Assign License: $UserPrincipalName" From d43c90364b3c5b0c8c9116ba752b5752566c9bad Mon Sep 17 00:00:00 2001 From: John Duprey Date: Wed, 14 Jan 2026 11:53:19 -0500 Subject: [PATCH 135/503] drop log message --- .../Administration/Users/Invoke-ListJITAdminTemplates.ps1 | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListJITAdminTemplates.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListJITAdminTemplates.ps1 index bfe8dc5eb820..aa5ad886758e 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListJITAdminTemplates.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListJITAdminTemplates.ps1 @@ -11,8 +11,6 @@ function Invoke-ListJITAdminTemplates { $APIName = $Request.Params.CIPPEndpoint $Headers = $Request.Headers - Write-LogMessage -headers $Headers -API $APIName -message 'Listing JIT Admin Templates' -Sev 'Info' - # Get the TenantFilter from query parameters $TenantFilter = $Request.Query.TenantFilter @@ -44,7 +42,7 @@ function Invoke-ListJITAdminTemplates { if ($TenantFilter) { if ($TenantFilter -eq 'AllTenants') { # When requesting AllTenants, return only templates stored under AllTenants - $Templates = $Templates | Where-Object -Property tenantFilter -eq 'AllTenants' + $Templates = $Templates | Where-Object -Property tenantFilter -EQ 'AllTenants' } else { # When requesting a specific tenant if ($IncludeAllTenants) { @@ -52,7 +50,7 @@ function Invoke-ListJITAdminTemplates { $Templates = $Templates | Where-Object { $_.tenantFilter -eq $TenantFilter -or $_.tenantFilter -eq 'AllTenants' } } else { # Return only tenant-specific templates (exclude AllTenants) - $Templates = $Templates | Where-Object -Property tenantFilter -eq $TenantFilter + $Templates = $Templates | Where-Object -Property tenantFilter -EQ $TenantFilter } } } @@ -62,7 +60,7 @@ function Invoke-ListJITAdminTemplates { # If a specific GUID is requested, filter to that template if ($Request.query.GUID) { - $Templates = $Templates | Where-Object -Property GUID -eq $Request.query.GUID + $Templates = $Templates | Where-Object -Property GUID -EQ $Request.query.GUID } $Templates = ConvertTo-Json -InputObject @($Templates) -Depth 100 From 4adc6fe62579300261fd05e7bf8c451632c3b232 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Wed, 14 Jan 2026 14:55:57 -0500 Subject: [PATCH 136/503] Remove redundant log message in template retrieval Eliminated an unnecessary Write-LogMessage call when retrieving a specific template by TemplateId to reduce log verbosity. --- .../Application Approval/Invoke-ExecAppPermissionTemplate.ps1 | 1 - 1 file changed, 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Application Approval/Invoke-ExecAppPermissionTemplate.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Application Approval/Invoke-ExecAppPermissionTemplate.ps1 index ed347dc8dc35..3cdc003452d4 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Application Approval/Invoke-ExecAppPermissionTemplate.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Application Approval/Invoke-ExecAppPermissionTemplate.ps1 @@ -73,7 +73,6 @@ function Invoke-ExecAppPermissionTemplate { if ($Request.Query.TemplateId) { $templateId = $Request.Query.TemplateId $filter = "PartitionKey eq 'Templates' and RowKey eq '$templateId'" - Write-LogMessage -headers $Headers -API 'ExecAppPermissionTemplate' -message "Retrieved specific template: $templateId" -Sev 'Info' } $Body = Get-CIPPAzDataTableEntity @Table -Filter $filter | ForEach-Object { From 121cecd80f9bbddbc1c2d81c84b4640a6f3d7532 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Wed, 14 Jan 2026 14:56:06 -0500 Subject: [PATCH 137/503] Optimize app template creation with bulk Graph requests Refactored the function to use Microsoft Graph bulk requests for retrieving app registrations and service principals, reducing redundant API calls and improving performance. Enhanced permission extraction logic to handle cases where app registration is inaccessible by building permissions from service principal grants and assignments. Improved translation of permission IDs to claim values using bulk-fetched service principal details. --- .../Invoke-ExecCreateAppTemplate.ps1 | 194 +++++++++++++++--- 1 file changed, 161 insertions(+), 33 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Application Approval/Invoke-ExecCreateAppTemplate.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Application Approval/Invoke-ExecCreateAppTemplate.ps1 index 202377450790..9e02c588bf36 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Application Approval/Invoke-ExecCreateAppTemplate.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Application Approval/Invoke-ExecCreateAppTemplate.ps1 @@ -27,45 +27,107 @@ function Invoke-ExecCreateAppTemplate { throw 'DisplayName is required' } + # Build initial bulk request to get app registration and all service principals + # The SP we need will be in the splist, so we don't need a separate call + $InitialBulkRequests = @( + [PSCustomObject]@{ + id = 'app' + method = 'GET' + url = "/applications(appId='$AppId')?`$select=id,appId,displayName,requiredResourceAccess" + } + [PSCustomObject]@{ + id = 'splist' + method = 'GET' + url = '/servicePrincipals?$top=999&$select=id,appId,displayName' + } + ) + + Write-Information "Retrieving app details for AppId: $AppId in tenant: $TenantFilter" + $InitialResults = New-GraphBulkRequest -tenantid $TenantFilter -Requests $InitialBulkRequests -NoAuthCheck $true -AsApp $true + + $AppResult = $InitialResults | Where-Object { $_.id -eq 'app' } | Select-Object -First 1 + $TenantInfo = ($InitialResults | Where-Object { $_.id -eq 'splist' }).body.value + + # Find the specific service principal in the list + $SPResult = $TenantInfo | Where-Object { $_.appId -eq $AppId } | Select-Object -First 1 + # Get the app details based on type if ($Type -eq 'servicePrincipal') { - # For enterprise apps (service principals) - $AppDetails = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/servicePrincipals?`$filter=appId eq '$AppId'&`$select=id,appId,displayName,appRoles,oauth2PermissionScopes,requiredResourceAccess" -tenantid $TenantFilter - - if (-not $AppDetails -or $AppDetails.Count -eq 0) { + if (-not $SPResult) { throw "Service principal not found for AppId: $AppId" } - $App = $AppDetails[0] + $App = $SPResult - # Get the application registration to access requiredResourceAccess - try { - $AppRegistration = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/applications?`$filter=appId eq '$AppId'&`$select=id,appId,displayName,requiredResourceAccess" -tenantid $TenantFilter - if ($AppRegistration -and $AppRegistration.Count -gt 0) { - $RequiredResourceAccess = $AppRegistration[0].requiredResourceAccess - } else { - $RequiredResourceAccess = @() + # Check if we got the app registration and it has permissions + if ($AppResult.status -eq 200 -and $AppResult.body.requiredResourceAccess -and $AppResult.body.requiredResourceAccess.Count -gt 0) { + Write-LogMessage -headers $Request.headers -API $APINAME -message "Retrieved requiredResourceAccess from app registration for $AppId" -Sev 'Info' + $Permissions = $AppResult.body.requiredResourceAccess + } else { + # App registration not accessible or no permissions configured + # Build permissions from oauth2PermissionGrants and appRoleAssignments + Write-LogMessage -headers $Request.headers -API $APINAME -message "Could not retrieve app registration for $AppId - extracting from service principal grants and role assignments" -Sev 'Info' + + # Bulk request to get grants and assignments + $GrantsBulkRequests = @( + [PSCustomObject]@{ + id = 'grants' + method = 'GET' + url = "/servicePrincipals(appId='$AppId')/oauth2PermissionGrants" + } + [PSCustomObject]@{ + id = 'assignments' + method = 'GET' + url = "/servicePrincipals(appId='$AppId')/appRoleAssignments" + } + ) + + $GrantsResults = New-GraphBulkRequest -tenantid $TenantFilter -Requests $GrantsBulkRequests -NoAuthCheck $true -AsApp $true + + $DelegatePermissionGrants = ($GrantsResults | Where-Object { $_.id -eq 'grants' }).body.value + $AppRoleAssignments = ($GrantsResults | Where-Object { $_.id -eq 'assignments' }).body.value + + $DelegateResourceAccess = $DelegatePermissionGrants | Group-Object -Property resourceId | ForEach-Object { + [pscustomobject]@{ + resourceAppId = ($TenantInfo | Where-Object -Property id -EQ $_.Name).appId + resourceAccess = @($_.Group | ForEach-Object { + [pscustomobject]@{ + id = $_.scope + type = 'Scope' + } + }) + } } - } catch { - Write-LogMessage -headers $Request.headers -API $APINAME -message "Could not retrieve app registration for $AppId - will extract from service principal" -Sev 'Warning' - $RequiredResourceAccess = @() - } - # Use requiredResourceAccess if available, otherwise we can't create a proper template - if ($RequiredResourceAccess -and $RequiredResourceAccess.Count -gt 0) { - $Permissions = $RequiredResourceAccess - } else { - # No permissions found - warn the user - Write-LogMessage -headers $Request.headers -API $APINAME -message "No permissions found for $AppId. The app registration may not have configured API permissions." -Sev 'Warning' - $Permissions = @() + $ApplicationResourceAccess = $AppRoleAssignments | Group-Object -Property ResourceId | ForEach-Object { + [pscustomobject]@{ + resourceAppId = ($TenantInfo | Where-Object -Property id -EQ $_.Name).appId + resourceAccess = @($_.Group | ForEach-Object { + [pscustomobject]@{ + id = $_.appRoleId + type = 'Role' + } + }) + } + } + + # Combine both delegated and application permissions + $Permissions = @($DelegateResourceAccess) + @($ApplicationResourceAccess) | Where-Object { $_ -ne $null } + + if ($Permissions.Count -eq 0) { + Write-LogMessage -headers $Request.headers -API $APINAME -message "No permissions found for $AppId via any method" -Sev 'Warning' + } else { + Write-LogMessage -headers $Request.headers -API $APINAME -message "Extracted $($Permissions.Count) resource permission(s) from service principal grants" -Sev 'Info' + } } } else { # For app registrations (applications) - $App = New-GraphGetRequest -uri "https://graph.microsoft.com/v1.0/applications(appId='$AppId')" -tenantid $TenantFilter - if (-not $App -or $App.Count -eq 0) { + if ($AppResult.status -ne 200 -or -not $AppResult.body) { throw "App registration not found for AppId: $AppId" } + $App = $AppResult.body + $Tenant = Get-Tenants -TenantFilter $TenantFilter if ($Tenant.customerId -ne $env:TenantID) { $ExistingApp = New-GraphGetRequest -uri "https://graph.microsoft.com/v1.0/applications?`$filter=displayName eq '$DisplayName'" -tenantid $env:TenantID -NoAuthCheck $true -AsApp $true @@ -136,7 +198,7 @@ function Invoke-ExecCreateAppTemplate { $Body = @{ appId = $AppId } - $NewSP = New-GraphPOSTRequest -uri 'https://graph.microsoft.com/v1.0/servicePrincipals' -tenantid $env:TenantID -NoAuthCheck $true -AsApp $true -type POST -body ($Body | ConvertTo-Json -Depth 10) + $null = New-GraphPOSTRequest -uri 'https://graph.microsoft.com/v1.0/servicePrincipals' -tenantid $env:TenantID -NoAuthCheck $true -AsApp $true -type POST -body ($Body | ConvertTo-Json -Depth 10) Write-LogMessage -headers $Request.headers -API $APINAME -message "App Registration $($AppDetails.displayName) copied to partner tenant" -Sev 'Info' } } @@ -152,21 +214,87 @@ function Invoke-ExecCreateAppTemplate { $PermissionSetName = "$DisplayName (Auto-created)" if ($Permissions -and $Permissions.Count -gt 0) { + # Build bulk requests to get all service principals efficiently using object IDs from cached list + $BulkRequests = [System.Collections.Generic.List[object]]::new() + $RequestIndex = 0 + $AppIdToRequestId = @{} + + foreach ($Resource in $Permissions) { + $ResourceAppId = $Resource.resourceAppId + + # Find the service principal object ID from the cached list + $ResourceSPInfo = $TenantInfo | Where-Object { $_.appId -eq $ResourceAppId } | Select-Object -First 1 + + if ($ResourceSPInfo) { + $RequestId = "sp-$RequestIndex" + $AppIdToRequestId[$ResourceAppId] = $RequestId + + # Use object ID to fetch full details with appRoles and oauth2PermissionScopes + $BulkRequests.Add([PSCustomObject]@{ + id = $RequestId + method = 'GET' + url = "/servicePrincipals/$($ResourceSPInfo.id)?`$select=id,appId,displayName,appRoles,oauth2PermissionScopes" + }) + $RequestIndex++ + } else { + Write-LogMessage -headers $Request.headers -API $APINAME -message "Service principal not found in tenant for appId: $ResourceAppId" -Sev 'Warning' + } + } + + # Execute bulk request to get all service principals at once (only if we have requests) + if ($BulkRequests.Count -gt 0) { + Write-Information "Fetching $($BulkRequests.Count) service principal(s) via bulk request" + $BulkResults = New-GraphBulkRequest -tenantid $TenantFilter -Requests $BulkRequests -NoAuthCheck $true -AsApp $true + + # Create lookup table for service principals by appId + $SPLookup = @{} + foreach ($Result in $BulkResults) { + if ($Result.status -eq 200 -and $Result.body) { + $SPLookup[$Result.body.appId] = $Result.body + } + } + } else { + $SPLookup = @{} + } + + # Now process permissions for each resource foreach ($Resource in $Permissions) { $ResourceAppId = $Resource.resourceAppId $AppPerms = [System.Collections.ArrayList]::new() $DelegatedPerms = [System.Collections.ArrayList]::new() - foreach ($Access in $Resource.resourceAccess) { - $PermObj = [PSCustomObject]@{ - id = $Access.id - value = $Access.id # In the permission set format, both id and value are the permission ID - } + $ResourceSP = $SPLookup[$ResourceAppId] + + if (!$ResourceSP) { + Write-LogMessage -headers $Request.headers -API $APINAME -message "Service principal not found for appId: $ResourceAppId - skipping permission translation" -Sev 'Warning' + continue + } + foreach ($Access in $Resource.resourceAccess) { if ($Access.type -eq 'Role') { - [void]$AppPerms.Add($PermObj) + # Look up application permission name from appRoles + $AppRole = $ResourceSP.appRoles | Where-Object { $_.id -eq $Access.id } | Select-Object -First 1 + if ($AppRole) { + $PermObj = [PSCustomObject]@{ + id = $Access.id + value = $AppRole.value # Use the claim value name, not the GUID + } + [void]$AppPerms.Add($PermObj) + } else { + Write-LogMessage -headers $Request.headers -API $APINAME -message "Application permission $($Access.id) not found in $ResourceAppId appRoles" -Sev 'Warning' + } } elseif ($Access.type -eq 'Scope') { - [void]$DelegatedPerms.Add($PermObj) + # Look up delegated permission name from oauth2PermissionScopes + $PermissionScope = $ResourceSP.oauth2PermissionScopes | Where-Object { $_.id -eq $Access.id } | Select-Object -First 1 + if ($PermissionScope) { + $PermObj = [PSCustomObject]@{ + id = $Access.id + value = $PermissionScope.value # Use the claim value name, not the GUID + } + [void]$DelegatedPerms.Add($PermObj) + } else { + Write-LogMessage -headers $Request.headers -API $APINAME -message "Delegated permission $($Access.id) not found in $ResourceAppId oauth2PermissionScopes" -Sev 'Warning' + } } } From 2ef973190404f7533d2146f2cd208aa70bb4e2b9 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Wed, 14 Jan 2026 15:43:13 -0500 Subject: [PATCH 138/503] Serialize non-string values in standards compare Added logic to convert non-string $CurrentValue and $ExpectedValue to compressed JSON strings in Set-CIPPStandardsCompareField. This ensures consistent handling of complex objects during comparison. --- Modules/CIPPCore/Public/Set-CIPPStandardsCompareField.ps1 | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Modules/CIPPCore/Public/Set-CIPPStandardsCompareField.ps1 b/Modules/CIPPCore/Public/Set-CIPPStandardsCompareField.ps1 index ed868fbb2663..a12bd21aaefb 100644 --- a/Modules/CIPPCore/Public/Set-CIPPStandardsCompareField.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPStandardsCompareField.ps1 @@ -27,6 +27,13 @@ function Set-CIPPStandardsCompareField { } } + if ($CurrentValue -and $CurrentValue -isnot [string]) { + $CurrentValue = [string](ConvertTo-Json -InputObject $CurrentValue -Depth 10 -Compress) + } + if ($ExpectedValue -and $ExpectedValue -isnot [string]) { + $ExpectedValue = [string](ConvertTo-Json -InputObject $ExpectedValue -Depth 10 -Compress) + } + # Handle bulk operations if ($BulkFields) { # Get all existing entities for this tenant in one query From e896fab590c24cc27b93a3c682181379e982cebb Mon Sep 17 00:00:00 2001 From: John Duprey Date: Wed, 14 Jan 2026 15:43:37 -0500 Subject: [PATCH 139/503] Refactor Exchange Connector template handling Streamlines retrieval and processing of Exchange Connector templates by fetching all relevant templates at once and using them for remediation, alerting, and reporting. Improves efficiency and consistency in connector management, and enhances reporting and alerting logic for template deployment status. --- ...-CIPPStandardExchangeConnectorTemplate.ps1 | 80 ++++++++++++++++--- 1 file changed, 68 insertions(+), 12 deletions(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardExchangeConnectorTemplate.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardExchangeConnectorTemplate.ps1 index 9213b8324650..e40f07aabcc4 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardExchangeConnectorTemplate.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardExchangeConnectorTemplate.ps1 @@ -34,34 +34,90 @@ function Invoke-CIPPStandardExchangeConnectorTemplate { Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. - ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'ExConnector' - if ($Settings.remediate -eq $true) { + $Table = Get-CippTable -tablename 'templates' + $Filter = "PartitionKey eq 'ExConnectorTemplate'" + $AllConnectorTemplates = Get-CIPPAzDataTableEntity @Table -Filter $Filter + $TemplateIds = $Settings.exConnectorTemplate.value ?? $Settings.exConnectorTemplate + $Templates = $AllConnectorTemplates | Where-Object { $TemplateIds -contains $_.RowKey } + $Types = $Templates.direction | Sort-Object -Unique + + $ExoBulkCommands = foreach ($Type in $Types) { + @{ + CmdletInput = @{ + CmdletName = "Get-$($Type)connector" + Parameters = @{} + } + } + } + $ExistingConnectors = New-ExoBulkRequest -tenantid $Tenant -cmdletArray @($ExoBulkCommands) -ReturnWithCommand $true - foreach ($Template in $Settings.TemplateList) { + if ($Settings.remediate -eq $true) { + foreach ($Template in $Templates) { try { - $Table = Get-CippTable -tablename 'templates' - $Filter = "PartitionKey eq 'ExConnectorTemplate' and RowKey eq '$($Template.value)'" - $connectorType = (Get-AzDataTableEntity @Table -Filter $Filter).direction - $RequestParams = (Get-AzDataTableEntity @Table -Filter $Filter).JSON | ConvertFrom-Json + $ConnectorType = $Template.direction + $RequestParams = $Template.JSON | ConvertFrom-Json if ($RequestParams.comment) { $RequestParams.comment = Get-CIPPTextReplacement -Text $RequestParams.comment -TenantFilter $Tenant } else { $RequestParams | Add-Member -NotePropertyValue 'no comment' -NotePropertyName comment -Force } - $Existing = New-ExoRequest -ErrorAction SilentlyContinue -tenantid $Tenant -cmdlet "Get-$($ConnectorType)connector" | Where-Object -Property Identity -EQ $RequestParams.name + $Existing = $ExistingConnectors.$("Get-$($ConnectorType)connector") | Where-Object -Property Identity -EQ $RequestParams.name if ($Existing) { $RequestParams | Add-Member -NotePropertyValue $Existing.Identity -NotePropertyName Identity -Force $null = New-ExoRequest -tenantid $Tenant -cmdlet "Set-$($ConnectorType)connector" -cmdParams $RequestParams -useSystemMailbox $true - Write-LogMessage -API 'Standards' -tenant $Tenant -message "Updated transport rule for $($Tenant, $Settings)" -sev info + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Updated transport rule $($RequestParams.name)" -sev info } else { $null = New-ExoRequest -tenantid $Tenant -cmdlet "New-$($ConnectorType)connector" -cmdParams $RequestParams -useSystemMailbox $true - Write-LogMessage -API 'Standards' -tenant $Tenant -message "Created transport rule for $($Tenant, $Settings)" -sev info + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Created transport rule $($RequestParams.name)" -sev info } } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message - Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to create or update Exchange Connector Rule: $ErrorMessage" -sev 'Error' + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to create or update Exchange Connector Rule: $ErrorMessage" -sev 'Error' } - } + } + if ($Settings.alert -eq $true) { + foreach ($Template in $Templates) { + $ConnectorType = $Template.direction + $RequestParams = $Template.JSON | ConvertFrom-Json + $Existing = $ExistingConnectors.$("Get-$($ConnectorType)connector") | Where-Object -Property Identity -EQ $RequestParams.name + if (-not $Existing) { + Write-StandardsAlert -message "Exchange Connector Template '$($RequestParams.name)' of type '$($ConnectorType)' is not deployed" -object $RequestParams -tenant $Tenant -standardName 'ExchangeConnectorTemplate' -standardId $Settings.standardId + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Exchange Connector Template '$($RequestParams.name)' of type '$($ConnectorType)' is not deployed" -sev Warning + } else { + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Exchange Connector Template '$($RequestParams.name)' of type '$($ConnectorType)' is deployed" -sev Info + } + } } + if ($Settings.report -eq $true) { + # Extract expected connectors from templates + $ExpectedConnectors = foreach ($Template in $Templates) { + $TemplateParams = $Template.JSON | ConvertFrom-Json + [PSCustomObject]@{ + Identity = $TemplateParams.name + Type = $Template.direction + } + } + + # Get matching deployed connectors + $DeployedConnectors = foreach ($ExpectedConnector in $ExpectedConnectors) { + $ConnectorType = $ExpectedConnector.Type + $ExistingConnector = $ExistingConnectors.$("Get-$($ConnectorType)connector") | Where-Object -Property Identity -EQ $ExpectedConnector.Identity + if ($ExistingConnector) { + [PSCustomObject]@{ + Identity = $ExistingConnector.Identity + Type = $ConnectorType + } + } + } + $CurrentValue = [PSCustomObject]@{ + Connectors = @($DeployedConnectors) + } + $ExpectedValue = [PSCustomObject]@{ + Connectors = @($ExpectedConnectors) + } + + Set-CIPPStandardsCompareField -FieldName 'standards.ExchangeConnectorTemplates' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant + Add-CIPPBPAField -FieldName 'ExchangeConnectorTemplatesDeployed' -FieldValue ($DeployedConnectors.Identity) -StoreAs StringArray -Tenant $tenant + } } From 32566f4384f33b253a29feaf36385af9671d2ac7 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Wed, 14 Jan 2026 15:44:09 -0500 Subject: [PATCH 140/503] Replace $User with $Headers in Write-LogMessage calls Updated all Write-LogMessage invocations to use the $Headers variable instead of $User for logging API actions in New-CIPPCAPolicy.ps1. This change ensures consistent use of the correct headers parameter throughout the script. --- Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 | 22 ++++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 b/Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 index 515c4c4934d8..250b31296dd8 100644 --- a/Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 +++ b/Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 @@ -89,14 +89,14 @@ function New-CIPPCAPolicy { $UserIds = [System.Collections.Generic.List[string]]::new() $userNames | ForEach-Object { if (Test-IsGuid $_) { - Write-LogMessage -Headers $User -API 'Create CA Policy' -message "Already GUID, no need to replace: $_" -Sev 'Debug' + Write-LogMessage -Headers $Headers -API 'Create CA Policy' -message "Already GUID, no need to replace: $_" -Sev 'Debug' $UserIds.Add($_) # it's a GUID, so we keep it } else { $userId = ($users | Where-Object -Property displayName -EQ $_).id # it's a display name, so we get the user ID if ($userId) { foreach ($uid in $userId) { Write-Warning "Replaced user name $_ with ID $uid" - $null = Write-LogMessage -Headers $User -API 'Create CA Policy' -message "Replaced user name $_ with ID $uid" -Sev 'Debug' + $null = Write-LogMessage -Headers $Headers -API 'Create CA Policy' -message "Replaced user name $_ with ID $uid" -Sev 'Debug' $UserIds.Add($uid) # add the ID to the list } } else { @@ -135,7 +135,7 @@ function New-CIPPCAPolicy { $Body = ConvertTo-Json -InputObject $JSONobj.GrantControls.authenticationStrength $GraphRequest = New-GraphPOSTRequest -uri 'https://graph.microsoft.com/beta/identity/conditionalAccess/authenticationStrength/policies' -body $body -Type POST -tenantid $tenantfilter -asApp $true $JSONobj.GrantControls.authenticationStrength = @{ id = $ExistingStrength.id } - Write-LogMessage -Headers $User -API $APINAME -message "Created new Authentication Strength Policy: $($JSONobj.GrantControls.authenticationStrength.displayName)" -Sev 'Info' + Write-LogMessage -Headers $Headers -API $APINAME -message "Created new Authentication Strength Policy: $($JSONobj.GrantControls.authenticationStrength.displayName)" -Sev 'Info' } } @@ -177,16 +177,16 @@ function New-CIPPCAPolicy { if ($Overwrite) { $LocationUpdate = $location | Select-Object * -ExcludeProperty id Remove-ODataProperties -Object $LocationUpdate - $Body = ConvertTo-Json -InputObject $LocationUpdate -Depth 10 + $Body = ConvertTo-Json -InputObject $LocationUpdate -Depth 10 try { $null = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/identity/conditionalAccess/namedLocations/$($ExistingLocation.id)" -body $body -Type PATCH -tenantid $tenantfilter -asApp $true - Write-LogMessage -Tenant $TenantFilter -Headers $User -API $APINAME -message "Updated existing Named Location: $($location.displayName)" -Sev 'Info' + Write-LogMessage -Tenant $TenantFilter -Headers $Headers -API $APINAME -message "Updated existing Named Location: $($location.displayName)" -Sev 'Info' } catch { Write-Warning "Failed to update location $($location.displayName): $_" - Write-LogMessage -Tenant $TenantFilter -Headers $User -API $APINAME -message "Failed to update existing Named Location: $($location.displayName). Error: $_" -Sev 'Error' + Write-LogMessage -Tenant $TenantFilter -Headers $Headers -API $APINAME -message "Failed to update existing Named Location: $($location.displayName). Error: $_" -Sev 'Error' } } else { - Write-LogMessage -Tenant $TenantFilter -Headers $User -API $APINAME -message "Matched a CA policy with the existing Named Location: $($location.displayName)" -Sev 'Info' + Write-LogMessage -Tenant $TenantFilter -Headers $Headers -API $APINAME -message "Matched a CA policy with the existing Named Location: $($location.displayName)" -Sev 'Info' } [pscustomobject]@{ id = $ExistingLocation.id @@ -207,7 +207,7 @@ function New-CIPPCAPolicy { Start-Sleep -Seconds 2 $retryCount++ } while ((!$LocationRequest -or !$LocationRequest.id) -and ($retryCount -lt 5)) - Write-LogMessage -Tenant $TenantFilter -Headers $User -API $APINAME -message "Created new Named Location: $($location.displayName)" -Sev 'Info' + Write-LogMessage -Tenant $TenantFilter -Headers $Headers -API $APINAME -message "Created new Named Location: $($location.displayName)" -Sev 'Info' [pscustomobject]@{ id = $GraphRequest.id name = $GraphRequest.displayName @@ -319,7 +319,7 @@ function New-CIPPCAPolicy { $body = '{ "isEnabled": false }' try { $null = New-GraphPostRequest -tenantid $TenantFilter -Uri 'https://graph.microsoft.com/beta/policies/identitySecurityDefaultsEnforcementPolicy' -Type patch -Body $body -asApp $true -ContentType 'application/json' - Write-LogMessage -Headers $User -API 'Create CA Policy' -tenant $TenantFilter -message "Disabled Security Defaults for tenant $($TenantFilter)" -Sev 'Info' + Write-LogMessage -Headers $Headers -API 'Create CA Policy' -tenant $TenantFilter -message "Disabled Security Defaults for tenant $($TenantFilter)" -Sev 'Info' Start-Sleep 3 } catch { $ErrorMessage = Get-CippException -Exception $_ @@ -366,7 +366,7 @@ function New-CIPPCAPolicy { } Write-Information "overwriting $($CheckExisting.id)" $null = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/identity/conditionalAccess/policies/$($CheckExisting.id)" -tenantid $tenantfilter -type PATCH -body $RawJSON -asApp $true - Write-LogMessage -Headers $User -API 'Create CA Policy' -tenant $($Tenant) -message "Updated Conditional Access Policy $($JSONobj.Displayname) to the template standard." -Sev 'Info' + Write-LogMessage -Headers $Headers -API 'Create CA Policy' -tenant $($Tenant) -message "Updated Conditional Access Policy $($JSONobj.Displayname) to the template standard." -Sev 'Info' return "Updated policy $displayname for $tenantfilter" } } else { @@ -375,7 +375,7 @@ function New-CIPPCAPolicy { Start-Sleep 3 } $null = New-GraphPOSTRequest -uri 'https://graph.microsoft.com/beta/identity/conditionalAccess/policies' -tenantid $tenantfilter -type POST -body $RawJSON -asApp $true - Write-LogMessage -Headers $User -API 'Create CA Policy' -tenant $($Tenant) -message "Added Conditional Access Policy $($JSONobj.Displayname)" -Sev 'Info' + Write-LogMessage -Headers $Headers -API 'Create CA Policy' -tenant $($Tenant) -message "Added Conditional Access Policy $($JSONobj.Displayname)" -Sev 'Info' return "Created policy $displayname for $tenantfilter" } } catch { From 64f11a9f60fd80ab8dd770d7480fa56cba6294b3 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Wed, 14 Jan 2026 15:44:30 -0500 Subject: [PATCH 141/503] Refactor templateId to TemplateId property usage Replaces all instances of $Item.templateId with $Item.TemplateId for consistency and to match property naming conventions throughout Push-CIPPStandard.ps1. --- .../Standards/Push-CIPPStandard.ps1 | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Standards/Push-CIPPStandard.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Standards/Push-CIPPStandard.ps1 index 020ff3a469f9..996c224490d9 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Standards/Push-CIPPStandard.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Standards/Push-CIPPStandard.ps1 @@ -16,9 +16,9 @@ function Push-CIPPStandard { Write-Information "We'll be running $FunctionName" if ($Standard -in @('IntuneTemplate', 'ConditionalAccessTemplate')) { - $API = "$($Standard)_$($Item.templateId)_$($Item.Settings.TemplateList.value)" + $API = "$($Standard)_$($Item.TemplateId)_$($Item.Settings.TemplateList.value)" } else { - $API = "$($Standard)_$($Item.templateId)" + $API = "$($Standard)_$($Item.TemplateId)" } $Rerun = Test-CIPPRerun -Type Standard -Tenant $Tenant -API $API @@ -31,7 +31,7 @@ function Push-CIPPStandard { $StandardInfo = @{ Standard = $Standard - StandardTemplateId = $Item.templateId + StandardTemplateId = $Item.TemplateId } if ($Standard -eq 'IntuneTemplate') { $StandardInfo.IntuneTemplateId = $Item.Settings.TemplateList.value @@ -64,7 +64,7 @@ function Push-CIPPStandard { InvocationId = $invocationId Tenant = $Tenant Standard = $Standard - TemplateId = $Item.templateId + TemplateId = $Item.TemplateId API = $API FunctionName = $FunctionName } | ConvertTo-Json -Compress) @@ -83,7 +83,7 @@ function Push-CIPPStandard { $metadata = @{ Standard = $Standard Tenant = $Tenant - TemplateId = $Item.templateId + TemplateId = $Item.TemplateId FunctionName = $FunctionName TriggerType = 'Standard' } @@ -118,7 +118,7 @@ function Push-CIPPStandard { InvocationId = $invocationId Tenant = $Tenant Standard = $Standard - TemplateId = $Item.templateId + TemplateId = $Item.TemplateId API = $API FunctionName = $FunctionName Result = $result From ec7bdcde132f68b20a19be4095bcc3e4db18bd82 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Wed, 14 Jan 2026 23:18:10 +0100 Subject: [PATCH 142/503] Mark as compliant fixes for policies in tags --- .../Public/Functions/Get-CIPPTenantAlignment.ps1 | 4 ++-- Modules/CIPPCore/Public/Get-CIPPDrift.ps1 | 12 +++++++++++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/Modules/CIPPCore/Public/Functions/Get-CIPPTenantAlignment.ps1 b/Modules/CIPPCore/Public/Functions/Get-CIPPTenantAlignment.ps1 index 1f080836f9aa..a2a6553fb250 100644 --- a/Modules/CIPPCore/Public/Functions/Get-CIPPTenantAlignment.ps1 +++ b/Modules/CIPPCore/Public/Functions/Get-CIPPTenantAlignment.ps1 @@ -178,7 +178,7 @@ function Get-CIPPTenantAlignment { $IntuneActions = if ($IntuneTemplate.action) { $IntuneTemplate.action } else { @() } $IntuneReportingEnabled = ($IntuneActions | Where-Object { $_.value -and ($_.value.ToLower() -eq 'report' -or $_.value.ToLower() -eq 'remediate') }).Count -gt 0 $TagTemplate = $TagTemplates | Where-Object -Property package -EQ $Tag.value - $TagTemplates | ForEach-Object { + $TagTemplate | ForEach-Object { $TagStandardId = "standards.IntuneTemplate.$($_.GUID)" [PSCustomObject]@{ StandardId = $TagStandardId @@ -259,7 +259,7 @@ function Get-CIPPTenantAlignment { } } - $IsCompliant = ($Value -eq $true) + $IsCompliant = ($Value -eq $true) -or ($StandardObject.CurrentValue -and $StandardObject.CurrentValue -eq $StandardObject.ExpectedValue) $IsLicenseMissing = ($Value -is [string] -and $Value -like 'License Missing:*') $ComplianceStatus = if ($IsReportingDisabled) { diff --git a/Modules/CIPPCore/Public/Get-CIPPDrift.ps1 b/Modules/CIPPCore/Public/Get-CIPPDrift.ps1 index 17da95a2ad11..e4915dadb20c 100644 --- a/Modules/CIPPCore/Public/Get-CIPPDrift.ps1 +++ b/Modules/CIPPCore/Public/Get-CIPPDrift.ps1 @@ -232,7 +232,17 @@ function Get-CIPPDrift { if ($Alignment.standardSettings) { if ($Alignment.standardSettings.IntuneTemplate) { - $IntuneTemplateIds = $Alignment.standardSettings.IntuneTemplate.TemplateList | ForEach-Object { $_.value } + $IntuneTemplateIds = [System.Collections.Generic.List[string]]::new() + foreach ($Template in $Alignment.standardSettings.IntuneTemplate) { + if ($Template.TemplateList.value) { + $IntuneTemplateIds.Add($Template.TemplateList.value) + } + if ($Template.'TemplateList-Tags'.rawData.templates) { + foreach ($TagTemplate in $Template.'TemplateList-Tags'.rawData.templates) { + $IntuneTemplateIds.Add($TagTemplate.GUID) + } + } + } } if ($Alignment.standardSettings.ConditionalAccessTemplate) { $CATemplateIds = $Alignment.standardSettings.ConditionalAccessTemplate.TemplateList | ForEach-Object { $_.value } From ab8911e829e6150d05eea6896075b8973b7fbb0e Mon Sep 17 00:00:00 2001 From: John Duprey Date: Wed, 14 Jan 2026 18:25:40 -0500 Subject: [PATCH 143/503] Standardize reporting for CIPP standards modules Refactored multiple standards modules to use a consistent reporting format with CurrentValue and ExpectedValue objects in Set-CIPPStandardsCompareField. This improves clarity and uniformity in reporting compliance states across all standards. --- .../Invoke-CIPPStandardEXODirectSend.ps1 | 5 +- ...e-CIPPStandardEXODisableAutoForwarding.ps1 | 11 +- ...voke-CIPPStandardEXOOutboundSpamLimits.ps1 | 31 ++-- .../Invoke-CIPPStandardExcludedfileExt.ps1 | 18 ++- .../Invoke-CIPPStandardExternalMFATrusted.ps1 | 18 ++- .../Invoke-CIPPStandardFocusedInbox.ps1 | 16 +- ...ke-CIPPStandardFormsPhishingProtection.ps1 | 11 +- ...PStandardGlobalQuarantineNotifications.ps1 | 18 ++- .../Invoke-CIPPStandardGroupTemplate.ps1 | 14 +- .../Invoke-CIPPStandardGuestInvite.ps1 | 15 +- ...e-CIPPStandardIntuneComplianceSettings.ps1 | 17 ++- .../Invoke-CIPPStandardIntuneTemplate.ps1 | 96 ++++++------ ...ke-CIPPStandardLegacyEmailReportAddins.ps1 | 55 ++++--- ...tandardMDMEnrollmentDuringRegistration.ps1 | 9 +- .../Standards/Invoke-CIPPStandardMDMScope.ps1 | 30 ++-- .../Invoke-CIPPStandardMailContacts.ps1 | 14 +- ...oke-CIPPStandardMailboxRecipientLimits.ps1 | 16 +- ...Invoke-CIPPStandardMalwareFilterPolicy.ps1 | 18 ++- .../Invoke-CIPPStandardMessageExpiration.ps1 | 13 +- .../Standards/Invoke-CIPPStandardNudgeMFA.ps1 | 12 +- ...-CIPPStandardOWAAttachmentRestrictions.ps1 | 25 ++-- .../Invoke-CIPPStandardOauthConsent.ps1 | 15 +- .../Invoke-CIPPStandardOauthConsentLowSec.ps1 | 33 ++-- .../Invoke-CIPPStandardOutBoundSpamAlert.ps1 | 16 +- ...CIPPStandardPWcompanionAppAllowedState.ps1 | 8 +- ...rdPWdisplayAppInformationRequiredState.ps1 | 18 ++- ...oke-CIPPStandardPasswordExpireDisabled.ps1 | 15 +- .../Invoke-CIPPStandardPerUserMFA.ps1 | 14 +- .../Invoke-CIPPStandardPhishProtection.ps1 | 9 +- ...-CIPPStandardPhishSimSpoofIntelligence.ps1 | 68 +++++---- ...Invoke-CIPPStandardPhishingSimulations.ps1 | 130 ++++++++-------- .../Invoke-CIPPStandardProfilePhotos.ps1 | 13 +- ...oke-CIPPStandardQuarantineRequestAlert.ps1 | 18 +-- .../Invoke-CIPPStandardQuarantineTemplate.ps1 | 141 ++++++++++-------- ...ndardRestrictThirdPartyStorageServices.ps1 | 19 +-- .../Invoke-CIPPStandardRetentionPolicyTag.ps1 | 29 ++-- .../Invoke-CIPPStandardRotateDKIM.ps1 | 14 +- .../Invoke-CIPPStandardSPAzureB2B.ps1 | 19 +-- .../Invoke-CIPPStandardSPDirectSharing.ps1 | 22 +-- ...e-CIPPStandardSPDisableLegacyWorkflows.ps1 | 23 +-- ...ke-CIPPStandardSPDisallowInfectedFiles.ps1 | 11 +- .../Invoke-CIPPStandardSPEmailAttestation.ps1 | 21 +-- ...e-CIPPStandardSPExternalUserExpiration.ps1 | 17 ++- .../Invoke-CIPPStandardSPFileRequests.ps1 | 40 ++--- .../Invoke-CIPPStandardSPSyncButtonState.ps1 | 13 +- ...nvoke-CIPPStandardSafeAttachmentPolicy.ps1 | 34 +++-- .../Invoke-CIPPStandardSafeLinksPolicy.ps1 | 41 +++-- ...ke-CIPPStandardSafeLinksTemplatePolicy.ps1 | 132 ++++++++-------- .../Invoke-CIPPStandardSafeSendersDisable.ps1 | 8 +- ...oke-CIPPStandardSecureScoreRemediation.ps1 | 9 +- .../Invoke-CIPPStandardSecurityDefaults.ps1 | 11 +- .../Invoke-CIPPStandardSendFromAlias.ps1 | 11 +- ...oke-CIPPStandardSendReceiveLimitTenant.ps1 | 13 +- ...IPPStandardSharePointMassDeletionAlert.ps1 | 20 ++- .../Invoke-CIPPStandardShortenMeetings.ps1 | 20 ++- .../Invoke-CIPPStandardSpamFilterPolicy.ps1 | 67 +++++++-- .../Invoke-CIPPStandardSpoofWarn.ps1 | 15 +- .../Invoke-CIPPStandardStaleEntraDevices.ps1 | 15 +- .../Standards/Invoke-CIPPStandardTAP.ps1 | 17 ++- ...Invoke-CIPPStandardTeamsChatProtection.ps1 | 13 +- ...voke-CIPPStandardTeamsEmailIntegration.ps1 | 21 ++- .../Invoke-CIPPStandardTeamsEnrollUser.ps1 | 18 +-- ...-CIPPStandardTeamsExternalAccessPolicy.ps1 | 22 +-- ...IPPStandardTeamsExternalChatWithAnyone.ps1 | 11 +- ...e-CIPPStandardTeamsExternalFileSharing.ps1 | 27 ++-- ...PPStandardTeamsFederationConfiguration.ps1 | 19 ++- ...e-CIPPStandardTeamsGlobalMeetingPolicy.ps1 | 25 +++- .../Invoke-CIPPStandardTeamsGuestAccess.ps1 | 18 +-- ...tandardTeamsMeetingRecordingExpiration.ps1 | 15 +- ...e-CIPPStandardTeamsMeetingVerification.ps1 | 21 ++- ...oke-CIPPStandardTeamsMeetingsByDefault.ps1 | 16 +- ...nvoke-CIPPStandardTeamsMessagingPolicy.ps1 | 53 ++++--- ...voke-CIPPStandardTenantDefaultTimezone.ps1 | 17 +-- ...voke-CIPPStandardTransportRuleTemplate.ps1 | 15 +- ...ke-CIPPStandardTwoClickEmailProtection.ps1 | 13 +- .../Invoke-CIPPStandardUndoOauth.ps1 | 31 ++-- ...voke-CIPPStandardUserPreferredLanguage.ps1 | 18 ++- .../Invoke-CIPPStandardUserSubmissions.ps1 | 49 ++++-- ...voke-CIPPStandardintuneBrandingProfile.ps1 | 31 +++- .../Invoke-CIPPStandardintuneDeviceReg.ps1 | 15 +- ...CIPPStandardintuneDeviceRetirementDays.ps1 | 13 +- .../Invoke-CIPPStandardintuneRequireMFA.ps1 | 10 +- .../Standards/Invoke-CIPPStandardlaps.ps1 | 1 - .../Invoke-CIPPStandardsharingCapability.ps1 | 16 +- ...e-CIPPStandardsharingDomainRestriction.ps1 | 20 ++- .../Invoke-CIPPStandardunmanagedSync.ps1 | 16 +- 86 files changed, 1289 insertions(+), 856 deletions(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEXODirectSend.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEXODirectSend.ps1 index 3cf07e245b89..81bfe45e8006 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEXODirectSend.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEXODirectSend.ps1 @@ -32,7 +32,6 @@ function Invoke-CIPPStandardEXODirectSend { param ($Tenant, $Settings) - # Determine desired state. These double negative MS loves are a bit confusing $DesiredStateName = $Settings.state.value ?? $Settings.state # Input validation @@ -87,10 +86,10 @@ function Invoke-CIPPStandardEXODirectSend { if ($Settings.report -eq $true) { $ExpectedState = @{ RejectDirectSend = $DesiredState - } | ConvertTo-Json -Depth 10 -Compress + } $CurrentState = @{ RejectDirectSend = $CurrentConfig - } | ConvertTo-Json -Depth 10 -Compress + } Set-CIPPStandardsCompareField -FieldName 'standards.EXODirectSend' -CurrentValue $CurrentState -ExpectedValue $ExpectedState -Tenant $Tenant Add-CIPPBPAField -FieldName 'EXODirectSend' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $Tenant } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEXODisableAutoForwarding.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEXODisableAutoForwarding.ps1 index 9b51a323a138..f39c7de785f5 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEXODisableAutoForwarding.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEXODisableAutoForwarding.ps1 @@ -43,7 +43,6 @@ function Invoke-CIPPStandardEXODisableAutoForwarding { Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. - ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'EXODisableAutoForwarding' try { $CurrentInfo = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-HostedOutboundSpamFilterPolicy' -cmdParams @{Identity = 'Default' } -useSystemMailbox $true @@ -75,8 +74,14 @@ function Invoke-CIPPStandardEXODisableAutoForwarding { } if ($Settings.report -eq $true) { - $state = $StateIsCorrect ?? ($CurrentInfo | Select-Object AutoForwardingMode) - Set-CIPPStandardsCompareField -FieldName 'standards.EXODisableAutoForwarding' -FieldValue $state -TenantFilter $Tenant + $CurrentValue = @{ + AutoForwardingMode = $CurrentInfo.AutoForwardingMode + } + $ExpectedValue = @{ + AutoForwardingMode = 'Off' + } + + Set-CIPPStandardsCompareField -FieldName 'standards.EXODisableAutoForwarding' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant Add-CIPPBPAField -FieldName 'AutoForwardingDisabled' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEXOOutboundSpamLimits.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEXOOutboundSpamLimits.ps1 index 21093d8428ea..537bb0133407 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEXOOutboundSpamLimits.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEXOOutboundSpamLimits.ps1 @@ -7,7 +7,7 @@ function Invoke-CIPPStandardEXOOutboundSpamLimits { .SYNOPSIS (Label) Set Exchange Outbound Spam Limits .DESCRIPTION - (Helptext) Configures the outbound spam recipient limits (external per hour, internal per hour, per day) and the action to take when a limit is reached. The 'Set Outbound Spam Alert e-mail' standard is recommended to configure together with this one. + (Helptext) Configures the outbound spam recipient limits (external per hour, internal per hour, per day) and the action to take when a limit is reached. The 'Set Outbound Spam Alert e-mail' standard is recommended to configure together with this one. (DocsDescription) Configures the Exchange Online outbound spam recipient limits for external per hour, internal per hour, and per day, along with the action to take (e.g., BlockUser, Alert) when these limits are exceeded. This helps prevent abuse and manage email flow. Microsoft's recommendations can be found [here.](https://learn.microsoft.com/en-us/defender-office-365/recommended-settings-for-eop-and-office365#eop-outbound-spam-policy-settings) The 'Set Outbound Spam Alert e-mail' standard is recommended to configure together with this one. .NOTES CAT @@ -72,9 +72,8 @@ function Invoke-CIPPStandardEXOOutboundSpamLimits { # Get current settings try { $CurrentInfo = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-HostedOutboundSpamFilterPolicy' -cmdParams @{Identity = 'Default' } -Select 'RecipientLimitExternalPerHour, RecipientLimitInternalPerHour, RecipientLimitPerDay, ActionWhenThresholdReached' -useSystemMailbox $true | - Select-Object -ExcludeProperty *data.type* - } - catch { + Select-Object -ExcludeProperty *data.type* + } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the EXOOutboundSpamLimits state for $Tenant. Error: $ErrorMessage" -Sev Error return @@ -82,12 +81,12 @@ function Invoke-CIPPStandardEXOOutboundSpamLimits { # Check if settings are already correct $StateIsCorrect = ($CurrentInfo.RecipientLimitExternalPerHour -eq $Settings.RecipientLimitExternalPerHour) -and - ($CurrentInfo.RecipientLimitInternalPerHour -eq $Settings.RecipientLimitInternalPerHour) -and - ($CurrentInfo.RecipientLimitPerDay -eq $Settings.RecipientLimitPerDay) -and - ($CurrentInfo.ActionWhenThresholdReached -eq $ActionWhenThresholdReached) + ($CurrentInfo.RecipientLimitInternalPerHour -eq $Settings.RecipientLimitInternalPerHour) -and + ($CurrentInfo.RecipientLimitPerDay -eq $Settings.RecipientLimitPerDay) -and + ($CurrentInfo.ActionWhenThresholdReached -eq $ActionWhenThresholdReached) # Remediation - If ($Settings.remediate -eq $true) { + if ($Settings.remediate -eq $true) { Write-Host 'Time to remediate' if ($StateIsCorrect -eq $false) { try { @@ -121,8 +120,20 @@ function Invoke-CIPPStandardEXOOutboundSpamLimits { # Report if ($Settings.report -eq $true) { - $State = $StateIsCorrect ? $true : $CurrentInfo - Set-CIPPStandardsCompareField -FieldName 'standards.EXOOutboundSpamLimits' -FieldValue $State -TenantFilter $Tenant + $CurrentValue = @{ + RecipientLimitExternalPerHour = $CurrentInfo.RecipientLimitExternalPerHour + RecipientLimitInternalPerHour = $CurrentInfo.RecipientLimitInternalPerHour + RecipientLimitPerDay = $CurrentInfo.RecipientLimitPerDay + ActionWhenThresholdReached = $CurrentInfo.ActionWhenThresholdReached + } + $ExpectedValue = @{ + RecipientLimitExternalPerHour = [Int32]$Settings.RecipientLimitExternalPerHour + RecipientLimitInternalPerHour = [Int32]$Settings.RecipientLimitInternalPerHour + RecipientLimitPerDay = [Int32]$Settings.RecipientLimitPerDay + ActionWhenThresholdReached = $ActionWhenThresholdReached + } + + Set-CIPPStandardsCompareField -FieldName 'standards.EXOOutboundSpamLimits' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant Add-CIPPBPAField -FieldName 'OutboundSpamLimitsConfigured' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $Tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardExcludedfileExt.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardExcludedfileExt.ps1 index 9924dc9cdf7b..8271da894f31 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardExcludedfileExt.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardExcludedfileExt.ps1 @@ -31,8 +31,7 @@ function Invoke-CIPPStandardExcludedfileExt { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'ExcludedfileExt' -TenantFilter $Tenant -RequiredCapabilities @('SHAREPOINTWAC', 'SHAREPOINTSTANDARD', 'SHAREPOINTENTERPRISE', 'SHAREPOINTENTERPRISE_EDU','ONEDRIVE_BASIC', 'ONEDRIVE_ENTERPRISE') - ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'ExcludedfileExt' + $TestResult = Test-CIPPStandardLicense -StandardName 'ExcludedfileExt' -TenantFilter $Tenant -RequiredCapabilities @('SHAREPOINTWAC', 'SHAREPOINTSTANDARD', 'SHAREPOINTENTERPRISE', 'SHAREPOINTENTERPRISE_EDU', 'ONEDRIVE_BASIC', 'ONEDRIVE_ENTERPRISE') if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." @@ -41,8 +40,7 @@ function Invoke-CIPPStandardExcludedfileExt { try { $CurrentInfo = New-GraphGetRequest -Uri 'https://graph.microsoft.com/beta/admin/sharepoint/settings' -tenantid $Tenant -AsApp $true - } - catch { + } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the ExcludedfileExt state for $Tenant. Error: $ErrorMessage" -Sev Error return @@ -61,7 +59,7 @@ function Invoke-CIPPStandardExcludedfileExt { Write-Host "MissingExclusions: $($MissingExclusions)" - If ($Settings.remediate -eq $true) { + if ($Settings.remediate -eq $true) { # If the number of extensions in the settings does not match the number of extensions in the current settings, we need to update the settings $MissingExclusions = if ($Exts.Count -ne $CurrentInfo.excludedFileExtensionsForSyncApp.Count) { $true } else { $MissingExclusions } @@ -92,8 +90,14 @@ function Invoke-CIPPStandardExcludedfileExt { } if ($Settings.report -eq $true) { - $state = $MissingExclusions ? (@{ ext = $CurrentInfo.excludedFileExtensionsForSyncApp -join ',' }): $true - Set-CIPPStandardsCompareField -FieldName 'standards.ExcludedfileExt' -FieldValue $state -Tenant $tenant + $CurrentValue = [PSCustomObject]@{ + Extensions = @($CurrentInfo.excludedFileExtensionsForSyncApp) + } + $ExpectedValue = [PSCustomObject]@{ + Extensions = @($Exts) + } + + Set-CIPPStandardsCompareField -FieldName 'standards.ExcludedfileExt' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant Add-CIPPBPAField -FieldName 'ExcludedfileExt' -FieldValue $CurrentInfo.excludedFileExtensionsForSyncApp -StoreAs json -Tenant $tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardExternalMFATrusted.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardExternalMFATrusted.ps1 index 719d1be1357a..f4d03b8edaeb 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardExternalMFATrusted.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardExternalMFATrusted.ps1 @@ -31,12 +31,10 @@ function Invoke-CIPPStandardExternalMFATrusted { #> param($Tenant, $Settings) - ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'ExternalMFATrusted' try { $ExternalMFATrusted = (New-GraphGetRequest -uri 'https://graph.microsoft.com/v1.0/policies/crossTenantAccessPolicy/default?$select=inboundTrust' -tenantid $Tenant) - } - catch { + } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the ExternalMFATrusted state for $Tenant. Error: $ErrorMessage" -Sev Error return @@ -47,8 +45,6 @@ function Invoke-CIPPStandardExternalMFATrusted { $WantedState = if ($state -eq 'true') { $true } else { $false } $StateMessage = if ($WantedState) { 'enabled' } else { 'disabled' } - - # Input validation if (([string]::IsNullOrWhiteSpace($state) -or $state -eq 'Select a value') -and ($Settings.remediate -eq $true -or $Settings.alert -eq $true)) { Write-LogMessage -API 'Standards' -tenant $Tenant -message 'ExternalMFATrusted: Invalid state parameter set' -sev Error @@ -73,10 +69,16 @@ function Invoke-CIPPStandardExternalMFATrusted { } } } + if ($Settings.report -eq $true) { - $state = $ExternalMFATrusted.inboundTrust.isMfaAccepted ? $true : $ExternalMFATrusted.inboundTrust - $ReportState = $ExternalMFATrusted.inboundTrust.isMfaAccepted -eq $WantedState - Set-CIPPStandardsCompareField -FieldName 'standards.ExternalMFATrusted' -FieldValue $ReportState -TenantFilter $Tenant + $CurrentValue = @{ + isMfaAccepted = $ExternalMFATrusted.inboundTrust.isMfaAccepted + } + $ExpectedValue = @{ + isMfaAccepted = $WantedState + } + + Set-CIPPStandardsCompareField -FieldName 'standards.ExternalMFATrusted' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant Add-CIPPBPAField -FieldName 'ExternalMFATrusted' -FieldValue $ExternalMFATrusted.inboundTrust.isMfaAccepted -StoreAs bool -Tenant $Tenant } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardFocusedInbox.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardFocusedInbox.ps1 index 4d668dbd3854..9c60877dd511 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardFocusedInbox.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardFocusedInbox.ps1 @@ -37,7 +37,6 @@ function Invoke-CIPPStandardFocusedInbox { Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. - ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'FocusedInbox' # Get state value using null-coalescing operator $state = $Settings.state.value ?? $Settings.state @@ -45,13 +44,12 @@ function Invoke-CIPPStandardFocusedInbox { # Input validation if ([string]::IsNullOrWhiteSpace($state) -or $state -eq 'Select a value') { Write-LogMessage -API 'Standards' -tenant $tenant -message 'ExternalMFATrusted: Invalid state parameter set' -sev Error - Return + return } try { $CurrentState = (New-ExoRequest -tenantid $Tenant -cmdlet 'Get-OrganizationConfig').FocusedInboxOn - } - catch { + } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the FocusedInbox state for $Tenant. Error: $ErrorMessage" -Sev Error return @@ -77,7 +75,6 @@ function Invoke-CIPPStandardFocusedInbox { } if ($Settings.alert -eq $true) { - if ($StateIsCorrect -eq $true) { Write-LogMessage -API 'Standards' -tenant $Tenant -message "Focused Inbox is set to $state." -sev Info } else { @@ -87,8 +84,13 @@ function Invoke-CIPPStandardFocusedInbox { } if ($Settings.report -eq $true) { - - Set-CIPPStandardsCompareField -FieldName 'standards.FocusedInbox' -FieldValue $StateIsCorrect -TenantFilter $Tenant + $CurrentValue = @{ + FocusedInboxOn = $CurrentState + } + $ExpectedValue = @{ + FocusedInboxOn = $WantedState + } + Set-CIPPStandardsCompareField -FieldName 'standards.FocusedInbox' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant Add-CIPPBPAField -FieldName 'FocusedInboxCorrectState' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $Tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardFormsPhishingProtection.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardFormsPhishingProtection.ps1 index 8c4598b93b57..957c2b2d7059 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardFormsPhishingProtection.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardFormsPhishingProtection.ps1 @@ -44,7 +44,7 @@ function Invoke-CIPPStandardFormsPhishingProtection { } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Standards' -tenant $Tenant -message "Could not get current Forms settings. Error: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Return + return } if ($Settings.remediate -eq $true) { @@ -82,7 +82,14 @@ function Invoke-CIPPStandardFormsPhishingProtection { } if ($Settings.report -eq $true) { - Set-CIPPStandardsCompareField -FieldName 'standards.FormsPhishingProtection' -FieldValue $CurrentState -Tenant $Tenant + $CurrentValue = @{ + isInOrgFormsPhishingScanEnabled = $CurrentState + } + $ExpectedValue = @{ + isInOrgFormsPhishingScanEnabled = $true + } + + Set-CIPPStandardsCompareField -FieldName 'standards.FormsPhishingProtection' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant Add-CIPPBPAField -FieldName 'FormsPhishingProtection' -FieldValue $CurrentState -StoreAs bool -Tenant $Tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardGlobalQuarantineNotifications.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardGlobalQuarantineNotifications.ps1 index 1eff1e59ac2f..e7a3b9f246ea 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardGlobalQuarantineNotifications.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardGlobalQuarantineNotifications.ps1 @@ -27,9 +27,7 @@ function Invoke-CIPPStandardGlobalQuarantineNotifications { .LINK https://docs.cipp.app/user-documentation/tenant/standards/list-standards #> - param ($Tenant, $Settings) - ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'GlobalQuarantineNotifications' $TestResult = Test-CIPPStandardLicense -StandardName 'GlobalQuarantineNotifications' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { @@ -39,9 +37,8 @@ function Invoke-CIPPStandardGlobalQuarantineNotifications { try { $CurrentState = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-QuarantinePolicy' -cmdParams @{ QuarantinePolicyType = 'GlobalQuarantinePolicy' } | - Select-Object -ExcludeProperty '*data.type' - } - catch { + Select-Object -ExcludeProperty '*data.type' + } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the GlobalQuarantineNotifications state for $Tenant. Error: $ErrorMessage" -Sev Error return @@ -102,8 +99,15 @@ function Invoke-CIPPStandardGlobalQuarantineNotifications { if ($Settings.report -eq $true) { $notificationInterval = @{ NotificationInterval = "$(($CurrentState.EndUserSpamNotificationFrequency).TotalHours) hours" } - $ReportState = $CurrentState.EndUserSpamNotificationFrequency -eq $WantedState ? $true : $notificationInterval - Set-CIPPStandardsCompareField -FieldName 'standards.GlobalQuarantineNotifications' -FieldValue $ReportState -Tenant $Tenant + + $CurrentValue = @{ + EndUserSpamNotificationFrequency = $CurrentState.EndUserSpamNotificationFrequency + } + $ExpectedValue = @{ + EndUserSpamNotificationFrequency = $WantedState + } + + Set-CIPPStandardsCompareField -FieldName 'standards.GlobalQuarantineNotifications' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant Add-CIPPBPAField -FieldName 'GlobalQuarantineNotificationsSet' -FieldValue [string]$CurrentState.EndUserSpamNotificationFrequency -StoreAs string -Tenant $Tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardGroupTemplate.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardGroupTemplate.ps1 index dae60252fc37..0024b7d7a5ee 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardGroupTemplate.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardGroupTemplate.ps1 @@ -31,7 +31,6 @@ function Invoke-CIPPStandardGroupTemplate { #> param($Tenant, $Settings) - ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'GroupTemplate' $existingGroups = New-GraphGETRequest -uri 'https://graph.microsoft.com/beta/groups?$top=999' -tenantid $tenant $TestResult = Test-CIPPStandardLicense -StandardName 'GroupTemplate' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') -SkipLog @@ -242,12 +241,15 @@ function Invoke-CIPPStandardGroupTemplate { } } - if ($MissingGroups.Count -eq 0) { - $fieldValue = $true - } else { - $fieldValue = $MissingGroups -join ', ' + $CurrentValue = @{ + ExistingGroups = $existingGroups.displayName + MissingGroups = @($MissingGroups) + } + $ExpectedValue = @{ + ExistingGroups = $GroupTemplates.displayName + MissingGroups = @() } - Set-CIPPStandardsCompareField -FieldName 'standards.GroupTemplate' -FieldValue $fieldValue -Tenant $Tenant + Set-CIPPStandardsCompareField -FieldName 'standards.GroupTemplate' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardGuestInvite.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardGuestInvite.ps1 index 9a6762f5723a..30891463c9ae 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardGuestInvite.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardGuestInvite.ps1 @@ -37,8 +37,7 @@ function Invoke-CIPPStandardGuestInvite { try { $CurrentState = New-GraphGetRequest -Uri 'https://graph.microsoft.com/beta/policies/authorizationPolicy/authorizationPolicy' -tenantid $Tenant - } - catch { + } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the GuestInvite state for $Tenant. Error: $ErrorMessage" -Sev Error return @@ -48,7 +47,7 @@ function Invoke-CIPPStandardGuestInvite { $AllowInvitesFromValue = $Settings.allowInvitesFrom.value ?? $Settings.allowInvitesFrom if (([string]::IsNullOrWhiteSpace($AllowInvitesFromValue) -or $AllowInvitesFromValue -eq 'Select a value') -and ($Settings.remediate -eq $true -or $Settings.alert -eq $true)) { Write-LogMessage -API 'Standards' -tenant $tenant -message 'GuestInvite: Invalid allowInvitesFrom parameter set' -sev Error - Return + return } $StateIsCorrect = ($CurrentState.allowInvitesFrom -eq $AllowInvitesFromValue) @@ -87,8 +86,14 @@ function Invoke-CIPPStandardGuestInvite { } if ($Settings.report -eq $true) { - $state = $StateIsCorrect ? $true : ($CurrentState | Select-Object allowInvitesFrom) - Set-CIPPStandardsCompareField -FieldName 'standards.GuestInvite' -FieldValue $state -TenantFilter $Tenant + $CurrentValue = @{ + allowInvitesFrom = $CurrentState.allowInvitesFrom + } + $ExpectedValue = @{ + allowInvitesFrom = $AllowInvitesFromValue + } + + Set-CIPPStandardsCompareField -FieldName 'standards.GuestInvite' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant Add-CIPPBPAField -FieldName 'GuestInvite' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardIntuneComplianceSettings.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardIntuneComplianceSettings.ps1 index e306641d550d..06ab67ebb055 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardIntuneComplianceSettings.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardIntuneComplianceSettings.ps1 @@ -41,9 +41,8 @@ function Invoke-CIPPStandardIntuneComplianceSettings { try { $CurrentState = New-GraphGetRequest -Uri 'https://graph.microsoft.com/beta/deviceManagement/settings' -tenantid $Tenant | - Select-Object secureByDefault, deviceComplianceCheckinThresholdDays - } - catch { + Select-Object secureByDefault, deviceComplianceCheckinThresholdDays + } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the intuneDeviceReg state for $Tenant. Error: $ErrorMessage" -Sev Error return @@ -93,8 +92,16 @@ function Invoke-CIPPStandardIntuneComplianceSettings { } if ($Settings.report -eq $true) { - $state = $StateIsCorrect ? $true : $CurrentState - Set-CIPPStandardsCompareField -FieldName 'standards.IntuneComplianceSettings' -FieldValue $state -Tenant $Tenant + $CurrentValue = @{ + secureByDefault = $CurrentState.secureByDefault + deviceComplianceCheckinThresholdDays = $CurrentState.deviceComplianceCheckinThresholdDays + } + $ExpectedValue = @{ + secureByDefault = $SecureByDefault + deviceComplianceCheckinThresholdDays = $DeviceComplianceCheckinThresholdDays + } + + Set-CIPPStandardsCompareField -FieldName 'standards.IntuneComplianceSettings' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant Add-CIPPBPAField -FieldName 'IntuneComplianceSettings' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $Tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardIntuneTemplate.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardIntuneTemplate.ps1 index 4ba8ebbc353d..f3d7ccca4a0c 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardIntuneTemplate.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardIntuneTemplate.ps1 @@ -37,7 +37,6 @@ function Invoke-CIPPStandardIntuneTemplate { #> param($Tenant, $Settings) $TestResult = Test-CIPPStandardLicense -StandardName 'IntuneTemplate_general' -TenantFilter $Tenant -RequiredCapabilities @('INTUNE_A', 'MDM_Services', 'EMS', 'SCCM', 'MICROSOFTINTUNEPLAN1') - ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'intuneTemplate' if ($TestResult -eq $false) { #writing to each item that the license is not present. @@ -94,42 +93,42 @@ function Invoke-CIPPStandardIntuneTemplate { if ($Compare) { Write-Host "IntuneTemplate: $($Template.TemplateList.value) - Compare found differences." [PSCustomObject]@{ - MatchFailed = $true - displayname = $displayname - description = $description - compare = $Compare - rawJSON = $RawJSON - body = $Request.body - assignTo = $Template.AssignTo - excludeGroup = $Template.excludeGroup - remediate = $Template.remediate - alert = $Template.alert - report = $Template.report - existingPolicyId = $ExistingPolicy.id - templateId = $Template.TemplateList.value - customGroup = $Template.customGroup - assignmentFilter = $Template.assignmentFilter - assignmentFilterType = $Template.assignmentFilterType + MatchFailed = $true + displayname = $displayname + description = $description + compare = $Compare + rawJSON = $RawJSON + body = $Request.body + assignTo = $Template.AssignTo + excludeGroup = $Template.excludeGroup + remediate = $Template.remediate + alert = $Template.alert + report = $Template.report + existingPolicyId = $ExistingPolicy.id + templateId = $Template.TemplateList.value + customGroup = $Template.customGroup + assignmentFilter = $Template.assignmentFilter + assignmentFilterType = $Template.assignmentFilterType } } else { Write-Host "IntuneTemplate: $($Template.TemplateList.value) - No differences found." [PSCustomObject]@{ - MatchFailed = $false - displayname = $displayname - description = $description - compare = $false - rawJSON = $RawJSON - body = $Request.body - assignTo = $Template.AssignTo - excludeGroup = $Template.excludeGroup - remediate = $Template.remediate - alert = $Template.alert - report = $Template.report - existingPolicyId = $ExistingPolicy.id - templateId = $Template.TemplateList.value - customGroup = $Template.customGroup - assignmentFilter = $Template.assignmentFilter - assignmentFilterType = $Template.assignmentFilterType + MatchFailed = $false + displayname = $displayname + description = $description + compare = $false + rawJSON = $RawJSON + body = $Request.body + assignTo = $Template.AssignTo + excludeGroup = $Template.excludeGroup + remediate = $Template.remediate + alert = $Template.alert + report = $Template.report + existingPolicyId = $ExistingPolicy.id + templateId = $Template.TemplateList.value + customGroup = $Template.customGroup + assignmentFilter = $Template.assignmentFilter + assignmentFilterType = $Template.assignmentFilterType } } } @@ -140,15 +139,15 @@ function Invoke-CIPPStandardIntuneTemplate { Write-Host "working on template deploy: $($TemplateFile.displayname)" try { $TemplateFile.customGroup ? ($TemplateFile.AssignTo = $TemplateFile.customGroup) : $null - + $PolicyParams = @{ - TemplateType = $TemplateFile.body.Type - Description = $TemplateFile.description - DisplayName = $TemplateFile.displayname - RawJSON = $templateFile.rawJSON - AssignTo = $TemplateFile.AssignTo - ExcludeGroup = $TemplateFile.excludeGroup - tenantFilter = $Tenant + TemplateType = $TemplateFile.body.Type + Description = $TemplateFile.description + DisplayName = $TemplateFile.displayname + RawJSON = $templateFile.rawJSON + AssignTo = $TemplateFile.AssignTo + ExcludeGroup = $TemplateFile.excludeGroup + tenantFilter = $Tenant } # Add assignment filter if specified @@ -188,9 +187,18 @@ function Invoke-CIPPStandardIntuneTemplate { foreach ($Template in $CompareList | Where-Object { $_.report -eq $true -or $_.remediate -eq $true }) { Write-Host "working on template report: $($Template.displayname)" $id = $Template.templateId - $CompareObj = $Template.compare - $state = $CompareObj ? $CompareObj : $true - Set-CIPPStandardsCompareField -FieldName "standards.IntuneTemplate.$id" -FieldValue $state -TenantFilter $Tenant + + $CurrentValue = @{ + displayName = $Template.displayname + description = $Template.description + isCompliant = if ($Template.compare) { $false } else { $true } + } + $ExpectedValue = @{ + displayName = $Template.displayname + description = $Template.description + isCompliant = $true + } + Set-CIPPStandardsCompareField -FieldName "standards.IntuneTemplate.$id" -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant } #Add-CIPPBPAField -FieldName "policy-$id" -FieldValue $Compare -StoreAs bool -Tenant $tenant } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardLegacyEmailReportAddins.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardLegacyEmailReportAddins.ps1 index 2cca54b4929f..899677c80a05 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardLegacyEmailReportAddins.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardLegacyEmailReportAddins.ps1 @@ -34,22 +34,21 @@ function Invoke-CIPPStandardLegacyEmailReportAddins { # Define the legacy add-ins to remove $LegacyAddins = @( @{ - AssetId = 'WA200002469' + AssetId = 'WA200002469' ProductId = '3f32746a-0586-4c54-b8ce-d3b611c5b6c8' - Name = 'Report Phishing' + Name = 'Report Phishing' }, @{ - AssetId = 'WA104381180' + AssetId = 'WA104381180' ProductId = '6046742c-3aee-485e-a4ac-92ab7199db2e' - Name = 'Report Message' + Name = 'Report Message' } ) try { $CurrentApps = New-GraphGetRequest -scope 'https://admin.microsoft.com/.default' -TenantID $Tenant -Uri 'https://admin.microsoft.com/fd/addins/api/apps?workloads=AzureActiveDirectory,WXPO,MetaOS,Teams,SharePoint' $InstalledApps = $CurrentApps.apps - } - catch { + } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the installed add-ins for $Tenant. Error: $ErrorMessage" -Sev Error return @@ -64,11 +63,11 @@ function Invoke-CIPPStandardLegacyEmailReportAddins { if ($InstalledAddin) { $InstalledLegacyAddins.Add($LegacyAddin.Name) $AddinsToRemove.Add([PSCustomObject]@{ - AppsourceAssetID = $LegacyAddin.AssetId - ProductID = $LegacyAddin.ProductId - Command = 'UNDEPLOY' - Workload = 'WXPO' - }) + AppsourceAssetID = $LegacyAddin.AssetId + ProductID = $LegacyAddin.ProductId + Command = 'UNDEPLOY' + Workload = 'WXPO' + }) } } @@ -82,18 +81,18 @@ function Invoke-CIPPStandardLegacyEmailReportAddins { foreach ($AddinToRemove in $AddinsToRemove) { try { $Body = @{ - Locale = 'en-US' + Locale = 'en-US' WorkloadManagementList = @($AddinToRemove) } | ConvertTo-Json -Depth 10 -Compress $GraphRequest = @{ - tenantID = $Tenant - uri = 'https://admin.microsoft.com/fd/addins/api/apps' - scope = 'https://admin.microsoft.com/.default' - AsApp = $false - Type = 'POST' + tenantID = $Tenant + uri = 'https://admin.microsoft.com/fd/addins/api/apps' + scope = 'https://admin.microsoft.com/.default' + AsApp = $false + Type = 'POST' ContentType = 'application/json; charset=utf-8' - Body = $Body + Body = $Body } $Response = New-GraphPostRequest @GraphRequest @@ -126,8 +125,7 @@ function Invoke-CIPPStandardLegacyEmailReportAddins { # Use fresh state for reporting/alerting $StateIsCorrect = ($FreshInstalledLegacyAddins.Count -eq 0) $InstalledLegacyAddins = $FreshInstalledLegacyAddins - } - catch { + } catch { Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get fresh add-in state after remediation for $Tenant" -Sev Warning } } @@ -143,15 +141,14 @@ function Invoke-CIPPStandardLegacyEmailReportAddins { } if ($Settings.report -eq $true) { - $ReportData = if ($StateIsCorrect) { - $true - } else { - @{ - InstalledLegacyAddins = $InstalledLegacyAddins - Status = 'Legacy add-ins still installed' - } + $CurrentValue = @{ + InstalledLegacyAddins = $InstalledLegacyAddins + } + $ExpectedValue = @{ + InstalledLegacyAddins = @() } - Set-CIPPStandardsCompareField -FieldName 'standards.LegacyEmailReportAddins' -FieldValue $ReportData -TenantFilter $Tenant - Add-CIPPBPAField -FieldName 'LegacyEmailReportAddins' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $tenant + + Set-CIPPStandardsCompareField -FieldName 'standards.LegacyEmailReportAddins' -Tenant $Tenant -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue + Add-CIPPBPAField -FieldName 'LegacyEmailReportAddins' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $Tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMDMEnrollmentDuringRegistration.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMDMEnrollmentDuringRegistration.ps1 index c1ee03aecaa5..250aaa273045 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMDMEnrollmentDuringRegistration.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMDMEnrollmentDuringRegistration.ps1 @@ -83,8 +83,13 @@ } if ($Settings.report -eq $true) { - $FieldValue = $StateIsCorrect ? $true : @{isMdmEnrollmentDuringRegistrationDisabled = $CurrentState; desiredState = $DesiredState } - Set-CIPPStandardsCompareField -FieldName 'standards.MDMEnrollmentDuringRegistration' -FieldValue $FieldValue -TenantFilter $Tenant + $CurrentValue = @{ + isMdmEnrollmentDuringRegistrationDisabled = $CurrentState + } + $ExpectedValue = @{ + isMdmEnrollmentDuringRegistrationDisabled = $DesiredState + } + Set-CIPPStandardsCompareField -FieldName 'standards.MDMEnrollmentDuringRegistration' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant Add-CIPPBPAField -FieldName 'MDMEnrollmentDuringRegistration' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMDMScope.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMDMScope.ps1 index 5566af2f0c46..b0e67306cc6a 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMDMScope.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMDMScope.ps1 @@ -41,18 +41,17 @@ function Invoke-CIPPStandardMDMScope { try { $CurrentInfo = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/policies/mobileDeviceManagementPolicies/0000000a-0000-0000-c000-000000000000?$expand=includedGroups' -tenantid $Tenant - } - catch { + } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the MDM Scope state for $Tenant. Error: $ErrorMessage" -Sev Error return } $StateIsCorrect = ($CurrentInfo.termsOfUseUrl -eq 'https://portal.manage.microsoft.com/TermsofUse.aspx') -and - ($CurrentInfo.discoveryUrl -eq 'https://enrollment.manage.microsoft.com/enrollmentserver/discovery.svc') -and - ($CurrentInfo.complianceUrl -eq 'https://portal.manage.microsoft.com/?portalAction=Compliance') -and - ($CurrentInfo.appliesTo -eq $Settings.appliesTo) -and - ($Settings.appliesTo -ne 'selected' -or ($CurrentInfo.includedGroups.displayName -contains $Settings.customGroup)) + ($CurrentInfo.discoveryUrl -eq 'https://enrollment.manage.microsoft.com/enrollmentserver/discovery.svc') -and + ($CurrentInfo.complianceUrl -eq 'https://portal.manage.microsoft.com/?portalAction=Compliance') -and + ($CurrentInfo.appliesTo -eq $Settings.appliesTo) -and + ($Settings.appliesTo -ne 'selected' -or ($CurrentInfo.includedGroups.displayName -contains $Settings.customGroup)) $CompareField = [PSCustomObject]@{ termsOfUseUrl = $CurrentInfo.termsOfUseUrl @@ -62,7 +61,7 @@ function Invoke-CIPPStandardMDMScope { customGroup = $CurrentInfo.includedGroups.displayName } - If ($Settings.remediate -eq $true) { + if ($Settings.remediate -eq $true) { if ($StateIsCorrect -eq $true) { Write-LogMessage -API 'Standards' -tenant $tenant -message 'MDM Scope already correctly configured' -sev Info } else { @@ -144,8 +143,21 @@ function Invoke-CIPPStandardMDMScope { } if ($Settings.report -eq $true) { - $FieldValue = $StateIsCorrect ? $true : $CompareField - Set-CIPPStandardsCompareField -FieldName 'standards.MDMScope' -FieldValue $FieldValue -TenantFilter $Tenant + $CurrentValue = @{ + termsOfUseUrl = $CurrentInfo.termsOfUseUrl + discoveryUrl = $CurrentInfo.discoveryUrl + complianceUrl = $CurrentInfo.complianceUrl + appliesTo = $CurrentInfo.appliesTo + customGroup = $CurrentInfo.includedGroups.displayName + } + $ExpectedValue = @{ + termsOfUseUrl = $Settings.termsOfUseUrl + discoveryUrl = $Settings.discoveryUrl + complianceUrl = $Settings.complianceUrl + appliesTo = $Settings.appliesTo + customGroup = $Settings.customGroup + } + Set-CIPPStandardsCompareField -FieldName 'standards.MDMScope' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant Add-CIPPBPAField -FieldName 'MDMScope' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $tenant } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMailContacts.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMailContacts.ps1 index 0c255ecfa1d6..d45e345f556c 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMailContacts.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMailContacts.ps1 @@ -34,7 +34,6 @@ function Invoke-CIPPStandardMailContacts { #> param($Tenant, $Settings) - ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'MailContacts' try { $TenantID = (New-GraphGetRequest -Uri 'https://graph.microsoft.com/beta/organization' -tenantid $tenant) @@ -107,8 +106,17 @@ function Invoke-CIPPStandardMailContacts { } if ($Settings.report -eq $true) { - $ReportState = $state ? $true : ($CurrentInfo | Select-Object marketingNotificationEmails, technicalNotificationMails, privacyProfile) - Set-CIPPStandardsCompareField -FieldName 'standards.MailContacts' -FieldValue $ReportState -Tenant $tenant + $CurrentValue = @{ + marketingNotificationEmails = $CurrentInfo.marketingNotificationEmails + technicalNotificationMails = @($CurrentInfo.technicalNotificationMails) + contactEmail = $CurrentInfo.privacyProfile.contactEmail + } + $ExpectedValue = @{ + marketingNotificationEmails = $Contacts.MarketingContact + technicalNotificationMails = @($Contacts.SecurityContact, $Contacts.TechContact) | Where-Object { $_ -ne $null } + contactEmail = $Contacts.GeneralContact + } + Set-CIPPStandardsCompareField -FieldName 'standards.MailContacts' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -Tenant $tenant Add-CIPPBPAField -FieldName 'MailContacts' -FieldValue $CurrentInfo -StoreAs json -Tenant $tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMailboxRecipientLimits.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMailboxRecipientLimits.ps1 index 72e7b220962b..a796462e2881 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMailboxRecipientLimits.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMailboxRecipientLimits.ps1 @@ -48,8 +48,7 @@ function Invoke-CIPPStandardMailboxRecipientLimits { # Get mailbox plans first try { $MailboxPlans = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-MailboxPlan' -cmdParams @{ ResultSize = 'Unlimited' } - } - catch { + } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the MailboxRecipientLimits state for $Tenant. Error: $ErrorMessage" -Sev Error return @@ -259,11 +258,14 @@ function Invoke-CIPPStandardMailboxRecipientLimits { } Add-CIPPBPAField -FieldName 'MailboxRecipientLimits' -FieldValue $ReportData -StoreAs json -Tenant $Tenant - if ($MailboxesToUpdate.Count -eq 0 -and $MailboxesWithPlanIssues.Count -eq 0) { - $FieldValue = $true - } else { - $FieldValue = $ReportData + $CurrentValue = @{ + MailboxesToUpdate = @($MailboxesToUpdate) + MailboxesWithPlanIssues = @($MailboxesWithPlanIssues) + } + $ExpectedValue = @{ + MailboxesToUpdate = @() + MailboxesWithPlanIssues = @() } - Set-CIPPStandardsCompareField -FieldName 'standards.MailboxRecipientLimits' -FieldValue $FieldValue -Tenant $Tenant + Set-CIPPStandardsCompareField -FieldName 'standards.MailboxRecipientLimits' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -Tenant $Tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMalwareFilterPolicy.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMalwareFilterPolicy.ps1 index 22c3f53fde45..640db0445b43 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMalwareFilterPolicy.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMalwareFilterPolicy.ps1 @@ -49,7 +49,6 @@ function Invoke-CIPPStandardMalwareFilterPolicy { Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. - ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'MalwareFilterPolicy' # Use custom name if provided, otherwise use default for backward compatibility $PolicyName = if ($Settings.name) { $Settings.name } else { 'CIPP Default Malware Policy' } @@ -193,8 +192,21 @@ function Invoke-CIPPStandardMalwareFilterPolicy { } if ($Settings.report -eq $true) { - $state = $StateIsCorrect ? $true : $CurrentState - Set-CIPPStandardsCompareField -FieldName 'standards.MalwareFilterPolicy' -FieldValue $state -TenantFilter $Tenant + $CurrentValue = $CurrentState | Select-Object Name, EnableFileFilter, FileTypeAction, FileTypes, ZapEnabled, QuarantineTag, EnableInternalSenderAdminNotifications, InternalSenderAdminAddress, EnableExternalSenderAdminNotifications, ExternalSenderAdminAddress + + $ExpectedValue = @{ + Name = $PolicyName + EnableFileFilter = $true + FileTypeAction = $FileTypeAction + FileTypes = $ExpectedFileTypes + ZapEnabled = $true + QuarantineTag = $Settings.QuarantineTag + EnableInternalSenderAdminNotifications = $Settings.EnableInternalSenderAdminNotifications + InternalSenderAdminAddress = $Settings.InternalSenderAdminAddress + EnableExternalSenderAdminNotifications = $Settings.EnableExternalSenderAdminNotifications + ExternalSenderAdminAddress = $Settings.ExternalSenderAdminAddress + } + Set-CIPPStandardsCompareField -FieldName 'standards.MalwareFilterPolicy' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant Add-CIPPBPAField -FieldName 'MalwareFilterPolicy' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $tenant } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMessageExpiration.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMessageExpiration.ps1 index b3d2e22adb6d..489366c54f0a 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMessageExpiration.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMessageExpiration.ps1 @@ -34,12 +34,10 @@ function Invoke-CIPPStandardMessageExpiration { Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. - ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'MessageExpiration' try { $MessageExpiration = (New-ExoRequest -tenantid $Tenant -cmdlet 'Get-TransportConfig').messageExpiration - } - catch { + } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the MessageExpiration state for $Tenant. Error: $ErrorMessage" -Sev Error return @@ -70,8 +68,13 @@ function Invoke-CIPPStandardMessageExpiration { } } if ($Settings.report -eq $true) { - if ($MessageExpiration -ne '12:00:00') { $MessageExpiration = $false } else { $MessageExpiration = $true } - Set-CIPPStandardsCompareField -FieldName 'standards.MessageExpiration' -FieldValue $MessageExpiration -TenantFilter $Tenant + $CurrentValue = @{ + MessageExpiration = $MessageExpiration + } + $ExpectedValue = @{ + MessageExpiration = '12:00:00' + } + Set-CIPPStandardsCompareField -FieldName 'standards.MessageExpiration' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant Add-CIPPBPAField -FieldName 'messageExpiration' -FieldValue $MessageExpiration -StoreAs bool -Tenant $tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardNudgeMFA.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardNudgeMFA.ps1 index e81fc47ee558..169ba51d7586 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardNudgeMFA.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardNudgeMFA.ps1 @@ -32,7 +32,6 @@ function Invoke-CIPPStandardNudgeMFA { #> param($Tenant, $Settings) - ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'NudgeMFA' Write-Host "NudgeMFA: $($Settings | ConvertTo-Json -Compress)" # Get state value using null-coalescing operator $State = $Settings.state.value ?? $Settings.state @@ -86,8 +85,15 @@ function Invoke-CIPPStandardNudgeMFA { } if ($Settings.report -eq $true) { - $State = $StateIsCorrect ? $true : ($CurrentState.registrationEnforcement.authenticationMethodsRegistrationCampaign | Select-Object snoozeDurationInDays, state) - Set-CIPPStandardsCompareField -FieldName 'standards.NudgeMFA' -FieldValue $State -Tenant $Tenant + $CurrentValue = @{ + snoozeDurationInDays = $CurrentState.registrationEnforcement.authenticationMethodsRegistrationCampaign.snoozeDurationInDays + state = $CurrentState.registrationEnforcement.authenticationMethodsRegistrationCampaign.state + } + $ExpectedValue = @{ + snoozeDurationInDays = $Settings.snoozeDurationInDays + state = $State + } + Set-CIPPStandardsCompareField -FieldName 'standards.NudgeMFA' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant Add-CIPPBPAField -FieldName 'NudgeMFA' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $Tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardOWAAttachmentRestrictions.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardOWAAttachmentRestrictions.ps1 index d63acb07b749..614df8aca6fe 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardOWAAttachmentRestrictions.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardOWAAttachmentRestrictions.ps1 @@ -119,19 +119,18 @@ function Invoke-CIPPStandardOWAAttachmentRestrictions { } if ($Settings.report -eq $true) { - if ($StateIsCorrect) { - Set-CIPPStandardsCompareField -FieldName 'standards.OWAAttachmentRestrictions' -FieldValue $true -TenantFilter $Tenant - Add-CIPPBPAField -FieldName 'OWAAttachmentRestrictions' -FieldValue $true -StoreAs bool -Tenant $Tenant - } else { - $ReportData = @{ - CurrentPolicy = $CurrentPolicy.ConditionalAccessPolicy - RequiredPolicy = $Settings.ConditionalAccessPolicy.value - PolicyName = $CurrentPolicy.Name - IsCompliant = $false - Description = 'OWA attachment restrictions not properly configured for unmanaged devices' - } - Set-CIPPStandardsCompareField -FieldName 'standards.OWAAttachmentRestrictions' -FieldValue $ReportData -TenantFilter $Tenant - Add-CIPPBPAField -FieldName 'OWAAttachmentRestrictions' -FieldValue $ReportData -StoreAs json -Tenant $Tenant + $CurrentValue = @{ + ConditionalAccessPolicy = $CurrentPolicy.ConditionalAccessPolicy + PolicyName = $CurrentPolicy.Name + IsCompliant = $StateIsCorrect } + $ExpectedValue = @{ + ConditionalAccessPolicy = $Settings.ConditionalAccessPolicy.value + PolicyName = 'OwaMailboxPolicy-Default' + IsCompliant = $true + } + + Set-CIPPStandardsCompareField -FieldName 'standards.OWAAttachmentRestrictions' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant + Add-CIPPBPAField -FieldName 'OWAAttachmentRestrictions' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $Tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardOauthConsent.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardOauthConsent.ps1 index 3c26e522a553..d4eee69db7a6 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardOauthConsent.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardOauthConsent.ps1 @@ -42,8 +42,7 @@ function Invoke-CIPPStandardOauthConsent { try { $State = New-GraphGetRequest -Uri 'https://graph.microsoft.com/beta/policies/authorizationPolicy/authorizationPolicy' -tenantid $tenant - } - catch { + } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the OauthConsent state for $Tenant. Error: $ErrorMessage" -Sev Error return @@ -101,12 +100,12 @@ function Invoke-CIPPStandardOauthConsent { if ($Settings.report -eq $true) { Add-CIPPBPAField -FieldName 'OauthConsent' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $tenant - if ($StateIsCorrect) { - $FieldValue = $true - } else { - $FieldValue = $State | Select-Object -Property permissionGrantPolicyIdsAssignedToDefaultUserRole + $CurrentValue = @{ + permissionGrantPolicyIdsAssignedToDefaultUserRole = $State.permissionGrantPolicyIdsAssignedToDefaultUserRole } - - Set-CIPPStandardsCompareField -FieldName 'standards.OauthConsent' -FieldValue $FieldValue -Tenant $tenant + $ExpectedValue = @{ + permissionGrantPolicyIdsAssignedToDefaultUserRole = @('managePermissionGrantsForSelf.cipp-consent-policy') + } + Set-CIPPStandardsCompareField -FieldName 'standards.OauthConsent' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -Tenant $tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardOauthConsentLowSec.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardOauthConsentLowSec.ps1 index 89a4336b0d84..2ba180c1a0ca 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardOauthConsentLowSec.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardOauthConsentLowSec.ps1 @@ -35,9 +35,8 @@ function Invoke-CIPPStandardOauthConsentLowSec { $State = (New-GraphGetRequest -Uri 'https://graph.microsoft.com/beta/policies/authorizationPolicy/authorizationPolicy' -tenantid $tenant) $PermissionState = (New-GraphGetRequest -Uri "https://graph.microsoft.com/beta/servicePrincipals(appId='00000003-0000-0000-c000-000000000000')/delegatedPermissionClassifications" -tenantid $tenant) | - Select-Object -Property permissionName - } - catch { + Select-Object -Property permissionName + } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the OauthConsentLowSec state for $Tenant. Error: $ErrorMessage" -Sev Error return @@ -108,23 +107,21 @@ function Invoke-CIPPStandardOauthConsentLowSec { } if ($Settings.report -eq $true) { - if ($State.permissionGrantPolicyIdsAssignedToDefaultUserRole -notin @('managePermissionGrantsForSelf.microsoft-user-default-low')) { - $State.permissionGrantPolicyIdsAssignedToDefaultUserRole = $false - $ValueField = @{ - authorizationPolicy = $State.permissionGrantPolicyIdsAssignedToDefaultUserRole - permissionClassifications = $PermissionState - } - if ($ConflictingStandard) { - $ValueField.conflictingStandard = @{ - name = $ConflictingStandard.Standard - templateid = $ConflictingStandard.TemplateId - } + $CurrentValue = @{ + permissionGrantPolicyIdsAssignedToDefaultUserRole = $State.permissionGrantPolicyIdsAssignedToDefaultUserRole + } + # Add conflicting standard info if applicable + if ($ConflictingStandard) { + $CurrentValue.conflictingStandard = @{ + name = $ConflictingStandard.Standard + templateid = $ConflictingStandard.TemplateId } - } else { - $State.permissionGrantPolicyIdsAssignedToDefaultUserRole = $true - $ValueField = $true + } + + $ExpectedValue = @{ + permissionGrantPolicyIdsAssignedToDefaultUserRole = @('managePermissionGrantsForSelf.microsoft-user-default-low') } Add-CIPPBPAField -FieldName 'OauthConsentLowSec' -FieldValue $State.permissionGrantPolicyIdsAssignedToDefaultUserRole -StoreAs bool -Tenant $tenant - Set-CIPPStandardsCompareField -FieldName 'standards.OauthConsentLowSec' -FieldValue $ValueField -Tenant $tenant + Set-CIPPStandardsCompareField -FieldName 'standards.OauthConsentLowSec' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -Tenant $tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardOutBoundSpamAlert.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardOutBoundSpamAlert.ps1 index 3407c342634c..746cb2632c46 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardOutBoundSpamAlert.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardOutBoundSpamAlert.ps1 @@ -40,8 +40,7 @@ function Invoke-CIPPStandardOutBoundSpamAlert { try { $CurrentInfo = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-HostedOutboundSpamFilterPolicy' -cmdParams @{ Identity = 'Default' } -useSystemMailbox $true - } - catch { + } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the OutBoundSpamAlert state for $Tenant. Error: $ErrorMessage" -Sev Error return @@ -76,11 +75,14 @@ function Invoke-CIPPStandardOutBoundSpamAlert { if ($Settings.report -eq $true) { Add-CIPPBPAField -FieldName 'OutboundSpamAlert' -FieldValue $CurrentInfo.NotifyOutboundSpam -StoreAs bool -Tenant $tenant - if ($CurrentInfo.NotifyOutboundSpam -ne $true -or $CurrentInfo.NotifyOutboundSpamRecipients -ne $settings.OutboundSpamContact) { - $ValueField = $CurrentInfo | Select-Object -Property NotifyOutboundSpamRecipients, NotifyOutboundSpam - } else { - $ValueField = $true + $CurrentValue = @{ + NotifyOutboundSpam = $CurrentInfo.NotifyOutboundSpam + NotifyOutboundSpamRecipients = $CurrentInfo.NotifyOutboundSpamRecipients + } + $ExpectedValue = @{ + NotifyOutboundSpam = $true + NotifyOutboundSpamRecipients = $settings.OutboundSpamContact } - Set-CIPPStandardsCompareField -FieldName 'standards.OutBoundSpamAlert' -FieldValue $ValueField -Tenant $tenant + Set-CIPPStandardsCompareField -FieldName 'standards.OutBoundSpamAlert' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -Tenant $tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPWcompanionAppAllowedState.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPWcompanionAppAllowedState.ps1 index cfc78fe70b03..474de9cf7e44 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPWcompanionAppAllowedState.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPWcompanionAppAllowedState.ps1 @@ -102,6 +102,12 @@ function Invoke-CIPPStandardPWcompanionAppAllowedState { } else { $FieldValue = $AuthenticatorFeaturesState.featureSettings.companionAppAllowedState } - Set-CIPPStandardsCompareField -FieldName 'standards.PWcompanionAppAllowedState' -FieldValue $FieldValue -Tenant $Tenant + $CurrentValue = @{ + companionAppAllowedState = $AuthenticatorFeaturesState.featureSettings.companionAppAllowedState.state + } + $ExpectedValue = @{ + companionAppAllowedState = $WantedState + } + Set-CIPPStandardsCompareField -FieldName 'standards.PWcompanionAppAllowedState' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -Tenant $Tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPWdisplayAppInformationRequiredState.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPWdisplayAppInformationRequiredState.ps1 index 05a71918a8c2..63723ed0fd13 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPWdisplayAppInformationRequiredState.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPWdisplayAppInformationRequiredState.ps1 @@ -42,8 +42,7 @@ function Invoke-CIPPStandardPWdisplayAppInformationRequiredState { try { $CurrentState = New-GraphGetRequest -Uri 'https://graph.microsoft.com/beta/policies/authenticationMethodsPolicy/authenticationMethodConfigurations/microsoftAuthenticator' -tenantid $Tenant - } - catch { + } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the PWdisplayAppInformationRequiredState state for $Tenant. Error: $ErrorMessage" -Sev Error return @@ -75,11 +74,16 @@ function Invoke-CIPPStandardPWdisplayAppInformationRequiredState { if ($Settings.report -eq $true) { Add-CIPPBPAField -FieldName 'PWdisplayAppInformationRequiredState' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $tenant - if ($StateIsCorrect) { - $FieldValue = $true - } else { - $FieldValue = $CurrentState + $CurrentValue = @{ + state = $CurrentState.state + numberMatchingRequiredState = $CurrentState.featureSettings.numberMatchingRequiredState.state + displayAppInformationRequiredState = $CurrentState.featureSettings.displayAppInformationRequiredState.state + } + $ExpectedValue = @{ + state = 'enabled' + numberMatchingRequiredState = 'enabled' + displayAppInformationRequiredState = 'enabled' } - Set-CIPPStandardsCompareField -FieldName 'standards.PWdisplayAppInformationRequiredState' -FieldValue $FieldValue -Tenant $tenant + Set-CIPPStandardsCompareField -FieldName 'standards.PWdisplayAppInformationRequiredState' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -Tenant $tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPasswordExpireDisabled.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPasswordExpireDisabled.ps1 index b26f7ba9b84c..403f80236b2e 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPasswordExpireDisabled.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPasswordExpireDisabled.ps1 @@ -37,8 +37,7 @@ function Invoke-CIPPStandardPasswordExpireDisabled { try { $GraphRequest = New-GraphGetRequest -uri 'https://graph.microsoft.com/v1.0/domains' -tenantid $Tenant - } - catch { + } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the PasswordExpireDisabled state for $Tenant. Error: $ErrorMessage" -Sev Error return @@ -82,11 +81,13 @@ function Invoke-CIPPStandardPasswordExpireDisabled { if ($Settings.report -eq $true) { Add-CIPPBPAField -FieldName 'PasswordExpireDisabled' -FieldValue $DomainsWithoutPassExpire -StoreAs json -Tenant $tenant - if ($DomainsWithoutPassExpire) { - $FieldValue = $DomainsWithoutPassExpire - } else { - $FieldValue = $true + + $CurrentValue = @{ + DomainsWithoutPassExpire = @($DomainsWithoutPassExpire) + } + $ExpectedValue = @{ + DomainsWithoutPassExpire = @() } - Set-CIPPStandardsCompareField -FieldName 'standards.PasswordExpireDisabled' -FieldValue $FieldValue -Tenant $tenant + Set-CIPPStandardsCompareField -FieldName 'standards.PasswordExpireDisabled' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -Tenant $tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPerUserMFA.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPerUserMFA.ps1 index 32cfa2b62748..e4ea97dc4389 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPerUserMFA.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPerUserMFA.ps1 @@ -42,15 +42,14 @@ function Invoke-CIPPStandardPerUserMFA { try { $GraphRequest = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/users?`$top=999&`$select=userPrincipalName,displayName,accountEnabled,perUserMfaState&`$filter=userType eq 'Member' and accountEnabled eq true and displayName ne 'On-Premises Directory Synchronization Service Account'&`$count=true" -tenantid $Tenant -ComplexFilter - } - catch { + } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the PerUserMFA state for $Tenant. Error: $ErrorMessage" -Sev Error return } $UsersWithoutMFA = $GraphRequest | Where-Object -Property perUserMfaState -NE 'enforced' | Select-Object -Property userPrincipalName, displayName, accountEnabled, perUserMfaState - If ($Settings.remediate -eq $true) { + if ($Settings.remediate -eq $true) { if (($UsersWithoutMFA | Measure-Object).Count -gt 0) { try { $MFAMessage = Set-CIPPPerUserMFA -TenantFilter $Tenant -userId @($UsersWithoutMFA.userPrincipalName) -State 'enforced' @@ -70,8 +69,13 @@ function Invoke-CIPPStandardPerUserMFA { } } if ($Settings.report -eq $true) { - $State = $UsersWithoutMFA ? $UsersWithoutMFA : $true - Set-CIPPStandardsCompareField -FieldName 'standards.PerUserMFA' -FieldValue $State -Tenant $tenant + $CurrentValue = @{ + UsersWithoutMFA = @($UsersWithoutMFA) + } + $ExpectedValue = @{ + UsersWithoutMFA = @() + } + Set-CIPPStandardsCompareField -FieldName 'standards.PerUserMFA' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -Tenant $tenant Add-CIPPBPAField -FieldName 'LegacyMFAUsers' -FieldValue $UsersWithoutMFA -StoreAs json -Tenant $tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPhishProtection.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPhishProtection.ps1 index ee3093fbd9c9..fdf203641250 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPhishProtection.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPhishProtection.ps1 @@ -33,7 +33,6 @@ function Invoke-CIPPStandardPhishProtection { #> param($Tenant, $Settings) - ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'PhishProtection' $TenantId = Get-Tenants | Where-Object -Property defaultDomainName -EQ $tenant @@ -100,7 +99,13 @@ function Invoke-CIPPStandardPhishProtection { } if ($Settings.report -eq $true) { if ($currentBody -like "*$CSS*") { $authState = $true } else { $authState = $false } + $CurrentValue = @{ + PhishingCSSEnabled = $authState + } + $ExpectedValue = @{ + PhishingCSSEnabled = $true + } Add-CIPPBPAField -FieldName 'PhishProtection' -FieldValue $authState -StoreAs bool -Tenant $tenant - Set-CIPPStandardsCompareField -FieldName 'standards.PhishProtection' -FieldValue $authState -Tenant $tenant + Set-CIPPStandardsCompareField -FieldName 'standards.PhishProtection' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -Tenant $tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPhishSimSpoofIntelligence.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPhishSimSpoofIntelligence.ps1 index 23691f879c08..8374e9803f06 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPhishSimSpoofIntelligence.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPhishSimSpoofIntelligence.ps1 @@ -39,9 +39,8 @@ function Invoke-CIPPStandardPhishSimSpoofIntelligence { # Fetch current Phishing Simulations Spoof Intelligence domains and ensure it is correctly configured try { $DomainState = New-ExoRequest -TenantId $Tenant -cmdlet 'Get-TenantAllowBlockListSpoofItems' | - Select-Object -Property Identity, SendingInfrastructure - } - catch { + Select-Object -Property Identity, SendingInfrastructure + } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the PhishSimSpoofIntelligence state for $Tenant. Error: $ErrorMessage" -Sev Error return @@ -51,7 +50,7 @@ function Invoke-CIPPStandardPhishSimSpoofIntelligence { if ($Settings.RemoveExtraDomains -eq $true) { $RemoveDomain = $DomainState | Where-Object { $_.SendingInfrastructure -notin $Settings.AllowedDomains.value } | - Select-Object -Property Identity,SendingInfrastructure + Select-Object -Property Identity, SendingInfrastructure } else { $RemoveDomain = @() } @@ -59,19 +58,19 @@ function Invoke-CIPPStandardPhishSimSpoofIntelligence { $StateIsCorrect = ($AddDomain.Count -eq 0 -and $RemoveDomain.Count -eq 0) $CompareField = [PSCustomObject]@{ - "Missing Domains" = $AddDomain -join ', ' - "Incorrect Domains" = $RemoveDomain.SendingInfrastructure -join ', ' + 'Missing Domains' = $AddDomain -join ', ' + 'Incorrect Domains' = $RemoveDomain.SendingInfrastructure -join ', ' } - If ($Settings.remediate -eq $true) { - If ($StateIsCorrect -eq $true) { + if ($Settings.remediate -eq $true) { + if ($StateIsCorrect -eq $true) { Write-LogMessage -API 'Standards' -Tenant $Tenant -message 'Spoof Intelligence Allow list already correctly configured' -sev Info - } Else { + } else { $BulkRequests = New-Object System.Collections.Generic.List[Hashtable] if ($Settings.RemoveExtraDomains -eq $true) { # Prepare removal requests - If ($RemoveDomain.Count -gt 0) { + if ($RemoveDomain.Count -gt 0) { Write-Host "Removing $($RemoveDomain.Count) domains from Spoof Intelligence" $BulkRequests.Add(@{ CmdletInput = @{ @@ -83,45 +82,52 @@ function Invoke-CIPPStandardPhishSimSpoofIntelligence { } # Prepare addition requests - ForEach ($Domain in $AddDomain) { + foreach ($Domain in $AddDomain) { $BulkRequests.Add(@{ - CmdletInput = @{ - CmdletName = 'New-TenantAllowBlockListSpoofItems' - Parameters = @{ Identity = 'default'; Action = 'Allow'; SendingInfrastructure = $Domain; SpoofedUser = '*'; SpoofType = 'Internal' } - } - }) + CmdletInput = @{ + CmdletName = 'New-TenantAllowBlockListSpoofItems' + Parameters = @{ Identity = 'default'; Action = 'Allow'; SendingInfrastructure = $Domain; SpoofedUser = '*'; SpoofType = 'Internal' } + } + }) $BulkRequests.Add(@{ - CmdletInput = @{ - CmdletName = 'New-TenantAllowBlockListSpoofItems' - Parameters = @{ Identity = 'default'; Action = 'Allow'; SendingInfrastructure = $Domain; SpoofedUser = '*'; SpoofType = 'External' } - } - }) + CmdletInput = @{ + CmdletName = 'New-TenantAllowBlockListSpoofItems' + Parameters = @{ Identity = 'default'; Action = 'Allow'; SendingInfrastructure = $Domain; SpoofedUser = '*'; SpoofType = 'External' } + } + }) } $RawExoRequest = New-ExoBulkRequest -tenantid $Tenant -cmdletArray @($BulkRequests) $LastError = $RawExoRequest | Select-Object -Last 1 - If ($LastError.error) { - Foreach ($ExoError in $LastError.error) { + if ($LastError.error) { + foreach ($ExoError in $LastError.error) { Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Failed to process Spoof Intelligence Domain with error: $ExoError" -Sev Error } - } Else { - Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Processed all Spoof Intelligence Domains successfully." -Sev Info + } else { + Write-LogMessage -API 'Standards' -Tenant $Tenant -Message 'Processed all Spoof Intelligence Domains successfully.' -Sev Info } } } - If ($Settings.alert -eq $true) { - If ($StateIsCorrect -eq $true) { + if ($Settings.alert -eq $true) { + if ($StateIsCorrect -eq $true) { Write-LogMessage -API 'Standards' -Tenant $Tenant -message 'Spoof Intelligence Allow list is correctly configured' -sev Info - } Else { + } else { Write-StandardsAlert -message 'Spoof Intelligence Allow list is not correctly configured' -object $CompareField -tenant $Tenant -standardName 'PhishSimSpoofIntelligence' -standardId $Settings.standardId Write-LogMessage -API 'Standards' -Tenant $Tenant -message 'Spoof Intelligence Allow list is not correctly configured' -sev Info } } - If ($Settings.report -eq $true) { - $FieldValue = $StateIsCorrect ? $true : $CompareField - Set-CIPPStandardsCompareField -FieldName 'standards.PhishSimSpoofIntelligence' -FieldValue $FieldValue -Tenant $Tenant + if ($Settings.report -eq $true) { + $CurrentValue = @{ + AllowedDomains = @($DomainState.SendingInfrastructure) + IsCompliant = [bool]$StateIsCorrect + } + $ExpectedValue = @{ + AllowedDomains = @($Settings.AllowedDomains.value) + IsCompliant = $true + } + Set-CIPPStandardsCompareField -FieldName 'standards.PhishSimSpoofIntelligence' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -Tenant $Tenant Add-CIPPBPAField -FieldName 'PhishSimSpoofIntelligence' -FieldValue [bool]$StateIsCorrect -StoreAs bool -Tenant $Tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPhishingSimulations.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPhishingSimulations.ps1 index 6cc3a314ea55..e5d64514d13c 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPhishingSimulations.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPhishingSimulations.ps1 @@ -43,10 +43,9 @@ function Invoke-CIPPStandardPhishingSimulations { # Fetch current Phishing Simulations Policy settings and ensure it is correctly configured try { $PolicyState = New-ExoRequest -TenantId $Tenant -cmdlet 'Get-PhishSimOverridePolicy' | - Where-Object -Property Name -EQ 'PhishSimOverridePolicy' | - Select-Object -Property Identity, Name, Mode, Enabled - } - catch { + Where-Object -Property Name -EQ 'PhishSimOverridePolicy' | + Select-Object -Property Identity, Name, Mode, Enabled + } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the PhishingSimulations state for $Tenant. Error: $ErrorMessage" -Sev Error return @@ -56,7 +55,7 @@ function Invoke-CIPPStandardPhishingSimulations { # Fetch current Phishing Simulations Policy Rule settings and ensure it is correctly configured $RuleState = New-ExoRequest -TenantId $Tenant -cmdlet 'Get-ExoPhishSimOverrideRule' | - Select-Object -Property Identity,Name,SenderIpRanges,Domains,SenderDomainIs + Select-Object -Property Identity, Name, SenderIpRanges, Domains, SenderDomainIs [String[]]$AddSenderIpRanges = $Settings.SenderIpRanges.value | Where-Object { $_ -notin $RuleState.SenderIpRanges } if ($Settings.RemoveExtraUrls -eq $true) { @@ -72,13 +71,13 @@ function Invoke-CIPPStandardPhishingSimulations { $RemoveDomains = @() } - $RuleIsCorrect = ($RuleState.Name -like "*PhishSimOverr*") -and + $RuleIsCorrect = ($RuleState.Name -like '*PhishSimOverr*') -and ($AddSenderIpRanges.Count -eq 0 -and $RemoveSenderIpRanges.Count -eq 0) -and ($AddDomains.Count -eq 0 -and $RemoveDomains.Count -eq 0) # Fetch current Phishing Simulations URLs and ensure it is correctly configured - $SimUrlState = New-ExoRequest -TenantId $Tenant -cmdlet 'Get-TenantAllowBlockListItems' -cmdParams @{ListType = 'Url'; ListSubType = 'AdvancedDelivery'} | - Select-Object -Property Value + $SimUrlState = New-ExoRequest -TenantId $Tenant -cmdlet 'Get-TenantAllowBlockListItems' -cmdParams @{ListType = 'Url'; ListSubType = 'AdvancedDelivery' } | + Select-Object -Property Value [String[]]$AddEntries = $Settings.PhishingSimUrls.value | Where-Object { $_ -notin $SimUrlState.value } if ($Settings.RemoveExtraUrls -eq $true) { @@ -98,107 +97,118 @@ function Invoke-CIPPStandardPhishingSimulations { PhishingSimUrls = $SimUrlState.value -join ', ' } - If ($Settings.remediate -eq $true) { - If ($StateIsCorrect -eq $true) { + if ($Settings.remediate -eq $true) { + if ($StateIsCorrect -eq $true) { Write-LogMessage -API 'Standards' -Tenant $Tenant -message 'Advanced Phishing Simulations already correctly configured' -sev Info - } Else { + } else { # Remediate incorrect Phishing Simulations Policy - If ($PolicyIsCorrect -eq $false) { - If ($PolicyState.Name -eq 'PhishSimOverridePolicy') { - Try { - $null = New-ExoRequest -TenantId $Tenant -cmdlet 'Set-PhishSimOverridePolicy' -cmdParams @{Identity = $PolicyName; Enabled = $true} - Write-LogMessage -API 'Standards' -Tenant $Tenant -message "Enabled Phishing Simulation override policy." -sev Info - } Catch { - Write-LogMessage -API 'Standards' -Tenant $Tenant -message "Failed to enable Phishing Simulation override policy." -sev Error -LogData $_ + if ($PolicyIsCorrect -eq $false) { + if ($PolicyState.Name -eq 'PhishSimOverridePolicy') { + try { + $null = New-ExoRequest -TenantId $Tenant -cmdlet 'Set-PhishSimOverridePolicy' -cmdParams @{Identity = $PolicyName; Enabled = $true } + Write-LogMessage -API 'Standards' -Tenant $Tenant -message 'Enabled Phishing Simulation override policy.' -sev Info + } catch { + Write-LogMessage -API 'Standards' -Tenant $Tenant -message 'Failed to enable Phishing Simulation override policy.' -sev Error -LogData $_ } - } Else { - Try { - $null = New-ExoRequest -TenantId $Tenant -cmdlet 'New-PhishSimOverridePolicy' -cmdParams @{Name = $PolicyName; Enabled = $true} - Write-LogMessage -API 'Standards' -Tenant $Tenant -message "Created Phishing Simulation override policy." -sev Info - } Catch { - Write-LogMessage -API 'Standards' -Tenant $Tenant -message "Failed to create Phishing Simulation override policy." -sev Error -LogData $_ + } else { + try { + $null = New-ExoRequest -TenantId $Tenant -cmdlet 'New-PhishSimOverridePolicy' -cmdParams @{Name = $PolicyName; Enabled = $true } + Write-LogMessage -API 'Standards' -Tenant $Tenant -message 'Created Phishing Simulation override policy.' -sev Info + } catch { + Write-LogMessage -API 'Standards' -Tenant $Tenant -message 'Failed to create Phishing Simulation override policy.' -sev Error -LogData $_ } } } # Remediate incorrect Phishing Simulations Policy Rule - If ($RuleIsCorrect -eq $false) { - If ($RuleState.Name -like "*PhishSimOverr*") { + if ($RuleIsCorrect -eq $false) { + if ($RuleState.Name -like '*PhishSimOverr*') { $cmdParams = @{ - Identity = $RuleState.Identity - AddSenderIpRanges = $AddSenderIpRanges - AddDomains = $AddDomains + Identity = $RuleState.Identity + AddSenderIpRanges = $AddSenderIpRanges + AddDomains = $AddDomains RemoveSenderIpRanges = $RemoveSenderIpRanges - RemoveDomains = $RemoveDomains + RemoveDomains = $RemoveDomains } - Try { + try { $null = New-ExoRequest -TenantId $Tenant -cmdlet 'Set-ExoPhishSimOverrideRule' -cmdParams $cmdParams - Write-LogMessage -API 'Standards' -Tenant $Tenant -message "Updated Phishing Simulation override rule." -sev Info - } Catch { - Write-LogMessage -API 'Standards' -Tenant $Tenant -message "Failed to update Phishing Simulation override rule." -sev Error -LogData $_ + Write-LogMessage -API 'Standards' -Tenant $Tenant -message 'Updated Phishing Simulation override rule.' -sev Info + } catch { + Write-LogMessage -API 'Standards' -Tenant $Tenant -message 'Failed to update Phishing Simulation override rule.' -sev Error -LogData $_ } - } Else { + } else { $cmdParams = @{ - Name = $PolicyName - Policy = 'PhishSimOverridePolicy' + Name = $PolicyName + Policy = 'PhishSimOverridePolicy' SenderIpRanges = $Settings.SenderIpRanges.value - Domains = $Settings.Domains.value + Domains = $Settings.Domains.value } - Try { + try { $null = New-ExoRequest -TenantId $Tenant -cmdlet 'New-ExoPhishSimOverrideRule' -cmdParams $cmdParams - Write-LogMessage -API 'Standards' -Tenant $Tenant -message "Created Phishing Simulation override rule." -sev Info - } Catch { - Write-LogMessage -API 'Standards' -Tenant $Tenant -message "Failed to create Phishing Simulation override rule." -sev Error -LogData $_ + Write-LogMessage -API 'Standards' -Tenant $Tenant -message 'Created Phishing Simulation override rule.' -sev Info + } catch { + Write-LogMessage -API 'Standards' -Tenant $Tenant -message 'Failed to create Phishing Simulation override rule.' -sev Error -LogData $_ } } } # Remediate incorrect Phishing Simulations URLs - If ($PhishingSimUrlsIsCorrect -eq $false) { + if ($PhishingSimUrlsIsCorrect -eq $false) { $cmdParams = @{ - ListType = 'Url' + ListType = 'Url' ListSubType = 'AdvancedDelivery' } if ($Settings.RemoveExtraUrls -eq $true) { # Remove entries that are not in the settings - If ($RemoveEntries.Count -gt 0) { + if ($RemoveEntries.Count -gt 0) { $cmdParams.Entries = $RemoveEntries - Try { + try { $null = New-ExoRequest -TenantId $Tenant -cmdlet 'Remove-TenantAllowBlockListItems' -cmdParams $cmdParams - Write-LogMessage -API 'Standards' -Tenant $Tenant -message "Removed Phishing Simulation URLs from Allowlist." -sev Info - } Catch { - Write-LogMessage -API 'Standards' -Tenant $Tenant -message "Failed to remove Phishing Simulation URLs from Allowlist." -sev Error -LogData $_ + Write-LogMessage -API 'Standards' -Tenant $Tenant -message 'Removed Phishing Simulation URLs from Allowlist.' -sev Info + } catch { + Write-LogMessage -API 'Standards' -Tenant $Tenant -message 'Failed to remove Phishing Simulation URLs from Allowlist.' -sev Error -LogData $_ } } } # Add entries that are in the settings - If ($AddEntries.Count -gt 0) { + if ($AddEntries.Count -gt 0) { $cmdParams.Entries = $AddEntries $cmdParams.NoExpiration = $true $cmdParams.Allow = $true - Try { + try { $null = New-ExoRequest -TenantId $Tenant -cmdlet 'New-TenantAllowBlockListItems' -cmdParams $cmdParams - Write-LogMessage -API 'Standards' -Tenant $Tenant -message "Added Phishing Simulation URLs to Allowlist." -sev Info - } Catch { - Write-LogMessage -API 'Standards' -Tenant $Tenant -message "Failed to add Phishing Simulation URLs to Allowlist." -sev Error -LogData $_ + Write-LogMessage -API 'Standards' -Tenant $Tenant -message 'Added Phishing Simulation URLs to Allowlist.' -sev Info + } catch { + Write-LogMessage -API 'Standards' -Tenant $Tenant -message 'Failed to add Phishing Simulation URLs to Allowlist.' -sev Error -LogData $_ } } } } } - If ($Settings.alert -eq $true) { - If ($StateIsCorrect -eq $true) { + if ($Settings.alert -eq $true) { + if ($StateIsCorrect -eq $true) { Write-LogMessage -API 'Standards' -Tenant $Tenant -message 'Phishing Simulation Configuration is correctly configured' -sev Info - } Else { + } else { Write-StandardsAlert -message 'Phishing Simulation Configuration is not correctly configured' -object $CompareField -tenant $Tenant -standardName 'PhishingSimulations' -standardId $Settings.standardId Write-LogMessage -API 'Standards' -Tenant $Tenant -message 'Phishing Simulation Configuration is not correctly configured' -sev Info } } - If ($Settings.report -eq $true) { - $FieldValue = $StateIsCorrect ? $true : $CompareField + if ($Settings.report -eq $true) { + $CurrentValue = @{ + Domains = @($RuleState.Domains) + SenderIpRanges = @($RuleState.SenderIpRanges) + PhishingSimUrls = @($SimUrlState.value) + IsCompliant = $StateIsCorrect + } + $ExpectedValue = @{ + Domains = @($Settings.Domains.value) + SenderIpRanges = @($Settings.SenderIpRanges.value) + PhishingSimUrls = @($Settings.PhishingSimUrls.value) + IsCompliant = $true + } Add-CIPPBPAField -FieldName 'PhishingSimulations' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $Tenant - Set-CIPPStandardsCompareField -FieldName 'standards.PhishingSimulations' -FieldValue $FieldValue -Tenant $Tenant + Set-CIPPStandardsCompareField -FieldName 'standards.PhishingSimulations' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -Tenant $Tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardProfilePhotos.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardProfilePhotos.ps1 index 181abe1da4ce..c802bc152a03 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardProfilePhotos.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardProfilePhotos.ps1 @@ -44,7 +44,7 @@ function Invoke-CIPPStandardProfilePhotos { # Input validation if ([string]::IsNullOrWhiteSpace($StateValue)) { Write-LogMessage -API 'Standards' -tenant $tenant -message 'ProfilePhotos: Invalid state parameter set' -sev Error - Return + return } # true if wanted state is enabled, false if disabled @@ -54,8 +54,7 @@ function Invoke-CIPPStandardProfilePhotos { try { $Uri = 'https://graph.microsoft.com/beta/admin/people/photoUpdateSettings' $CurrentGraphState = New-GraphGetRequest -uri $Uri -tenantid $Tenant - } - catch { + } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the ProfilePhotos state for $Tenant. Error: $ErrorMessage" -Sev Error return @@ -133,6 +132,12 @@ function Invoke-CIPPStandardProfilePhotos { GraphStateCorrect = $GraphStateCorrect } } - Set-CIPPStandardsCompareField -FieldName 'standards.ProfilePhotos' -FieldValue $FieldValue -Tenant $Tenant + $CurrentValue = @{ + ProfilePhotosEnabled = $UsersCanChangePhotos + } + $ExpectedValue = @{ + ProfilePhotosEnabled = $DesiredState + } + Set-CIPPStandardsCompareField -FieldName 'standards.ProfilePhotos' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -Tenant $Tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardQuarantineRequestAlert.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardQuarantineRequestAlert.ps1 index 4942da7410c9..8e22526e6b29 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardQuarantineRequestAlert.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardQuarantineRequestAlert.ps1 @@ -41,10 +41,9 @@ function Invoke-CIPPStandardQuarantineRequestAlert { try { $CurrentState = New-ExoRequest -TenantId $Tenant -cmdlet 'Get-ProtectionAlert' -Compliance | - Where-Object { $_.Name -eq $PolicyName } | - Select-Object -Property * - } - catch { + Where-Object { $_.Name -eq $PolicyName } | + Select-Object -Property * + } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the QuarantineRequestAlert state for $Tenant. Error: $ErrorMessage" -Sev Error return @@ -101,11 +100,12 @@ function Invoke-CIPPStandardQuarantineRequestAlert { if ($Settings.report -eq $true) { Add-CIPPBPAField -FieldName 'QuarantineRequestAlert' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $Tenant - if ($StateIsCorrect) { - $FieldValue = $true - } else { - $FieldValue = @{NotifyUser = $CurrentState.notifyUser } + $CurrentValue = @{ + NotifyUser = @($CurrentState.NotifyUser) + } + $ExpectedValue = @{ + NotifyUser = @($Settings.NotifyUser) } - Set-CIPPStandardsCompareField -FieldName 'standards.QuarantineRequestAlert' -FieldValue $FieldValue -Tenant $Tenant + Set-CIPPStandardsCompareField -FieldName 'standards.QuarantineRequestAlert' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -Tenant $Tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardQuarantineTemplate.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardQuarantineTemplate.ps1 index 474775e9002b..a89ce3829c0a 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardQuarantineTemplate.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardQuarantineTemplate.ps1 @@ -53,117 +53,114 @@ function Invoke-CIPPStandardQuarantineTemplate { try { # Get the current custom quarantine policies - $CurrentPolicies = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-QuarantinePolicy' | Where-Object -Property Guid -ne '00000000-0000-0000-0000-000000000000' -ErrorAction Stop + $CurrentPolicies = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-QuarantinePolicy' | Where-Object -Property Guid -NE '00000000-0000-0000-0000-000000000000' -ErrorAction Stop # Compare the settings from standard with the current policies $CompareList = foreach ($Policy in $Settings) { try { # Create hashtable with desired Quarantine Setting - $EndUserQuarantinePermissions = @{ + $EndUserQuarantinePermissions = @{ # ViewHeader and Download are set to false because the value 0 or 1 does nothing per Microsoft documentation - PermissionToViewHeader = $false - PermissionToDownload = $false - PermissionToBlockSender = $Policy.PermissionToBlockSender - PermissionToDelete = $Policy.PermissionToDelete - PermissionToPreview = $Policy.PermissionToPreview - PermissionToRelease = $Policy.ReleaseAction -eq "PermissionToRelease" ? $true : $false - PermissionToRequestRelease = $Policy.ReleaseAction -eq "PermissionToRequestRelease" ? $true : $false - PermissionToAllowSender = $Policy.PermissionToAllowSender + PermissionToViewHeader = $false + PermissionToDownload = $false + PermissionToBlockSender = $Policy.PermissionToBlockSender + PermissionToDelete = $Policy.PermissionToDelete + PermissionToPreview = $Policy.PermissionToPreview + PermissionToRelease = $Policy.ReleaseAction -eq 'PermissionToRelease' ? $true : $false + PermissionToRequestRelease = $Policy.ReleaseAction -eq 'PermissionToRequestRelease' ? $true : $false + PermissionToAllowSender = $Policy.PermissionToAllowSender } # If the Quarantine Policy already exists if ($Policy.displayName.value -in $CurrentPolicies.Name) { #Get the current policy and convert EndUserQuarantinePermissions from string to hashtable for compare - $ExistingPolicy = $CurrentPolicies | Where-Object -Property Name -eq $Policy.displayName.value + $ExistingPolicy = $CurrentPolicies | Where-Object -Property Name -EQ $Policy.displayName.value $ExistingPolicyEndUserQuarantinePermissions = Convert-QuarantinePermissionsValue -InputObject $ExistingPolicy.EndUserQuarantinePermissions -ErrorAction Stop #Compare the current policy $StateIsCorrect = ($ExistingPolicy.Name -eq $Policy.displayName.value) -and - ($ExistingPolicy.ESNEnabled -eq $Policy.ESNEnabled) -and - ($ExistingPolicy.IncludeMessagesFromBlockedSenderAddress -eq $Policy.IncludeMessagesFromBlockedSenderAddress) -and - (!(Compare-Object @($ExistingPolicyEndUserQuarantinePermissions.values) @($EndUserQuarantinePermissions.values))) + ($ExistingPolicy.ESNEnabled -eq $Policy.ESNEnabled) -and + ($ExistingPolicy.IncludeMessagesFromBlockedSenderAddress -eq $Policy.IncludeMessagesFromBlockedSenderAddress) -and + (!(Compare-Object @($ExistingPolicyEndUserQuarantinePermissions.values) @($EndUserQuarantinePermissions.values))) # If the current policy is correct if ($StateIsCorrect -eq $true) { [PSCustomObject]@{ - missing = $false - StateIsCorrect = $StateIsCorrect - Action = "None" - displayName = $Policy.displayName.value - EndUserQuarantinePermissions = $EndUserQuarantinePermissions - ESNEnabled = $Policy.ESNEnabled + missing = $false + StateIsCorrect = $StateIsCorrect + Action = 'None' + displayName = $Policy.displayName.value + EndUserQuarantinePermissions = $EndUserQuarantinePermissions + ESNEnabled = $Policy.ESNEnabled IncludeMessagesFromBlockedSenderAddress = $Policy.IncludeMessagesFromBlockedSenderAddress - remediate = $Policy.remediate - alert = $Policy.alert - report = $Policy.report + remediate = $Policy.remediate + alert = $Policy.alert + report = $Policy.report } } #If the current policy doesn't match the desired settings else { [PSCustomObject]@{ - missing = $false - StateIsCorrect = $StateIsCorrect - Action = "Update" - displayName = $Policy.displayName.value - EndUserQuarantinePermissions = $EndUserQuarantinePermissions - ESNEnabled = $Policy.ESNEnabled + missing = $false + StateIsCorrect = $StateIsCorrect + Action = 'Update' + displayName = $Policy.displayName.value + EndUserQuarantinePermissions = $EndUserQuarantinePermissions + ESNEnabled = $Policy.ESNEnabled IncludeMessagesFromBlockedSenderAddress = $Policy.IncludeMessagesFromBlockedSenderAddress - remediate = $Policy.remediate - alert = $Policy.alert - report = $Policy.report + remediate = $Policy.remediate + alert = $Policy.alert + report = $Policy.report } } } #If no existing Quarantine Policy with the same name was found else { [PSCustomObject]@{ - missing = $true - StateIsCorrect = $false - Action = "Create" - displayName = $Policy.displayName.value - EndUserQuarantinePermissions = $EndUserQuarantinePermissions - ESNEnabled = $Policy.ESNEnabled + missing = $true + StateIsCorrect = $false + Action = 'Create' + displayName = $Policy.displayName.value + EndUserQuarantinePermissions = $EndUserQuarantinePermissions + ESNEnabled = $Policy.ESNEnabled IncludeMessagesFromBlockedSenderAddress = $Policy.IncludeMessagesFromBlockedSenderAddress - remediate = $Policy.remediate - alert = $Policy.alert - report = $Policy.report + remediate = $Policy.remediate + alert = $Policy.alert + report = $Policy.report } } - } - catch { + } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message $Message = "Failed to compare Quarantine policy $($Policy.displayName.value), Error: $ErrorMessage" Write-LogMessage -API $APIName -tenant $tenant -message $Message -sev 'Error' - Return $Message + return $Message } } - If ($true -in $Settings.remediate) { + if ($true -in $Settings.remediate) { # Remediate each policy which is incorrect or missing - foreach ($Policy in $CompareList | Where-Object { $_.remediate -EQ $true -and $_.StateIsCorrect -eq $false }) { + foreach ($Policy in $CompareList | Where-Object { $_.remediate -eq $true -and $_.StateIsCorrect -eq $false }) { try { # Parameters for splatting to Set-CIPPQuarantinePolicy $Params = @{ - Action = $Policy.Action - Identity = $Policy.displayName - EndUserQuarantinePermissions = $Policy.EndUserQuarantinePermissions - ESNEnabled = $Policy.ESNEnabled + Action = $Policy.Action + Identity = $Policy.displayName + EndUserQuarantinePermissions = $Policy.EndUserQuarantinePermissions + ESNEnabled = $Policy.ESNEnabled IncludeMessagesFromBlockedSenderAddress = $Policy.IncludeMessagesFromBlockedSenderAddress - tenantFilter = $Tenant - APIName = $APIName + tenantFilter = $Tenant + APIName = $APIName } try { Set-CIPPQuarantinePolicy @Params Write-LogMessage -API $APIName -tenant $Tenant -message "$($Policy.Action)d Custom Quarantine Policy '$($Policy.displayName)'" -sev Info - } - catch { + } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API $APIName -tenant $tenant -message "Failed to $($Policy.Action) Quarantine policy $($Policy.displayName), Error: $ErrorMessage" -sev 'Error' } - } - catch { + } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API $APIName -tenant $tenant -message "Failed to create or update Quarantine policy $($Policy.displayName), Error: $ErrorMessage" -sev 'Error' } @@ -174,15 +171,13 @@ function Invoke-CIPPStandardQuarantineTemplate { foreach ($Policy in $CompareList | Where-Object -Property alert -EQ $true) { if ($Policy.StateIsCorrect) { Write-LogMessage -API $APIName -tenant $Tenant -message "Quarantine policy $($Policy.displayName) has the correct configuration." -sev Info - } - else { + } else { if ($Policy.missing) { $CurrentInfo = $Policy | Select-Object -Property displayName, missing Write-StandardsAlert -message "Quarantine policy $($Policy.displayName) is missing." -object $CurrentInfo -tenant $Tenant -standardName 'QuarantineTemplate' -standardId $Settings.templateId Write-LogMessage -API $APIName -tenant $Tenant -message "Quarantine policy $($Policy.displayName) is missing." -sev info - } - else { - $CurrentInfo = $CurrentPolicies | Where-Object -Property Name -eq $Policy.displayName | Select-Object -Property Name, ESNEnabled, IncludeMessagesFromBlockedSenderAddress, EndUserQuarantinePermissions + } else { + $CurrentInfo = $CurrentPolicies | Where-Object -Property Name -EQ $Policy.displayName | Select-Object -Property Name, ESNEnabled, IncludeMessagesFromBlockedSenderAddress, EndUserQuarantinePermissions Write-StandardsAlert -message "Quarantine policy $($Policy.displayName) does not match the expected configuration." -object $CurrentInfo -tenant $Tenant -standardName 'QuarantineTemplate' -standardId $Settings.templateId Write-LogMessage -API $APIName -tenant $Tenant -message "Quarantine policy $($Policy.displayName) does not match the expected configuration. We've generated an alert" -sev info } @@ -194,11 +189,29 @@ function Invoke-CIPPStandardQuarantineTemplate { foreach ($Policy in $CompareList | Where-Object -Property report -EQ $true) { # Convert displayName to hex to avoid invalid characters "/, \, #, ?" which are not allowed in RowKey, but "\, #, ?" can be used in quarantine displayName $HexName = -join ($Policy.displayName.ToCharArray() | ForEach-Object { '{0:X2}' -f [int][char]$_ }) - Set-CIPPStandardsCompareField -FieldName "standards.QuarantineTemplate.$HexName" -FieldValue $Policy.StateIsCorrect -TenantFilter $Tenant + + $CurrentValue = @{ + displayName = $Policy.displayName + EndUserQuarantinePermissions = $Policy.EndUserQuarantinePermissions + ESNEnabled = $Policy.ESNEnabled + IncludeMessagesFromBlockedSenderAddress = $Policy.IncludeMessagesFromBlockedSenderAddress + StateIsCorrect = $Policy.StateIsCorrect + missing = $Policy.missing + } + + $ExpectedValue = @{ + displayName = $Policy.displayName + EndUserQuarantinePermissions = $Policy.EndUserQuarantinePermissions + ESNEnabled = $Policy.ESNEnabled + IncludeMessagesFromBlockedSenderAddress = $Policy.IncludeMessagesFromBlockedSenderAddress + StateIsCorrect = $true + missing = $false + } + + Set-CIPPStandardsCompareField -FieldName "standards.QuarantineTemplate.$HexName" -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant } } - } - catch { + } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API $APIName -tenant $tenant -message "Failed to create or update Quarantine policy/policies, Error: $ErrorMessage" -sev 'Error' } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardRestrictThirdPartyStorageServices.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardRestrictThirdPartyStorageServices.ps1 index b84aca5fe2c9..d140c767a1da 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardRestrictThirdPartyStorageServices.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardRestrictThirdPartyStorageServices.ps1 @@ -32,8 +32,7 @@ function Invoke-CIPPStandardRestrictThirdPartyStorageServices { #> param ($Tenant, $Settings) - ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'RestrictThirdPartyStorageServices' - $TestResult = Test-CIPPStandardLicense -StandardName 'ThirdPartyStorageServicesRestricted' -TenantFilter $Tenant -RequiredCapabilities @('SHAREPOINTWAC', 'SHAREPOINTSTANDARD', 'SHAREPOINTENTERPRISE', 'SHAREPOINTENTERPRISE_EDU','ONEDRIVE_BASIC', 'ONEDRIVE_ENTERPRISE') + $TestResult = Test-CIPPStandardLicense -StandardName 'ThirdPartyStorageServicesRestricted' -TenantFilter $Tenant -RequiredCapabilities @('SHAREPOINTWAC', 'SHAREPOINTSTANDARD', 'SHAREPOINTENTERPRISE', 'SHAREPOINTENTERPRISE_EDU', 'ONEDRIVE_BASIC', 'ONEDRIVE_ENTERPRISE') if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." @@ -89,13 +88,15 @@ function Invoke-CIPPStandardRestrictThirdPartyStorageServices { } if ($Settings.report -eq $true) { - if ($null -eq $CurrentState.accountEnabled -or $CurrentState.accountEnabled -eq $true) { - Set-CIPPStandardsCompareField -FieldName 'standards.RestrictThirdPartyStorageServices' -FieldValue $false -Tenant $Tenant - Add-CIPPBPAField -FieldName 'ThirdPartyStorageServicesRestricted' -FieldValue $false -StoreAs bool -Tenant $Tenant - } else { - $CorrectState = $CurrentState.accountEnabled -eq $false ? $true : $false - Set-CIPPStandardsCompareField -FieldName 'standards.RestrictThirdPartyStorageServices' -FieldValue $CorrectState -Tenant $Tenant - Add-CIPPBPAField -FieldName 'ThirdPartyStorageServicesRestricted' -FieldValue $CorrectState -StoreAs bool -Tenant $Tenant + + $CurrentValue = @{ + thirdPartyStorageRestricted = $CurrentState.accountEnabled -eq $false } + $ExpectedValue = @{ + thirdPartyStorageRestricted = $false + } + + Set-CIPPStandardsCompareField -FieldName 'standards.RestrictThirdPartyStorageServices' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant + Add-CIPPBPAField -FieldName 'ThirdPartyStorageServicesRestricted' -FieldValue ($CurrentState.accountEnabled -eq $false) -StoreAs bool -Tenant $Tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardRetentionPolicyTag.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardRetentionPolicyTag.ps1 index 438d70c82afa..cb992130b094 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardRetentionPolicyTag.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardRetentionPolicyTag.ps1 @@ -43,12 +43,11 @@ function Invoke-CIPPStandardRetentionPolicyTag { try { $CurrentState = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-RetentionPolicyTag' | - Where-Object -Property Identity -EQ $PolicyName + Where-Object -Property Identity -EQ $PolicyName $PolicyState = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-RetentionPolicy' | - Where-Object -Property Identity -EQ 'Default MRM Policy' - } - catch { + Where-Object -Property Identity -EQ 'Default MRM Policy' + } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the RetentionPolicy state for $Tenant. Error: $ErrorMessage" -Sev Error return @@ -126,12 +125,22 @@ function Invoke-CIPPStandardRetentionPolicyTag { if ($Settings.report -eq $true) { Add-CIPPBPAField -FieldName 'RetentionPolicy' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $tenant - if ($StateIsCorrect) { - $FieldValue = $true - } else { - $FieldValue = @{ CurrentState = $CurrentState; PolicyState = $PolicyState } + $CurrentValue = @{ + retentionEnabled = $CurrentState.RetentionEnabled + retentionAction = $CurrentState.RetentionAction + ageLimitForRetention = $CurrentState.AgeLimitForRetention.TotalDays + type = $CurrentState.Type + policyTagLinked = $PolicyState.RetentionPolicyTagLinks -contains $PolicyName + + } + $ExpectedValue = @{ + retentionEnabled = $true + retentionAction = 'PermanentlyDelete' + ageLimitForRetention = $Settings.AgeLimitForRetention + type = 'DeletedItems' + policyTagLinked = $true } - Set-CIPPStandardsCompareField -FieldName 'standards.RetentionPolicyTag' -FieldValue $FieldValue -Tenant $Tenant - } + Set-CIPPStandardsCompareField -FieldName 'standards.RetentionPolicyTag' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -Tenant $Tenant + } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardRotateDKIM.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardRotateDKIM.ps1 index 846f3b93eadb..931795adb66b 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardRotateDKIM.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardRotateDKIM.ps1 @@ -42,8 +42,7 @@ function Invoke-CIPPStandardRotateDKIM { try { $DKIM = (New-ExoRequest -tenantid $tenant -cmdlet 'Get-DkimSigningConfig') | Where-Object { $_.Selector1KeySize -eq 1024 -and $_.Enabled -eq $true } - } - catch { + } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the DKIM state for $Tenant. Error: $ErrorMessage" -Sev Error return @@ -78,10 +77,13 @@ function Invoke-CIPPStandardRotateDKIM { if ($Settings.report -eq $true) { Add-CIPPBPAField -FieldName 'DKIM' -FieldValue $DKIM -StoreAs json -Tenant $tenant - if ($DKIM) { - Set-CIPPStandardsCompareField -FieldName 'standards.RotateDKIM' -FieldValue $DKIM -Tenant $tenant - } else { - Set-CIPPStandardsCompareField -FieldName 'standards.RotateDKIM' -FieldValue $true -Tenant $tenant + + $CurrentValue = @{ + domainsWith1024BitDKIM = if ($DKIM) { $DKIM.Identity } else { @() } + } + $ExpectedValue = @{ + domainsWith1024BitDKIM = @() } + Set-CIPPStandardsCompareField -FieldName 'standards.RotateDKIM' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -Tenant $tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPAzureB2B.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPAzureB2B.ps1 index 767910a0793f..6a4d323ed5a1 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPAzureB2B.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPAzureB2B.ps1 @@ -32,7 +32,7 @@ function Invoke-CIPPStandardSPAzureB2B { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'SPAzureB2B' -TenantFilter $Tenant -RequiredCapabilities @('SHAREPOINTWAC', 'SHAREPOINTSTANDARD', 'SHAREPOINTENTERPRISE', 'SHAREPOINTENTERPRISE_EDU','ONEDRIVE_BASIC', 'ONEDRIVE_ENTERPRISE') + $TestResult = Test-CIPPStandardLicense -StandardName 'SPAzureB2B' -TenantFilter $Tenant -RequiredCapabilities @('SHAREPOINTWAC', 'SHAREPOINTSTANDARD', 'SHAREPOINTENTERPRISE', 'SHAREPOINTENTERPRISE_EDU', 'ONEDRIVE_BASIC', 'ONEDRIVE_ENTERPRISE') if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." @@ -41,9 +41,8 @@ function Invoke-CIPPStandardSPAzureB2B { try { $CurrentState = Get-CIPPSPOTenant -TenantFilter $Tenant | - Select-Object -Property _ObjectIdentity_, TenantFilter, EnableAzureADB2BIntegration - } - catch { + Select-Object -Property _ObjectIdentity_, TenantFilter, EnableAzureADB2BIntegration + } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the SPAzureB2B state for $Tenant. Error: $ErrorMessage" -Sev Error return @@ -81,11 +80,13 @@ function Invoke-CIPPStandardSPAzureB2B { if ($Settings.report -eq $true) { Add-CIPPBPAField -FieldName 'AzureB2B' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $tenant - if ($StateIsCorrect) { - $FieldValue = $true - } else { - $FieldValue = $CurrentState + + $CurrentValue = @{ + EnableAzureADB2BIntegration = $CurrentState.EnableAzureADB2BIntegration + } + $ExpectedValue = @{ + EnableAzureADB2BIntegration = $true } - Set-CIPPStandardsCompareField -FieldName 'standards.SPAzureB2B' -FieldValue $FieldValue -Tenant $Tenant + Set-CIPPStandardsCompareField -FieldName 'standards.SPAzureB2B' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -Tenant $Tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPDirectSharing.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPDirectSharing.ps1 index a7e9e3a8a2ff..a2ac80b497d4 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPDirectSharing.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPDirectSharing.ps1 @@ -7,8 +7,8 @@ function Invoke-CIPPStandardSPDirectSharing { .SYNOPSIS (Label) Default sharing to Direct users .DESCRIPTION - (Helptext) This standard has been deprecated in favor of the Default Sharing Link standard. - (DocsDescription) This standard has been deprecated in favor of the Default Sharing Link standard. + (Helptext) This standard has been deprecated in favor of the Default Sharing Link standard. + (DocsDescription) This standard has been deprecated in favor of the Default Sharing Link standard. .NOTES CAT SharePoint Standards @@ -32,7 +32,7 @@ function Invoke-CIPPStandardSPDirectSharing { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'SPDirectSharing' -TenantFilter $Tenant -RequiredCapabilities @('SHAREPOINTWAC', 'SHAREPOINTSTANDARD', 'SHAREPOINTENTERPRISE', 'SHAREPOINTENTERPRISE_EDU','ONEDRIVE_BASIC', 'ONEDRIVE_ENTERPRISE') + $TestResult = Test-CIPPStandardLicense -StandardName 'SPDirectSharing' -TenantFilter $Tenant -RequiredCapabilities @('SHAREPOINTWAC', 'SHAREPOINTSTANDARD', 'SHAREPOINTENTERPRISE', 'SHAREPOINTENTERPRISE_EDU', 'ONEDRIVE_BASIC', 'ONEDRIVE_ENTERPRISE') if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." @@ -43,9 +43,8 @@ function Invoke-CIPPStandardSPDirectSharing { Write-LogMessage -API 'Standards' -Tenant $Tenant -Message 'The default sharing to Direct users standard has been deprecated in favor of the "Set Default Sharing Link Settings" standard. Please update your standards to use new standard. However this will continue to function.' -Sev Alert try { $CurrentState = Get-CIPPSPOTenant -TenantFilter $Tenant | - Select-Object -Property _ObjectIdentity_, TenantFilter, DefaultSharingLinkType - } - catch { + Select-Object -Property _ObjectIdentity_, TenantFilter, DefaultSharingLinkType + } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the SPDirectSharing state for $Tenant. Error: $ErrorMessage" -Sev Error return @@ -84,11 +83,12 @@ function Invoke-CIPPStandardSPDirectSharing { if ($Settings.report -eq $true) { Add-CIPPBPAField -FieldName 'DirectSharing' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $tenant - if ($StateIsCorrect) { - $FieldValue = $true - } else { - $FieldValue = $CurrentState + $CurrentValue = @{ + DefaultSharingLinkType = $CurrentState.DefaultSharingLinkType + } + $ExpectedValue = @{ + DefaultSharingLinkType = 1 } - Set-CIPPStandardsCompareField -FieldName 'standards.SPDirectSharing' -FieldValue $FieldValue -Tenant $Tenant + Set-CIPPStandardsCompareField -FieldName 'standards.SPDirectSharing' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -Tenant $Tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPDisableLegacyWorkflows.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPDisableLegacyWorkflows.ps1 index dbbf748b9739..5612ce7c034e 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPDisableLegacyWorkflows.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPDisableLegacyWorkflows.ps1 @@ -29,7 +29,7 @@ function Invoke-CIPPStandardSPDisableLegacyWorkflows { https://docs.cipp.app/user-documentation/tenant/standards/list-standards #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'SPDisableLegacyWorkflows' -TenantFilter $Tenant -RequiredCapabilities @('SHAREPOINTWAC', 'SHAREPOINTSTANDARD', 'SHAREPOINTENTERPRISE', 'SHAREPOINTENTERPRISE_EDU','ONEDRIVE_BASIC', 'ONEDRIVE_ENTERPRISE') + $TestResult = Test-CIPPStandardLicense -StandardName 'SPDisableLegacyWorkflows' -TenantFilter $Tenant -RequiredCapabilities @('SHAREPOINTWAC', 'SHAREPOINTSTANDARD', 'SHAREPOINTENTERPRISE', 'SHAREPOINTENTERPRISE_EDU', 'ONEDRIVE_BASIC', 'ONEDRIVE_ENTERPRISE') if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." @@ -38,9 +38,8 @@ function Invoke-CIPPStandardSPDisableLegacyWorkflows { try { $CurrentState = Get-CIPPSPOTenant -TenantFilter $Tenant | - Select-Object -Property * - } - catch { + Select-Object -Property * + } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the SPDisableLegacyWorkflows state for $Tenant. Error: $ErrorMessage" -Sev Error return @@ -82,11 +81,17 @@ function Invoke-CIPPStandardSPDisableLegacyWorkflows { if ($Settings.report -eq $true) { Add-CIPPBPAField -FieldName 'SPDisableLegacyWorkflows' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $Tenant - if ($StateIsCorrect) { - $FieldValue = $true - } else { - $FieldValue = $CurrentState + + $CurrentValue = @{ + StopNew2010Workflows = $CurrentState.StopNew2010Workflows + StopNew2013Workflows = $CurrentState.StopNew2013Workflows + DisableBackToClassic = $CurrentState.DisableBackToClassic + } + $ExpectedValue = @{ + StopNew2010Workflows = $true + StopNew2013Workflows = $true + DisableBackToClassic = $true } - Set-CIPPStandardsCompareField -FieldName 'standards.SPDisableLegacyWorkflows' -FieldValue $FieldValue -Tenant $Tenant + Set-CIPPStandardsCompareField -FieldName 'standards.SPDisableLegacyWorkflows' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -Tenant $Tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPDisallowInfectedFiles.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPDisallowInfectedFiles.ps1 index 03bd09556c31..650647f6a0e0 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPDisallowInfectedFiles.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPDisallowInfectedFiles.ps1 @@ -85,11 +85,12 @@ function Invoke-CIPPStandardSPDisallowInfectedFiles { if ($Settings.report -eq $true) { Add-CIPPBPAField -FieldName 'SPDisallowInfectedFiles' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $Tenant - if ($StateIsCorrect) { - $FieldValue = $true - } else { - $FieldValue = $CurrentState + $CurrentValue = @{ + DisallowInfectedFileDownload = $CurrentState.DisallowInfectedFileDownload + } + $ExpectedValue = @{ + DisallowInfectedFileDownload = $true } - Set-CIPPStandardsCompareField -FieldName 'standards.SPDisallowInfectedFiles' -FieldValue $FieldValue -Tenant $Tenant + Set-CIPPStandardsCompareField -FieldName 'standards.SPDisallowInfectedFiles' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -Tenant $Tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPEmailAttestation.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPEmailAttestation.ps1 index c454a1976170..965b5bf90d5c 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPEmailAttestation.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPEmailAttestation.ps1 @@ -35,7 +35,7 @@ function Invoke-CIPPStandardSPEmailAttestation { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'SPEmailAttestation' -TenantFilter $Tenant -RequiredCapabilities @('SHAREPOINTWAC', 'SHAREPOINTSTANDARD', 'SHAREPOINTENTERPRISE', 'SHAREPOINTENTERPRISE_EDU','ONEDRIVE_BASIC', 'ONEDRIVE_ENTERPRISE') + $TestResult = Test-CIPPStandardLicense -StandardName 'SPEmailAttestation' -TenantFilter $Tenant -RequiredCapabilities @('SHAREPOINTWAC', 'SHAREPOINTSTANDARD', 'SHAREPOINTENTERPRISE', 'SHAREPOINTENTERPRISE_EDU', 'ONEDRIVE_BASIC', 'ONEDRIVE_ENTERPRISE') if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." @@ -44,9 +44,8 @@ function Invoke-CIPPStandardSPEmailAttestation { try { $CurrentState = Get-CIPPSPOTenant -TenantFilter $Tenant | - Select-Object -Property _ObjectIdentity_, TenantFilter, EmailAttestationReAuthDays, EmailAttestationRequired - } - catch { + Select-Object -Property _ObjectIdentity_, TenantFilter, EmailAttestationReAuthDays, EmailAttestationRequired + } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the SPEmailAttestation state for $Tenant. Error: $ErrorMessage" -Sev Error return @@ -91,11 +90,15 @@ function Invoke-CIPPStandardSPEmailAttestation { if ($Settings.report -eq $true) { Add-CIPPBPAField -FieldName 'SPEmailAttestation' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $Tenant - if ($StateIsCorrect) { - $FieldValue = $true - } else { - $FieldValue = $CurrentState + + $CurrentValue = @{ + EmailAttestationReAuthDays = $CurrentState.EmailAttestationReAuthDays + EmailAttestationRequired = $CurrentState.EmailAttestationRequired + } + $ExpectedValue = @{ + EmailAttestationReAuthDays = [int]$Settings.Days + EmailAttestationRequired = $true } - Set-CIPPStandardsCompareField -FieldName 'standards.SPEmailAttestation' -FieldValue $FieldValue -TenantFilter $Tenant + Set-CIPPStandardsCompareField -FieldName 'standards.SPEmailAttestation' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPExternalUserExpiration.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPExternalUserExpiration.ps1 index 96be5ff61600..f5595d9263f1 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPExternalUserExpiration.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPExternalUserExpiration.ps1 @@ -34,7 +34,7 @@ function Invoke-CIPPStandardSPExternalUserExpiration { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'SPExternalUserExpiration' -TenantFilter $Tenant -RequiredCapabilities @('SHAREPOINTWAC', 'SHAREPOINTSTANDARD', 'SHAREPOINTENTERPRISE', 'SHAREPOINTENTERPRISE_EDU','ONEDRIVE_BASIC', 'ONEDRIVE_ENTERPRISE') + $TestResult = Test-CIPPStandardLicense -StandardName 'SPExternalUserExpiration' -TenantFilter $Tenant -RequiredCapabilities @('SHAREPOINTWAC', 'SHAREPOINTSTANDARD', 'SHAREPOINTENTERPRISE', 'SHAREPOINTENTERPRISE_EDU', 'ONEDRIVE_BASIC', 'ONEDRIVE_ENTERPRISE') if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." @@ -43,9 +43,8 @@ function Invoke-CIPPStandardSPExternalUserExpiration { try { $CurrentState = Get-CIPPSPOTenant -TenantFilter $Tenant | - Select-Object -Property _ObjectIdentity_, TenantFilter, ExternalUserExpireInDays, ExternalUserExpirationRequired - } - catch { + Select-Object -Property _ObjectIdentity_, TenantFilter, ExternalUserExpireInDays, ExternalUserExpirationRequired + } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the SPExternalUserExpiration state for $Tenant. Error: $ErrorMessage" -Sev Error return @@ -90,7 +89,15 @@ function Invoke-CIPPStandardSPExternalUserExpiration { } else { $FieldValue = $CurrentState } - Set-CIPPStandardsCompareField -FieldName 'standards.SPExternalUserExpiration' -FieldValue $FieldValue -TenantFilter $Tenant + $CurrentValue = @{ + ExternalUserExpireInDays = $CurrentState.ExternalUserExpireInDays + ExternalUserExpirationRequired = $CurrentState.ExternalUserExpirationRequired + } + $ExpectedValue = @{ + ExternalUserExpireInDays = $Settings.Days + ExternalUserExpirationRequired = $true + } + Set-CIPPStandardsCompareField -FieldName 'standards.SPExternalUserExpiration' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant Add-CIPPBPAField -FieldName 'standards.SPExternalUserExpiration' -FieldValue $FieldValue -StoreAs bool -Tenant $Tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPFileRequests.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPFileRequests.ps1 index 7d01dbbcdca4..1354beba7a7d 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPFileRequests.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPFileRequests.ps1 @@ -33,7 +33,7 @@ function Invoke-CIPPStandardSPFileRequests { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'SPFileRequests' -TenantFilter $Tenant -RequiredCapabilities @('SHAREPOINTWAC', 'SHAREPOINTSTANDARD', 'SHAREPOINTENTERPRISE', 'SHAREPOINTENTERPRISE_EDU','ONEDRIVE_BASIC', 'ONEDRIVE_ENTERPRISE') + $TestResult = Test-CIPPStandardLicense -StandardName 'SPFileRequests' -TenantFilter $Tenant -RequiredCapabilities @('SHAREPOINTWAC', 'SHAREPOINTSTANDARD', 'SHAREPOINTENTERPRISE', 'SHAREPOINTENTERPRISE_EDU', 'ONEDRIVE_BASIC', 'ONEDRIVE_ENTERPRISE') if ($TestResult -eq $false) { Write-LogMessage -API 'Standards' -tenant $tenant -message 'The tenant is not licenced for this standard SPFileRequests' -sev Error @@ -42,14 +42,13 @@ function Invoke-CIPPStandardSPFileRequests { try { $CurrentState = Get-CIPPSPOTenant -TenantFilter $Tenant | Select-Object _ObjectIdentity_, TenantFilter, CoreRequestFilesLinkEnabled, OneDriveRequestFilesLinkEnabled, CoreRequestFilesLinkExpirationInDays, OneDriveRequestFilesLinkExpirationInDays - } - catch { + } catch { Write-LogMessage -API 'Standards' -tenant $tenant -message 'Failed to get current state of SPO tenant details' -sev Error return } # Input validation - if (($Settings.state -eq $null) -and ($Settings.remediate -eq $true -or $Settings.alert -eq $true)) { + if (($null -eq $Settings.state) -and ($Settings.remediate -eq $true -or $Settings.alert -eq $true)) { Write-LogMessage -API 'Standards' -tenant $tenant -message 'Invalid state parameter set for standard SPFileRequests' -sev Error return } @@ -65,7 +64,7 @@ function Invoke-CIPPStandardSPFileRequests { # Check expiration settings if specified $ExpirationIsCorrect = $true - if ($ExpirationDays -ne $null -and $WantedState -eq $true) { + if ($null -ne $ExpirationDays -and $WantedState -eq $true) { $CoreExpirationIsCorrect = ($CurrentState.CoreRequestFilesLinkExpirationInDays -eq $ExpirationDays) $OneDriveExpirationIsCorrect = ($CurrentState.OneDriveRequestFilesLinkExpirationInDays -eq $ExpirationDays) $ExpirationIsCorrect = $CoreExpirationIsCorrect -and $OneDriveExpirationIsCorrect @@ -78,37 +77,37 @@ function Invoke-CIPPStandardSPFileRequests { if ($AllSettingsCorrect -eq $false) { try { $Properties = @{ - CoreRequestFilesLinkEnabled = $WantedState + CoreRequestFilesLinkEnabled = $WantedState OneDriveRequestFilesLinkEnabled = $WantedState } # Add expiration settings if specified and feature is being enabled - if ($ExpirationDays -ne $null -and $WantedState -eq $true) { + if ($null -ne $ExpirationDays -and $WantedState -eq $true) { $Properties['CoreRequestFilesLinkExpirationInDays'] = $ExpirationDays $Properties['OneDriveRequestFilesLinkExpirationInDays'] = $ExpirationDays } $CurrentState | Set-CIPPSPOTenant -Properties $Properties - $ExpirationMessage = if ($ExpirationDays -ne $null -and $WantedState -eq $true) { " with $ExpirationDays day expiration" } else { "" } + $ExpirationMessage = if ($null -ne $ExpirationDays -and $WantedState -eq $true) { " with $ExpirationDays day expiration" } else { '' } Write-LogMessage -API 'Standards' -tenant $tenant -message "Successfully set File Requests to $HumanReadableState$ExpirationMessage" -sev Info } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to set File Requests to $HumanReadableState. Error: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage } } else { - $ExpirationMessage = if ($ExpirationDays -ne $null -and $WantedState -eq $true) { " with $ExpirationDays day expiration" } else { "" } + $ExpirationMessage = if ($null -ne $ExpirationDays -and $WantedState -eq $true) { " with $ExpirationDays day expiration" } else { '' } Write-LogMessage -API 'Standards' -tenant $tenant -message "File Requests are already set to the wanted state of $HumanReadableState$ExpirationMessage" -sev Info } } if ($Settings.alert -eq $true) { if ($AllSettingsCorrect -eq $true) { - $ExpirationMessage = if ($ExpirationDays -ne $null -and $WantedState -eq $true) { " with $ExpirationDays day expiration" } else { "" } + $ExpirationMessage = if ($null -ne $ExpirationDays -and $WantedState -eq $true) { " with $ExpirationDays day expiration" } else { '' } Write-LogMessage -API 'Standards' -tenant $tenant -message "File Requests are already set to the wanted state of $HumanReadableState$ExpirationMessage" -sev Info } else { $AlertMessage = "File Requests are not set to the wanted state of $HumanReadableState" - if ($ExpirationDays -ne $null -and $WantedState -eq $true) { + if ($null -ne $ExpirationDays -and $WantedState -eq $true) { $AlertMessage += " with $ExpirationDays day expiration" } Write-StandardsAlert -message $AlertMessage -object $CurrentState -tenant $tenant -standardName 'SPFileRequests' -standardId $Settings.standardId @@ -121,16 +120,23 @@ function Invoke-CIPPStandardSPFileRequests { Add-CIPPBPAField -FieldName 'SPCoreFileRequestsEnabled' -FieldValue $CurrentState.CoreRequestFilesLinkEnabled -StoreAs bool -Tenant $Tenant Add-CIPPBPAField -FieldName 'SPOneDriveFileRequestsEnabled' -FieldValue $CurrentState.OneDriveRequestFilesLinkEnabled -StoreAs bool -Tenant $Tenant - if ($ExpirationDays -ne $null) { + if ($null -ne $ExpirationDays) { Add-CIPPBPAField -FieldName 'SPCoreFileRequestsExpirationDays' -FieldValue $CurrentState.CoreRequestFilesLinkExpirationInDays -StoreAs string -Tenant $Tenant Add-CIPPBPAField -FieldName 'SPOneDriveFileRequestsExpirationDays' -FieldValue $CurrentState.OneDriveRequestFilesLinkExpirationInDays -StoreAs string -Tenant $Tenant } - if ($AllSettingsCorrect) { - $FieldValue = $true - } else { - $FieldValue = $CurrentState + $CurrentValue = @{ + CoreRequestFilesLinkEnabled = $CurrentState.CoreRequestFilesLinkEnabled + OneDriveRequestFilesLinkEnabled = $CurrentState.OneDriveRequestFilesLinkEnabled + CoreRequestFilesLinkExpirationInDays = $CurrentState.CoreRequestFilesLinkExpirationInDays + OneDriveRequestFilesLinkExpirationInDays = $CurrentState.OneDriveRequestFilesLinkExpirationInDays + } + $ExpectedValue = @{ + CoreRequestFilesLinkEnabled = $WantedState + OneDriveRequestFilesLinkEnabled = $WantedState + CoreRequestFilesLinkExpirationInDays = if ($null -ne $ExpirationDays -and $WantedState -eq $true) { $ExpirationDays } else { $null } + OneDriveRequestFilesLinkExpirationInDays = if ($null -ne $ExpirationDays -and $WantedState -eq $true) { $ExpirationDays } else { $null } } - Set-CIPPStandardsCompareField -FieldName 'standards.SPFileRequests' -FieldValue $FieldValue -Tenant $Tenant + Set-CIPPStandardsCompareField -FieldName 'standards.SPFileRequests' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -Tenant $Tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPSyncButtonState.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPSyncButtonState.ps1 index fec4d5f3a99f..0c39478cd85b 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPSyncButtonState.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPSyncButtonState.ps1 @@ -31,7 +31,7 @@ function Invoke-CIPPStandardSPSyncButtonState { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'SPSyncButtonState' -TenantFilter $Tenant -RequiredCapabilities @('SHAREPOINTWAC', 'SHAREPOINTSTANDARD', 'SHAREPOINTENTERPRISE', 'SHAREPOINTENTERPRISE_EDU','ONEDRIVE_BASIC', 'ONEDRIVE_ENTERPRISE') + $TestResult = Test-CIPPStandardLicense -StandardName 'SPSyncButtonState' -TenantFilter $Tenant -RequiredCapabilities @('SHAREPOINTWAC', 'SHAREPOINTSTANDARD', 'SHAREPOINTENTERPRISE', 'SHAREPOINTENTERPRISE_EDU', 'ONEDRIVE_BASIC', 'ONEDRIVE_ENTERPRISE') if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." @@ -40,8 +40,7 @@ function Invoke-CIPPStandardSPSyncButtonState { try { $CurrentState = Get-CIPPSPOTenant -TenantFilter $Tenant | Select-Object _ObjectIdentity_, TenantFilter, HideSyncButtonOnDocLib - } - catch { + } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the SPSyncButtonState state for $Tenant. Error: $ErrorMessage" -Sev Error return @@ -92,7 +91,13 @@ function Invoke-CIPPStandardSPSyncButtonState { } else { $FieldValue = $CurrentState } - Set-CIPPStandardsCompareField -FieldName 'standards.SPSyncButtonState' -FieldValue $FieldValue -Tenant $Tenant + $CurrentValue = @{ + HideSyncButtonOnDocLib = $CurrentState.HideSyncButtonOnDocLib + } + $ExpectedValue = @{ + HideSyncButtonOnDocLib = $WantedState + } + Set-CIPPStandardsCompareField -FieldName 'standards.SPSyncButtonState' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -Tenant $Tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeAttachmentPolicy.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeAttachmentPolicy.ps1 index 6acbaaeca1c6..1627aaceaa5d 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeAttachmentPolicy.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeAttachmentPolicy.ps1 @@ -76,10 +76,9 @@ function Invoke-CIPPStandardSafeAttachmentPolicy { try { $CurrentState = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-SafeAttachmentPolicy' | - Where-Object -Property Name -EQ $PolicyName | - Select-Object Name, Enable, Action, QuarantineTag, Redirect, RedirectAddress - } - catch { + Where-Object -Property Name -EQ $PolicyName | + Select-Object Name, Enable, Action, QuarantineTag, Redirect, RedirectAddress + } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the SafeAttachmentPolicy state for $Tenant. Error: $ErrorMessage" -Sev Error return @@ -95,8 +94,8 @@ function Invoke-CIPPStandardSafeAttachmentPolicy { $AcceptedDomains = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-AcceptedDomain' $RuleState = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-SafeAttachmentRule' | - 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 @@ -177,12 +176,25 @@ function Invoke-CIPPStandardSafeAttachmentPolicy { if ($Settings.report -eq $true) { Add-CIPPBPAField -FieldName 'SafeAttachmentPolicy' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $tenant - if ($StateIsCorrect) { - $FieldValue = $true - } else { - $FieldValue = $CurrentState + + $CurrentValue = @{ + name = $CurrentState.Name + enable = $CurrentState.Enable + action = $CurrentState.Action + quarantineTag = $CurrentState.QuarantineTag + redirect = $CurrentState.Redirect + redirectAddress = $CurrentState.RedirectAddress + } + + $ExpectedValue = @{ + name = $PolicyName + enable = $true + action = $Settings.SafeAttachmentAction + quarantineTag = $Settings.QuarantineTag + redirect = $Settings.Redirect + redirectAddress = $Settings.RedirectAddress } - Set-CIPPStandardsCompareField -FieldName 'standards.SafeAttachmentPolicy' -FieldValue $FieldValue -Tenant $Tenant + Set-CIPPStandardsCompareField -FieldName 'standards.SafeAttachmentPolicy' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -Tenant $Tenant } } else { if ($Settings.remediate -eq $true) { diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeLinksPolicy.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeLinksPolicy.ps1 index 1927d31de13d..bd7fadbfbbb4 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeLinksPolicy.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeLinksPolicy.ps1 @@ -75,10 +75,9 @@ function Invoke-CIPPStandardSafeLinksPolicy { try { $CurrentState = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-SafeLinksPolicy' | - Where-Object -Property Name -EQ $PolicyName | - Select-Object Name, EnableSafeLinksForEmail, EnableSafeLinksForTeams, EnableSafeLinksForOffice, TrackClicks, AllowClickThrough, ScanUrls, EnableForInternalSenders, DeliverMessageAfterScan, DisableUrlRewrite, EnableOrganizationBranding, DoNotRewriteUrls - } - catch { + Where-Object -Property Name -EQ $PolicyName | + Select-Object Name, EnableSafeLinksForEmail, EnableSafeLinksForTeams, EnableSafeLinksForOffice, TrackClicks, AllowClickThrough, ScanUrls, EnableForInternalSenders, DeliverMessageAfterScan, DisableUrlRewrite, EnableOrganizationBranding, DoNotRewriteUrls + } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the SafeLinksPolicy state for $Tenant. Error: $ErrorMessage" -Sev Error return @@ -188,12 +187,36 @@ function Invoke-CIPPStandardSafeLinksPolicy { if ($Settings.report -eq $true) { Add-CIPPBPAField -FieldName 'SafeLinksPolicy' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $tenant - if ($StateIsCorrect) { - $FieldValue = $true - } else { - $FieldValue = $CurrentState + + $CurrentValue = @{ + Name = $CurrentState.Name + EnableSafeLinksForEmail = $CurrentState.EnableSafeLinksForEmail + EnableSafeLinksForTeams = $CurrentState.EnableSafeLinksForTeams + EnableSafeLinksForOffice = $CurrentState.EnableSafeLinksForOffice + TrackClicks = $CurrentState.TrackClicks + AllowClickThrough = $CurrentState.AllowClickThrough + ScanUrls = $CurrentState.ScanUrls + EnableForInternalSenders = $CurrentState.EnableForInternalSenders + DeliverMessageAfterScan = $CurrentState.DeliverMessageAfterScan + DisableUrlRewrite = $CurrentState.DisableUrlRewrite + EnableOrganizationBranding = $CurrentState.EnableOrganizationBranding + DoNotRewriteUrls = $CurrentState.DoNotRewriteUrls + } + $ExpectedValue = @{ + Name = $PolicyName + EnableSafeLinksForEmail = $true + EnableSafeLinksForTeams = $true + EnableSafeLinksForOffice = $true + TrackClicks = $true + AllowClickThrough = $Settings.AllowClickThrough + ScanUrls = $true + EnableForInternalSenders = $true + DeliverMessageAfterScan = $true + DisableUrlRewrite = $Settings.DisableUrlRewrite + EnableOrganizationBranding = $Settings.EnableOrganizationBranding + DoNotRewriteUrls = $Settings.DoNotRewriteUrls.value ?? @() } - Set-CIPPStandardsCompareField -FieldName 'standards.SafeLinksPolicy' -FieldValue $FieldValue -Tenant $Tenant + Set-CIPPStandardsCompareField -FieldName 'standards.SafeLinksPolicy' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -Tenant $Tenant } } else { if ($Settings.remediate -eq $true) { diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeLinksTemplatePolicy.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeLinksTemplatePolicy.ps1 index a7d12eafa791..f3a91c2a43e6 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeLinksTemplatePolicy.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeLinksTemplatePolicy.ps1 @@ -48,7 +48,7 @@ function Invoke-CIPPStandardSafeLinksTemplatePolicy { # Normalize template list property $TemplateList = Get-NormalizedTemplateList -Settings $Settings if (-not $TemplateList) { - Write-LogMessage -API 'Standards' -tenant $Tenant -message "No templates selected for SafeLinks policy deployment" -sev Error + Write-LogMessage -API 'Standards' -tenant $Tenant -message 'No templates selected for SafeLinks policy deployment' -sev Error return } @@ -101,8 +101,7 @@ function Get-NormalizedTemplateList { if ($Settings.'standards.SafeLinksTemplatePolicy.TemplateIds') { return $Settings.'standards.SafeLinksTemplatePolicy.TemplateIds' - } - elseif ($Settings.TemplateIds) { + } elseif ($Settings.TemplateIds) { return $Settings.TemplateIds } @@ -134,17 +133,13 @@ function ConvertTo-SafeArray { foreach ($item in $Field) { if ($item -is [string]) { $ResultList.Add($item) - } - elseif ($item.value) { + } elseif ($item.value) { $ResultList.Add($item.value) - } - elseif ($item.userPrincipalName) { + } elseif ($item.userPrincipalName) { $ResultList.Add($item.userPrincipalName) - } - elseif ($item.id) { + } elseif ($item.id) { $ResultList.Add($item.id) - } - else { + } else { $ResultList.Add($item.ToString()) } } @@ -184,22 +179,20 @@ function Get-ExistingSafeLinksObjects { try { $ExistingPolicies = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-SafeLinksPolicy' -useSystemMailbox $true $PolicyExists = $ExistingPolicies | Where-Object { $_.Name -eq $PolicyName } - } - catch { + } catch { Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to retrieve existing policies: $($_.Exception.Message)" -sev Warning } try { $ExistingRules = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-SafeLinksRule' -useSystemMailbox $true $RuleExists = $ExistingRules | Where-Object { $_.Name -eq $RuleName } - } - catch { + } catch { Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to retrieve existing rules: $($_.Exception.Message)" -sev Warning } return @{ PolicyExists = $PolicyExists - RuleExists = $RuleExists + RuleExists = $RuleExists } } @@ -207,17 +200,17 @@ function New-SafeLinksPolicyParameters { param($Template) $PolicyMappings = @{ - 'EnableSafeLinksForEmail' = 'EnableSafeLinksForEmail' - 'EnableSafeLinksForTeams' = 'EnableSafeLinksForTeams' - 'EnableSafeLinksForOffice' = 'EnableSafeLinksForOffice' - 'TrackClicks' = 'TrackClicks' - 'AllowClickThrough' = 'AllowClickThrough' - 'ScanUrls' = 'ScanUrls' - 'EnableForInternalSenders' = 'EnableForInternalSenders' - 'DeliverMessageAfterScan' = 'DeliverMessageAfterScan' - 'DisableUrlRewrite' = 'DisableUrlRewrite' - 'AdminDisplayName' = 'AdminDisplayName' - 'CustomNotificationText' = 'CustomNotificationText' + 'EnableSafeLinksForEmail' = 'EnableSafeLinksForEmail' + 'EnableSafeLinksForTeams' = 'EnableSafeLinksForTeams' + 'EnableSafeLinksForOffice' = 'EnableSafeLinksForOffice' + 'TrackClicks' = 'TrackClicks' + 'AllowClickThrough' = 'AllowClickThrough' + 'ScanUrls' = 'ScanUrls' + 'EnableForInternalSenders' = 'EnableForInternalSenders' + 'DeliverMessageAfterScan' = 'DeliverMessageAfterScan' + 'DisableUrlRewrite' = 'DisableUrlRewrite' + 'AdminDisplayName' = 'AdminDisplayName' + 'CustomNotificationText' = 'CustomNotificationText' 'EnableOrganizationBranding' = 'EnableOrganizationBranding' } @@ -249,11 +242,11 @@ function New-SafeLinksRuleParameters { # Array-based rule parameters $ArrayMappings = @{ - 'SentTo' = ConvertTo-SafeArray -Field $Template.SentTo - 'SentToMemberOf' = ConvertTo-SafeArray -Field $Template.SentToMemberOf - 'RecipientDomainIs' = ConvertTo-SafeArray -Field $Template.RecipientDomainIs - 'ExceptIfSentTo' = ConvertTo-SafeArray -Field $Template.ExceptIfSentTo - 'ExceptIfSentToMemberOf' = ConvertTo-SafeArray -Field $Template.ExceptIfSentToMemberOf + 'SentTo' = ConvertTo-SafeArray -Field $Template.SentTo + 'SentToMemberOf' = ConvertTo-SafeArray -Field $Template.SentToMemberOf + 'RecipientDomainIs' = ConvertTo-SafeArray -Field $Template.RecipientDomainIs + 'ExceptIfSentTo' = ConvertTo-SafeArray -Field $Template.ExceptIfSentTo + 'ExceptIfSentToMemberOf' = ConvertTo-SafeArray -Field $Template.ExceptIfSentToMemberOf 'ExceptIfRecipientDomainIs' = ConvertTo-SafeArray -Field $Template.ExceptIfRecipientDomainIs } @@ -272,8 +265,8 @@ function Set-SafeLinksRuleState { if ($null -eq $State) { return } $IsEnabled = switch ($State) { - "Enabled" { $true } - "Disabled" { $false } + 'Enabled' { $true } + 'Disabled' { $false } $true { $true } $false { $false } default { $null } @@ -282,7 +275,7 @@ function Set-SafeLinksRuleState { if ($null -ne $IsEnabled) { $Cmdlet = $IsEnabled ? 'Enable-SafeLinksRule' : 'Disable-SafeLinksRule' $null = New-ExoRequest -tenantid $Tenant -cmdlet $Cmdlet -cmdParams @{ Identity = $RuleName } -useSystemMailbox $true - return $IsEnabled ? "enabled" : "disabled" + return $IsEnabled ? 'enabled' : 'disabled' } return $null @@ -320,8 +313,7 @@ function Invoke-SafeLinksRemediation { $null = New-ExoRequest -tenantid $Tenant -cmdlet 'Set-SafeLinksPolicy' -cmdParams $PolicyParams -useSystemMailbox $true $ActionsTaken.Add("Updated SafeLinks policy '$PolicyName'") Write-LogMessage -API 'Standards' -tenant $Tenant -message "Updated SafeLinks policy '$PolicyName'" -sev Info - } - else { + } else { # Create new policy $PolicyParams['Name'] = $PolicyName $null = New-ExoRequest -tenantid $Tenant -cmdlet 'New-SafeLinksPolicy' -cmdParams $PolicyParams -useSystemMailbox $true @@ -338,8 +330,7 @@ function Invoke-SafeLinksRemediation { $null = New-ExoRequest -tenantid $Tenant -cmdlet 'Set-SafeLinksRule' -cmdParams $RuleParams -useSystemMailbox $true $ActionsTaken.Add("Updated SafeLinks rule '$RuleName'") Write-LogMessage -API 'Standards' -tenant $Tenant -message "Updated SafeLinks rule '$RuleName'" -sev Info - } - else { + } else { # Create new rule $RuleParams['Name'] = $RuleName $RuleParams['SafeLinksPolicy'] = $PolicyName @@ -356,21 +347,20 @@ function Invoke-SafeLinksRemediation { } $TemplateResults[$TemplateId] = @{ - Success = $true + Success = $true ActionsTaken = $ActionsTaken.ToArray() TemplateName = $Template.TemplateName ?? $Template.Name - PolicyName = $PolicyName - RuleName = $RuleName + PolicyName = $PolicyName + RuleName = $RuleName } Write-LogMessage -API 'Standards' -tenant $Tenant -message "Successfully applied SafeLinks template '$($Template.TemplateName ?? $Template.Name)'" -sev Info - } - catch { + } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message $TemplateResults[$TemplateId] = @{ - Success = $false - Message = $ErrorMessage - TemplateName = $Template.TemplateName ?? $Template.Name ?? "Unknown" + Success = $false + Message = $ErrorMessage + TemplateName = $Template.TemplateName ?? $Template.Name ?? 'Unknown' } $OverallSuccess = $false @@ -380,9 +370,8 @@ function Invoke-SafeLinksRemediation { # Report overall results if ($OverallSuccess) { - Write-LogMessage -API 'Standards' -tenant $Tenant -message "Successfully applied all SafeLinks templates" -sev Info - } - else { + Write-LogMessage -API 'Standards' -tenant $Tenant -message 'Successfully applied all SafeLinks templates' -sev Info + } else { $SuccessCount = ($TemplateResults.Values | Where-Object { $_.Success -eq $true }).Count $TotalCount = $TemplateList.Count Write-LogMessage -API 'Standards' -tenant $Tenant -message "Applied $SuccessCount out of $TotalCount SafeLinks templates" -sev Info @@ -419,8 +408,7 @@ function Invoke-SafeLinksAlert { $AlertMessages.Add($Status) } - } - catch { + } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message $AlertMessages.Add("Failed to check template with ID $TemplateId : $ErrorMessage") $AllTemplatesApplied = $false @@ -428,13 +416,12 @@ function Invoke-SafeLinksAlert { } if ($AllTemplatesApplied) { - Write-LogMessage -API 'Standards' -tenant $Tenant -message "All SafeLinks templates are correctly applied" -sev Info - } - else { - $AlertMessage = "One or more SafeLinks templates are not correctly applied: " + ($AlertMessages.ToArray() -join " | ") + Write-LogMessage -API 'Standards' -tenant $Tenant -message 'All SafeLinks templates are correctly applied' -sev Info + } else { + $AlertMessage = 'One or more SafeLinks templates are not correctly applied: ' + ($AlertMessages.ToArray() -join ' | ') Write-StandardsAlert -message $AlertMessage -object @{ Templates = $TemplateList - Issues = $AlertMessages.ToArray() + Issues = $AlertMessages.ToArray() } -tenant $Tenant -standardName 'SafeLinksTemplatePolicy' -standardId $Settings.standardId Write-LogMessage -API 'Standards' -tenant $Tenant -message $AlertMessage -sev Info @@ -458,19 +445,18 @@ function Invoke-SafeLinksReport { $ExistingObjects = Get-ExistingSafeLinksObjects -Tenant $Tenant -PolicyName $PolicyName -RuleName $RuleName $ReportResults[$TemplateId] = @{ - Success = ($ExistingObjects.PolicyExists -and $ExistingObjects.RuleExists) + Success = ($ExistingObjects.PolicyExists -and $ExistingObjects.RuleExists) TemplateName = $Template.TemplateName ?? $Template.Name - PolicyName = $PolicyName - RuleName = $RuleName + PolicyName = $PolicyName + RuleName = $RuleName PolicyExists = [bool]$ExistingObjects.PolicyExists - RuleExists = [bool]$ExistingObjects.RuleExists + RuleExists = [bool]$ExistingObjects.RuleExists } if (-not $ExistingObjects.PolicyExists -or -not $ExistingObjects.RuleExists) { $AllTemplatesApplied = $false } - } - catch { + } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message $ReportResults[$TemplateId] = @{ Success = $false @@ -482,14 +468,18 @@ function Invoke-SafeLinksReport { Add-CIPPBPAField -FieldName 'SafeLinksTemplatePolicy' -FieldValue $AllTemplatesApplied -StoreAs bool -Tenant $Tenant - if ($AllTemplatesApplied) { - Set-CIPPStandardsCompareField -FieldName 'standards.SafeLinksTemplatePolicy' -FieldValue $true -Tenant $Tenant + $CurrentValue = @{ + TemplateResults = $ReportResults + ProcessedTemplates = $TemplateList.Count + SuccessfulTemplates = ($ReportResults.Values | Where-Object { $_.Success -eq $true }).Count + AllTemplatesApplied = $AllTemplatesApplied } - else { - Set-CIPPStandardsCompareField -FieldName 'standards.SafeLinksTemplatePolicy' -FieldValue @{ - TemplateResults = $ReportResults - ProcessedTemplates = $TemplateList.Count - SuccessfulTemplates = ($ReportResults.Values | Where-Object { $_.Success -eq $true }).Count - } -Tenant $Tenant + $ExpectedValue = @{ + TemplateResults = $ReportResults + ProcessedTemplates = $TemplateList.Count + SuccessfulTemplates = $TemplateList.Count + AllTemplatesApplied = $true } + + Set-CIPPStandardsCompareField -FieldName 'standards.SafeLinksTemplatePolicy' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -Tenant $Tenant } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeSendersDisable.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeSendersDisable.ps1 index 74f06f4fa3fa..b879e3250c8d 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeSendersDisable.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeSendersDisable.ps1 @@ -72,7 +72,13 @@ function Invoke-CIPPStandardSafeSendersDisable { if ($Settings.report -eq $true) { #This script always returns true, as it only disables the Safe Senders list - Set-CIPPStandardsCompareField -FieldName 'standards.SafeSendersDisable' -FieldValue $true -Tenant $Tenant + $CurrentValue = @{ + SafeSendersDisabled = $true + } + $ExpectedValue = @{ + SafeSendersDisabled = $true + } + Set-CIPPStandardsCompareField -FieldName 'standards.SafeSendersDisable' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -Tenant $Tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSecureScoreRemediation.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSecureScoreRemediation.ps1 index 59d994b42f30..91e18f80dc42 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSecureScoreRemediation.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSecureScoreRemediation.ps1 @@ -185,7 +185,14 @@ function Invoke-CIPPStandardSecureScoreRemediation { $ReportData = $true } - Set-CIPPStandardsCompareField -FieldName 'standards.SecureScoreRemediation' -FieldValue $ReportData -Tenant $tenant + $CurrentValue = @{ + ControlsToUpdate = $ReportData + } + $ExpectedValue = @{ + ControlsToUpdate = @() + } + + Set-CIPPStandardsCompareField -FieldName 'standards.SecureScoreRemediation' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -Tenant $tenant Add-CIPPBPAField -FieldName 'SecureScoreRemediation' -FieldValue $ReportData -StoreAs json -Tenant $tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSecurityDefaults.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSecurityDefaults.ps1 index 6cda698e6143..bc5c73b053b5 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSecurityDefaults.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSecurityDefaults.ps1 @@ -34,8 +34,7 @@ function Invoke-CIPPStandardSecurityDefaults { try { $SecureDefaultsState = (New-GraphGetRequest -Uri 'https://graph.microsoft.com/beta/policies/identitySecurityDefaultsEnforcementPolicy' -tenantid $tenant) - } - catch { + } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the Security Defaults state for $Tenant. Error: $ErrorMessage" -Sev Error return @@ -70,6 +69,12 @@ function Invoke-CIPPStandardSecurityDefaults { if ($Settings.report -eq $true) { Add-CIPPBPAField -FieldName 'SecurityDefaults' -FieldValue $SecureDefaultsState.IsEnabled -StoreAs bool -Tenant $tenant - Set-CIPPStandardsCompareField -FieldName 'standards.SecurityDefaults' -FieldValue $SecureDefaultsState.IsEnabled -Tenant $tenant + $CurrentData = @{ + SecurityDefaultsEnabled = $SecureDefaultsState.IsEnabled + } + $ExpectedData = @{ + SecurityDefaultsEnabled = $true + } + Set-CIPPStandardsCompareField -FieldName 'standards.SecurityDefaults' -CurrentValue $CurrentData -ExpectedValue $ExpectedData -Tenant $tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSendFromAlias.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSendFromAlias.ps1 index 9aa1e244c5c7..4f2d624577b9 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSendFromAlias.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSendFromAlias.ps1 @@ -40,8 +40,7 @@ function Invoke-CIPPStandardSendFromAlias { try { $CurrentInfo = (New-ExoRequest -tenantid $Tenant -cmdlet 'Get-OrganizationConfig').SendFromAliasEnabled - } - catch { + } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the SendFromAlias state for $Tenant. Error: $ErrorMessage" -Sev Error return @@ -73,6 +72,12 @@ function Invoke-CIPPStandardSendFromAlias { if ($Settings.report -eq $true) { Add-CIPPBPAField -FieldName 'SendFromAlias' -FieldValue $CurrentInfo -StoreAs bool -Tenant $tenant - Set-CIPPStandardsCompareField -FieldName 'standards.SendFromAlias' -FieldValue $CurrentInfo -Tenant $tenant + $CurrentValue = @{ + SendFromAliasEnabled = $CurrentInfo + } + $ExpectedValue = @{ + SendFromAliasEnabled = $true + } + Set-CIPPStandardsCompareField -FieldName 'standards.SendFromAlias' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -Tenant $tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSendReceiveLimitTenant.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSendReceiveLimitTenant.ps1 index 1ea8f1ff543f..be9fe0909549 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSendReceiveLimitTenant.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSendReceiveLimitTenant.ps1 @@ -109,11 +109,14 @@ function Invoke-CIPPStandardSendReceiveLimitTenant { if ($Settings.report -eq $true) { Add-CIPPBPAField -FieldName 'SendReceiveLimit' -FieldValue $NotSetCorrectly -StoreAs json -Tenant $tenant - if ($NotSetCorrectly.Count -eq 0) { - $FieldValue = $true - } else { - $FieldValue = $NotSetCorrectly + $CurrentValue = @{ + SendLimit = $Settings.SendLimit + ReceiveLimit = $Settings.ReceiveLimit + } + $ExpectedValue = @{ + SendLimit = $Settings.SendLimit + ReceiveLimit = $Settings.ReceiveLimit } - Set-CIPPStandardsCompareField -FieldName 'standards.SendReceiveLimitTenant' -FieldValue $FieldValue -Tenant $tenant + Set-CIPPStandardsCompareField -FieldName 'standards.SendReceiveLimitTenant' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -Tenant $tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSharePointMassDeletionAlert.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSharePointMassDeletionAlert.ps1 index 951c6e00b437..4891f990cf02 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSharePointMassDeletionAlert.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSharePointMassDeletionAlert.ps1 @@ -44,10 +44,9 @@ function Invoke-CIPPStandardSharePointMassDeletionAlert { try { $CurrentState = New-ExoRequest -TenantId $Tenant -cmdlet 'Get-ProtectionAlert' -Compliance | - Where-Object { $_.Name -eq $PolicyName } | - Select-Object -Property * - } - catch { + Where-Object { $_.Name -eq $PolicyName } | + Select-Object -Property * + } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the sharingCapability state for $Tenant. Error: $ErrorMessage" -Sev Error return @@ -115,8 +114,17 @@ function Invoke-CIPPStandardSharePointMassDeletionAlert { } if ($Settings.report -eq $true) { - $FieldValue = $StateIsCorrect ? $true : $CompareField - Set-CIPPStandardsCompareField -FieldName 'standards.SharePointMassDeletionAlert' -FieldValue $FieldValue -TenantFilter $Tenant + $CurrentValue = @{ + Threshold = $CurrentState.Threshold + TimeWindow = $CurrentState.TimeWindow + NotifyUser = @($CurrentState.NotifyUser) + } + $ExpectedValue = @{ + Threshold = $Settings.Threshold + TimeWindow = $Settings.TimeWindow + NotifyUser = @($Settings.NotifyUser.value) + } + Set-CIPPStandardsCompareField -FieldName 'standards.SharePointMassDeletionAlert' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant Add-CIPPBPAField -FieldName 'SharePointMassDeletionAlert' -FieldValue [bool]$StateIsCorrect -StoreAs bool -Tenant $Tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardShortenMeetings.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardShortenMeetings.ps1 index c0a703565124..b05f3abb32db 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardShortenMeetings.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardShortenMeetings.ps1 @@ -45,9 +45,8 @@ function Invoke-CIPPStandardShortenMeetings { try { $CurrentState = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-OrganizationConfig' | - Select-Object -Property ShortenEventScopeDefault, DefaultMinutesToReduceShortEventsBy, DefaultMinutesToReduceLongEventsBy - } - catch { + Select-Object -Property ShortenEventScopeDefault, DefaultMinutesToReduceShortEventsBy, DefaultMinutesToReduceLongEventsBy + } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the ShortenMeetings state for $Tenant. Error: $ErrorMessage" -Sev Error return @@ -96,11 +95,16 @@ function Invoke-CIPPStandardShortenMeetings { Add-CIPPBPAField @BPAField -StoreAs json } - if ($CorrectState -eq $true) { - $FieldValue = $true - } else { - $FieldValue = $CurrentState + $CurrentValue = @{ + ShortenEventScopeDefault = $CurrentState.ShortenEventScopeDefault + DefaultMinutesToReduceShortEventsBy = $CurrentState.DefaultMinutesToReduceShortEventsBy + DefaultMinutesToReduceLongEventsBy = $CurrentState.DefaultMinutesToReduceLongEventsBy + } + $ExpectedValue = @{ + ShortenEventScopeDefault = $scopeDefault + DefaultMinutesToReduceShortEventsBy = $Settings.DefaultMinutesToReduceShortEventsBy + DefaultMinutesToReduceLongEventsBy = $Settings.DefaultMinutesToReduceLongEventsBy } - Set-CIPPStandardsCompareField -FieldName 'standards.ShortenMeetings' -FieldValue $FieldValue -Tenant $Tenant + Set-CIPPStandardsCompareField -FieldName 'standards.ShortenMeetings' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -Tenant $Tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSpamFilterPolicy.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSpamFilterPolicy.ps1 index 68322ddf5b2a..5a9fd298f25c 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSpamFilterPolicy.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSpamFilterPolicy.ps1 @@ -64,10 +64,9 @@ function Invoke-CIPPStandardSpamFilterPolicy { try { $CurrentState = New-ExoRequest -TenantId $Tenant -cmdlet 'Get-HostedContentFilterPolicy' | - Where-Object -Property Name -EQ $PolicyName | - Select-Object -Property * - } - catch { + Where-Object -Property Name -EQ $PolicyName | + Select-Object -Property * + } 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 return @@ -130,8 +129,7 @@ function Invoke-CIPPStandardSpamFilterPolicy { ($CurrentState.EnableRegionBlockList -eq $Settings.EnableRegionBlockList) -and ((($null -eq $CurrentState.RegionBlockList -or $CurrentState.RegionBlockList.Count -eq 0) -and ($null -eq $Settings.RegionBlockList.value)) -or ($null -ne $CurrentState.RegionBlockList -and $CurrentState.RegionBlockList.Count -gt 0 -and $null -ne $Settings.RegionBlockList.value -and !(Compare-Object -ReferenceObject $CurrentState.RegionBlockList -DifferenceObject $Settings.RegionBlockList.value))) -and ((($null -eq $CurrentState.AllowedSenderDomains -or $CurrentState.AllowedSenderDomains.Count -eq 0) -and ($null -eq ($Settings.AllowedSenderDomains.value ?? $Settings.AllowedSenderDomains))) -or ($null -ne $CurrentState.AllowedSenderDomains -and $CurrentState.AllowedSenderDomains.Count -gt 0 -and $null -ne ($Settings.AllowedSenderDomains.value ?? $Settings.AllowedSenderDomains) -and !(Compare-Object -ReferenceObject $CurrentState.AllowedSenderDomains -DifferenceObject ($Settings.AllowedSenderDomains.value ?? $Settings.AllowedSenderDomains)))) - } - catch { + } catch { $StateIsCorrect = $false } @@ -260,11 +258,58 @@ function Invoke-CIPPStandardSpamFilterPolicy { if ($Settings.report -eq $true) { Add-CIPPBPAField -FieldName 'SpamFilterPolicy' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $Tenant - if ($StateIsCorrect) { - $FieldValue = $true - } else { - $FieldValue = $StateIsCorrect -eq $true ? $true : ($CurrentState ?? @{ state = 'Spam filter policy not found' }) + $CurrentValue = @{ + Name = $CurrentState.Name + SpamAction = $CurrentState.SpamAction + SpamQuarantineTag = $CurrentState.SpamQuarantineTag + HighConfidenceSpamAction = $CurrentState.HighConfidenceSpamAction + HighConfidenceSpamQuarantineTag = $CurrentState.HighConfidenceSpamQuarantineTag + BulkSpamAction = $CurrentState.BulkSpamAction + BulkQuarantineTag = $CurrentState.BulkQuarantineTag + PhishSpamAction = $CurrentState.PhishSpamAction + PhishQuarantineTag = $CurrentState.PhishQuarantineTag + HighConfidencePhishQuarantineTag = $CurrentState.HighConfidencePhishQuarantineTag + BulkThreshold = $CurrentState.BulkThreshold + IncreaseScoreWithImageLinks = $CurrentState.IncreaseScoreWithImageLinks + IncreaseScoreWithBizOrInfoUrls = $CurrentState.IncreaseScoreWithBizOrInfoUrls + MarkAsSpamFramesInHtml = $CurrentState.MarkAsSpamFramesInHtml + MarkAsSpamObjectTagsInHtml = $CurrentState.MarkAsSpamObjectTagsInHtml + MarkAsSpamEmbedTagsInHtml = $CurrentState.MarkAsSpamEmbedTagsInHtml + MarkAsSpamFormTagsInHtml = $CurrentState.MarkAsSpamFormTagsInHtml + MarkAsSpamWebBugsInHtml = $CurrentState.MarkAsSpamWebBugsInHtml + MarkAsSpamSensitiveWordList = $CurrentState.MarkAsSpamSensitiveWordList + EnableLanguageBlockList = $CurrentState.EnableLanguageBlockList + LanguageBlockList = $CurrentState.LanguageBlockList + EnableRegionBlockList = $CurrentState.EnableRegionBlockList + RegionBlockList = $CurrentState.RegionBlockList + AllowedSenderDomains = $CurrentState.AllowedSenderDomains + } + $ExpectedValue = @{ + Name = $PolicyName + SpamAction = $SpamAction + SpamQuarantineTag = $SpamQuarantineTag + HighConfidenceSpamAction = $HighConfidenceSpamAction + HighConfidenceSpamQuarantineTag = $HighConfidenceSpamQuarantineTag + BulkSpamAction = $BulkSpamAction + BulkQuarantineTag = $BulkQuarantineTag + PhishSpamAction = $PhishSpamAction + PhishQuarantineTag = $PhishQuarantineTag + HighConfidencePhishQuarantineTag = $HighConfidencePhishQuarantineTag + BulkThreshold = [int]$Settings.BulkThreshold + IncreaseScoreWithImageLinks = $IncreaseScoreWithImageLinks + IncreaseScoreWithBizOrInfoUrls = $IncreaseScoreWithBizOrInfoUrls + MarkAsSpamFramesInHtml = $MarkAsSpamFramesInHtml + MarkAsSpamObjectTagsInHtml = $MarkAsSpamObjectTagsInHtml + MarkAsSpamEmbedTagsInHtml = $MarkAsSpamEmbedTagsInHtml + MarkAsSpamFormTagsInHtml = $MarkAsSpamFormTagsInHtml + MarkAsSpamWebBugsInHtml = $MarkAsSpamWebBugsInHtml + MarkAsSpamSensitiveWordList = $MarkAsSpamSensitiveWordList + EnableLanguageBlockList = $Settings.EnableLanguageBlockList + LanguageBlockList = if ($Settings.EnableLanguageBlockList -eq $true) { $Settings.LanguageBlockList.value } else { @() } + EnableRegionBlockList = $Settings.EnableRegionBlockList + RegionBlockList = if ($Settings.EnableRegionBlockList -eq $true) { $Settings.RegionBlockList.value } else { @() } + AllowedSenderDomains = $Settings.AllowedSenderDomains.value ?? @() } - Set-CIPPStandardsCompareField -FieldName 'standards.SpamFilterPolicy' -FieldValue $FieldValue -Tenant $Tenant + Set-CIPPStandardsCompareField -FieldName 'standards.SpamFilterPolicy' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -Tenant $Tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSpoofWarn.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSpoofWarn.ps1 index 11676a8f2146..c07e9e96fb3f 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSpoofWarn.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSpoofWarn.ps1 @@ -132,11 +132,16 @@ function Invoke-CIPPStandardSpoofWarn { if ($Settings.report -eq $true) { Add-CIPPBPAField -FieldName 'SpoofingWarnings' -FieldValue $CurrentInfo.Enabled -StoreAs bool -Tenant $Tenant - if ($AllowListCorrect -eq $true -and $CurrentInfo.Enabled -eq $IsEnabled) { - $FieldValue = $true - } else { - $FieldValue = $CurrentInfo | Select-Object Enabled, AllowList + $CurrentValue = @{ + Enabled = $CurrentInfo.Enabled + AllowList = $CurrentInfo.AllowList + IsCompliant = $CurrentInfo.Enabled -eq $IsEnabled -and $AllowListCorrect + } + $ExpectedValue = @{ + Enabled = $IsEnabled + AllowList = $Settings.AllowListAdd.value ?? $Settings.AllowListAdd + IsCompliant = $true } - Set-CIPPStandardsCompareField -FieldName 'standards.SpoofWarn' -FieldValue $FieldValue -Tenant $Tenant + Set-CIPPStandardsCompareField -FieldName 'standards.SpoofWarn' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -Tenant $Tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardStaleEntraDevices.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardStaleEntraDevices.ps1 index 8d95d769d8f1..87cd2d6b39e4 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardStaleEntraDevices.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardStaleEntraDevices.ps1 @@ -47,8 +47,7 @@ function Invoke-CIPPStandardStaleEntraDevices { try { $AllDevices = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/devices' -tenantid $Tenant | Where-Object { $null -ne $_.approximateLastSignInDateTime } - } - catch { + } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the StaleEntraDevices state for $Tenant. Error: $ErrorMessage" -Sev Error return @@ -98,6 +97,16 @@ function Invoke-CIPPStandardStaleEntraDevices { } else { $FieldValue = $true } - Set-CIPPStandardsCompareField -FieldName 'standards.StaleEntraDevices' -FieldValue $FieldValue -Tenant $Tenant + $CurrentValue = @{ + StaleDevicesCount = $StaleDevices.Count + StaleDevices = @($FieldValue) + DeviceAgeThreshold = [int]$Settings.deviceAgeThreshold + } + $ExpectedValue = @{ + StaleDevicesCount = 0 + StaleDevices = @() + DeviceAgeThreshold = [int]$Settings.deviceAgeThreshold + } + Set-CIPPStandardsCompareField -FieldName 'standards.StaleEntraDevices' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -Tenant $Tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTAP.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTAP.ps1 index 696ecfec1802..1d5dff424012 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTAP.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTAP.ps1 @@ -32,12 +32,10 @@ function Invoke-CIPPStandardTAP { #> param($Tenant, $Settings) - ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'TAP' try { $CurrentState = New-GraphGetRequest -Uri 'https://graph.microsoft.com/beta/policies/authenticationmethodspolicy/authenticationMethodConfigurations/TemporaryAccessPass' -tenantid $Tenant - } - catch { + } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the TAP state for $Tenant. Error: $ErrorMessage" -Sev Error return @@ -74,11 +72,14 @@ function Invoke-CIPPStandardTAP { if ($Settings.report -eq $true) { Add-CIPPBPAField -FieldName 'TemporaryAccessPass' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $Tenant - if ($StateIsCorrect) { - $FieldValue = $true - } else { - $FieldValue = $CurrentState | Select-Object state, isUsableOnce + $CurrentValue = @{ + state = $CurrentState.state + isUsableOnce = $CurrentState.isUsableOnce + } + $ExpectedValue = @{ + state = 'enabled' + isUsableOnce = [System.Convert]::ToBoolean($config) } - Set-CIPPStandardsCompareField -FieldName 'standards.TAP' -FieldValue $FieldValue -Tenant $Tenant + Set-CIPPStandardsCompareField -FieldName 'standards.TAP' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -Tenant $Tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsChatProtection.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsChatProtection.ps1 index 08c2a813cce6..06bd0ed34f07 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsChatProtection.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsChatProtection.ps1 @@ -91,11 +91,14 @@ function Invoke-CIPPStandardTeamsChatProtection { if ($Settings.report -eq $true) { Add-CIPPBPAField -FieldName 'TeamsChatProtection' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $Tenant - if ($StateIsCorrect) { - $FieldValue = $true - } else { - $FieldValue = $CurrentState + $CurrentValue = @{ + FileTypeCheck = $CurrentState.FileTypeCheck + UrlReputationCheck = $CurrentState.UrlReputationCheck + } + $ExpectedValue = @{ + FileTypeCheck = $FileTypeCheckState + UrlReputationCheck = $UrlReputationCheckState } - Set-CIPPStandardsCompareField -FieldName 'standards.TeamsChatProtection' -FieldValue $FieldValue -Tenant $Tenant + Set-CIPPStandardsCompareField -FieldName 'standards.TeamsChatProtection' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -Tenant $Tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsEmailIntegration.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsEmailIntegration.ps1 index eb182da573dc..6cf019c7353e 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsEmailIntegration.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsEmailIntegration.ps1 @@ -1,4 +1,4 @@ -Function Invoke-CIPPStandardTeamsEmailIntegration { +function Invoke-CIPPStandardTeamsEmailIntegration { <# .FUNCTIONALITY Internal @@ -33,8 +33,7 @@ Function Invoke-CIPPStandardTeamsEmailIntegration { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'TeamsEmailIntegration' -TenantFilter $Tenant -RequiredCapabilities @('MCOSTANDARD', 'MCOEV', 'MCOIMP', 'TEAMS1','Teams_Room_Standard') - ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'TeamsEmailIntegration' + $TestResult = Test-CIPPStandardLicense -StandardName 'TeamsEmailIntegration' -TenantFilter $Tenant -RequiredCapabilities @('MCOSTANDARD', 'MCOEV', 'MCOIMP', 'TEAMS1', 'Teams_Room_Standard') if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." @@ -43,9 +42,8 @@ Function Invoke-CIPPStandardTeamsEmailIntegration { try { $CurrentState = New-TeamsRequest -TenantFilter $Tenant -Cmdlet 'Get-CsTeamsClientConfiguration' -CmdParams @{Identity = 'Global' } | - Select-Object AllowEmailIntoChannel - } - catch { + Select-Object AllowEmailIntoChannel + } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the TeamsEmailIntegration state for $Tenant. Error: $ErrorMessage" -Sev Error return @@ -85,12 +83,13 @@ Function Invoke-CIPPStandardTeamsEmailIntegration { if ($Settings.report -eq $true) { - if ($StateIsCorrect) { - $FieldValue = $true - } else { - $FieldValue = $CurrentState + $CurrentValue = @{ + AllowEmailIntoChannel = $CurrentState.AllowEmailIntoChannel + } + $ExpectedValue = @{ + AllowEmailIntoChannel = $AllowEmailIntoChannel } - Set-CIPPStandardsCompareField -FieldName 'standards.TeamsEmailIntegration' -FieldValue $FieldValue -Tenant $Tenant + Set-CIPPStandardsCompareField -FieldName 'standards.TeamsEmailIntegration' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -Tenant $Tenant Add-CIPPBPAField -FieldName 'TeamsEmailIntoChannel' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $Tenant } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsEnrollUser.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsEnrollUser.ps1 index 1d0aa58d7626..ac908df686cb 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsEnrollUser.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsEnrollUser.ps1 @@ -31,7 +31,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 -RequiredCapabilities @('MCOSTANDARD', 'MCOEV', 'MCOIMP', 'TEAMS1', 'Teams_Room_Standard') # Get EnrollUserOverride value using null-coalescing operator @@ -43,9 +43,8 @@ function Invoke-CIPPStandardTeamsEnrollUser { try { $CurrentState = New-TeamsRequest -TenantFilter $Tenant -Cmdlet 'Get-CsTeamsMeetingPolicy' -cmdParams @{Identity = 'Global' } | - Select-Object EnrollUserOverride - } - catch { + Select-Object EnrollUserOverride + } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the TeamsEnrollUser state for $Tenant. Error: $ErrorMessage" -Sev Error return @@ -84,11 +83,12 @@ function Invoke-CIPPStandardTeamsEnrollUser { if ($Settings.report -eq $true) { Add-CIPPBPAField -FieldName 'TeamsEnrollUser' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $Tenant - if ($StateIsCorrect) { - $FieldValue = $true - } else { - $FieldValue = $CurrentState + $CurrentValue = @{ + EnrollUserOverride = $CurrentState.EnrollUserOverride + } + $ExpectedValue = @{ + EnrollUserOverride = $enrollUserOverride } - Set-CIPPStandardsCompareField -FieldName 'standards.TeamsEnrollUser' -FieldValue $FieldValue -Tenant $Tenant + Set-CIPPStandardsCompareField -FieldName 'standards.TeamsEnrollUser' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -Tenant $Tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsExternalAccessPolicy.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsExternalAccessPolicy.ps1 index fe93267775aa..3b09e9240683 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsExternalAccessPolicy.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsExternalAccessPolicy.ps1 @@ -32,8 +32,7 @@ function Invoke-CIPPStandardTeamsExternalAccessPolicy { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'TeamsExternalAccessPolicy' -TenantFilter $Tenant -RequiredCapabilities @('MCOSTANDARD', 'MCOEV', 'MCOIMP', 'TEAMS1','Teams_Room_Standard') - ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'TeamsExternalAccessPolicy' + $TestResult = Test-CIPPStandardLicense -StandardName 'TeamsExternalAccessPolicy' -TenantFilter $Tenant -RequiredCapabilities @('MCOSTANDARD', 'MCOEV', 'MCOIMP', 'TEAMS1', 'Teams_Room_Standard') if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." @@ -42,9 +41,8 @@ function Invoke-CIPPStandardTeamsExternalAccessPolicy { try { $CurrentState = New-TeamsRequest -TenantFilter $Tenant -Cmdlet 'Get-CsExternalAccessPolicy' -CmdParams @{Identity = 'Global' } | - Select-Object * - } - catch { + Select-Object * + } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the TeamsExternalAccessPolicy state for $Tenant. Error: $ErrorMessage" -Sev Error return @@ -88,12 +86,14 @@ function Invoke-CIPPStandardTeamsExternalAccessPolicy { if ($Settings.report -eq $true) { Add-CIPPBPAField -FieldName 'TeamsExternalAccessPolicy' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $Tenant - if ($StateIsCorrect -eq $true) { - $FieldValue = $true - } else { - $FieldValue = $CurrentState | Select-Object EnableFederationAccess, EnableTeamsConsumerAccess + $CurrentValue = @{ + EnableFederationAccess = $CurrentState.EnableFederationAccess + EnableTeamsConsumerAccess = $CurrentState.EnableTeamsConsumerAccess } - - Set-CIPPStandardsCompareField -FieldName 'standards.TeamsExternalAccessPolicy' -FieldValue $FieldValue -Tenant $Tenant + $ExpectedValue = @{ + EnableFederationAccess = $EnableFederationAccess + EnableTeamsConsumerAccess = $EnableTeamsConsumerAccess + } + Set-CIPPStandardsCompareField -FieldName 'standards.TeamsExternalAccessPolicy' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -Tenant $Tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsExternalChatWithAnyone.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsExternalChatWithAnyone.ps1 index c6e38220857a..659a46c5d73a 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsExternalChatWithAnyone.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsExternalChatWithAnyone.ps1 @@ -83,11 +83,12 @@ function Invoke-CIPPStandardTeamsExternalChatWithAnyone { if ($Settings.report -eq $true) { Add-CIPPBPAField -FieldName 'TeamsExternalChatWithAnyone' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $Tenant - if ($StateIsCorrect) { - $FieldValue = $true - } else { - $FieldValue = $CurrentState + $CurrentValue = @{ + UseB2BInvitesToAddExternalUsers = $CurrentState.UseB2BInvitesToAddExternalUsers + } + $ExpectedValue = @{ + UseB2BInvitesToAddExternalUsers = $DesiredState } - Set-CIPPStandardsCompareField -FieldName 'standards.TeamsExternalChatWithAnyone' -FieldValue $FieldValue -Tenant $Tenant + Set-CIPPStandardsCompareField -FieldName 'standards.TeamsExternalChatWithAnyone' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -Tenant $Tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsExternalFileSharing.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsExternalFileSharing.ps1 index 9d101bb66f15..a24252704227 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsExternalFileSharing.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsExternalFileSharing.ps1 @@ -37,8 +37,7 @@ function Invoke-CIPPStandardTeamsExternalFileSharing { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'TeamsExternalFileSharing' -TenantFilter $Tenant -RequiredCapabilities @('MCOSTANDARD', 'MCOEV', 'MCOIMP', 'TEAMS1','Teams_Room_Standard') - ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'TeamsExternalFileSharing' + $TestResult = Test-CIPPStandardLicense -StandardName 'TeamsExternalFileSharing' -TenantFilter $Tenant -RequiredCapabilities @('MCOSTANDARD', 'MCOEV', 'MCOIMP', 'TEAMS1', 'Teams_Room_Standard') if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." @@ -47,9 +46,8 @@ function Invoke-CIPPStandardTeamsExternalFileSharing { try { $CurrentState = New-TeamsRequest -TenantFilter $Tenant -Cmdlet 'Get-CsTeamsClientConfiguration' | - Select-Object AllowGoogleDrive, AllowShareFile, AllowBox, AllowDropBox, AllowEgnyte - } - catch { + Select-Object AllowGoogleDrive, AllowShareFile, AllowBox, AllowDropBox, AllowEgnyte + } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the TeamsExternalFileSharing state for $Tenant. Error: $ErrorMessage" -Sev Error return @@ -96,11 +94,20 @@ function Invoke-CIPPStandardTeamsExternalFileSharing { if ($Settings.report -eq $true) { Add-CIPPBPAField -FieldName 'TeamsExternalFileSharing' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $Tenant - if ($StateIsCorrect -eq $true) { - $FieldValue = $true - } else { - $FieldValue = $CurrentState + $CurrentValue = @{ + AllowGoogleDrive = $CurrentState.AllowGoogleDrive + AllowShareFile = $CurrentState.AllowShareFile + AllowBox = $CurrentState.AllowBox + AllowDropBox = $CurrentState.AllowDropBox + AllowEgnyte = $CurrentState.AllowEgnyte + } + $ExpectedValue = @{ + AllowGoogleDrive = $Settings.AllowGoogleDrive + AllowShareFile = $Settings.AllowShareFile + AllowBox = $Settings.AllowBox + AllowDropBox = $Settings.AllowDropBox + AllowEgnyte = $Settings.AllowEgnyte } - Set-CIPPStandardsCompareField -FieldName 'standards.TeamsExternalFileSharing' -FieldValue $FieldValue -Tenant $Tenant + Set-CIPPStandardsCompareField -FieldName 'standards.TeamsExternalFileSharing' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -Tenant $Tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsFederationConfiguration.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsFederationConfiguration.ps1 index 9aba0bc770c6..ee526b9037bb 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsFederationConfiguration.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsFederationConfiguration.ps1 @@ -34,7 +34,6 @@ function Invoke-CIPPStandardTeamsFederationConfiguration { param($Tenant, $Settings) $TestResult = Test-CIPPStandardLicense -StandardName 'TeamsFederationConfiguration' -TenantFilter $Tenant -RequiredCapabilities @('MCOSTANDARD', 'MCOEV', 'MCOIMP', 'TEAMS1','Teams_Room_Standard') - ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'TeamsFederationConfiguration' if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." @@ -144,11 +143,19 @@ function Invoke-CIPPStandardTeamsFederationConfiguration { if ($Settings.report -eq $true) { Add-CIPPBPAField -FieldName 'FederationConfiguration' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $Tenant - if ($StateIsCorrect -eq $true) { - $FieldValue = $true - } else { - $FieldValue = $CurrentState | Select-Object AllowTeamsConsumer, AllowFederatedUsers, AllowedDomains, BlockedDomains + + $CurrentValue = @{ + AllowTeamsConsumer = $CurrentState.AllowTeamsConsumer + AllowFederatedUsers = $CurrentState.AllowFederatedUsers + AllowedDomains = if ($CurrentAllowedDomains.GetType().Name -eq 'Deserialized.Microsoft.Rtc.Management.WritableConfig.Settings.Edge.AllowAllKnownDomains') { $CurrentAllowedDomains.ToString() } else { $CurrentAllowedDomains } + BlockedDomains = $CurrentState.BlockedDomains + } + $ExpectedValue = @{ + AllowTeamsConsumer = $Settings.AllowTeamsConsumer + AllowFederatedUsers = $AllowFederatedUsers + AllowedDomains = $AllowedDomains + BlockedDomains = $BlockedDomains } - Set-CIPPStandardsCompareField -FieldName 'standards.TeamsFederationConfiguration' -FieldValue $FieldValue -Tenant $Tenant + Set-CIPPStandardsCompareField -FieldName 'standards.TeamsFederationConfiguration' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -Tenant $Tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsGlobalMeetingPolicy.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsGlobalMeetingPolicy.ps1 index 8d8a2255f866..163e626da422 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsGlobalMeetingPolicy.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsGlobalMeetingPolicy.ps1 @@ -40,8 +40,6 @@ function Invoke-CIPPStandardTeamsGlobalMeetingPolicy { .LINK https://docs.cipp.app/user-documentation/tenant/standards/list-standards #> - ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'TeamsGlobalMeetingPolicy' - param($Tenant, $Settings) $TestResult = Test-CIPPStandardLicense -StandardName 'TeamsGlobalMeetingPolicy' -TenantFilter $Tenant -RequiredCapabilities @('MCOSTANDARD', 'MCOEV', 'MCOIMP', 'TEAMS1', 'Teams_Room_Standard') @@ -108,12 +106,25 @@ function Invoke-CIPPStandardTeamsGlobalMeetingPolicy { if ($Settings.report -eq $true) { - if ($StateIsCorrect) { - $FieldValue = $true - } else { - $FieldValue = $CurrentState + $CurrentValue = @{ + AllowAnonymousUsersToJoinMeeting = $CurrentState.AllowAnonymousUsersToJoinMeeting + AllowAnonymousUsersToStartMeeting = $CurrentState.AllowAnonymousUsersToStartMeeting + AutoAdmittedUsers = $CurrentState.AutoAdmittedUsers + AllowPSTNUsersToBypassLobby = $CurrentState.AllowPSTNUsersToBypassLobby + MeetingChatEnabledType = $CurrentState.MeetingChatEnabledType + DesignatedPresenterRoleMode = $CurrentState.DesignatedPresenterRoleMode + AllowExternalParticipantGiveRequestControl = $CurrentState.AllowExternalParticipantGiveRequestControl + } + $ExpectedValue = @{ + AllowAnonymousUsersToJoinMeeting = $Settings.AllowAnonymousUsersToJoinMeeting + AllowAnonymousUsersToStartMeeting = $false + AutoAdmittedUsers = $AutoAdmittedUsers + AllowPSTNUsersToBypassLobby = $false + MeetingChatEnabledType = $MeetingChatEnabledType + DesignatedPresenterRoleMode = $DesignatedPresenterRoleMode + AllowExternalParticipantGiveRequestControl = $Settings.AllowExternalParticipantGiveRequestControl } - Set-CIPPStandardsCompareField -FieldName 'standards.TeamsGlobalMeetingPolicy' -FieldValue $FieldValue -Tenant $Tenant + Set-CIPPStandardsCompareField -FieldName 'standards.TeamsGlobalMeetingPolicy' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -Tenant $Tenant Add-CIPPBPAField -FieldName 'TeamsGlobalMeetingPolicy' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $Tenant } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsGuestAccess.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsGuestAccess.ps1 index bef968173610..3c5c3d6336f9 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsGuestAccess.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsGuestAccess.ps1 @@ -31,7 +31,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 -RequiredCapabilities @('MCOSTANDARD', 'MCOEV', 'MCOIMP', 'TEAMS1', 'Teams_Room_Standard') if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." @@ -40,9 +40,8 @@ function Invoke-CIPPStandardTeamsGuestAccess { try { $CurrentState = New-TeamsRequest -TenantFilter $Tenant -Cmdlet 'Get-CsTeamsClientConfiguration' -CmdParams @{Identity = 'Global' } | - Select-Object AllowGuestUser - } - catch { + Select-Object AllowGuestUser + } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the TeamsGuestAccess state for $Tenant. Error: $ErrorMessage" -Sev Error return @@ -81,12 +80,13 @@ function Invoke-CIPPStandardTeamsGuestAccess { } if ($Settings.report -eq $true) { - if ($StateIsCorrect) { - $FieldValue = $true - } else { - $FieldValue = $CurrentState + $CurrentValue = @{ + AllowGuestUser = $CurrentState.AllowGuestUser + } + $ExpectedValue = @{ + AllowGuestUser = $AllowGuestUser } - Set-CIPPStandardsCompareField -FieldName 'standards.TeamsGuestAccess' -FieldValue $FieldValue -Tenant $Tenant + Set-CIPPStandardsCompareField -FieldName 'standards.TeamsGuestAccess' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -Tenant $Tenant Add-CIPPBPAField -FieldName 'TeamsGuestAccess' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $Tenant } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsMeetingRecordingExpiration.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsMeetingRecordingExpiration.ps1 index 662046683c77..9004c3de2b30 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsMeetingRecordingExpiration.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsMeetingRecordingExpiration.ps1 @@ -29,10 +29,9 @@ function Invoke-CIPPStandardTeamsMeetingRecordingExpiration { .LINK https://docs.cipp.app/user-documentation/tenant/standards/list-standards #> - ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'TeamsMeetingRecordingExpiration' 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 -RequiredCapabilities @('MCOSTANDARD', 'MCOEV', 'MCOIMP', 'TEAMS1', 'Teams_Room_Standard') # Input validation @@ -48,8 +47,7 @@ function Invoke-CIPPStandardTeamsMeetingRecordingExpiration { try { $CurrentExpirationDays = (New-TeamsRequest -TenantFilter $Tenant -Cmdlet 'Get-CsTeamsMeetingPolicy' -CmdParams @{Identity = 'Global' }).NewMeetingRecordingExpirationDays - } - catch { + } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the TeamsMeetingRecordingExpiration state for $Tenant. Error: $ErrorMessage" -Sev Error return @@ -90,7 +88,12 @@ function Invoke-CIPPStandardTeamsMeetingRecordingExpiration { Add-CIPPBPAField -FieldName 'TeamsMeetingRecordingExpiration' -FieldValue $CurrentExpirationDays -StoreAs string -Tenant $Tenant $CurrentExpirationDays = if ($StateIsCorrect) { $true } else { $CurrentExpirationDays } - - Set-CIPPStandardsCompareField -FieldName 'standards.TeamsMeetingRecordingExpiration' -FieldValue $CurrentExpirationDays -Tenant $Tenant + $CurrentValue = @{ + MeetingRecordingExpirationDays = $CurrentExpirationDays + } + $ExpectedValue = @{ + MeetingRecordingExpirationDays = $ExpirationDays + } + Set-CIPPStandardsCompareField -FieldName 'standards.TeamsMeetingRecordingExpiration' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -Tenant $Tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsMeetingVerification.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsMeetingVerification.ps1 index ed238109defc..119e3a0af58a 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsMeetingVerification.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsMeetingVerification.ps1 @@ -30,10 +30,9 @@ function Invoke-CIPPStandardTeamsMeetingVerification { .LINK https://docs.cipp.app/user-documentation/tenant/standards/list-standards #> - ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'TeamsMeetingVerification' 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 -RequiredCapabilities @('MCOSTANDARD', 'MCOEV', 'MCOIMP', 'TEAMS1', 'Teams_Room_Standard') if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." @@ -42,9 +41,8 @@ function Invoke-CIPPStandardTeamsMeetingVerification { try { $CurrentState = New-TeamsRequest -TenantFilter $Tenant -Cmdlet 'Get-CsTeamsMeetingPolicy' -CmdParams @{Identity = 'Global' } | - Select-Object CaptchaVerificationForMeetingJoin - } - catch { + Select-Object CaptchaVerificationForMeetingJoin + } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the TeamsMeetingVerification state for $Tenant. Error: $ErrorMessage" -Sev Error return @@ -58,7 +56,7 @@ function Invoke-CIPPStandardTeamsMeetingVerification { Write-LogMessage -API 'Standards' -tenant $Tenant -message 'Teams Meeting Verification Policy already set.' -sev Info } else { $cmdParams = @{ - Identity = 'Global' + Identity = 'Global' CaptchaVerificationForMeetingJoin = $CaptchaVerificationForMeetingJoin } @@ -82,12 +80,13 @@ function Invoke-CIPPStandardTeamsMeetingVerification { } if ($Settings.report -eq $true) { - if ($StateIsCorrect) { - $FieldValue = $true - } else { - $FieldValue = $CurrentState + $CurrentState = @{ + CaptchaVerificationForMeetingJoin = $CurrentState.CaptchaVerificationForMeetingJoin + } + $ExpectedState = @{ + CaptchaVerificationForMeetingJoin = $CaptchaVerificationForMeetingJoin } - Set-CIPPStandardsCompareField -FieldName 'standards.TeamsMeetingVerification' -FieldValue $FieldValue -Tenant $Tenant + Set-CIPPStandardsCompareField -FieldName 'standards.TeamsMeetingVerification' -CurrentValue $CurrentState -ExpectedValue $ExpectedState -Tenant $Tenant Add-CIPPBPAField -FieldName 'TeamsMeetingVerification' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $Tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsMeetingsByDefault.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsMeetingsByDefault.ps1 index b595ba79dabf..38cb28d5efae 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsMeetingsByDefault.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsMeetingsByDefault.ps1 @@ -37,15 +37,13 @@ function Invoke-CIPPStandardTeamsMeetingsByDefault { Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. - ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'TeamsMeetingsByDefault' # Get state value using null-coalescing operator $state = $Settings.state.value ?? $Settings.state try { $CurrentState = (New-ExoRequest -tenantid $Tenant -cmdlet 'Get-OrganizationConfig').OnlineMeetingsByDefaultEnabled - } - catch { + } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the TeamsMeetingsByDefault state for $Tenant. Error: $ErrorMessage" -Sev Error return @@ -89,11 +87,13 @@ function Invoke-CIPPStandardTeamsMeetingsByDefault { # Default is not set, not set means it's enabled if ($null -eq $CurrentState ) { $CurrentState = $true } Add-CIPPBPAField -FieldName 'TeamsMeetingsByDefault' -FieldValue $CurrentState -StoreAs bool -Tenant $Tenant - if ($StateIsCorrect) { - $FieldValue = $true - } else { - $FieldValue = $CurrentState + + $CurrentValue = @{ + OnlineMeetingsByDefaultEnabled = $CurrentState + } + $ExpectedValue = @{ + OnlineMeetingsByDefaultEnabled = $WantedState } - Set-CIPPStandardsCompareField -FieldName 'standards.TeamsMeetingsByDefault' -FieldValue $FieldValue -Tenant $Tenant + Set-CIPPStandardsCompareField -FieldName 'standards.TeamsMeetingsByDefault' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -Tenant $Tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsMessagingPolicy.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsMessagingPolicy.ps1 index 8b6a11935043..d2c938e26366 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsMessagingPolicy.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsMessagingPolicy.ps1 @@ -1,4 +1,4 @@ -Function Invoke-CIPPStandardTeamsMessagingPolicy { +function Invoke-CIPPStandardTeamsMessagingPolicy { <# .FUNCTIONALITY Internal @@ -37,10 +37,9 @@ Function Invoke-CIPPStandardTeamsMessagingPolicy { .LINK https://docs.cipp.app/user-documentation/tenant/standards/list-standards #> - ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'TeamsMessagingPolicy' 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 -RequiredCapabilities @('MCOSTANDARD', 'MCOEV', 'MCOIMP', 'TEAMS1', 'Teams_Room_Standard') if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." @@ -49,8 +48,7 @@ Function Invoke-CIPPStandardTeamsMessagingPolicy { try { $CurrentState = New-TeamsRequest -TenantFilter $Tenant -Cmdlet 'Get-CsTeamsMessagingPolicy' -CmdParams @{Identity = 'Global' } - } - catch { + } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the TeamsMessagingPolicy state for $Tenant. Error: $ErrorMessage" -Sev Error return @@ -68,14 +66,14 @@ Function Invoke-CIPPStandardTeamsMessagingPolicy { $ReadReceiptsEnabledType = $Settings.ReadReceiptsEnabledType.value ?? $Settings.ReadReceiptsEnabledType $StateIsCorrect = ($CurrentState.AllowOwnerDeleteMessage -eq $Settings.AllowOwnerDeleteMessage) -and - ($CurrentState.AllowUserDeleteMessage -eq $Settings.AllowUserDeleteMessage) -and - ($CurrentState.AllowUserEditMessage -eq $Settings.AllowUserEditMessage) -and - ($CurrentState.AllowUserDeleteChat -eq $Settings.AllowUserDeleteChat) -and - ($CurrentState.ReadReceiptsEnabledType -eq $ReadReceiptsEnabledType) -and - ($CurrentState.CreateCustomEmojis -eq $Settings.CreateCustomEmojis) -and - ($CurrentState.DeleteCustomEmojis -eq $Settings.DeleteCustomEmojis) -and - ($CurrentState.AllowSecurityEndUserReporting -eq $Settings.AllowSecurityEndUserReporting) -and - ($CurrentState.AllowCommunicationComplianceEndUserReporting -eq $Settings.AllowCommunicationComplianceEndUserReporting) + ($CurrentState.AllowUserDeleteMessage -eq $Settings.AllowUserDeleteMessage) -and + ($CurrentState.AllowUserEditMessage -eq $Settings.AllowUserEditMessage) -and + ($CurrentState.AllowUserDeleteChat -eq $Settings.AllowUserDeleteChat) -and + ($CurrentState.ReadReceiptsEnabledType -eq $ReadReceiptsEnabledType) -and + ($CurrentState.CreateCustomEmojis -eq $Settings.CreateCustomEmojis) -and + ($CurrentState.DeleteCustomEmojis -eq $Settings.DeleteCustomEmojis) -and + ($CurrentState.AllowSecurityEndUserReporting -eq $Settings.AllowSecurityEndUserReporting) -and + ($CurrentState.AllowCommunicationComplianceEndUserReporting -eq $Settings.AllowCommunicationComplianceEndUserReporting) if ($Settings.remediate -eq $true) { if ($StateIsCorrect -eq $true) { @@ -108,7 +106,7 @@ Function Invoke-CIPPStandardTeamsMessagingPolicy { if ($StateIsCorrect -eq $true) { Write-LogMessage -API 'Standards' -tenant $Tenant -message 'Global Teams messaging policy is configured correctly.' -sev Info } else { - Write-StandardsAlert -message "Global Teams messaging policy is not configured correctly." -object $CurrentState -tenant $Tenant -standardName 'TeamsMessagingPolicy' -standardId $Settings.standardId + Write-StandardsAlert -message 'Global Teams messaging policy is not configured correctly.' -object $CurrentState -tenant $Tenant -standardName 'TeamsMessagingPolicy' -standardId $Settings.standardId Write-LogMessage -API 'Standards' -tenant $Tenant -message 'Global Teams messaging policy is not configured correctly.' -sev Info } } @@ -116,11 +114,28 @@ Function Invoke-CIPPStandardTeamsMessagingPolicy { if ($Settings.report -eq $true) { Add-CIPPBPAField -FieldName 'TeamsMessagingPolicy' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $Tenant - if ($StateIsCorrect) { - $FieldValue = $true - } else { - $FieldValue = $CurrentState + $CurrentValue = @{ + AllowOwnerDeleteMessage = $CurrentState.AllowOwnerDeleteMessage + AllowUserDeleteMessage = $CurrentState.AllowUserDeleteMessage + AllowUserEditMessage = $CurrentState.AllowUserEditMessage + AllowUserDeleteChat = $CurrentState.AllowUserDeleteChat + ReadReceiptsEnabledType = $CurrentState.ReadReceiptsEnabledType + CreateCustomEmojis = $CurrentState.CreateCustomEmojis + DeleteCustomEmojis = $CurrentState.DeleteCustomEmojis + AllowSecurityEndUserReporting = $CurrentState.AllowSecurityEndUserReporting + AllowCommunicationComplianceEndUserReporting = $CurrentState.AllowCommunicationComplianceEndUserReporting + } + $ExpectedValue = @{ + AllowOwnerDeleteMessage = $Settings.AllowOwnerDeleteMessage + AllowUserDeleteMessage = $Settings.AllowUserDeleteMessage + AllowUserEditMessage = $Settings.AllowUserEditMessage + AllowUserDeleteChat = $Settings.AllowUserDeleteChat + ReadReceiptsEnabledType = $ReadReceiptsEnabledType + CreateCustomEmojis = $Settings.CreateCustomEmojis + DeleteCustomEmojis = $Settings.DeleteCustomEmojis + AllowSecurityEndUserReporting = $Settings.AllowSecurityEndUserReporting + AllowCommunicationComplianceEndUserReporting = $Settings.AllowCommunicationComplianceEndUserReporting } - Set-CIPPStandardsCompareField -FieldName 'standards.TeamsMessagingPolicy' -FieldValue $FieldValue -Tenant $Tenant + Set-CIPPStandardsCompareField -FieldName 'standards.TeamsMessagingPolicy' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -Tenant $Tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTenantDefaultTimezone.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTenantDefaultTimezone.ps1 index e930f7ef6fa7..265ff6116e17 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTenantDefaultTimezone.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTenantDefaultTimezone.ps1 @@ -31,8 +31,7 @@ function Invoke-CIPPStandardTenantDefaultTimezone { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'TenantDefaultTimezone' -TenantFilter $Tenant -RequiredCapabilities @('SHAREPOINTWAC', 'SHAREPOINTSTANDARD', 'SHAREPOINTENTERPRISE', 'SHAREPOINTENTERPRISE_EDU','ONEDRIVE_BASIC', 'ONEDRIVE_ENTERPRISE') - ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'TenantDefaultTimezone' + $TestResult = Test-CIPPStandardLicense -StandardName 'TenantDefaultTimezone' -TenantFilter $Tenant -RequiredCapabilities @('SHAREPOINTWAC', 'SHAREPOINTSTANDARD', 'SHAREPOINTENTERPRISE', 'SHAREPOINTENTERPRISE_EDU', 'ONEDRIVE_BASIC', 'ONEDRIVE_ENTERPRISE') if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." @@ -41,8 +40,7 @@ function Invoke-CIPPStandardTenantDefaultTimezone { try { $CurrentState = New-GraphGetRequest -Uri 'https://graph.microsoft.com/beta/admin/sharepoint/settings' -tenantid $Tenant -AsApp $true - } - catch { + } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the TenantDefaultTimezone state for $Tenant. Error: $ErrorMessage" -Sev Error return @@ -82,11 +80,12 @@ function Invoke-CIPPStandardTenantDefaultTimezone { if ($Settings.report -eq $true) { Add-CIPPBPAField -FieldName 'TenantDefaultTimezone' -FieldValue $CurrentState.tenantDefaultTimezone -StoreAs string -Tenant $Tenant - if ($StateIsCorrect) { - $FieldValue = $true - } else { - $FieldValue = $CurrentState | Select-Object tenantDefaultTimezone + $CurrentValue = @{ + tenantDefaultTimezone = $CurrentState.tenantDefaultTimezone + } + $ExpectedValue = @{ + tenantDefaultTimezone = $ExpectedTimezone } - Set-CIPPStandardsCompareField -FieldName 'standards.TenantDefaultTimezone' -FieldValue $FieldValue -Tenant $Tenant + Set-CIPPStandardsCompareField -FieldName 'standards.TenantDefaultTimezone' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -Tenant $Tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTransportRuleTemplate.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTransportRuleTemplate.ps1 index 5ac0d53dffbf..9e645e6597cd 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTransportRuleTemplate.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTransportRuleTemplate.ps1 @@ -34,7 +34,7 @@ function Invoke-CIPPStandardTransportRuleTemplate { Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. - ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'TransportRuleTemplate' + $existingRules = New-ExoRequest -ErrorAction SilentlyContinue -tenantid $Tenant -cmdlet 'Get-TransportRule' -useSystemMailbox $true if ($Settings.remediate -eq $true) { Write-Host "Settings: $($Settings | ConvertTo-Json)" @@ -78,12 +78,15 @@ function Invoke-CIPPStandardTransportRuleTemplate { } } - if ($MissingRules.Count -eq 0) { - $fieldValue = $true - } else { - $fieldValue = $MissingRules -join ', ' + $CurrentValue = @{ + DeployedTransportRules = $existingRules.DisplayName | Where-Object { $rules.displayname -contains $_ } | Sort-Object + MissingTransportRules = $MissingRules + } + $ExpectedValue = @{ + DeployedTransportRules = $rules.displayname | Sort-Object + MissingTransportRules = @() } - Set-CIPPStandardsCompareField -FieldName 'standards.TransportRuleTemplate' -FieldValue $fieldValue -Tenant $Tenant + Set-CIPPStandardsCompareField -FieldName 'standards.TransportRuleTemplate' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -Tenant $Tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTwoClickEmailProtection.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTwoClickEmailProtection.ps1 index 6c5edef137fe..3368eaa5be6c 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTwoClickEmailProtection.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTwoClickEmailProtection.ps1 @@ -37,7 +37,6 @@ function Invoke-CIPPStandardTwoClickEmailProtection { Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. - ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'TwoClickEmailProtection' # Get state value using null-coalescing operator $State = $Settings.state.value ?? $Settings.state @@ -45,7 +44,7 @@ function Invoke-CIPPStandardTwoClickEmailProtection { # Input validation if ([string]::IsNullOrWhiteSpace($State)) { Write-LogMessage -API 'Standards' -tenant $Tenant -message 'TwoClickEmailProtection: Invalid state parameter set' -sev Error - Return + return } try { @@ -53,7 +52,7 @@ function Invoke-CIPPStandardTwoClickEmailProtection { } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Standards' -tenant $Tenant -message "Could not get current two-click email protection state. Error: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - Return + return } $WantedState = $State -eq 'enabled' ? $true : $false @@ -86,7 +85,13 @@ function Invoke-CIPPStandardTwoClickEmailProtection { } if ($Settings.report -eq $true) { - Set-CIPPStandardsCompareField -FieldName 'standards.TwoClickEmailProtection' -FieldValue $StateIsCorrect -Tenant $Tenant + $CurrentValue = @{ + TwoClickMailPreviewEnabled = $CurrentState + } + $ExpectedValue = @{ + TwoClickMailPreviewEnabled = $WantedState + } + Set-CIPPStandardsCompareField -FieldName 'standards.TwoClickEmailProtection' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -Tenant $Tenant Add-CIPPBPAField -FieldName 'TwoClickEmailProtection' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $Tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardUndoOauth.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardUndoOauth.ps1 index 96d2bed4cf05..9d292ddeb938 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardUndoOauth.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardUndoOauth.ps1 @@ -30,12 +30,10 @@ function Invoke-CIPPStandardUndoOauth { #> param($Tenant, $Settings) - ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'UndoOauth' try { $CurrentState = New-GraphGetRequest -tenantid $Tenant -Uri 'https://graph.microsoft.com/beta/policies/authorizationPolicy/authorizationPolicy?$select=permissionGrantPolicyIdsAssignedToDefaultUserRole' - } - catch { + } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the App Consent state for $Tenant. Error: $ErrorMessage" -Sev Error return @@ -43,23 +41,23 @@ function Invoke-CIPPStandardUndoOauth { $StateIsCorrect = ($CurrentState.permissionGrantPolicyIdsAssignedToDefaultUserRole -eq 'ManagePermissionGrantsForSelf.microsoft-user-default-legacy') - If ($Settings.remediate -eq $true) { + if ($Settings.remediate -eq $true) { if ($StateIsCorrect -eq $true) { Write-LogMessage -API 'Standards' -tenant $Tenant -message 'Application Consent Mode is already disabled.' -sev Info } else { try { $GraphRequest = @{ - tenantid = $tenant - uri = 'https://graph.microsoft.com/beta/policies/authorizationPolicy/authorizationPolicy' - AsApp = $false - Type = 'PATCH' + tenantid = $tenant + uri = 'https://graph.microsoft.com/beta/policies/authorizationPolicy/authorizationPolicy' + AsApp = $false + Type = 'PATCH' ContentType = 'application/json' - Body = '{"permissionGrantPolicyIdsAssignedToDefaultUserRole":["ManagePermissionGrantsForSelf.microsoft-user-default-legacy"]}' + Body = '{"permissionGrantPolicyIdsAssignedToDefaultUserRole":["ManagePermissionGrantsForSelf.microsoft-user-default-legacy"]}' } New-GraphPostRequest @GraphRequest Write-LogMessage -API 'Standards' -tenant $tenant -message 'Application Consent Mode has been disabled.' -sev Info } catch { - Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to set Application Consent Mode to disabled." -sev Error -LogData $_ + Write-LogMessage -API 'Standards' -tenant $tenant -message 'Failed to set Application Consent Mode to disabled.' -sev Error -LogData $_ } } @@ -69,18 +67,19 @@ function Invoke-CIPPStandardUndoOauth { if ($StateIsCorrect -eq $true) { Write-LogMessage -API 'Standards' -tenant $Tenant -message 'Application Consent Mode is disabled.' -sev Info } else { - Write-StandardsAlert -message "Application Consent Mode is not disabled." -object $CurrentState -tenant $Tenant -standardName 'UndoOauth' -standardId $Settings.standardId + Write-StandardsAlert -message 'Application Consent Mode is not disabled.' -object $CurrentState -tenant $Tenant -standardName 'UndoOauth' -standardId $Settings.standardId Write-LogMessage -API 'Standards' -tenant $Tenant -message 'Application Consent Mode is not disabled.' -sev Info } } if ($Settings.report -eq $true) { Add-CIPPBPAField -FieldName 'UndoOauth' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $tenant - if ($StateIsCorrect) { - $FieldValue = $true - } else { - $FieldValue = $CurrentState + $CurrentValue = @{ + permissionGrantPolicyIdsAssignedToDefaultUserRole = $CurrentState.permissionGrantPolicyIdsAssignedToDefaultUserRole + } + $ExpectedValue = @{ + permissionGrantPolicyIdsAssignedToDefaultUserRole = @('ManagePermissionGrantsForSelf.microsoft-user-default-legacy') } - Set-CIPPStandardsCompareField -FieldName 'standards.UndoOauth' -FieldValue $FieldValue -Tenant $Tenant + Set-CIPPStandardsCompareField -FieldName 'standards.UndoOauth' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -Tenant $Tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardUserPreferredLanguage.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardUserPreferredLanguage.ps1 index f203d1a427d1..5c18d9d18d94 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardUserPreferredLanguage.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardUserPreferredLanguage.ps1 @@ -34,8 +34,7 @@ function Invoke-CIPPStandardUserPreferredLanguage { try { $IncorrectUsers = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/users?`$top=999&`$select=userPrincipalName,displayName,preferredLanguage,userType,onPremisesSyncEnabled&`$filter=preferredLanguage ne '$preferredLanguage' and userType eq 'Member' and onPremisesSyncEnabled ne true&`$count=true" -tenantid $Tenant -ComplexFilter - } - catch { + } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the UserPreferredLanguage state for $Tenant. Error: $ErrorMessage" -Sev Error return @@ -77,11 +76,16 @@ function Invoke-CIPPStandardUserPreferredLanguage { if ($Settings.report -eq $true) { Add-CIPPBPAField -FieldName 'IncorrectUsers' -FieldValue $IncorrectUsers -StoreAs json -Tenant $Tenant - if ($IncorrectUsers.userPrincipalName) { - $FieldValue = $IncorrectUsers | Select-Object -Property userPrincipalName, displayName, preferredLanguage, userType - } else { - $FieldValue = $true + if ($IncorrectUsers.userPrincipalName) { $FieldValue = $IncorrectUsers | Select-Object -Property userPrincipalName, displayName, preferredLanguage, userType } else { $FieldValue = @() } + + $CurrentValue = @{ + preferredLanguage = $preferredLanguage + incorrectUsers = $FieldValue + } + $ExpectedValue = @{ + preferredLanguage = $preferredLanguage + incorrectUsers = @() } - Set-CIPPStandardsCompareField -FieldName 'standards.UserPreferredLanguage' -FieldValue $FieldValue -Tenant $Tenant + Set-CIPPStandardsCompareField -FieldName 'standards.UserPreferredLanguage' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -Tenant $Tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardUserSubmissions.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardUserSubmissions.ps1 index 37dab2d39c17..f2867357c2f8 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardUserSubmissions.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardUserSubmissions.ps1 @@ -38,7 +38,6 @@ function Invoke-CIPPStandardUserSubmissions { Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. - ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'UserSubmissions' # Get state value using null-coalescing operator $state = $Settings.state.value ?? $Settings.state @@ -62,8 +61,7 @@ function Invoke-CIPPStandardUserSubmissions { try { $PolicyState = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-ReportSubmissionPolicy' $RuleState = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-ReportSubmissionRule' - } - catch { + } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the UserSubmissions state for $Tenant. Error: $ErrorMessage" -Sev Error } @@ -199,7 +197,6 @@ function Invoke-CIPPStandardUserSubmissions { } } - if ($Settings.report -eq $true) { if ($PolicyState.length -eq 0) { Add-CIPPBPAField -FieldName 'UserSubmissionPolicy' -FieldValue $false -StoreAs bool -Tenant $Tenant @@ -207,14 +204,42 @@ function Invoke-CIPPStandardUserSubmissions { Add-CIPPBPAField -FieldName 'UserSubmissionPolicy' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $Tenant } - if ($StateIsCorrect) { - $FieldValue = $true - } else { - $PolicyState = $PolicyState | Select-Object EnableReportToMicrosoft, ReportJunkToCustomizedAddress, ReportNotJunkToCustomizedAddress, ReportPhishToCustomizedAddress, ReportJunkAddresses, ReportNotJunkAddresses, ReportPhishAddresses - $RuleState = $RuleState | Select-Object State, SentTo - $FieldValue = @{ PolicyState = $PolicyState; RuleState = $RuleState } + $PolicyState = $PolicyState | Select-Object EnableReportToMicrosoft, ReportJunkToCustomizedAddress, ReportNotJunkToCustomizedAddress, ReportPhishToCustomizedAddress, ReportJunkAddresses, ReportNotJunkAddresses, ReportPhishAddresses + $RuleState = $RuleState | Select-Object State, SentTo + + $CurrentValue = @{ + EnableReportToMicrosoft = $PolicyState.EnableReportToMicrosoft + ReportJunkToCustomizedAddress = $PolicyState.ReportJunkToCustomizedAddress + ReportNotJunkToCustomizedAddress = $PolicyState.ReportNotJunkToCustomizedAddress + ReportPhishToCustomizedAddress = $PolicyState.ReportPhishToCustomizedAddress + ReportJunkAddresses = $PolicyState.ReportJunkAddresses + ReportNotJunkAddresses = $PolicyState.ReportNotJunkAddresses + ReportPhishAddresses = $PolicyState.ReportPhishAddresses + RuleState = @{ + State = $RuleState.State + SentTo = $RuleState.SentTo + } } - - Set-CIPPStandardsCompareField -FieldName 'standards.UserSubmissions' -FieldValue $FieldValue -TenantFilter $Tenant + $ExpectedValue = @{ + EnableReportToMicrosoft = $state -eq 'enable' + ReportJunkToCustomizedAddress = if ([string]::IsNullOrWhiteSpace($Email)) { $false } else { $true } + ReportNotJunkToCustomizedAddress = if ([string]::IsNullOrWhiteSpace($Email)) { $false } else { $true } + ReportPhishToCustomizedAddress = if ([string]::IsNullOrWhiteSpace($Email)) { $false } else { $true } + ReportJunkAddresses = if ([string]::IsNullOrWhiteSpace($Email)) { $null } else { $Email } + ReportNotJunkAddresses = if ([string]::IsNullOrWhiteSpace($Email)) { $null } else { $Email } + ReportPhishAddresses = if ([string]::IsNullOrWhiteSpace($Email)) { $null } else { $Email } + RuleState = if ([string]::IsNullOrWhiteSpace($Email)) { + @{ + State = 'Disabled' + SentTo = $null + } + } else { + @{ + State = 'Enabled' + SentTo = $Email + } + } + } + Set-CIPPStandardsCompareField -FieldName 'standards.UserSubmissions' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardintuneBrandingProfile.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardintuneBrandingProfile.ps1 index ff2d5495278a..cd36588ae4ad 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardintuneBrandingProfile.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardintuneBrandingProfile.ps1 @@ -41,7 +41,6 @@ function Invoke-CIPPStandardintuneBrandingProfile { param($Tenant, $Settings) $TestResult = Test-CIPPStandardLicense -StandardName 'intuneBrandingProfile' -TenantFilter $Tenant -RequiredCapabilities @('INTUNE_A', 'MDM_Services', 'EMS', 'SCCM', 'MICROSOFTINTUNEPLAN1') - ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'intuneBrandingProfile' if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." @@ -50,8 +49,7 @@ function Invoke-CIPPStandardintuneBrandingProfile { try { $CurrentState = New-GraphGetRequest -Uri 'https://graph.microsoft.com/beta/deviceManagement/intuneBrandingProfiles/c3a59481-1bf2-46ce-94b3-66eec07a8d60' -tenantid $Tenant -AsApp $true - } - catch { + } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the intuneBrandingProfile state for $Tenant. Error: $ErrorMessage" -Sev Error return @@ -113,8 +111,31 @@ function Invoke-CIPPStandardintuneBrandingProfile { } if ($Settings.report -eq $true) { - $ReportState = $StateIsCorrect ? $true : $CurrentState - Set-CIPPStandardsCompareField -FieldName 'standards.intuneBrandingProfile' -FieldValue $ReportState -TenantFilter $Tenant + $CurrentValue = @{ + displayName = $CurrentState.displayName + showLogo = $CurrentState.showLogo + showDisplayNameNextToLogo = $CurrentState.showDisplayNameNextToLogo + contactITName = $CurrentState.contactITName + contactITPhoneNumber = $CurrentState.contactITPhoneNumber + contactITEmailAddress = $CurrentState.contactITEmailAddress + contactITNotes = $CurrentState.contactITNotes + onlineSupportSiteName = $CurrentState.onlineSupportSiteName + onlineSupportSiteUrl = $CurrentState.onlineSupportSiteUrl + privacyUrl = $CurrentState.privacyUrl + } + $ExpectedValue = @{ + displayName = $Settings.displayName + showLogo = $Settings.showLogo + showDisplayNameNextToLogo = $Settings.showDisplayNameNextToLogo + contactITName = $Settings.contactITName + contactITPhoneNumber = $Settings.contactITPhoneNumber + contactITEmailAddress = $Settings.contactITEmailAddress + contactITNotes = $Settings.contactITNotes + onlineSupportSiteName = $Settings.onlineSupportSiteName + onlineSupportSiteUrl = $Settings.onlineSupportSiteUrl + privacyUrl = $Settings.privacyUrl + } + Set-CIPPStandardsCompareField -FieldName 'standards.intuneBrandingProfile' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant Add-CIPPBPAField -FieldName 'intuneBrandingProfile' -FieldValue [bool]$StateIsCorrect -StoreAs bool -Tenant $tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardintuneDeviceReg.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardintuneDeviceReg.ps1 index 25a4f7168828..24948cf10bf7 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardintuneDeviceReg.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardintuneDeviceReg.ps1 @@ -33,7 +33,6 @@ function Invoke-CIPPStandardintuneDeviceReg { param($Tenant, $Settings) $TestResult = Test-CIPPStandardLicense -StandardName 'intuneDeviceReg' -TenantFilter $Tenant -RequiredCapabilities @('INTUNE_A', 'MDM_Services', 'EMS', 'SCCM', 'MICROSOFTINTUNEPLAN1') - ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'intuneDeviceReg' if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." @@ -42,15 +41,14 @@ function Invoke-CIPPStandardintuneDeviceReg { try { $PreviousSetting = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/policies/deviceRegistrationPolicy' -tenantid $Tenant - } - catch { + } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the intuneDeviceReg state for $Tenant. Error: $ErrorMessage" -Sev Error return } $StateIsCorrect = if ($PreviousSetting.userDeviceQuota -eq $Settings.max) { $true } else { $false } - If ($Settings.remediate -eq $true) { + if ($Settings.remediate -eq $true) { if ($PreviousSetting.userDeviceQuota -eq $Settings.max) { Write-LogMessage -API 'Standards' -tenant $tenant -message "User device quota is already set to $($Settings.max)" -sev Info @@ -78,8 +76,13 @@ function Invoke-CIPPStandardintuneDeviceReg { } if ($Settings.report -eq $true) { - $state = $StateIsCorrect ? $true : $PreviousSetting.userDeviceQuota - Set-CIPPStandardsCompareField -FieldName 'standards.intuneDeviceReg' -FieldValue $state -TenantFilter $Tenant + $CurrentValue = @{ + userDeviceQuota = $PreviousSetting.userDeviceQuota + } + $ExpectedValue = @{ + userDeviceQuota = $Settings.max + } + Set-CIPPStandardsCompareField -FieldName 'standards.intuneDeviceReg' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant Add-CIPPBPAField -FieldName 'intuneDeviceReg' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardintuneDeviceRetirementDays.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardintuneDeviceRetirementDays.ps1 index 41faf6faf746..b980c034ddd0 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardintuneDeviceRetirementDays.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardintuneDeviceRetirementDays.ps1 @@ -33,7 +33,6 @@ function Invoke-CIPPStandardintuneDeviceRetirementDays { param($Tenant, $Settings) $TestResult = Test-CIPPStandardLicense -StandardName 'intuneDeviceRetirementDays' -TenantFilter $Tenant -RequiredCapabilities @('INTUNE_A', 'MDM_Services', 'EMS', 'SCCM', 'MICROSOFTINTUNEPLAN1') - ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'intuneDeviceRetirementDays' if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." @@ -42,8 +41,7 @@ function Invoke-CIPPStandardintuneDeviceRetirementDays { try { $CurrentInfo = (New-GraphGetRequest -Uri 'https://graph.microsoft.com/beta/deviceManagement/managedDeviceCleanupRules' -tenantid $Tenant) - } - catch { + } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the intuneDeviceRetirementDays state for $Tenant. Error: $ErrorMessage" -Sev Error return @@ -84,8 +82,13 @@ function Invoke-CIPPStandardintuneDeviceRetirementDays { } if ($Settings.report -eq $true) { - $state = $StateIsCorrect ? $true : $CurrentInfo.DeviceInactivityBeforeRetirementInDays - Set-CIPPStandardsCompareField -FieldName 'standards.intuneDeviceRetirementDays' -FieldValue $state -Tenant $tenant + $CurrentValue = @{ + deviceInactivityBeforeRetirementInDays = $CurrentInfo.DeviceInactivityBeforeRetirementInDays + } + $ExpectedValue = @{ + deviceInactivityBeforeRetirementInDays = $Settings.days + } + Set-CIPPStandardsCompareField -FieldName 'standards.intuneDeviceRetirementDays' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant Add-CIPPBPAField -FieldName 'intuneDeviceRetirementDays' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardintuneRequireMFA.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardintuneRequireMFA.ps1 index 2ed285caec7f..81fa6fae17c9 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardintuneRequireMFA.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardintuneRequireMFA.ps1 @@ -29,7 +29,6 @@ function Invoke-CIPPStandardintuneRequireMFA { #> param($Tenant, $Settings) - ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'intuneRequireMFA' try { $PreviousSetting = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/policies/deviceRegistrationPolicy' -tenantid $Tenant @@ -69,7 +68,14 @@ function Invoke-CIPPStandardintuneRequireMFA { if ($Settings.report -eq $true) { $RequireMFA = if ($PreviousSetting.multiFactorAuthConfiguration -eq 'required') { $true } else { $false } - Set-CIPPStandardsCompareField -FieldName 'standards.intuneRequireMFA' -FieldValue $RequireMFA -Tenant $Tenant + + $CurrentValue = @{ + multiFactorAuthConfiguration = $PreviousSetting.multiFactorAuthConfiguration + } + $ExpectedValue = @{ + multiFactorAuthConfiguration = 'required' + } + Set-CIPPStandardsCompareField -FieldName 'standards.intuneRequireMFA' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -Tenant $Tenant Add-CIPPBPAField -FieldName 'intuneRequireMFA' -FieldValue $RequireMFA -StoreAs bool -Tenant $Tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardlaps.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardlaps.ps1 index 6f1886a9a3fc..b08508db2e6e 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardlaps.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardlaps.ps1 @@ -31,7 +31,6 @@ function Invoke-CIPPStandardlaps { #> param($Tenant, $Settings) - ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'laps' try { $PreviousSetting = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/policies/deviceRegistrationPolicy' -tenantid $Tenant diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardsharingCapability.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardsharingCapability.ps1 index 69e82b3fc8a9..054066c38646 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardsharingCapability.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardsharingCapability.ps1 @@ -36,7 +36,7 @@ function Invoke-CIPPStandardsharingCapability { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'sharingCapability' -TenantFilter $Tenant -RequiredCapabilities @('SHAREPOINTWAC', 'SHAREPOINTSTANDARD', 'SHAREPOINTENTERPRISE', 'SHAREPOINTENTERPRISE_EDU','ONEDRIVE_BASIC', 'ONEDRIVE_ENTERPRISE') + $TestResult = Test-CIPPStandardLicense -StandardName 'sharingCapability' -TenantFilter $Tenant -RequiredCapabilities @('SHAREPOINTWAC', 'SHAREPOINTSTANDARD', 'SHAREPOINTENTERPRISE', 'SHAREPOINTENTERPRISE_EDU', 'ONEDRIVE_BASIC', 'ONEDRIVE_ENTERPRISE') if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." @@ -45,8 +45,7 @@ function Invoke-CIPPStandardsharingCapability { try { $CurrentInfo = New-GraphGetRequest -Uri 'https://graph.microsoft.com/beta/admin/sharepoint/settings' -tenantid $Tenant -AsApp $true - } - catch { + } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the sharingCapability state for $Tenant. Error: $ErrorMessage" -Sev Error return @@ -96,11 +95,12 @@ function Invoke-CIPPStandardsharingCapability { } if ($Settings.report -eq $true) { - if ($CurrentInfo.sharingCapability -eq $level) { - $FieldValue = $true - } else { - $FieldValue = $CurrentInfo | Select-Object -Property sharingCapability + $CurrentValue = @{ + sharingCapability = $CurrentInfo.sharingCapability + } + $ExpectedValue = @{ + sharingCapability = $level } - Set-CIPPStandardsCompareField -FieldName 'standards.sharingCapability' -FieldValue $FieldValue -Tenant $Tenant + Set-CIPPStandardsCompareField -FieldName 'standards.sharingCapability' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -Tenant $Tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardsharingDomainRestriction.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardsharingDomainRestriction.ps1 index 16602aaac0a6..6aae7a5b8dcd 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardsharingDomainRestriction.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardsharingDomainRestriction.ps1 @@ -35,7 +35,7 @@ function Invoke-CIPPStandardsharingDomainRestriction { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'sharingDomainRestriction' -TenantFilter $Tenant -RequiredCapabilities @('SHAREPOINTWAC', 'SHAREPOINTSTANDARD', 'SHAREPOINTENTERPRISE', 'SHAREPOINTENTERPRISE_EDU','ONEDRIVE_BASIC', 'ONEDRIVE_ENTERPRISE') + $TestResult = Test-CIPPStandardLicense -StandardName 'sharingDomainRestriction' -TenantFilter $Tenant -RequiredCapabilities @('SHAREPOINTWAC', 'SHAREPOINTSTANDARD', 'SHAREPOINTENTERPRISE', 'SHAREPOINTENTERPRISE_EDU', 'ONEDRIVE_BASIC', 'ONEDRIVE_ENTERPRISE') if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." @@ -44,8 +44,7 @@ function Invoke-CIPPStandardsharingDomainRestriction { try { $CurrentState = New-GraphGetRequest -Uri 'https://graph.microsoft.com/beta/admin/sharepoint/settings' -tenantid $Tenant -AsApp $true - } - catch { + } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the SharingDomainRestriction state for $Tenant. Error: $ErrorMessage" -Sev Error return @@ -114,11 +113,16 @@ function Invoke-CIPPStandardsharingDomainRestriction { if ($Settings.report -eq $true) { Add-CIPPBPAField -FieldName 'sharingDomainRestriction' -FieldValue [bool]$StateIsCorrect -StoreAs bool -Tenant $tenant - if ($StateIsCorrect) { - $FieldValue = $true - } else { - $FieldValue = $CurrentState | Select-Object sharingAllowedDomainList, sharingDomainRestrictionMode + $CurrentValue = @{ + sharingDomainRestrictionMode = $CurrentState.sharingDomainRestrictionMode + sharingAllowedDomainList = $CurrentState.sharingAllowedDomainList + sharingBlockedDomainList = $CurrentState.sharingBlockedDomainList + } + $ExpectedValue = @{ + sharingDomainRestrictionMode = $mode + sharingAllowedDomainList = if ($mode -eq 'allowList') { $SelectedDomains } else { @() } + sharingBlockedDomainList = if ($mode -eq 'blockList') { $SelectedDomains } else { @() } } - Set-CIPPStandardsCompareField -FieldName 'standards.sharingDomainRestriction' -FieldValue $FieldValue -Tenant $Tenant + Set-CIPPStandardsCompareField -FieldName 'standards.sharingDomainRestriction' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -Tenant $Tenant } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardunmanagedSync.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardunmanagedSync.ps1 index d333a06c1adc..18b75b21b643 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardunmanagedSync.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardunmanagedSync.ps1 @@ -36,7 +36,6 @@ function Invoke-CIPPStandardunmanagedSync { param($Tenant, $Settings) $TestResult = Test-CIPPStandardLicense -StandardName 'unmanagedSync' -TenantFilter $Tenant -RequiredCapabilities @('INTUNE_A', 'MDM_Services', 'EMS', 'SCCM', 'MICROSOFTINTUNEPLAN1') - ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'unmanagedSync' if ($TestResult -eq $false) { Write-Host "We're exiting as the correct license is not present for this standard." @@ -45,9 +44,8 @@ function Invoke-CIPPStandardunmanagedSync { try { $CurrentState = Get-CIPPSPOTenant -TenantFilter $Tenant | - Select-Object _ObjectIdentity_, TenantFilter, ConditionalAccessPolicy - } - catch { + Select-Object _ObjectIdentity_, TenantFilter, ConditionalAccessPolicy + } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the unmanagedSync state for $Tenant. Error: $ErrorMessage" -Sev Error return @@ -83,9 +81,13 @@ function Invoke-CIPPStandardunmanagedSync { } if ($Settings.report -eq $true) { - - $State = $StateIsCorrect ? $true : $CurrentState.ConditionalAccessPolicy - Set-CIPPStandardsCompareField -FieldName 'standards.unmanagedSync' -FieldValue $State -Tenant $Tenant + $CurrentValue = @{ + ConditionalAccessPolicy = $CurrentState.ConditionalAccessPolicy + } + $ExpectedValue = @{ + ConditionalAccessPolicy = $WantedState + } + Set-CIPPStandardsCompareField -FieldName 'standards.unmanagedSync' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -Tenant $Tenant Add-CIPPBPAField -FieldName 'unmanagedSync' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $Tenant } } From e30606accfd3fc9a553ac8699330c0cf54c75a2f Mon Sep 17 00:00:00 2001 From: John Duprey Date: Wed, 14 Jan 2026 22:24:14 -0500 Subject: [PATCH 144/503] Improve group assignment handling in intune apps Refactored group assignment logic in Invoke-AddMSPApp.ps1 and Invoke-AddOfficeApp.ps1 to support custom group assignments. Enhanced Set-CIPPAssignedApplication.ps1 to fetch group IDs with additional query parameters and fixed variable usage in group matching. --- .../Endpoint/Applications/Invoke-AddMSPApp.ps1 | 2 +- .../Endpoint/Applications/Invoke-AddOfficeApp.ps1 | 2 +- Modules/CIPPCore/Public/Set-CIPPAssignedApplication.ps1 | 7 +++---- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-AddMSPApp.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-AddMSPApp.ps1 index 55e74c8693e0..bdbc7f7dba1f 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-AddMSPApp.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-AddMSPApp.ps1 @@ -13,7 +13,7 @@ function Invoke-AddMSPApp { $RMMApp = $Request.Body - $AssignTo = $Request.Body.AssignTo + $AssignTo = $Request.Body.AssignTo -eq 'customGroup' ? $Request.Body.CustomGroup : $Request.Body.AssignTo $intuneBody = Get-Content "AddMSPApp\$($RMMApp.RMMName.value).app.json" | ConvertFrom-Json $intuneBody.displayName = $RMMApp.DisplayName diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-AddOfficeApp.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-AddOfficeApp.ps1 index 4395e899a8fe..97d65b678542 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-AddOfficeApp.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-AddOfficeApp.ps1 @@ -13,7 +13,7 @@ function Invoke-AddOfficeApp { $Headers = $Request.Headers $APIName = $Request.Params.CIPPEndpoint if ('AllTenants' -in $Tenants) { $Tenants = (Get-Tenants).defaultDomainName } - $AssignTo = if ($Request.Body.AssignTo -ne 'on') { $Request.Body.AssignTo } + $AssignTo = $Request.Body.AssignTo -eq 'customGroup' ? $Request.Body.CustomGroup : $Request.Body.AssignTo $Results = foreach ($Tenant in $Tenants) { try { diff --git a/Modules/CIPPCore/Public/Set-CIPPAssignedApplication.ps1 b/Modules/CIPPCore/Public/Set-CIPPAssignedApplication.ps1 index 17b9201199e3..771c6f4fc6fe 100644 --- a/Modules/CIPPCore/Public/Set-CIPPAssignedApplication.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPAssignedApplication.ps1 @@ -88,11 +88,11 @@ function Set-CIPPAssignedApplication { $resolvedGroupIds = $GroupIds } else { $GroupNames = $GroupName.Split(',') - $resolvedGroupIds = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/groups' -tenantid $TenantFilter | ForEach-Object { + $resolvedGroupIds = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/groups?$top=999&$select=id,displayName' -tenantid $TenantFilter | ForEach-Object { $Group = $_ foreach ($SingleName in $GroupNames) { - if ($_.displayName -like $SingleName) { - $group.id + if ($Group.displayName -like $SingleName) { + $Group.id } } } @@ -177,7 +177,6 @@ function Set-CIPPAssignedApplication { } if ($PSCmdlet.ShouldProcess($GroupName, "Assigning Application $ApplicationId")) { Start-Sleep -Seconds 1 - # Write-Information (ConvertTo-Json $DefaultAssignmentObject -Depth 10) $null = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/deviceAppManagement/mobileApps/$($ApplicationId)/assign" -tenantid $TenantFilter -type POST -body ($DefaultAssignmentObject | ConvertTo-Json -Compress -Depth 10) Write-LogMessage -headers $Headers -API $APIName -message "Assigned Application $ApplicationId to $($GroupName)" -Sev 'Info' -tenant $TenantFilter } From 3813692f60b13fa7433c30228608ff6850762f8a Mon Sep 17 00:00:00 2001 From: John Duprey Date: Wed, 14 Jan 2026 22:31:51 -0500 Subject: [PATCH 145/503] Fix assignment logic in Invoke-AddStoreApp Corrects the assignment of the $assignTo variable to use the value of CustomGroup when AssignTo is 'customGroup'. Also updates function definition to use lowercase 'function' for consistency. --- .../Endpoint/Applications/Invoke-AddStoreApp.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-AddStoreApp.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-AddStoreApp.ps1 index 6b34aaaea5d4..af6eb44c1be7 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-AddStoreApp.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-AddStoreApp.ps1 @@ -1,4 +1,4 @@ -Function Invoke-AddStoreApp { +function Invoke-AddStoreApp { <# .FUNCTIONALITY Entrypoint @@ -13,7 +13,7 @@ Function Invoke-AddStoreApp { $WinGetApp = $Request.Body - $assignTo = $Request.body.AssignTo + $assignTo = $Request.Body.AssignTo -eq 'customGroup' ? $Request.Body.CustomGroup : $Request.Body.AssignTo if ($ChocoApp.InstallAsSystem) { 'system' } else { 'user' } $WinGetData = [ordered]@{ From bb22fbc7f78fbb848a71063fd8e1f8758e25ada2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= <31723128+kris6673@users.noreply.github.com> Date: Thu, 15 Jan 2026 11:07:08 +0100 Subject: [PATCH 146/503] Fix: Update return message for license assignment --- Modules/CIPPCore/Public/Set-CIPPUserLicense.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Set-CIPPUserLicense.ps1 b/Modules/CIPPCore/Public/Set-CIPPUserLicense.ps1 index 19b9db51942c..e72fb7b69701 100644 --- a/Modules/CIPPCore/Public/Set-CIPPUserLicense.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPUserLicense.ps1 @@ -55,5 +55,5 @@ function Set-CIPPUserLicense { } Write-LogMessage -Headers $Headers -API $APIName -tenant $TenantFilter -message "Assigned licenses to user $UserId. Added: $AddLicenses; Removed: $RemoveLicenses" -Sev 'Info' - return 'Set licenses successfully' + return "Successfully set licenses for $UserId. It may take 2–5 minutes before the changes become visible." } From ce3125ac617c18bc3231b2096ab52fdf8672b3c0 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 15 Jan 2026 11:38:54 -0500 Subject: [PATCH 147/503] Require tenantFilter in Invoke-AddUser endpoint Added validation to ensure tenantFilter is present in the request body when creating a user. Returns a BadRequest response if tenantFilter is missing to prevent incomplete user creation. --- .../Administration/Users/Invoke-AddUser.ps1 | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-AddUser.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-AddUser.ps1 index 8d679686c945..ccc2dff99f7c 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-AddUser.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-AddUser.ps1 @@ -14,6 +14,18 @@ function Invoke-AddUser { $UserObj = $Request.Body + if (!$UserObj.tenantFilter) { + return ([HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::BadRequest + Body = [pscustomobject]@{ + 'Results' = @{ + resultText = 'tenantFilter is required to create a user.' + state = 'error' + } + } + }) + } + if ($UserObj.Scheduled.Enabled) { $Username = $UserObj.username ?? $UserObj.mailNickname $TaskBody = [pscustomobject]@{ @@ -23,10 +35,10 @@ function Invoke-AddUser { value = 'New-CIPPUserTask' label = 'New-CIPPUserTask' } - Parameters = [pscustomobject]@{ UserObj = $UserObj } - ScheduledTime = $UserObj.Scheduled.date - Reference = $UserObj.reference ?? $null - PostExecution = @{ + Parameters = [pscustomobject]@{ UserObj = $UserObj } + ScheduledTime = $UserObj.Scheduled.date + Reference = $UserObj.reference ?? $null + PostExecution = @{ Webhook = [bool]$Request.Body.PostExecution.Webhook Email = [bool]$Request.Body.PostExecution.Email PSA = [bool]$Request.Body.PostExecution.PSA From ecce6fc645330dee211e033c5175064dcfb46e45 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Thu, 15 Jan 2026 23:28:48 +0100 Subject: [PATCH 148/503] reliability changes --- Modules/CIPPCore/Public/Get-CIPPDrift.ps1 | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/Modules/CIPPCore/Public/Get-CIPPDrift.ps1 b/Modules/CIPPCore/Public/Get-CIPPDrift.ps1 index e4915dadb20c..a0e26898dcc3 100644 --- a/Modules/CIPPCore/Public/Get-CIPPDrift.ps1 +++ b/Modules/CIPPCore/Public/Get-CIPPDrift.ps1 @@ -103,24 +103,30 @@ function Get-CIPPDrift { # Reset displayName and description for each deviation to prevent carryover from previous iterations $displayName = $null $standardDescription = $null - #if the $ComparisonItem.StandardName contains *intuneTemplate*, then it's an Intune policy deviation, and we need to grab the correct displayname from the template table - if ($ComparisonItem.StandardName -like '*intuneTemplate*') { - $CompareGuid = $ComparisonItem.StandardName.Split('.') | Select-Object -Index 2 - Write-Verbose "Extracted GUID: $CompareGuid" + #if the $ComparisonItem.StandardName contains *IntuneTemplate*, then it's an Intune policy deviation, and we need to grab the correct displayname from the template table + if ($ComparisonItem.StandardName -like '*IntuneTemplate*') { + $CompareGuid = $ComparisonItem.StandardName.Split('.') | Select-Object -Last 1 + Write-Verbose "Extracted Intune GUID: $CompareGuid from $($ComparisonItem.StandardName)" $Template = $AllIntuneTemplates | Where-Object { $_.GUID -eq "$CompareGuid" } if ($Template) { $displayName = $Template.displayName $standardDescription = $Template.description + Write-Verbose "Found Intune template: $displayName" + } else { + Write-Warning "Intune template not found for GUID: $CompareGuid" } } # Handle Conditional Access templates if ($ComparisonItem.StandardName -like '*ConditionalAccessTemplate*') { - $CompareGuid = $ComparisonItem.StandardName.Split('.') | Select-Object -Index 2 - Write-Verbose "Extracted CA GUID: $CompareGuid" + $CompareGuid = $ComparisonItem.StandardName.Split('.') | Select-Object -Last 1 + Write-Verbose "Extracted CA GUID: $CompareGuid from $($ComparisonItem.StandardName)" $Template = $AllCATemplates | Where-Object { $_.GUID -eq "$CompareGuid" } if ($Template) { $displayName = $Template.displayName $standardDescription = $Template.description + Write-Verbose "Found CA template: $displayName" + } else { + Write-Warning "CA template not found for GUID: $CompareGuid" } } $reason = if ($ExistingDriftStates.ContainsKey($ComparisonItem.StandardName)) { $ExistingDriftStates[$ComparisonItem.StandardName].Reason } From 8bbd74a9893c982586cc837e6dad0890c486b01b Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Thu, 15 Jan 2026 23:37:28 +0100 Subject: [PATCH 149/503] bug fixes drift --- Modules/CIPPCore/Public/Get-CIPPDrift.ps1 | 59 +++++++++++------------ 1 file changed, 29 insertions(+), 30 deletions(-) diff --git a/Modules/CIPPCore/Public/Get-CIPPDrift.ps1 b/Modules/CIPPCore/Public/Get-CIPPDrift.ps1 index a0e26898dcc3..bca1388b5b65 100644 --- a/Modules/CIPPCore/Public/Get-CIPPDrift.ps1 +++ b/Modules/CIPPCore/Public/Get-CIPPDrift.ps1 @@ -32,37 +32,36 @@ function Get-CIPPDrift { $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') $IntuneTable = Get-CippTable -tablename 'templates' - if ($IntuneCapable) { - $IntuneFilter = "PartitionKey eq 'IntuneTemplate'" - $RawIntuneTemplates = (Get-CIPPAzDataTableEntity @IntuneTable -Filter $IntuneFilter) - $AllIntuneTemplates = $RawIntuneTemplates | ForEach-Object { - try { - $JSONData = $_.JSON | ConvertFrom-Json -Depth 10 -ErrorAction SilentlyContinue - $data = $JSONData.RAWJson | ConvertFrom-Json -Depth 10 -ErrorAction SilentlyContinue - $data | Add-Member -NotePropertyName 'displayName' -NotePropertyValue $JSONData.Displayname -Force - $data | Add-Member -NotePropertyName 'description' -NotePropertyValue $JSONData.Description -Force - $data | Add-Member -NotePropertyName 'Type' -NotePropertyValue $JSONData.Type -Force - $data | Add-Member -NotePropertyName 'GUID' -NotePropertyValue $_.RowKey -Force - $data - } catch { - # Skip invalid templates - } - } | Sort-Object -Property displayName - } + + # Always load templates for display name resolution, even if tenant doesn't have licenses + $IntuneFilter = "PartitionKey eq 'IntuneTemplate'" + $RawIntuneTemplates = (Get-CIPPAzDataTableEntity @IntuneTable -Filter $IntuneFilter) + $AllIntuneTemplates = $RawIntuneTemplates | ForEach-Object { + try { + $JSONData = $_.JSON | ConvertFrom-Json -Depth 10 -ErrorAction SilentlyContinue + $data = $JSONData.RAWJson | ConvertFrom-Json -Depth 10 -ErrorAction SilentlyContinue + $data | Add-Member -NotePropertyName 'displayName' -NotePropertyValue $JSONData.Displayname -Force + $data | Add-Member -NotePropertyName 'description' -NotePropertyValue $JSONData.Description -Force + $data | Add-Member -NotePropertyName 'Type' -NotePropertyValue $JSONData.Type -Force + $data | Add-Member -NotePropertyName 'GUID' -NotePropertyValue $_.RowKey -Force + $data + } catch { + # Skip invalid templates + } + } | Sort-Object -Property displayName + # Load all CA templates - if ($ConditionalAccessCapable) { - $CAFilter = "PartitionKey eq 'CATemplate'" - $RawCATemplates = (Get-CIPPAzDataTableEntity @IntuneTable -Filter $CAFilter) - $AllCATemplates = $RawCATemplates | ForEach-Object { - try { - $data = $_.JSON | ConvertFrom-Json -Depth 100 -ErrorAction SilentlyContinue - $data | Add-Member -NotePropertyName 'GUID' -NotePropertyValue $_.RowKey -Force - $data - } catch { - # Skip invalid templates - } - } | Sort-Object -Property displayName - } + $CAFilter = "PartitionKey eq 'CATemplate'" + $RawCATemplates = (Get-CIPPAzDataTableEntity @IntuneTable -Filter $CAFilter) + $AllCATemplates = $RawCATemplates | ForEach-Object { + try { + $data = $_.JSON | ConvertFrom-Json -Depth 100 -ErrorAction SilentlyContinue + $data | Add-Member -NotePropertyName 'GUID' -NotePropertyValue $_.RowKey -Force + $data + } catch { + # Skip invalid templates + } + } | Sort-Object -Property displayName try { $AlignmentData = Get-CIPPTenantAlignment -TenantFilter $TenantFilter -TemplateId $TemplateId | Where-Object -Property standardType -EQ 'drift' From b13b1a49b86a04b43593e6ade8a9dbd3adb2340a Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 15 Jan 2026 19:46:32 -0500 Subject: [PATCH 150/503] Improve filtering logic in Get-CIPPDbItem Refactored the filtering logic for the CountsOnly path to support combined TenantFilter and Type conditions. Now uses a list to build filter expressions and selects only relevant properties for results. --- Modules/CIPPCore/Public/Get-CIPPDbItem.ps1 | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/Modules/CIPPCore/Public/Get-CIPPDbItem.ps1 b/Modules/CIPPCore/Public/Get-CIPPDbItem.ps1 index 4ec9970979e0..e72d59d10203 100644 --- a/Modules/CIPPCore/Public/Get-CIPPDbItem.ps1 +++ b/Modules/CIPPCore/Public/Get-CIPPDbItem.ps1 @@ -37,13 +37,16 @@ function Get-CIPPDbItem { $Table = Get-CippTable -tablename 'CippReportingDB' if ($CountsOnly) { - if ($TenantFilter -eq 'allTenants') { - $Filter = $null - } else { - $Filter = "PartitionKey eq '{0}'" -f $TenantFilter + $Conditions = [System.Collections.Generic.List[string]]::new() + if ($TenantFilter -ne 'allTenants') { + $Conditions.Add("PartitionKey eq '{0}'" -f $TenantFilter) } - $Results = Get-CIPPAzDataTableEntity @Table -Filter $Filter - $Results = $Results | Where-Object { $_.RowKey -like '*-Count' } + if ($Type) { + $Conditions.Add("RowKey ge '{0}-' and RowKey lt '{0}.'" -f $Type) + } + $Filter = [string]::Join(' and ', $Conditions) + $Results = Get-CIPPAzDataTableEntity @Table -Filter $Filter -Property 'PartitionKey', 'RowKey', 'DataCount', 'Timestamp' + $Results = $Results | Where-Object { $_.RowKey -like '*-Count' } | Select-Object PartitionKey, RowKey, DataCount, Timestamp } else { if (-not $Type) { throw 'Type parameter is required when CountsOnly is not specified' From 095a981f9eda04e81d0ce40b2c66334d11ac8ff7 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 15 Jan 2026 19:46:46 -0500 Subject: [PATCH 151/503] Add Get-CIPPMailboxPermissionReport function Introduces a new function to generate mailbox permission reports from the CIPP Reporting database. Supports grouping results by mailbox or by user, and includes error handling and logging. --- .../Get-CIPPMailboxPermissionReport.ps1 | 177 ++++++++++++++++++ 1 file changed, 177 insertions(+) create mode 100644 Modules/CIPPCore/Public/Get-CIPPMailboxPermissionReport.ps1 diff --git a/Modules/CIPPCore/Public/Get-CIPPMailboxPermissionReport.ps1 b/Modules/CIPPCore/Public/Get-CIPPMailboxPermissionReport.ps1 new file mode 100644 index 000000000000..0ad921dd7877 --- /dev/null +++ b/Modules/CIPPCore/Public/Get-CIPPMailboxPermissionReport.ps1 @@ -0,0 +1,177 @@ +function Get-CIPPMailboxPermissionReport { + <# + .SYNOPSIS + Generates a mailbox permission report from the CIPP Reporting database + + .DESCRIPTION + Retrieves mailbox permissions for a tenant and formats them into a report. + Default view shows permissions per mailbox. Use -ByUser to pivot by user. + + .PARAMETER TenantFilter + The tenant to generate the report for + + .PARAMETER ByUser + If specified, groups results by user instead of by mailbox + + .EXAMPLE + Get-CIPPMailboxPermissionReport -TenantFilter 'contoso.onmicrosoft.com' + Shows which users have access to each mailbox + + .EXAMPLE + Get-CIPPMailboxPermissionReport -TenantFilter 'contoso.onmicrosoft.com' -ByUser + Shows what mailboxes each user has access to + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter, + + [Parameter(Mandatory = $false)] + [switch]$ByUser + ) + + try { + Write-LogMessage -API 'MailboxPermissionReport' -tenant $TenantFilter -message 'Generating mailbox permission report' -sev Info + + # Get mailboxes from reporting DB + $MailboxItems = Get-CIPPDbItem -TenantFilter $TenantFilter -Type 'Mailboxes' + if (-not $MailboxItems) { + throw 'No mailbox data found in reporting database. Run Set-CIPPDBCacheMailboxes first.' + } + + # Get the most recent mailbox cache timestamp + $MailboxCacheTimestamp = ($MailboxItems | Where-Object { $_.Timestamp } | Sort-Object Timestamp -Descending | Select-Object -First 1).Timestamp + + # Parse mailbox data and create lookup by UPN, ID, and ExternalDirectoryObjectId (case-insensitive) + $MailboxLookup = @{} + $MailboxByIdLookup = @{} + $MailboxByExternalIdLookup = @{} + foreach ($Item in $MailboxItems | Where-Object { $_.RowKey -ne 'Mailboxes-Count' }) { + $Mailbox = $Item.Data | ConvertFrom-Json + if ($Mailbox.UPN) { + $MailboxLookup[$Mailbox.UPN.ToLower()] = $Mailbox + } + if ($Mailbox.primarySmtpAddress) { + $MailboxLookup[$Mailbox.primarySmtpAddress.ToLower()] = $Mailbox + } + if ($Mailbox.Id) { + $MailboxByIdLookup[$Mailbox.Id] = $Mailbox + } + if ($Mailbox.ExternalDirectoryObjectId) { + $MailboxByExternalIdLookup[$Mailbox.ExternalDirectoryObjectId] = $Mailbox + } + } + + # Get mailbox permissions from reporting DB + $PermissionItems = Get-CIPPDbItem -TenantFilter $TenantFilter -Type 'MailboxPermissions' + if (-not $PermissionItems) { + throw 'No mailbox permission data found in reporting database. Run Set-CIPPDBCacheMailboxes first.' + } + + # Get the most recent permission cache timestamp + $PermissionCacheTimestamp = ($PermissionItems | Where-Object { $_.Timestamp } | Sort-Object Timestamp -Descending | Select-Object -First 1).Timestamp + + # Parse all permissions + $AllPermissions = [System.Collections.Generic.List[PSCustomObject]]::new() + foreach ($Item in $PermissionItems | Where-Object { $_.RowKey -ne 'MailboxPermissions-Count' }) { + $Permissions = $Item.Data | ConvertFrom-Json + foreach ($Permission in $Permissions) { + # Skip SELF permissions and inherited deny permissions + if ($Permission.User -eq 'NT AUTHORITY\SELF' -or $Permission.Deny -eq $true) { + continue + } + + # Get mailbox info - try multiple match strategies like CustomDataSync does + $Mailbox = $null + if ($Permission.Identity) { + # Try UPN/primarySmtpAddress lookup (case-insensitive) + $Mailbox = $MailboxLookup[$Permission.Identity.ToLower()] + + # If not found, try ExternalDirectoryObjectId lookup + if (-not $Mailbox) { + $Mailbox = $MailboxByExternalIdLookup[$Permission.Identity] + } + + # If not found, try ID lookup + if (-not $Mailbox) { + $Mailbox = $MailboxByIdLookup[$Permission.Identity] + } + } + + if (-not $Mailbox) { + Write-Verbose "No mailbox found for Identity: $($Permission.Identity)" + continue + } + + $AllPermissions.Add([PSCustomObject]@{ + MailboxUPN = if ($Mailbox.UPN) { $Mailbox.UPN } elseif ($Mailbox.primarySmtpAddress) { $Mailbox.primarySmtpAddress } else { $Permission.Identity } + MailboxDisplayName = $Mailbox.displayName + MailboxType = $Mailbox.recipientTypeDetails + User = $Permission.User + UserKey = if ($Permission.User -match '@') { $Permission.User.ToLower() } else { $Permission.User } + AccessRights = ($Permission.AccessRights -join ', ') + IsInherited = $Permission.IsInherited + Deny = $Permission.Deny + }) + } + } + + if ($AllPermissions.Count -eq 0) { + Write-LogMessage -API 'MailboxPermissionReport' -tenant $TenantFilter -message 'No mailbox permissions found (excluding SELF)' -sev Debug + Write-Information -Message 'No mailbox permissions found (excluding SELF)' + return @() + } + + # Format results based on grouping preference + if ($ByUser) { + # Group by user - calculate which mailboxes each user has access to + # Use UserKey for grouping to handle case-insensitive email addresses + $Report = $AllPermissions | Group-Object -Property UserKey | ForEach-Object { + $UserKey = $_.Name + $UserDisplay = $_.Group[0].User # Use original User value for display + + # Build detailed permissions list with mailbox and access rights + $PermissionDetails = $_.Group | ForEach-Object { + [PSCustomObject]@{ + Mailbox = $_.MailboxDisplayName + MailboxUPN = $_.MailboxUPN + AccessRights = $_.AccessRights + } + } + + [PSCustomObject]@{ + User = $UserDisplay + UserType = if ($UserDisplay -match '@') { 'Email/UPN' } else { 'Display Name' } + MailboxCount = $_.Count + Permissions = $PermissionDetails + MailboxCacheTimestamp = $MailboxCacheTimestamp + PermissionCacheTimestamp = $PermissionCacheTimestamp + } + } | Sort-Object User + } else { + # Default: Group by mailbox + $Report = $AllPermissions | Group-Object -Property MailboxUPN | ForEach-Object { + $MailboxUPN = $_.Name + $MailboxInfo = $_.Group[0] + + [PSCustomObject]@{ + MailboxUPN = $MailboxUPN + MailboxDisplayName = $MailboxInfo.MailboxDisplayName + MailboxType = $MailboxInfo.MailboxType + PermissionCount = $_.Count + Users = ($_.Group | Select-Object -ExpandProperty User | Sort-Object -Unique) -join '; ' + Permissions = ($_.Group | ForEach-Object { "$($_.User) ($($_.AccessRights))" }) -join '; ' + MailboxCacheTimestamp = $MailboxCacheTimestamp + PermissionCacheTimestamp = $PermissionCacheTimestamp + } + } | Sort-Object MailboxDisplayName + } + + Write-LogMessage -API 'MailboxPermissionReport' -tenant $TenantFilter -message "Generated report with $($Report.Count) entries" -sev Debug + return $Report + + } catch { + Write-LogMessage -API 'MailboxPermissionReport' -tenant $TenantFilter -message "Failed to generate mailbox permission report: $($_.Exception.Message)" -sev Error -LogData (Get-CippException -Exception $_) + throw "Failed to generate mailbox permission report: $($_.Exception.Message)" + } +} From f57e45886a84d44668bcb8956734a86ca04436c9 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 15 Jan 2026 19:47:18 -0500 Subject: [PATCH 152/503] Refactor Intune policy caching to use bulk Graph requests Replaces individual requests with Microsoft Graph bulk requests for fetching Intune policy types, assignments, and device statuses. Improves performance and efficiency by batching requests, adds support for expanded assignment and device status retrieval, and enhances error handling and logging. Includes device statuses as well. --- .../Public/Set-CIPPDBCacheIntunePolicies.ps1 | 130 +++++++++++++----- 1 file changed, 99 insertions(+), 31 deletions(-) diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheIntunePolicies.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheIntunePolicies.ps1 index cac6e1120670..77aebe9bfa1d 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheIntunePolicies.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheIntunePolicies.ps1 @@ -23,46 +23,114 @@ function Set-CIPPDBCacheIntunePolicies { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching Intune policies' -sev Info $PolicyTypes = @( - @{ Type = 'DeviceCompliancePolicies'; Uri = 'https://graph.microsoft.com/beta/deviceManagement/deviceCompliancePolicies'; SupportsExpand = $true } - @{ Type = 'DeviceConfigurations'; Uri = 'https://graph.microsoft.com/beta/deviceManagement/deviceConfigurations'; SupportsExpand = $true } - @{ Type = 'ConfigurationPolicies'; Uri = 'https://graph.microsoft.com/beta/deviceManagement/configurationPolicies'; SupportsExpand = $true; ExpandSettings = $true } - @{ Type = 'GroupPolicyConfigurations'; Uri = 'https://graph.microsoft.com/beta/deviceManagement/groupPolicyConfigurations'; SupportsExpand = $true } - @{ Type = 'MobileAppConfigurations'; Uri = 'https://graph.microsoft.com/beta/deviceManagement/mobileAppConfigurations'; SupportsExpand = $true } - @{ Type = 'AppProtectionPolicies'; Uri = 'https://graph.microsoft.com/beta/deviceAppManagement/managedAppPolicies'; SupportsExpand = $false } - @{ Type = 'WindowsAutopilotDeploymentProfiles'; Uri = 'https://graph.microsoft.com/beta/deviceManagement/windowsAutopilotDeploymentProfiles'; SupportsExpand = $true } - @{ Type = 'DeviceEnrollmentConfigurations'; Uri = 'https://graph.microsoft.com/beta/deviceManagement/deviceEnrollmentConfigurations'; SupportsExpand = $false } - @{ Type = 'DeviceManagementScripts'; Uri = 'https://graph.microsoft.com/beta/deviceManagement/deviceManagementScripts'; SupportsExpand = $true } - @{ Type = 'MobileApps'; Uri = 'https://graph.microsoft.com/beta/deviceAppManagement/mobileApps'; SupportsExpand = $false } + @{ Type = 'DeviceCompliancePolicies'; Uri = '/deviceManagement/deviceCompliancePolicies?$top=999&$expand=assignments'; FetchDeviceStatuses = $true } + @{ Type = 'DeviceConfigurations'; Uri = '/deviceManagement/deviceConfigurations?$top=999&$expand=assignments' } + @{ Type = 'ConfigurationPolicies'; Uri = '/deviceManagement/configurationPolicies?$top=999&$expand=assignments,settings' } + @{ Type = 'GroupPolicyConfigurations'; Uri = '/deviceManagement/groupPolicyConfigurations?$top=999&$expand=assignments' } + @{ Type = 'MobileAppConfigurations'; Uri = '/deviceManagement/mobileAppConfigurations?$top=999&$expand=assignments' } + @{ Type = 'AppProtectionPolicies'; Uri = '/deviceAppManagement/managedAppPolicies?$top=999'; FetchAssignments = $true } + @{ Type = 'WindowsAutopilotDeploymentProfiles'; Uri = '/deviceManagement/windowsAutopilotDeploymentProfiles?$top=999&$expand=assignments' } + @{ Type = 'DeviceEnrollmentConfigurations'; Uri = '/deviceManagement/deviceEnrollmentConfigurations?$top=999'; FetchAssignments = $true } + @{ Type = 'DeviceManagementScripts'; Uri = '/deviceManagement/deviceManagementScripts?$top=999&$expand=assignments' } + @{ Type = 'MobileApps'; Uri = '/deviceAppManagement/mobileApps?$top=999&$select=id,displayName,description,publisher,isAssigned,createdDateTime,lastModifiedDateTime'; FetchAssignments = $true } ) - foreach ($PolicyType in $PolicyTypes) { + # Build bulk requests for all policy types + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Fetching all policy types using bulk request' -sev Info + $PolicyRequests = foreach ($PolicyType in $PolicyTypes) { + [PSCustomObject]@{ + id = $PolicyType.Type + method = 'GET' + url = $PolicyType.Uri + } + } + + try { + $PolicyResults = New-GraphBulkRequest -Requests @($PolicyRequests) -tenantid $TenantFilter + } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to fetch policies in bulk: $($_.Exception.Message)" -sev Error + throw + } + + # Process each policy type result + foreach ($Result in $PolicyResults) { + $PolicyType = $PolicyTypes | Where-Object { $_.Type -eq $Result.id } + if (-not $PolicyType) { continue } + try { - $UriWithParams = $PolicyType.Uri + '?$top=999' - if ($PolicyType.SupportsExpand) { - $UriWithParams += '&$expand=assignments' - } - if ($PolicyType.ExpandSettings) { - $UriWithParams += ',settings' + $Policies = $Result.body.value ?? $Result.body + + if (-not $Policies) { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "No policies found for $($PolicyType.Type)" -sev Debug + continue } - $Policies = New-GraphGetRequest -uri $UriWithParams -tenantid $TenantFilter - - if ($Policies) { - if (-not $PolicyType.SupportsExpand) { - foreach ($Policy in $Policies) { - try { - $AssignmentUri = "$($PolicyType.Uri)/$($Policy.id)/assignments" - $Assignments = New-GraphGetRequest -uri $AssignmentUri -tenantid $TenantFilter - $Policy | Add-Member -NotePropertyName 'assignments' -NotePropertyValue $Assignments -Force - } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to get assignments for $($Policy.id): $($_.Exception.Message)" -sev Verbose + # Get assignments for policies that don't support expand using bulk requests + if ($PolicyType.FetchAssignments -and ($Policies | Measure-Object).Count -gt 0) { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Fetching assignments for $($Policies.Count) $($PolicyType.Type) using bulk request" -sev Debug + + $BaseUri = ($PolicyType.Uri -split '\?')[0] + # Build bulk request array for assignments + $AssignmentRequests = $Policies | ForEach-Object { + [PSCustomObject]@{ + id = $_.id + method = 'GET' + url = "$BaseUri/$($_.id)/assignments" + } + } + + try { + $AssignmentResults = New-GraphBulkRequest -Requests @($AssignmentRequests) -tenantid $TenantFilter + + if ($AssignmentResults) { + foreach ($AssignResult in $AssignmentResults) { + $Policy = $Policies | Where-Object { $_.id -eq $AssignResult.id } + if ($Policy) { + $Assignments = $AssignResult.body.value ?? $AssignResult.body + $Policy | Add-Member -NotePropertyName 'assignments' -NotePropertyValue $Assignments -Force + } } } + } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to fetch assignments in bulk for $($PolicyType.Type): $($_.Exception.Message)" -sev Warning } + } + + Add-CIPPDbItem -TenantFilter $TenantFilter -Type "Intune$($PolicyType.Type)" -Data $Policies + Add-CIPPDbItem -TenantFilter $TenantFilter -Type "Intune$($PolicyType.Type)" -Data $Policies -Count + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($Policies.Count) $($PolicyType.Type)" -sev Info - Add-CIPPDbItem -TenantFilter $TenantFilter -Type "Intune$($PolicyType.Type)" -Data $Policies - Add-CIPPDbItem -TenantFilter $TenantFilter -Type "Intune$($PolicyType.Type)" -Data $Policies -Count - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($Policies.Count) $($PolicyType.Type)" -sev Info + # Fetch device statuses for compliance policies using bulk requests + if ($PolicyType.FetchDeviceStatuses -and ($Policies | Measure-Object).Count -gt 0) { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Fetching device statuses for $($Policies.Count) compliance policies using bulk request" -sev Info + + $BaseUri = ($PolicyType.Uri -split '\?')[0] + # Build bulk request array + $DeviceStatusRequests = $Policies | ForEach-Object { + [PSCustomObject]@{ + id = $_.id + method = 'GET' + url = "$BaseUri/$($_.id)/deviceStatuses?`$top=999" + } + } + + try { + $DeviceStatusResults = New-GraphBulkRequest -Requests @($DeviceStatusRequests) -tenantid $TenantFilter + + if ($DeviceStatusResults) { + foreach ($StatusResult in $DeviceStatusResults) { + $Data = $StatusResult.body.value ?? $StatusResult.body + if ($Data) { + # Store device statuses with policy ID in the type name (matching extension cache pattern) + $StatusType = "Intune$($PolicyType.Type)_$($StatusResult.id)" + Add-CIPPDbItem -TenantFilter $TenantFilter -Type $StatusType -Data $Data + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $(($Data | Measure-Object).Count) device statuses for policy ID $($StatusResult.id)" -sev Debug + } + } + } + } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to fetch device statuses in bulk: $($_.Exception.Message)" -sev Warning + } } $Policies = $null From 6f882521d10e3ae354c8dd588658697752028e1f Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 15 Jan 2026 20:53:58 -0500 Subject: [PATCH 153/503] Add mailbox and OneDrive usage cache functions Introduces Set-CIPPDBCacheMailboxUsage and Set-CIPPDBCacheOneDriveUsage functions to cache mailbox and OneDrive usage details for tenants. Updates Push-CIPPDBCacheData to invoke these new functions and handle errors accordingly. --- .../Push-CIPPDBCacheData.ps1 | 10 +++++++ .../Public/Set-CIPPDBCacheMailboxUsage.ps1 | 27 +++++++++++++++++++ .../Public/Set-CIPPDBCacheOneDriveUsage.ps1 | 27 +++++++++++++++++++ 3 files changed, 64 insertions(+) create mode 100644 Modules/CIPPCore/Public/Set-CIPPDBCacheMailboxUsage.ps1 create mode 100644 Modules/CIPPCore/Public/Set-CIPPDBCacheOneDriveUsage.ps1 diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 index 07b0fc9c5f7b..d2c0f155009e 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 @@ -367,6 +367,16 @@ function Push-CIPPDBCacheData { try { Set-CIPPDBCacheMailboxes -TenantFilter $TenantFilter } catch { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Mailboxes collection failed: $($_.Exception.Message)" -sev Error } + + Write-Host 'Getting cache for MailboxUsage' + try { Set-CIPPDBCacheMailboxUsage -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "MailboxUsage collection failed: $($_.Exception.Message)" -sev Error + } + + Write-Host 'Getting cache for OneDriveUsage' + try { Set-CIPPDBCacheOneDriveUsage -TenantFilter $TenantFilter } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "OneDriveUsage collection failed: $($_.Exception.Message)" -sev Error + } } else { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Skipping Mailboxes data collection - tenant does not have required Exchange license' -sev Info } diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheMailboxUsage.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheMailboxUsage.ps1 new file mode 100644 index 000000000000..fdfaef374765 --- /dev/null +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheMailboxUsage.ps1 @@ -0,0 +1,27 @@ +function Set-CIPPDBCacheMailboxUsage { + <# + .SYNOPSIS + Caches mailbox usage details for a tenant + + .PARAMETER TenantFilter + The tenant to cache mailbox usage for + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter + ) + + try { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching mailbox usage' -sev Info + + $MailboxUsage = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/reports/getMailboxUsageDetail(period='D7')?`$format=application%2fjson" -tenantid $TenantFilter + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'MailboxUsage' -Data $MailboxUsage + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'MailboxUsage' -Data $MailboxUsage -Count + $MailboxUsage = $null + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached mailbox usage successfully' -sev Info + + } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache mailbox usage: $($_.Exception.Message)" -sev Error + } +} diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheOneDriveUsage.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheOneDriveUsage.ps1 new file mode 100644 index 000000000000..7ee193199394 --- /dev/null +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheOneDriveUsage.ps1 @@ -0,0 +1,27 @@ +function Set-CIPPDBCacheOneDriveUsage { + <# + .SYNOPSIS + Caches OneDrive usage details for a tenant + + .PARAMETER TenantFilter + The tenant to cache OneDrive usage for + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter + ) + + try { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching OneDrive usage' -sev Info + + $OneDriveUsage = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/reports/getOneDriveUsageAccountDetail(period='D7')?`$format=application%2fjson" -tenantid $TenantFilter + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'OneDriveUsage' -Data $OneDriveUsage + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'OneDriveUsage' -Data $OneDriveUsage -Count + $OneDriveUsage = $null + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached OneDrive usage successfully' -sev Info + + } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache OneDrive usage: $($_.Exception.Message)" -sev Error + } +} From 44a894264e22ee401a47e13508ef0e8a3d31d4fc Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 15 Jan 2026 20:54:13 -0500 Subject: [PATCH 154/503] Switch to CIPP Reporting DB for extension sync Added Get-CippExtensionReportingData to retrieve extension sync data from the new CIPP Reporting DB, replacing legacy cache calls. Updated Invoke-HuduExtensionSync to use the new function and handle inline members for roles and groups, and changed device compliance policy status retrieval. Improved API key retrieval logic in Get-ExtensionAPIKey. --- .../Get-CippExtensionReportingData.ps1 | 102 ++++++++++++++++++ .../Get-ExtensionAPIKey.ps1 | 2 +- .../Public/Hudu/Invoke-HuduExtensionSync.ps1 | 16 +-- 3 files changed, 113 insertions(+), 7 deletions(-) create mode 100644 Modules/CippExtensions/Public/Extension Functions/Get-CippExtensionReportingData.ps1 diff --git a/Modules/CippExtensions/Public/Extension Functions/Get-CippExtensionReportingData.ps1 b/Modules/CippExtensions/Public/Extension Functions/Get-CippExtensionReportingData.ps1 new file mode 100644 index 000000000000..ebbc55fd58e7 --- /dev/null +++ b/Modules/CippExtensions/Public/Extension Functions/Get-CippExtensionReportingData.ps1 @@ -0,0 +1,102 @@ +function Get-CippExtensionReportingData { + <# + .SYNOPSIS + Retrieves cached data from CIPP Reporting DB for extension sync + + .DESCRIPTION + This function replaces Get-ExtensionCacheData by retrieving data from the new CIPP Reporting DB + instead of the legacy CacheExtensionSync table. It handles property mappings and data transformations + to maintain compatibility with existing extension sync code. + + .PARAMETER TenantFilter + The tenant to retrieve data for + + .PARAMETER IncludeMailboxes + Include mailbox data (requires separate cache run with Type 'Mailboxes') + + .EXAMPLE + $ExtensionCache = Get-CippExtensionReportingData -TenantFilter 'contoso.onmicrosoft.com' + + .EXAMPLE + $ExtensionCache = Get-CippExtensionReportingData -TenantFilter 'contoso.onmicrosoft.com' -IncludeMailboxes + + .FUNCTIONALITY + Internal + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter, + + [Parameter(Mandatory = $false)] + [switch]$IncludeMailboxes + ) + + try { + $Return = @{} + + # Direct mappings - loop through items and parse each .Data property (filter out count entries) + $UsersItems = Get-CIPPDbItem -TenantFilter $TenantFilter -Type 'Users' | Where-Object { $_.RowKey -notlike '*-Count' } + $Return.Users = if ($UsersItems) { $UsersItems | ForEach-Object { $_.Data | ConvertFrom-Json } } else { @() } + + $DomainsItems = Get-CIPPDbItem -TenantFilter $TenantFilter -Type 'Domains' | Where-Object { $_.RowKey -notlike '*-Count' } + $Return.Domains = if ($DomainsItems) { $DomainsItems | ForEach-Object { $_.Data | ConvertFrom-Json } } else { @() } + + $ConditionalAccessItems = Get-CIPPDbItem -TenantFilter $TenantFilter -Type 'ConditionalAccessPolicies' | Where-Object { $_.RowKey -notlike '*-Count' } + $Return.ConditionalAccess = if ($ConditionalAccessItems) { $ConditionalAccessItems | ForEach-Object { $_.Data | ConvertFrom-Json } } else { @() } + + $ManagedDevicesItems = Get-CIPPDbItem -TenantFilter $TenantFilter -Type 'ManagedDevices' | Where-Object { $_.RowKey -notlike '*-Count' } + $Return.Devices = if ($ManagedDevicesItems) { $ManagedDevicesItems | ForEach-Object { $_.Data | ConvertFrom-Json } } else { @() } + + $OrganizationItems = Get-CIPPDbItem -TenantFilter $TenantFilter -Type 'Organization' | Where-Object { $_.RowKey -notlike '*-Count' } + $Return.Organization = if ($OrganizationItems) { ($OrganizationItems | ForEach-Object { $_.Data | ConvertFrom-Json } | Select-Object -First 1) } else { $null } + + # Groups with inline members (members are now in each group object) + $GroupsItems = Get-CIPPDbItem -TenantFilter $TenantFilter -Type 'Groups' | Where-Object { $_.RowKey -notlike '*-Count' } + $Return.Groups = if ($GroupsItems) { $GroupsItems | ForEach-Object { $_.Data | ConvertFrom-Json } } else { @() } + + # Roles with inline members (members are now in each role object) + $RolesItems = Get-CIPPDbItem -TenantFilter $TenantFilter -Type 'Roles' | Where-Object { $_.RowKey -notlike '*-Count' } + $Return.AllRoles = if ($RolesItems) { $RolesItems | ForEach-Object { $_.Data | ConvertFrom-Json } } else { @() } + + # License mapping with property translation to maintain compatibility + $LicenseItems = Get-CIPPDbItem -TenantFilter $TenantFilter -Type 'LicenseOverview' | Where-Object { $_.RowKey -notlike '*-Count' } + if ($LicenseItems) { + $ParsedLicenseData = $LicenseItems | ForEach-Object { $_.Data | ConvertFrom-Json } + $Return.Licenses = $ParsedLicenseData | Select-Object @{N = 'skuId'; E = { $_.skuId } }, + @{N = 'skuPartNumber'; E = { $_.skuPartNumber } }, + @{N = 'consumedUnits'; E = { $_.CountUsed } }, + @{N = 'prepaidUnits'; E = { @{enabled = $_.TotalLicenses } } } + } else { + $Return.Licenses = @() + } + + # Intune policies (renamed from DeviceCompliancePolicies to IntuneDeviceCompliancePolicies) + $IntunePoliciesItems = Get-CIPPDbItem -TenantFilter $TenantFilter -Type 'IntuneDeviceCompliancePolicies' | Where-Object { $_.RowKey -notlike '*-Count' } + $Return.DeviceCompliancePolicies = if ($IntunePoliciesItems) { $IntunePoliciesItems | ForEach-Object { $_.Data | ConvertFrom-Json } } else { @() } + + # Mailboxes (optional - requires separate cache run) + if ($IncludeMailboxes) { + $MailboxesItems = Get-CIPPDbItem -TenantFilter $TenantFilter -Type 'Mailboxes' | Where-Object { $_.RowKey -notlike '*-Count' } + $Return.Mailboxes = if ($MailboxesItems) { $MailboxesItems | ForEach-Object { $_.Data | ConvertFrom-Json } } else { @() } + + $CASMailboxItems = Get-CIPPDbItem -TenantFilter $TenantFilter -Type 'CASMailbox' | Where-Object { $_.RowKey -notlike '*-Count' } + $Return.CASMailbox = if ($CASMailboxItems) { $CASMailboxItems | ForEach-Object { $_.Data | ConvertFrom-Json } } else { @() } + + $MailboxPermissionsItems = Get-CIPPDbItem -TenantFilter $TenantFilter -Type 'MailboxPermissions' | Where-Object { $_.RowKey -notlike '*-Count' } + $Return.MailboxPermissions = if ($MailboxPermissionsItems) { $MailboxPermissionsItems | ForEach-Object { $_.Data | ConvertFrom-Json } } else { @() } + + $OneDriveUsageItems = Get-CIPPDbItem -TenantFilter $TenantFilter -Type 'OneDriveUsage' | Where-Object { $_.RowKey -notlike '*-Count' } + $Return.OneDriveUsage = if ($OneDriveUsageItems) { $OneDriveUsageItems | ForEach-Object { $_.Data | ConvertFrom-Json } } else { @() } + + $MailboxUsageItems = Get-CIPPDbItem -TenantFilter $TenantFilter -Type 'MailboxUsage' | Where-Object { $_.RowKey -notlike '*-Count' } + $Return.MailboxUsage = if ($MailboxUsageItems) { $MailboxUsageItems | ForEach-Object { $_.Data | ConvertFrom-Json } } else { @() } + } + + return $Return + + } catch { + Write-LogMessage -API 'ExtensionCache' -tenant $TenantFilter -message "Failed to retrieve extension reporting data: $($_.Exception.Message)" -sev Error + throw + } +} diff --git a/Modules/CippExtensions/Public/Extension Functions/Get-ExtensionAPIKey.ps1 b/Modules/CippExtensions/Public/Extension Functions/Get-ExtensionAPIKey.ps1 index 5d85b9d5c54b..561893aee457 100644 --- a/Modules/CippExtensions/Public/Extension Functions/Get-ExtensionAPIKey.ps1 +++ b/Modules/CippExtensions/Public/Extension Functions/Get-ExtensionAPIKey.ps1 @@ -12,7 +12,7 @@ function Get-ExtensionAPIKey { $Var = "Ext_$Extension" $APIKey = Get-Item -Path "env:$Var" -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Value - if ($APIKey) { + if ($APIKey -and -not $Force) { Write-Information "Using cached API Key for $Extension" } else { Write-Information "Retrieving API Key for $Extension" diff --git a/Modules/CippExtensions/Public/Hudu/Invoke-HuduExtensionSync.ps1 b/Modules/CippExtensions/Public/Hudu/Invoke-HuduExtensionSync.ps1 index 867c6fd8082e..19b1e8c13def 100644 --- a/Modules/CippExtensions/Public/Hudu/Invoke-HuduExtensionSync.ps1 +++ b/Modules/CippExtensions/Public/Hudu/Invoke-HuduExtensionSync.ps1 @@ -44,8 +44,9 @@ function Invoke-HuduExtensionSync { $CIPPURL = 'https://{0}' -f $Config.Value $EnableCIPP = $true - # Get Hudu Extension Cache - $ExtensionCache = Get-ExtensionCacheData -TenantFilter $Tenant.defaultDomainName + # Get CIPP Extension Reporting Data (from new CippReportingDB) + # Include mailboxes if needed for Hudu sync + $ExtensionCache = Get-CippExtensionReportingData -TenantFilter $Tenant.defaultDomainName -IncludeMailboxes $company_id = $TenantMap.IntegrationId # If tenant not found in mapping table, return error @@ -166,8 +167,8 @@ function Invoke-HuduExtensionSync { $Roles = foreach ($Role in $AllRoles) { - # Get members from cache - $Members = ($ExtensionCache."AllRoles_$($Role.id)") + # Members are now inline with each role object + $Members = $Role.members [PSCustomObject]@{ ID = $Role.id DisplayName = $Role.displayName @@ -254,7 +255,9 @@ function Invoke-HuduExtensionSync { $DeviceCompliancePolicies = $ExtensionCache.DeviceCompliancePolicies $DeviceComplianceDetails = foreach ($Policy in $DeviceCompliancePolicies) { - $DeviceStatuses = $ExtensionCache."DeviceCompliancePolicies_$($Policy.id)" + # Device statuses are cached per policy with new naming: IntuneDeviceCompliancePolicies_{policyId} + $DeviceStatusItems = Get-CIPPDbItem -TenantFilter $TenantFilter -Type "IntuneDeviceCompliancePolicies_$($Policy.id)" | Where-Object { $_.RowKey -notlike '*-Count' } + $DeviceStatuses = if ($DeviceStatusItems) { $DeviceStatusItems | ForEach-Object { $_.Data | ConvertFrom-Json } } else { @() } [pscustomobject]@{ ID = $Policy.id DisplayName = $Policy.displayName @@ -265,7 +268,8 @@ function Invoke-HuduExtensionSync { $AllGroups = $ExtensionCache.Groups $Groups = foreach ($Group in $AllGroups) { - $Members = $ExtensionCache."Groups_$($Group.id)" + # Members are now inline with each group object + $Members = $Group.members [pscustomobject]@{ ID = $Group.id DisplayName = $Group.displayName From 01f3a534660cca73b0573255ce3cadc6e472516d Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 15 Jan 2026 21:12:53 -0500 Subject: [PATCH 155/503] Enhance reporting data and update NinjaOne sync logic Added Secure Score and Secure Score Control Profiles to Get-CippExtensionReportingData. Updated Invoke-NinjaOneTenantSync to use the new reporting data, improved mapping of cached data, and refactored role and group member retrieval to use inline properties instead of separate cache entries. Also adjusted device compliance policy status retrieval to query directly from the database. --- .../Get-CippExtensionReportingData.ps1 | 8 ++++++++ .../NinjaOne/Invoke-NinjaOneTenantSync.ps1 | 20 ++++++++++--------- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/Modules/CippExtensions/Public/Extension Functions/Get-CippExtensionReportingData.ps1 b/Modules/CippExtensions/Public/Extension Functions/Get-CippExtensionReportingData.ps1 index ebbc55fd58e7..894bb7a4469b 100644 --- a/Modules/CippExtensions/Public/Extension Functions/Get-CippExtensionReportingData.ps1 +++ b/Modules/CippExtensions/Public/Extension Functions/Get-CippExtensionReportingData.ps1 @@ -75,6 +75,14 @@ function Get-CippExtensionReportingData { $IntunePoliciesItems = Get-CIPPDbItem -TenantFilter $TenantFilter -Type 'IntuneDeviceCompliancePolicies' | Where-Object { $_.RowKey -notlike '*-Count' } $Return.DeviceCompliancePolicies = if ($IntunePoliciesItems) { $IntunePoliciesItems | ForEach-Object { $_.Data | ConvertFrom-Json } } else { @() } + # Secure Score + $SecureScoreItems = Get-CIPPDbItem -TenantFilter $TenantFilter -Type 'SecureScore' | Where-Object { $_.RowKey -notlike '*-Count' } + $Return.SecureScore = if ($SecureScoreItems) { $SecureScoreItems | ForEach-Object { $_.Data | ConvertFrom-Json } } else { @() } + + # Secure Score Control Profiles + $SecureScoreControlProfilesItems = Get-CIPPDbItem -TenantFilter $TenantFilter -Type 'SecureScoreControlProfiles' | Where-Object { $_.RowKey -notlike '*-Count' } + $Return.SecureScoreControlProfiles = if ($SecureScoreControlProfilesItems) { $SecureScoreControlProfilesItems | ForEach-Object { $_.Data | ConvertFrom-Json } } else { @() } + # Mailboxes (optional - requires separate cache run) if ($IncludeMailboxes) { $MailboxesItems = Get-CIPPDbItem -TenantFilter $TenantFilter -Type 'Mailboxes' | Where-Object { $_.RowKey -notlike '*-Count' } diff --git a/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneTenantSync.ps1 b/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneTenantSync.ps1 index c5f16239f905..7d2b1a4b9aab 100644 --- a/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneTenantSync.ps1 +++ b/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneTenantSync.ps1 @@ -287,7 +287,7 @@ function Invoke-NinjaOneTenantSync { [System.Collections.Generic.List[PSCustomObject]]$NinjaLicenseCreation = @() # Replace direct Graph/Exchange calls with cached data - $ExtensionCache = Get-ExtensionCacheData -TenantFilter $Customer.defaultDomainName + $ExtensionCache = Get-CippExtensionReportingData -TenantFilter $Customer.defaultDomainName -IncludeMailboxes # Map cached data to variables $Users = $ExtensionCache.Users @@ -301,9 +301,9 @@ function Invoke-NinjaOneTenantSync { $MailboxStatsFull = $ExtensionCache.MailboxUsage $Permissions = $ExtensionCache.MailboxPermissions $SecureScore = $ExtensionCache.SecureScore - $Subscriptions = $ExtensionCache.Subscriptions + $Subscriptions = if ($ExtensionCache.Licenses) { $ExtensionCache.Licenses.TermInfo | Where-Object { $null -ne $_ } } else { @() } $SecureScoreProfiles = $ExtensionCache.SecureScoreControlProfiles - $TenantDetails = $ExtensionCache.TenantDetails + $TenantDetails = $ExtensionCache.Organization $RawDomains = $ExtensionCache.Domains $AllGroups = $ExtensionCache.Groups $Licenses = $ExtensionCache.Licenses @@ -337,14 +337,14 @@ function Invoke-NinjaOneTenantSync { $licensedUsers = $Users | Where-Object { $null -ne $_.AssignedLicenses.SkuId } | Sort-Object UserPrincipalName $Roles = foreach ($Role in $AllRoles) { - # Get members from cache - $Members = ($ExtensionCache."AllRoles_$($Role.id)") + # Get members from inline property (no longer separate cache entries) + $Members = $Role.members [PSCustomObject]@{ - ID = $Result.id + ID = $Role.id DisplayName = $Role.displayName Description = $Role.description Members = $Members - ParsedMembers = $Members.displayName -join ', ' + ParsedMembers = if ($Members) { $Members.displayName -join ', ' } else { '' } } } @@ -364,7 +364,8 @@ function Invoke-NinjaOneTenantSync { Write-Verbose "$(Get-Date) - Parsing Device Compliance Policies" $DeviceComplianceDetails = foreach ($Policy in $DeviceCompliancePolicies) { - $DeviceStatuses = $ExtensionCache."DeviceCompliancePolicy_$($Policy.id)" + $StatusItems = Get-CIPPDbItem -TenantFilter $Customer.defaultDomainName -Type "IntuneDeviceCompliancePolicies_$($Policy.id)" | Where-Object { $_.RowKey -notlike '*-Count' } + $DeviceStatuses = if ($StatusItems) { $StatusItems | ForEach-Object { $_.Data | ConvertFrom-Json } } else { @() } [pscustomobject]@{ ID = $Policy.id DisplayName = $Policy.displayName @@ -375,7 +376,8 @@ function Invoke-NinjaOneTenantSync { Write-Verbose "$(Get-Date) - Parsing Groups" $Groups = foreach ($Group in $AllGroups) { - $Members = $ExtensionCache."Groups_$($Result.id)" + # Get members from inline property (no longer separate cache entries) + $Members = $Group.members [pscustomobject]@{ ID = $Group.id DisplayName = $Group.displayName From 958fd19372ba41378f75ae85d66978b398cf931c Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 15 Jan 2026 21:36:27 -0500 Subject: [PATCH 156/503] Deprecate legacy extension sync tasks and update data flow Removed legacy Sync-CippExtensionData scheduled tasks and deprecated related code, transitioning all extension data sync to use CippReportingDB and Push-CIPPDBCacheData. Updated filtering logic and cache retrieval in Invoke-CustomDataSync, and added CacheExtensionSync to table cleanup. These changes streamline extension data management and remove obsolete scheduled tasks. --- .../CustomData/Invoke-CustomDataSync.ps1 | 4 +- .../Timer Functions/Start-TableCleanup.ps1 | 3 +- .../Register-CippExtensionScheduledTasks.ps1 | 39 +++++++------------ .../Sync-CippExtensionData.ps1 | 4 ++ 4 files changed, 20 insertions(+), 30 deletions(-) diff --git a/Modules/CIPPCore/Public/CustomData/Invoke-CustomDataSync.ps1 b/Modules/CIPPCore/Public/CustomData/Invoke-CustomDataSync.ps1 index 2e96f68871a9..f49792e6b003 100644 --- a/Modules/CIPPCore/Public/CustomData/Invoke-CustomDataSync.ps1 +++ b/Modules/CIPPCore/Public/CustomData/Invoke-CustomDataSync.ps1 @@ -12,7 +12,7 @@ function Invoke-CustomDataSync { } Write-Information "Found $($Mappings.Count) Custom Data mappings" - $Mappings = $Mappings | Where-Object { $_.sourceType.value -eq 'extensionSync' -and $_.tenantFilter.value -contains $TenantFilter -or $_.tenantFilter.value -contains 'AllTenants' } + $Mappings = $Mappings | Where-Object { ($_.sourceType.value -eq 'reportingDb' -or $_.sourceType.value -eq 'extensionSync') -and ($_.tenantFilter.value -contains $TenantFilter -or $_.tenantFilter.value -contains 'AllTenants') } if ($Mappings.Count -eq 0) { Write-Warning "No Custom Data mappings found for tenant $TenantFilter" @@ -20,7 +20,7 @@ function Invoke-CustomDataSync { } Write-Information "Getting cached data for tenant $TenantFilter" - $Cache = Get-ExtensionCacheData -TenantFilter $TenantFilter + $Cache = Get-CippExtensionReportingData -TenantFilter $TenantFilter -IncludeMailboxes $BulkRequests = [System.Collections.Generic.List[object]]::new() $DirectoryObjectQueries = [System.Collections.Generic.List[object]]::new() $SyncConfigs = foreach ($Mapping in $Mappings) { diff --git a/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-TableCleanup.ps1 b/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-TableCleanup.ps1 index 177e67cdb378..4989829ba7a9 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-TableCleanup.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-TableCleanup.ps1 @@ -3,7 +3,6 @@ function Start-TableCleanup { .SYNOPSIS Start the Table Cleanup Timer #> - [CmdletBinding(SupportsShouldProcess = $true)] param() $Batch = @( @@ -60,7 +59,7 @@ function Start-TableCleanup { @{ FunctionName = 'TableCleanupTask' Type = 'DeleteTable' - Tables = @('knownlocationdb') + Tables = @('knownlocationdb', 'CacheExtensionSync', 'ExtensionSync') } ) diff --git a/Modules/CippExtensions/Public/Extension Functions/Register-CippExtensionScheduledTasks.ps1 b/Modules/CippExtensions/Public/Extension Functions/Register-CippExtensionScheduledTasks.ps1 index bbb6c22e56c1..1cef75d498fe 100644 --- a/Modules/CippExtensions/Public/Extension Functions/Register-CippExtensionScheduledTasks.ps1 +++ b/Modules/CippExtensions/Public/Extension Functions/Register-CippExtensionScheduledTasks.ps1 @@ -16,6 +16,15 @@ function Register-CIPPExtensionScheduledTasks { $PushTasks = Get-CIPPAzDataTableEntity @ScheduledTasksTable -Filter 'Hidden eq true' | Where-Object { $_.Command -match 'Push-CippExtensionData' } $Tenants = Get-Tenants -IncludeErrors + # Remove all legacy Sync-CippExtensionData tasks (now deprecated - extensions use CippReportingDB) + Write-Information "Removing $($ScheduledTasks.Count) legacy Sync-CippExtensionData scheduled tasks" + foreach ($Task in $ScheduledTasks) { + Write-Information "Removing legacy task: $($Task.Name) for tenant $($Task.Tenant)" + $Entity = $Task | Select-Object -Property PartitionKey, RowKey + Remove-AzDataTableEntity -Force @ScheduledTasksTable -Entity $Entity + } + $ScheduledTasks = @() # Clear the list since we removed them all + $MappedTenants = [System.Collections.Generic.List[string]]::new() foreach ($Extension in $Extensions) { $ExtensionConfig = $Config.$Extension @@ -24,7 +33,7 @@ function Register-CIPPExtensionScheduledTasks { $CustomDataMappingTable = Get-CIPPTable -TableName CustomDataMappings $Mappings = Get-CIPPAzDataTableEntity @CustomDataMappingTable | ForEach-Object { $Mapping = $_.JSON | ConvertFrom-Json - if ($Mapping.sourceType.value -eq 'extensionSync') { + if ($Mapping.sourceType.value -eq 'reportingDb' -or $Mapping.sourceType.value -eq 'extensionSync') { $TenantMappings = if ($Mapping.tenantFilter.value -contains 'AllTenants') { $Tenants } else { @@ -68,31 +77,9 @@ function Register-CIPPExtensionScheduledTasks { continue } $MappedTenants.Add($Tenant.defaultDomainName) - foreach ($SyncType in $SyncTypes) { - $ExistingTask = $ScheduledTasks | Where-Object { $_.Tenant -eq $Tenant.defaultDomainName -and $_.SyncType -eq $SyncType } - if (!$ExistingTask) { - $unixtime = [int64](([datetime]::UtcNow) - (Get-Date '1/1/1970')).TotalSeconds - $Task = [pscustomobject]@{ - Name = "Extension Sync - $SyncType" - Command = @{ - value = 'Sync-CippExtensionData' - label = 'Sync-CippExtensionData' - } - Parameters = [pscustomobject]@{ - TenantFilter = $Tenant.defaultDomainName - SyncType = $SyncType - } - Recurrence = '1d' - ScheduledTime = $unixtime - TenantFilter = $Tenant.defaultDomainName - } - if ($ExistingTask) { - $Task | Add-Member -NotePropertyName 'RowKey' -NotePropertyValue $ExistingTask.RowKey -Force - } - $null = Add-CIPPScheduledTask -Task $Task -hidden $true -SyncType $SyncType - Write-Information "Creating $SyncType task for tenant $($Tenant.defaultDomainName)" - } - } + + # Legacy Sync-CippExtensionData tasks are no longer needed - extensions now use CippReportingDB + # All cache data is now collected by Push-CIPPDBCacheData scheduled tasks $ExistingPushTask = $PushTasks | Where-Object { $_.Tenant -eq $Tenant.defaultDomainName -and $_.SyncType -eq $Extension } if ((!$ExistingPushTask -or $Reschedule.IsPresent) -and $Extension -ne 'NinjaOne') { diff --git a/Modules/CippExtensions/Public/Extension Functions/Sync-CippExtensionData.ps1 b/Modules/CippExtensions/Public/Extension Functions/Sync-CippExtensionData.ps1 index 3ddeb244de00..480717d693bd 100644 --- a/Modules/CippExtensions/Public/Extension Functions/Sync-CippExtensionData.ps1 +++ b/Modules/CippExtensions/Public/Extension Functions/Sync-CippExtensionData.ps1 @@ -9,6 +9,10 @@ function Sync-CippExtensionData { $SyncType ) + # Legacy cache system is deprecated - all extensions now use CippReportingDB + Write-Warning "Sync-CippExtensionData is deprecated. This scheduled task should be removed. Extensions now use Push-CIPPDBCacheData and Get-CippExtensionReportingData." + return + $Table = Get-CIPPTable -TableName ExtensionSync $Extensions = Get-CIPPAzDataTableEntity @Table -Filter "PartitionKey eq '$($SyncType)'" $LastSync = $Extensions | Where-Object { $_.RowKey -eq $TenantFilter } From 0b7d6047ee94c225754bcf19e25f026a614663c8 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 15 Jan 2026 22:04:04 -0500 Subject: [PATCH 157/503] Add universal search for CIPP Reporting DB data Introduces Search-CIPPDbData.ps1, a function for searching JSON objects in the CIPP Reporting DB across multiple data types and tenants using regex or wildcard terms. Also updates Get-CIPPDbItem.ps1 to handle 'allTenants' filtering logic for improved search support. --- Modules/CIPPCore/Public/Get-CIPPDbItem.ps1 | 6 +- Modules/CIPPCore/Public/Search-CIPPDbData.ps1 | 170 ++++++++++++++++++ 2 files changed, 175 insertions(+), 1 deletion(-) create mode 100644 Modules/CIPPCore/Public/Search-CIPPDbData.ps1 diff --git a/Modules/CIPPCore/Public/Get-CIPPDbItem.ps1 b/Modules/CIPPCore/Public/Get-CIPPDbItem.ps1 index e72d59d10203..13924c6794d0 100644 --- a/Modules/CIPPCore/Public/Get-CIPPDbItem.ps1 +++ b/Modules/CIPPCore/Public/Get-CIPPDbItem.ps1 @@ -51,7 +51,11 @@ function Get-CIPPDbItem { if (-not $Type) { throw 'Type parameter is required when CountsOnly is not specified' } - $Filter = "PartitionKey eq '{0}' and RowKey ge '{1}-' and RowKey lt '{1}.'" -f $TenantFilter, $Type + if ($TenantFilter -ne 'allTenants') { + $Filter = "PartitionKey eq '{0}' and RowKey ge '{1}-' and RowKey lt '{1}.'" -f $TenantFilter, $Type + } else { + $Filter = "RowKey ge '{0}-' and RowKey lt '{0}.'" -f $Type + } $Results = Get-CIPPAzDataTableEntity @Table -Filter $Filter } diff --git a/Modules/CIPPCore/Public/Search-CIPPDbData.ps1 b/Modules/CIPPCore/Public/Search-CIPPDbData.ps1 new file mode 100644 index 000000000000..76397685c367 --- /dev/null +++ b/Modules/CIPPCore/Public/Search-CIPPDbData.ps1 @@ -0,0 +1,170 @@ +function Search-CIPPDbData { + <# + .SYNOPSIS + Universal search function for CIPP Reporting DB data + + .DESCRIPTION + Searches JSON objects in the CIPP Reporting DB for matching search terms. + Supports wildcard and regular expression searches across multiple data types. + Returns results as a flat list with Type property included. + + .PARAMETER TenantFilter + Optional tenant domain or GUID to filter search. If not specified, searches all tenants. + + .PARAMETER SearchTerms + Search terms to look for. Uses regex matching by default (special characters are escaped). + Can be a single string or array of strings. + + .PARAMETER Types + Array of data types to search. If not specified, searches all available types. + Valid types: Users, Domains, ConditionalAccessPolicies, ManagedDevices, Organization, + Groups, Roles, LicenseOverview, IntuneDeviceCompliancePolicies, SecureScore, + SecureScoreControlProfiles, Mailboxes, CASMailbox, MailboxPermissions, OneDriveUsage, MailboxUsage + + .PARAMETER CaseSensitive + If specified, performs case-sensitive search + + .PARAMETER MaxResultsPerType + Maximum number of results to return per type. Default is unlimited (0) + + .EXAMPLE + Search-CIPPDbData -TenantFilter 'contoso.onmicrosoft.com' -SearchTerms 'john.doe' -Types 'Users', 'Groups' + + .EXAMPLE + Search-CIPPDbData -SearchTerms 'admin' -Types 'Users' + + .EXAMPLE + Search-CIPPDbData -SearchTerms 'SecurityDefaults', 'ConditionalAccess' -Types 'ConditionalAccessPolicies', 'Organization' + + .EXAMPLE + Search-CIPPDbData -SearchTerms 'SecurityDefaults', 'ConditionalAccess' -Types 'ConditionalAccessPolicies', 'Organization' + + .FUNCTIONALITY + Internal + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $false)] + [string]$TenantFilter, + + [Parameter(Mandatory = $true)] + [string[]]$SearchTerms, + + [Parameter(Mandatory = $false)] + [ValidateSet( + 'Users', 'Domains', 'ConditionalAccessPolicies', 'ManagedDevices', 'Organization', + 'Groups', 'Roles', 'LicenseOverview', 'IntuneDeviceCompliancePolicies', 'SecureScore', + 'SecureScoreControlProfiles', 'Mailboxes', 'CASMailbox', 'MailboxPermissions', + 'OneDriveUsage', 'MailboxUsage', 'Devices', 'AllRoles', 'Licenses', 'DeviceCompliancePolicies' + )] + [string[]]$Types, + + [Parameter(Mandatory = $false)] + [switch]$CaseSensitive, + + [Parameter(Mandatory = $false)] + [int]$MaxResultsPerType = 0 + ) + + try { + # Initialize results list + $Results = [System.Collections.Generic.List[object]]::new() + + # Define all available types if not specified + if (-not $Types) { + $Types = @( + 'Users', 'Domains', 'ConditionalAccessPolicies', 'ManagedDevices', 'Organization', + 'Groups', 'Roles', 'LicenseOverview', 'IntuneDeviceCompliancePolicies', 'SecureScore', + 'SecureScoreControlProfiles', 'Mailboxes', 'CASMailbox', 'MailboxPermissions', + 'OneDriveUsage', 'MailboxUsage' + ) + } + + # Get tenants to search - use 'allTenants' if no filter specified + $TenantsToSearch = @() + if ($TenantFilter) { + $TenantsToSearch = @($TenantFilter) + } else { + # Use 'allTenants' to search across all tenants + $TenantsToSearch = @('allTenants') + Write-Verbose 'Searching all tenants' + } + + # Process each data type + foreach ($Type in $Types) { + Write-Verbose "Searching type: $Type" + $TypeResults = [System.Collections.Generic.List[object]]::new() + + # Search across all tenants + foreach ($Tenant in $TenantsToSearch) { + if (-not $Tenant) { continue } + + try { + # Get items for this type and tenant + $Items = Get-CIPPDbItem -TenantFilter $Tenant -Type $Type | Where-Object { $_.RowKey -notlike '*-Count' } + Write-Verbose "Found $(@($Items).Count) items for type '$Type' in tenant '$Tenant'" + + if ($Items) { + foreach ($Item in $Items) { + # Data is already in JSON format, do a quick text search first + if (-not $Item.Data) { continue } + + # Check if any search term matches in the JSON string + $IsMatch = $false + + foreach ($SearchTerm in $SearchTerms) { + # Use -match operator with escaped search term + $SearchPattern = [regex]::Escape($SearchTerm) + + if ($CaseSensitive) { + if ($Item.Data -cmatch $SearchPattern) { + $IsMatch = $true + break + } + } else { + if ($Item.Data -match $SearchPattern) { + $IsMatch = $true + break + } + } + } + + # Only parse JSON if we have a match + if ($IsMatch) { + try { + $Data = $Item.Data | ConvertFrom-Json + $ResultItem = [PSCustomObject]@{ + Tenant = $Item.PartitionKey + Type = $Type + RowKey = $Item.RowKey + Data = $Data + Timestamp = $Item.Timestamp + } + $Results.Add($ResultItem) + + # Check max results per type + if ($MaxResultsPerType -gt 0 -and $Results.Count -ge $MaxResultsPerType) { + break + } + } catch { + Write-Verbose "Failed to parse JSON for $($Item.RowKey): $($_.Exception.Message)" + } + } + } + } + + } catch { + Write-Verbose "Error searching type '$Type' for tenant '$Tenant': $($_.Exception.Message)" + } + } + } + + Write-Verbose "Found $($Results.Count) total results" + # Return results as flat list + return $Results.ToArray() + + } catch { + Write-LogMessage -API 'UniversalSearch' -tenant $TenantFilter -message "Failed to perform universal search: $($_.Exception.Message)" -sev Error + throw + } +} From 251eaec6d032f0b033fe67ce42c0b3b766f730b2 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 15 Jan 2026 22:18:29 -0500 Subject: [PATCH 158/503] Update search parameters The CaseSensitive parameter was removed and replaced with MatchAll, which requires all search terms to be found when specified. The default behavior now matches any term. Documentation and logic were updated accordingly. --- Modules/CIPPCore/Public/Search-CIPPDbData.ps1 | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/Modules/CIPPCore/Public/Search-CIPPDbData.ps1 b/Modules/CIPPCore/Public/Search-CIPPDbData.ps1 index 76397685c367..1ef40a996260 100644 --- a/Modules/CIPPCore/Public/Search-CIPPDbData.ps1 +++ b/Modules/CIPPCore/Public/Search-CIPPDbData.ps1 @@ -21,8 +21,8 @@ function Search-CIPPDbData { Groups, Roles, LicenseOverview, IntuneDeviceCompliancePolicies, SecureScore, SecureScoreControlProfiles, Mailboxes, CASMailbox, MailboxPermissions, OneDriveUsage, MailboxUsage - .PARAMETER CaseSensitive - If specified, performs case-sensitive search + .PARAMETER MatchAll + If specified, all search terms must be found. Default is false (any term matches). .PARAMETER MaxResultsPerType Maximum number of results to return per type. Default is unlimited (0) @@ -60,7 +60,7 @@ function Search-CIPPDbData { [string[]]$Types, [Parameter(Mandatory = $false)] - [switch]$CaseSensitive, + [switch]$MatchAll, [Parameter(Mandatory = $false)] [int]$MaxResultsPerType = 0 @@ -112,16 +112,20 @@ function Search-CIPPDbData { # Check if any search term matches in the JSON string $IsMatch = $false - foreach ($SearchTerm in $SearchTerms) { - # Use -match operator with escaped search term - $SearchPattern = [regex]::Escape($SearchTerm) - - if ($CaseSensitive) { - if ($Item.Data -cmatch $SearchPattern) { - $IsMatch = $true + if ($MatchAll) { + # All terms must match + $IsMatch = $true + foreach ($SearchTerm in $SearchTerms) { + $SearchPattern = [regex]::Escape($SearchTerm) + if ($Item.Data -notmatch $SearchPattern) { + $IsMatch = $false break } - } else { + } + } else { + # Any term can match (default) + foreach ($SearchTerm in $SearchTerms) { + $SearchPattern = [regex]::Escape($SearchTerm) if ($Item.Data -match $SearchPattern) { $IsMatch = $true break From 1da358ed1f7b303cf80335ca40e3fed2abf93614 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 16 Jan 2026 00:00:42 -0500 Subject: [PATCH 159/503] Prevent duplicate scheduled tasks unless completed or failed Updated the filter in Add-CIPPScheduledTask to only disallow duplicate task names if existing tasks are not in 'Completed' or 'Failed' state. This allows new tasks with the same name if previous ones have finished or failed. --- Modules/CIPPCore/Public/Add-CIPPScheduledTask.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Add-CIPPScheduledTask.ps1 b/Modules/CIPPCore/Public/Add-CIPPScheduledTask.ps1 index 6728082f8750..4024fa1ea68b 100644 --- a/Modules/CIPPCore/Public/Add-CIPPScheduledTask.ps1 +++ b/Modules/CIPPCore/Public/Add-CIPPScheduledTask.ps1 @@ -53,7 +53,7 @@ function Add-CIPPScheduledTask { } if ($DisallowDuplicateName) { - $Filter = "PartitionKey eq 'ScheduledTask' and Name eq '$($Task.Name)'" + $Filter = "PartitionKey eq 'ScheduledTask' and Name eq '$($Task.Name)' and TaskState ne 'Completed' and TaskState ne 'Failed'" $ExistingTask = (Get-CIPPAzDataTableEntity @Table -Filter $Filter) if ($ExistingTask) { return "Task with name $($Task.Name) already exists" From 4b36d9665def0080f2a5824767d8d56a29865bfe Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 16 Jan 2026 00:01:06 -0500 Subject: [PATCH 160/503] Enhance mailbox permission report Added multi-strategy lookup for user mailbox type in Get-CIPPMailboxPermissionReport, returning 'UserMailboxType' in the report. Updated Invoke-ListmailboxPermissions to support UseReportDB and ByUser query parameters, enabling report-based retrieval without a specific user ID. --- .../Invoke-ListmailboxPermissions.ps1 | 25 ++++++++++- .../Get-CIPPMailboxPermissionReport.ps1 | 43 +++++++++++++++---- 2 files changed, 58 insertions(+), 10 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListmailboxPermissions.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListmailboxPermissions.ps1 index 6945b4881e7b..95b4a7de57bb 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListmailboxPermissions.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListmailboxPermissions.ps1 @@ -1,4 +1,4 @@ -Function Invoke-ListmailboxPermissions { +function Invoke-ListmailboxPermissions { <# .FUNCTIONALITY Entrypoint @@ -10,8 +10,31 @@ Function Invoke-ListmailboxPermissions { # Interact with query parameters or the body of the request. $TenantFilter = $Request.Query.tenantFilter $UserID = $Request.Query.userId + $UseReportDB = $Request.Query.UseReportDB + $ByUser = $Request.Query.ByUser try { + # If UseReportDB is specified and no specific UserID, retrieve from report database + if ($UseReportDB -eq 'true' -and -not $UserID) { + + # Call the report function with proper parameters + $ReportParams = @{ + TenantFilter = $TenantFilter + } + if ($ByUser -eq 'true') { + $ReportParams.ByUser = $true + } + + $GraphRequest = Get-CIPPMailboxPermissionReport @ReportParams + $StatusCode = [HttpStatusCode]::OK + + return ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = @($GraphRequest) + }) + } + + # Original live query logic for specific user $Requests = @( @{ CmdletInput = @{ diff --git a/Modules/CIPPCore/Public/Get-CIPPMailboxPermissionReport.ps1 b/Modules/CIPPCore/Public/Get-CIPPMailboxPermissionReport.ps1 index 0ad921dd7877..5b19811356a8 100644 --- a/Modules/CIPPCore/Public/Get-CIPPMailboxPermissionReport.ps1 +++ b/Modules/CIPPCore/Public/Get-CIPPMailboxPermissionReport.ps1 @@ -130,18 +130,36 @@ function Get-CIPPMailboxPermissionReport { $UserKey = $_.Name $UserDisplay = $_.Group[0].User # Use original User value for display - # Build detailed permissions list with mailbox and access rights - $PermissionDetails = $_.Group | ForEach-Object { - [PSCustomObject]@{ - Mailbox = $_.MailboxDisplayName - MailboxUPN = $_.MailboxUPN - AccessRights = $_.AccessRights + # Look up the user's mailbox type using multi-strategy approach + $UserMailbox = $null + if ($UserDisplay) { + # Try UPN/primarySmtpAddress lookup (case-insensitive) + $UserMailbox = $MailboxLookup[$UserDisplay.ToLower()] + + # If not found, try ExternalDirectoryObjectId lookup + if (-not $UserMailbox) { + $UserMailbox = $MailboxByExternalIdLookup[$UserDisplay] + } + + # If not found, try ID lookup + if (-not $UserMailbox) { + $UserMailbox = $MailboxByIdLookup[$UserDisplay] } } + $UserMailboxType = if ($UserMailbox) { $UserMailbox.recipientTypeDetails } else { 'Unknown' } + + # Build detailed permissions list with mailbox and access rights + $PermissionDetails = @($_.Group | ForEach-Object { + [PSCustomObject]@{ + Mailbox = $_.MailboxDisplayName + MailboxUPN = $_.MailboxUPN + AccessRights = $_.AccessRights + } + }) [PSCustomObject]@{ User = $UserDisplay - UserType = if ($UserDisplay -match '@') { 'Email/UPN' } else { 'Display Name' } + UserMailboxType = $UserMailboxType MailboxCount = $_.Count Permissions = $PermissionDetails MailboxCacheTimestamp = $MailboxCacheTimestamp @@ -154,13 +172,20 @@ function Get-CIPPMailboxPermissionReport { $MailboxUPN = $_.Name $MailboxInfo = $_.Group[0] + # Build detailed permissions list with user and access rights + $PermissionDetails = @($_.Group | ForEach-Object { + [PSCustomObject]@{ + User = $_.User + AccessRights = $_.AccessRights + } + }) + [PSCustomObject]@{ MailboxUPN = $MailboxUPN MailboxDisplayName = $MailboxInfo.MailboxDisplayName MailboxType = $MailboxInfo.MailboxType PermissionCount = $_.Count - Users = ($_.Group | Select-Object -ExpandProperty User | Sort-Object -Unique) -join '; ' - Permissions = ($_.Group | ForEach-Object { "$($_.User) ($($_.AccessRights))" }) -join '; ' + Permissions = $PermissionDetails MailboxCacheTimestamp = $MailboxCacheTimestamp PermissionCacheTimestamp = $PermissionCacheTimestamp } From a6b616281f2f6be5f99d629f333813ec48349d43 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 16 Jan 2026 00:16:23 -0500 Subject: [PATCH 161/503] Add support for AllTenants in mailbox permission report The Get-CIPPMailboxPermissionReport function now handles the 'AllTenants' filter, aggregating mailbox permission data across all tenants. Each result now includes a 'Tenant' property for better identification. This improves reporting capabilities for multi-tenant environments. --- .../Get-CIPPMailboxPermissionReport.ps1 | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/Modules/CIPPCore/Public/Get-CIPPMailboxPermissionReport.ps1 b/Modules/CIPPCore/Public/Get-CIPPMailboxPermissionReport.ps1 index 5b19811356a8..95a5e7e32e41 100644 --- a/Modules/CIPPCore/Public/Get-CIPPMailboxPermissionReport.ps1 +++ b/Modules/CIPPCore/Public/Get-CIPPMailboxPermissionReport.ps1 @@ -33,6 +33,28 @@ function Get-CIPPMailboxPermissionReport { try { Write-LogMessage -API 'MailboxPermissionReport' -tenant $TenantFilter -message 'Generating mailbox permission report' -sev Info + # Handle AllTenants + if ($TenantFilter -eq 'AllTenants') { + # Get all tenants that have mailbox data + $AllMailboxItems = Get-CIPPDbItem -TenantFilter 'allTenants' -Type 'Mailboxes' + $Tenants = @($AllMailboxItems | Where-Object { $_.RowKey -ne 'Mailboxes-Count' } | Select-Object -ExpandProperty PartitionKey -Unique) + + $AllResults = [System.Collections.Generic.List[PSCustomObject]]::new() + foreach ($Tenant in $Tenants) { + try { + $TenantResults = Get-CIPPMailboxPermissionReport -TenantFilter $Tenant -ByUser:$ByUser + foreach ($Result in $TenantResults) { + # Add Tenant property to each result + $Result | Add-Member -NotePropertyName 'Tenant' -NotePropertyValue $Tenant -Force + $AllResults.Add($Result) + } + } catch { + Write-LogMessage -API 'MailboxPermissionReport' -tenant $Tenant -message "Failed to get report for tenant: $($_.Exception.Message)" -sev Warning + } + } + return $AllResults + } + # Get mailboxes from reporting DB $MailboxItems = Get-CIPPDbItem -TenantFilter $TenantFilter -Type 'Mailboxes' if (-not $MailboxItems) { @@ -162,6 +184,7 @@ function Get-CIPPMailboxPermissionReport { UserMailboxType = $UserMailboxType MailboxCount = $_.Count Permissions = $PermissionDetails + Tenant = $TenantFilter MailboxCacheTimestamp = $MailboxCacheTimestamp PermissionCacheTimestamp = $PermissionCacheTimestamp } @@ -186,6 +209,7 @@ function Get-CIPPMailboxPermissionReport { MailboxType = $MailboxInfo.MailboxType PermissionCount = $_.Count Permissions = $PermissionDetails + Tenant = $TenantFilter MailboxCacheTimestamp = $MailboxCacheTimestamp PermissionCacheTimestamp = $PermissionCacheTimestamp } From 2fe8c1de20d957c309bc167eede902e4374ce862 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 16 Jan 2026 00:28:50 -0500 Subject: [PATCH 162/503] Show relative time until scheduled task runs Adds logic to calculate and display the relative time until a newly scheduled task will run in Add-CIPPScheduledTask.ps1. Also refactors DisallowDuplicateName handling in Invoke-AddScheduledItem.ps1 to support both query and body sources. --- .../CIPPCore/Public/Add-CIPPScheduledTask.ps1 | 41 ++++++++++++++++++- .../Scheduler/Invoke-AddScheduledItem.ps1 | 4 +- 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/Modules/CIPPCore/Public/Add-CIPPScheduledTask.ps1 b/Modules/CIPPCore/Public/Add-CIPPScheduledTask.ps1 index 4024fa1ea68b..0981e4b361d3 100644 --- a/Modules/CIPPCore/Public/Add-CIPPScheduledTask.ps1 +++ b/Modules/CIPPCore/Public/Add-CIPPScheduledTask.ps1 @@ -260,7 +260,46 @@ function Add-CIPPScheduledTask { return "Error - Could not add task: $ErrorMessage" } Write-LogMessage -headers $Headers -API 'ScheduledTask' -message "Added task $($entity.Name) with ID $($entity.RowKey)" -Sev 'Info' -Tenant $tenantFilter - return "Successfully added task: $($entity.Name)" + + # Calculate relative time for next run + $scheduledEpoch = [int64]$entity.ScheduledTime + $currentTime = [datetime]::UtcNow + + if ($scheduledEpoch -eq 0 -or $scheduledEpoch -le ([int64](($currentTime) - (Get-Date '1/1/1970')).TotalSeconds)) { + # Task will run at next 15-minute interval - calculate efficiently + $minutesToAdd = 15 - ($currentTime.Minute % 15) + $nextRunTime = $currentTime.AddMinutes($minutesToAdd).AddSeconds(-$currentTime.Second).AddMilliseconds(-$currentTime.Millisecond) + $timeUntilRun = $nextRunTime - $currentTime + } else { + # Task is scheduled for a specific time in the future + $scheduledTime = [datetime]'1/1/1970' + [TimeSpan]::FromSeconds($scheduledEpoch) + $timeUntilRun = $scheduledTime - $currentTime + } + + # Format relative time + $relativeTime = switch ($timeUntilRun.TotalMinutes) { + { $_ -ge 1440 } { + $days = [Math]::Floor($timeUntilRun.TotalDays) + $hours = $timeUntilRun.Hours + $result = "$days day$(if ($days -ne 1) { 's' })" + if ($hours -gt 0) { $result += " and $hours hour$(if ($hours -ne 1) { 's' })" } + $result + break + } + { $_ -ge 60 } { + $hours = [Math]::Floor($timeUntilRun.TotalHours) + $minutes = $timeUntilRun.Minutes + $result = "$hours hour$(if ($hours -ne 1) { 's' })" + if ($minutes -gt 0) { $result += " and $minutes minute$(if ($minutes -ne 1) { 's' })" } + $result + break + } + { $_ -ge 2 } { "about $([Math]::Round($_)) minutes"; break } + { $_ -ge 1 } { 'about 1 minute'; break } + default { 'less than a minute' } + } + + return "Successfully added task: $($entity.Name). It will run in $relativeTime." } } catch { Write-Warning "Failed to add scheduled task: $($_.Exception.Message)" diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Scheduler/Invoke-AddScheduledItem.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Scheduler/Invoke-AddScheduledItem.ps1 index 048c881f4a9e..18aecda3ab90 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Scheduler/Invoke-AddScheduledItem.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Scheduler/Invoke-AddScheduledItem.ps1 @@ -13,6 +13,8 @@ function Invoke-AddScheduledItem { $hidden = $true } + $DisallowDuplicateName = $Request.Query.DisallowDuplicateName ?? $Request.Body.DisallowDuplicateName + if ($Request.Body.RunNow -eq $true) { try { $Table = Get-CIPPTable -TableName 'ScheduledTasks' @@ -33,7 +35,7 @@ function Invoke-AddScheduledItem { Task = $Request.Body Headers = $Request.Headers Hidden = $hidden - DisallowDuplicateName = $Request.Query.DisallowDuplicateName + DisallowDuplicateName = $DisallowDuplicateName DesiredStartTime = $Request.Body.DesiredStartTime } $Result = Add-CIPPScheduledTask @ScheduledTask From a7b91e7c3397876f9dc4dc5933eee0d3dcb84eaa Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 16 Jan 2026 01:00:29 -0500 Subject: [PATCH 163/503] Handle multi-tenant scheduled task completion logic Introduces $IsMultiTenantTask to distinguish multi-tenant tasks and updates logic to prevent marking such tasks as completed prematurely. Also refactors result storage conditions to use the new variable for clarity and correctness. --- .../Activity Triggers/Push-ExecScheduledCommand.ps1 | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecScheduledCommand.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecScheduledCommand.ps1 index 474ead168783..ccc7249ed798 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecScheduledCommand.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecScheduledCommand.ps1 @@ -20,6 +20,9 @@ function Push-ExecScheduledCommand { # Handle tenant resolution - support both direct tenant and group-expanded tenants $Tenant = $Item.Parameters.TenantFilter ?? $Item.TaskInfo.Tenant + # Detect if this is a multi-tenant task that should store results per-tenant + $IsMultiTenantTask = ($task.Tenant -eq 'AllTenants' -or $task.TenantGroup) + # For tenant group tasks, the tenant will be the expanded tenant from the orchestrator # We don't need to expand groups here as that's handled in the orchestrator $TenantInfo = Get-Tenants -TenantFilter $Tenant @@ -30,7 +33,7 @@ function Push-ExecScheduledCommand { Remove-Variable -Name ScheduledTaskId -Scope Script -ErrorAction SilentlyContinue return } - if ($CurrentTask.TaskState -eq 'Completed') { + if ($CurrentTask.TaskState -eq 'Completed' -and !$IsMultiTenantTask) { Write-Information "The task $($task.Name) for tenant $($task.Tenant) is already completed. Skipping execution." Remove-Variable -Name ScheduledTaskId -Scope Script -ErrorAction SilentlyContinue return @@ -262,7 +265,7 @@ function Push-ExecScheduledCommand { } } Write-Information "Results: $($results | ConvertTo-Json -Depth 10)" - if ($StoredResults.Length -gt 64000 -or $task.Tenant -eq 'AllTenants' -or $task.TenantGroup) { + if ($StoredResults.Length -gt 64000 -or $IsMultiTenantTask) { $TaskResultsTable = Get-CippTable -tablename 'ScheduledTaskResults' $TaskResults = @{ PartitionKey = $task.RowKey From 37fc09401a80cf01c594399693c82ffd3d67a295 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Fri, 16 Jan 2026 12:11:53 +0100 Subject: [PATCH 164/503] update text --- Modules/CIPPCore/Public/Get-CIPPMailboxPermissionReport.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Modules/CIPPCore/Public/Get-CIPPMailboxPermissionReport.ps1 b/Modules/CIPPCore/Public/Get-CIPPMailboxPermissionReport.ps1 index 95a5e7e32e41..88cecefea547 100644 --- a/Modules/CIPPCore/Public/Get-CIPPMailboxPermissionReport.ps1 +++ b/Modules/CIPPCore/Public/Get-CIPPMailboxPermissionReport.ps1 @@ -58,7 +58,7 @@ function Get-CIPPMailboxPermissionReport { # Get mailboxes from reporting DB $MailboxItems = Get-CIPPDbItem -TenantFilter $TenantFilter -Type 'Mailboxes' if (-not $MailboxItems) { - throw 'No mailbox data found in reporting database. Run Set-CIPPDBCacheMailboxes first.' + throw 'No mailbox data found in reporting database. Run a scan first. ' } # Get the most recent mailbox cache timestamp @@ -87,7 +87,7 @@ function Get-CIPPMailboxPermissionReport { # Get mailbox permissions from reporting DB $PermissionItems = Get-CIPPDbItem -TenantFilter $TenantFilter -Type 'MailboxPermissions' if (-not $PermissionItems) { - throw 'No mailbox permission data found in reporting database. Run Set-CIPPDBCacheMailboxes first.' + throw 'No mailbox permission data found in reporting database. Run a scan first.' } # Get the most recent permission cache timestamp From 8b940570d0e74c0abdf46aa43f8e8e13b2dcba59 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Fri, 16 Jan 2026 12:26:04 +0100 Subject: [PATCH 165/503] JSON convert fix --- .../Tenant/Standards/Invoke-RemoveStandardTemplate.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-RemoveStandardTemplate.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-RemoveStandardTemplate.ps1 index cfd69a6fe214..f5440b35b796 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-RemoveStandardTemplate.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-RemoveStandardTemplate.ps1 @@ -16,9 +16,9 @@ function Invoke-RemoveStandardTemplate { $ID = $Request.Body.ID ?? $Request.Query.ID try { $Table = Get-CippTable -tablename 'templates' - $Filter = "PartitionKey eq 'StandardsTemplateV2' and RowKey eq '$ID'" + $Filter = "PartitionKey eq 'StandardsTemplateV2' and GUID eq '$ID'" $ClearRow = Get-CIPPAzDataTableEntity @Table -Filter $Filter -Property PartitionKey, RowKey, JSON - $TemplateName = (ConvertFrom-Json -InputObject $ClearRow.JSON).templateName + $TemplateName = (ConvertFrom-Json -InputObject $ClearRow.JSON -ErrorAction SilentlyContinue).templateName Remove-AzDataTableEntity -Force @Table -Entity $ClearRow $Result = "Removed Standards Template named: '$($TemplateName)' with id: $($ID)" Write-LogMessage -Headers $Headers -API $APIName -message $Result -Sev Info From 59805eddf35df7d1cb7a7ed431aa791f3a7a70e5 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Fri, 16 Jan 2026 15:21:16 +0100 Subject: [PATCH 166/503] Fixes cippcatemplate --- Modules/CIPPCore/Public/New-CIPPCATemplate.ps1 | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Modules/CIPPCore/Public/New-CIPPCATemplate.ps1 b/Modules/CIPPCore/Public/New-CIPPCATemplate.ps1 index bcb928d5700a..060b91863208 100644 --- a/Modules/CIPPCore/Public/New-CIPPCATemplate.ps1 +++ b/Modules/CIPPCore/Public/New-CIPPCATemplate.ps1 @@ -62,7 +62,7 @@ function New-CIPPCATemplate { $isArray = $hasConditionsUsers -and ($JSON.conditions.users -is [Array] -or $JSON.conditions.users -is [System.Collections.IList]) $isPSCustomObject = $hasConditionsUsers -and -not $isArray -and ($JSON.conditions.users -is [PSCustomObject] -or ($JSON.conditions.users.PSObject.Properties.Count -gt 0 -and -not $isArray)) $hasIncludeUsers = $isPSCustomObject -and ($null -ne $JSON.conditions.users.includeUsers) - + if ($isPSCustomObject -and $hasIncludeUsers) { $JSON.conditions.users.includeUsers = @($JSON.conditions.users.includeUsers | ForEach-Object { $originalID = $_ @@ -106,7 +106,9 @@ function New-CIPPCATemplate { $AllLocations.Add($Location) } - $JSON | Add-Member -NotePropertyName 'LocationInfo' -NotePropertyValue @($AllLocations | Select-Object -Unique) -Force + # 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 $JSON = (ConvertTo-Json -Compress -Depth 100 -InputObject $JSON) return $JSON } From 8fbe5c4a3423447fa3e8894650765d8b177f86d3 Mon Sep 17 00:00:00 2001 From: James Tarran Date: Fri, 16 Jan 2026 15:15:58 +0000 Subject: [PATCH 167/503] Update identity-openapispec.json - Make tenantFilter required on ExecSendPush --- Tools/identity-openapispec.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Tools/identity-openapispec.json b/Tools/identity-openapispec.json index 72e8871a8913..96c5c2b5d28a 100644 --- a/Tools/identity-openapispec.json +++ b/Tools/identity-openapispec.json @@ -178,9 +178,13 @@ "UserEmail": { "type": "string", "description": "User Principal Name" + }, + "tenantFilter": { + "type": "string", + "description": "Tenant to filter by" } }, - "required": ["UserEmail"] + "required": ["UserEmail","tenantFilter"] } } } From 37d53a27e85be9290889e0f08aab537bd33c8cac Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 16 Jan 2026 10:21:52 -0500 Subject: [PATCH 168/503] Refactor mailbox permissions caching to use batching Introduces new entrypoint functions for executing and storing mailbox permissions in batches. Updates Set-CIPPDBCacheMailboxes to orchestrate mailbox permission caching in batches of 10, improving scalability and reliability. Adds Push-ExecCIPPDBCache, Push-GetMailboxPermissionsBatch, Push-StoreMailboxPermissions, and Invoke-ExecCIPPDBCache to support the new workflow. --- .../CIPPDBCache/Push-ExecCIPPDBCache.ps1 | 41 +++++++ .../Push-GetMailboxPermissionsBatch.ps1 | 103 ++++++++++++++++++ .../Push-StoreMailboxPermissions.ps1 | 80 ++++++++++++++ .../CIPP/Core/Invoke-ExecCIPPDBCache.ps1 | 76 +++++++++++++ .../Public/Set-CIPPDBCacheMailboxes.ps1 | 52 ++++++--- 5 files changed, 338 insertions(+), 14 deletions(-) create mode 100644 Modules/CIPPCore/Public/Entrypoints/Activity Triggers/CIPPDBCache/Push-ExecCIPPDBCache.ps1 create mode 100644 Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Mailbox Permissions/Push-GetMailboxPermissionsBatch.ps1 create mode 100644 Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Mailbox Permissions/Push-StoreMailboxPermissions.ps1 create mode 100644 Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ExecCIPPDBCache.ps1 diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/CIPPDBCache/Push-ExecCIPPDBCache.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/CIPPDBCache/Push-ExecCIPPDBCache.ps1 new file mode 100644 index 000000000000..041420b2d5c5 --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/CIPPDBCache/Push-ExecCIPPDBCache.ps1 @@ -0,0 +1,41 @@ +function Push-ExecCIPPDBCache { + <# + .SYNOPSIS + Generic wrapper to execute CIPP DB cache functions + + .DESCRIPTION + Executes the specified Set-CIPPDBCache* function with the provided parameters + + .FUNCTIONALITY + Entrypoint + #> + [CmdletBinding()] + param($Item) + + $Name = $Item.Name + $TenantFilter = $Item.TenantFilter + + try { + Write-Information "Colllecting $Name for tenant $TenantFilter" + + # Build the full function name + $FullFunctionName = "Set-CIPPDBCache$Name" + + # Check if function exists + $Function = Get-Command -Name $FullFunctionName -ErrorAction SilentlyContinue + if (-not $Function) { + throw "Function $FullFunctionName does not exist" + } + + # Execute the cache function + & $FullFunctionName -TenantFilter $TenantFilter + + Write-Information "Completed $Name for tenant $TenantFilter" + return "Successfully executed $Name for tenant $TenantFilter" + + } catch { + $ErrorMsg = "Failed to execute $Name for tenant $TenantFilter : $($_.Exception.Message)" + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message $ErrorMsg -sev Error + throw $ErrorMsg + } +} diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Mailbox Permissions/Push-GetMailboxPermissionsBatch.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Mailbox Permissions/Push-GetMailboxPermissionsBatch.ps1 new file mode 100644 index 000000000000..d86c51cd10a6 --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Mailbox Permissions/Push-GetMailboxPermissionsBatch.ps1 @@ -0,0 +1,103 @@ +function Push-GetMailboxPermissionsBatch { + <# + .SYNOPSIS + Process a batch of mailbox permission queries + + .DESCRIPTION + Queries mailbox permissions for a batch of mailboxes and stores in the reporting database + + .FUNCTIONALITY + Entrypoint + #> + param($Item) + + $TenantFilter = $Item.TenantFilter + $Mailboxes = $Item.Mailboxes + $BatchNumber = $Item.BatchNumber + $TotalBatches = $Item.TotalBatches + + try { + Write-Information "Processing batch $BatchNumber of $TotalBatches for tenant $TenantFilter with $($Mailboxes.Count) mailboxes" + Write-Information "Mailbox UPNs in batch: $($Mailboxes -join ', ')" + + # Build bulk requests for this batch (2 queries per mailbox: MailboxPermission + RecipientPermission) + # Calendar permissions require locale-specific folder names and will be collected separately if needed + $ExoBulkRequests = foreach ($MailboxUPN in $Mailboxes) { + @{ + CmdletInput = @{ + CmdletName = 'Get-MailboxPermission' + Parameters = @{ Identity = $MailboxUPN } + } + } + @{ + CmdletInput = @{ + CmdletName = 'Get-RecipientPermission' + Parameters = @{ Identity = $MailboxUPN } + } + } + } + + Write-Information "Built $($ExoBulkRequests.Count) bulk requests for batch $BatchNumber" + + # Execute bulk request for this batch with ReturnWithCommand to separate permission types + $MailboxPermissions = New-ExoBulkRequest -cmdletArray @($ExoBulkRequests) -tenantid $TenantFilter -ReturnWithCommand $true + + Write-Information "Bulk request completed. Result type: $($MailboxPermissions.GetType().Name)" + if ($MailboxPermissions -is [hashtable]) { + Write-Information "Result keys: $($MailboxPermissions.Keys -join ', ')" + if ($MailboxPermissions['Get-MailboxPermission']) { + Write-Information "Sample MailboxPermission: $($MailboxPermissions['Get-MailboxPermission'][0] | ConvertTo-Json -Depth 2 -Compress)" + } + if ($MailboxPermissions['Get-RecipientPermission']) { + Write-Information "Sample RecipientPermission: $($MailboxPermissions['Get-RecipientPermission'][0] | ConvertTo-Json -Depth 2 -Compress)" + } + } + + # Normalize MailboxPermission results + if ($MailboxPermissions['Get-MailboxPermission']) { + $NormalizedMailboxPerms = foreach ($Perm in $MailboxPermissions['Get-MailboxPermission']) { + # Create normalized object with consistent property names and unique ID + [PSCustomObject]@{ + id = [guid]::NewGuid().ToString() + Identity = $Perm.Identity + User = $Perm.User + AccessRights = $Perm.AccessRights + IsInherited = $Perm.IsInherited + Deny = $Perm.Deny + } + } + $MailboxPermissions['Get-MailboxPermission'] = $NormalizedMailboxPerms + } + + # Normalize the results - RecipientPermission uses 'Trustee' instead of 'User' + if ($MailboxPermissions['Get-RecipientPermission']) { + $NormalizedRecipientPerms = foreach ($Perm in $MailboxPermissions['Get-RecipientPermission']) { + # Create normalized object with consistent property names and unique ID + [PSCustomObject]@{ + id = [guid]::NewGuid().ToString() + Identity = $Perm.Identity + User = if ($Perm.Trustee) { $Perm.Trustee } else { $Perm.User } + AccessRights = $Perm.AccessRights + IsInherited = $Perm.IsInherited + Deny = $Perm.Deny + } + } + $MailboxPermissions['Get-RecipientPermission'] = $NormalizedRecipientPerms + } + + $MailboxPermCount = if ($MailboxPermissions['Get-MailboxPermission']) { $MailboxPermissions['Get-MailboxPermission'].Count } else { 0 } + $RecipientPermCount = if ($MailboxPermissions['Get-RecipientPermission']) { $MailboxPermissions['Get-RecipientPermission'].Count } else { 0 } + + Write-Information "Completed batch $BatchNumber of $TotalBatches - processed $($Mailboxes.Count) mailboxes: $MailboxPermCount mailbox permissions, $RecipientPermCount recipient permissions" + + # Return results to be aggregated by post-execution function + return $MailboxPermissions + + } catch { + $ErrorMsg = "Failed to process batch $BatchNumber of $TotalBatches for tenant $TenantFilter : $($_.Exception.Message)" + Write-Information "ERROR in Push-GetMailboxPermissionsBatch: $ErrorMsg" + Write-Information "Stack trace: $($_.ScriptStackTrace)" + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message $ErrorMsg -sev Error + throw $ErrorMsg + } +} diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Mailbox Permissions/Push-StoreMailboxPermissions.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Mailbox Permissions/Push-StoreMailboxPermissions.ps1 new file mode 100644 index 000000000000..fc5a967664b3 --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Mailbox Permissions/Push-StoreMailboxPermissions.ps1 @@ -0,0 +1,80 @@ +function Push-StoreMailboxPermissions { + <# + .SYNOPSIS + Post-execution function to aggregate and store all mailbox permissions + + .DESCRIPTION + Collects results from all batches and stores them in the reporting database + + .FUNCTIONALITY + Entrypoint + #> + [CmdletBinding()] + param($Item) + + $TenantFilter = $Item.Parameters.TenantFilter + $Results = $Item.Results + + try { + Write-Information "Storing mailbox permissions for tenant $TenantFilter" + Write-Information "Received $($Results.Count) batch results" + + # Log each result for debugging + for ($i = 0; $i -lt $Results.Count; $i++) { + $result = $Results[$i] + Write-Information "Result $i type: $($result.GetType().Name), value: $($result | ConvertTo-Json -Depth 2 -Compress)" + } + + # Aggregate results by command type from all batches + $AllMailboxPermissions = [System.Collections.Generic.List[object]]::new() + $AllRecipientPermissions = [System.Collections.Generic.List[object]]::new() + + foreach ($BatchResult in $Results) { + # Activity functions may return an array [hashtable, "status message"] + # Extract the actual hashtable if result is an array + $ActualResult = $BatchResult + if ($BatchResult -is [array] -and $BatchResult.Count -gt 0) { + Write-Information "Result is array with $($BatchResult.Count) elements, extracting first element" + $ActualResult = $BatchResult[0] + } + + if ($ActualResult -and $ActualResult -is [hashtable]) { + Write-Information "Processing hashtable result with keys: $($ActualResult.Keys -join ', ')" + # Results are grouped by cmdlet name due to ReturnWithCommand + if ($ActualResult['Get-MailboxPermission']) { + Write-Information "Adding $($ActualResult['Get-MailboxPermission'].Count) mailbox permissions" + $AllMailboxPermissions.AddRange($ActualResult['Get-MailboxPermission']) + } + if ($ActualResult['Get-RecipientPermission']) { + Write-Information "Adding $($ActualResult['Get-RecipientPermission'].Count) recipient permissions" + $AllRecipientPermissions.AddRange($ActualResult['Get-RecipientPermission']) + } + } else { + Write-Information "Skipping non-hashtable result: $($ActualResult.GetType().Name)" + } + } + +# Combine all permissions (mailbox and recipient) into a single collection + $AllPermissions = [System.Collections.Generic.List[object]]::new() + $AllPermissions.AddRange($AllMailboxPermissions) + $AllPermissions.AddRange($AllRecipientPermissions) + + Write-Information "Aggregated $($AllPermissions.Count) total permissions ($($AllMailboxPermissions.Count) mailbox + $($AllRecipientPermissions.Count) recipient)" + + # Store all permissions together as MailboxPermissions + if ($AllPermissions.Count -gt 0) { + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'MailboxPermissions' -Data $AllPermissions + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'MailboxPermissions' -Data $AllPermissions -Count + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($AllPermissions.Count) mailbox permission records" -sev Info + } else { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'No permissions found to cache' -sev Info + } + + return + + } catch { + $ErrorMsg = "Failed to store mailbox permissions for tenant $TenantFilter : $($_.Exception.Message)" + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message $ErrorMsg -sev Error + throw $ErrorMsg + } +} diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ExecCIPPDBCache.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ExecCIPPDBCache.ps1 new file mode 100644 index 000000000000..131760484736 --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ExecCIPPDBCache.ps1 @@ -0,0 +1,76 @@ +function Invoke-ExecCIPPDBCache { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + CIPP.Core.ReadWrite + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $TenantFilter = $Request.Query.TenantFilter + $Name = $Request.Query.Name + + Write-Information "ExecCIPPDBCache called with Name: '$Name', TenantFilter: '$TenantFilter'" + + try { + if ([string]::IsNullOrEmpty($Name)) { + throw 'Name parameter is required' + } + + if ([string]::IsNullOrEmpty($TenantFilter)) { + throw 'TenantFilter parameter is required' + } + + if ($TenantFilter -eq 'AllTenants') { + throw 'TenantFilter cannot be AllTenants for this operation' + } + + # Validate the function exists + $FunctionName = "Set-CIPPDBCache$Name" + $Function = Get-Command -Name $FunctionName -ErrorAction SilentlyContinue + if (-not $Function) { + throw "Cache function '$FunctionName' not found" + } + + Write-LogMessage -API $APIName -tenant $TenantFilter -message "Starting CIPP DB cache for $Name" -sev Info + + # Start orchestrator to run the cache function + $InputObject = [PSCustomObject]@{ + Batch = @([PSCustomObject]@{ + FunctionName = 'ExecCIPPDBCache' + Name = $Name + TenantFilter = $TenantFilter + }) + OrchestratorName = "CIPPDBCache_${Name}_$TenantFilter" + SkipLog = $false + } + + $InstanceId = Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Compress -Depth 5) + + Write-LogMessage -API $APIName -tenant $TenantFilter -message "Started CIPP DB cache orchestrator for $Name with instance ID: $InstanceId" -sev Info + + $Body = [PSCustomObject]@{ + Results = "Successfully started cache operation for $Name on tenant $TenantFilter" + Metadata = @{ + Name = $Name + Tenant = $TenantFilter + InstanceId = $InstanceId + } + } + $StatusCode = [HttpStatusCode]::OK + } catch { + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + Write-LogMessage -API $APIName -tenant $TenantFilter -message "Failed to start CIPP DB cache for $Name : $ErrorMessage" -sev Error + $Body = [PSCustomObject]@{ + Results = "Failed to start cache operation: $ErrorMessage" + } + $StatusCode = [HttpStatusCode]::BadRequest + } + + return ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = $Body + }) +} diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheMailboxes.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheMailboxes.ps1 index 3e3d93f776ba..41438fec3fd6 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheMailboxes.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheMailboxes.ps1 @@ -40,7 +40,7 @@ function Set-CIPPDBCacheMailboxes { Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Mailboxes' -Data $Mailboxes Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Mailboxes' -Data $Mailboxes -Count - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached mailboxes successfully' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($Mailboxes.Count) mailboxes successfully" -sev Info # Get CAS mailboxes Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching CAS mailboxes' -sev Info @@ -50,22 +50,46 @@ function Set-CIPPDBCacheMailboxes { $CASMailboxes = $null Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached CAS mailboxes successfully' -sev Info - # Get mailbox permissions using bulk request - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching mailbox permissions' -sev Info - $ExoBulkRequests = foreach ($Mailbox in $Mailboxes) { - @{ - CmdletInput = @{ - CmdletName = 'Get-MailboxPermission' - Parameters = @{ Identity = $Mailbox.UPN } + # Start orchestrator to cache mailbox permissions in batches + $MailboxCount = ($Mailboxes | Measure-Object).Count + if ($MailboxCount -gt 0) { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Starting mailbox permission caching for $MailboxCount mailboxes" -sev Info + + # Create batches of 10 mailboxes each + $BatchSize = 10 + $Batches = [System.Collections.Generic.List[object]]::new() + + for ($i = 0; $i -lt $Mailboxes.Count; $i += $BatchSize) { + $BatchMailboxes = $Mailboxes[$i..[Math]::Min($i + $BatchSize - 1, $Mailboxes.Count - 1)] + + # Only send UPN to batch function to reduce payload size + $BatchMailboxUPNs = $BatchMailboxes | Select-Object -ExpandProperty UPN + + $Batches.Add([PSCustomObject]@{ + FunctionName = 'GetMailboxPermissionsBatch' + TenantFilter = $TenantFilter + Mailboxes = $BatchMailboxUPNs + BatchNumber = [Math]::Floor($i / $BatchSize) + 1 + TotalBatches = [Math]::Ceiling($Mailboxes.Count / $BatchSize) + }) + } + + $InputObject = [PSCustomObject]@{ + Batch = $Batches + OrchestratorName = "MailboxPermissions_$TenantFilter" + DurableMode = 'Sequence' + PostExecution = @{ + FunctionName = 'StoreMailboxPermissions' + Parameters = @{ + TenantFilter = $TenantFilter + } } } + Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Compress -Depth 5) + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Started mailbox permission caching orchestrator with $($Batches.Count) batches" -sev Info + } else { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'No mailboxes found to cache permissions for' -sev Info } - $MailboxPermissions = New-ExoBulkRequest -cmdletArray @($ExoBulkRequests) -tenantid $TenantFilter - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'MailboxPermissions' -Data $MailboxPermissions - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'MailboxPermissions' -Data $MailboxPermissions -Count - $MailboxPermissions = $null - $Mailboxes = $null - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached mailbox permissions successfully' -sev Info } catch { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache mailboxes: $($_.Exception.Message)" -sev Error From fb72b2a4aa4c73ff3f930757c229a0da4296df7f Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 16 Jan 2026 11:45:44 -0500 Subject: [PATCH 169/503] Improve AllTenants handling in Invoke-ExecCIPPDBCache Refactored Invoke-ExecCIPPDBCache to support batch operations across all tenants by dynamically generating batches for each tenant when TenantFilter is 'AllTenants'. Updated log messages and result output to reflect multi-tenant operations. Also clarified error message in Get-CIPPMailboxPermissionReport to instruct users to sync mailbox permissions if no data is found. --- .../CIPP/Core/Invoke-ExecCIPPDBCache.ps1 | 40 +++++++++++++------ .../Get-CIPPMailboxPermissionReport.ps1 | 2 +- 2 files changed, 29 insertions(+), 13 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ExecCIPPDBCache.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ExecCIPPDBCache.ps1 index 131760484736..92e4249af55b 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ExecCIPPDBCache.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ExecCIPPDBCache.ps1 @@ -23,10 +23,6 @@ function Invoke-ExecCIPPDBCache { throw 'TenantFilter parameter is required' } - if ($TenantFilter -eq 'AllTenants') { - throw 'TenantFilter cannot be AllTenants for this operation' - } - # Validate the function exists $FunctionName = "Set-CIPPDBCache$Name" $Function = Get-Command -Name $FunctionName -ErrorAction SilentlyContinue @@ -36,15 +32,35 @@ function Invoke-ExecCIPPDBCache { Write-LogMessage -API $APIName -tenant $TenantFilter -message "Starting CIPP DB cache for $Name" -sev Info - # Start orchestrator to run the cache function - $InputObject = [PSCustomObject]@{ - Batch = @([PSCustomObject]@{ + # Handle AllTenants - create a batch for each tenant + if ($TenantFilter -eq 'AllTenants') { + $TenantList = Get-Tenants -IncludeErrors + $Batch = $TenantList | ForEach-Object { + [PSCustomObject]@{ FunctionName = 'ExecCIPPDBCache' Name = $Name - TenantFilter = $TenantFilter - }) - OrchestratorName = "CIPPDBCache_${Name}_$TenantFilter" - SkipLog = $false + TenantFilter = $_.defaultDomainName + } + } + + $InputObject = [PSCustomObject]@{ + Batch = @($Batch) + OrchestratorName = "CIPPDBCache_${Name}_AllTenants" + SkipLog = $false + } + + Write-LogMessage -API $APIName -tenant $TenantFilter -message "Starting CIPP DB cache for $Name across $($TenantList.Count) tenants" -sev Info + } else { + # Single tenant + $InputObject = [PSCustomObject]@{ + Batch = @([PSCustomObject]@{ + FunctionName = 'ExecCIPPDBCache' + Name = $Name + TenantFilter = $TenantFilter + }) + OrchestratorName = "CIPPDBCache_${Name}_$TenantFilter" + SkipLog = $false + } } $InstanceId = Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Compress -Depth 5) @@ -52,7 +68,7 @@ function Invoke-ExecCIPPDBCache { Write-LogMessage -API $APIName -tenant $TenantFilter -message "Started CIPP DB cache orchestrator for $Name with instance ID: $InstanceId" -sev Info $Body = [PSCustomObject]@{ - Results = "Successfully started cache operation for $Name on tenant $TenantFilter" + Results = "Successfully started cache operation for $Name$(if ($TenantFilter -eq 'AllTenants') { ' for all tenants' } else { " on tenant $TenantFilter" })" Metadata = @{ Name = $Name Tenant = $TenantFilter diff --git a/Modules/CIPPCore/Public/Get-CIPPMailboxPermissionReport.ps1 b/Modules/CIPPCore/Public/Get-CIPPMailboxPermissionReport.ps1 index 88cecefea547..29bb8efb1ac7 100644 --- a/Modules/CIPPCore/Public/Get-CIPPMailboxPermissionReport.ps1 +++ b/Modules/CIPPCore/Public/Get-CIPPMailboxPermissionReport.ps1 @@ -58,7 +58,7 @@ function Get-CIPPMailboxPermissionReport { # Get mailboxes from reporting DB $MailboxItems = Get-CIPPDbItem -TenantFilter $TenantFilter -Type 'Mailboxes' if (-not $MailboxItems) { - throw 'No mailbox data found in reporting database. Run a scan first. ' + throw 'No mailbox data found in reporting database. Sync the mailbox permissions first. ' } # Get the most recent mailbox cache timestamp From 5bba822a31f8d47248617ce105b86bbf9bb68156 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 16 Jan 2026 11:45:54 -0500 Subject: [PATCH 170/503] Enhance MFA state reporting and CA policy resolution Added Get-CIPPMFAStateReport.ps1 to retrieve MFA state from the reporting database and updated Invoke-ListMFAUsers.ps1 to support a UseReportDB query parameter. Improved Get-CIPPMFAState.ps1 to resolve Conditional Access policies with group membership and exclusions, providing more accurate per-user policy coverage. Set-CIPPDBCacheMFAState.ps1 now logs the number of cached MFA state records. --- .../Identity/Reports/Invoke-ListMFAUsers.ps1 | 91 +++--- Modules/CIPPCore/Public/Get-CIPPMFAState.ps1 | 258 ++++++++++++++++-- .../Public/Get-CIPPMFAStateReport.ps1 | 78 ++++++ .../Public/Set-CIPPDBCacheMFAState.ps1 | 4 +- 4 files changed, 369 insertions(+), 62 deletions(-) create mode 100644 Modules/CIPPCore/Public/Get-CIPPMFAStateReport.ps1 diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Reports/Invoke-ListMFAUsers.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Reports/Invoke-ListMFAUsers.ps1 index 65c79f2bb82c..04d6ceed951f 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Reports/Invoke-ListMFAUsers.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Reports/Invoke-ListMFAUsers.ps1 @@ -1,4 +1,4 @@ -Function Invoke-ListMFAUsers { +function Invoke-ListMFAUsers { <# .FUNCTIONALITY Entrypoint @@ -9,48 +9,69 @@ Function Invoke-ListMFAUsers { param($Request, $TriggerMetadata) # Interact with query parameters or the body of the request. $TenantFilter = $Request.Query.tenantFilter + $UseReportDB = $Request.Query.UseReportDB - if ($TenantFilter -ne 'AllTenants') { - $GraphRequest = Get-CIPPMFAState -TenantFilter $TenantFilter - } else { - $Table = Get-CIPPTable -TableName cachemfa + try { + # If UseReportDB is specified, retrieve from report database + if ($UseReportDB -eq 'true') { + $GraphRequest = Get-CIPPMFAStateReport -TenantFilter $TenantFilter + $StatusCode = [HttpStatusCode]::OK - $Rows = Get-CIPPAzDataTableEntity @Table | Where-Object -Property Timestamp -GT (Get-Date).AddHours(-2) - if (!$Rows) { - $TenantList = Get-Tenants -IncludeErrors - $Queue = New-CippQueueEntry -Name 'MFA Users - All Tenants' -Link '/identity/reports/mfa-report?customerId=AllTenants' -TotalTasks ($TenantList | Measure-Object).Count - Write-Information ($Queue | ConvertTo-Json) - $GraphRequest = [PSCustomObject]@{ - UPN = 'Loading data for all tenants. Please check back in a few minutes' - } - $Batch = $TenantList | ForEach-Object { - $_ | Add-Member -NotePropertyName FunctionName -NotePropertyValue 'ListMFAUsersQueue' - $_ | Add-Member -NotePropertyName QueueId -NotePropertyValue $Queue.RowKey - $_ - } - if (($Batch | Measure-Object).Count -gt 0) { - $InputObject = [PSCustomObject]@{ - OrchestratorName = 'ListMFAUsersOrchestrator' - Batch = @($Batch) - SkipLog = $true - } - #Write-Host ($InputObject | ConvertTo-Json) - $InstanceId = Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Depth 5 -Compress) - Write-Host "Started permissions orchestration with ID = '$InstanceId'" - } + return ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = @($GraphRequest) + }) + } + + # Original cache table logic + if ($TenantFilter -ne 'AllTenants') { + $GraphRequest = Get-CIPPMFAState -TenantFilter $TenantFilter } else { - $Rows = foreach ($Row in $Rows) { - if ($Row.CAPolicies) { - $Row.CAPolicies = try { $Row.CAPolicies | ConvertFrom-Json } catch { $Row.CAPolicies } + $Table = Get-CIPPTable -TableName cachemfa + + $Rows = Get-CIPPAzDataTableEntity @Table | Where-Object -Property Timestamp -GT (Get-Date).AddHours(-2) + if (!$Rows) { + $TenantList = Get-Tenants -IncludeErrors + $Queue = New-CippQueueEntry -Name 'MFA Users - All Tenants' -Link '/identity/reports/mfa-report?customerId=AllTenants' -TotalTasks ($TenantList | Measure-Object).Count + Write-Information ($Queue | ConvertTo-Json) + $GraphRequest = [PSCustomObject]@{ + UPN = 'Loading data for all tenants. Please check back in a few minutes' + } + $Batch = $TenantList | ForEach-Object { + $_ | Add-Member -NotePropertyName FunctionName -NotePropertyValue 'ListMFAUsersQueue' + $_ | Add-Member -NotePropertyName QueueId -NotePropertyValue $Queue.RowKey + $_ } - if ($Row.MFAMethods) { - $Row.MFAMethods = try { $Row.MFAMethods | ConvertFrom-Json } catch { $Row.MFAMethods } + if (($Batch | Measure-Object).Count -gt 0) { + $InputObject = [PSCustomObject]@{ + OrchestratorName = 'ListMFAUsersOrchestrator' + Batch = @($Batch) + SkipLog = $true + } + #Write-Host ($InputObject | ConvertTo-Json) + $InstanceId = Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Depth 5 -Compress) + Write-Host "Started permissions orchestration with ID = '$InstanceId'" } - $Row + } else { + $Rows = foreach ($Row in $Rows) { + if ($Row.CAPolicies) { + $Row.CAPolicies = try { $Row.CAPolicies | ConvertFrom-Json } catch { $Row.CAPolicies } + } + if ($Row.MFAMethods) { + $Row.MFAMethods = try { $Row.MFAMethods | ConvertFrom-Json } catch { $Row.MFAMethods } + } + $Row + } + $GraphRequest = $Rows } - $GraphRequest = $Rows } + $StatusCode = [HttpStatusCode]::OK + } catch { + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + $StatusCode = [HttpStatusCode]::Forbidden + $GraphRequest = $ErrorMessage } + return ([HttpResponseContext]@{ StatusCode = [HttpStatusCode]::OK Body = @($GraphRequest) diff --git a/Modules/CIPPCore/Public/Get-CIPPMFAState.ps1 b/Modules/CIPPCore/Public/Get-CIPPMFAState.ps1 index 6f5eef720383..1f16664f1da3 100644 --- a/Modules/CIPPCore/Public/Get-CIPPMFAState.ps1 +++ b/Modules/CIPPCore/Public/Get-CIPPMFAState.ps1 @@ -26,7 +26,7 @@ function Get-CIPPMFAState { } $CAState = [System.Collections.Generic.List[object]]::new() - Try { + try { $MFARegistration = (New-GraphGetRequest -uri "https://graph.microsoft.com/beta/reports/authenticationMethods/userRegistrationDetails?$top=999&$select=userPrincipalName,isMfaRegistered,isMfaCapable,methodsRegistered" -tenantid $TenantFilter -asapp $true) $MFAIndex = @{} foreach ($MFAEntry in $MFARegistration) { @@ -45,15 +45,158 @@ function Get-CIPPMFAState { if ($null -ne $MFARegistration) { $CASuccess = $true try { - $CAPolicies = (New-GraphGetRequest -Uri 'https://graph.microsoft.com/beta/identity/conditionalAccess/policies?$top=999&$filter=state eq 'enabled'&$select=id,displayName,state,grantControls,conditions' -tenantid $TenantFilter -ErrorAction Stop) + $CAPolicies = (New-GraphGetRequest -Uri 'https://graph.microsoft.com/beta/identity/conditionalAccess/policies?$top=999&$filter=state eq ''enabled''&$select=id,displayName,state,grantControls,conditions' -tenantid $TenantFilter -ErrorAction Stop -AsApp $true) $PolicyTable = @{} + $AllUserPolicies = [System.Collections.Generic.List[object]]::new() + $GroupsToResolve = [System.Collections.Generic.HashSet[string]]::new() + $ExcludeGroupsToResolve = [System.Collections.Generic.HashSet[string]]::new() + foreach ($Policy in $CAPolicies) { - if ($Policy.conditions.users.includeUsers -ne $null) { - foreach ($UserId in $Policy.conditions.users.includeUsers) { - if (-not $PolicyTable.ContainsKey($UserId)) { - $PolicyTable[$UserId] = [System.Collections.Generic.List[object]]::new() + # Only include policies that require MFA + $RequiresMFA = $false + if ($Policy.grantControls.builtInControls -contains 'mfa') { + $RequiresMFA = $true + } + # Check for authentication strength requiring MFA + if ($Policy.grantControls.authenticationStrength.requirementsSatisfied -eq 'mfa') { + $RequiresMFA = $true + } + + if ($RequiresMFA) { + # Handle user assignments + if ($Policy.conditions.users.includeUsers -ne $null) { + # Check if "All" is included + if ($Policy.conditions.users.includeUsers -contains 'All') { + $AllUserPolicies.Add($Policy) + } else { + foreach ($UserId in $Policy.conditions.users.includeUsers) { + if (-not $PolicyTable.ContainsKey($UserId)) { + $PolicyTable[$UserId] = [System.Collections.Generic.List[object]]::new() + } + $PolicyTable[$UserId].Add($Policy) + } + } + } + + # Collect groups to resolve + if ($Policy.conditions.users.includeGroups -ne $null -and $Policy.conditions.users.includeGroups.Count -gt 0) { + foreach ($GroupId in $Policy.conditions.users.includeGroups) { + [void]$GroupsToResolve.Add($GroupId) + } + } + + # Collect exclude groups to resolve + if ($Policy.conditions.users.excludeGroups -ne $null -and $Policy.conditions.users.excludeGroups.Count -gt 0) { + foreach ($GroupId in $Policy.conditions.users.excludeGroups) { + [void]$ExcludeGroupsToResolve.Add($GroupId) + } + } + } + } + + # Resolve group memberships using bulk request + $UserGroupMembership = @{} + $UserExcludeGroupMembership = @{} + $GroupNameLookup = @{} + + if ($GroupsToResolve.Count -gt 0 -or $ExcludeGroupsToResolve.Count -gt 0) { + $GroupMemberRequests = [system.collections.generic.list[object]]::new() + $GroupDetailsRequests = [system.collections.generic.list[object]]::new() + Write-Information "Resolving group memberships for $($GroupsToResolve.Count) include groups and $($ExcludeGroupsToResolve.Count) exclude groups" + # Add include group requests + foreach ($GroupId in $GroupsToResolve) { + $GroupMemberRequests.Add(@{ + id = "include-$GroupId" + method = 'GET' + url = "groups/$($GroupId)/members?`$select=id" + }) + $GroupDetailsRequests.Add(@{ + id = "details-$GroupId" + method = 'GET' + url = "groups/$($GroupId)?`$select=id,displayName" + }) + } + + # Add exclude group requests + foreach ($GroupId in $ExcludeGroupsToResolve) { + $GroupMemberRequests.Add(@{ + id = "exclude-$GroupId" + method = 'GET' + url = "groups/$($GroupId)/members?`$select=id" + }) + $GroupDetailsRequests.Add(@{ + id = "details-$GroupId" + method = 'GET' + url = "groups/$($GroupId)?`$select=id,displayName" + }) + } + + $GroupMembersResults = New-GraphBulkRequest -Requests @($GroupMemberRequests) -tenantid $TenantFilter + $GroupDetailsResults = New-GraphBulkRequest -Requests @($GroupDetailsRequests) -tenantid $TenantFilter + + # Build group name lookup + $GroupNameLookup = @{} + foreach ($GroupDetail in $GroupDetailsResults) { + if ($GroupDetail.status -eq 200 -and $GroupDetail.body) { + $GroupId = $GroupDetail.id -replace '^details-', '' + $GroupNameLookup[$GroupId] = $GroupDetail.body.displayName + Write-Host "Added group to lookup: $GroupId = $($GroupDetail.body.displayName)" + } else { + Write-Host "Failed to get group details: $($GroupDetail.id) - Status: $($GroupDetail.status)" + } + } + + # Build mapping of user to groups they're in + foreach ($GroupResult in $GroupMembersResults) { + if ($GroupResult.status -eq 200 -and $GroupResult.body.value) { + $IsExclude = $GroupResult.id -like 'exclude-*' + $GroupId = $GroupResult.id -replace '^(include-|exclude-)', '' + + foreach ($Member in $GroupResult.body.value) { + if ($IsExclude) { + if (-not $UserExcludeGroupMembership.ContainsKey($Member.id)) { + $UserExcludeGroupMembership[$Member.id] = [System.Collections.Generic.HashSet[string]]::new() + } + [void]$UserExcludeGroupMembership[$Member.id].Add($GroupId) + } else { + if (-not $UserGroupMembership.ContainsKey($Member.id)) { + $UserGroupMembership[$Member.id] = [System.Collections.Generic.HashSet[string]]::new() + } + [void]$UserGroupMembership[$Member.id].Add($GroupId) + } + } + } + } + + # Now add policies to users based on group membership + foreach ($Policy in $CAPolicies | Where-Object { $_.conditions.users.includeGroups -ne $null -and $_.conditions.users.includeGroups.Count -gt 0 }) { + # Check if this policy requires MFA + $RequiresMFA = $false + if ($Policy.grantControls.builtInControls -contains 'mfa') { + $RequiresMFA = $true + } + if ($Policy.grantControls.authenticationStrength.requirementsSatisfied -eq 'mfa') { + $RequiresMFA = $true + } + + if ($RequiresMFA) { + foreach ($UserId in $UserGroupMembership.Keys) { + # Check if user is member of any of the policy's included groups + $IsMember = $false + foreach ($GroupId in $Policy.conditions.users.includeGroups) { + if ($UserGroupMembership[$UserId].Contains($GroupId)) { + $IsMember = $true + break + } + } + + if ($IsMember) { + if (-not $PolicyTable.ContainsKey($UserId)) { + $PolicyTable[$UserId] = [System.Collections.Generic.List[object]]::new() + } + $PolicyTable[$UserId].Add($Policy) + } } - $PolicyTable[$UserId].Add($Policy) } } } @@ -66,32 +209,97 @@ function Get-CIPPMFAState { if ($CAState.count -eq 0) { $CAState.Add('None') | Out-Null } - $assignments = New-GraphGetRequest -uri "https://graph.microsoft.com/v1.0/roleManagement/directory/roleAssignments?`$expand=principal" -tenantid $TenantFilter -ErrorAction SilentlyContinue + $assignments = New-GraphGetRequest -uri "https://graph.microsoft.com/v1.0/roleManagement/directory/roleAssignments?`$expand=principal" -tenantid $TenantFilter -ErrorAction SilentlyContinue $adminObjectIds = $assignments | - Where-Object { - $_.principal.'@odata.type' -eq '#microsoft.graph.user' - } | - ForEach-Object { - $_.principal.id - } + Where-Object { + $_.principal.'@odata.type' -eq '#microsoft.graph.user' + } | + ForEach-Object { + $_.principal.id + } # Interact with query parameters or the body of the request. $GraphRequest = $Users | ForEach-Object { $UserCAState = [System.Collections.Generic.List[object]]::new() - foreach ($CA in $CAState) { - if ($CA.IncludedUsers -eq 'All' -or $CA.IncludedUsers -contains $_.ObjectId) { - $UserCAState.Add([PSCustomObject]@{ - DisplayName = $CA.DisplayName - UserIncluded = ($CA.ExcludedUsers -notcontains $_.ObjectId) - AllApps = $CA.IsAllApps - PolicyState = $CA.State - Platforms = $CA.Platforms -join ', ' - }) + + # Add policies that apply to this specific user + if ($PolicyTable.ContainsKey($_.ObjectId)) { + foreach ($Policy in $PolicyTable[$_.ObjectId]) { + # Check if user is excluded directly or via group + $IsExcluded = $Policy.conditions.users.excludeUsers -contains $_.ObjectId + $ExcludedViaGroup = $null + + # Check exclude groups + if (-not $IsExcluded -and $Policy.conditions.users.excludeGroups -ne $null -and $Policy.conditions.users.excludeGroups.Count -gt 0) { + if ($UserExcludeGroupMembership.ContainsKey($_.ObjectId)) { + foreach ($ExcludeGroupId in $Policy.conditions.users.excludeGroups) { + if ($UserExcludeGroupMembership[$_.ObjectId].Contains($ExcludeGroupId)) { + $IsExcluded = $true + $ExcludedViaGroup = if ($GroupNameLookup.ContainsKey($ExcludeGroupId)) { + $GroupNameLookup[$ExcludeGroupId] + } else { + $ExcludeGroupId + } + break + } + } + } + } + + $PolicyObj = [PSCustomObject]@{ + DisplayName = $Policy.displayName + UserIncluded = -not $IsExcluded + AllApps = ($Policy.conditions.applications.includeApplications -contains 'All') + PolicyState = $Policy.state + } + if ($ExcludedViaGroup) { + $PolicyObj | Add-Member -NotePropertyName 'ExcludedViaGroup' -NotePropertyValue $ExcludedViaGroup + } + $UserCAState.Add($PolicyObj) } } - if ($UserCAState.UserIncluded -eq $true -and $UserCAState.PolicyState -eq 'enabled') { - if ($UserCAState.UserIncluded -eq $true -and $UserCAState.PolicyState -eq 'enabled' -and $UserCAState.AllApps) { + + # Add policies that apply to all users + foreach ($Policy in $AllUserPolicies) { + # Check if user is excluded directly or via group + $IsExcluded = $Policy.conditions.users.excludeUsers -contains $_.ObjectId + $ExcludedViaGroup = $null + + # Check exclude groups + if (-not $IsExcluded -and $Policy.conditions.users.excludeGroups -ne $null -and $Policy.conditions.users.excludeGroups.Count -gt 0) { + if ($UserExcludeGroupMembership.ContainsKey($_.ObjectId)) { + foreach ($ExcludeGroupId in $Policy.conditions.users.excludeGroups) { + if ($UserExcludeGroupMembership[$_.ObjectId].Contains($ExcludeGroupId)) { + $IsExcluded = $true + $ExcludedViaGroup = if ($GroupNameLookup.ContainsKey($ExcludeGroupId)) { + $GroupNameLookup[$ExcludeGroupId] + } else { + $ExcludeGroupId + } + break + } + } + } + } + + # Always add the policy to show it applies (even if excluded) + $PolicyObj = [PSCustomObject]@{ + DisplayName = $Policy.displayName + UserIncluded = -not $IsExcluded + AllApps = ($Policy.conditions.applications.includeApplications -contains 'All') + PolicyState = $Policy.state + } + if ($ExcludedViaGroup) { + $PolicyObj | Add-Member -NotePropertyName 'ExcludedViaGroup' -NotePropertyValue $ExcludedViaGroup + } + $UserCAState.Add($PolicyObj) + } + + # Determine if user is covered by CA + if ($UserCAState.Count -gt 0 -and ($UserCAState | Where-Object { $_.UserIncluded -eq $true -and $_.PolicyState -eq 'enabled' })) { + $EnabledPolicies = $UserCAState | Where-Object { $_.UserIncluded -eq $true -and $_.PolicyState -eq 'enabled' } + if ($EnabledPolicies | Where-Object { $_.AllApps -eq $true }) { $CoveredByCA = 'Enforced - All Apps' } else { $CoveredByCA = 'Enforced - Specific Apps' diff --git a/Modules/CIPPCore/Public/Get-CIPPMFAStateReport.ps1 b/Modules/CIPPCore/Public/Get-CIPPMFAStateReport.ps1 new file mode 100644 index 000000000000..d0b32d9bbf55 --- /dev/null +++ b/Modules/CIPPCore/Public/Get-CIPPMFAStateReport.ps1 @@ -0,0 +1,78 @@ +function Get-CIPPMFAStateReport { + <# + .SYNOPSIS + Generates an MFA state report from the CIPP Reporting database + + .DESCRIPTION + Retrieves MFA state data for a tenant from the reporting database + + .PARAMETER TenantFilter + The tenant to generate the report for + + .EXAMPLE + Get-CIPPMFAStateReport -TenantFilter 'contoso.onmicrosoft.com' + Gets MFA state for all users in the tenant + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter + ) + + try { + + # Handle AllTenants + if ($TenantFilter -eq 'AllTenants') { + # Get all tenants that have MFA data + $AllMFAItems = Get-CIPPDbItem -TenantFilter 'allTenants' -Type 'MFAState' + $Tenants = @($AllMFAItems | Where-Object { $_.RowKey -ne 'MFAState-Count' } | Select-Object -ExpandProperty PartitionKey -Unique) + + $AllResults = [System.Collections.Generic.List[PSCustomObject]]::new() + foreach ($Tenant in $Tenants) { + try { + $TenantResults = Get-CIPPMFAStateReport -TenantFilter $Tenant + foreach ($Result in $TenantResults) { + # Add Tenant property to each result + $Result | Add-Member -NotePropertyName 'Tenant' -NotePropertyValue $Tenant -Force + $AllResults.Add($Result) + } + } catch { + Write-LogMessage -API 'MFAStateReport' -tenant $Tenant -message "Failed to get report for tenant: $($_.Exception.Message)" -sev Warning + } + } + return $AllResults + } + + # Get MFA state from reporting DB + $MFAItems = Get-CIPPDbItem -TenantFilter $TenantFilter -Type 'MFAState' + if (-not $MFAItems) { + throw 'No MFA state data found in reporting database. Sync the report data first.' + } + # Get the most recent cache timestamp + $CacheTimestamp = ($MFAItems | Where-Object { $_.Timestamp } | Sort-Object Timestamp -Descending | Select-Object -First 1).Timestamp + # Parse MFA state data + $AllMFAState = [System.Collections.Generic.List[PSCustomObject]]::new() + foreach ($Item in $MFAItems | Where-Object { $_.RowKey -ne 'MFAState-Count' }) { + $MFAUser = $Item.Data | ConvertFrom-Json + + # Parse nested JSON properties if they're strings + if ($MFAUser.CAPolicies -is [string]) { + $MFAUser.CAPolicies = try { $MFAUser.CAPolicies | ConvertFrom-Json } catch { $MFAUser.CAPolicies } + } + if ($MFAUser.MFAMethods -is [string]) { + $MFAUser.MFAMethods = try { $MFAUser.MFAMethods | ConvertFrom-Json } catch { $MFAUser.MFAMethods } + } + + # Add cache timestamp + $MFAUser | Add-Member -NotePropertyName 'CacheTimestamp' -NotePropertyValue $CacheTimestamp -Force + + $AllMFAState.Add($MFAUser) + } + + return $AllMFAState + + } catch { + Write-LogMessage -API 'MFAStateReport' -tenant $TenantFilter -message "Failed to generate MFA state report: $($_.Exception.Message)" -sev Error + throw + } +} diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheMFAState.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheMFAState.ps1 index 131f5a1ac16c..2f2da95b6e9f 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheMFAState.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheMFAState.ps1 @@ -17,9 +17,9 @@ function Set-CIPPDBCacheMFAState { $MFAState = Get-CIPPMFAState -TenantFilter $TenantFilter Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'MFAState' -Data @($MFAState) - $MFAState = $null + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'MFAState' -Data @($MFAState) -Count - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached MFA state successfully' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($MFAState.Count) MFA state records successfully" -sev Info } catch { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache MFA state: $($_.Exception.Message)" -sev Error From c9d2940ab3836e905c6a2df803b350a9de524fe5 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 16 Jan 2026 11:51:34 -0500 Subject: [PATCH 171/503] Fix typo in log message in Push-ExecCIPPDBCache.ps1 --- .../Activity Triggers/CIPPDBCache/Push-ExecCIPPDBCache.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/CIPPDBCache/Push-ExecCIPPDBCache.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/CIPPDBCache/Push-ExecCIPPDBCache.ps1 index 041420b2d5c5..a6a68cfc8510 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/CIPPDBCache/Push-ExecCIPPDBCache.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/CIPPDBCache/Push-ExecCIPPDBCache.ps1 @@ -16,7 +16,7 @@ function Push-ExecCIPPDBCache { $TenantFilter = $Item.TenantFilter try { - Write-Information "Colllecting $Name for tenant $TenantFilter" + Write-Information "Collecting $Name for tenant $TenantFilter" # Build the full function name $FullFunctionName = "Set-CIPPDBCache$Name" From f6f26d53cd4ff6d274d4032e5011961b1b697f95 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 16 Jan 2026 12:16:36 -0500 Subject: [PATCH 172/503] Update default version to 10.0.0 Changed the defaultVersion in host.json and updated version_latest.txt to reflect the new version 10.0.0, replacing 8.8.2. --- host.json | 2 +- version_latest.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/host.json b/host.json index 0e7bc9b1617d..ae2a91f1ca55 100644 --- a/host.json +++ b/host.json @@ -16,7 +16,7 @@ "distributedTracingEnabled": false, "version": "None" }, - "defaultVersion": "8.8.2", + "defaultVersion": "10.0.0", "versionMatchStrategy": "Strict", "versionFailureStrategy": "Fail" } diff --git a/version_latest.txt b/version_latest.txt index 11f1d47dac93..a13e7b9c87e4 100644 --- a/version_latest.txt +++ b/version_latest.txt @@ -1 +1 @@ -8.8.2 +10.0.0 From 2f19fe51714eef96271f2ee9d381f813a31fbf1a Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Fri, 16 Jan 2026 18:20:04 +0100 Subject: [PATCH 173/503] minor prerelease change --- .../Tenant/Standards/Invoke-RemoveStandardTemplate.ps1 | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-RemoveStandardTemplate.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-RemoveStandardTemplate.ps1 index f5440b35b796..d9d3f9e2dfc0 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-RemoveStandardTemplate.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-RemoveStandardTemplate.ps1 @@ -18,7 +18,11 @@ function Invoke-RemoveStandardTemplate { $Table = Get-CippTable -tablename 'templates' $Filter = "PartitionKey eq 'StandardsTemplateV2' and GUID eq '$ID'" $ClearRow = Get-CIPPAzDataTableEntity @Table -Filter $Filter -Property PartitionKey, RowKey, JSON - $TemplateName = (ConvertFrom-Json -InputObject $ClearRow.JSON -ErrorAction SilentlyContinue).templateName + if ($ClearRow.JSON) { + $TemplateName = (ConvertFrom-Json -InputObject $ClearRow.JSON -ErrorAction SilentlyContinue).templateName + } else { + $TemplateName = '' + } Remove-AzDataTableEntity -Force @Table -Entity $ClearRow $Result = "Removed Standards Template named: '$($TemplateName)' with id: $($ID)" Write-LogMessage -Headers $Headers -API $APIName -message $Result -Sev Info From 72050d3249040f00ea4204dda9c14d3bd2452c05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Fri, 16 Jan 2026 19:05:17 +0100 Subject: [PATCH 174/503] feat: add auto enable archive mailbox standard --- .../Invoke-CIPPStandardAutoArchiveMailbox.ps1 | 96 +++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAutoArchiveMailbox.ps1 diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAutoArchiveMailbox.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAutoArchiveMailbox.ps1 new file mode 100644 index 000000000000..ab6d73d2d135 --- /dev/null +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAutoArchiveMailbox.ps1 @@ -0,0 +1,96 @@ +function Invoke-CIPPStandardAutoArchiveMailbox { + <# + .FUNCTIONALITY + Internal + .COMPONENT + (APIName) AutoArchiveMailbox + .SYNOPSIS + (Label) Set auto enable archive mailbox state + .DESCRIPTION + (Helptext) Enables or disables the tenant policy that automatically provisions an archive mailbox when a user's primary mailbox reaches 90% of its quota. + (DocsDescription) Enables or disables the tenant policy that automatically provisions an archive mailbox when a user's primary mailbox reaches 90% of its quota. This is separate from auto-archiving thresholds and does not enable archives for all users immediately. + .NOTES + CAT + Exchange Standards + TAG + EXECUTIVETEXT + Automatically provisions archive mailboxes only when users reach 90% of their mailbox capacity, reducing manual intervention and preventing mailbox quota issues without enabling archives for everyone. + ADDEDCOMPONENT + {"type":"autoComplete","multiple":false,"creatable":false,"label":"Select value","name":"standards.AutoArchiveMailbox.state","options":[{"label":"Enabled","value":"enabled"},{"label":"Disabled","value":"disabled"}]} + IMPACT + Low Impact + ADDEDDATE + 2026-01-16 + POWERSHELLEQUIVALENT + Set-OrganizationConfig -AutoEnableArchiveMailbox \$true\|\$false + RECOMMENDEDBY + UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block + .LINK + https://docs.cipp.app/user-documentation/tenant/standards/list-standards + #> + + param($Tenant, $Settings) + $TestResult = Test-CIPPStandardLicense -StandardName 'AutoArchiveMailbox' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') + + if ($TestResult -eq $false) { + Write-Host "We're exiting as the correct license is not present for this standard." + return $true + } + + $StateValue = $Settings.state.value ?? $Settings.state + + if ([string]::IsNullOrWhiteSpace($StateValue)) { + Write-LogMessage -API 'Standards' -Tenant $Tenant -Message 'AutoArchiveMailbox: Invalid state parameter set' -Sev Error + return + } + + $DesiredState = $StateValue -eq 'enabled' + + try { + $CurrentState = (New-ExoRequest -tenantid $Tenant -cmdlet 'Get-OrganizationConfig' -Select 'AutoEnableArchiveMailbox').AutoEnableArchiveMailbox + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the AutoArchiveMailbox state for $Tenant. Error: $($ErrorMessage.NormalizedError)" -Sev Error -LogData $ErrorMessage + return + } + + $CorrectState = $CurrentState -eq $DesiredState + + $ExpectedValue = [PSCustomObject]@{ + AutoEnableArchiveMailbox = $DesiredState + } + $CurrentValue = [PSCustomObject]@{ + AutoEnableArchiveMailbox = $CurrentState + } + + if ($Settings.remediate -eq $true) { + Write-Host 'Time to remediate' + + if ($CorrectState) { + Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Auto enable archive mailbox is already set to $StateValue." -Sev Info + } else { + try { + New-ExoRequest -tenantid $Tenant -cmdlet 'Set-OrganizationConfig' -cmdParams @{ AutoEnableArchiveMailbox = $DesiredState } + Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Auto enable archive mailbox has been set to $StateValue." -Sev Info + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Failed to set auto enable archive mailbox to $StateValue. Error: $($ErrorMessage.NormalizedError)" -Sev Error -LogData $ErrorMessage + } + } + } + + if ($Settings.alert -eq $true) { + if ($CorrectState) { + Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Auto enable archive mailbox is correctly set to $StateValue." -Sev Info + } else { + Write-StandardsAlert -message "Auto enable archive mailbox is set to $CurrentState but should be $DesiredState." -object @{ CurrentState = $CurrentState; DesiredState = $DesiredState } -tenant $Tenant -standardName 'AutoArchiveMailbox' -standardId $Settings.standardId + Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Auto enable archive mailbox is set to $CurrentState but should be $DesiredState." -Sev Info + } + } + + if ($Settings.report -eq $true) { + Set-CIPPStandardsCompareField -FieldName 'standards.AutoArchiveMailbox' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant + Add-CIPPBPAField -FieldName 'AutoArchiveMailbox' -FieldValue $CurrentState -StoreAs bool -Tenant $Tenant + } +} From c5eeab14e029f7b34e1f69e6cdf33e255896fd59 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 16 Jan 2026 13:20:27 -0500 Subject: [PATCH 175/503] Change log message severity from Info to Debug Updated all Write-LogMessage calls in public CIPPCore module scripts to use severity 'Debug' instead of 'Info' for standard operation messages. This reduces log verbosity for routine cache and add operations, reserving 'Info' for more significant events. --- Modules/CIPPCore/Public/Add-CIPPDbItem.ps1 | 2 +- .../Set-CIPPDBCacheAdminConsentRequestPolicy.ps1 | 4 ++-- .../Public/Set-CIPPDBCacheAppRoleAssignments.ps1 | 4 ++-- Modules/CIPPCore/Public/Set-CIPPDBCacheApps.ps1 | 4 ++-- .../Set-CIPPDBCacheAuthenticationFlowsPolicy.ps1 | 4 ++-- .../Set-CIPPDBCacheAuthenticationMethodsPolicy.ps1 | 4 ++-- .../Public/Set-CIPPDBCacheAuthorizationPolicy.ps1 | 4 ++-- .../Public/Set-CIPPDBCacheB2BManagementPolicy.ps1 | 6 +++--- .../Set-CIPPDBCacheConditionalAccessPolicies.ps1 | 12 ++++++------ ...IPPDBCacheCredentialUserRegistrationDetails.ps1 | 4 ++-- .../Set-CIPPDBCacheCrossTenantAccessPolicy.ps1 | 4 ++-- .../Set-CIPPDBCacheDefaultAppManagementPolicy.ps1 | 4 ++-- .../Set-CIPPDBCacheDeviceRegistrationPolicy.ps1 | 4 ++-- .../Public/Set-CIPPDBCacheDeviceSettings.ps1 | 4 ++-- Modules/CIPPCore/Public/Set-CIPPDBCacheDevices.ps1 | 4 ++-- .../Set-CIPPDBCacheDirectoryRecommendations.ps1 | 4 ++-- Modules/CIPPCore/Public/Set-CIPPDBCacheDomains.ps1 | 4 ++-- .../Public/Set-CIPPDBCacheExoAcceptedDomains.ps1 | 4 ++-- .../Set-CIPPDBCacheExoAdminAuditLogConfig.ps1 | 4 ++-- .../Public/Set-CIPPDBCacheExoAntiPhishPolicies.ps1 | 6 +++--- .../Public/Set-CIPPDBCacheExoAntiPhishPolicy.ps1 | 4 ++-- .../Public/Set-CIPPDBCacheExoAtpPolicyForO365.ps1 | 4 ++-- .../Public/Set-CIPPDBCacheExoDkimSigningConfig.ps1 | 4 ++-- ...Set-CIPPDBCacheExoHostedContentFilterPolicy.ps1 | 4 ++-- ...IPPDBCacheExoHostedOutboundSpamFilterPolicy.ps1 | 4 ++-- .../Set-CIPPDBCacheExoMalwareFilterPolicies.ps1 | 6 +++--- .../Set-CIPPDBCacheExoMalwareFilterPolicy.ps1 | 4 ++-- .../Set-CIPPDBCacheExoOrganizationConfig.ps1 | 4 ++-- .../Set-CIPPDBCacheExoPresetSecurityPolicy.ps1 | 4 ++-- .../Public/Set-CIPPDBCacheExoQuarantinePolicy.ps1 | 4 ++-- .../Public/Set-CIPPDBCacheExoRemoteDomain.ps1 | 4 ++-- .../Set-CIPPDBCacheExoSafeAttachmentPolicies.ps1 | 6 +++--- .../Set-CIPPDBCacheExoSafeAttachmentPolicy.ps1 | 4 ++-- .../Public/Set-CIPPDBCacheExoSafeLinksPolicies.ps1 | 6 +++--- .../Public/Set-CIPPDBCacheExoSafeLinksPolicy.ps1 | 4 ++-- .../Public/Set-CIPPDBCacheExoSharingPolicy.ps1 | 4 ++-- .../Set-CIPPDBCacheExoTenantAllowBlockList.ps1 | 6 +++--- .../Public/Set-CIPPDBCacheExoTransportRules.ps1 | 4 ++-- Modules/CIPPCore/Public/Set-CIPPDBCacheGroups.ps1 | 6 +++--- Modules/CIPPCore/Public/Set-CIPPDBCacheGuests.ps1 | 4 ++-- .../Set-CIPPDBCacheIntuneAppProtectionPolicies.ps1 | 6 +++--- .../Public/Set-CIPPDBCacheIntunePolicies.ps1 | 12 ++++++------ .../Public/Set-CIPPDBCacheLicenseOverview.ps1 | 4 ++-- .../CIPPCore/Public/Set-CIPPDBCacheMFAState.ps1 | 4 ++-- .../Public/Set-CIPPDBCacheMailboxUsage.ps1 | 4 ++-- .../CIPPCore/Public/Set-CIPPDBCacheMailboxes.ps1 | 14 +++++++------- ...et-CIPPDBCacheManagedDeviceEncryptionStates.ps1 | 4 ++-- .../Public/Set-CIPPDBCacheManagedDevices.ps1 | 4 ++-- .../Set-CIPPDBCacheOAuth2PermissionGrants.ps1 | 4 ++-- .../Public/Set-CIPPDBCacheOneDriveUsage.ps1 | 4 ++-- .../Public/Set-CIPPDBCacheOrganization.ps1 | 4 ++-- .../CIPPCore/Public/Set-CIPPDBCachePIMSettings.ps1 | 10 +++++----- .../Public/Set-CIPPDBCacheRiskDetections.ps1 | 6 +++--- .../Set-CIPPDBCacheRiskyServicePrincipals.ps1 | 6 +++--- .../CIPPCore/Public/Set-CIPPDBCacheRiskyUsers.ps1 | 6 +++--- .../Set-CIPPDBCacheRoleEligibilitySchedules.ps1 | 4 ++-- .../Set-CIPPDBCacheRoleManagementPolicies.ps1 | 4 ++-- Modules/CIPPCore/Public/Set-CIPPDBCacheRoles.ps1 | 6 +++--- .../CIPPCore/Public/Set-CIPPDBCacheSecureScore.ps1 | 4 ++-- ...t-CIPPDBCacheServicePrincipalRiskDetections.ps1 | 6 +++--- .../Public/Set-CIPPDBCacheServicePrincipals.ps1 | 4 ++-- .../CIPPCore/Public/Set-CIPPDBCacheSettings.ps1 | 4 ++-- .../Set-CIPPDBCacheUserRegistrationDetails.ps1 | 4 ++-- Modules/CIPPCore/Public/Set-CIPPDBCacheUsers.ps1 | 4 ++-- 64 files changed, 156 insertions(+), 156 deletions(-) diff --git a/Modules/CIPPCore/Public/Add-CIPPDbItem.ps1 b/Modules/CIPPCore/Public/Add-CIPPDbItem.ps1 index 913083ba513d..88b630bdf701 100644 --- a/Modules/CIPPCore/Public/Add-CIPPDbItem.ps1 +++ b/Modules/CIPPCore/Public/Add-CIPPDbItem.ps1 @@ -82,7 +82,7 @@ function Add-CIPPDbItem { } - Write-LogMessage -API 'CIPPDbItem' -tenant $TenantFilter -message "Added $($Data.Count) items of type $Type$(if ($Count) { ' (count mode)' })" -sev Info + Write-LogMessage -API 'CIPPDbItem' -tenant $TenantFilter -message "Added $($Data.Count) items of type $Type$(if ($Count) { ' (count mode)' })" -sev Debug } catch { Write-LogMessage -API 'CIPPDbItem' -tenant $TenantFilter -message "Failed to add items of type $Type : $($_.Exception.Message)" -sev Error diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheAdminConsentRequestPolicy.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheAdminConsentRequestPolicy.ps1 index e865512367d1..945d69f854cd 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheAdminConsentRequestPolicy.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheAdminConsentRequestPolicy.ps1 @@ -13,12 +13,12 @@ function Set-CIPPDBCacheAdminConsentRequestPolicy { ) try { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching admin consent request policy' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching admin consent request policy' -sev Debug $ConsentPolicy = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/policies/adminConsentRequestPolicy' -tenantid $TenantFilter Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'AdminConsentRequestPolicy' -Data @($ConsentPolicy) $ConsentPolicy = $null - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached admin consent request policy successfully' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached admin consent request policy successfully' -sev Debug } catch { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter ` diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheAppRoleAssignments.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheAppRoleAssignments.ps1 index f056e4ec470f..3ba077a2cdeb 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheAppRoleAssignments.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheAppRoleAssignments.ps1 @@ -13,7 +13,7 @@ function Set-CIPPDBCacheAppRoleAssignments { ) try { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching app role assignments' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching app role assignments' -sev Debug # Get all service principals first $ServicePrincipals = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/servicePrincipals?$select=id,appId,displayName&$top=999&expand=appRoleAssignments' -tenantid $TenantFilter @@ -37,7 +37,7 @@ function Set-CIPPDBCacheAppRoleAssignments { if ($AllAppRoleAssignments.Count -gt 0) { Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'AppRoleAssignments' -Data $AllAppRoleAssignments Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'AppRoleAssignments' -Data $AllAppRoleAssignments -Count - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($AllAppRoleAssignments.Count) app role assignments" -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($AllAppRoleAssignments.Count) app role assignments" -sev Debug } $AllAppRoleAssignments = $null diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheApps.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheApps.ps1 index 9844836372d6..22200d76dbf3 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheApps.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheApps.ps1 @@ -13,14 +13,14 @@ function Set-CIPPDBCacheApps { ) try { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching applications' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching applications' -sev Debug $Apps = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/applications?$top=999&expand=owners' -tenantid $TenantFilter Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Apps' -Data $Apps Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Apps' -Data $Apps -Count $Apps = $null - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached applications successfully' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached applications successfully' -sev Debug } catch { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter ` diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheAuthenticationFlowsPolicy.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheAuthenticationFlowsPolicy.ps1 index 587eaa8a587b..7b75a2b23eaa 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheAuthenticationFlowsPolicy.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheAuthenticationFlowsPolicy.ps1 @@ -13,13 +13,13 @@ function Set-CIPPDBCacheAuthenticationFlowsPolicy { ) try { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching authentication flows policy' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching authentication flows policy' -sev Debug $AuthFlowPolicy = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/policies/authenticationFlowsPolicy' -tenantid $TenantFilter -AsApp $true if ($AuthFlowPolicy) { Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'AuthenticationFlowsPolicy' -Data @($AuthFlowPolicy) - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached authentication flows policy successfully' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached authentication flows policy successfully' -sev Debug } } catch { diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheAuthenticationMethodsPolicy.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheAuthenticationMethodsPolicy.ps1 index 2700a8e2c3c2..98ea20dd05d7 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheAuthenticationMethodsPolicy.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheAuthenticationMethodsPolicy.ps1 @@ -13,12 +13,12 @@ function Set-CIPPDBCacheAuthenticationMethodsPolicy { ) try { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching authentication methods policy' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching authentication methods policy' -sev Debug $AuthMethodsPolicy = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/policies/authenticationMethodsPolicy' -tenantid $TenantFilter Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'AuthenticationMethodsPolicy' -Data @($AuthMethodsPolicy) $AuthMethodsPolicy = $null - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached authentication methods policy successfully' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached authentication methods policy successfully' -sev Debug } catch { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache authentication methods policy: $($_.Exception.Message)" -sev Error diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheAuthorizationPolicy.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheAuthorizationPolicy.ps1 index 7167e39f66a8..ca6c92bfe624 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheAuthorizationPolicy.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheAuthorizationPolicy.ps1 @@ -13,12 +13,12 @@ function Set-CIPPDBCacheAuthorizationPolicy { ) try { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching authorization policy' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching authorization policy' -sev Debug $AuthPolicy = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/policies/authorizationPolicy' -tenantid $TenantFilter Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'AuthorizationPolicy' -Data @($AuthPolicy) $AuthPolicy = $null - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached authorization policy successfully' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached authorization policy successfully' -sev Debug } catch { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache authorization policy: $($_.Exception.Message)" -sev Error diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheB2BManagementPolicy.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheB2BManagementPolicy.ps1 index 168866b475c2..f00d7d4c8fc0 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheB2BManagementPolicy.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheB2BManagementPolicy.ps1 @@ -13,16 +13,16 @@ function Set-CIPPDBCacheB2BManagementPolicy { ) try { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching B2B management policy' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching B2B management policy' -sev Debug $LegacyPolicies = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/policies/b2bManagementPolicies' -tenantid $TenantFilter $B2BManagementPolicy = $LegacyPolicies if ($B2BManagementPolicy) { Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'B2BManagementPolicy' -Data @($B2BManagementPolicy) - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached B2B management policy successfully' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached B2B management policy successfully' -sev Debug } else { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'No B2B management policy found' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'No B2B management policy found' -sev Debug } } catch { diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheConditionalAccessPolicies.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheConditionalAccessPolicies.ps1 index 81a21f5bbe9f..729644ed5d40 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheConditionalAccessPolicies.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheConditionalAccessPolicies.ps1 @@ -16,18 +16,18 @@ function Set-CIPPDBCacheConditionalAccessPolicies { $TestResult = Test-CIPPStandardLicense -StandardName 'ConditionalAccessCache' -TenantFilter $TenantFilter -RequiredCapabilities @('AAD_PREMIUM', 'AAD_PREMIUM_P2') -SkipLog if ($TestResult -eq $false) { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Tenant does not have Azure AD Premium license, skipping CA' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Tenant does not have Azure AD Premium license, skipping CA' -sev Debug return } - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching Conditional Access policies' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching Conditional Access policies' -sev Debug try { $CAPolicies = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/identity/conditionalAccess/policies?$top=999' -tenantid $TenantFilter if ($CAPolicies) { Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ConditionalAccessPolicies' -Data $CAPolicies Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ConditionalAccessPolicies' -Data $CAPolicies -Count - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($CAPolicies.Count) CA policies" -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($CAPolicies.Count) CA policies" -sev Debug } $CAPolicies = $null } catch { @@ -40,7 +40,7 @@ function Set-CIPPDBCacheConditionalAccessPolicies { if ($NamedLocations) { Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'NamedLocations' -Data $NamedLocations Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'NamedLocations' -Data $NamedLocations -Count - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($NamedLocations.Count) named locations" -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($NamedLocations.Count) named locations" -sev Debug } $NamedLocations = $null } catch { @@ -53,14 +53,14 @@ function Set-CIPPDBCacheConditionalAccessPolicies { if ($AuthStrengths) { Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'AuthenticationStrengths' -Data $AuthStrengths Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'AuthenticationStrengths' -Data $AuthStrengths -Count - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($AuthStrengths.Count) authentication strengths" -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($AuthStrengths.Count) authentication strengths" -sev Debug } $AuthStrengths = $null } catch { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache authentication strengths: $($_.Exception.Message)" -sev Warning } - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached CA data successfully' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached CA data successfully' -sev Debug } catch { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache Conditional Access data: $($_.Exception.Message)" -sev Error diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheCredentialUserRegistrationDetails.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheCredentialUserRegistrationDetails.ps1 index 861d0c25a50c..888c398ffea3 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheCredentialUserRegistrationDetails.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheCredentialUserRegistrationDetails.ps1 @@ -13,14 +13,14 @@ function Set-CIPPDBCacheCredentialUserRegistrationDetails { ) try { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching credential user registration details' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching credential user registration details' -sev Debug $CredentialUserRegistrationDetails = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/reports/credentialUserRegistrationDetails' -tenantid $TenantFilter if ($CredentialUserRegistrationDetails) { Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'CredentialUserRegistrationDetails' -Data $CredentialUserRegistrationDetails Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'CredentialUserRegistrationDetails' -Data $CredentialUserRegistrationDetails -Count - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($CredentialUserRegistrationDetails.Count) credential user registration details" -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($CredentialUserRegistrationDetails.Count) credential user registration details" -sev Debug } $CredentialUserRegistrationDetails = $null diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheCrossTenantAccessPolicy.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheCrossTenantAccessPolicy.ps1 index e9e66753dcd3..cc4203420b96 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheCrossTenantAccessPolicy.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheCrossTenantAccessPolicy.ps1 @@ -13,11 +13,11 @@ function Set-CIPPDBCacheCrossTenantAccessPolicy { ) try { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching cross-tenant access policy' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching cross-tenant access policy' -sev Debug $CrossTenantPolicy = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/policies/crossTenantAccessPolicy/default' -tenantid $TenantFilter Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'CrossTenantAccessPolicy' -Data @($CrossTenantPolicy) $CrossTenantPolicy = $null - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached cross-tenant access policy successfully' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached cross-tenant access policy successfully' -sev Debug } catch { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter ` diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheDefaultAppManagementPolicy.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheDefaultAppManagementPolicy.ps1 index 556094e09ada..c053f36435a4 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheDefaultAppManagementPolicy.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheDefaultAppManagementPolicy.ps1 @@ -13,11 +13,11 @@ function Set-CIPPDBCacheDefaultAppManagementPolicy { ) try { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching default app management policy' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching default app management policy' -sev Debug $AppMgmtPolicy = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/policies/defaultAppManagementPolicy' -tenantid $TenantFilter Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'DefaultAppManagementPolicy' -Data @($AppMgmtPolicy) $AppMgmtPolicy = $null - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached default app management policy successfully' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached default app management policy successfully' -sev Debug } catch { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter ` diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheDeviceRegistrationPolicy.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheDeviceRegistrationPolicy.ps1 index ec2da92abc21..3a9eada7c7a6 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheDeviceRegistrationPolicy.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheDeviceRegistrationPolicy.ps1 @@ -13,13 +13,13 @@ function Set-CIPPDBCacheDeviceRegistrationPolicy { ) try { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching device registration policy' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching device registration policy' -sev Debug $DeviceRegistrationPolicy = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/policies/deviceRegistrationPolicy' -tenantid $TenantFilter if ($DeviceRegistrationPolicy) { Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'DeviceRegistrationPolicy' -Data @($DeviceRegistrationPolicy) - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached device registration policy successfully' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached device registration policy successfully' -sev Debug } } catch { diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheDeviceSettings.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheDeviceSettings.ps1 index aa2c1f6b9d01..7845f72ffa8a 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheDeviceSettings.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheDeviceSettings.ps1 @@ -13,13 +13,13 @@ function Set-CIPPDBCacheDeviceSettings { ) try { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching device settings' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching device settings' -sev Debug $DeviceSettings = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/directory/deviceLocalCredentials' -tenantid $TenantFilter Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'DeviceSettings' -Data @($DeviceSettings) $DeviceSettings = $null - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached device settings successfully' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached device settings successfully' -sev Debug } catch { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter ` diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheDevices.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheDevices.ps1 index 3bc9b96a0ec7..2953483d47a7 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheDevices.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheDevices.ps1 @@ -13,14 +13,14 @@ function Set-CIPPDBCacheDevices { ) try { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching Azure AD devices' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching Azure AD devices' -sev Debug $Devices = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/devices?$top=999&$select=id,displayName,operatingSystem,operatingSystemVersion,trustType,accountEnabled,approximateLastSignInDateTime' -tenantid $TenantFilter Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Devices' -Data $Devices Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Devices' -Data $Devices -Count $Devices = $null - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached Azure AD devices successfully' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached Azure AD devices successfully' -sev Debug } catch { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache Azure AD devices: $($_.Exception.Message)" -sev Error diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheDirectoryRecommendations.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheDirectoryRecommendations.ps1 index dcd28623b7e9..616c7ab82503 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheDirectoryRecommendations.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheDirectoryRecommendations.ps1 @@ -13,14 +13,14 @@ function Set-CIPPDBCacheDirectoryRecommendations { ) try { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching directory recommendations' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching directory recommendations' -sev Debug $Recommendations = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/directory/recommendations?$top=999' -tenantid $TenantFilter Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'DirectoryRecommendations' -Data $Recommendations Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'DirectoryRecommendations' -Data $Recommendations -Count $Recommendations = $null - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached directory recommendations successfully' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached directory recommendations successfully' -sev Debug } catch { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache directory recommendations: $($_.Exception.Message)" -sev Error diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheDomains.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheDomains.ps1 index a4943be1759d..b546382b3103 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheDomains.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheDomains.ps1 @@ -13,12 +13,12 @@ function Set-CIPPDBCacheDomains { ) try { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching domains' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching domains' -sev Debug $Domains = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/domains' -tenantid $TenantFilter Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Domains' -Data @($Domains) $Domains = $null - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached domains successfully' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached domains successfully' -sev Debug } catch { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache domains: $($_.Exception.Message)" -sev Error diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoAcceptedDomains.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoAcceptedDomains.ps1 index 31e57744c336..cbe7bd854481 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoAcceptedDomains.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoAcceptedDomains.ps1 @@ -13,14 +13,14 @@ function Set-CIPPDBCacheExoAcceptedDomains { ) try { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching Exchange Accepted Domains' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching Exchange Accepted Domains' -sev Debug $AcceptedDomains = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-AcceptedDomain' if ($AcceptedDomains) { Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoAcceptedDomains' -Data $AcceptedDomains Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoAcceptedDomains' -Data $AcceptedDomains -Count - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($AcceptedDomains.Count) Accepted Domains" -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($AcceptedDomains.Count) Accepted Domains" -sev Debug } $AcceptedDomains = $null diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoAdminAuditLogConfig.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoAdminAuditLogConfig.ps1 index 9ee38f5e1185..892e471647fe 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoAdminAuditLogConfig.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoAdminAuditLogConfig.ps1 @@ -13,7 +13,7 @@ function Set-CIPPDBCacheExoAdminAuditLogConfig { ) try { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching Exchange Admin Audit Log configuration' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching Exchange Admin Audit Log configuration' -sev Debug $AuditConfig = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-AdminAuditLogConfig' @@ -22,7 +22,7 @@ function Set-CIPPDBCacheExoAdminAuditLogConfig { $AuditConfigArray = @($AuditConfig) Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoAdminAuditLogConfig' -Data $AuditConfigArray Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoAdminAuditLogConfig' -Data $AuditConfigArray -Count - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached Exchange Admin Audit Log configuration' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached Exchange Admin Audit Log configuration' -sev Debug } $AuditConfig = $null diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoAntiPhishPolicies.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoAntiPhishPolicies.ps1 index cfd709c4b04b..98a525d1f0a0 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoAntiPhishPolicies.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoAntiPhishPolicies.ps1 @@ -13,14 +13,14 @@ function Set-CIPPDBCacheExoAntiPhishPolicies { ) try { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching Exchange Anti-Phishing policies and rules' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching Exchange Anti-Phishing policies and rules' -sev Debug # Get Anti-Phishing policies $AntiPhishPolicies = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-AntiPhishPolicy' if ($AntiPhishPolicies) { Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoAntiPhishPolicies' -Data $AntiPhishPolicies Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoAntiPhishPolicies' -Data $AntiPhishPolicies -Count - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($AntiPhishPolicies.Count) Anti-Phishing policies" -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($AntiPhishPolicies.Count) Anti-Phishing policies" -sev Debug } $AntiPhishPolicies = $null @@ -29,7 +29,7 @@ function Set-CIPPDBCacheExoAntiPhishPolicies { if ($AntiPhishRules) { Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoAntiPhishRules' -Data $AntiPhishRules Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoAntiPhishRules' -Data $AntiPhishRules -Count - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($AntiPhishRules.Count) Anti-Phishing rules" -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($AntiPhishRules.Count) Anti-Phishing rules" -sev Debug } $AntiPhishRules = $null diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoAntiPhishPolicy.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoAntiPhishPolicy.ps1 index ba0a6603efff..b65b084e9ae1 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoAntiPhishPolicy.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoAntiPhishPolicy.ps1 @@ -13,13 +13,13 @@ function Set-CIPPDBCacheExoAntiPhishPolicy { ) try { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching Exchange Anti-Phish policies (detailed)' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching Exchange Anti-Phish policies (detailed)' -sev Debug $AntiPhishPolicies = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-AntiPhishPolicy' if ($AntiPhishPolicies) { Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoAntiPhishPolicy' -Data $AntiPhishPolicies Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoAntiPhishPolicy' -Data $AntiPhishPolicies -Count - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($AntiPhishPolicies.Count) Anti-Phish policies (detailed)" -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($AntiPhishPolicies.Count) Anti-Phish policies (detailed)" -sev Debug } $AntiPhishPolicies = $null diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoAtpPolicyForO365.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoAtpPolicyForO365.ps1 index 96912b26c3ae..ee0b2203fa0b 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoAtpPolicyForO365.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoAtpPolicyForO365.ps1 @@ -13,13 +13,13 @@ function Set-CIPPDBCacheExoAtpPolicyForO365 { ) try { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching Exchange ATP policies for Office 365' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching Exchange ATP policies for Office 365' -sev Debug $AtpPolicies = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-AtpPolicyForO365' if ($AtpPolicies) { Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoAtpPolicyForO365' -Data $AtpPolicies Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoAtpPolicyForO365' -Data $AtpPolicies -Count - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($AtpPolicies.Count) ATP policies for Office 365" -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($AtpPolicies.Count) ATP policies for Office 365" -sev Debug } $AtpPolicies = $null diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoDkimSigningConfig.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoDkimSigningConfig.ps1 index 161d582f3fe5..fb72c35dec68 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoDkimSigningConfig.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoDkimSigningConfig.ps1 @@ -13,14 +13,14 @@ function Set-CIPPDBCacheExoDkimSigningConfig { ) try { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching Exchange DKIM signing configuration' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching Exchange DKIM signing configuration' -sev Debug $DkimConfig = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-DkimSigningConfig' if ($DkimConfig) { Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoDkimSigningConfig' -Data $DkimConfig Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoDkimSigningConfig' -Data $DkimConfig -Count - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($DkimConfig.Count) DKIM configurations" -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($DkimConfig.Count) DKIM configurations" -sev Debug } $DkimConfig = $null diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoHostedContentFilterPolicy.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoHostedContentFilterPolicy.ps1 index c8fc088bf333..b6bb5fd5c571 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoHostedContentFilterPolicy.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoHostedContentFilterPolicy.ps1 @@ -13,12 +13,12 @@ function Set-CIPPDBCacheExoHostedContentFilterPolicy { ) try { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching Exchange Hosted Content Filter policies' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching Exchange Hosted Content Filter policies' -sev Debug $HostedContentFilterPolicies = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-HostedContentFilterPolicy' if ($HostedContentFilterPolicies) { Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoHostedContentFilterPolicy' -Data $HostedContentFilterPolicies Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoHostedContentFilterPolicy' -Data $HostedContentFilterPolicies -Count - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($HostedContentFilterPolicies.Count) Hosted Content Filter policies" -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($HostedContentFilterPolicies.Count) Hosted Content Filter policies" -sev Debug } $HostedContentFilterPolicies = $null diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoHostedOutboundSpamFilterPolicy.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoHostedOutboundSpamFilterPolicy.ps1 index 1b9f0319e578..06201cd38b63 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoHostedOutboundSpamFilterPolicy.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoHostedOutboundSpamFilterPolicy.ps1 @@ -13,13 +13,13 @@ function Set-CIPPDBCacheExoHostedOutboundSpamFilterPolicy { ) try { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching Exchange Hosted Outbound Spam Filter policies' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching Exchange Hosted Outbound Spam Filter policies' -sev Debug $HostedOutboundSpamFilterPolicies = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-HostedOutboundSpamFilterPolicy' if ($HostedOutboundSpamFilterPolicies) { Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoHostedOutboundSpamFilterPolicy' -Data $HostedOutboundSpamFilterPolicies Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoHostedOutboundSpamFilterPolicy' -Data $HostedOutboundSpamFilterPolicies -Count - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($HostedOutboundSpamFilterPolicies.Count) Hosted Outbound Spam Filter policies" -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($HostedOutboundSpamFilterPolicies.Count) Hosted Outbound Spam Filter policies" -sev Debug } $HostedOutboundSpamFilterPolicies = $null diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoMalwareFilterPolicies.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoMalwareFilterPolicies.ps1 index 025401125b02..f50d64d610b8 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoMalwareFilterPolicies.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoMalwareFilterPolicies.ps1 @@ -13,14 +13,14 @@ function Set-CIPPDBCacheExoMalwareFilterPolicies { ) try { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching Exchange Malware Filter policies and rules' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching Exchange Malware Filter policies and rules' -sev Debug # Get Malware Filter policies $MalwarePolicies = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-MalwareFilterPolicy' if ($MalwarePolicies) { Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoMalwareFilterPolicies' -Data $MalwarePolicies Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoMalwareFilterPolicies' -Data $MalwarePolicies -Count - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($MalwarePolicies.Count) Malware Filter policies" -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($MalwarePolicies.Count) Malware Filter policies" -sev Debug } $MalwarePolicies = $null @@ -29,7 +29,7 @@ function Set-CIPPDBCacheExoMalwareFilterPolicies { if ($MalwareRules) { Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoMalwareFilterRules' -Data $MalwareRules Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoMalwareFilterRules' -Data $MalwareRules -Count - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($MalwareRules.Count) Malware Filter rules" -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($MalwareRules.Count) Malware Filter rules" -sev Debug } $MalwareRules = $null diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoMalwareFilterPolicy.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoMalwareFilterPolicy.ps1 index 3103dbebf29c..194501e09e9a 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoMalwareFilterPolicy.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoMalwareFilterPolicy.ps1 @@ -13,13 +13,13 @@ function Set-CIPPDBCacheExoMalwareFilterPolicy { ) try { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching Exchange Malware Filter policies (detailed)' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching Exchange Malware Filter policies (detailed)' -sev Debug $MalwareFilterPolicies = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-MalwareFilterPolicy' if ($MalwareFilterPolicies) { Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoMalwareFilterPolicy' -Data $MalwareFilterPolicies Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoMalwareFilterPolicy' -Data $MalwareFilterPolicies -Count - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($MalwareFilterPolicies.Count) Malware Filter policies (detailed)" -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($MalwareFilterPolicies.Count) Malware Filter policies (detailed)" -sev Debug } $MalwareFilterPolicies = $null diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoOrganizationConfig.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoOrganizationConfig.ps1 index d0733cac780a..6138bd21a3e3 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoOrganizationConfig.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoOrganizationConfig.ps1 @@ -13,7 +13,7 @@ function Set-CIPPDBCacheExoOrganizationConfig { ) try { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching Exchange Organization configuration' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching Exchange Organization configuration' -sev Debug $OrgConfig = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-OrganizationConfig' @@ -22,7 +22,7 @@ function Set-CIPPDBCacheExoOrganizationConfig { $OrgConfigArray = @($OrgConfig) Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoOrganizationConfig' -Data $OrgConfigArray Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoOrganizationConfig' -Data $OrgConfigArray -Count - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached Exchange Organization configuration' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached Exchange Organization configuration' -sev Debug } $OrgConfig = $null diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoPresetSecurityPolicy.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoPresetSecurityPolicy.ps1 index 9d227da4122e..a10092fba9ce 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoPresetSecurityPolicy.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoPresetSecurityPolicy.ps1 @@ -13,7 +13,7 @@ function Set-CIPPDBCacheExoPresetSecurityPolicy { ) try { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching Exchange Preset Security Policies' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching Exchange Preset Security Policies' -sev Debug $EOPRules = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-EOPProtectionPolicyRule' $ATPRules = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-ATPProtectionPolicyRule' @@ -30,7 +30,7 @@ function Set-CIPPDBCacheExoPresetSecurityPolicy { if ($AllRules.Count -gt 0) { Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoPresetSecurityPolicy' -Data $AllRules Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoPresetSecurityPolicy' -Data $AllRules -Count - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($AllRules.Count) Preset Security Policy rules" -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($AllRules.Count) Preset Security Policy rules" -sev Debug } $EOPRules = $null $ATPRules = $null diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoQuarantinePolicy.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoQuarantinePolicy.ps1 index 894d03f3b1b4..2ef8bf63639a 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoQuarantinePolicy.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoQuarantinePolicy.ps1 @@ -13,13 +13,13 @@ function Set-CIPPDBCacheExoQuarantinePolicy { ) try { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching Exchange Quarantine policies' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching Exchange Quarantine policies' -sev Debug $QuarantinePolicies = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-QuarantinePolicy' if ($QuarantinePolicies) { Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoQuarantinePolicy' -Data $QuarantinePolicies Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoQuarantinePolicy' -Data $QuarantinePolicies -Count - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($QuarantinePolicies.Count) Quarantine policies" -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($QuarantinePolicies.Count) Quarantine policies" -sev Debug } $QuarantinePolicies = $null diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoRemoteDomain.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoRemoteDomain.ps1 index d22c8fd0ccf1..692ba803c6de 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoRemoteDomain.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoRemoteDomain.ps1 @@ -13,13 +13,13 @@ function Set-CIPPDBCacheExoRemoteDomain { ) try { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching Exchange Remote Domains' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching Exchange Remote Domains' -sev Debug $RemoteDomains = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-RemoteDomain' if ($RemoteDomains) { Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoRemoteDomain' -Data $RemoteDomains Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoRemoteDomain' -Data $RemoteDomains -Count - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($RemoteDomains.Count) Remote Domains" -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($RemoteDomains.Count) Remote Domains" -sev Debug } $RemoteDomains = $null diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoSafeAttachmentPolicies.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoSafeAttachmentPolicies.ps1 index e49576235842..172e86887e66 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoSafeAttachmentPolicies.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoSafeAttachmentPolicies.ps1 @@ -13,14 +13,14 @@ function Set-CIPPDBCacheExoSafeAttachmentPolicies { ) try { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching Exchange Safe Attachment policies and rules' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching Exchange Safe Attachment policies and rules' -sev Debug # Get Safe Attachment policies $SafeAttachmentPolicies = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-SafeAttachmentPolicy' if ($SafeAttachmentPolicies) { Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoSafeAttachmentPolicies' -Data $SafeAttachmentPolicies Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoSafeAttachmentPolicies' -Data $SafeAttachmentPolicies -Count - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($SafeAttachmentPolicies.Count) Safe Attachment policies" -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($SafeAttachmentPolicies.Count) Safe Attachment policies" -sev Debug } $SafeAttachmentPolicies = $null @@ -29,7 +29,7 @@ function Set-CIPPDBCacheExoSafeAttachmentPolicies { if ($SafeAttachmentRules) { Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoSafeAttachmentRules' -Data $SafeAttachmentRules Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoSafeAttachmentRules' -Data $SafeAttachmentRules -Count - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($SafeAttachmentRules.Count) Safe Attachment rules" -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($SafeAttachmentRules.Count) Safe Attachment rules" -sev Debug } $SafeAttachmentRules = $null diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoSafeAttachmentPolicy.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoSafeAttachmentPolicy.ps1 index c6869a72064a..6ebe371e5291 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoSafeAttachmentPolicy.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoSafeAttachmentPolicy.ps1 @@ -13,13 +13,13 @@ function Set-CIPPDBCacheExoSafeAttachmentPolicy { ) try { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching Exchange Safe Attachment policies (detailed)' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching Exchange Safe Attachment policies (detailed)' -sev Debug $SafeAttachmentPolicies = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-SafeAttachmentPolicy' if ($SafeAttachmentPolicies) { Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoSafeAttachmentPolicy' -Data $SafeAttachmentPolicies Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoSafeAttachmentPolicy' -Data $SafeAttachmentPolicies -Count - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($SafeAttachmentPolicies.Count) Safe Attachment policies (detailed)" -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($SafeAttachmentPolicies.Count) Safe Attachment policies (detailed)" -sev Debug } $SafeAttachmentPolicies = $null diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoSafeLinksPolicies.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoSafeLinksPolicies.ps1 index 33276c8fb993..c06fb2f08971 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoSafeLinksPolicies.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoSafeLinksPolicies.ps1 @@ -13,14 +13,14 @@ function Set-CIPPDBCacheExoSafeLinksPolicies { ) try { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching Exchange Safe Links policies and rules' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching Exchange Safe Links policies and rules' -sev Debug # Get Safe Links policies $SafeLinksPolicies = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-SafeLinksPolicy' if ($SafeLinksPolicies) { Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoSafeLinksPolicies' -Data $SafeLinksPolicies Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoSafeLinksPolicies' -Data $SafeLinksPolicies -Count - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($SafeLinksPolicies.Count) Safe Links policies" -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($SafeLinksPolicies.Count) Safe Links policies" -sev Debug } $SafeLinksPolicies = $null @@ -29,7 +29,7 @@ function Set-CIPPDBCacheExoSafeLinksPolicies { if ($SafeLinksRules) { Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoSafeLinksRules' -Data $SafeLinksRules Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoSafeLinksRules' -Data $SafeLinksRules -Count - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($SafeLinksRules.Count) Safe Links rules" -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($SafeLinksRules.Count) Safe Links rules" -sev Debug } $SafeLinksRules = $null diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoSafeLinksPolicy.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoSafeLinksPolicy.ps1 index 176e3c76f168..a3252b245bcd 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoSafeLinksPolicy.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoSafeLinksPolicy.ps1 @@ -13,13 +13,13 @@ function Set-CIPPDBCacheExoSafeLinksPolicy { ) try { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching Exchange Safe Links policies (detailed)' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching Exchange Safe Links policies (detailed)' -sev Debug $SafeLinksPolicies = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-SafeLinksPolicy' if ($SafeLinksPolicies) { Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoSafeLinksPolicy' -Data $SafeLinksPolicies Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoSafeLinksPolicy' -Data $SafeLinksPolicies -Count - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($SafeLinksPolicies.Count) Safe Links policies (detailed)" -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($SafeLinksPolicies.Count) Safe Links policies (detailed)" -sev Debug } $SafeLinksPolicies = $null diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoSharingPolicy.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoSharingPolicy.ps1 index 4b59088adfa0..bd4c28da68f5 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoSharingPolicy.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoSharingPolicy.ps1 @@ -13,14 +13,14 @@ function Set-CIPPDBCacheExoSharingPolicy { ) try { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching Exchange Sharing Policies' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching Exchange Sharing Policies' -sev Debug $SharingPolicies = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-SharingPolicy' if ($SharingPolicies) { Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoSharingPolicy' -Data $SharingPolicies Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoSharingPolicy' -Data $SharingPolicies -Count - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($SharingPolicies.Count) Sharing Policies" -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($SharingPolicies.Count) Sharing Policies" -sev Debug } $SharingPolicies = $null diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoTenantAllowBlockList.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoTenantAllowBlockList.ps1 index fe6c1ee1f9b4..62b4385a5c8a 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoTenantAllowBlockList.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoTenantAllowBlockList.ps1 @@ -13,7 +13,7 @@ function Set-CIPPDBCacheExoTenantAllowBlockList { ) try { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching Tenant Allow/Block List items' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching Tenant Allow/Block List items' -sev Debug $SenderItems = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-TenantAllowBlockListItems' -cmdParams @{ListType = 'Sender' } $UrlItems = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-TenantAllowBlockListItems' -cmdParams @{ListType = 'Url' } @@ -34,12 +34,12 @@ function Set-CIPPDBCacheExoTenantAllowBlockList { if ($AllItems.Count -gt 0) { Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoTenantAllowBlockList' -Data $AllItems Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoTenantAllowBlockList' -Data $AllItems -Count - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($AllItems.Count) Tenant Allow/Block List items" -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($AllItems.Count) Tenant Allow/Block List items" -sev Debug } else { # Even if empty, store an empty array so test knows cache was populated Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoTenantAllowBlockList' -Data @() Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoTenantAllowBlockList' -Data @() -Count - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached empty Tenant Allow/Block List' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached empty Tenant Allow/Block List' -sev Debug } $SenderItems = $null $UrlItems = $null diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoTransportRules.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoTransportRules.ps1 index 5328e63b99af..6f83273af842 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoTransportRules.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoTransportRules.ps1 @@ -13,14 +13,14 @@ function Set-CIPPDBCacheExoTransportRules { ) try { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching Exchange Transport Rules' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching Exchange Transport Rules' -sev Debug $TransportRules = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-TransportRule' if ($TransportRules) { Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoTransportRules' -Data $TransportRules Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ExoTransportRules' -Data $TransportRules -Count - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($TransportRules.Count) Transport Rules" -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($TransportRules.Count) Transport Rules" -sev Debug } $TransportRules = $null diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheGroups.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheGroups.ps1 index 2725887c2fa9..d3be98c3f17e 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheGroups.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheGroups.ps1 @@ -13,7 +13,7 @@ function Set-CIPPDBCacheGroups { ) try { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching groups' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching groups' -sev Debug $Groups = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/groups?$top=999&$select=id,displayName,groupTypes,mail,mailEnabled,securityEnabled,membershipRule,onPremisesSyncEnabled' -tenantid $TenantFilter @@ -29,7 +29,7 @@ function Set-CIPPDBCacheGroups { } if ($MemberRequests) { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Fetching group members' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Fetching group members' -sev Debug $MemberResults = New-GraphBulkRequest -Requests @($MemberRequests) -tenantid $TenantFilter # Add members to each group object @@ -49,7 +49,7 @@ function Set-CIPPDBCacheGroups { $Groups = $null } - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached groups with members successfully' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached groups with members successfully' -sev Debug } catch { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter ` diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheGuests.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheGuests.ps1 index 96a1b5f02f52..0fc9a324ef6a 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheGuests.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheGuests.ps1 @@ -13,14 +13,14 @@ function Set-CIPPDBCacheGuests { ) try { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching guest users' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching guest users' -sev Debug $Guests = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/users?`$filter=userType eq 'Guest'&`$expand=sponsors&`$top=999" -tenantid $TenantFilter Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Guests' -Data $Guests Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Guests' -Data $Guests -Count $Guests = $null - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached guest users successfully' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached guest users successfully' -sev Debug } catch { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache guest users: $($_.Exception.Message)" -sev Error diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheIntuneAppProtectionPolicies.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheIntuneAppProtectionPolicies.ps1 index 3330ad5f8255..1906b3bcdfda 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheIntuneAppProtectionPolicies.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheIntuneAppProtectionPolicies.ps1 @@ -13,14 +13,14 @@ function Set-CIPPDBCacheIntuneAppProtectionPolicies { ) try { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching Intune App Protection Policies' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching Intune App Protection Policies' -sev Debug # iOS Managed App Protection Policies $IosPolicies = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/deviceAppManagement/iosManagedAppProtections?$expand=assignments' -tenantid $TenantFilter if ($IosPolicies) { Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'IntuneIosAppProtectionPolicies' -Data $IosPolicies Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'IntuneIosAppProtectionPolicies' -Data $IosPolicies -Count - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($IosPolicies.Count) iOS app protection policies" -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($IosPolicies.Count) iOS app protection policies" -sev Debug } $IosPolicies = $null @@ -29,7 +29,7 @@ function Set-CIPPDBCacheIntuneAppProtectionPolicies { if ($AndroidPolicies) { Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'IntuneAndroidAppProtectionPolicies' -Data $AndroidPolicies Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'IntuneAndroidAppProtectionPolicies' -Data $AndroidPolicies -Count - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($AndroidPolicies.Count) Android app protection policies" -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($AndroidPolicies.Count) Android app protection policies" -sev Debug } $AndroidPolicies = $null diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheIntunePolicies.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheIntunePolicies.ps1 index 77aebe9bfa1d..51c2642f1397 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheIntunePolicies.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheIntunePolicies.ps1 @@ -16,11 +16,11 @@ function Set-CIPPDBCacheIntunePolicies { $TestResult = Test-CIPPStandardLicense -StandardName 'IntunePoliciesCache' -TenantFilter $TenantFilter -RequiredCapabilities @('INTUNE_A', 'MDM_Services', 'EMS', 'SCCM', 'MICROSOFTINTUNEPLAN1') -SkipLog if ($TestResult -eq $false) { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Tenant does not have Intune license, skipping' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Tenant does not have Intune license, skipping' -sev Debug return } - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching Intune policies' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching Intune policies' -sev Debug $PolicyTypes = @( @{ Type = 'DeviceCompliancePolicies'; Uri = '/deviceManagement/deviceCompliancePolicies?$top=999&$expand=assignments'; FetchDeviceStatuses = $true } @@ -36,7 +36,7 @@ function Set-CIPPDBCacheIntunePolicies { ) # Build bulk requests for all policy types - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Fetching all policy types using bulk request' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Fetching all policy types using bulk request' -sev Debug $PolicyRequests = foreach ($PolicyType in $PolicyTypes) { [PSCustomObject]@{ id = $PolicyType.Type @@ -98,11 +98,11 @@ function Set-CIPPDBCacheIntunePolicies { Add-CIPPDbItem -TenantFilter $TenantFilter -Type "Intune$($PolicyType.Type)" -Data $Policies Add-CIPPDbItem -TenantFilter $TenantFilter -Type "Intune$($PolicyType.Type)" -Data $Policies -Count - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($Policies.Count) $($PolicyType.Type)" -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($Policies.Count) $($PolicyType.Type)" -sev Debug # Fetch device statuses for compliance policies using bulk requests if ($PolicyType.FetchDeviceStatuses -and ($Policies | Measure-Object).Count -gt 0) { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Fetching device statuses for $($Policies.Count) compliance policies using bulk request" -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Fetching device statuses for $($Policies.Count) compliance policies using bulk request" -sev Debug $BaseUri = ($PolicyType.Uri -split '\?')[0] # Build bulk request array @@ -140,7 +140,7 @@ function Set-CIPPDBCacheIntunePolicies { } } - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached Intune policies successfully' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached Intune policies successfully' -sev Debug } catch { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache Intune policies: $($_.Exception.Message)" -sev Error diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheLicenseOverview.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheLicenseOverview.ps1 index 5556ac3d307c..5ba1ef461f0b 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheLicenseOverview.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheLicenseOverview.ps1 @@ -13,13 +13,13 @@ function Set-CIPPDBCacheLicenseOverview { ) try { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching license overview' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching license overview' -sev Debug $LicenseOverview = Get-CIPPLicenseOverview -TenantFilter $TenantFilter Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'LicenseOverview' -Data @($LicenseOverview) $LicenseOverview = $null - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached license overview successfully' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached license overview successfully' -sev Debug } catch { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache license overview: $($_.Exception.Message)" -sev Error diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheMFAState.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheMFAState.ps1 index 2f2da95b6e9f..1a430f382a6a 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheMFAState.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheMFAState.ps1 @@ -13,13 +13,13 @@ function Set-CIPPDBCacheMFAState { ) try { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching MFA state' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching MFA state' -sev Debug $MFAState = Get-CIPPMFAState -TenantFilter $TenantFilter Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'MFAState' -Data @($MFAState) Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'MFAState' -Data @($MFAState) -Count - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($MFAState.Count) MFA state records successfully" -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($MFAState.Count) MFA state records successfully" -sev Debug } catch { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache MFA state: $($_.Exception.Message)" -sev Error diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheMailboxUsage.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheMailboxUsage.ps1 index fdfaef374765..0b8e91fbc176 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheMailboxUsage.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheMailboxUsage.ps1 @@ -13,13 +13,13 @@ function Set-CIPPDBCacheMailboxUsage { ) try { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching mailbox usage' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching mailbox usage' -sev Debug $MailboxUsage = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/reports/getMailboxUsageDetail(period='D7')?`$format=application%2fjson" -tenantid $TenantFilter Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'MailboxUsage' -Data $MailboxUsage Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'MailboxUsage' -Data $MailboxUsage -Count $MailboxUsage = $null - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached mailbox usage successfully' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached mailbox usage successfully' -sev Debug } catch { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache mailbox usage: $($_.Exception.Message)" -sev Error diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheMailboxes.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheMailboxes.ps1 index 41438fec3fd6..071c910c7677 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheMailboxes.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheMailboxes.ps1 @@ -13,7 +13,7 @@ function Set-CIPPDBCacheMailboxes { ) try { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching mailboxes' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching mailboxes' -sev Debug # Get mailboxes with select properties $Select = 'id,ExchangeGuid,ArchiveGuid,UserPrincipalName,DisplayName,PrimarySMTPAddress,RecipientType,RecipientTypeDetails,EmailAddresses,WhenSoftDeleted,IsInactiveMailbox,ForwardingSmtpAddress,DeliverToMailboxAndForward,ForwardingAddress,HiddenFromAddressListsEnabled,ExternalDirectoryObjectId,MessageCopyForSendOnBehalfEnabled,MessageCopyForSentAsEnabled' @@ -40,20 +40,20 @@ function Set-CIPPDBCacheMailboxes { Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Mailboxes' -Data $Mailboxes Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Mailboxes' -Data $Mailboxes -Count - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($Mailboxes.Count) mailboxes successfully" -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($Mailboxes.Count) mailboxes successfully" -sev Debug # Get CAS mailboxes - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching CAS mailboxes' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching CAS mailboxes' -sev Debug $CASMailboxes = New-GraphGetRequest -uri "https://outlook.office365.com/adminapi/beta/$($TenantFilter)/CasMailbox" -Tenantid $TenantFilter -scope 'ExchangeOnline' -noPagination $true Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'CASMailbox' -Data $CASMailboxes Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'CASMailbox' -Data $CASMailboxes -Count $CASMailboxes = $null - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached CAS mailboxes successfully' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached CAS mailboxes successfully' -sev Debug # Start orchestrator to cache mailbox permissions in batches $MailboxCount = ($Mailboxes | Measure-Object).Count if ($MailboxCount -gt 0) { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Starting mailbox permission caching for $MailboxCount mailboxes" -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Starting mailbox permission caching for $MailboxCount mailboxes" -sev Debug # Create batches of 10 mailboxes each $BatchSize = 10 @@ -86,9 +86,9 @@ function Set-CIPPDBCacheMailboxes { } } Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Compress -Depth 5) - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Started mailbox permission caching orchestrator with $($Batches.Count) batches" -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Started mailbox permission caching orchestrator with $($Batches.Count) batches" -sev Debug } else { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'No mailboxes found to cache permissions for' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'No mailboxes found to cache permissions for' -sev Debug } } catch { diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheManagedDeviceEncryptionStates.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheManagedDeviceEncryptionStates.ps1 index 9ab751d41ce0..4e4f78f33c2a 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheManagedDeviceEncryptionStates.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheManagedDeviceEncryptionStates.ps1 @@ -13,14 +13,14 @@ function Set-CIPPDBCacheManagedDeviceEncryptionStates { ) try { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching managed device encryption states' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching managed device encryption states' -sev Debug $ManagedDeviceEncryptionStates = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/deviceManagement/managedDeviceEncryptionStates?$top=999' -tenantid $TenantFilter if ($ManagedDeviceEncryptionStates) { Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ManagedDeviceEncryptionStates' -Data $ManagedDeviceEncryptionStates Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ManagedDeviceEncryptionStates' -Data $ManagedDeviceEncryptionStates -Count - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($ManagedDeviceEncryptionStates.Count) managed device encryption states" -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($ManagedDeviceEncryptionStates.Count) managed device encryption states" -sev Debug } $ManagedDeviceEncryptionStates = $null diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheManagedDevices.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheManagedDevices.ps1 index f9f6fbb7f11d..3e41edab2337 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheManagedDevices.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheManagedDevices.ps1 @@ -13,12 +13,12 @@ function Set-CIPPDBCacheManagedDevices { ) try { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching managed devices' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching managed devices' -sev Debug $ManagedDevices = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/deviceManagement/managedDevices?$top=999&$select=id,deviceName,operatingSystem,osVersion,complianceState,managedDeviceOwnerType,enrolledDateTime,lastSyncDateTime' -tenantid $TenantFilter Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ManagedDevices' -Data $ManagedDevices Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ManagedDevices' -Data $ManagedDevices -Count $ManagedDevices = $null - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached managed devices successfully' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached managed devices successfully' -sev Debug } catch { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache managed devices: $($_.Exception.Message)" -sev Error diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheOAuth2PermissionGrants.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheOAuth2PermissionGrants.ps1 index 3c4da6f7d188..78ea6366ae5d 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheOAuth2PermissionGrants.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheOAuth2PermissionGrants.ps1 @@ -13,14 +13,14 @@ function Set-CIPPDBCacheOAuth2PermissionGrants { ) try { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching OAuth2 permission grants' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching OAuth2 permission grants' -sev Debug $OAuth2PermissionGrants = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/oauth2PermissionGrants?$top=999' -tenantid $TenantFilter if ($OAuth2PermissionGrants) { Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'OAuth2PermissionGrants' -Data $OAuth2PermissionGrants Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'OAuth2PermissionGrants' -Data $OAuth2PermissionGrants -Count - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($OAuth2PermissionGrants.Count) OAuth2 permission grants" -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($OAuth2PermissionGrants.Count) OAuth2 permission grants" -sev Debug } $OAuth2PermissionGrants = $null diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheOneDriveUsage.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheOneDriveUsage.ps1 index 7ee193199394..4df2d347f02e 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheOneDriveUsage.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheOneDriveUsage.ps1 @@ -13,13 +13,13 @@ function Set-CIPPDBCacheOneDriveUsage { ) try { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching OneDrive usage' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching OneDrive usage' -sev Debug $OneDriveUsage = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/reports/getOneDriveUsageAccountDetail(period='D7')?`$format=application%2fjson" -tenantid $TenantFilter Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'OneDriveUsage' -Data $OneDriveUsage Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'OneDriveUsage' -Data $OneDriveUsage -Count $OneDriveUsage = $null - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached OneDrive usage successfully' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached OneDrive usage successfully' -sev Debug } catch { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache OneDrive usage: $($_.Exception.Message)" -sev Error diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheOrganization.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheOrganization.ps1 index c6c32918367b..4710caf76427 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheOrganization.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheOrganization.ps1 @@ -13,13 +13,13 @@ function Set-CIPPDBCacheOrganization { ) try { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching organization data' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching organization data' -sev Debug $Organization = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/organization' -tenantid $TenantFilter Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Organization' -Data $Organization $Organization = $null - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached organization data successfully' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached organization data successfully' -sev Debug } catch { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter ` diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCachePIMSettings.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCachePIMSettings.ps1 index 24f8dbef7d8b..224d357389d6 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCachePIMSettings.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCachePIMSettings.ps1 @@ -16,11 +16,11 @@ function Set-CIPPDBCachePIMSettings { $TestResult = Test-CIPPStandardLicense -StandardName 'PIMSettingsCache' -TenantFilter $TenantFilter -RequiredCapabilities @('AAD_PREMIUM_P2') -SkipLog if ($TestResult -eq $false) { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Tenant does not have Azure AD Premium P2 license, skipping PIM' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Tenant does not have Azure AD Premium P2 license, skipping PIM' -sev Debug return } - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching PIM settings' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching PIM settings' -sev Debug try { $PIMRoleSettings = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/policies/roleManagementPolicyAssignments?$top=999' -tenantid $TenantFilter @@ -28,7 +28,7 @@ function Set-CIPPDBCachePIMSettings { if ($PIMRoleSettings) { Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'PIMRoleSettings' -Data $PIMRoleSettings Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'PIMRoleSettings' -Data $PIMRoleSettings -Count - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($PIMRoleSettings.Count) PIM role settings" -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($PIMRoleSettings.Count) PIM role settings" -sev Debug } $PIMRoleSettings = $null } catch { @@ -41,14 +41,14 @@ function Set-CIPPDBCachePIMSettings { if ($PIMAssignments) { Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'PIMAssignments' -Data $PIMAssignments Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'PIMAssignments' -Data $PIMAssignments -Count - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($PIMAssignments.Count) PIM assignments" -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($PIMAssignments.Count) PIM assignments" -sev Debug } $PIMAssignments = $null } catch { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache PIM assignments: $($_.Exception.Message)" -sev Warning } - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached PIM settings successfully' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached PIM settings successfully' -sev Debug } catch { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache PIM settings: $($_.Exception.Message)" -sev Error diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheRiskDetections.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheRiskDetections.ps1 index c85118401d4f..2acc5fa2f099 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheRiskDetections.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheRiskDetections.ps1 @@ -13,7 +13,7 @@ function Set-CIPPDBCacheRiskDetections { ) try { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching risk detections from Identity Protection' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching risk detections from Identity Protection' -sev Debug # Requires P2 licensing $RiskDetections = New-GraphGetRequest -uri 'https://graph.microsoft.com/v1.0/identityProtection/riskDetections' -tenantid $TenantFilter @@ -21,9 +21,9 @@ function Set-CIPPDBCacheRiskDetections { if ($RiskDetections) { Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'RiskDetections' -Data $RiskDetections Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'RiskDetections' -Data $RiskDetections -Count - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($RiskDetections.Count) risk detections successfully" -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($RiskDetections.Count) risk detections successfully" -sev Debug } else { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'No risk detections found or Identity Protection not available' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'No risk detections found or Identity Protection not available' -sev Debug } } catch { diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheRiskyServicePrincipals.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheRiskyServicePrincipals.ps1 index 950e9998ec0b..09092dd18716 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheRiskyServicePrincipals.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheRiskyServicePrincipals.ps1 @@ -13,7 +13,7 @@ function Set-CIPPDBCacheRiskyServicePrincipals { ) try { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching risky service principals from Identity Protection' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching risky service principals from Identity Protection' -sev Debug # Requires Workload Identity Premium licensing $RiskyServicePrincipals = New-GraphGetRequest -uri 'https://graph.microsoft.com/v1.0/identityProtection/riskyServicePrincipals' -tenantid $TenantFilter @@ -21,9 +21,9 @@ function Set-CIPPDBCacheRiskyServicePrincipals { if ($RiskyServicePrincipals) { Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'RiskyServicePrincipals' -Data $RiskyServicePrincipals Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'RiskyServicePrincipals' -Data $RiskyServicePrincipals -Count - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($RiskyServicePrincipals.Count) risky service principals successfully" -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($RiskyServicePrincipals.Count) risky service principals successfully" -sev Debug } else { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'No risky service principals found or Workload Identity Protection not available' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'No risky service principals found or Workload Identity Protection not available' -sev Debug } } catch { diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheRiskyUsers.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheRiskyUsers.ps1 index 417110f483bd..813dff9da5ac 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheRiskyUsers.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheRiskyUsers.ps1 @@ -13,7 +13,7 @@ function Set-CIPPDBCacheRiskyUsers { ) try { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching risky users from Identity Protection' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching risky users from Identity Protection' -sev Debug # Requires P2 or Governance licensing $RiskyUsers = New-GraphGetRequest -uri 'https://graph.microsoft.com/v1.0/identityProtection/riskyUsers' -tenantid $TenantFilter @@ -21,9 +21,9 @@ function Set-CIPPDBCacheRiskyUsers { if ($RiskyUsers) { Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'RiskyUsers' -Data $RiskyUsers Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'RiskyUsers' -Data $RiskyUsers -Count - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($RiskyUsers.Count) risky users successfully" -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($RiskyUsers.Count) risky users successfully" -sev Debug } else { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'No risky users found or Identity Protection not available' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'No risky users found or Identity Protection not available' -sev Debug } } catch { diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheRoleEligibilitySchedules.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheRoleEligibilitySchedules.ps1 index 6433570cf810..a4f559b5bd6b 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheRoleEligibilitySchedules.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheRoleEligibilitySchedules.ps1 @@ -13,12 +13,12 @@ function Set-CIPPDBCacheRoleEligibilitySchedules { ) try { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching role eligibility schedules' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching role eligibility schedules' -sev Debug $RoleEligibilitySchedules = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/roleManagement/directory/roleEligibilitySchedules' -tenantid $TenantFilter Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'RoleEligibilitySchedules' -Data @($RoleEligibilitySchedules) $RoleEligibilitySchedules = $null - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached role eligibility schedules successfully' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached role eligibility schedules successfully' -sev Debug } catch { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache role eligibility schedules: $($_.Exception.Message)" -sev Error diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheRoleManagementPolicies.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheRoleManagementPolicies.ps1 index fcba68720a1f..a2c3f97b99d5 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheRoleManagementPolicies.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheRoleManagementPolicies.ps1 @@ -13,12 +13,12 @@ function Set-CIPPDBCacheRoleManagementPolicies { ) try { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching role management policies' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching role management policies' -sev Debug $RoleManagementPolicies = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/policies/roleManagementPolicies' -tenantid $TenantFilter Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'RoleManagementPolicies' -Data @($RoleManagementPolicies) $RoleManagementPolicies = $null - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached role management policies successfully' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached role management policies successfully' -sev Debug } catch { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache role management policies: $($_.Exception.Message)" -sev Error diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheRoles.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheRoles.ps1 index 4d926c1c722c..f9f6bd66c77d 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheRoles.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheRoles.ps1 @@ -13,7 +13,7 @@ function Set-CIPPDBCacheRoles { ) try { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching directory roles' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching directory roles' -sev Debug $Roles = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/directoryRoles' -tenantid $TenantFilter @@ -29,7 +29,7 @@ function Set-CIPPDBCacheRoles { } if ($MemberRequests) { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Fetching role members' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Fetching role members' -sev Debug $MemberResults = New-GraphBulkRequest -Requests @($MemberRequests) -tenantid $TenantFilter # Add members to each role object @@ -55,7 +55,7 @@ function Set-CIPPDBCacheRoles { $Roles = $null } - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached directory roles successfully' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached directory roles successfully' -sev Debug } catch { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache directory roles: $($_.Exception.Message)" -sev Error diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheSecureScore.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheSecureScore.ps1 index b1db9a6c1234..de3eab54f0ed 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheSecureScore.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheSecureScore.ps1 @@ -13,7 +13,7 @@ function Set-CIPPDBCacheSecureScore { ) try { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching secure score' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching secure score' -sev Debug # Cache secure score history (last 14 days) $SecureScore = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/security/secureScores?$top=14' -tenantid $TenantFilter -noPagination $true @@ -27,7 +27,7 @@ function Set-CIPPDBCacheSecureScore { Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'SecureScoreControlProfiles' -Data $SecureScoreControlProfiles -Count $SecureScoreControlProfiles = $null - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached secure score and control profiles successfully' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached secure score and control profiles successfully' -sev Debug } catch { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache secure score: $($_.Exception.Message)" -sev Error diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheServicePrincipalRiskDetections.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheServicePrincipalRiskDetections.ps1 index 79aeec84eada..a437723e0abd 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheServicePrincipalRiskDetections.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheServicePrincipalRiskDetections.ps1 @@ -13,7 +13,7 @@ function Set-CIPPDBCacheServicePrincipalRiskDetections { ) try { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching service principal risk detections from Identity Protection' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching service principal risk detections from Identity Protection' -sev Debug # Requires Workload Identity Premium licensing $ServicePrincipalRiskDetections = New-GraphGetRequest -uri 'https://graph.microsoft.com/v1.0/identityProtection/servicePrincipalRiskDetections' -tenantid $TenantFilter @@ -21,9 +21,9 @@ function Set-CIPPDBCacheServicePrincipalRiskDetections { if ($ServicePrincipalRiskDetections) { Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ServicePrincipalRiskDetections' -Data $ServicePrincipalRiskDetections Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ServicePrincipalRiskDetections' -Data $ServicePrincipalRiskDetections -Count - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($ServicePrincipalRiskDetections.Count) service principal risk detections successfully" -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($ServicePrincipalRiskDetections.Count) service principal risk detections successfully" -sev Debug } else { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'No service principal risk detections found or Workload Identity Protection not available' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'No service principal risk detections found or Workload Identity Protection not available' -sev Debug } } catch { diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheServicePrincipals.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheServicePrincipals.ps1 index e2422e6028b7..b91941940e66 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheServicePrincipals.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheServicePrincipals.ps1 @@ -13,14 +13,14 @@ function Set-CIPPDBCacheServicePrincipals { ) try { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching service principals' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching service principals' -sev Debug $ServicePrincipals = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/servicePrincipals' -tenantid $TenantFilter Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ServicePrincipals' -Data $ServicePrincipals Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ServicePrincipals' -Data $ServicePrincipals -Count $ServicePrincipals = $null - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached service principals successfully' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached service principals successfully' -sev Debug } catch { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter ` diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheSettings.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheSettings.ps1 index 3f809db48f57..08aee0f383a7 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheSettings.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheSettings.ps1 @@ -13,12 +13,12 @@ function Set-CIPPDBCacheSettings { ) try { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching directory settings' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching directory settings' -sev Debug $Settings = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/settings?$top=999' -tenantid $TenantFilter Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Settings' -Data $Settings $Settings = $null - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached directory settings successfully' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached directory settings successfully' -sev Debug } catch { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter ` diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheUserRegistrationDetails.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheUserRegistrationDetails.ps1 index 51ff2b467e55..818a441dc0f3 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheUserRegistrationDetails.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheUserRegistrationDetails.ps1 @@ -13,14 +13,14 @@ function Set-CIPPDBCacheUserRegistrationDetails { ) try { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching user registration details' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching user registration details' -sev Debug $UserRegistrationDetails = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/reports/authenticationMethods/userRegistrationDetails' -tenantid $TenantFilter if ($UserRegistrationDetails) { Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'UserRegistrationDetails' -Data $UserRegistrationDetails Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'UserRegistrationDetails' -Data $UserRegistrationDetails -Count - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($UserRegistrationDetails.Count) user registration details" -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($UserRegistrationDetails.Count) user registration details" -sev Debug } $UserRegistrationDetails = $null diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheUsers.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheUsers.ps1 index 2a03fedc46ab..a1ffd11ef337 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheUsers.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheUsers.ps1 @@ -13,13 +13,13 @@ function Set-CIPPDBCacheUsers { ) try { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching users' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching users' -sev Debug $Users = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/users?$top=999' -tenantid $TenantFilter Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Users' -Data $Users Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Users' -Data $Users -Count $Users = $null - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached users successfully' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached users successfully' -sev Debug } catch { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache users: $($_.Exception.Message)" -sev Error From 6d85886a66ff91a17809a1f07edfc5556a2f9762 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 16 Jan 2026 13:28:42 -0500 Subject: [PATCH 176/503] Filter MFADevices to exclude password auth methods Updated the Push-BECRun function to filter out entries in MFADevices where '@odata.type' equals '#microsoft.graph.passwordAuthenticationMethod'. This ensures only relevant MFA devices are included in the output. --- .../Public/Entrypoints/Activity Triggers/BEC/Push-BECRun.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/BEC/Push-BECRun.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/BEC/Push-BECRun.ps1 index 09eabf880be8..f4cc20ab450b 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/BEC/Push-BECRun.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/BEC/Push-BECRun.ps1 @@ -157,7 +157,7 @@ function Push-BECRun { NewRules = @($RulesLog) MailboxPermissionChanges = @($PermissionsLog) NewUsers = @($NewUsers) - MFADevices = @($MFADevices) + MFADevices = @($MFADevices | Where-Object { $_.'@odata.type' -ne '#microsoft.graph.passwordAuthenticationMethod' }) ChangedPasswords = @($PasswordChanges) ExtractedAt = (Get-Date) ExtractResult = $ExtractResult From 869e76883a815c80e8c82f775d9efcfab92705ed Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 16 Jan 2026 13:36:44 -0500 Subject: [PATCH 177/503] Update default and latest version to 10.0.1 Bump the defaultVersion in host.json and update version_latest.txt to 10.0.1 to reflect the latest release. --- host.json | 2 +- version_latest.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/host.json b/host.json index ae2a91f1ca55..ec9e853f1eed 100644 --- a/host.json +++ b/host.json @@ -16,7 +16,7 @@ "distributedTracingEnabled": false, "version": "None" }, - "defaultVersion": "10.0.0", + "defaultVersion": "10.0.1", "versionMatchStrategy": "Strict", "versionFailureStrategy": "Fail" } diff --git a/version_latest.txt b/version_latest.txt index a13e7b9c87e4..1532420512a9 100644 --- a/version_latest.txt +++ b/version_latest.txt @@ -1 +1 @@ -10.0.0 +10.0.1 From 7d4f249a06c27cb877249eb11a544678e398c280 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Sat, 17 Jan 2026 10:25:09 -0500 Subject: [PATCH 178/503] fix typo --- .../CIPP/Settings/Invoke-ExecExchangeRoleRepair.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecExchangeRoleRepair.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecExchangeRoleRepair.ps1 index facaa423a70a..af8f95f2edc7 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecExchangeRoleRepair.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecExchangeRoleRepair.ps1 @@ -89,7 +89,7 @@ function Invoke-ExecExchangeRoleRepair { } } - returns ([HttpResponseContext]@{ + return ([HttpResponseContext]@{ StatusCode = [System.Net.HttpStatusCode]::OK Body = $Results }) From 5d3c3624f00d17e121d37a8b3d77310aaf547f6e Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Sat, 17 Jan 2026 17:25:17 +0100 Subject: [PATCH 179/503] Add or update the Azure App Service build and deployment workflow config --- .github/workflows/master_cippjiuus.yml | 30 ++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 .github/workflows/master_cippjiuus.yml diff --git a/.github/workflows/master_cippjiuus.yml b/.github/workflows/master_cippjiuus.yml new file mode 100644 index 000000000000..6b404e985583 --- /dev/null +++ b/.github/workflows/master_cippjiuus.yml @@ -0,0 +1,30 @@ +# Docs for the Azure Web Apps Deploy action: https://github.com/azure/functions-action +# More GitHub Actions for Azure: https://github.com/Azure/actions + +name: Build and deploy Powershell project to Azure Function App - cippjiuus + +on: + push: + branches: + - master + workflow_dispatch: + +env: + AZURE_FUNCTIONAPP_PACKAGE_PATH: '.' # set this to the path to your web app project, defaults to the repository root + +jobs: + deploy: + runs-on: ubuntu-latest + + steps: + - name: 'Checkout GitHub Action' + uses: actions/checkout@v4 + + - name: 'Run Azure Functions Action' + uses: Azure/functions-action@v1 + id: fa + with: + app-name: 'cippjiuus' + slot-name: 'Production' + package: ${{ env.AZURE_FUNCTIONAPP_PACKAGE_PATH }} + publish-profile: ${{ secrets.AZUREAPPSERVICE_PUBLISHPROFILE_E2C0118215FF474E9A6386578DD008AD }} \ No newline at end of file From dd32e67bd658765a7bb7e9f93365e7b7c9ecbfe9 Mon Sep 17 00:00:00 2001 From: Chris Riani <153139246+criani@users.noreply.github.com> Date: Sat, 17 Jan 2026 16:58:53 -0500 Subject: [PATCH 180/503] Delete .github/workflows/master_cippjiuus.yml --- .github/workflows/master_cippjiuus.yml | 30 -------------------------- 1 file changed, 30 deletions(-) delete mode 100644 .github/workflows/master_cippjiuus.yml diff --git a/.github/workflows/master_cippjiuus.yml b/.github/workflows/master_cippjiuus.yml deleted file mode 100644 index 6b404e985583..000000000000 --- a/.github/workflows/master_cippjiuus.yml +++ /dev/null @@ -1,30 +0,0 @@ -# Docs for the Azure Web Apps Deploy action: https://github.com/azure/functions-action -# More GitHub Actions for Azure: https://github.com/Azure/actions - -name: Build and deploy Powershell project to Azure Function App - cippjiuus - -on: - push: - branches: - - master - workflow_dispatch: - -env: - AZURE_FUNCTIONAPP_PACKAGE_PATH: '.' # set this to the path to your web app project, defaults to the repository root - -jobs: - deploy: - runs-on: ubuntu-latest - - steps: - - name: 'Checkout GitHub Action' - uses: actions/checkout@v4 - - - name: 'Run Azure Functions Action' - uses: Azure/functions-action@v1 - id: fa - with: - app-name: 'cippjiuus' - slot-name: 'Production' - package: ${{ env.AZURE_FUNCTIONAPP_PACKAGE_PATH }} - publish-profile: ${{ secrets.AZUREAPPSERVICE_PUBLISHPROFILE_E2C0118215FF474E9A6386578DD008AD }} \ No newline at end of file From d9c60c7f3b85043b5847cbf06f3419212e6a14ea Mon Sep 17 00:00:00 2001 From: Chris Riani <153139246+criani@users.noreply.github.com> Date: Sat, 17 Jan 2026 22:32:33 -0500 Subject: [PATCH 181/503] Update Invoke-ListMFAUsers.ps1 --- .../HTTP Functions/Identity/Reports/Invoke-ListMFAUsers.ps1 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Reports/Invoke-ListMFAUsers.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Reports/Invoke-ListMFAUsers.ps1 index 04d6ceed951f..05b050b43956 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Reports/Invoke-ListMFAUsers.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Reports/Invoke-ListMFAUsers.ps1 @@ -73,8 +73,9 @@ function Invoke-ListMFAUsers { } return ([HttpResponseContext]@{ - StatusCode = [HttpStatusCode]::OK + StatusCode = $StatusCode Body = @($GraphRequest) }) + } From 79e523eafa7fa11cd2b54e4ad9eb4af43f971b96 Mon Sep 17 00:00:00 2001 From: Chris Riani <153139246+criani@users.noreply.github.com> Date: Sat, 17 Jan 2026 22:39:18 -0500 Subject: [PATCH 182/503] Update Get-CIPPMFAState.ps1 --- Modules/CIPPCore/Public/Get-CIPPMFAState.ps1 | 33 ++++++++++++-------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/Modules/CIPPCore/Public/Get-CIPPMFAState.ps1 b/Modules/CIPPCore/Public/Get-CIPPMFAState.ps1 index 1f16664f1da3..234d6b01dba5 100644 --- a/Modules/CIPPCore/Public/Get-CIPPMFAState.ps1 +++ b/Modules/CIPPCore/Public/Get-CIPPMFAState.ps1 @@ -18,19 +18,31 @@ function Get-CIPPMFAState { } $Errors = [System.Collections.Generic.List[object]]::new() + $SecureDefaultsState = $null + $CASuccess = $false + $CAError = $null + $PolicyTable = @{} + $AllUserPolicies = @() + $UserGroupMembership = @{} + $UserExcludeGroupMembership = @{} + $GroupNameLookup = @{} + $MFAIndex = @{} + try { $SecureDefaultsState = (New-GraphGetRequest -Uri 'https://graph.microsoft.com/beta/policies/identitySecurityDefaultsEnforcementPolicy' -tenantid $TenantFilter ).IsEnabled } catch { Write-Host "Secure Defaults not available: $($_.Exception.Message)" $Errors.Add(@{Step = 'SecureDefaults'; Message = $_.Exception.Message }) + $SecureDefaultsState = $null } $CAState = [System.Collections.Generic.List[object]]::new() try { - $MFARegistration = (New-GraphGetRequest -uri "https://graph.microsoft.com/beta/reports/authenticationMethods/userRegistrationDetails?$top=999&$select=userPrincipalName,isMfaRegistered,isMfaCapable,methodsRegistered" -tenantid $TenantFilter -asapp $true) - $MFAIndex = @{} + $MFARegistration = (New-GraphGetRequest -uri "https://graph.microsoft.com/beta/reports/authenticationMethods/userRegistrationDetails?`$top=999&`$select=userPrincipalName,isMfaRegistered,isMfaCapable,methodsRegistered" -tenantid $TenantFilter -asapp $true) foreach ($MFAEntry in $MFARegistration) { - $MFAIndex[$MFAEntry.userPrincipalName] = $MFAEntry + if ($null -ne $MFAEntry.userPrincipalName) { + $MFAIndex[$MFAEntry.userPrincipalName] = $MFAEntry + } } } catch { $CAState.Add('Not Licensed for Conditional Access') | Out-Null @@ -39,12 +51,11 @@ function Get-CIPPMFAState { $Errors.Add(@{Step = 'MFARegistration'; Message = $_.Exception.Message }) } Write-Host "User registration details not available: $($_.Exception.Message)" - $MFAIndex = @{} } if ($null -ne $MFARegistration) { - $CASuccess = $true try { + $CASuccess = $true $CAPolicies = (New-GraphGetRequest -Uri 'https://graph.microsoft.com/beta/identity/conditionalAccess/policies?$top=999&$filter=state eq ''enabled''&$select=id,displayName,state,grantControls,conditions' -tenantid $TenantFilter -ErrorAction Stop -AsApp $true) $PolicyTable = @{} $AllUserPolicies = [System.Collections.Generic.List[object]]::new() @@ -315,11 +326,7 @@ function Get-CIPPMFAState { $PerUser = $_.PerUserMFAState - $MFARegUser = if ($null -eq ($MFAIndex[$_.UserPrincipalName])) { - $false - } else { - $MFAIndex[$_.UserPrincipalName] - } + $MFARegUser = $MFAIndex[$_.UserPrincipalName] [PSCustomObject]@{ Tenant = $TenantFilter @@ -329,9 +336,9 @@ function Get-CIPPMFAState { AccountEnabled = $_.accountEnabled PerUser = $PerUser isLicensed = $_.isLicensed - MFARegistration = if ($MFARegUser) { $MFARegUser.isMfaRegistered } else { $false } - MFACapable = if ($MFARegUser) { $MFARegUser.isMfaCapable } else { $false } - MFAMethods = if ($MFARegUser) { $MFARegUser.methodsRegistered } else { @() } + MFARegistration = if ($null -ne $MFARegUser) { [bool]$MFARegUser.isMfaRegistered } else { $null } + MFACapable = if ($null -ne $MFARegUser) { [bool]$MFARegUser.isMfaCapable } else { $null } + MFAMethods = if ($null -ne $MFARegUser) { @($MFARegUser.methodsRegistered) } else { @() } CoveredByCA = $CoveredByCA CAPolicies = $UserCAState CoveredBySD = $SecureDefaultsState From ff01a4904ed6892fe6cddad0faf6ceb2948dbdc0 Mon Sep 17 00:00:00 2001 From: Chris Riani <153139246+criani@users.noreply.github.com> Date: Sun, 18 Jan 2026 09:23:32 -0500 Subject: [PATCH 183/503] Delete .github/workflows/master_prospectorcipp62evl.yml --- .../workflows/master_prospectorcipp62evl.yml | 30 ------------------- 1 file changed, 30 deletions(-) delete mode 100644 .github/workflows/master_prospectorcipp62evl.yml diff --git a/.github/workflows/master_prospectorcipp62evl.yml b/.github/workflows/master_prospectorcipp62evl.yml deleted file mode 100644 index 855645fd95e5..000000000000 --- a/.github/workflows/master_prospectorcipp62evl.yml +++ /dev/null @@ -1,30 +0,0 @@ -# Docs for the Azure Web Apps Deploy action: https://github.com/azure/functions-action -# More GitHub Actions for Azure: https://github.com/Azure/actions - -name: Build and deploy Powershell project to Azure Function App - prospectorcipp62evl - -on: - push: - branches: - - master - workflow_dispatch: - -env: - AZURE_FUNCTIONAPP_PACKAGE_PATH: '.' # set this to the path to your web app project, defaults to the repository root - -jobs: - deploy: - runs-on: windows-latest - - steps: - - name: 'Checkout GitHub Action' - uses: actions/checkout@v4 - - - name: 'Run Azure Functions Action' - uses: Azure/functions-action@v1 - id: fa - with: - app-name: 'prospectorcipp62evl' - slot-name: 'Production' - package: ${{ env.AZURE_FUNCTIONAPP_PACKAGE_PATH }} - publish-profile: ${{ secrets.AZUREAPPSERVICE_PUBLISHPROFILE_30309544A6BA4EED8D77428610F14DBE }} \ No newline at end of file From d96bb4a00c39e07674f8f01fca9930e3c4d93404 Mon Sep 17 00:00:00 2001 From: Chris Riani <153139246+criani@users.noreply.github.com> Date: Sun, 18 Jan 2026 09:27:01 -0500 Subject: [PATCH 184/503] Update Invoke-CIPPStandardPhishProtection.ps1 --- .../Public/Standards/Invoke-CIPPStandardPhishProtection.ps1 | 1 - 1 file changed, 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPhishProtection.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPhishProtection.ps1 index 095b53fb2ea5..fdf203641250 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPhishProtection.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPhishProtection.ps1 @@ -50,7 +50,6 @@ function Invoke-CIPPStandardPhishProtection { background-image: url(https://clone.cipp.app/api/PublicPhishingCheck?Tenantid=$($tenant)&URL=https://$($CIPPUrl)); } "@ - if ($Settings.remediate -eq $true) { $malformedCSSPattern = '\.ext-sign-in-box\s*\{\s*background-image:\s*url\(https://clone\.cipp\.app/api/PublicPhishingCheck\?Tenantid=[^&]*&URL=\);\s*\}' From 8634610c02dcd98daf42ab7d0b471182b9817f9f Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Mon, 19 Jan 2026 16:09:36 +0800 Subject: [PATCH 185/503] Create Get-CIPPAlertSecureScore.ps1 --- .../Alerts/Get-CIPPAlertSecureScore.ps1 | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 Modules/CIPPCore/Public/Alerts/Get-CIPPAlertSecureScore.ps1 diff --git a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertSecureScore.ps1 b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertSecureScore.ps1 new file mode 100644 index 000000000000..4bf49b56bbc7 --- /dev/null +++ b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertSecureScore.ps1 @@ -0,0 +1,46 @@ + +function Get-CippAlertSecureScore { + <# + .FUNCTIONALITY + Entrypoint + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $false)] + [Alias('input')] + $InputValue, + $TenantFilter + ) + try { + $SecureScore = New-GraphGetRequest -uri 'https://graph.microsoft.com/v1.0/security/secureScores?$top=1' -tenantid $TenantFilter -noPagination $true + if ($InputValue.ThresholdType.value -eq "absolute") { + if ($SecureScore.currentScore -lt $InputValue.InputValue) { + $SecureScoreResult = [PSCustomObject]@{ + Message = "Secure Score is below acceptable threshold" + Tenant = $TenantFilter + CurrentScore = $SecureScore.currentScore + MaxSecureScore = $SecureScore.maxScore + } + } else { + $SecureScoreResult = @() + } + } elseif ($InputValue.ThresholdType.value -eq "percent") { + $PercentageScore = [math]::Round((($SecureScore.currentScore / $SecureScore.maxScore) * 100),2) + if ($PercentageScore -lt $InputValue.InputValue) { + $SecureScoreResult = [PSCustomObject]@{ + Message = "Secure Score is below acceptable threshold" + Tenant = $TenantFilter + CurrentScore = $SecureScore.currentScore + MaxScore = $SecureScore.maxScore + CurrentScorePercentage = [math]::Round($PercentageScore,2) + ScoreThresholdPercentage = $InputValue.InputValue + } + } else { + $SecureScoreResult = @() + } + } + Write-AlertTrace -cmdletName $MyInvocation.MyCommand -tenantFilter $TenantFilter -data $SecureScoreResult -PartitionKey SecureScore + } catch { + Write-AlertMessage -tenant $($TenantFilter) -message "Could not get Secure Score for $($TenantFilter): $(Get-NormalizedError -message $_.Exception.message)" + } +} From 65c48fca96c1e0988316f72dfec4b8e7611cebe7 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Mon, 19 Jan 2026 09:43:01 -0500 Subject: [PATCH 186/503] Filter tenants using Get-Tenants in report scripts Updated Get-CIPPMFAStateReport and Get-CIPPMailboxPermissionReport to filter tenants based on the defaultDomainName property from Get-Tenants with -IncludeErrors. This ensures only valid tenants are processed in the reports. --- Modules/CIPPCore/Public/Get-CIPPMFAStateReport.ps1 | 3 +++ Modules/CIPPCore/Public/Get-CIPPMailboxPermissionReport.ps1 | 3 +++ 2 files changed, 6 insertions(+) diff --git a/Modules/CIPPCore/Public/Get-CIPPMFAStateReport.ps1 b/Modules/CIPPCore/Public/Get-CIPPMFAStateReport.ps1 index d0b32d9bbf55..7a490c1d9cc1 100644 --- a/Modules/CIPPCore/Public/Get-CIPPMFAStateReport.ps1 +++ b/Modules/CIPPCore/Public/Get-CIPPMFAStateReport.ps1 @@ -27,6 +27,9 @@ function Get-CIPPMFAStateReport { $AllMFAItems = Get-CIPPDbItem -TenantFilter 'allTenants' -Type 'MFAState' $Tenants = @($AllMFAItems | Where-Object { $_.RowKey -ne 'MFAState-Count' } | Select-Object -ExpandProperty PartitionKey -Unique) + $TenantList = Get-Tenants -IncludeErrors + $Tenants = $Tenants | Where-Object { $TenantList.defaultDomainName -contains $_ } + $AllResults = [System.Collections.Generic.List[PSCustomObject]]::new() foreach ($Tenant in $Tenants) { try { diff --git a/Modules/CIPPCore/Public/Get-CIPPMailboxPermissionReport.ps1 b/Modules/CIPPCore/Public/Get-CIPPMailboxPermissionReport.ps1 index 29bb8efb1ac7..7e7892398fce 100644 --- a/Modules/CIPPCore/Public/Get-CIPPMailboxPermissionReport.ps1 +++ b/Modules/CIPPCore/Public/Get-CIPPMailboxPermissionReport.ps1 @@ -39,6 +39,9 @@ function Get-CIPPMailboxPermissionReport { $AllMailboxItems = Get-CIPPDbItem -TenantFilter 'allTenants' -Type 'Mailboxes' $Tenants = @($AllMailboxItems | Where-Object { $_.RowKey -ne 'Mailboxes-Count' } | Select-Object -ExpandProperty PartitionKey -Unique) + $TenantList = Get-Tenants -IncludeErrors + $Tenants = $Tenants | Where-Object { $TenantList.defaultDomainName -contains $_ } + $AllResults = [System.Collections.Generic.List[PSCustomObject]]::new() foreach ($Tenant in $Tenants) { try { From 682bda5166e30b4b53a51fbf95603b83649bd86c Mon Sep 17 00:00:00 2001 From: John Duprey Date: Mon, 19 Jan 2026 23:00:35 -0500 Subject: [PATCH 187/503] Nullify Package and SHA only if present in template Updated Invoke-ExecCloneTemplate to set Package and SHA to null only if they exist in the template object, preventing unnecessary property assignments. --- .../HTTP Functions/CIPP/Core/Invoke-ExecCloneTemplate.ps1 | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ExecCloneTemplate.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ExecCloneTemplate.ps1 index 2db5725ce7b6..ccd902350988 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ExecCloneTemplate.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ExecCloneTemplate.ps1 @@ -21,8 +21,12 @@ function Invoke-ExecCloneTemplate { $NewGuid = [guid]::NewGuid().ToString() $Template.RowKey = $NewGuid $Template.JSON = $Template.JSON -replace $GUID, $NewGuid - $Template.Package = $null - $Template.SHA = $null + if ($Template.Package) { + $Template.Package = $null + } + if ($Template.SHA) { + $Template.SHA = $null + } try { Add-CIPPAzDataTableEntity @Table -Entity $Template $body = @{ From 7bc5a5e71a99bcba4df28ca128f13f6c1e9270fc Mon Sep 17 00:00:00 2001 From: John Duprey Date: Mon, 19 Jan 2026 23:55:28 -0500 Subject: [PATCH 188/503] sort names --- Modules/CIPPCore/Public/Get-CIPPMFAStateReport.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Get-CIPPMFAStateReport.ps1 b/Modules/CIPPCore/Public/Get-CIPPMFAStateReport.ps1 index 7a490c1d9cc1..3595d69f5295 100644 --- a/Modules/CIPPCore/Public/Get-CIPPMFAStateReport.ps1 +++ b/Modules/CIPPCore/Public/Get-CIPPMFAStateReport.ps1 @@ -72,7 +72,7 @@ function Get-CIPPMFAStateReport { $AllMFAState.Add($MFAUser) } - return $AllMFAState + return $AllMFAState | Sort-Object -Property DisplayName } catch { Write-LogMessage -API 'MFAStateReport' -tenant $TenantFilter -message "Failed to generate MFA state report: $($_.Exception.Message)" -sev Error From c5b167f7b3da313bcf10a2a4d86625eb93a23e28 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Tue, 20 Jan 2026 00:20:44 -0500 Subject: [PATCH 189/503] Improve filtering logic for count rows in Get-CIPPDbItem Refines the filter logic to use an exact match for the count row when the Type parameter is specified, and applies server-side filtering for count rows when Type is not specified. This change improves efficiency by reducing unnecessary client-side filtering. --- Modules/CIPPCore/Public/Get-CIPPDbItem.ps1 | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Modules/CIPPCore/Public/Get-CIPPDbItem.ps1 b/Modules/CIPPCore/Public/Get-CIPPDbItem.ps1 index 13924c6794d0..9f324f5eaf3d 100644 --- a/Modules/CIPPCore/Public/Get-CIPPDbItem.ps1 +++ b/Modules/CIPPCore/Public/Get-CIPPDbItem.ps1 @@ -42,11 +42,15 @@ function Get-CIPPDbItem { $Conditions.Add("PartitionKey eq '{0}'" -f $TenantFilter) } if ($Type) { - $Conditions.Add("RowKey ge '{0}-' and RowKey lt '{0}.'" -f $Type) + # Exact match for count row when type is specified + $Conditions.Add("RowKey eq '{0}-Count'" -f $Type) + } else { + # Filter by DataCount property to get only count rows (server-side filtering) + $Conditions.Add('DataCount ge 0') } $Filter = [string]::Join(' and ', $Conditions) $Results = Get-CIPPAzDataTableEntity @Table -Filter $Filter -Property 'PartitionKey', 'RowKey', 'DataCount', 'Timestamp' - $Results = $Results | Where-Object { $_.RowKey -like '*-Count' } | Select-Object PartitionKey, RowKey, DataCount, Timestamp + $Results = $Results | Select-Object PartitionKey, RowKey, DataCount, Timestamp } else { if (-not $Type) { throw 'Type parameter is required when CountsOnly is not specified' From 70f86d2a8d5a2c687aa1e34dbdda7e87a8864b13 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Tue, 20 Jan 2026 00:33:07 -0500 Subject: [PATCH 190/503] Add calendar permissions batch processing and storage Introduces Push-GetCalendarPermissionsBatch.ps1 to process calendar permissions in batches. Updates Push-StoreMailboxPermissions.ps1 to aggregate and store calendar permissions alongside mailbox and recipient permissions. Modifies Set-CIPPDBCacheMailboxes.ps1 to create and orchestrate both mailbox and calendar permission batches, enabling unified caching and reporting for all permission types. --- .../Push-GetCalendarPermissionsBatch.ps1 | 68 ++++++++++++++++++ .../Push-StoreMailboxPermissions.ps1 | 23 ++++-- .../Public/Set-CIPPDBCacheMailboxes.ps1 | 71 ++++++++++++------- 3 files changed, 131 insertions(+), 31 deletions(-) create mode 100644 Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Mailbox Permissions/Push-GetCalendarPermissionsBatch.ps1 diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Mailbox Permissions/Push-GetCalendarPermissionsBatch.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Mailbox Permissions/Push-GetCalendarPermissionsBatch.ps1 new file mode 100644 index 000000000000..5c98bacd6aec --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Mailbox Permissions/Push-GetCalendarPermissionsBatch.ps1 @@ -0,0 +1,68 @@ +function Push-GetCalendarPermissionsBatch { + <# + .SYNOPSIS + Process a batch of calendar permission queries + + .DESCRIPTION + Queries calendar permissions for a batch of mailboxes + + .FUNCTIONALITY + Entrypoint + #> + param($Item) + + $TenantFilter = $Item.TenantFilter + $Mailboxes = $Item.Mailboxes + $BatchNumber = $Item.BatchNumber + $TotalBatches = $Item.TotalBatches + + try { + Write-Information "Processing calendar permissions batch $BatchNumber of $TotalBatches for tenant $TenantFilter with $($Mailboxes.Count) mailboxes" + + $AllCalendarPermissions = [System.Collections.Generic.List[object]]::new() + + foreach ($MailboxUPN in $Mailboxes) { + try { + # Step 1: Get the calendar folder name (locale-specific) + $GetCalParam = @{Identity = $MailboxUPN; FolderScope = 'Calendar' } + $CalendarFolder = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-MailboxFolderStatistics' -anchor $MailboxUPN -cmdParams $GetCalParam | Select-Object -First 1 + + if ($CalendarFolder -and $CalendarFolder.name) { + # Step 2: Get calendar permissions using the folder name + $CalParam = @{Identity = "$($MailboxUPN):\$($CalendarFolder.name)" } + $CalendarPermissions = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-MailboxFolderPermission' -anchor $MailboxUPN -cmdParams $CalParam -UseSystemMailbox $true + + # Normalize the results + foreach ($Perm in $CalendarPermissions) { + $AllCalendarPermissions.Add([PSCustomObject]@{ + id = [guid]::NewGuid().ToString() + Identity = $Perm.Identity + User = $Perm.User + AccessRights = $Perm.AccessRights + FolderName = $Perm.FolderName + }) + } + } else { + Write-Information "No calendar folder found for mailbox $MailboxUPN" + } + } catch { + Write-Information "Failed to get calendar permissions for $MailboxUPN : $($_.Exception.Message)" + # Continue processing other mailboxes + } + } + + Write-Information "Completed calendar permissions batch $BatchNumber of $TotalBatches - processed $($Mailboxes.Count) mailboxes: $($AllCalendarPermissions.Count) calendar permissions" + + # Return results grouped by command type for consistency with mailbox permissions + return @{ + 'Get-MailboxFolderPermission' = $AllCalendarPermissions + } + + } catch { + $ErrorMsg = "Failed to process calendar permissions batch $BatchNumber of $TotalBatches for tenant $TenantFilter : $($_.Exception.Message)" + Write-Information "ERROR in Push-GetCalendarPermissionsBatch: $ErrorMsg" + Write-Information "Stack trace: $($_.ScriptStackTrace)" + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message $ErrorMsg -sev Error + throw $ErrorMsg + } +} diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Mailbox Permissions/Push-StoreMailboxPermissions.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Mailbox Permissions/Push-StoreMailboxPermissions.ps1 index fc5a967664b3..911745c1b06b 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Mailbox Permissions/Push-StoreMailboxPermissions.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Mailbox Permissions/Push-StoreMailboxPermissions.ps1 @@ -1,7 +1,7 @@ function Push-StoreMailboxPermissions { <# .SYNOPSIS - Post-execution function to aggregate and store all mailbox permissions + Post-execution function to aggregate and store all mailbox and calendar permissions .DESCRIPTION Collects results from all batches and stores them in the reporting database @@ -16,7 +16,7 @@ function Push-StoreMailboxPermissions { $Results = $Item.Results try { - Write-Information "Storing mailbox permissions for tenant $TenantFilter" + Write-Information "Storing mailbox and calendar permissions for tenant $TenantFilter" Write-Information "Received $($Results.Count) batch results" # Log each result for debugging @@ -28,6 +28,7 @@ function Push-StoreMailboxPermissions { # Aggregate results by command type from all batches $AllMailboxPermissions = [System.Collections.Generic.List[object]]::new() $AllRecipientPermissions = [System.Collections.Generic.List[object]]::new() + $AllCalendarPermissions = [System.Collections.Generic.List[object]]::new() foreach ($BatchResult in $Results) { # Activity functions may return an array [hashtable, "status message"] @@ -49,17 +50,22 @@ function Push-StoreMailboxPermissions { Write-Information "Adding $($ActualResult['Get-RecipientPermission'].Count) recipient permissions" $AllRecipientPermissions.AddRange($ActualResult['Get-RecipientPermission']) } + if ($ActualResult['Get-MailboxFolderPermission']) { + Write-Information "Adding $($ActualResult['Get-MailboxFolderPermission'].Count) calendar permissions" + $AllCalendarPermissions.AddRange($ActualResult['Get-MailboxFolderPermission']) + } } else { Write-Information "Skipping non-hashtable result: $($ActualResult.GetType().Name)" } } -# Combine all permissions (mailbox and recipient) into a single collection + # Combine all permissions (mailbox and recipient) into a single collection $AllPermissions = [System.Collections.Generic.List[object]]::new() $AllPermissions.AddRange($AllMailboxPermissions) $AllPermissions.AddRange($AllRecipientPermissions) Write-Information "Aggregated $($AllPermissions.Count) total permissions ($($AllMailboxPermissions.Count) mailbox + $($AllRecipientPermissions.Count) recipient)" + Write-Information "Aggregated $($AllCalendarPermissions.Count) calendar permissions" # Store all permissions together as MailboxPermissions if ($AllPermissions.Count -gt 0) { @@ -67,7 +73,16 @@ function Push-StoreMailboxPermissions { Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'MailboxPermissions' -Data $AllPermissions -Count Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($AllPermissions.Count) mailbox permission records" -sev Info } else { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'No permissions found to cache' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'No mailbox permissions found to cache' -sev Info + } + + # Store calendar permissions separately + if ($AllCalendarPermissions.Count -gt 0) { + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'CalendarPermissions' -Data $AllCalendarPermissions + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'CalendarPermissions' -Data $AllCalendarPermissions -Count + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($AllCalendarPermissions.Count) calendar permission records" -sev Info + } else { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'No calendar permissions found to cache' -sev Info } return diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheMailboxes.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheMailboxes.ps1 index 071c910c7677..350d97a32af7 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheMailboxes.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheMailboxes.ps1 @@ -24,19 +24,19 @@ function Set-CIPPDBCacheMailboxes { Select = $Select } $Mailboxes = (New-ExoRequest @ExoRequest) | Select-Object id, ExchangeGuid, ArchiveGuid, WhenSoftDeleted, - @{ Name = 'UPN'; Expression = { $_.'UserPrincipalName' } }, - @{ Name = 'displayName'; Expression = { $_.'DisplayName' } }, - @{ Name = 'primarySmtpAddress'; Expression = { $_.'PrimarySMTPAddress' } }, - @{ Name = 'recipientType'; Expression = { $_.'RecipientType' } }, - @{ Name = 'recipientTypeDetails'; Expression = { $_.'RecipientTypeDetails' } }, - @{ Name = 'AdditionalEmailAddresses'; Expression = { ($_.'EmailAddresses' | Where-Object { $_ -clike 'smtp:*' }).Replace('smtp:', '') -join ', ' } }, - @{ Name = 'ForwardingSmtpAddress'; Expression = { $_.'ForwardingSmtpAddress' -replace 'smtp:', '' } }, - @{ Name = 'InternalForwardingAddress'; Expression = { $_.'ForwardingAddress' } }, - DeliverToMailboxAndForward, - HiddenFromAddressListsEnabled, - ExternalDirectoryObjectId, - MessageCopyForSendOnBehalfEnabled, - MessageCopyForSentAsEnabled + @{ Name = 'UPN'; Expression = { $_.'UserPrincipalName' } }, + @{ Name = 'displayName'; Expression = { $_.'DisplayName' } }, + @{ Name = 'primarySmtpAddress'; Expression = { $_.'PrimarySMTPAddress' } }, + @{ Name = 'recipientType'; Expression = { $_.'RecipientType' } }, + @{ Name = 'recipientTypeDetails'; Expression = { $_.'RecipientTypeDetails' } }, + @{ Name = 'AdditionalEmailAddresses'; Expression = { ($_.'EmailAddresses' | Where-Object { $_ -clike 'smtp:*' }).Replace('smtp:', '') -join ', ' } }, + @{ Name = 'ForwardingSmtpAddress'; Expression = { $_.'ForwardingSmtpAddress' -replace 'smtp:', '' } }, + @{ Name = 'InternalForwardingAddress'; Expression = { $_.'ForwardingAddress' } }, + DeliverToMailboxAndForward, + HiddenFromAddressListsEnabled, + ExternalDirectoryObjectId, + MessageCopyForSendOnBehalfEnabled, + MessageCopyForSentAsEnabled Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Mailboxes' -Data $Mailboxes Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Mailboxes' -Data $Mailboxes -Count @@ -54,28 +54,45 @@ function Set-CIPPDBCacheMailboxes { $MailboxCount = ($Mailboxes | Measure-Object).Count if ($MailboxCount -gt 0) { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Starting mailbox permission caching for $MailboxCount mailboxes" -sev Debug - - # Create batches of 10 mailboxes each + + # Create batches of 10 mailboxes each for both mailbox and calendar permissions $BatchSize = 10 $Batches = [System.Collections.Generic.List[object]]::new() - + $TotalBatches = [Math]::Ceiling($Mailboxes.Count / $BatchSize) + for ($i = 0; $i -lt $Mailboxes.Count; $i += $BatchSize) { $BatchMailboxes = $Mailboxes[$i..[Math]::Min($i + $BatchSize - 1, $Mailboxes.Count - 1)] - + # Only send UPN to batch function to reduce payload size $BatchMailboxUPNs = $BatchMailboxes | Select-Object -ExpandProperty UPN - + $BatchNumber = [Math]::Floor($i / $BatchSize) + 1 + + # Add mailbox permissions batch + $Batches.Add([PSCustomObject]@{ + FunctionName = 'GetMailboxPermissionsBatch' + TenantFilter = $TenantFilter + Mailboxes = $BatchMailboxUPNs + BatchNumber = $BatchNumber + TotalBatches = $TotalBatches + }) + + # Add calendar permissions batch for the same mailboxes $Batches.Add([PSCustomObject]@{ - FunctionName = 'GetMailboxPermissionsBatch' - TenantFilter = $TenantFilter - Mailboxes = $BatchMailboxUPNs - BatchNumber = [Math]::Floor($i / $BatchSize) + 1 - TotalBatches = [Math]::Ceiling($Mailboxes.Count / $BatchSize) - }) + FunctionName = 'GetCalendarPermissionsBatch' + TenantFilter = $TenantFilter + Mailboxes = $BatchMailboxUPNs + BatchNumber = $BatchNumber + TotalBatches = $TotalBatches + }) } - + + # Split batches into mailbox and calendar permissions for separate post-execution + $MailboxPermBatches = $Batches | Where-Object { $_.FunctionName -eq 'GetMailboxPermissionsBatch' } + $CalendarPermBatches = $Batches | Where-Object { $_.FunctionName -eq 'GetCalendarPermissionsBatch' } + + # Start single orchestrator for both mailbox and calendar permissions $InputObject = [PSCustomObject]@{ - Batch = $Batches + Batch = @($Batches) OrchestratorName = "MailboxPermissions_$TenantFilter" DurableMode = 'Sequence' PostExecution = @{ @@ -86,7 +103,7 @@ function Set-CIPPDBCacheMailboxes { } } Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Compress -Depth 5) - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Started mailbox permission caching orchestrator with $($Batches.Count) batches" -sev Debug + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Started mailbox and calendar permission caching orchestrator with $($Batches.Count) batches" -sev Debug } else { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'No mailboxes found to cache permissions for' -sev Debug } From c9c73204aef550792da72ce7c1fad9772cd26e95 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Tue, 20 Jan 2026 00:33:34 -0500 Subject: [PATCH 191/503] Refactor cache data collection to dynamic orchestration Push-CIPPDBCacheData.ps1 now builds a dynamic batch of cache collection tasks based on tenant license capabilities, replacing sequential static calls. The orchestration is started with a single batch, improving maintainability and extensibility. Related changes remove the 'Mailboxes' batch from Start-CIPPDBCacheOrchestrator.ps1 and update test run handling in Invoke-ExecTestRun.ps1 to align with the new orchestration approach. --- .../Push-CIPPDBCacheData.ps1 | 527 ++++++------------ .../HTTP Functions/Invoke-ExecTestRun.ps1 | 7 +- .../Start-CIPPDBCacheOrchestrator.ps1 | 9 +- 3 files changed, 173 insertions(+), 370 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 index d2c0f155009e..46d0e601541c 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 @@ -1,24 +1,22 @@ function Push-CIPPDBCacheData { <# .SYNOPSIS - Activity function to collect and cache all data for a single tenant + Orchestrator function to collect and cache all data for a single tenant .DESCRIPTION - Calls all collection functions sequentially, storing data immediately after each collection + Builds a dynamic batch of cache collection tasks based on tenant license capabilities .FUNCTIONALITY Entrypoint #> [CmdletBinding()] param($Item) - Write-Host "Starting cache collection for tenant: $($Item.TenantFilter) - Queue: $($Item.QueueName) (ID: $($Item.QueueId))" + Write-Host "Starting cache collection orchestration for tenant: $($Item.TenantFilter) - Queue: $($Item.QueueName) (ID: $($Item.QueueId))" $TenantFilter = $Item.TenantFilter - $Type = $Item.Type ?? 'Default' + $QueueId = $Item.QueueId - #This collects all data for a tenant and caches it in the CIPP Reporting database. DO NOT ADD PROCESSING OR LOGIC HERE. - #The point of this file is to always be <10 minutes execution time. try { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Starting database cache collection for tenant' -sev Info + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Starting database cache orchestration for tenant' -sev Info # Check tenant capabilities for license-specific features $IntuneCapable = Test-CIPPStandardLicense -StandardName 'IntuneLicenseCheck' -TenantFilter $TenantFilter -RequiredCapabilities @('INTUNE_A', 'MDM_Services', 'EMS', 'SCCM', 'MICROSOFTINTUNEPLAN1') -SkipLog @@ -28,365 +26,182 @@ function Push-CIPPDBCacheData { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "License capabilities - Intune: $IntuneCapable, Conditional Access: $ConditionalAccessCapable, Azure AD Premium P2: $AzureADPremiumP2Capable, Exchange: $ExchangeCapable" -sev Info - switch ($Type) { - 'Default' { - #region All Licenses - Basic tenant data collection - Write-Host 'Getting cache for Users' - try { Set-CIPPDBCacheUsers -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Users collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for Groups' - try { Set-CIPPDBCacheGroups -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Groups collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for Guests' - try { Set-CIPPDBCacheGuests -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Guests collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for ServicePrincipals' - try { Set-CIPPDBCacheServicePrincipals -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ServicePrincipals collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for Apps' - try { Set-CIPPDBCacheApps -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Apps collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for Devices' - try { Set-CIPPDBCacheDevices -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Devices collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for Organization' - try { Set-CIPPDBCacheOrganization -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Organization collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for Roles' - try { Set-CIPPDBCacheRoles -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Roles collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for AdminConsentRequestPolicy' - try { Set-CIPPDBCacheAdminConsentRequestPolicy -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "AdminConsentRequestPolicy collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for AuthorizationPolicy' - try { Set-CIPPDBCacheAuthorizationPolicy -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "AuthorizationPolicy collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for AuthenticationMethodsPolicy' - try { Set-CIPPDBCacheAuthenticationMethodsPolicy -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "AuthenticationMethodsPolicy collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for DeviceSettings' - try { Set-CIPPDBCacheDeviceSettings -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "DeviceSettings collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for DirectoryRecommendations' - try { Set-CIPPDBCacheDirectoryRecommendations -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "DirectoryRecommendations collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for CrossTenantAccessPolicy' - try { Set-CIPPDBCacheCrossTenantAccessPolicy -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "CrossTenantAccessPolicy collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for DefaultAppManagementPolicy' - try { Set-CIPPDBCacheDefaultAppManagementPolicy -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "DefaultAppManagementPolicy collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for Settings' - try { Set-CIPPDBCacheSettings -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Settings collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for SecureScore' - try { Set-CIPPDBCacheSecureScore -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "SecureScore collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for PIMSettings' - try { Set-CIPPDBCachePIMSettings -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "PIMSettings collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for Domains' - try { Set-CIPPDBCacheDomains -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Domains collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for RoleEligibilitySchedules' - try { Set-CIPPDBCacheRoleEligibilitySchedules -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "RoleEligibilitySchedules collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for RoleManagementPolicies' - try { Set-CIPPDBCacheRoleManagementPolicies -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "RoleManagementPolicies collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for RoleAssignmentScheduleInstances' - try { Set-CIPPDBCacheRoleAssignmentScheduleInstances -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "RoleAssignmentScheduleInstances collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for B2BManagementPolicy' - try { Set-CIPPDBCacheB2BManagementPolicy -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "B2BManagementPolicy collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for AuthenticationFlowsPolicy' - try { Set-CIPPDBCacheAuthenticationFlowsPolicy -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "AuthenticationFlowsPolicy collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for DeviceRegistrationPolicy' - try { Set-CIPPDBCacheDeviceRegistrationPolicy -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "DeviceRegistrationPolicy collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for CredentialUserRegistrationDetails' - try { Set-CIPPDBCacheCredentialUserRegistrationDetails -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "CredentialUserRegistrationDetails collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for UserRegistrationDetails' - try { Set-CIPPDBCacheUserRegistrationDetails -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "UserRegistrationDetails collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for OAuth2PermissionGrants' - try { Set-CIPPDBCacheOAuth2PermissionGrants -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "OAuth2PermissionGrants collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for AppRoleAssignments' - try { Set-CIPPDBCacheAppRoleAssignments -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "AppRoleAssignments collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for License Overview' - try { Set-CIPPDBCacheLicenseOverview -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "License Overview collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for MFA State' - try { Set-CIPPDBCacheMFAState -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "MFA State collection failed: $($_.Exception.Message)" -sev Error - } - #endregion All Licenses - - #region Exchange Licensed - Exchange Online features - if ($ExchangeCapable) { - Write-Host 'Getting cache for ExoAntiPhishPolicies' - try { Set-CIPPDBCacheExoAntiPhishPolicies -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoAntiPhishPolicies collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for ExoMalwareFilterPolicies' - try { Set-CIPPDBCacheExoMalwareFilterPolicies -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoMalwareFilterPolicies collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for ExoSafeLinksPolicies' - try { Set-CIPPDBCacheExoSafeLinksPolicies -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoSafeLinksPolicies collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for ExoSafeAttachmentPolicies' - try { Set-CIPPDBCacheExoSafeAttachmentPolicies -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoSafeAttachmentPolicies collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for ExoTransportRules' - try { Set-CIPPDBCacheExoTransportRules -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoTransportRules collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for ExoDkimSigningConfig' - try { Set-CIPPDBCacheExoDkimSigningConfig -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoDkimSigningConfig collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for ExoOrganizationConfig' - try { Set-CIPPDBCacheExoOrganizationConfig -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoOrganizationConfig collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for ExoAcceptedDomains' - try { Set-CIPPDBCacheExoAcceptedDomains -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoAcceptedDomains collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for ExoHostedContentFilterPolicy' - try { Set-CIPPDBCacheExoHostedContentFilterPolicy -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoHostedContentFilterPolicy collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for ExoHostedOutboundSpamFilterPolicy' - try { Set-CIPPDBCacheExoHostedOutboundSpamFilterPolicy -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoHostedOutboundSpamFilterPolicy collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for ExoAntiPhishPolicy' - try { Set-CIPPDBCacheExoAntiPhishPolicy -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoAntiPhishPolicy collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for ExoSafeLinksPolicy' - try { Set-CIPPDBCacheExoSafeLinksPolicy -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoSafeLinksPolicy collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for ExoSafeAttachmentPolicy' - try { Set-CIPPDBCacheExoSafeAttachmentPolicy -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoSafeAttachmentPolicy collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for ExoMalwareFilterPolicy' - try { Set-CIPPDBCacheExoMalwareFilterPolicy -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoMalwareFilterPolicy collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for ExoAtpPolicyForO365' - try { Set-CIPPDBCacheExoAtpPolicyForO365 -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoAtpPolicyForO365 collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for ExoQuarantinePolicy' - try { Set-CIPPDBCacheExoQuarantinePolicy -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoQuarantinePolicy collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for ExoRemoteDomain' - try { Set-CIPPDBCacheExoRemoteDomain -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoRemoteDomain collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for ExoSharingPolicy' - try { Set-CIPPDBCacheExoSharingPolicy -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoSharingPolicy collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for ExoAdminAuditLogConfig' - try { Set-CIPPDBCacheExoAdminAuditLogConfig -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoAdminAuditLogConfig collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for ExoPresetSecurityPolicy' - try { Set-CIPPDBCacheExoPresetSecurityPolicy -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoPresetSecurityPolicy collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for ExoTenantAllowBlockList' - try { Set-CIPPDBCacheExoTenantAllowBlockList -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ExoTenantAllowBlockList collection failed: $($_.Exception.Message)" -sev Error - } - } else { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Skipping Exchange Online data collection - tenant does not have required license' -sev Info - } - #endregion Exchange Licensed - - #region Conditional Access Licensed - Azure AD Premium features - if ($ConditionalAccessCapable) { - Write-Host 'Getting cache for ConditionalAccessPolicies' - try { Set-CIPPDBCacheConditionalAccessPolicies -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ConditionalAccessPolicies collection failed: $($_.Exception.Message)" -sev Error - } - } else { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Skipping Conditional Access data collection - tenant does not have required license' -sev Info - } - #endregion Conditional Access Licensed - - #region Azure AD Premium P2 - Identity Protection features - if ($AzureADPremiumP2Capable) { - Write-Host 'Getting cache for RiskyUsers' - try { Set-CIPPDBCacheRiskyUsers -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "RiskyUsers collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for RiskyServicePrincipals' - try { Set-CIPPDBCacheRiskyServicePrincipals -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "RiskyServicePrincipals collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for ServicePrincipalRiskDetections' - try { Set-CIPPDBCacheServicePrincipalRiskDetections -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ServicePrincipalRiskDetections collection failed: $($_.Exception.Message)" -sev Error - } - - Write-Host 'Getting cache for RiskDetections' - try { Set-CIPPDBCacheRiskDetections -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "RiskDetections collection failed: $($_.Exception.Message)" -sev Error - } - } else { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Skipping Azure AD Premium P2 Identity Protection data collection - tenant does not have required license' -sev Info - } - #endregion Azure AD Premium P2 - - #region Intune Licensed - Intune management features - if ($IntuneCapable) { - Write-Host 'Getting cache for ManagedDevices' - try { Set-CIPPDBCacheManagedDevices -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ManagedDevices collection failed: $($_.Exception.Message)" -sev Error - } + # Build dynamic batch of cache collection tasks based on license capabilities + $Batch = [System.Collections.Generic.List[object]]::new() + + #region All Licenses - Basic tenant data collection + $BasicCacheFunctions = @( + 'Users' + 'Groups' + 'Guests' + 'ServicePrincipals' + 'Apps' + 'Devices' + 'Organization' + 'Roles' + 'AdminConsentRequestPolicy' + 'AuthorizationPolicy' + 'AuthenticationMethodsPolicy' + 'DeviceSettings' + 'DirectoryRecommendations' + 'CrossTenantAccessPolicy' + 'DefaultAppManagementPolicy' + 'Settings' + 'SecureScore' + 'PIMSettings' + 'Domains' + 'RoleEligibilitySchedules' + 'RoleManagementPolicies' + 'RoleAssignmentScheduleInstances' + 'B2BManagementPolicy' + 'AuthenticationFlowsPolicy' + 'DeviceRegistrationPolicy' + 'CredentialUserRegistrationDetails' + 'UserRegistrationDetails' + 'OAuth2PermissionGrants' + 'AppRoleAssignments' + 'LicenseOverview' + 'MFAState' + ) + + foreach ($CacheFunction in $BasicCacheFunctions) { + $Batch.Add(@{ + FunctionName = 'ExecCIPPDBCache' + Name = $CacheFunction + TenantFilter = $TenantFilter + QueueId = $QueueId + }) + } + #endregion All Licenses + + #region Exchange Licensed - Exchange Online features + if ($ExchangeCapable) { + $ExchangeCacheFunctions = @( + 'ExoAntiPhishPolicies' + 'ExoMalwareFilterPolicies' + 'ExoSafeLinksPolicies' + 'ExoSafeAttachmentPolicies' + 'ExoTransportRules' + 'ExoDkimSigningConfig' + 'ExoOrganizationConfig' + 'ExoAcceptedDomains' + 'ExoHostedContentFilterPolicy' + 'ExoHostedOutboundSpamFilterPolicy' + 'ExoAntiPhishPolicy' + 'ExoSafeLinksPolicy' + 'ExoSafeAttachmentPolicy' + 'ExoMalwareFilterPolicy' + 'ExoAtpPolicyForO365' + 'ExoQuarantinePolicy' + 'ExoRemoteDomain' + 'ExoSharingPolicy' + 'ExoAdminAuditLogConfig' + 'ExoPresetSecurityPolicy' + 'ExoTenantAllowBlockList' + 'Mailboxes' + 'MailboxUsage' + 'OneDriveUsage' + ) + + foreach ($CacheFunction in $ExchangeCacheFunctions) { + $Batch.Add(@{ + FunctionName = 'ExecCIPPDBCache' + Name = $CacheFunction + TenantFilter = $TenantFilter + QueueId = $QueueId + }) + } + } else { + Write-Host 'Skipping Exchange Online data collection - tenant does not have required license' + } + #endregion Exchange Licensed + + #region Conditional Access Licensed - Azure AD Premium features + if ($ConditionalAccessCapable) { + $Batch.Add(@{ + FunctionName = 'ExecCIPPDBCache' + Name = 'ConditionalAccessPolicies' + TenantFilter = $TenantFilter + QueueId = $QueueId + }) + } else { + Write-Host 'Skipping Conditional Access data collection - tenant does not have required license' + } + #endregion Conditional Access Licensed + + #region Azure AD Premium P2 - Identity Protection features + if ($AzureADPremiumP2Capable) { + $P2CacheFunctions = @( + 'RiskyUsers' + 'RiskyServicePrincipals' + 'ServicePrincipalRiskDetections' + 'RiskDetections' + ) + foreach ($CacheFunction in $P2CacheFunctions) { + $Batch.Add(@{ + FunctionName = 'ExecCIPPDBCache' + Name = $CacheFunction + TenantFilter = $TenantFilter + QueueId = $QueueId + }) + } + } else { + Write-Host 'Skipping Azure AD Premium P2 Identity Protection data collection - tenant does not have required license' + } + #endregion Azure AD Premium P2 + + #region Intune Licensed - Intune management features + if ($IntuneCapable) { + $IntuneCacheFunctions = @( + 'ManagedDevices' + 'IntunePolicies' + 'ManagedDeviceEncryptionStates' + 'IntuneAppProtectionPolicies' + ) + foreach ($CacheFunction in $IntuneCacheFunctions) { + $Batch.Add(@{ + FunctionName = 'ExecCIPPDBCache' + Name = $CacheFunction + TenantFilter = $TenantFilter + QueueId = $QueueId + }) + } + } else { + Write-Host 'Skipping Intune data collection - tenant does not have required license' + } + #endregion Intune Licensed - Write-Host 'Getting cache for IntunePolicies' - try { Set-CIPPDBCacheIntunePolicies -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "IntunePolicies collection failed: $($_.Exception.Message)" -sev Error - } + Write-Information "Built batch of $($Batch.Count) cache collection activities for tenant $TenantFilter" - Write-Host 'Getting cache for ManagedDeviceEncryptionStates' - try { Set-CIPPDBCacheManagedDeviceEncryptionStates -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "ManagedDeviceEncryptionStates collection failed: $($_.Exception.Message)" -sev Error - } + # Start orchestration for this tenant's cache collection + $InputObject = [PSCustomObject]@{ + OrchestratorName = "CIPPDBCacheTenant_$TenantFilter" + Batch = @($Batch) + SkipLog = $true + } - Write-Host 'Getting cache for IntuneAppProtectionPolicies' - try { Set-CIPPDBCacheIntuneAppProtectionPolicies -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "IntuneAppProtectionPolicies collection failed: $($_.Exception.Message)" -sev Error - } - } else { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Skipping Intune data collection - tenant does not have required license' -sev Info + if ($Item.TestRun -eq $true) { + $InputObject | Add-Member -NotePropertyName PostExecution -NotePropertyValue @{ + FunctionName = 'CIPPTestsRun' + Parameters = @{ + TenantFilter = $TenantFilter } - #endregion Intune Licensed } - 'Mailboxes' { - if ($ExchangeCapable) { - Write-Host 'Getting cache for Mailboxes' - try { Set-CIPPDBCacheMailboxes -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Mailboxes collection failed: $($_.Exception.Message)" -sev Error - } + } - Write-Host 'Getting cache for MailboxUsage' - try { Set-CIPPDBCacheMailboxUsage -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "MailboxUsage collection failed: $($_.Exception.Message)" -sev Error - } + $InstanceId = Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Depth 5 -Compress) + Write-Information "Started cache collection orchestration for $TenantFilter with ID = '$InstanceId'" + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Started cache collection orchestration with $($Batch.Count) activities. Instance ID: $InstanceId" -sev Info - Write-Host 'Getting cache for OneDriveUsage' - try { Set-CIPPDBCacheOneDriveUsage -TenantFilter $TenantFilter } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "OneDriveUsage collection failed: $($_.Exception.Message)" -sev Error - } - } else { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Skipping Mailboxes data collection - tenant does not have required Exchange license' -sev Info - } - } + return @{ + InstanceId = $InstanceId + BatchCount = $Batch.Count + Message = "Cache collection orchestration started for $TenantFilter" } - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Completed database cache collection for tenant' -sev Info - } catch { $ErrorMessage = Get-CippException -Exception $_ - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to complete database cache collection: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to start cache collection orchestration: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + throw $ErrorMessage } } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ExecTestRun.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ExecTestRun.ps1 index bbf455e74581..1a228cff5dc3 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ExecTestRun.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ExecTestRun.ps1 @@ -19,17 +19,12 @@ function Invoke-ExecTestRun { TenantFilter = $TenantFilter QueueId = $Queue.RowKey QueueName = "Cache - $TenantFilter" + TestRun = $true } ) $InputObject = [PSCustomObject]@{ OrchestratorName = 'TestDataCollectionAndRun' Batch = $Batch - PostExecution = @{ - FunctionName = 'CIPPTestsRun' - Parameters = @{ - TenantFilter = $TenantFilter - } - } SkipLog = $false } diff --git a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-CIPPDBCacheOrchestrator.ps1 b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-CIPPDBCacheOrchestrator.ps1 index 51e0861ca294..8717d60bdef9 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-CIPPDBCacheOrchestrator.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-CIPPDBCacheOrchestrator.ps1 @@ -22,7 +22,7 @@ function Start-CIPPDBCacheOrchestrator { return } - $TaskCount = $TenantList.Count * 2 + $TaskCount = $TenantList.Count $Queue = New-CippQueueEntry -Name 'Database Cache Collection' -TotalTasks $TaskCount $Batch = [system.collections.generic.list[object]]::new() @@ -33,13 +33,6 @@ function Start-CIPPDBCacheOrchestrator { QueueId = $Queue.RowKey QueueName = "DB Cache - $($Tenant.defaultDomainName)" }) - $Batch.Add([PSCustomObject]@{ - FunctionName = 'CIPPDBCacheData' - TenantFilter = $Tenant.defaultDomainName - QueueId = $Queue.RowKey - Type = 'Mailboxes' - QueueName = "DB Cache Mailboxes - $($Tenant.defaultDomainName)" - }) } Write-Host "Created queue $($Queue.RowKey) for database cache collection of $($TenantList.Count) tenants" Write-Host "Starting batch of $($Batch.Count) cache collection activities" From 8f75c4d72ea2aa43c07a69b233a789b31065660a Mon Sep 17 00:00:00 2001 From: John Duprey Date: Tue, 20 Jan 2026 00:33:42 -0500 Subject: [PATCH 192/503] remove log --- .../Public/Entrypoints/HTTP Functions/Invoke-ListTests.ps1 | 1 - 1 file changed, 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ListTests.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ListTests.ps1 index 08946b093fe9..421a6a4ca38e 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ListTests.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ListTests.ps1 @@ -13,7 +13,6 @@ function Invoke-ListTests { param($Request, $TriggerMetadata) $APIName = $TriggerMetadata.FunctionName - Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -message 'Accessed this API' -Sev 'Debug' try { $TenantFilter = $Request.Query.tenantFilter ?? $Request.Body.tenantFilter From 5baa5d1024378b5a02dd9be418da26e087412b25 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Tue, 20 Jan 2026 14:26:23 +0100 Subject: [PATCH 193/503] fixes compares for some results --- Modules/CIPPCore/Public/Get-CIPPDrift.ps1 | 2 +- .../Public/Set-CIPPStandardsCompareField.ps1 | 17 +++++++++++++++++ ...ke-CIPPStandardConditionalAccessTemplate.ps1 | 2 +- 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/Modules/CIPPCore/Public/Get-CIPPDrift.ps1 b/Modules/CIPPCore/Public/Get-CIPPDrift.ps1 index bca1388b5b65..f6cb38f81a04 100644 --- a/Modules/CIPPCore/Public/Get-CIPPDrift.ps1 +++ b/Modules/CIPPCore/Public/Get-CIPPDrift.ps1 @@ -337,7 +337,7 @@ function Get-CIPPDrift { standardName = $PolicyKey standardDisplayName = "Conditional Access - $($TenantCAPolicy.displayName)" expectedValue = 'This policy only exists in the tenant, not in the template.' - receivedValue = $TenantCAPolicy | Out-String + receivedValue = (ConvertTo-Json -InputObject $TenantCAPolicy -Depth 10 -Compress) state = 'current' Status = $Status Reason = $reason diff --git a/Modules/CIPPCore/Public/Set-CIPPStandardsCompareField.ps1 b/Modules/CIPPCore/Public/Set-CIPPStandardsCompareField.ps1 index a12bd21aaefb..46fc23118c68 100644 --- a/Modules/CIPPCore/Public/Set-CIPPStandardsCompareField.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPStandardsCompareField.ps1 @@ -26,6 +26,15 @@ function Set-CIPPStandardsCompareField { return [string]$JsonValue } } + function ConvertTo-NormalizedJson { + param([string]$JsonString) + + if ([string]::IsNullOrEmpty($JsonString)) { + return $JsonString + } + $JsonString = $JsonString -replace ':"(\d+)"([,}])', ':$1$2' + return $JsonString + } if ($CurrentValue -and $CurrentValue -isnot [string]) { $CurrentValue = [string](ConvertTo-Json -InputObject $CurrentValue -Depth 10 -Compress) @@ -34,6 +43,14 @@ function Set-CIPPStandardsCompareField { $ExpectedValue = [string](ConvertTo-Json -InputObject $ExpectedValue -Depth 10 -Compress) } + # Normalize both values for consistent comparison (handle quoted numbers) + if ($CurrentValue) { + $CurrentValue = ConvertTo-NormalizedJson -JsonString $CurrentValue + } + if ($ExpectedValue) { + $ExpectedValue = ConvertTo-NormalizedJson -JsonString $ExpectedValue + } + # Handle bulk operations if ($BulkFields) { # Get all existing entities for this tenant in one query diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardConditionalAccessTemplate.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardConditionalAccessTemplate.ps1 index acdb5f3757f6..52dc8d395a92 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardConditionalAccessTemplate.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardConditionalAccessTemplate.ps1 @@ -108,7 +108,7 @@ function Invoke-CIPPStandardConditionalAccessTemplate { $templateResult = New-CIPPCATemplate -TenantFilter $tenant -JSON $CheckExististing $CompareObj = ConvertFrom-Json -ErrorAction SilentlyContinue -InputObject $templateResult try { - $Compare = Compare-CIPPIntuneObject -ReferenceObject $policy -DifferenceObject $CompareObj + $Compare = Compare-CIPPIntuneObject -ReferenceObject $policy -DifferenceObject $CompareObj } catch { Write-LogMessage -API 'Standards' -tenant $Tenant -message "Error comparing CA policy: $($_.Exception.Message)" -sev Error Set-CIPPStandardsCompareField -FieldName "standards.ConditionalAccessTemplate.$($Setting.value)" -FieldValue "Error comparing policy: $($_.Exception.Message)" -Tenant $Tenant From a8a7b6fb31827dc0f33e1609781f18415bd93581 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Tue, 20 Jan 2026 15:05:45 +0100 Subject: [PATCH 194/503] arr fix --- Modules/CIPPCore/Public/Set-CIPPStandardsCompareField.ps1 | 3 ++- .../Public/Standards/Invoke-CIPPStandardRotateDKIM.ps1 | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Modules/CIPPCore/Public/Set-CIPPStandardsCompareField.ps1 b/Modules/CIPPCore/Public/Set-CIPPStandardsCompareField.ps1 index 46fc23118c68..ab01757ac4e6 100644 --- a/Modules/CIPPCore/Public/Set-CIPPStandardsCompareField.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPStandardsCompareField.ps1 @@ -26,12 +26,13 @@ function Set-CIPPStandardsCompareField { return [string]$JsonValue } } - function ConvertTo-NormalizedJson { + function ConvertTo-NormalizedJson { param([string]$JsonString) if ([string]::IsNullOrEmpty($JsonString)) { return $JsonString } + #Replace quoted numbers with unquoted numbers for consistent comparison $JsonString = $JsonString -replace ':"(\d+)"([,}])', ':$1$2' return $JsonString } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardRotateDKIM.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardRotateDKIM.ps1 index 931795adb66b..9e9e96d44c85 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardRotateDKIM.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardRotateDKIM.ps1 @@ -79,7 +79,7 @@ function Invoke-CIPPStandardRotateDKIM { Add-CIPPBPAField -FieldName 'DKIM' -FieldValue $DKIM -StoreAs json -Tenant $tenant $CurrentValue = @{ - domainsWith1024BitDKIM = if ($DKIM) { $DKIM.Identity } else { @() } + domainsWith1024BitDKIM = @(@($DKIM.Identity) | Where-Object { $_ }) } $ExpectedValue = @{ domainsWith1024BitDKIM = @() From efe1c1a2d6afb6edf964c4638b6ca62fe181b0cc Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Tue, 20 Jan 2026 15:07:37 +0100 Subject: [PATCH 195/503] arr fix --- .../Standards/Invoke-CIPPStandardUserPreferredLanguage.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardUserPreferredLanguage.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardUserPreferredLanguage.ps1 index 5c18d9d18d94..c124c14d352e 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardUserPreferredLanguage.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardUserPreferredLanguage.ps1 @@ -80,7 +80,7 @@ function Invoke-CIPPStandardUserPreferredLanguage { $CurrentValue = @{ preferredLanguage = $preferredLanguage - incorrectUsers = $FieldValue + incorrectUsers = @($FieldValue) } $ExpectedValue = @{ preferredLanguage = $preferredLanguage From 3f915123dd53966dab6274033fa7a24a95d376b5 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Tue, 20 Jan 2026 15:31:55 +0100 Subject: [PATCH 196/503] control update --- .../Standards/Invoke-CIPPStandardSecureScoreRemediation.ps1 | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSecureScoreRemediation.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSecureScoreRemediation.ps1 index 91e18f80dc42..4534d08d73ef 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSecureScoreRemediation.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSecureScoreRemediation.ps1 @@ -181,12 +181,10 @@ function Invoke-CIPPStandardSecureScoreRemediation { }) } } - if ($ReportData.count -eq 0) { - $ReportData = $true - } + $CurrentValue = @{ - ControlsToUpdate = $ReportData + ControlsToUpdate = $ReportData ?? @() } $ExpectedValue = @{ ControlsToUpdate = @() From 4a2a9fca2f8d72228a233d6a3ff2ce6b1956522e Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Tue, 20 Jan 2026 15:34:07 +0100 Subject: [PATCH 197/503] fix reporting --- .../Standards/Invoke-CIPPStandardStaleEntraDevices.ps1 | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardStaleEntraDevices.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardStaleEntraDevices.ps1 index 87cd2d6b39e4..88deaae2c234 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardStaleEntraDevices.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardStaleEntraDevices.ps1 @@ -94,12 +94,11 @@ function Invoke-CIPPStandardStaleEntraDevices { if ($StaleDevices.Count -gt 0) { $FieldValue = $StaleDevices | Select-Object -Property displayName, id, approximateLastSignInDateTime, accountEnabled, enrollmentProfileName, operatingSystem, managementType, profileType - } else { - $FieldValue = $true } + $CurrentValue = @{ StaleDevicesCount = $StaleDevices.Count - StaleDevices = @($FieldValue) + StaleDevices = ($FieldValue ? @($FieldValue) :@()) DeviceAgeThreshold = [int]$Settings.deviceAgeThreshold } $ExpectedValue = @{ From e9a3a63c5b10bacbc858975f0aed9021465603b9 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Tue, 20 Jan 2026 15:36:46 +0100 Subject: [PATCH 198/503] fixes reporting --- .../Standards/Invoke-CIPPStandardDisableBasicAuthSMTP.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableBasicAuthSMTP.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableBasicAuthSMTP.ps1 index 5d5e4b55f0c3..adefbeb157ca 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableBasicAuthSMTP.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableBasicAuthSMTP.ps1 @@ -46,7 +46,7 @@ function Invoke-CIPPStandardDisableBasicAuthSMTP { $CurrentInfo = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-TransportConfig' $SMTPusers = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-CASMailbox' -cmdParams @{ ResultSize = 'Unlimited' } | - Where-Object { ($_.SmtpClientAuthenticationDisabled -eq $false) } + Where-Object { ($_.SmtpClientAuthenticationDisabled -eq $false) } } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the DisableBasicAuthSMTP state for $Tenant. Error: $ErrorMessage" -Sev Error @@ -110,7 +110,7 @@ function Invoke-CIPPStandardDisableBasicAuthSMTP { $CurrentValue = [PSCustomObject]@{ SmtpClientAuthenticationDisabled = $CurrentInfo.SmtpClientAuthenticationDisabled - UsersWithSmtpAuthEnabled = @($SMTPusers.PrimarySmtpAddress) + UsersWithSmtpAuthEnabled = $SMTPusers.PrimarySmtpAddress ? @($SMTPusers.PrimarySmtpAddress) : @() } $ExpectedValue = [PSCustomObject]@{ SmtpClientAuthenticationDisabled = $true From 493cb3881bdb6283ded179fa90cee91fbf4a14cb Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Tue, 20 Jan 2026 15:39:21 +0100 Subject: [PATCH 199/503] fixes reporting --- .../Standards/Invoke-CIPPStandardTransportRuleTemplate.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTransportRuleTemplate.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTransportRuleTemplate.ps1 index 9e645e6597cd..d129d05ab124 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTransportRuleTemplate.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTransportRuleTemplate.ps1 @@ -80,7 +80,7 @@ function Invoke-CIPPStandardTransportRuleTemplate { $CurrentValue = @{ DeployedTransportRules = $existingRules.DisplayName | Where-Object { $rules.displayname -contains $_ } | Sort-Object - MissingTransportRules = $MissingRules + MissingTransportRules = $MissingRules ? @($MissingRules) : @() } $ExpectedValue = @{ DeployedTransportRules = $rules.displayname | Sort-Object From 3b5927f7c28115ca51f3aefc4cd21c01843944ed Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Tue, 20 Jan 2026 16:16:19 +0100 Subject: [PATCH 200/503] improve CA handling. --- .../Public/Compare-CIPPIntuneObject.ps1 | 56 +++++++++---------- ...-CIPPStandardConditionalAccessTemplate.ps1 | 2 +- 2 files changed, 26 insertions(+), 32 deletions(-) diff --git a/Modules/CIPPCore/Public/Compare-CIPPIntuneObject.ps1 b/Modules/CIPPCore/Public/Compare-CIPPIntuneObject.ps1 index 20f5464d1bfa..616f0ef600a9 100644 --- a/Modules/CIPPCore/Public/Compare-CIPPIntuneObject.ps1 +++ b/Modules/CIPPCore/Public/Compare-CIPPIntuneObject.ps1 @@ -29,7 +29,8 @@ function Compare-CIPPIntuneObject { 'qualityUpdatesPauseStartDate', 'featureUpdatesPauseStartDate' 'wslDistributions', - 'lastSuccessfulSyncDateTime' + 'lastSuccessfulSyncDateTime', + 'tenantFilter' ) $excludeProps = $defaultExcludeProperties + $ExcludeProperties @@ -58,13 +59,6 @@ function Compare-CIPPIntuneObject { [int]$MaxDepth = 20 ) - # Check for arrays at the start of every recursive call - this catches arrays at any nesting level - $isObj1Array = $Object1 -is [Array] -or $Object1 -is [System.Collections.IList] - $isObj2Array = $Object2 -is [Array] -or $Object2 -is [System.Collections.IList] - if ($isObj1Array -or $isObj2Array) { - return - } - if ($Depth -ge $MaxDepth) { $result.Add([PSCustomObject]@{ Property = $PropertyPath @@ -97,7 +91,7 @@ function Compare-CIPPIntuneObject { } # Short-circuit recursion for primitive types - $primitiveTypes = @([double], [decimal], [datetime], [timespan], [guid] ) + $primitiveTypes = @([string], [int], [long], [bool], [double], [decimal], [datetime], [timespan], [guid] ) foreach ($type in $primitiveTypes) { if ($Object1 -is $type -and $Object2 -is $type) { if ($Object1 -ne $Object2) { @@ -166,7 +160,7 @@ function Compare-CIPPIntuneObject { if ($isObj1Array -or $isObj2Array) { return } - + # Safely get property names - ensure objects are not arrays before accessing PSObject.Properties $allPropertyNames = @() try { @@ -202,7 +196,7 @@ function Compare-CIPPIntuneObject { if ($prop1Exists -and $prop2Exists) { try { # Double-check arrays before accessing properties - if (($Object1 -is [Array] -or $Object1 -is [System.Collections.IList]) -or + if (($Object1 -is [Array] -or $Object1 -is [System.Collections.IList]) -or ($Object2 -is [Array] -or $Object2 -is [System.Collections.IList])) { continue } @@ -316,11 +310,11 @@ function Compare-CIPPIntuneObject { } $results.Add([PSCustomObject]@{ - Key = "GroupChild-$($child.settingDefinitionId)" - Label = $childLabel - Value = $childValue - Source = $Source - }) + Key = "GroupChild-$($child.settingDefinitionId)" + Label = $childLabel + Value = $childValue + Source = $Source + }) } '#microsoft.graph.deviceManagementConfigurationSimpleSettingInstance' { $childValue = $null @@ -329,11 +323,11 @@ function Compare-CIPPIntuneObject { } $results.Add([PSCustomObject]@{ - Key = "GroupChild-$($child.settingDefinitionId)" - Label = $childLabel - Value = $childValue - Source = $Source - }) + Key = "GroupChild-$($child.settingDefinitionId)" + Label = $childLabel + Value = $childValue + Source = $Source + }) } '#microsoft.graph.deviceManagementConfigurationChoiceSettingCollectionInstance' { if ($child.choiceSettingCollectionValue) { @@ -352,11 +346,11 @@ function Compare-CIPPIntuneObject { $childValue = $values -join ', ' $results.Add([PSCustomObject]@{ - Key = "GroupChild-$($child.settingDefinitionId)" - Label = $childLabel - Value = $childValue - Source = $Source - }) + Key = "GroupChild-$($child.settingDefinitionId)" + Label = $childLabel + Value = $childValue + Source = $Source + }) } } '#microsoft.graph.deviceManagementConfigurationSimpleSettingCollectionInstance' { @@ -368,11 +362,11 @@ function Compare-CIPPIntuneObject { $childValue = $values -join ', ' $results.Add([PSCustomObject]@{ - Key = "GroupChild-$($child.settingDefinitionId)" - Label = $childLabel - Value = $childValue - Source = $Source - }) + Key = "GroupChild-$($child.settingDefinitionId)" + Label = $childLabel + Value = $childValue + Source = $Source + }) } } default { diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardConditionalAccessTemplate.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardConditionalAccessTemplate.ps1 index 52dc8d395a92..495abd2cded3 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardConditionalAccessTemplate.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardConditionalAccessTemplate.ps1 @@ -108,7 +108,7 @@ function Invoke-CIPPStandardConditionalAccessTemplate { $templateResult = New-CIPPCATemplate -TenantFilter $tenant -JSON $CheckExististing $CompareObj = ConvertFrom-Json -ErrorAction SilentlyContinue -InputObject $templateResult try { - $Compare = Compare-CIPPIntuneObject -ReferenceObject $policy -DifferenceObject $CompareObj + $Compare = Compare-CIPPIntuneObject -ReferenceObject $policy -DifferenceObject $CompareObj -CompareType 'ca' } catch { Write-LogMessage -API 'Standards' -tenant $Tenant -message "Error comparing CA policy: $($_.Exception.Message)" -sev Error Set-CIPPStandardsCompareField -FieldName "standards.ConditionalAccessTemplate.$($Setting.value)" -FieldValue "Error comparing policy: $($_.Exception.Message)" -Tenant $Tenant From 978075abb5a1b81aa781cd701241bb1192281239 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Tue, 20 Jan 2026 16:21:44 +0100 Subject: [PATCH 201/503] Fixes CA compare --- .../Standards/Invoke-CIPPStandardConditionalAccessTemplate.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardConditionalAccessTemplate.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardConditionalAccessTemplate.ps1 index 495abd2cded3..51628aace755 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardConditionalAccessTemplate.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardConditionalAccessTemplate.ps1 @@ -117,7 +117,7 @@ function Invoke-CIPPStandardConditionalAccessTemplate { if (!$Compare) { Set-CIPPStandardsCompareField -FieldName "standards.ConditionalAccessTemplate.$($Setting.value)" -FieldValue $true -Tenant $Tenant } else { - Set-CIPPStandardsCompareField -FieldName "standards.ConditionalAccessTemplate.$($Setting.value)" -FieldValue $Compare -Tenant $Tenant + Set-CIPPStandardsCompareField -FieldName "standards.ConditionalAccessTemplate.$($Setting.value)" -CurrentValue $CompareObj -ExpectedValue $policy -Tenant $Tenant } } } From 4314e73295c2bba6275fdd039b5ce20492561c56 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Tue, 20 Jan 2026 10:50:40 -0500 Subject: [PATCH 202/503] Add 'On' mode to Set-CIPPAssignedPolicy Introduces an 'On' case to allow enabling a policy without assigning it to any group. This provides flexibility for scenarios where a policy should be active but not targeted to specific groups. --- Modules/CIPPCore/Public/Set-CIPPAssignedPolicy.ps1 | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Modules/CIPPCore/Public/Set-CIPPAssignedPolicy.ps1 b/Modules/CIPPCore/Public/Set-CIPPAssignedPolicy.ps1 index 72fedbbeace3..eadfeef81f39 100644 --- a/Modules/CIPPCore/Public/Set-CIPPAssignedPolicy.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPAssignedPolicy.ps1 @@ -72,6 +72,9 @@ function Set-CIPPAssignedPolicy { } ) } + 'On' { + # Do not assign to any group - used to turn on policy without assignments + } default { # Use GroupIds if provided, otherwise resolve by name $resolvedGroupIds = @() From 1aa1df286c1a4fc5575f35ad6d6427350b3e839a Mon Sep 17 00:00:00 2001 From: John Duprey Date: Tue, 20 Jan 2026 11:04:40 -0500 Subject: [PATCH 203/503] Add DateFilter support and output to log listing Introduces a DateFilter parameter when querying a single log entry and includes the DateFilter (PartitionKey) in the output objects for both single and multiple log entries. This enhances filtering and provides more context in the log results. --- .../Public/Entrypoints/Invoke-ListLogs.ps1 | 29 ++++++++++--------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/Invoke-ListLogs.ps1 b/Modules/CIPPCore/Public/Entrypoints/Invoke-ListLogs.ps1 index dd504b9700e7..8c4d4953bdf0 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Invoke-ListLogs.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Invoke-ListLogs.ps1 @@ -21,7 +21,8 @@ function Invoke-ListLogs { } } elseif ($Request.Query.logentryid) { # Return single log entry by RowKey - $Filter = "RowKey eq '{0}'" -f $Request.Query.logentryid + $DateFilter = $Request.Query.DateFilter ?? (Get-Date -UFormat '%Y%m%d') + $Filter = "RowKey eq '{0}'" -f $Request.Query.logentryid, $DateFilter $AllowedTenants = Test-CIPPAccess -Request $Request -TenantList Write-Host "Getting single log entry for RowKey: $($Request.Query.logentryid)" @@ -59,22 +60,23 @@ function Invoke-ListLogs { $Row.LogData | ConvertFrom-Json } else { $Row.LogData } [PSCustomObject]@{ - DateTime = $Row.Timestamp - Tenant = $Row.Tenant - API = $Row.API - Message = $Row.Message - User = $Row.Username - Severity = $Row.Severity - LogData = $LogData - TenantID = if ($Row.TenantID -ne $null) { + DateTime = $Row.Timestamp + Tenant = $Row.Tenant + API = $Row.API + Message = $Row.Message + User = $Row.Username + Severity = $Row.Severity + LogData = $LogData + TenantID = if ($Row.TenantID -ne $null) { $Row.TenantID } else { 'None' } - AppId = $Row.AppId - IP = $Row.IP - RowKey = $Row.RowKey - Standard = $StandardInfo + AppId = $Row.AppId + IP = $Row.IP + RowKey = $Row.RowKey + Standard = $StandardInfo + DateFilter = $Row.PartitionKey } } } @@ -164,6 +166,7 @@ function Invoke-ListLogs { IP = $Row.IP RowKey = $Row.RowKey StandardInfo = $StandardInfo + DateFilter = $Row.PartitionKey } } } From 98a3ceb0899900c69437249743565864cfeffead Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Tue, 20 Jan 2026 17:20:21 +0100 Subject: [PATCH 204/503] CA compares --- .../Invoke-CIPPStandardConditionalAccessTemplate.ps1 | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardConditionalAccessTemplate.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardConditionalAccessTemplate.ps1 index 51628aace755..95765f0dd839 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardConditionalAccessTemplate.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardConditionalAccessTemplate.ps1 @@ -117,7 +117,10 @@ function Invoke-CIPPStandardConditionalAccessTemplate { if (!$Compare) { Set-CIPPStandardsCompareField -FieldName "standards.ConditionalAccessTemplate.$($Setting.value)" -FieldValue $true -Tenant $Tenant } else { - Set-CIPPStandardsCompareField -FieldName "standards.ConditionalAccessTemplate.$($Setting.value)" -CurrentValue $CompareObj -ExpectedValue $policy -Tenant $Tenant + #this can still be prettified but is for later. + $ExpectedValue = @{ 'Differences' = @() } + $CurrentValue = @{ 'Differences' = $Compare } + Set-CIPPStandardsCompareField -FieldName "standards.ConditionalAccessTemplate.$($Setting.value)" -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -Tenant $Tenant } } } From 191e26935c7e7505b9457bd41f009ffcd98bb960 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Tue, 20 Jan 2026 11:26:53 -0500 Subject: [PATCH 205/503] Deprecate Sync-CippExtensionData and update cleanup rules Replaced the warning and return in Sync-CippExtensionData with a thrown exception to enforce deprecation. Added a new cleanup rule in Start-TableCleanup.ps1 to target scheduled tasks for Sync-CippExtensionData. --- .../Entrypoints/Timer Functions/Start-TableCleanup.ps1 | 9 +++++++++ .../Extension Functions/Sync-CippExtensionData.ps1 | 3 +-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-TableCleanup.ps1 b/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-TableCleanup.ps1 index 4989829ba7a9..35a101109294 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-TableCleanup.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-TableCleanup.ps1 @@ -56,6 +56,15 @@ function Start-TableCleanup { Property = @('PartitionKey', 'RowKey', 'ETag') } } + @{ + FunctionName = 'TableCleanupTask' + Type = 'CleanupRule' + TableName = 'ScheduledTasks' + DataTableProps = @{ + Filter = "PartitionKey eq 'ScheduledTask' and Command eq 'Sync-CippExtensionData'" + Property = @('PartitionKey', 'RowKey', 'ETag') + } + } @{ FunctionName = 'TableCleanupTask' Type = 'DeleteTable' diff --git a/Modules/CippExtensions/Public/Extension Functions/Sync-CippExtensionData.ps1 b/Modules/CippExtensions/Public/Extension Functions/Sync-CippExtensionData.ps1 index 480717d693bd..5de6978331df 100644 --- a/Modules/CippExtensions/Public/Extension Functions/Sync-CippExtensionData.ps1 +++ b/Modules/CippExtensions/Public/Extension Functions/Sync-CippExtensionData.ps1 @@ -10,8 +10,7 @@ function Sync-CippExtensionData { ) # Legacy cache system is deprecated - all extensions now use CippReportingDB - Write-Warning "Sync-CippExtensionData is deprecated. This scheduled task should be removed. Extensions now use Push-CIPPDBCacheData and Get-CippExtensionReportingData." - return + throw 'Sync-CippExtensionData is deprecated. This scheduled task should be removed. Extensions now use Push-CIPPDBCacheData and Get-CippExtensionReportingData.' $Table = Get-CIPPTable -TableName ExtensionSync $Extensions = Get-CIPPAzDataTableEntity @Table -Filter "PartitionKey eq '$($SyncType)'" From 7d5913453f1f48a3b3d17d4d48ddcae2a06ca558 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Tue, 20 Jan 2026 12:01:20 -0500 Subject: [PATCH 206/503] Improve error handling in report endpoints Added try/catch blocks to Invoke-ListmailboxPermissions and Invoke-ListMFAUsers to return proper HTTP 500 responses and error messages on failure. Updated Get-CIPPMFAStateReport and Get-CIPPMailboxPermissionReport to exclude count rows from database query results. --- .../Administration/Invoke-ListmailboxPermissions.ps1 | 10 +++++++--- .../Identity/Reports/Invoke-ListMFAUsers.ps1 | 10 ++++++++-- Modules/CIPPCore/Public/Get-CIPPMFAStateReport.ps1 | 2 +- .../Public/Get-CIPPMailboxPermissionReport.ps1 | 2 +- 4 files changed, 17 insertions(+), 7 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListmailboxPermissions.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListmailboxPermissions.ps1 index 95b4a7de57bb..a569ed418d5e 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListmailboxPermissions.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListmailboxPermissions.ps1 @@ -24,9 +24,13 @@ function Invoke-ListmailboxPermissions { if ($ByUser -eq 'true') { $ReportParams.ByUser = $true } - - $GraphRequest = Get-CIPPMailboxPermissionReport @ReportParams - $StatusCode = [HttpStatusCode]::OK + try { + $GraphRequest = Get-CIPPMailboxPermissionReport @ReportParams + $StatusCode = [HttpStatusCode]::OK + } catch { + $StatusCode = [HttpStatusCode]::InternalServerError + $GraphRequest = $_.Exception.Message + } return ([HttpResponseContext]@{ StatusCode = $StatusCode diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Reports/Invoke-ListMFAUsers.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Reports/Invoke-ListMFAUsers.ps1 index 05b050b43956..6598473b4c57 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Reports/Invoke-ListMFAUsers.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Reports/Invoke-ListMFAUsers.ps1 @@ -14,8 +14,14 @@ function Invoke-ListMFAUsers { try { # If UseReportDB is specified, retrieve from report database if ($UseReportDB -eq 'true') { - $GraphRequest = Get-CIPPMFAStateReport -TenantFilter $TenantFilter - $StatusCode = [HttpStatusCode]::OK + try { + $GraphRequest = Get-CIPPMFAStateReport -TenantFilter $TenantFilter -ErrorAction Stop + $StatusCode = [HttpStatusCode]::OK + } catch { + Write-Host "Error retrieving MFA state from report database: $($_.Exception.Message)" + $StatusCode = [HttpStatusCode]::InternalServerError + $GraphRequest = $_.Exception.Message + } return ([HttpResponseContext]@{ StatusCode = $StatusCode diff --git a/Modules/CIPPCore/Public/Get-CIPPMFAStateReport.ps1 b/Modules/CIPPCore/Public/Get-CIPPMFAStateReport.ps1 index 3595d69f5295..89815560e573 100644 --- a/Modules/CIPPCore/Public/Get-CIPPMFAStateReport.ps1 +++ b/Modules/CIPPCore/Public/Get-CIPPMFAStateReport.ps1 @@ -47,7 +47,7 @@ function Get-CIPPMFAStateReport { } # Get MFA state from reporting DB - $MFAItems = Get-CIPPDbItem -TenantFilter $TenantFilter -Type 'MFAState' + $MFAItems = Get-CIPPDbItem -TenantFilter $TenantFilter -Type 'MFAState' | Where-Object { $_.RowKey -ne 'MFAState-Count' } if (-not $MFAItems) { throw 'No MFA state data found in reporting database. Sync the report data first.' } diff --git a/Modules/CIPPCore/Public/Get-CIPPMailboxPermissionReport.ps1 b/Modules/CIPPCore/Public/Get-CIPPMailboxPermissionReport.ps1 index 7e7892398fce..389dad4ddd1d 100644 --- a/Modules/CIPPCore/Public/Get-CIPPMailboxPermissionReport.ps1 +++ b/Modules/CIPPCore/Public/Get-CIPPMailboxPermissionReport.ps1 @@ -59,7 +59,7 @@ function Get-CIPPMailboxPermissionReport { } # Get mailboxes from reporting DB - $MailboxItems = Get-CIPPDbItem -TenantFilter $TenantFilter -Type 'Mailboxes' + $MailboxItems = Get-CIPPDbItem -TenantFilter $TenantFilter -Type 'Mailboxes' | Where-Object { $_.RowKey -ne 'Mailboxes-Count' } if (-not $MailboxItems) { throw 'No mailbox data found in reporting database. Sync the mailbox permissions first. ' } From 7f88c323af59c1a8fc2933489a447764b836d427 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Tue, 20 Jan 2026 12:14:21 -0500 Subject: [PATCH 207/503] Add calendar permission report retrieval Introduces Get-CIPPCalendarPermissionReport to generate calendar permission reports from the reporting database, supporting grouping by calendar or user. Updates Invoke-ListCalendarPermissions to optionally use the report database for bulk queries, improving performance and flexibility. --- .../Invoke-ListCalendarPermissions.ps1 | 27 ++ .../Get-CIPPCalendarPermissionReport.ps1 | 235 ++++++++++++++++++ 2 files changed, 262 insertions(+) create mode 100644 Modules/CIPPCore/Public/Get-CIPPCalendarPermissionReport.ps1 diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListCalendarPermissions.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListCalendarPermissions.ps1 index 61f14eef0be4..902909f7cd64 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListCalendarPermissions.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListCalendarPermissions.ps1 @@ -11,8 +11,35 @@ Function Invoke-ListCalendarPermissions { $APIName = $Request.Params.CIPPEndpoint $UserID = $Request.Query.UserID $TenantFilter = $Request.Query.tenantFilter + $UseReportDB = $Request.Query.UseReportDB + $ByUser = $Request.Query.ByUser try { + # If UseReportDB is specified and no specific UserID, retrieve from report database + if ($UseReportDB -eq 'true' -and -not $UserID) { + + # Call the report function with proper parameters + $ReportParams = @{ + TenantFilter = $TenantFilter + } + if ($ByUser -eq 'true') { + $ReportParams.ByUser = $true + } + try { + $GraphRequest = Get-CIPPCalendarPermissionReport @ReportParams + $StatusCode = [HttpStatusCode]::OK + } catch { + $StatusCode = [HttpStatusCode]::InternalServerError + $GraphRequest = $_.Exception.Message + } + + return ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = @($GraphRequest) + }) + } + + # Original live query logic for specific user $GetCalParam = @{Identity = $UserID; FolderScope = 'Calendar' } $CalendarFolder = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-MailboxFolderStatistics' -anchor $UserID -cmdParams $GetCalParam | Select-Object -First 1 -ExcludeProperty *data.type* $CalParam = @{Identity = "$($UserID):\$($CalendarFolder.name)" } diff --git a/Modules/CIPPCore/Public/Get-CIPPCalendarPermissionReport.ps1 b/Modules/CIPPCore/Public/Get-CIPPCalendarPermissionReport.ps1 new file mode 100644 index 000000000000..4f7bd038fea4 --- /dev/null +++ b/Modules/CIPPCore/Public/Get-CIPPCalendarPermissionReport.ps1 @@ -0,0 +1,235 @@ +function Get-CIPPCalendarPermissionReport { + <# + .SYNOPSIS + Generates a calendar permission report from the CIPP Reporting database + + .DESCRIPTION + Retrieves calendar permissions for a tenant and formats them into a report. + Default view shows permissions per calendar. Use -ByUser to pivot by user. + + .PARAMETER TenantFilter + The tenant to generate the report for + + .PARAMETER ByUser + If specified, groups results by user instead of by calendar + + .EXAMPLE + Get-CIPPCalendarPermissionReport -TenantFilter 'contoso.onmicrosoft.com' + Shows which users have access to each calendar + + .EXAMPLE + Get-CIPPCalendarPermissionReport -TenantFilter 'contoso.onmicrosoft.com' -ByUser + Shows what calendars each user has access to + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter, + + [Parameter(Mandatory = $false)] + [switch]$ByUser + ) + + try { + Write-LogMessage -API 'CalendarPermissionReport' -tenant $TenantFilter -message 'Generating calendar permission report' -sev Info + + # Handle AllTenants + if ($TenantFilter -eq 'AllTenants') { + # Get all tenants that have calendar data + $AllCalendarItems = Get-CIPPDbItem -TenantFilter 'allTenants' -Type 'CalendarPermissions' + $Tenants = @($AllCalendarItems | Where-Object { $_.RowKey -ne 'CalendarPermissions-Count' } | Select-Object -ExpandProperty PartitionKey -Unique) + + $TenantList = Get-Tenants -IncludeErrors + $Tenants = $Tenants | Where-Object { $TenantList.defaultDomainName -contains $_ } + + $AllResults = [System.Collections.Generic.List[PSCustomObject]]::new() + foreach ($Tenant in $Tenants) { + try { + $TenantResults = Get-CIPPCalendarPermissionReport -TenantFilter $Tenant -ByUser:$ByUser + foreach ($Result in $TenantResults) { + # Add Tenant property to each result + $Result | Add-Member -NotePropertyName 'Tenant' -NotePropertyValue $Tenant -Force + $AllResults.Add($Result) + } + } catch { + Write-LogMessage -API 'CalendarPermissionReport' -tenant $Tenant -message "Failed to get report for tenant: $($_.Exception.Message)" -sev Warning + } + } + return $AllResults + } + + # Get mailboxes from reporting DB + $MailboxItems = Get-CIPPDbItem -TenantFilter $TenantFilter -Type 'Mailboxes' | Where-Object { $_.RowKey -ne 'Mailboxes-Count' } + if (-not $MailboxItems) { + throw 'No mailbox data found in reporting database. Sync the mailbox permissions first.' + } + + # Get the most recent mailbox cache timestamp + $MailboxCacheTimestamp = ($MailboxItems | Where-Object { $_.Timestamp } | Sort-Object Timestamp -Descending | Select-Object -First 1).Timestamp + + # Parse mailbox data and create lookup by UPN, ID, and ExternalDirectoryObjectId (case-insensitive) + $MailboxLookup = @{} + $MailboxByIdLookup = @{} + $MailboxByExternalIdLookup = @{} + foreach ($Item in $MailboxItems | Where-Object { $_.RowKey -ne 'Mailboxes-Count' }) { + $Mailbox = $Item.Data | ConvertFrom-Json + if ($Mailbox.UPN) { + $MailboxLookup[$Mailbox.UPN.ToLower()] = $Mailbox + } + if ($Mailbox.primarySmtpAddress) { + $MailboxLookup[$Mailbox.primarySmtpAddress.ToLower()] = $Mailbox + } + if ($Mailbox.Id) { + $MailboxByIdLookup[$Mailbox.Id] = $Mailbox + } + if ($Mailbox.ExternalDirectoryObjectId) { + $MailboxByExternalIdLookup[$Mailbox.ExternalDirectoryObjectId] = $Mailbox + } + } + + # Get calendar permissions from reporting DB + $PermissionItems = Get-CIPPDbItem -TenantFilter $TenantFilter -Type 'CalendarPermissions' + if (-not $PermissionItems) { + throw 'No calendar permission data found in reporting database. Run a scan first.' + } + + # Get the most recent permission cache timestamp + $PermissionCacheTimestamp = ($PermissionItems | Where-Object { $_.Timestamp } | Sort-Object Timestamp -Descending | Select-Object -First 1).Timestamp + + # Parse all permissions + $AllPermissions = [System.Collections.Generic.List[PSCustomObject]]::new() + foreach ($Item in $PermissionItems | Where-Object { $_.RowKey -ne 'CalendarPermissions-Count' }) { + $Permission = $Item.Data | ConvertFrom-Json + + # Skip Default and Anonymous permissions as they're standard and not typically relevant + if ($Permission.User -in @('Default', 'Anonymous', 'NT AUTHORITY\SELF')) { + continue + } + + # Extract the mailbox identifier from Identity (format: "mailbox-id:\Calendar" or "mailbox-upn:\Calendar") + # The Identity can contain either a GUID, UPN, or alias before the colon-backslash separator + $IdentityParts = $Permission.Identity -split ':\\' + if ($IdentityParts.Count -lt 1) { + Write-Verbose "Invalid Identity format: $($Permission.Identity)" + continue + } + $MailboxIdentifier = $IdentityParts[0] + + # Get mailbox info - try multiple match strategies + $Mailbox = $null + + # Try UPN/primarySmtpAddress lookup (case-insensitive) + $Mailbox = $MailboxLookup[$MailboxIdentifier.ToLower()] + + # If not found, try ExternalDirectoryObjectId lookup + if (-not $Mailbox) { + $Mailbox = $MailboxByExternalIdLookup[$MailboxIdentifier] + } + + # If not found, try ID lookup + if (-not $Mailbox) { + $Mailbox = $MailboxByIdLookup[$MailboxIdentifier] + } + + if (-not $Mailbox) { + Write-Verbose "No mailbox found for Identity: $MailboxIdentifier" + continue + } + + $AllPermissions.Add([PSCustomObject]@{ + MailboxUPN = if ($Mailbox.UPN) { $Mailbox.UPN } elseif ($Mailbox.primarySmtpAddress) { $Mailbox.primarySmtpAddress } else { $MailboxIdentifier } + MailboxDisplayName = $Mailbox.displayName + MailboxType = $Mailbox.recipientTypeDetails + User = $Permission.User + UserKey = if ($Permission.User -match '@') { $Permission.User.ToLower() } else { $Permission.User } + AccessRights = ($Permission.AccessRights -join ', ') + FolderName = $Permission.FolderName + }) + } + + if ($AllPermissions.Count -eq 0) { + Write-LogMessage -API 'CalendarPermissionReport' -tenant $TenantFilter -message 'No calendar permissions found (excluding Default/Anonymous)' -sev Debug + Write-Information -Message 'No calendar permissions found (excluding Default/Anonymous)' + return @() + } + + # Format results based on grouping preference + if ($ByUser) { + # Group by user - calculate which calendars each user has access to + # Use UserKey for grouping to handle case-insensitive email addresses + $Report = $AllPermissions | Group-Object -Property UserKey | ForEach-Object { + $UserKey = $_.Name + $UserDisplay = $_.Group[0].User # Use original User value for display + + # Look up the user's mailbox type using multi-strategy approach + $UserMailbox = $null + if ($UserDisplay) { + # Try UPN/primarySmtpAddress lookup (case-insensitive) + $UserMailbox = $MailboxLookup[$UserDisplay.ToLower()] + + # If not found, try ExternalDirectoryObjectId lookup + if (-not $UserMailbox) { + $UserMailbox = $MailboxByExternalIdLookup[$UserDisplay] + } + + # If not found, try ID lookup + if (-not $UserMailbox) { + $UserMailbox = $MailboxByIdLookup[$UserDisplay] + } + } + $UserMailboxType = if ($UserMailbox) { $UserMailbox.recipientTypeDetails } else { 'Unknown' } + + # Build detailed permissions list with calendar and access rights + $PermissionDetails = @($_.Group | ForEach-Object { + [PSCustomObject]@{ + Calendar = $_.MailboxDisplayName + CalendarUPN = $_.MailboxUPN + AccessRights = $_.AccessRights + } + }) + + [PSCustomObject]@{ + User = $UserDisplay + UserMailboxType = $UserMailboxType + CalendarCount = $_.Count + Permissions = $PermissionDetails + Tenant = $TenantFilter + MailboxCacheTimestamp = $MailboxCacheTimestamp + PermissionCacheTimestamp = $PermissionCacheTimestamp + } + } | Sort-Object User + } else { + # Default: Group by calendar + $Report = $AllPermissions | Group-Object -Property MailboxUPN | ForEach-Object { + $CalendarUPN = $_.Name + $CalendarInfo = $_.Group[0] + + # Build detailed permissions list with user and access rights + $PermissionDetails = @($_.Group | ForEach-Object { + [PSCustomObject]@{ + User = $_.User + AccessRights = $_.AccessRights + } + }) + + [PSCustomObject]@{ + CalendarUPN = $CalendarUPN + CalendarDisplayName = $CalendarInfo.MailboxDisplayName + CalendarType = $CalendarInfo.MailboxType + PermissionCount = $_.Count + Permissions = $PermissionDetails + Tenant = $TenantFilter + MailboxCacheTimestamp = $MailboxCacheTimestamp + PermissionCacheTimestamp = $PermissionCacheTimestamp + } + } | Sort-Object CalendarDisplayName + } + + Write-LogMessage -API 'CalendarPermissionReport' -tenant $TenantFilter -message "Generated report with $($Report.Count) entries" -sev Debug + return $Report + + } catch { + Write-LogMessage -API 'CalendarPermissionReport' -tenant $TenantFilter -message "Failed to generate calendar permission report: $($_.Exception.Message)" -sev Error -LogData (Get-CippException -Exception $_) + throw "Failed to generate calendar permission report: $($_.Exception.Message)" + } +} From 9f77cf52ed75bb16c0c553a5ae81a3bf3f4cdcbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Tue, 20 Jan 2026 18:32:31 +0100 Subject: [PATCH 208/503] fix: update tenant data check to fix unhandled exception - Fixes running the reports for singular tenants - ERROR: Cannot process argument because the value of argument "Property" is not valid. Change the value of the "Property" argument and run the operation again. --- .../Activity Triggers/Tests/Invoke-CIPPTestsRun.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Tests/Invoke-CIPPTestsRun.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Tests/Invoke-CIPPTestsRun.ps1 index 9cc264d9af27..af97857c8e67 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Tests/Invoke-CIPPTestsRun.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Tests/Invoke-CIPPTestsRun.ps1 @@ -31,7 +31,7 @@ function Invoke-CIPPTestsRun { $TenantsWithData } else { $DbCounts = Get-CIPPDbItem -TenantFilter $TenantFilter -CountsOnly - if (($DbCounts | Measure-Object -Property Count -Sum).Sum -gt 0) { + if (($DbCounts | Measure-Object -Property DataCount -Sum).Sum -gt 0) { @($TenantFilter) } else { Write-LogMessage -API 'Tests' -tenant $TenantFilter -message 'Tenant has no data in database. Skipping tests.' -sev Info @@ -55,7 +55,7 @@ function Invoke-CIPPTestsRun { } } - Write-Information "Built batch of $($Batch.Count) test activities ($($AllTests.Count) tests × $($AllTenantsList.Count) tenants)" + Write-Information "Built batch of $($Batch.Count) test activities ($($AllTests.Count) tests x $($AllTenantsList.Count) tenants)" $InputObject = [PSCustomObject]@{ OrchestratorName = 'TestsRun' From 2adc33f6ab940c19ffb1fab675544d8c080bc590 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Tue, 20 Jan 2026 15:13:53 -0500 Subject: [PATCH 209/503] Include OpenIdConfig in external tenant info response Refactored to store the OpenId configuration in a variable and added it to the HTTP response body. This provides more context about the tenant's OpenId configuration in the API response. --- .../Public/Entrypoints/Invoke-ListExternalTenantInfo.ps1 | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/Invoke-ListExternalTenantInfo.ps1 b/Modules/CIPPCore/Public/Entrypoints/Invoke-ListExternalTenantInfo.ps1 index 31844a48a08d..1bf2c4bfaabc 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Invoke-ListExternalTenantInfo.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Invoke-ListExternalTenantInfo.ps1 @@ -17,13 +17,15 @@ function Invoke-ListExternalTenantInfo { $Tenant = $Request.Query.tenant # Normalize to tenantid and determine if tenant exists - $TenantId = (Invoke-RestMethod -Method GET "https://login.windows.net/$Tenant/.well-known/openid-configuration").token_endpoint.Split('/')[3] + $OpenIdConfig = Invoke-RestMethod -Method GET "https://login.windows.net/$Tenant/.well-known/openid-configuration" + $TenantId = $OpenIdConfig.token_endpoint.Split('/')[3] if ($TenantId) { $GraphRequest = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/tenantRelationships/findTenantInformationByTenantId(tenantId='$TenantId')" -NoAuthCheck $true -tenantid $env:TenantID $StatusCode = [HttpStatusCode]::OK $HttpResponse.Body = [PSCustomObject]@{ GraphRequest = $GraphRequest + OpenIdConfig = $OpenIdConfig } } else { $HttpResponse.StatusCode = [HttpStatusCode]::BadRequest From 1bcce8319cb3f59dc4976dc67d4c9bbef21ff561 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Tue, 20 Jan 2026 21:18:57 +0100 Subject: [PATCH 210/503] fix mx alert so that cache data is stored before trace is written. --- .../Public/Alerts/Get-CIPPAlertMXRecordChanged.ps1 | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertMXRecordChanged.ps1 b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertMXRecordChanged.ps1 index f2c268a4ed75..31811b78ed68 100644 --- a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertMXRecordChanged.ps1 +++ b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertMXRecordChanged.ps1 @@ -24,11 +24,6 @@ function Get-CIPPAlertMXRecordChanged { "$($Domain.Domain): MX records changed from [$($PreviousRecords -join ', ')] to [$($CurrentRecords -join ', ')]" } } - - if ($ChangedDomains) { - Write-AlertTrace -cmdletName $MyInvocation.MyCommand -tenantFilter $TenantFilter -data $ChangedDomains - } - # Update cache with current data foreach ($Domain in $DomainData) { $CurrentRecords = $Domain.ActualMXRecords.Hostname | Sort-Object @@ -42,6 +37,12 @@ function Get-CIPPAlertMXRecordChanged { } Add-CIPPAzDataTableEntity @CacheTable -Entity $CacheEntity -Force } + + if ($ChangedDomains) { + Write-AlertTrace -cmdletName $MyInvocation.MyCommand -tenantFilter $TenantFilter -data $ChangedDomains + } + return $true + } catch { Write-LogMessage -message "Failed to check MX record changes: $($_.Exception.Message)" -API 'MX Record Alert' -tenant $TenantFilter -sev Error } From 8a6a648b58a38436f3435af667eb674419393243 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Tue, 20 Jan 2026 20:10:08 +0100 Subject: [PATCH 211/503] feat: dedupe licenses and add in new ones --- Config/ExcludeSkuList.JSON | 144 ++++++++----------------------------- 1 file changed, 28 insertions(+), 116 deletions(-) diff --git a/Config/ExcludeSkuList.JSON b/Config/ExcludeSkuList.JSON index e2c80e82a1e0..ec0dfb32d4a9 100644 --- a/Config/ExcludeSkuList.JSON +++ b/Config/ExcludeSkuList.JSON @@ -3,42 +3,10 @@ "GUID": "90d8b3f8-712e-4f7b-aa1e-62e7ae6cbe96", "Product_Display_Name": "Business Apps (free)" }, - { - "GUID": "90d8b3f8-712e-4f7b-aa1e-62e7ae6cbe96", - "Product_Display_Name": "Business Apps (free)" - }, - { - "GUID": "f30db892-07e9-47e9-837c-80727f46fd3d", - "Product_Display_Name": "MICROSOFT FLOW FREE" - }, { "GUID": "f30db892-07e9-47e9-837c-80727f46fd3d", "Product_Display_Name": "MICROSOFT FLOW FREE" }, - { - "GUID": "f30db892-07e9-47e9-837c-80727f46fd3d", - "Product_Display_Name": "MICROSOFT FLOW FREE" - }, - { - "GUID": "16ddbbfc-09ea-4de2-b1d7-312db6112d70", - "Product_Display_Name": "MICROSOFT TEAMS (FREE)" - }, - { - "GUID": "16ddbbfc-09ea-4de2-b1d7-312db6112d70", - "Product_Display_Name": "MICROSOFT TEAMS (FREE)" - }, - { - "GUID": "16ddbbfc-09ea-4de2-b1d7-312db6112d70", - "Product_Display_Name": "MICROSOFT TEAMS (FREE)" - }, - { - "GUID": "16ddbbfc-09ea-4de2-b1d7-312db6112d70", - "Product_Display_Name": "MICROSOFT TEAMS (FREE)" - }, - { - "GUID": "16ddbbfc-09ea-4de2-b1d7-312db6112d70", - "Product_Display_Name": "MICROSOFT TEAMS (FREE)" - }, { "GUID": "16ddbbfc-09ea-4de2-b1d7-312db6112d70", "Product_Display_Name": "MICROSOFT TEAMS (FREE)" @@ -47,10 +15,6 @@ "GUID": "a403ebcc-fae0-4ca2-8c8c-7a907fd6c235", "Product_Display_Name": "Power BI (free)" }, - { - "GUID": "a403ebcc-fae0-4ca2-8c8c-7a907fd6c235", - "Product_Display_Name": "Power BI (free)" - }, { "GUID": "61e6bd70-fbdb-4deb-82ea-912842f39431", "Product_Display_Name": "Dynamics 365 Customer Service Insights Trial" @@ -59,26 +23,6 @@ "GUID": "bc946dac-7877-4271-b2f7-99d2db13cd2c", "Product_Display_Name": "Dynamics 365 Customer Voice Trial" }, - { - "GUID": "bc946dac-7877-4271-b2f7-99d2db13cd2c", - "Product_Display_Name": "Dynamics 365 Customer Voice Trial" - }, - { - "GUID": "bc946dac-7877-4271-b2f7-99d2db13cd2c", - "Product_Display_Name": "Dynamics 365 Customer Voice Trial" - }, - { - "GUID": "bc946dac-7877-4271-b2f7-99d2db13cd2c", - "Product_Display_Name": "Dynamics 365 Customer Voice Trial" - }, - { - "GUID": "bc946dac-7877-4271-b2f7-99d2db13cd2c", - "Product_Display_Name": "Dynamics 365 Customer Voice Trial" - }, - { - "GUID": "338148b6-1b11-4102-afb9-f92b6cdc0f8d", - "Product_Display_Name": "DYNAMICS 365 P1 TRIAL FOR INFORMATION WORKERS" - }, { "GUID": "338148b6-1b11-4102-afb9-f92b6cdc0f8d", "Product_Display_Name": "DYNAMICS 365 P1 TRIAL FOR INFORMATION WORKERS" @@ -87,26 +31,6 @@ "GUID": "fcecd1f9-a91e-488d-a918-a96cdb6ce2b0", "Product_Display_Name": "Microsoft Dynamics AX7 User Trial" }, - { - "GUID": "fcecd1f9-a91e-488d-a918-a96cdb6ce2b0", - "Product_Display_Name": "Microsoft Dynamics AX7 User Trial" - }, - { - "GUID": "dcb1a3ae-b33f-4487-846a-a640262fadf4", - "Product_Display_Name": "Microsoft Power Apps Plan 2 Trial" - }, - { - "GUID": "dcb1a3ae-b33f-4487-846a-a640262fadf4", - "Product_Display_Name": "Microsoft Power Apps Plan 2 Trial" - }, - { - "GUID": "dcb1a3ae-b33f-4487-846a-a640262fadf4", - "Product_Display_Name": "Microsoft Power Apps Plan 2 Trial" - }, - { - "GUID": "dcb1a3ae-b33f-4487-846a-a640262fadf4", - "Product_Display_Name": "Microsoft Power Apps Plan 2 Trial" - }, { "GUID": "dcb1a3ae-b33f-4487-846a-a640262fadf4", "Product_Display_Name": "Microsoft Power Apps Plan 2 Trial" @@ -116,71 +40,59 @@ "Product_Display_Name": "Microsoft Teams Trial" }, { - "GUID": "74fbf1bb-47c6-4796-9623-77dc7371723b", - "Product_Display_Name": "Microsoft Teams Trial" - }, - { - "GUID": "74fbf1bb-47c6-4796-9623-77dc7371723b", - "Product_Display_Name": "Microsoft Teams Trial" - }, - { - "GUID": "74fbf1bb-47c6-4796-9623-77dc7371723b", - "Product_Display_Name": "Microsoft Teams Trial" - }, - { - "GUID": "74fbf1bb-47c6-4796-9623-77dc7371723b", - "Product_Display_Name": "Microsoft Teams Trial" + "GUID": "606b54a9-78d8-4298-ad8b-df6ef4481c80", + "Product_Display_Name": "Power Virtual Agents Viral Trial" }, { - "GUID": "74fbf1bb-47c6-4796-9623-77dc7371723b", - "Product_Display_Name": "Microsoft Teams Trial" + "GUID": "1f2f344a-700d-42c9-9427-5cea1d5d7ba6", + "Product_Display_Name": "MICROSOFT STREAM" }, { - "GUID": "74fbf1bb-47c6-4796-9623-77dc7371723b", - "Product_Display_Name": "Microsoft Teams Trial" + "GUID": "6470687e-a428-4b7a-bef2-8a291ad947c9", + "Product_Display_Name": "WINDOWS STORE FOR BUSINESS" }, { - "GUID": "74fbf1bb-47c6-4796-9623-77dc7371723b", - "Product_Display_Name": "Microsoft Teams Trial" + "GUID": "710779e8-3d4a-4c88-adb9-386c958d1fdf", + "Product_Display_Name": "MICROSOFT TEAMS EXPLORATORY" }, { - "GUID": "74fbf1bb-47c6-4796-9623-77dc7371723b", - "Product_Display_Name": "Microsoft Teams Trial" + "GUID": "8c4ce438-32a7-4ac5-91a6-e22ae08d9c8b", + "Product_Display_Name": "Rights Management Adhoc" }, { - "GUID": "74fbf1bb-47c6-4796-9623-77dc7371723b", - "Product_Display_Name": "Microsoft Teams Trial" + "GUID": "5b631642-bd26-49fe-bd20-1daaa972ef80", + "Product_Display_Name": "Microsoft Power Apps for Developer" }, { - "GUID": "606b54a9-78d8-4298-ad8b-df6ef4481c80", - "Product_Display_Name": "Power Virtual Agents Viral Trial" + "GUID": "6a4a1628-9b9a-424d-bed5-4118f0ede3fd", + "Product_Display_Name": "Dynamics 365 Business Central for IWs" }, { - "GUID": "606b54a9-78d8-4298-ad8b-df6ef4481c80", - "Product_Display_Name": "Power Virtual Agents Viral Trial" + "GUID": "6ec92958-3cc1-49db-95bd-bc6b3798df71", + "Product_Display_Name": "Dynamics 365 Sales Premium Viral Trial" }, { - "GUID": "606b54a9-78d8-4298-ad8b-df6ef4481c80", - "Product_Display_Name": "Power Virtual Agents Viral Trial" + "GUID": "3f9f06f5-3c31-472c-985f-62d9c10ec167", + "Product_Display_Name": "Power Pages vTrial for Makers" }, { - "GUID": "1f2f344a-700d-42c9-9427-5cea1d5d7ba6", - "Product_Display_Name": "MICROSOFT STREAM" + "GUID": "9c7bff7a-3715-4da7-88d3-07f57f8d0fb6", + "Product_Display_Name": "Dynamics 365 For Sales Professional Trial" }, { - "GUID": "1f2f344a-700d-42c9-9427-5cea1d5d7ba6", - "Product_Display_Name": "MICROSOFT STREAM" + "GUID": "8f0c5670-4e56-4892-b06d-91c085d7004f", + "Product_Display_Name": "App Connect IW" }, { - "GUID": "6470687e-a428-4b7a-bef2-8a291ad947c9", - "Product_Display_Name": "WINDOWS STORE FOR BUSINESS" + "GUID": "87bbbc60-4754-4998-8c88-227dca264858", + "Product_Display_Name": "Power Apps and Logic Flows" }, { - "GUID": "6470687e-a428-4b7a-bef2-8a291ad947c9", - "Product_Display_Name": "WINDOWS STORE FOR BUSINESS" + "GUID": "e5788282-6381-469f-84f0-3d7d4021d34d", + "Product_Display_Name": "Office 365 Extra File Storage for GCC" }, { - "GUID": "710779e8-3d4a-4c88-adb9-386c958d1fdf", - "Product_Display_Name": "MICROSOFT TEAMS EXPLORATORY" + "GUID": "99049c9c-6011-4908-bf17-15f496e6519d", + "Product_Display_Name": "Office 365 Extra File Storage" } ] From 06ce65a582232fd45a41898b4205ddb378662971 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Tue, 20 Jan 2026 20:30:42 +0100 Subject: [PATCH 212/503] feat: initialize excluded licenses from config - Update Invoke-ExecExcludeLicenses to load excluded licenses from a JSON config file when the count is low. - Clean up logging and variable usage --- .../Settings/Invoke-ExecExcludeLicenses.ps1 | 42 ++++++++++++------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecExcludeLicenses.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecExcludeLicenses.ps1 index 26d3db6edf2f..9486c792e12d 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecExcludeLicenses.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecExcludeLicenses.ps1 @@ -1,4 +1,4 @@ -Function Invoke-ExecExcludeLicenses { +function Invoke-ExecExcludeLicenses { <# .FUNCTIONALITY Entrypoint @@ -9,14 +9,20 @@ Function Invoke-ExecExcludeLicenses { param($Request, $TriggerMetadata) $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers $Table = Get-CIPPTable -TableName ExcludedLicenses try { if ($Request.Query.List) { $Rows = Get-CIPPAzDataTableEntity @Table + + # If no excluded licenses exist, initialize from config file if ($Rows.Count -lt 1) { - $TableBaseData = '[{"GUID":"16ddbbfc-09ea-4de2-b1d7-312db6112d70","Product_Display_Name":"MICROSOFT TEAMS (FREE)"},{"GUID":"1f2f344a-700d-42c9-9427-5cea1d5d7ba6","Product_Display_Name":"MICROSOFT STREAM"},{"GUID":"338148b6-1b11-4102-afb9-f92b6cdc0f8d","Product_Display_Name":"DYNAMICS 365 P1 TRIAL FOR INFORMATION WORKERS"},{"GUID":"606b54a9-78d8-4298-ad8b-df6ef4481c80","Product_Display_Name":"Power Virtual Agents Viral Trial"},{"GUID":"61e6bd70-fbdb-4deb-82ea-912842f39431","Product_Display_Name":"Dynamics 365 Customer Service Insights Trial"},{"GUID":"6470687e-a428-4b7a-bef2-8a291ad947c9","Product_Display_Name":"WINDOWS STORE FOR BUSINESS"},{"GUID":"710779e8-3d4a-4c88-adb9-386c958d1fdf","Product_Display_Name":"MICROSOFT TEAMS EXPLORATORY"},{"GUID":"74fbf1bb-47c6-4796-9623-77dc7371723b","Product_Display_Name":"Microsoft Teams Trial"},{"GUID":"90d8b3f8-712e-4f7b-aa1e-62e7ae6cbe96","Product_Display_Name":"Business Apps (free)"},{"GUID":"a403ebcc-fae0-4ca2-8c8c-7a907fd6c235","Product_Display_Name":"Power BI (free)"},{"GUID":"bc946dac-7877-4271-b2f7-99d2db13cd2c","Product_Display_Name":"Dynamics 365 Customer Voice Trial"},{"GUID":"dcb1a3ae-b33f-4487-846a-a640262fadf4","Product_Display_Name":"Microsoft Power Apps Plan 2 Trial"},{"GUID":"f30db892-07e9-47e9-837c-80727f46fd3d","Product_Display_Name":"MICROSOFT FLOW FREE"},{"GUID":"fcecd1f9-a91e-488d-a918-a96cdb6ce2b0","Product_Display_Name":"Microsoft Dynamics AX7 User Trial"}]' | ConvertFrom-Json -AsHashtable -Depth 10 - $TableRows = foreach ($Row in $TableBaseData) { + Write-Information "Excluded licenses count is low ($($Rows.Count)). Initializing from config file." + $CIPPCoreModuleRoot = Get-Module -Name CIPPCore | Select-Object -ExpandProperty ModuleBase + $CIPPRoot = (Get-Item $CIPPCoreModuleRoot).Parent.Parent + $TableBaseData = Get-Content -Path (Join-Path $CIPPRoot 'Config\ExcludeSkuList.JSON') -Raw | ConvertFrom-Json -AsHashtable -Depth 10 + $null = foreach ($Row in $TableBaseData) { $Row.PartitionKey = 'License' $Row.RowKey = $Row.GUID @@ -25,40 +31,44 @@ Function Invoke-ExecExcludeLicenses { $Rows = Get-CIPPAzDataTableEntity @Table - Write-LogMessage -API $APINAME -headers $Request.Headers -message 'got excluded licenses list' -Sev 'Info' + Write-LogMessage -API $APIName -headers $Headers -message "Initialized $($TableBaseData.Count) excluded licenses from config file" -Sev 'Info' } $body = @($Rows) } # Interact with query parameters or the body of the request. - $name = $Request.Query.TenantFilter + $GUID = $Request.Body.GUID + $DisplayName = $Request.Body.SKUName if ($Request.Query.AddExclusion) { $AddObject = @{ PartitionKey = 'License' - RowKey = $Request.body.GUID - 'GUID' = $Request.body.GUID - 'Product_Display_Name' = $request.body.SKUName + RowKey = $GUID + 'GUID' = $GUID + 'Product_Display_Name' = $DisplayName } Add-CIPPAzDataTableEntity @Table -Entity $AddObject -Force - Write-LogMessage -API $APINAME -headers $Request.Headers -message "Added exclusion $($request.body.SKUName)" -Sev 'Info' - $body = [pscustomobject]@{'Results' = "Success. We've added $($request.body.SKUName) to the excluded list." } + Write-LogMessage -API $APIName -headers $Headers -message "Added exclusion $DisplayName" -Sev 'Info' + $body = [pscustomobject]@{'Results' = "Success. We've added $DisplayName to the excluded list." } } if ($Request.Query.RemoveExclusion) { - $Filter = "RowKey eq '{0}' and PartitionKey eq 'License'" -f $Request.Body.GUID + $Filter = "RowKey eq '{0}' and PartitionKey eq 'License'" -f $GUID $Entity = Get-CIPPAzDataTableEntity @Table -Filter $Filter -Property PartitionKey, RowKey Remove-AzDataTableEntity -Force @Table -Entity $Entity - Write-LogMessage -API $APINAME -headers $Request.Headers -message "Removed exclusion $($Request.Query.GUID)" -Sev 'Info' - $body = [pscustomobject]@{'Results' = "Success. We've removed $($Request.query.guid) from the excluded list." } + Write-LogMessage -API $APIName -headers $Headers -message "Removed exclusion $GUID" -Sev 'Info' + $body = [pscustomobject]@{'Results' = "Success. We've removed $GUID from the excluded list." } } } catch { - Write-LogMessage -API $APINAME -headers $Request.Headers -message "Exclusion API failed. $($_.Exception.Message)" -Sev 'Error' - $body = [pscustomobject]@{'Results' = "Failed. $($_.Exception.Message)" } + $ErrorMessage = Get-CippException -Exception $_ + $StatusCode = [HttpStatusCode]::InternalServerError + $Result = "Failed to process exclusion request. $($ErrorMessage.NormalizedError)" + Write-LogMessage -API $APIName -headers $Headers -message $Result -Sev 'Error' -LogData $ErrorMessage + $body = [pscustomobject]@{'Results' = $Result } } return ([HttpResponseContext]@{ - StatusCode = [HttpStatusCode]::OK + StatusCode = $StatusCode ?? [HttpStatusCode]::OK Body = $body }) From b768b5ed7dfdb180d7cc1090d59eb8d650a08d18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Tue, 20 Jan 2026 23:06:20 +0100 Subject: [PATCH 213/503] feat: refactor license exclusion management - Refactor Invoke-ExecExcludeLicenses function to only manage excluded licenses. - Introduce action for restoring default exclusions. - Split list functionality into Invoke-ListExcludedLicenses function. - Implement Initialize-CIPPExcludedLicenses function to populate excluded licenses from config instead of hardcoded variable. --- .../Settings/Invoke-ExecExcludeLicenses.ps1 | 76 +++++++-------- .../Settings/Invoke-ListExcludedLicenses.ps1 | 37 ++++++++ .../Tools/Initialize-CIPPExcludedLicenses.ps1 | 93 +++++++++++++++++++ 3 files changed, 165 insertions(+), 41 deletions(-) create mode 100644 Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ListExcludedLicenses.ps1 create mode 100644 Modules/CIPPCore/Public/Tools/Initialize-CIPPExcludedLicenses.ps1 diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecExcludeLicenses.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecExcludeLicenses.ps1 index 9486c792e12d..0c44628dbac1 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecExcludeLicenses.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecExcludeLicenses.ps1 @@ -11,65 +11,59 @@ function Invoke-ExecExcludeLicenses { $APIName = $Request.Params.CIPPEndpoint $Headers = $Request.Headers $Table = Get-CIPPTable -TableName ExcludedLicenses - try { - - if ($Request.Query.List) { - $Rows = Get-CIPPAzDataTableEntity @Table - # If no excluded licenses exist, initialize from config file - if ($Rows.Count -lt 1) { - Write-Information "Excluded licenses count is low ($($Rows.Count)). Initializing from config file." - $CIPPCoreModuleRoot = Get-Module -Name CIPPCore | Select-Object -ExpandProperty ModuleBase - $CIPPRoot = (Get-Item $CIPPCoreModuleRoot).Parent.Parent - $TableBaseData = Get-Content -Path (Join-Path $CIPPRoot 'Config\ExcludeSkuList.JSON') -Raw | ConvertFrom-Json -AsHashtable -Depth 10 - $null = foreach ($Row in $TableBaseData) { - $Row.PartitionKey = 'License' - $Row.RowKey = $Row.GUID + # Interact with query parameters or the body of the request. + try { + $Action = $Request.Body.Action + $GUID = $Request.Body.GUID + $DisplayName = $Request.Body.SKUName - Add-CIPPAzDataTableEntity @Table -Entity ([pscustomobject]$Row) -Force | Out-Null + switch ($Action) { + 'AddExclusion' { + $AddObject = @{ + PartitionKey = 'License' + RowKey = $GUID + 'GUID' = $GUID + 'Product_Display_Name' = $DisplayName } + Add-CIPPAzDataTableEntity @Table -Entity $AddObject -Force + $Result = "Success. Added $DisplayName($GUID) to the excluded licenses list." + Write-LogMessage -API $APIName -headers $Headers -message $Result -Sev 'Info' - $Rows = Get-CIPPAzDataTableEntity @Table - - Write-LogMessage -API $APIName -headers $Headers -message "Initialized $($TableBaseData.Count) excluded licenses from config file" -Sev 'Info' } - $body = @($Rows) - } + 'RemoveExclusion' { + $Filter = "RowKey eq '{0}' and PartitionKey eq 'License'" -f $GUID + $Entity = Get-CIPPAzDataTableEntity @Table -Filter $Filter -Property PartitionKey, RowKey + Remove-AzDataTableEntity -Force @Table -Entity $Entity + $Result = "Success. Removed $DisplayName($GUID) from the excluded licenses list." + Write-LogMessage -API $APIName -headers $Headers -message $Result -Sev 'Info' - # Interact with query parameters or the body of the request. - $GUID = $Request.Body.GUID - $DisplayName = $Request.Body.SKUName - if ($Request.Query.AddExclusion) { - $AddObject = @{ - PartitionKey = 'License' - RowKey = $GUID - 'GUID' = $GUID - 'Product_Display_Name' = $DisplayName } - Add-CIPPAzDataTableEntity @Table -Entity $AddObject -Force + 'RestoreDefaults' { + $FullReset = [bool]$Request.Body.FullReset + if ($FullReset) { + $InitResult = Initialize-CIPPExcludedLicenses -Force -Headers $Headers -APIName $APIName + } else { + $InitResult = Initialize-CIPPExcludedLicenses -Headers $Headers -APIName $APIName + } + $Result = $InitResult.Message - Write-LogMessage -API $APIName -headers $Headers -message "Added exclusion $DisplayName" -Sev 'Info' - $body = [pscustomobject]@{'Results' = "Success. We've added $DisplayName to the excluded list." } + } + default { + $StatusCode = [HttpStatusCode]::BadRequest + $Result = "Invalid action specified: $Action" + } } - if ($Request.Query.RemoveExclusion) { - $Filter = "RowKey eq '{0}' and PartitionKey eq 'License'" -f $GUID - $Entity = Get-CIPPAzDataTableEntity @Table -Filter $Filter -Property PartitionKey, RowKey - Remove-AzDataTableEntity -Force @Table -Entity $Entity - Write-LogMessage -API $APIName -headers $Headers -message "Removed exclusion $GUID" -Sev 'Info' - $body = [pscustomobject]@{'Results' = "Success. We've removed $GUID from the excluded list." } - } } catch { $ErrorMessage = Get-CippException -Exception $_ $StatusCode = [HttpStatusCode]::InternalServerError $Result = "Failed to process exclusion request. $($ErrorMessage.NormalizedError)" Write-LogMessage -API $APIName -headers $Headers -message $Result -Sev 'Error' -LogData $ErrorMessage - $body = [pscustomobject]@{'Results' = $Result } } return ([HttpResponseContext]@{ StatusCode = $StatusCode ?? [HttpStatusCode]::OK - Body = $body + Body = [pscustomobject]@{ 'Results' = $Result } }) - } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ListExcludedLicenses.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ListExcludedLicenses.ps1 new file mode 100644 index 000000000000..764cbdc2ce79 --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ListExcludedLicenses.ps1 @@ -0,0 +1,37 @@ +function Invoke-ListExcludedLicenses { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + CIPP.AppSettings.Read + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + + try { + $Table = Get-CIPPTable -TableName ExcludedLicenses + $Rows = Get-CIPPAzDataTableEntity @Table + + # If no excluded licenses exist, initialize them + if ($Rows.Count -lt 1) { + Write-Information 'Excluded licenses table is empty. Initializing from config file.' + $null = Initialize-CIPPExcludedLicenses -Headers $Headers -APIName $APIName + $Rows = Get-CIPPAzDataTableEntity @Table + } + + $Results = @($Rows) + } catch { + $ErrorMessage = Get-CippException -Exception $_ + $StatusCode = [HttpStatusCode]::InternalServerError + $Results = "Failed to list excluded licenses. $($ErrorMessage.NormalizedError)" + Write-LogMessage -API $APIName -headers $Headers -message $Results -Sev 'Error' -LogData $ErrorMessage + } + + return ([HttpResponseContext]@{ + StatusCode = $StatusCode ?? [HttpStatusCode]::OK + Body = [pscustomobject]@{ 'Results' = $Results } + }) +} diff --git a/Modules/CIPPCore/Public/Tools/Initialize-CIPPExcludedLicenses.ps1 b/Modules/CIPPCore/Public/Tools/Initialize-CIPPExcludedLicenses.ps1 new file mode 100644 index 000000000000..6ea65fb9f555 --- /dev/null +++ b/Modules/CIPPCore/Public/Tools/Initialize-CIPPExcludedLicenses.ps1 @@ -0,0 +1,93 @@ +function Initialize-CIPPExcludedLicenses { + <# + .SYNOPSIS + Initialize the ExcludedLicenses table from the default config file + + .DESCRIPTION + Reads the ExcludeSkuList.JSON config file and adds missing licenses to the ExcludedLicenses Azure Table. + Only adds licenses that don't already exist, preserving any manually added entries. + Use -Force to clear the table and reset to defaults. + + .FUNCTIONALITY + Internal + + .PARAMETER Force + If specified, clears existing entries before initializing from config + + .PARAMETER Headers + Request headers for logging + + .PARAMETER APIName + API name for logging purposes + + .EXAMPLE + Initialize-CIPPExcludedLicenses -Headers $Request.Headers -APIName 'ExecExcludeLicenses' + #> + [CmdletBinding()] + param( + [switch]$Force, + $Headers, + $APIName = 'Initialize-CIPPExcludedLicenses' + ) + + try { + $Table = Get-CIPPTable -TableName ExcludedLicenses + + # If Force is specified, clear existing entries first + if ($Force) { + $ExistingRows = Get-CIPPAzDataTableEntity @Table + foreach ($Row in $ExistingRows) { + Remove-AzDataTableEntity -Force @Table -Entity $Row + } + Write-LogMessage -API $APIName -headers $Headers -message 'Cleared existing excluded licenses' -Sev 'Info' + } + + # Get the config file path + $CIPPCoreModuleRoot = Get-Module -Name CIPPCore | Select-Object -ExpandProperty ModuleBase + $CIPPRoot = (Get-Item $CIPPCoreModuleRoot).Parent.Parent + $ConfigPath = Join-Path $CIPPRoot 'Config\ExcludeSkuList.JSON' + + if (-not (Test-Path $ConfigPath)) { + throw "Config file not found: $ConfigPath" + } + + $TableBaseData = Get-Content -Path $ConfigPath -Raw | ConvertFrom-Json -AsHashtable -Depth 10 + + # Get existing GUIDs to avoid overwriting manually added entries + $ExistingRows = Get-CIPPAzDataTableEntity @Table + $ExistingGUIDs = @($ExistingRows | ForEach-Object { $_.GUID }) + + $AddedCount = 0 + $SkippedCount = 0 + foreach ($Row in $TableBaseData) { + if ($Row.GUID -in $ExistingGUIDs) { + $SkippedCount++ + continue + } + $Row.PartitionKey = 'License' + $Row.RowKey = $Row.GUID + Add-CIPPAzDataTableEntity @Table -Entity ([pscustomobject]$Row) -Force | Out-Null + $AddedCount++ + } + + if ($Force) { + $Message = "Successfully performed full reset. Restored $AddedCount default licenses from config file" + } else { + $Message = "Successfully added $AddedCount missing licenses from config file ($SkippedCount already existed)" + } + Write-LogMessage -API $APIName -headers $Headers -message $Message -Sev 'Info' + + return @{ + Success = $true + Message = $Message + AddedCount = $AddedCount + SkippedCount = $SkippedCount + FullReset = [bool]$Force + } + } catch { + $ErrorMessage = Get-CippException -Exception $_ + $Result = "Failed to initialize excluded licenses. $($ErrorMessage.NormalizedError)" + Write-LogMessage -API $APIName -headers $Headers -message $Result -Sev 'Error' -LogData $ErrorMessage + throw $Result + } +} From af03ef5b7718cf9f8dc01829fcf9163006a82b28 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Tue, 20 Jan 2026 23:35:05 +0100 Subject: [PATCH 214/503] Fixes list groups --- .../Identity/Administration/Groups/Invoke-ListGroups.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-ListGroups.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-ListGroups.ps1 index fc66527eb142..da7d70327cd1 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-ListGroups.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-ListGroups.ps1 @@ -102,8 +102,8 @@ function Invoke-ListGroups { } }, @{Name = 'dynamicGroupBool'; Expression = { if ($_.groupTypes -contains 'DynamicMembership') { $true } else { $false } } } - members = ($RawGraphRequest | Where-Object { $_.id -eq 2 }).body.value | Sort-Object displayName - owners = ($RawGraphRequest | Where-Object { $_.id -eq 3 }).body.value | Sort-Object displayName + members = @(($RawGraphRequest | Where-Object { $_.id -eq 2 }).body.value | Sort-Object displayName) + owners = @(($RawGraphRequest | Where-Object { $_.id -eq 3 }).body.value | Sort-Object displayName) allowExternal = (!$OnlyAllowInternal) sendCopies = $SendCopies hideFromOutlookClients = if ($GroupType -eq 'Microsoft 365') { $UnifiedGroupInfo.HiddenFromExchangeClientsEnabled } else { $null } From 942e7ed9660088d6a88ce3552c617bafbb56effd Mon Sep 17 00:00:00 2001 From: John Duprey Date: Tue, 20 Jan 2026 19:42:33 -0500 Subject: [PATCH 215/503] Skip members without GroupId in tenant group scripts Added checks in Get-TenantGroups.ps1 and Update-CIPPDynamicTenantGroups.ps1 to skip processing members that do not have a GroupId. This prevents errors and ensures only valid group members are handled. --- Modules/CIPPCore/Public/TenantGroups/Get-TenantGroups.ps1 | 3 +++ .../Public/TenantGroups/Update-CIPPDynamicTenantGroups.ps1 | 3 +++ 2 files changed, 6 insertions(+) diff --git a/Modules/CIPPCore/Public/TenantGroups/Get-TenantGroups.ps1 b/Modules/CIPPCore/Public/TenantGroups/Get-TenantGroups.ps1 index 76db653fa17f..0b15f3f62402 100644 --- a/Modules/CIPPCore/Public/TenantGroups/Get-TenantGroups.ps1 +++ b/Modules/CIPPCore/Public/TenantGroups/Get-TenantGroups.ps1 @@ -67,6 +67,9 @@ function Get-TenantGroups { $script:TenantGroupsCache.MembersByGroup = @{} foreach ($Member in $script:TenantGroupsCache.Members) { $GId = $Member.GroupId + if (-not $GId) { + continue + } if (-not $script:TenantGroupsCache.MembersByGroup.ContainsKey($GId)) { $script:TenantGroupsCache.MembersByGroup[$GId] = [System.Collections.Generic.List[object]]::new() } diff --git a/Modules/CIPPCore/Public/TenantGroups/Update-CIPPDynamicTenantGroups.ps1 b/Modules/CIPPCore/Public/TenantGroups/Update-CIPPDynamicTenantGroups.ps1 index 9cbc794e05fd..185018453aa2 100644 --- a/Modules/CIPPCore/Public/TenantGroups/Update-CIPPDynamicTenantGroups.ps1 +++ b/Modules/CIPPCore/Public/TenantGroups/Update-CIPPDynamicTenantGroups.ps1 @@ -49,6 +49,9 @@ function Update-CIPPDynamicTenantGroups { $script:TenantGroupMembersCache = @{} $AllGroupMembers = Get-CIPPAzDataTableEntity @MembersTable -Filter "PartitionKey eq 'Member'" foreach ($Member in $AllGroupMembers) { + if (-not $Member.GroupId) { + continue + } if (-not $script:TenantGroupMembersCache.ContainsKey($Member.GroupId)) { $script:TenantGroupMembersCache[$Member.GroupId] = [system.collections.generic.list[string]]::new() } From 25e10efb54da77f7e93e289d94372cb1d11c561c Mon Sep 17 00:00:00 2001 From: John Duprey Date: Tue, 20 Jan 2026 23:33:04 -0500 Subject: [PATCH 216/503] Add licenseProcessingState to group select fields Included the 'licenseProcessingState' property in the $SelectString for group queries to ensure this field is returned in group listings and when querying by GroupID. --- .../Identity/Administration/Groups/Invoke-ListGroups.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-ListGroups.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-ListGroups.ps1 index da7d70327cd1..9db18151e54e 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-ListGroups.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-ListGroups.ps1 @@ -15,7 +15,7 @@ function Invoke-ListGroups { $ExpandMembers = $Request.Query.expandMembers ?? $false - $SelectString = 'id,createdDateTime,displayName,description,mail,mailEnabled,mailNickname,resourceProvisioningOptions,securityEnabled,visibility,organizationId,onPremisesSamAccountName,membershipRule,groupTypes,onPremisesSyncEnabled,resourceProvisioningOptions,assignedLicenses,userPrincipalName' + $SelectString = 'id,createdDateTime,displayName,description,mail,mailEnabled,mailNickname,resourceProvisioningOptions,securityEnabled,visibility,organizationId,onPremisesSamAccountName,membershipRule,groupTypes,onPremisesSyncEnabled,resourceProvisioningOptions,assignedLicenses,userPrincipalName,licenseProcessingState' if ($ExpandMembers -ne $false) { $SelectString = '{0}&$expand=members($select=userPrincipalName)' -f $SelectString } @@ -24,7 +24,7 @@ function Invoke-ListGroups { $BulkRequestArrayList = [System.Collections.Generic.List[object]]::new() if ($Request.Query.GroupID) { - $SelectString = 'id,createdDateTime,displayName,description,mail,mailEnabled,mailNickname,resourceProvisioningOptions,securityEnabled,visibility,organizationId,onPremisesSamAccountName,membershipRule,groupTypes,assignedLicenses,userPrincipalName,onPremisesSyncEnabled' + $SelectString = 'id,createdDateTime,displayName,description,mail,mailEnabled,mailNickname,resourceProvisioningOptions,securityEnabled,visibility,organizationId,onPremisesSamAccountName,membershipRule,groupTypes,assignedLicenses,userPrincipalName,onPremisesSyncEnabled,licenseProcessingState' $BulkRequestArrayList.add(@{ id = 1 method = 'GET' From 89e37466f234676c8507006b07db875135a0835a Mon Sep 17 00:00:00 2001 From: John Duprey Date: Wed, 21 Jan 2026 00:09:27 -0500 Subject: [PATCH 217/503] perform tenant data collection in sequence --- .../Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 | 1 + 1 file changed, 1 insertion(+) diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 index 46d0e601541c..69c88b742398 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 @@ -178,6 +178,7 @@ function Push-CIPPDBCacheData { OrchestratorName = "CIPPDBCacheTenant_$TenantFilter" Batch = @($Batch) SkipLog = $true + DurableMode = 'Sequence' } if ($Item.TestRun -eq $true) { From 86cdd9d16051cf754e7a74162d31563a4c53b4d8 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Wed, 21 Jan 2026 00:23:45 -0500 Subject: [PATCH 218/503] Add function to reprocess user license assignments Introduces Invoke-ExecReprocessUserLicenses.ps1, an HTTP entrypoint for triggering Microsoft Graph API calls to reprocess license assignments for a specified user. Handles request parameters, logs outcomes, and returns appropriate HTTP responses. --- .../Invoke-ExecReprocessUserLicenses.ps1 | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecReprocessUserLicenses.ps1 diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecReprocessUserLicenses.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecReprocessUserLicenses.ps1 new file mode 100644 index 000000000000..3aaf3829e0ff --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecReprocessUserLicenses.ps1 @@ -0,0 +1,36 @@ +function Invoke-ExecReprocessUserLicenses { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Identity.User.ReadWrite + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + + # Interact with query parameters or the body of the request. + $TenantFilter = $Request.Query.tenantFilter ?? $Request.Body.tenantFilter + $UserID = $Request.Query.ID ?? $Request.Body.ID + $UserPrincipalName = $Request.Query.userPrincipalName ?? $Request.Body.userPrincipalName + + try { + $GraphRequest = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/users/$UserID/reprocessLicenseAssignment" -tenantid $TenantFilter -type POST -body '{}' -AsApp $true + + $Result = "Successfully reprocessed license assignments for user $UserPrincipalName. License assignment states will be updated shortly." + Write-LogMessage -Headers $Headers -API $APIName -tenant $TenantFilter -message $Result -sev 'Info' + $StatusCode = [HttpStatusCode]::OK + } catch { + $ErrorMessage = Get-CippException -Exception $_ + $Result = "Failed to reprocess license assignments for $UserPrincipalName. $($ErrorMessage.NormalizedError)" + Write-LogMessage -Headers $Headers -API $APIName -tenant $TenantFilter -message $Result -sev 'Error' -LogData $ErrorMessage + $StatusCode = [HttpStatusCode]::InternalServerError + } + + return ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = @{ 'Results' = $Result } + }) +} From a0af36e3ca9c954e3cccc00b134f71b1562a5d53 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Wed, 21 Jan 2026 13:10:24 +0100 Subject: [PATCH 219/503] expansion fix --- .../CIPPCore/Public/TenantGroups/Expand-CIPPTenantGroups.ps1 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/TenantGroups/Expand-CIPPTenantGroups.ps1 b/Modules/CIPPCore/Public/TenantGroups/Expand-CIPPTenantGroups.ps1 index 3f8405c0d619..4e10fc0ffb80 100644 --- a/Modules/CIPPCore/Public/TenantGroups/Expand-CIPPTenantGroups.ps1 +++ b/Modules/CIPPCore/Public/TenantGroups/Expand-CIPPTenantGroups.ps1 @@ -17,7 +17,8 @@ function Expand-CIPPTenantGroups { $FilterValue = $_ # Group lookup if ($_.type -eq 'Group') { - $members = (Get-TenantGroups -GroupId $_.value).members + $GroupResult = Get-TenantGroups -GroupId $_.value + $members = if ($GroupResult) { $GroupResult.members } else { @() } $TenantList | Where-Object -Property customerId -In $members.customerId | ForEach-Object { $GroupMember = $_ [PSCustomObject]@{ From 1b1ca422992293479e14b1ee0fb3e62df8635ede Mon Sep 17 00:00:00 2001 From: John Duprey Date: Wed, 21 Jan 2026 09:34:38 -0500 Subject: [PATCH 220/503] Improve MFA user data handling and error logging Enhanced parsing of CAPolicies and MFAMethods in Invoke-ListMFAUsers to ensure consistent array output and handle empty or non-string values. Updated Get-CIPPMFAState to wrap CAPolicies in an array and added a more descriptive error message for licensing issues. Improved error logging in Set-CIPPDBCacheMFAState by including detailed exception data. --- .../Identity/Reports/Invoke-ListMFAUsers.ps1 | 12 ++++++++---- Modules/CIPPCore/Public/Get-CIPPMFAState.ps1 | 3 ++- Modules/CIPPCore/Public/Set-CIPPDBCacheMFAState.ps1 | 2 +- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Reports/Invoke-ListMFAUsers.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Reports/Invoke-ListMFAUsers.ps1 index 6598473b4c57..8dd0274fe7f9 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Reports/Invoke-ListMFAUsers.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Reports/Invoke-ListMFAUsers.ps1 @@ -60,11 +60,15 @@ function Invoke-ListMFAUsers { } } else { $Rows = foreach ($Row in $Rows) { - if ($Row.CAPolicies) { - $Row.CAPolicies = try { $Row.CAPolicies | ConvertFrom-Json } catch { $Row.CAPolicies } + if ($Row.CAPolicies -and $Row.CAPolicies -is [string]) { + $Row.CAPolicies = try { $Row.CAPolicies | ConvertFrom-Json } catch { @() } + } elseif (-not $Row.CAPolicies) { + $Row.CAPolicies = @() } - if ($Row.MFAMethods) { - $Row.MFAMethods = try { $Row.MFAMethods | ConvertFrom-Json } catch { $Row.MFAMethods } + if ($Row.MFAMethods -and $Row.MFAMethods -is [string]) { + $Row.MFAMethods = try { $Row.MFAMethods | ConvertFrom-Json } catch { @() } + } elseif (-not $Row.MFAMethods) { + $Row.MFAMethods = @() } $Row } diff --git a/Modules/CIPPCore/Public/Get-CIPPMFAState.ps1 b/Modules/CIPPCore/Public/Get-CIPPMFAState.ps1 index 234d6b01dba5..193a2b1ca532 100644 --- a/Modules/CIPPCore/Public/Get-CIPPMFAState.ps1 +++ b/Modules/CIPPCore/Public/Get-CIPPMFAState.ps1 @@ -47,6 +47,7 @@ function Get-CIPPMFAState { } catch { $CAState.Add('Not Licensed for Conditional Access') | Out-Null $MFARegistration = $null + $CAError = "MFA registration not available - licensing required for Conditional Access reporting" if ($_.Exception.Message -ne "Tenant is not a B2C tenant and doesn't have premium licenses") { $Errors.Add(@{Step = 'MFARegistration'; Message = $_.Exception.Message }) } @@ -340,7 +341,7 @@ function Get-CIPPMFAState { MFACapable = if ($null -ne $MFARegUser) { [bool]$MFARegUser.isMfaCapable } else { $null } MFAMethods = if ($null -ne $MFARegUser) { @($MFARegUser.methodsRegistered) } else { @() } CoveredByCA = $CoveredByCA - CAPolicies = $UserCAState + CAPolicies = @($UserCAState) CoveredBySD = $SecureDefaultsState IsAdmin = $IsAdmin RowKey = [string]($_.UserPrincipalName).replace('#', '') diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheMFAState.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheMFAState.ps1 index 1a430f382a6a..40fd5bb12ddf 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheMFAState.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheMFAState.ps1 @@ -22,6 +22,6 @@ function Set-CIPPDBCacheMFAState { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($MFAState.Count) MFA state records successfully" -sev Debug } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache MFA state: $($_.Exception.Message)" -sev Error + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache MFA state: $($_.Exception.Message)" -sev Error -LogData (Get-CippException -Exception $_) } } From defe3b919ff518bf4d3c85cb4e069d91e0b0d9ca Mon Sep 17 00:00:00 2001 From: John Duprey Date: Wed, 21 Jan 2026 09:54:35 -0500 Subject: [PATCH 221/503] bump version to 10.0.3 --- host.json | 2 +- version_latest.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/host.json b/host.json index ec9e853f1eed..fcd66850583e 100644 --- a/host.json +++ b/host.json @@ -16,7 +16,7 @@ "distributedTracingEnabled": false, "version": "None" }, - "defaultVersion": "10.0.1", + "defaultVersion": "10.0.3", "versionMatchStrategy": "Strict", "versionFailureStrategy": "Fail" } diff --git a/version_latest.txt b/version_latest.txt index 1532420512a9..6a7144d3047f 100644 --- a/version_latest.txt +++ b/version_latest.txt @@ -1 +1 @@ -10.0.1 +10.0.3 From 2063a86d28a0e2553b58b7934564e54a96bd1ee0 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Wed, 21 Jan 2026 10:29:30 -0500 Subject: [PATCH 222/503] Switch to GitHub API for release notes generation Replaces the mikepenz/release-changelog-builder-action with the GitHub API via actions/github-script to generate release notes. Adds a step to fetch the previous tag for more accurate changelog generation. --- .github/workflows/publish_release.yml | 31 ++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/.github/workflows/publish_release.yml b/.github/workflows/publish_release.yml index 81acfa86790f..ed540c3ac714 100644 --- a/.github/workflows/publish_release.yml +++ b/.github/workflows/publish_release.yml @@ -48,11 +48,36 @@ jobs: echo "tag_exists=false" >> $GITHUB_ENV fi + # Get Previous Tag + - name: Get Previous Tag + id: previous_tag + if: env.tag_exists == 'false' + run: | + PREV_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "") + echo "previous_tag=$PREV_TAG" >> $GITHUB_OUTPUT + echo "Previous tag: $PREV_TAG" + # Generate Release Notes - name: Generate Release Notes id: changelog if: env.tag_exists == 'false' - uses: mikepenz/release-changelog-builder-action@v5.0.0 + uses: actions/github-script@v7 + with: + script: | + const params = { + owner: context.repo.owner, + repo: context.repo.repo, + tag_name: '${{ steps.get_version.outputs.version }}', + target_commitish: context.sha + }; + + const previousTag = '${{ steps.previous_tag.outputs.previous_tag }}'; + if (previousTag) { + params.previous_tag_name = previousTag; + } + + const { data } = await github.rest.repos.generateReleaseNotes(params); + core.setOutput('changelog', data.body); env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -75,10 +100,6 @@ jobs: if: env.tag_exists == 'false' run: | mkdir -p src/releases - zip -r src/releases/release_${{ steps.get_version.outputs.version }}.zip . \ - --exclude "./src/releases/*" \ - --exclude ".*" \ - --exclude ".*/**" zip -r src/releases/latest.zip . \ --exclude "./src/releases/*" \ --exclude ".*" \ From d2d5469ae7ef4289986fdba7b451a1d4a6d151a8 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Wed, 21 Jan 2026 11:02:24 -0500 Subject: [PATCH 223/503] standards fixes --- ...Invoke-CIPPStandardEnableAppConsentRequests.ps1 | 2 +- .../Standards/Invoke-CIPPStandardMDMScope.ps1 | 14 ++++---------- .../Standards/Invoke-CIPPStandardSpoofWarn.ps1 | 8 ++++---- 3 files changed, 9 insertions(+), 15 deletions(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableAppConsentRequests.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableAppConsentRequests.ps1 index 21a52e52dda7..5fbbb936a4c1 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableAppConsentRequests.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableAppConsentRequests.ps1 @@ -121,7 +121,7 @@ function Invoke-CIPPStandardEnableAppConsentRequests { if ($Settings.report -eq $true) { $CurrentValue = [PSCustomObject]@{ - EnableAppConsentRequests = $CurrentInfo.isEnabled + EnableAppConsentRequests = [bool]$CurrentInfo.isEnabled ReviewerCount = $CurrentInfo.reviewers.count } $ExpectedValue = [PSCustomObject]@{ diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMDMScope.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMDMScope.ps1 index b0e67306cc6a..e01b2616abe1 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMDMScope.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMDMScope.ps1 @@ -144,18 +144,12 @@ function Invoke-CIPPStandardMDMScope { if ($Settings.report -eq $true) { $CurrentValue = @{ - termsOfUseUrl = $CurrentInfo.termsOfUseUrl - discoveryUrl = $CurrentInfo.discoveryUrl - complianceUrl = $CurrentInfo.complianceUrl - appliesTo = $CurrentInfo.appliesTo - customGroup = $CurrentInfo.includedGroups.displayName + appliesTo = $CurrentInfo.appliesTo + customGroup = $CurrentInfo.includedGroups.displayName ?? '' } $ExpectedValue = @{ - termsOfUseUrl = $Settings.termsOfUseUrl - discoveryUrl = $Settings.discoveryUrl - complianceUrl = $Settings.complianceUrl - appliesTo = $Settings.appliesTo - customGroup = $Settings.customGroup + appliesTo = $Settings.appliesTo + customGroup = $Settings.customGroup ?? '' } Set-CIPPStandardsCompareField -FieldName 'standards.MDMScope' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant Add-CIPPBPAField -FieldName 'MDMScope' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $tenant diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSpoofWarn.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSpoofWarn.ps1 index c07e9e96fb3f..8155161f5954 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSpoofWarn.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSpoofWarn.ps1 @@ -133,13 +133,13 @@ function Invoke-CIPPStandardSpoofWarn { Add-CIPPBPAField -FieldName 'SpoofingWarnings' -FieldValue $CurrentInfo.Enabled -StoreAs bool -Tenant $Tenant $CurrentValue = @{ - Enabled = $CurrentInfo.Enabled - AllowList = $CurrentInfo.AllowList + Enabled = $CurrentInfo.Enabled + AllowList = $CurrentInfo.AllowList IsCompliant = $CurrentInfo.Enabled -eq $IsEnabled -and $AllowListCorrect } $ExpectedValue = @{ - Enabled = $IsEnabled - AllowList = $Settings.AllowListAdd.value ?? $Settings.AllowListAdd + Enabled = $IsEnabled + AllowList = @($Settings.AllowListAdd.value ?? $Settings.AllowListAdd ?? @()) IsCompliant = $true } Set-CIPPStandardsCompareField -FieldName 'standards.SpoofWarn' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -Tenant $Tenant From 7fa131a118244014c9ab05c429cefe5ec2e7b4d0 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Wed, 21 Jan 2026 11:53:09 -0500 Subject: [PATCH 224/503] fix permissions for limited tenant lists --- .../Entrypoints/HTTP Functions/Invoke-ListAvailableTests.ps1 | 2 +- .../Entrypoints/HTTP Functions/Invoke-ListTestReports.ps1 | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ListAvailableTests.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ListAvailableTests.ps1 index ceee5d026d31..15c26f800b8f 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ListAvailableTests.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ListAvailableTests.ps1 @@ -1,7 +1,7 @@ function Invoke-ListAvailableTests { <# .FUNCTIONALITY - Entrypoint + Entrypoint,AnyTenant .ROLE CIPP.Dashboard.Read #> diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ListTestReports.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ListTestReports.ps1 index eae3538134ce..a0d0f4ee5869 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ListTestReports.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ListTestReports.ps1 @@ -4,7 +4,7 @@ function Invoke-ListTestReports { Lists all available test reports from JSON files and database .FUNCTIONALITY - Entrypoint + Entrypoint,AnyTenant .ROLE Tenant.Reports.Read From 9e8702c64e0f2996deb40e87389028dfe78a65f4 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Wed, 21 Jan 2026 11:53:26 -0500 Subject: [PATCH 225/503] Add direct role assignments to Get-CippDbRoleMembers Enhanced Get-CippDbRoleMembers to include direct role assignments by querying 'Roles' and merging unique members. Updated Invoke-CippTestZTNA21836 to use the correct parameter name '-RoleTemplateId' for Get-CippDbRoleMembers. --- Modules/CIPPCore/Public/Get-CippDbRoleMembers.ps1 | 14 ++++++++++++++ .../ZTNA/Identity/Invoke-CippTestZTNA21836.ps1 | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Get-CippDbRoleMembers.ps1 b/Modules/CIPPCore/Public/Get-CippDbRoleMembers.ps1 index 907917fa69d7..d51612448da5 100644 --- a/Modules/CIPPCore/Public/Get-CippDbRoleMembers.ps1 +++ b/Modules/CIPPCore/Public/Get-CippDbRoleMembers.ps1 @@ -10,6 +10,7 @@ function Get-CippDbRoleMembers { $RoleAssignments = New-CIPPDbRequest -TenantFilter $TenantFilter -Type 'RoleAssignmentScheduleInstances' $RoleEligibilities = New-CIPPDbRequest -TenantFilter $TenantFilter -Type 'RoleEligibilitySchedules' + $DirectRoleAssignments = New-CIPPDbRequest -TenantFilter $TenantFilter -Type 'Roles' | Where-Object { $_.roleTemplateId -eq $RoleTemplateId } | Select-Object -ExpandProperty members $ActiveMembers = $RoleAssignments | Where-Object { $_.roleDefinitionId -eq $RoleTemplateId -and $_.assignmentType -eq 'Assigned' @@ -45,5 +46,18 @@ function Get-CippDbRoleMembers { } } + foreach ($member in $DirectRoleAssignments) { + if ($AllMembers.id -notcontains $member.id) { + $memberObj = [PSCustomObject]@{ + id = $member.id + displayName = $member.displayName + userPrincipalName = $member.userPrincipalName + '@odata.type' = $member.'@odata.type' + AssignmentType = 'Direct' + } + $AllMembers.Add($memberObj) + } + } + return $AllMembers } diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21836.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21836.ps1 index e6129bd4a135..8253e3737d3d 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21836.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21836.ps1 @@ -20,7 +20,7 @@ function Invoke-CippTestZTNA21836 { $WorkloadIdentitiesWithPrivilegedRoles = [System.Collections.Generic.List[object]]::new() foreach ($Role in $PrivilegedRoles) { - $RoleMembers = Get-CippDbRoleMembers -TenantFilter $Tenant -RoleId $Role.id + $RoleMembers = Get-CippDbRoleMembers -TenantFilter $Tenant -RoleTemplateId $Role.id foreach ($Member in $RoleMembers) { if ($Member.'@odata.type' -eq '#microsoft.graph.servicePrincipal') { From 69e9e6fce0291ba630c3ce453eb01a76e792a652 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Wed, 21 Jan 2026 12:00:16 -0500 Subject: [PATCH 226/503] Fix parameter naming inconsistencies in test scripts Standardized parameter names in Add-CippTestResult.ps1 and Invoke-CippTestORCA119.ps1 for consistency. Changed 'testType' to 'TestType' and updated usage of 'TestType' to 'Type' in New-CIPPDbRequest call. --- Modules/CIPPCore/Public/Add-CippTestResult.ps1 | 2 +- .../Public/Tests/ORCA/Identity/Invoke-CippTestORCA119.ps1 | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Modules/CIPPCore/Public/Add-CippTestResult.ps1 b/Modules/CIPPCore/Public/Add-CippTestResult.ps1 index a4bee90dae78..37007ff93508 100644 --- a/Modules/CIPPCore/Public/Add-CippTestResult.ps1 +++ b/Modules/CIPPCore/Public/Add-CippTestResult.ps1 @@ -48,7 +48,7 @@ function Add-CippTestResult { [string]$TestId, [Parameter(Mandatory = $false)] - [string]$testType = 'Identity', + [string]$TestType = 'Identity', [Parameter(Mandatory = $true)] [string]$Status, diff --git a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA119.ps1 b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA119.ps1 index 8f8cbbca6ece..c751816a320d 100644 --- a/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA119.ps1 +++ b/Modules/CIPPCore/Public/Tests/ORCA/Identity/Invoke-CippTestORCA119.ps1 @@ -6,7 +6,7 @@ function Invoke-CippTestORCA119 { param($Tenant) try { - $Policies = New-CIPPDbRequest -TenantFilter $Tenant -TestType 'ExoAntiPhishPolicies' + $Policies = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ExoAntiPhishPolicies' if (-not $Policies) { Add-CippTestResult -TenantFilter $Tenant -TestId 'ORCA119' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'Similar Domains Safety Tips is enabled' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Anti-Phish' From 5a97b56a7a1146f5e9684b60f9749bf9842fe0dc Mon Sep 17 00:00:00 2001 From: John Duprey Date: Wed, 21 Jan 2026 16:02:56 -0500 Subject: [PATCH 227/503] Refactor mailbox CAS retrieval Replaced usage of New-GraphGetRequest with New-ExoRequest for CAS mailbox retrieval --- .../Reports/Invoke-ListMailboxCAS.ps1 | 4 +- .../Public/Set-CIPPDBCacheMailboxes.ps1 | 51 +++++++++++-------- ...nvoke-CIPPStandardDisableBasicAuthSMTP.ps1 | 2 +- 3 files changed, 32 insertions(+), 25 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Reports/Invoke-ListMailboxCAS.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Reports/Invoke-ListMailboxCAS.ps1 index e90ed122fc89..2e66a472ddab 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Reports/Invoke-ListMailboxCAS.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Reports/Invoke-ListMailboxCAS.ps1 @@ -1,4 +1,4 @@ -Function Invoke-ListMailboxCAS { +function Invoke-ListMailboxCAS { <# .FUNCTIONALITY Entrypoint @@ -10,7 +10,7 @@ Function Invoke-ListMailboxCAS { # Interact with query parameters or the body of the request. $TenantFilter = $Request.Query.TenantFilter try { - $GraphRequest = New-GraphGetRequest -uri "https://outlook.office365.com/adminapi/beta/$($tenantfilter)/CasMailbox" -Tenantid $tenantfilter -scope ExchangeOnline | Select-Object @{ Name = 'displayName'; Expression = { $_.'DisplayName' } }, + $GraphRequest = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-CasMailbox' | Select-Object @{ Name = 'displayName'; Expression = { $_.'DisplayName' } }, @{ Name = 'primarySmtpAddress'; Expression = { $_.'PrimarySMTPAddress' } }, @{ Name = 'ecpenabled'; Expression = { $_.'ECPEnabled' } }, @{ Name = 'owaenabled'; Expression = { $_.'OWAEnabled' } }, diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheMailboxes.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheMailboxes.ps1 index 350d97a32af7..e169189600de 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheMailboxes.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheMailboxes.ps1 @@ -23,33 +23,36 @@ function Set-CIPPDBCacheMailboxes { cmdParams = @{} Select = $Select } - $Mailboxes = (New-ExoRequest @ExoRequest) | Select-Object id, ExchangeGuid, ArchiveGuid, WhenSoftDeleted, - @{ Name = 'UPN'; Expression = { $_.'UserPrincipalName' } }, - @{ Name = 'displayName'; Expression = { $_.'DisplayName' } }, - @{ Name = 'primarySmtpAddress'; Expression = { $_.'PrimarySMTPAddress' } }, - @{ Name = 'recipientType'; Expression = { $_.'RecipientType' } }, - @{ Name = 'recipientTypeDetails'; Expression = { $_.'RecipientTypeDetails' } }, - @{ Name = 'AdditionalEmailAddresses'; Expression = { ($_.'EmailAddresses' | Where-Object { $_ -clike 'smtp:*' }).Replace('smtp:', '') -join ', ' } }, - @{ Name = 'ForwardingSmtpAddress'; Expression = { $_.'ForwardingSmtpAddress' -replace 'smtp:', '' } }, - @{ Name = 'InternalForwardingAddress'; Expression = { $_.'ForwardingAddress' } }, - DeliverToMailboxAndForward, - HiddenFromAddressListsEnabled, - ExternalDirectoryObjectId, - MessageCopyForSendOnBehalfEnabled, - MessageCopyForSentAsEnabled + # Use Generic List for better memory efficiency with large datasets + $MailboxList = [System.Collections.Generic.List[PSObject]]::new() + $RawMailboxes = New-ExoRequest @ExoRequest + + foreach ($Mailbox in $RawMailboxes) { + $MailboxList.Add(($Mailbox | Select-Object id, ExchangeGuid, ArchiveGuid, WhenSoftDeleted, + @{ Name = 'UPN'; Expression = { $_.'UserPrincipalName' } }, + @{ Name = 'displayName'; Expression = { $_.'DisplayName' } }, + @{ Name = 'primarySmtpAddress'; Expression = { $_.'PrimarySMTPAddress' } }, + @{ Name = 'recipientType'; Expression = { $_.'RecipientType' } }, + @{ Name = 'recipientTypeDetails'; Expression = { $_.'RecipientTypeDetails' } }, + @{ Name = 'AdditionalEmailAddresses'; Expression = { ($_.'EmailAddresses' | Where-Object { $_ -clike 'smtp:*' }).Replace('smtp:', '') -join ', ' } }, + @{ Name = 'ForwardingSmtpAddress'; Expression = { $_.'ForwardingSmtpAddress' -replace 'smtp:', '' } }, + @{ Name = 'InternalForwardingAddress'; Expression = { $_.'ForwardingAddress' } }, + DeliverToMailboxAndForward, + HiddenFromAddressListsEnabled, + ExternalDirectoryObjectId, + MessageCopyForSendOnBehalfEnabled, + MessageCopyForSentAsEnabled)) + } + + $Mailboxes = $MailboxList.ToArray() + $RawMailboxes = $null + $MailboxList.Clear() + $MailboxList = $null Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Mailboxes' -Data $Mailboxes Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Mailboxes' -Data $Mailboxes -Count Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($Mailboxes.Count) mailboxes successfully" -sev Debug - # Get CAS mailboxes - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching CAS mailboxes' -sev Debug - $CASMailboxes = New-GraphGetRequest -uri "https://outlook.office365.com/adminapi/beta/$($TenantFilter)/CasMailbox" -Tenantid $TenantFilter -scope 'ExchangeOnline' -noPagination $true - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'CASMailbox' -Data $CASMailboxes - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'CASMailbox' -Data $CASMailboxes -Count - $CASMailboxes = $null - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached CAS mailboxes successfully' -sev Debug - # Start orchestrator to cache mailbox permissions in batches $MailboxCount = ($Mailboxes | Measure-Object).Count if ($MailboxCount -gt 0) { @@ -108,6 +111,10 @@ function Set-CIPPDBCacheMailboxes { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'No mailboxes found to cache permissions for' -sev Debug } + # Clear mailbox data to free memory + $Mailboxes = $null + [System.GC]::Collect() + } catch { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache mailboxes: $($_.Exception.Message)" -sev Error } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableBasicAuthSMTP.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableBasicAuthSMTP.ps1 index adefbeb157ca..d648b2d8589b 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableBasicAuthSMTP.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableBasicAuthSMTP.ps1 @@ -45,7 +45,7 @@ function Invoke-CIPPStandardDisableBasicAuthSMTP { try { $CurrentInfo = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-TransportConfig' - $SMTPusers = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-CASMailbox' -cmdParams @{ ResultSize = 'Unlimited' } | + $SMTPusers = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-CASMailbox' | Where-Object { ($_.SmtpClientAuthenticationDisabled -eq $false) } } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message From 54bf53c3fc24ce4760110a45e9b89021efa3118f Mon Sep 17 00:00:00 2001 From: John Duprey Date: Wed, 21 Jan 2026 16:03:16 -0500 Subject: [PATCH 228/503] Improve memory efficiency for large dataset processing Refactored Add-CIPPDbItem to process data in batches, reducing memory usage when handling large datasets. Updated Push-StoreMailboxPermissions and Set-CIPPDBCacheUsers to use generic lists and explicit cleanup for better memory management. Added new Set-CIPPDBCacheCASMailboxes function to cache CAS mailboxes, and registered 'CASMailboxes' in Push-CIPPDBCacheData. These changes enhance reliability and scalability when caching large volumes of data. --- Modules/CIPPCore/Public/Add-CIPPDbItem.ps1 | 35 ++++++++++++---- .../Push-StoreMailboxPermissions.ps1 | 41 +++++++++++++++---- .../Push-CIPPDBCacheData.ps1 | 1 + .../Public/Set-CIPPDBCacheCASMailboxes.ps1 | 38 +++++++++++++++++ .../CIPPCore/Public/Set-CIPPDBCacheUsers.ps1 | 16 ++++++-- 5 files changed, 110 insertions(+), 21 deletions(-) create mode 100644 Modules/CIPPCore/Public/Set-CIPPDBCacheCASMailboxes.ps1 diff --git a/Modules/CIPPCore/Public/Add-CIPPDbItem.ps1 b/Modules/CIPPCore/Public/Add-CIPPDbItem.ps1 index 88b630bdf701..9876118722b8 100644 --- a/Modules/CIPPCore/Public/Add-CIPPDbItem.ps1 +++ b/Modules/CIPPCore/Public/Add-CIPPDbItem.ps1 @@ -69,19 +69,36 @@ function Add-CIPPDbItem { if ($ExistingEntities) { Remove-AzDataTableEntity @Table -Entity $ExistingEntities -Force | Out-Null } - $Entities = foreach ($Item in $Data) { - $ItemId = $Item.id ?? $Item.ExternalDirectoryObjectId ?? $Item.Identity ?? $Item.skuId - @{ - PartitionKey = $TenantFilter - RowKey = Format-RowKey "$Type-$ItemId" - Data = [string]($Item | ConvertTo-Json -Depth 10 -Compress) - Type = $Type + + # Process in batches to avoid memory issues with large datasets + $BatchSize = 1000 + $TotalCount = $Data.Count + $ProcessedCount = 0 + Write-Information "Adding $TotalCount items of type $Type to CIPP Reporting DB for tenant $TenantFilter in batches of $BatchSize" + for ($i = 0; $i -lt $TotalCount; $i += $BatchSize) { + $BatchEnd = [Math]::Min($i + $BatchSize, $TotalCount) + $Batch = $Data[$i..($BatchEnd - 1)] + + $Entities = foreach ($Item in $Batch) { + $ItemId = $Item.id ?? $Item.ExternalDirectoryObjectId ?? $Item.Identity ?? $Item.skuId + @{ + PartitionKey = $TenantFilter + RowKey = Format-RowKey "$Type-$ItemId" + Data = [string]($Item | ConvertTo-Json -Depth 10 -Compress) + Type = $Type + } } + + Add-CIPPAzDataTableEntity @Table -Entity $Entities -Force | Out-Null + $ProcessedCount += $Batch.Count + + # Clear batch variables to free memory + $Entities = $null + $Batch = $null + [System.GC]::Collect() } - Add-CIPPAzDataTableEntity @Table -Entity $Entities -Force | Out-Null } - Write-LogMessage -API 'CIPPDbItem' -tenant $TenantFilter -message "Added $($Data.Count) items of type $Type$(if ($Count) { ' (count mode)' })" -sev Debug } catch { diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Mailbox Permissions/Push-StoreMailboxPermissions.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Mailbox Permissions/Push-StoreMailboxPermissions.ps1 index 911745c1b06b..c7b22a576847 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Mailbox Permissions/Push-StoreMailboxPermissions.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Mailbox Permissions/Push-StoreMailboxPermissions.ps1 @@ -44,15 +44,21 @@ function Push-StoreMailboxPermissions { # Results are grouped by cmdlet name due to ReturnWithCommand if ($ActualResult['Get-MailboxPermission']) { Write-Information "Adding $($ActualResult['Get-MailboxPermission'].Count) mailbox permissions" - $AllMailboxPermissions.AddRange($ActualResult['Get-MailboxPermission']) + foreach ($perm in $ActualResult['Get-MailboxPermission']) { + $AllMailboxPermissions.Add($perm) + } } if ($ActualResult['Get-RecipientPermission']) { Write-Information "Adding $($ActualResult['Get-RecipientPermission'].Count) recipient permissions" - $AllRecipientPermissions.AddRange($ActualResult['Get-RecipientPermission']) + foreach ($perm in $ActualResult['Get-RecipientPermission']) { + $AllRecipientPermissions.Add($perm) + } } if ($ActualResult['Get-MailboxFolderPermission']) { Write-Information "Adding $($ActualResult['Get-MailboxFolderPermission'].Count) calendar permissions" - $AllCalendarPermissions.AddRange($ActualResult['Get-MailboxFolderPermission']) + foreach ($perm in $ActualResult['Get-MailboxFolderPermission']) { + $AllCalendarPermissions.Add($perm) + } } } else { Write-Information "Skipping non-hashtable result: $($ActualResult.GetType().Name)" @@ -61,30 +67,47 @@ function Push-StoreMailboxPermissions { # Combine all permissions (mailbox and recipient) into a single collection $AllPermissions = [System.Collections.Generic.List[object]]::new() - $AllPermissions.AddRange($AllMailboxPermissions) - $AllPermissions.AddRange($AllRecipientPermissions) + foreach ($perm in $AllMailboxPermissions) { + $AllPermissions.Add($perm) + } + foreach ($perm in $AllRecipientPermissions) { + $AllPermissions.Add($perm) + } Write-Information "Aggregated $($AllPermissions.Count) total permissions ($($AllMailboxPermissions.Count) mailbox + $($AllRecipientPermissions.Count) recipient)" Write-Information "Aggregated $($AllCalendarPermissions.Count) calendar permissions" # Store all permissions together as MailboxPermissions if ($AllPermissions.Count -gt 0) { - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'MailboxPermissions' -Data $AllPermissions - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'MailboxPermissions' -Data $AllPermissions -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'MailboxPermissions' -Data $AllPermissions.ToArray() + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'MailboxPermissions' -Data @{ Count = $AllPermissions.Count } -Count Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($AllPermissions.Count) mailbox permission records" -sev Info } else { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'No mailbox permissions found to cache' -sev Info } + # Clear to free memory before processing calendar permissions + $AllMailboxPermissions.Clear() + $AllRecipientPermissions.Clear() + $AllPermissions.Clear() + $AllMailboxPermissions = $null + $AllRecipientPermissions = $null + $AllPermissions = $null + # Store calendar permissions separately if ($AllCalendarPermissions.Count -gt 0) { - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'CalendarPermissions' -Data $AllCalendarPermissions - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'CalendarPermissions' -Data $AllCalendarPermissions -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'CalendarPermissions' -Data $AllCalendarPermissions.ToArray() + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'CalendarPermissions' -Data @{ Count = $AllCalendarPermissions.Count } -Count Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($AllCalendarPermissions.Count) calendar permission records" -sev Info } else { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'No calendar permissions found to cache' -sev Info } + # Final cleanup + $AllCalendarPermissions.Clear() + $AllCalendarPermissions = $null + [System.GC]::Collect() + return } catch { diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 index 69c88b742398..10992abe278b 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 @@ -99,6 +99,7 @@ function Push-CIPPDBCacheData { 'ExoPresetSecurityPolicy' 'ExoTenantAllowBlockList' 'Mailboxes' + 'CASMailboxes' 'MailboxUsage' 'OneDriveUsage' ) diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheCASMailboxes.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheCASMailboxes.ps1 new file mode 100644 index 000000000000..283b056f9ba6 --- /dev/null +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheCASMailboxes.ps1 @@ -0,0 +1,38 @@ +function Set-CIPPDBCacheCASMailboxes { + <# + .SYNOPSIS + Caches all CAS mailboxes for a tenant + + .PARAMETER TenantFilter + The tenant to cache CAS mailboxes for + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter + ) + + try { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching CAS mailboxes' -sev Debug + + # Use Generic List for better memory efficiency with large datasets + $CASMailboxList = [System.Collections.Generic.List[PSObject]]::new() + $CASMailboxesResponse = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-CasMailbox' + foreach ($Mailbox in $CASMailboxesResponse) { + $CASMailboxList.Add($Mailbox) + } + + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'CASMailbox' -Data $CASMailboxList.ToArray() + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'CASMailbox' -Data @{ Count = $CASMailboxList.Count } -Count + + $CASMailboxesResponse = $null + $CASMailboxList.Clear() + $CASMailboxList = $null + [System.GC]::Collect() + + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached CAS mailboxes successfully' -sev Debug + + } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache CAS mailboxes: $($_.Exception.Message)" -sev Error + } +} diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheUsers.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheUsers.ps1 index a1ffd11ef337..7c23b2f4a9cb 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheUsers.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheUsers.ps1 @@ -15,10 +15,20 @@ function Set-CIPPDBCacheUsers { try { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching users' -sev Debug - $Users = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/users?$top=999' -tenantid $TenantFilter - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Users' -Data $Users - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Users' -Data $Users -Count + $Users = [System.Collections.Generic.List[PSObject]]::new() + $UsersResponse = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/users?$top=999' -tenantid $TenantFilter + foreach ($User in $UsersResponse) { + $Users.Add($User) + } + + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Users' -Data $Users.ToArray() + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Users' -Data @{ Count = $Users.Count } -Count + + $Users.Clear() $Users = $null + $UsersResponse = $null + [System.GC]::Collect() + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached users successfully' -sev Debug } catch { From 9969b9f96a0cb0fba25091b04c48efd60891d76f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Wed, 21 Jan 2026 23:23:29 +0100 Subject: [PATCH 229/503] feat(named-locations): support removing multiple IPs and locations - Updated Set-CIPPNamedLocation to handle multiple IPs and locations for removal. - Enhanced Invoke-ExecNamedLocation to correctly process input values. --- .../Tenant/Conditional/Invoke-ExecNamedLocation.ps1 | 11 ++++------- Modules/CIPPCore/Public/Set-CIPPNamedLocation.ps1 | 10 ++++++---- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Conditional/Invoke-ExecNamedLocation.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Conditional/Invoke-ExecNamedLocation.ps1 index 95bfae0262f2..1a7c6b021b23 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Conditional/Invoke-ExecNamedLocation.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Conditional/Invoke-ExecNamedLocation.ps1 @@ -11,28 +11,25 @@ function Invoke-ExecNamedLocation { $APIName = $Request.Params.CIPPEndpoint $Headers = $Request.Headers - - # Interact with query parameters or the body of the request. $TenantFilter = $Request.Body.tenantFilter ?? $Request.Query.tenantFilter $NamedLocationId = $Request.Body.namedLocationId ?? $Request.Query.namedLocationId $Change = $Request.Body.change ?? $Request.Query.change - $Content = $Request.Body.input ?? $Request.Query.input - if ($content.value) { $content = $content.value } + $Content = $Request.Body.input.value ?? $Request.Query.input.value try { - $results = Set-CIPPNamedLocation -NamedLocationId $NamedLocationId -TenantFilter $TenantFilter -Change $Change -Content $Content -Headers $Headers + $Results = Set-CIPPNamedLocation -NamedLocationId $NamedLocationId -TenantFilter $TenantFilter -Change $Change -Content $Content -Headers $Headers $StatusCode = [HttpStatusCode]::OK } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -headers $Headers -API $APIName -message "Failed to edit named location: $($ErrorMessage.NormalizedError)" -Sev 'Error' -tenant $TenantFilter -LogData $ErrorMessage - $results = "Failed to edit named location. Error: $($ErrorMessage.NormalizedError)" + $Results = "Failed to edit named location. Error: $($ErrorMessage.NormalizedError)" $StatusCode = [HttpStatusCode]::InternalServerError } return ([HttpResponseContext]@{ StatusCode = $StatusCode - Body = @{'Results' = @($results) } + Body = @{'Results' = @($Results) } }) } diff --git a/Modules/CIPPCore/Public/Set-CIPPNamedLocation.ps1 b/Modules/CIPPCore/Public/Set-CIPPNamedLocation.ps1 index 888622741f86..3e23d70b8edf 100644 --- a/Modules/CIPPCore/Public/Set-CIPPNamedLocation.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPNamedLocation.ps1 @@ -24,12 +24,14 @@ function Set-CIPPNamedLocation { $ActionDescription = "Adding location $Content to named location" } 'removeIp' { - $NamedLocations.ipRanges = @($NamedLocations.ipRanges | Where-Object -Property cidrAddress -NE $Content) - $ActionDescription = "Removing IP $Content from named location" + $IpsToRemove = @($Content) + $NamedLocations.ipRanges = @($NamedLocations.ipRanges | Where-Object { $_.cidrAddress -notin $IpsToRemove }) + $ActionDescription = "Removing IP(s) $($IpsToRemove -join ', ') from named location" } 'removeLocation' { - $NamedLocations.countriesAndRegions = @($NamedLocations.countriesAndRegions | Where-Object { $_ -NE $Content }) - $ActionDescription = "Removing location $Content from named location" + $LocationsToRemove = @($Content) + $NamedLocations.countriesAndRegions = @($NamedLocations.countriesAndRegions | Where-Object { $_ -notin $LocationsToRemove }) + $ActionDescription = "Removing location(s) $($LocationsToRemove -join ', ') from named location" } 'rename' { $NamedLocations.displayName = $Content From 16a6a11aa79fbae1632f2f3582b91d055420151d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Wed, 21 Jan 2026 23:23:29 +0100 Subject: [PATCH 230/503] feat(named-locations): support removing multiple IPs and locations - Updated Set-CIPPNamedLocation to handle multiple IPs and locations for removal. - Enhanced Invoke-ExecNamedLocation to correctly process input values. --- .../Tenant/Conditional/Invoke-ExecNamedLocation.ps1 | 11 ++++------- Modules/CIPPCore/Public/Set-CIPPNamedLocation.ps1 | 10 ++++++---- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Conditional/Invoke-ExecNamedLocation.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Conditional/Invoke-ExecNamedLocation.ps1 index 95bfae0262f2..1a7c6b021b23 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Conditional/Invoke-ExecNamedLocation.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Conditional/Invoke-ExecNamedLocation.ps1 @@ -11,28 +11,25 @@ function Invoke-ExecNamedLocation { $APIName = $Request.Params.CIPPEndpoint $Headers = $Request.Headers - - # Interact with query parameters or the body of the request. $TenantFilter = $Request.Body.tenantFilter ?? $Request.Query.tenantFilter $NamedLocationId = $Request.Body.namedLocationId ?? $Request.Query.namedLocationId $Change = $Request.Body.change ?? $Request.Query.change - $Content = $Request.Body.input ?? $Request.Query.input - if ($content.value) { $content = $content.value } + $Content = $Request.Body.input.value ?? $Request.Query.input.value try { - $results = Set-CIPPNamedLocation -NamedLocationId $NamedLocationId -TenantFilter $TenantFilter -Change $Change -Content $Content -Headers $Headers + $Results = Set-CIPPNamedLocation -NamedLocationId $NamedLocationId -TenantFilter $TenantFilter -Change $Change -Content $Content -Headers $Headers $StatusCode = [HttpStatusCode]::OK } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -headers $Headers -API $APIName -message "Failed to edit named location: $($ErrorMessage.NormalizedError)" -Sev 'Error' -tenant $TenantFilter -LogData $ErrorMessage - $results = "Failed to edit named location. Error: $($ErrorMessage.NormalizedError)" + $Results = "Failed to edit named location. Error: $($ErrorMessage.NormalizedError)" $StatusCode = [HttpStatusCode]::InternalServerError } return ([HttpResponseContext]@{ StatusCode = $StatusCode - Body = @{'Results' = @($results) } + Body = @{'Results' = @($Results) } }) } diff --git a/Modules/CIPPCore/Public/Set-CIPPNamedLocation.ps1 b/Modules/CIPPCore/Public/Set-CIPPNamedLocation.ps1 index 888622741f86..3e23d70b8edf 100644 --- a/Modules/CIPPCore/Public/Set-CIPPNamedLocation.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPNamedLocation.ps1 @@ -24,12 +24,14 @@ function Set-CIPPNamedLocation { $ActionDescription = "Adding location $Content to named location" } 'removeIp' { - $NamedLocations.ipRanges = @($NamedLocations.ipRanges | Where-Object -Property cidrAddress -NE $Content) - $ActionDescription = "Removing IP $Content from named location" + $IpsToRemove = @($Content) + $NamedLocations.ipRanges = @($NamedLocations.ipRanges | Where-Object { $_.cidrAddress -notin $IpsToRemove }) + $ActionDescription = "Removing IP(s) $($IpsToRemove -join ', ') from named location" } 'removeLocation' { - $NamedLocations.countriesAndRegions = @($NamedLocations.countriesAndRegions | Where-Object { $_ -NE $Content }) - $ActionDescription = "Removing location $Content from named location" + $LocationsToRemove = @($Content) + $NamedLocations.countriesAndRegions = @($NamedLocations.countriesAndRegions | Where-Object { $_ -notin $LocationsToRemove }) + $ActionDescription = "Removing location(s) $($LocationsToRemove -join ', ') from named location" } 'rename' { $NamedLocations.displayName = $Content From 4d5387a65b7576fda534c5519fdb10c09e4e773d Mon Sep 17 00:00:00 2001 From: John Duprey Date: Wed, 21 Jan 2026 17:39:56 -0500 Subject: [PATCH 231/503] Improve batch processing and error logging in CIPP modules Add-CIPPDbItem.ps1 now dynamically calculates batch size based on available memory and estimated item size, improving efficiency and stability for large datasets. Both Add-CIPPDbItem.ps1 and Set-CIPPDBCacheUsers.ps1 now include enhanced error logging with detailed exception data using Get-CippException. --- Modules/CIPPCore/Public/Add-CIPPDbItem.ps1 | 22 +++++++++++++++---- .../CIPPCore/Public/Set-CIPPDBCacheUsers.ps1 | 2 +- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/Modules/CIPPCore/Public/Add-CIPPDbItem.ps1 b/Modules/CIPPCore/Public/Add-CIPPDbItem.ps1 index 9876118722b8..a4bde901ae8a 100644 --- a/Modules/CIPPCore/Public/Add-CIPPDbItem.ps1 +++ b/Modules/CIPPCore/Public/Add-CIPPDbItem.ps1 @@ -70,11 +70,25 @@ function Add-CIPPDbItem { Remove-AzDataTableEntity @Table -Entity $ExistingEntities -Force | Out-Null } - # Process in batches to avoid memory issues with large datasets - $BatchSize = 1000 + # Calculate batch size based on available memory + $AvailableMemory = [System.GC]::GetTotalMemory($false) + $AvailableMemoryMB = [math]::Round($AvailableMemory / 1MB, 2) + + # Estimate item size from first item (with fallback) + $EstimatedItemSizeBytes = 1KB # Default assumption + if ($Data.Count -gt 0) { + $SampleJson = $Data[0] | ConvertTo-Json -Depth 10 -Compress + $EstimatedItemSizeBytes = [System.Text.Encoding]::UTF8.GetByteCount($SampleJson) + } + + # Use 25% of available memory for batch processing, with min/max bounds + $TargetBatchMemoryMB = [Math]::Max(50, $AvailableMemoryMB * 0.25) + $CalculatedBatchSize = [Math]::Floor(($TargetBatchMemoryMB * 1MB) / $EstimatedItemSizeBytes) + $BatchSize = [Math]::Max(100, [Math]::Min(2000, $CalculatedBatchSize)) + $TotalCount = $Data.Count $ProcessedCount = 0 - Write-Information "Adding $TotalCount items of type $Type to CIPP Reporting DB for tenant $TenantFilter in batches of $BatchSize" + Write-Information "Adding $TotalCount items of type $Type to CIPP Reporting DB for tenant $TenantFilter | Available Memory: ${AvailableMemoryMB}MB | Batch Size: $BatchSize (est. item size: $([math]::Round($EstimatedItemSizeBytes/1KB, 2))KB)" for ($i = 0; $i -lt $TotalCount; $i += $BatchSize) { $BatchEnd = [Math]::Min($i + $BatchSize, $TotalCount) $Batch = $Data[$i..($BatchEnd - 1)] @@ -102,7 +116,7 @@ function Add-CIPPDbItem { Write-LogMessage -API 'CIPPDbItem' -tenant $TenantFilter -message "Added $($Data.Count) items of type $Type$(if ($Count) { ' (count mode)' })" -sev Debug } catch { - Write-LogMessage -API 'CIPPDbItem' -tenant $TenantFilter -message "Failed to add items of type $Type : $($_.Exception.Message)" -sev Error + Write-LogMessage -API 'CIPPDbItem' -tenant $TenantFilter -message "Failed to add items of type $Type : $($_.Exception.Message)" -sev Error -LogData (Get-CippException -Exception $_) throw } } diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheUsers.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheUsers.ps1 index 7c23b2f4a9cb..3355fbdd9f33 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheUsers.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheUsers.ps1 @@ -32,6 +32,6 @@ function Set-CIPPDBCacheUsers { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached users successfully' -sev Debug } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache users: $($_.Exception.Message)" -sev Error + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache users: $($_.Exception.Message)" -sev Error -LogData (Get-CippException -Exception $_) } } From b03a0b9d2242ba2d440cd87230a29a14a4689eb4 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Wed, 21 Jan 2026 17:50:09 -0500 Subject: [PATCH 232/503] Lower max batch size to 500 in Add-CIPPDbItem Reduced the maximum batch size from 2000 to 500 to prevent out-of-memory errors with large datasets. Updated the information log to include target memory and calculated batch size for better diagnostics. --- Modules/CIPPCore/Public/Add-CIPPDbItem.ps1 | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Modules/CIPPCore/Public/Add-CIPPDbItem.ps1 b/Modules/CIPPCore/Public/Add-CIPPDbItem.ps1 index a4bde901ae8a..bd9c4d733eff 100644 --- a/Modules/CIPPCore/Public/Add-CIPPDbItem.ps1 +++ b/Modules/CIPPCore/Public/Add-CIPPDbItem.ps1 @@ -84,11 +84,12 @@ function Add-CIPPDbItem { # Use 25% of available memory for batch processing, with min/max bounds $TargetBatchMemoryMB = [Math]::Max(50, $AvailableMemoryMB * 0.25) $CalculatedBatchSize = [Math]::Floor(($TargetBatchMemoryMB * 1MB) / $EstimatedItemSizeBytes) - $BatchSize = [Math]::Max(100, [Math]::Min(2000, $CalculatedBatchSize)) + # Reduce max to 500 to prevent OOM with large datasets + $BatchSize = [Math]::Max(100, [Math]::Min(500, $CalculatedBatchSize)) $TotalCount = $Data.Count $ProcessedCount = 0 - Write-Information "Adding $TotalCount items of type $Type to CIPP Reporting DB for tenant $TenantFilter | Available Memory: ${AvailableMemoryMB}MB | Batch Size: $BatchSize (est. item size: $([math]::Round($EstimatedItemSizeBytes/1KB, 2))KB)" + Write-Information "Adding $TotalCount items of type $Type to CIPP Reporting DB for tenant $TenantFilter | Available Memory: ${AvailableMemoryMB}MB | Target Memory: ${TargetBatchMemoryMB}MB | Calculated: $CalculatedBatchSize | Batch Size: $BatchSize (est. item size: $([math]::Round($EstimatedItemSizeBytes/1KB, 2))KB)" for ($i = 0; $i -lt $TotalCount; $i += $BatchSize) { $BatchEnd = [Math]::Min($i + $BatchSize, $TotalCount) $Batch = $Data[$i..($BatchEnd - 1)] From f8670f8f1d0d0d97e6501fc0aac4a4463128f7b7 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Wed, 21 Jan 2026 18:32:34 -0500 Subject: [PATCH 233/503] Improve memory management in user and batch processing Enhanced memory cleanup in Add-CIPPDbItem and Set-CIPPDBCacheUsers by explicitly clearing variables and invoking garbage collection with logging. Optimized hashtable property removal in Add-CIPPAzDataTableEntity for better performance and reliability. --- .../CIPPCore/Public/Add-CIPPAzDataTableEntity.ps1 | 8 ++++---- Modules/CIPPCore/Public/Add-CIPPDbItem.ps1 | 15 ++++++++++++++- Modules/CIPPCore/Public/Set-CIPPDBCacheUsers.ps1 | 6 ++++++ 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/Modules/CIPPCore/Public/Add-CIPPAzDataTableEntity.ps1 b/Modules/CIPPCore/Public/Add-CIPPAzDataTableEntity.ps1 index 260f8691f0f1..8c1879993ec1 100644 --- a/Modules/CIPPCore/Public/Add-CIPPAzDataTableEntity.ps1 +++ b/Modules/CIPPCore/Public/Add-CIPPAzDataTableEntity.ps1 @@ -65,11 +65,11 @@ function Add-CIPPAzDataTableEntity { try { # Ensure all property values are not null for string properties if ($SingleEnt -is [hashtable]) { - foreach ($key in @($SingleEnt.Keys)) { - if ($null -eq $SingleEnt[$key]) { - $SingleEnt.Remove($key) - } + $keysToRemove = @($SingleEnt.Keys | Where-Object { $null -eq $SingleEnt[$_] }) + foreach ($key in $keysToRemove) { + $SingleEnt.Remove($key) } + $keysToRemove = $null } elseif ($SingleEnt -is [PSCustomObject]) { $propsToRemove = [system.Collections.Generic.List[string]]::new() foreach ($prop in $SingleEnt.PSObject.Properties) { diff --git a/Modules/CIPPCore/Public/Add-CIPPDbItem.ps1 b/Modules/CIPPCore/Public/Add-CIPPDbItem.ps1 index bd9c4d733eff..ec27d4688b16 100644 --- a/Modules/CIPPCore/Public/Add-CIPPDbItem.ps1 +++ b/Modules/CIPPCore/Public/Add-CIPPDbItem.ps1 @@ -110,7 +110,20 @@ function Add-CIPPDbItem { # Clear batch variables to free memory $Entities = $null $Batch = $null - [System.GC]::Collect() + + # Capture memory before GC + $MemoryBeforeGC = [System.GC]::GetTotalMemory($false) + + # Force GC between batches to prevent accumulation + [System.GC]::Collect([System.GC]::MaxGeneration, [System.GCCollectionMode]::Forced) + [System.GC]::WaitForPendingFinalizers() + [System.GC]::Collect([System.GC]::MaxGeneration, [System.GCCollectionMode]::Forced) + + # Log memory usage after GC + $MemoryAfterGC = [System.GC]::GetTotalMemory($false) + $FreedMB = [math]::Round(($MemoryBeforeGC - $MemoryAfterGC) / 1MB, 2) + $CurrentMemoryMB = [math]::Round($MemoryAfterGC / 1MB, 2) + Write-Information "Batch $([Math]::Ceiling($ProcessedCount / $BatchSize))/$([Math]::Ceiling($TotalCount / $BatchSize)) complete. Processed: $ProcessedCount/$TotalCount | Memory: ${CurrentMemoryMB}MB | Freed: ${FreedMB}MB" } } diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheUsers.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheUsers.ps1 index 3355fbdd9f33..e76fcef89640 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheUsers.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheUsers.ps1 @@ -32,6 +32,12 @@ function Set-CIPPDBCacheUsers { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached users successfully' -sev Debug } catch { + # Cleanup on error to prevent memory leak + if ($null -ne $Users) { + $Users.Clear() + $Users = $null + } + $UsersResponse = $null Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache users: $($_.Exception.Message)" -sev Error -LogData (Get-CippException -Exception $_) } } From 48875ba6b020357e4df54a678f8b9cb51245ea92 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Wed, 21 Jan 2026 18:53:13 -0500 Subject: [PATCH 234/503] Revert "Improve memory management in user and batch processing" This reverts commit f8670f8f1d0d0d97e6501fc0aac4a4463128f7b7. --- .../CIPPCore/Public/Add-CIPPAzDataTableEntity.ps1 | 8 ++++---- Modules/CIPPCore/Public/Add-CIPPDbItem.ps1 | 15 +-------------- Modules/CIPPCore/Public/Set-CIPPDBCacheUsers.ps1 | 6 ------ 3 files changed, 5 insertions(+), 24 deletions(-) diff --git a/Modules/CIPPCore/Public/Add-CIPPAzDataTableEntity.ps1 b/Modules/CIPPCore/Public/Add-CIPPAzDataTableEntity.ps1 index 8c1879993ec1..260f8691f0f1 100644 --- a/Modules/CIPPCore/Public/Add-CIPPAzDataTableEntity.ps1 +++ b/Modules/CIPPCore/Public/Add-CIPPAzDataTableEntity.ps1 @@ -65,11 +65,11 @@ function Add-CIPPAzDataTableEntity { try { # Ensure all property values are not null for string properties if ($SingleEnt -is [hashtable]) { - $keysToRemove = @($SingleEnt.Keys | Where-Object { $null -eq $SingleEnt[$_] }) - foreach ($key in $keysToRemove) { - $SingleEnt.Remove($key) + foreach ($key in @($SingleEnt.Keys)) { + if ($null -eq $SingleEnt[$key]) { + $SingleEnt.Remove($key) + } } - $keysToRemove = $null } elseif ($SingleEnt -is [PSCustomObject]) { $propsToRemove = [system.Collections.Generic.List[string]]::new() foreach ($prop in $SingleEnt.PSObject.Properties) { diff --git a/Modules/CIPPCore/Public/Add-CIPPDbItem.ps1 b/Modules/CIPPCore/Public/Add-CIPPDbItem.ps1 index ec27d4688b16..bd9c4d733eff 100644 --- a/Modules/CIPPCore/Public/Add-CIPPDbItem.ps1 +++ b/Modules/CIPPCore/Public/Add-CIPPDbItem.ps1 @@ -110,20 +110,7 @@ function Add-CIPPDbItem { # Clear batch variables to free memory $Entities = $null $Batch = $null - - # Capture memory before GC - $MemoryBeforeGC = [System.GC]::GetTotalMemory($false) - - # Force GC between batches to prevent accumulation - [System.GC]::Collect([System.GC]::MaxGeneration, [System.GCCollectionMode]::Forced) - [System.GC]::WaitForPendingFinalizers() - [System.GC]::Collect([System.GC]::MaxGeneration, [System.GCCollectionMode]::Forced) - - # Log memory usage after GC - $MemoryAfterGC = [System.GC]::GetTotalMemory($false) - $FreedMB = [math]::Round(($MemoryBeforeGC - $MemoryAfterGC) / 1MB, 2) - $CurrentMemoryMB = [math]::Round($MemoryAfterGC / 1MB, 2) - Write-Information "Batch $([Math]::Ceiling($ProcessedCount / $BatchSize))/$([Math]::Ceiling($TotalCount / $BatchSize)) complete. Processed: $ProcessedCount/$TotalCount | Memory: ${CurrentMemoryMB}MB | Freed: ${FreedMB}MB" + [System.GC]::Collect() } } diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheUsers.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheUsers.ps1 index e76fcef89640..3355fbdd9f33 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheUsers.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheUsers.ps1 @@ -32,12 +32,6 @@ function Set-CIPPDBCacheUsers { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached users successfully' -sev Debug } catch { - # Cleanup on error to prevent memory leak - if ($null -ne $Users) { - $Users.Clear() - $Users = $null - } - $UsersResponse = $null Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache users: $($_.Exception.Message)" -sev Error -LogData (Get-CippException -Exception $_) } } From 99c2ff4ea89ae366ebce1b90a965124d2e3dbe8a Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Thu, 22 Jan 2026 10:19:17 +0100 Subject: [PATCH 235/503] data collection sequence change --- .../Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 | 1 - Modules/CIPPCore/Public/Set-CIPPDBCacheMailboxes.ps1 | 1 - 2 files changed, 2 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 index 10992abe278b..dca6a6e87de8 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 @@ -179,7 +179,6 @@ function Push-CIPPDBCacheData { OrchestratorName = "CIPPDBCacheTenant_$TenantFilter" Batch = @($Batch) SkipLog = $true - DurableMode = 'Sequence' } if ($Item.TestRun -eq $true) { diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheMailboxes.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheMailboxes.ps1 index e169189600de..8c78abbdab4b 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheMailboxes.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheMailboxes.ps1 @@ -97,7 +97,6 @@ function Set-CIPPDBCacheMailboxes { $InputObject = [PSCustomObject]@{ Batch = @($Batches) OrchestratorName = "MailboxPermissions_$TenantFilter" - DurableMode = 'Sequence' PostExecution = @{ FunctionName = 'StoreMailboxPermissions' Parameters = @{ From 96ffe06f8d8f071fc9cab0ae1f42c7fe3648a136 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Thu, 22 Jan 2026 11:26:39 +0100 Subject: [PATCH 236/503] update null saftey so it does not generate error when loading data --- Modules/CIPPCore/Public/Set-CIPPDBCacheApps.ps1 | 1 + Modules/CIPPCore/Public/Set-CIPPDBCacheDevices.ps1 | 1 + Modules/CIPPCore/Public/Set-CIPPDBCacheGuests.ps1 | 1 + Modules/CIPPCore/Public/Set-CIPPDBCacheSettings.ps1 | 4 ++-- 4 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheApps.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheApps.ps1 index 22200d76dbf3..a0f1c667be2f 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheApps.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheApps.ps1 @@ -16,6 +16,7 @@ function Set-CIPPDBCacheApps { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching applications' -sev Debug $Apps = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/applications?$top=999&expand=owners' -tenantid $TenantFilter + if (!$Apps) { $Apps = @() } Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Apps' -Data $Apps Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Apps' -Data $Apps -Count $Apps = $null diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheDevices.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheDevices.ps1 index 2953483d47a7..63996ee51294 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheDevices.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheDevices.ps1 @@ -16,6 +16,7 @@ function Set-CIPPDBCacheDevices { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching Azure AD devices' -sev Debug $Devices = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/devices?$top=999&$select=id,displayName,operatingSystem,operatingSystemVersion,trustType,accountEnabled,approximateLastSignInDateTime' -tenantid $TenantFilter + if (!$Devices) { $Devices = @() } Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Devices' -Data $Devices Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Devices' -Data $Devices -Count $Devices = $null diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheGuests.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheGuests.ps1 index 0fc9a324ef6a..36abaabef11d 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheGuests.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheGuests.ps1 @@ -16,6 +16,7 @@ function Set-CIPPDBCacheGuests { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching guest users' -sev Debug $Guests = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/users?`$filter=userType eq 'Guest'&`$expand=sponsors&`$top=999" -tenantid $TenantFilter + if (!$Guests) { $Guests = @() } Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Guests' -Data $Guests Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Guests' -Data $Guests -Count $Guests = $null diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheSettings.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheSettings.ps1 index 08aee0f383a7..76c682ba3adb 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheSettings.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheSettings.ps1 @@ -16,12 +16,12 @@ function Set-CIPPDBCacheSettings { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching directory settings' -sev Debug $Settings = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/settings?$top=999' -tenantid $TenantFilter + if(!$Settings){ $Settings = @()} Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Settings' -Data $Settings $Settings = $null Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached directory settings successfully' -sev Debug } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter ` - -message "Failed to cache directory settings: $($_.Exception.Message)" -sev Error + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache directory settings: $($_.Exception.Message)" -sev Error } } From 19ea1a3313a37f103552115dbffcf1deff848311 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Thu, 22 Jan 2026 11:34:24 +0100 Subject: [PATCH 237/503] fixes array compare --- .../Invoke-CIPPStandardSpamFilterPolicy.ps1 | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSpamFilterPolicy.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSpamFilterPolicy.ps1 index 5a9fd298f25c..dfbc4ef14ff7 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSpamFilterPolicy.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSpamFilterPolicy.ps1 @@ -64,8 +64,8 @@ function Invoke-CIPPStandardSpamFilterPolicy { try { $CurrentState = New-ExoRequest -TenantId $Tenant -cmdlet 'Get-HostedContentFilterPolicy' | - Where-Object -Property Name -EQ $PolicyName | - Select-Object -Property * + Where-Object -Property Name -EQ $PolicyName | + Select-Object -Property * } 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 @@ -136,8 +136,8 @@ 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 | - Select-Object -Property * + Where-Object -Property Name -EQ $PolicyName | + Select-Object -Property * $RuleStateIsCorrect = ($RuleState.Name -eq $PolicyName) -and ($RuleState.HostedContentFilterPolicy -eq $PolicyName) -and @@ -305,9 +305,9 @@ function Invoke-CIPPStandardSpamFilterPolicy { MarkAsSpamWebBugsInHtml = $MarkAsSpamWebBugsInHtml MarkAsSpamSensitiveWordList = $MarkAsSpamSensitiveWordList EnableLanguageBlockList = $Settings.EnableLanguageBlockList - LanguageBlockList = if ($Settings.EnableLanguageBlockList -eq $true) { $Settings.LanguageBlockList.value } else { @() } + LanguageBlockList = $Settings.EnableLanguageBlockList ? @($Settings.EnableLanguageBlockList) : @() EnableRegionBlockList = $Settings.EnableRegionBlockList - RegionBlockList = if ($Settings.EnableRegionBlockList -eq $true) { $Settings.RegionBlockList.value } else { @() } + RegionBlockList = $Settings.RegionBlockList.value ? @($Settings.RegionBlockList.value) : @() AllowedSenderDomains = $Settings.AllowedSenderDomains.value ?? @() } Set-CIPPStandardsCompareField -FieldName 'standards.SpamFilterPolicy' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -Tenant $Tenant From 8cb605104721258c98992f91a84a5aa439b17c25 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Thu, 22 Jan 2026 11:57:20 +0100 Subject: [PATCH 238/503] member troubleshooting for groups --- Modules/CIPPCore/Public/TenantGroups/Get-TenantGroups.ps1 | 1 + 1 file changed, 1 insertion(+) diff --git a/Modules/CIPPCore/Public/TenantGroups/Get-TenantGroups.ps1 b/Modules/CIPPCore/Public/TenantGroups/Get-TenantGroups.ps1 index 0b15f3f62402..74a0f4a65ef6 100644 --- a/Modules/CIPPCore/Public/TenantGroups/Get-TenantGroups.ps1 +++ b/Modules/CIPPCore/Public/TenantGroups/Get-TenantGroups.ps1 @@ -159,6 +159,7 @@ function Get-TenantGroups { if ($GroupMembers) { foreach ($Member in $GroupMembers) { # Use indexed lookup instead of Where-Object + if (!$Member.customerId) { continue } $Tenant = $TenantByCustomerId[$Member.customerId] if ($Tenant) { $MembersList.Add(@{ From 932f43a322bb66f7a8821a36b8763335392947a6 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Thu, 22 Jan 2026 12:12:22 +0100 Subject: [PATCH 239/503] fix compares --- .../Invoke-CIPPStandardDeployMailContact.ps1 | 13 ++++++++++++- .../Standards/Invoke-CIPPStandardOauthConsent.ps1 | 2 +- .../Invoke-CIPPStandardSafeAttachmentPolicy.ps1 | 12 ++++++------ .../Invoke-CIPPStandardSpamFilterPolicy.ps1 | 4 ++-- 4 files changed, 21 insertions(+), 10 deletions(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployMailContact.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployMailContact.ps1 index 4ee810e1f85a..8dc3df011a96 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployMailContact.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployMailContact.ps1 @@ -103,8 +103,19 @@ function Invoke-CIPPStandardDeployMailContact { # Report if ($Settings.report -eq $true) { $ReportData = $ContactData.Clone() + $ContactData = @{ + DisplayName = $Settings.DisplayName + ExternalEmailAddress = $Settings.ExternalEmailAddress + FirstName = $Settings.FirstName + LastName = $Settings.LastName + } $CurrentValue = $ExistingContact | Select-Object DisplayName, ExternalEmailAddress, FirstName, LastName - $ReportData.Exists = [bool]$ExistingContact + $currentValue = @{ + DisplayName = $ExistingContact.displayName + ExternalEmailAddress = ($ExistingContact.ExternalEmailAddress -replace 'SMTP:', '') + FirstName = $ExistingContact.firstName + LastName = $ExistingContact.lastName + } Add-CIPPBPAField -FieldName 'DeployMailContact' -FieldValue $ReportData -StoreAs json -Tenant $Tenant Set-CIPPStandardsCompareField -FieldName 'standards.DeployMailContact' -CurrentValue $CurrentValue -ExpectedValue $ReportData -Tenant $Tenant } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardOauthConsent.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardOauthConsent.ps1 index d4eee69db7a6..adc90a5a342d 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardOauthConsent.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardOauthConsent.ps1 @@ -104,7 +104,7 @@ function Invoke-CIPPStandardOauthConsent { permissionGrantPolicyIdsAssignedToDefaultUserRole = $State.permissionGrantPolicyIdsAssignedToDefaultUserRole } $ExpectedValue = @{ - permissionGrantPolicyIdsAssignedToDefaultUserRole = @('managePermissionGrantsForSelf.cipp-consent-policy') + permissionGrantPolicyIdsAssignedToDefaultUserRole = @('ManagePermissionGrantsForSelf.cipp-consent-policy') } Set-CIPPStandardsCompareField -FieldName 'standards.OauthConsent' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -Tenant $tenant } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeAttachmentPolicy.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeAttachmentPolicy.ps1 index 1627aaceaa5d..f23855cbbeee 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeAttachmentPolicy.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeAttachmentPolicy.ps1 @@ -76,8 +76,8 @@ function Invoke-CIPPStandardSafeAttachmentPolicy { try { $CurrentState = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-SafeAttachmentPolicy' | - Where-Object -Property Name -EQ $PolicyName | - Select-Object Name, Enable, Action, QuarantineTag, Redirect, RedirectAddress + Where-Object -Property Name -EQ $PolicyName | + Select-Object Name, Enable, Action, QuarantineTag, Redirect, RedirectAddress } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the SafeAttachmentPolicy state for $Tenant. Error: $ErrorMessage" -Sev Error @@ -94,8 +94,8 @@ function Invoke-CIPPStandardSafeAttachmentPolicy { $AcceptedDomains = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-AcceptedDomain' $RuleState = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-SafeAttachmentRule' | - 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 @@ -186,13 +186,13 @@ function Invoke-CIPPStandardSafeAttachmentPolicy { redirectAddress = $CurrentState.RedirectAddress } - $ExpectedValue = @{ + $ExpectedValue = [pscustomobject]@{ name = $PolicyName enable = $true action = $Settings.SafeAttachmentAction quarantineTag = $Settings.QuarantineTag redirect = $Settings.Redirect - redirectAddress = $Settings.RedirectAddress + redirectAddress = "$($Settings.RedirectAddress)" } Set-CIPPStandardsCompareField -FieldName 'standards.SafeAttachmentPolicy' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -Tenant $Tenant } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSpamFilterPolicy.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSpamFilterPolicy.ps1 index dfbc4ef14ff7..32a9cb7f22b1 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSpamFilterPolicy.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSpamFilterPolicy.ps1 @@ -284,7 +284,7 @@ function Invoke-CIPPStandardSpamFilterPolicy { RegionBlockList = $CurrentState.RegionBlockList AllowedSenderDomains = $CurrentState.AllowedSenderDomains } - $ExpectedValue = @{ + $ExpectedValue = [pscustomobject]@{ Name = $PolicyName SpamAction = $SpamAction SpamQuarantineTag = $SpamQuarantineTag @@ -308,7 +308,7 @@ function Invoke-CIPPStandardSpamFilterPolicy { LanguageBlockList = $Settings.EnableLanguageBlockList ? @($Settings.EnableLanguageBlockList) : @() EnableRegionBlockList = $Settings.EnableRegionBlockList RegionBlockList = $Settings.RegionBlockList.value ? @($Settings.RegionBlockList.value) : @() - AllowedSenderDomains = $Settings.AllowedSenderDomains.value ?? @() + AllowedSenderDomains = $Settings.AllowedSenderDomains.value ? @($Settings.AllowedSenderDomains.value) : @() } Set-CIPPStandardsCompareField -FieldName 'standards.SpamFilterPolicy' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -Tenant $Tenant } From 71b0a1890a9634901222d2125ae664c468fd2ed1 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Thu, 22 Jan 2026 12:16:09 +0100 Subject: [PATCH 240/503] compares --- .../Public/Standards/Invoke-CIPPStandardGroupTemplate.ps1 | 6 ++---- .../Public/Standards/Invoke-CIPPStandardSpoofWarn.ps1 | 2 +- .../Standards/Invoke-CIPPStandardUserSubmissions.ps1 | 8 ++++---- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardGroupTemplate.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardGroupTemplate.ps1 index 0024b7d7a5ee..a7539fe40fd1 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardGroupTemplate.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardGroupTemplate.ps1 @@ -242,12 +242,10 @@ function Invoke-CIPPStandardGroupTemplate { } $CurrentValue = @{ - ExistingGroups = $existingGroups.displayName - MissingGroups = @($MissingGroups) + MissingGroups = $MissingGroups ? @($MissingGroups) : @() } $ExpectedValue = @{ - ExistingGroups = $GroupTemplates.displayName - MissingGroups = @() + MissingGroups = @() } Set-CIPPStandardsCompareField -FieldName 'standards.GroupTemplate' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSpoofWarn.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSpoofWarn.ps1 index 8155161f5954..fc1989311104 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSpoofWarn.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSpoofWarn.ps1 @@ -139,7 +139,7 @@ function Invoke-CIPPStandardSpoofWarn { } $ExpectedValue = @{ Enabled = $IsEnabled - AllowList = @($Settings.AllowListAdd.value ?? $Settings.AllowListAdd ?? @()) + AllowList = $Settings.AllowListAdd.value ? @($Settings.AllowListAdd.value) : @() IsCompliant = $true } Set-CIPPStandardsCompareField -FieldName 'standards.SpoofWarn' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -Tenant $Tenant diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardUserSubmissions.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardUserSubmissions.ps1 index f2867357c2f8..d0653e4d2c8f 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardUserSubmissions.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardUserSubmissions.ps1 @@ -225,9 +225,9 @@ function Invoke-CIPPStandardUserSubmissions { ReportJunkToCustomizedAddress = if ([string]::IsNullOrWhiteSpace($Email)) { $false } else { $true } ReportNotJunkToCustomizedAddress = if ([string]::IsNullOrWhiteSpace($Email)) { $false } else { $true } ReportPhishToCustomizedAddress = if ([string]::IsNullOrWhiteSpace($Email)) { $false } else { $true } - ReportJunkAddresses = if ([string]::IsNullOrWhiteSpace($Email)) { $null } else { $Email } - ReportNotJunkAddresses = if ([string]::IsNullOrWhiteSpace($Email)) { $null } else { $Email } - ReportPhishAddresses = if ([string]::IsNullOrWhiteSpace($Email)) { $null } else { $Email } + ReportJunkAddresses = if ([string]::IsNullOrWhiteSpace($Email)) { $null } else { @($Email) } + ReportNotJunkAddresses = if ([string]::IsNullOrWhiteSpace($Email)) { $null } else { @($Email) } + ReportPhishAddresses = if ([string]::IsNullOrWhiteSpace($Email)) { $null } else { @($Email) } RuleState = if ([string]::IsNullOrWhiteSpace($Email)) { @{ State = 'Disabled' @@ -236,7 +236,7 @@ function Invoke-CIPPStandardUserSubmissions { } else { @{ State = 'Enabled' - SentTo = $Email + SentTo = @($Email) } } } From b624a7bcdb43a3b93be369c59f1010e949598cc9 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Thu, 22 Jan 2026 12:19:52 +0100 Subject: [PATCH 241/503] up version --- host.json | 2 +- version_latest.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/host.json b/host.json index fcd66850583e..03a1061b6423 100644 --- a/host.json +++ b/host.json @@ -16,7 +16,7 @@ "distributedTracingEnabled": false, "version": "None" }, - "defaultVersion": "10.0.3", + "defaultVersion": "10.0.4", "versionMatchStrategy": "Strict", "versionFailureStrategy": "Fail" } diff --git a/version_latest.txt b/version_latest.txt index 6a7144d3047f..6b48f258c7a4 100644 --- a/version_latest.txt +++ b/version_latest.txt @@ -1 +1 @@ -10.0.3 +10.0.4 From 17c92d6e45d46b94d3752032bae99a7b890c67ec Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Thu, 22 Jan 2026 13:01:15 +0100 Subject: [PATCH 242/503] managed device cache --- Modules/CIPPCore/Public/Set-CIPPDBCacheManagedDevices.ps1 | 1 + 1 file changed, 1 insertion(+) diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheManagedDevices.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheManagedDevices.ps1 index 3e41edab2337..ee724f9343af 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheManagedDevices.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheManagedDevices.ps1 @@ -15,6 +15,7 @@ function Set-CIPPDBCacheManagedDevices { try { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching managed devices' -sev Debug $ManagedDevices = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/deviceManagement/managedDevices?$top=999&$select=id,deviceName,operatingSystem,osVersion,complianceState,managedDeviceOwnerType,enrolledDateTime,lastSyncDateTime' -tenantid $TenantFilter + if (!$ManagedDevices) { $ManagedDevices = @() } Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ManagedDevices' -Data $ManagedDevices Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ManagedDevices' -Data $ManagedDevices -Count $ManagedDevices = $null From 2ddf931505389626a2f835d484263e9b4e1f8ebd Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Thu, 22 Jan 2026 13:43:02 +0100 Subject: [PATCH 243/503] Pushy-cippTest --- .../Entrypoints/Activity Triggers/Tests/Push-CIPPTest.ps1 | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Tests/Push-CIPPTest.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Tests/Push-CIPPTest.ps1 index 7492a7c15abc..844c8ff80695 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Tests/Push-CIPPTest.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Tests/Push-CIPPTest.ps1 @@ -22,6 +22,8 @@ function Push-CIPPTest { Write-Information "Executing $FunctionName for $TenantFilter" & $FunctionName -Tenant $TenantFilter + Write-Host "Returning true, test has run for $tenantFilter" + return @{ testRun = $true } } catch { $ErrorMessage = Get-CippException -Exception $_ From e01d6af29303b63664b65000d392f093fd54b47f Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Thu, 22 Jan 2026 14:02:07 +0100 Subject: [PATCH 244/503] rerun protection --- .../Activity Triggers/Tests/Invoke-CIPPTestsRun.ps1 | 8 ++++++++ Modules/CIPPCore/Public/Test-CIPPRerun.ps1 | 1 + 2 files changed, 9 insertions(+) diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Tests/Invoke-CIPPTestsRun.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Tests/Invoke-CIPPTestsRun.ps1 index af97857c8e67..ffcc15fa808a 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Tests/Invoke-CIPPTestsRun.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Tests/Invoke-CIPPTestsRun.ps1 @@ -13,6 +13,14 @@ function Invoke-CIPPTestsRun { Write-Information "Starting tests run for tenant: $TenantFilter" + Write-Host 'Checking rerun protection' + $RerunParams = @{ + TenantFilter = $TenantFilter + Type = 'CippTests' + API = 'CippTests' + } + $Rerun = Test-CIPPRerun @RerunParams + if ($Rerun -eq $true) { return $true } try { $AllTests = Get-Command -Name 'Invoke-CippTest*' -Module CIPPCore | Select-Object -ExpandProperty Name | ForEach-Object { $_ -replace '^Invoke-CippTest', '' diff --git a/Modules/CIPPCore/Public/Test-CIPPRerun.ps1 b/Modules/CIPPCore/Public/Test-CIPPRerun.ps1 index 87caf6fc8950..ac19ef9039e2 100644 --- a/Modules/CIPPCore/Public/Test-CIPPRerun.ps1 +++ b/Modules/CIPPCore/Public/Test-CIPPRerun.ps1 @@ -20,6 +20,7 @@ function Test-CIPPRerun { $EstimatedDifference = switch ($Type) { 'Standard' { 9800 } # 2 hours 45 minutes ish. 'BPA' { 85000 } # 24 hours ish. + 'CippTests' { 85000 } # 24 hours ish. default { throw "Unknown type: $Type" } } } From 6bc3245f897d2e37f692b3b006c1edac5121568f Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Thu, 22 Jan 2026 14:08:03 +0100 Subject: [PATCH 245/503] eleviation --- .../Activity Triggers/Tests/Invoke-CIPPTestsRun.ps1 | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Tests/Invoke-CIPPTestsRun.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Tests/Invoke-CIPPTestsRun.ps1 index ffcc15fa808a..8a407db9b3b9 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Tests/Invoke-CIPPTestsRun.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Tests/Invoke-CIPPTestsRun.ps1 @@ -20,7 +20,10 @@ function Invoke-CIPPTestsRun { API = 'CippTests' } $Rerun = Test-CIPPRerun @RerunParams - if ($Rerun -eq $true) { return $true } + if ($Rerun -eq $true) { + Write-Host "rerun is true for $($TenantFilter)" + return $true + } try { $AllTests = Get-Command -Name 'Invoke-CippTest*' -Module CIPPCore | Select-Object -ExpandProperty Name | ForEach-Object { $_ -replace '^Invoke-CippTest', '' From 03ef8e6d10db8f449a1bbf851a36a3a1e2e19793 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Thu, 22 Jan 2026 14:14:04 +0100 Subject: [PATCH 246/503] Fix for db rerun --- .../Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 | 2 +- .../Activity Triggers/Tests/Invoke-CIPPTestsRun.ps1 | 2 +- .../Entrypoints/Activity Triggers/Tests/Push-CIPPTestsRun.ps1 | 4 ++-- .../Orchestrator Functions/Start-TestsOrchestrator.ps1 | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 index dca6a6e87de8..6bdf8f889ddf 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 @@ -183,7 +183,7 @@ function Push-CIPPDBCacheData { if ($Item.TestRun -eq $true) { $InputObject | Add-Member -NotePropertyName PostExecution -NotePropertyValue @{ - FunctionName = 'CIPPTestsRun' + FunctionName = 'CIPPDBTestsRun' Parameters = @{ TenantFilter = $TenantFilter } diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Tests/Invoke-CIPPTestsRun.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Tests/Invoke-CIPPTestsRun.ps1 index 8a407db9b3b9..7c30210d90a3 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Tests/Invoke-CIPPTestsRun.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Tests/Invoke-CIPPTestsRun.ps1 @@ -1,4 +1,4 @@ -function Invoke-CIPPTestsRun { +function Invoke-CIPPDBTestsRun { <# .FUNCTIONALITY Entrypoint diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Tests/Push-CIPPTestsRun.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Tests/Push-CIPPTestsRun.ps1 index 7bea0f014457..c9f43f7211da 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Tests/Push-CIPPTestsRun.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Tests/Push-CIPPTestsRun.ps1 @@ -1,4 +1,4 @@ -function Push-CIPPTestsRun { +function Push-CIPPDBTestsRun { <# .SYNOPSIS PostExecution function to run tests after data collection completes @@ -13,7 +13,7 @@ function Push-CIPPTestsRun { Write-LogMessage -API 'Tests' -tenant $TenantFilter -message 'Starting test run after data collection' -sev Info # Call the test run function - $Result = Invoke-CIPPTestsRun -TenantFilter $TenantFilter + $Result = Invoke-CIPPDBTestsRun -TenantFilter $TenantFilter Write-LogMessage -API 'Tests' -tenant $TenantFilter -message "Test run started. Instance ID: $($Result.InstanceId)" -sev Info Write-Information "PostExecution: Tests started with Instance ID: $($Result.InstanceId)" diff --git a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-TestsOrchestrator.ps1 b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-TestsOrchestrator.ps1 index da22c521107e..87cf8c2c38cb 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-TestsOrchestrator.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-TestsOrchestrator.ps1 @@ -11,6 +11,6 @@ function Start-TestsOrchestrator { if ($PSCmdlet.ShouldProcess('Start-TestsOrchestrator', 'Starting Tests Orchestrator')) { Write-LogMessage -API 'Tests' -message 'Starting Tests Schedule' -sev Info - Invoke-CIPPTestsRun -TenantFilter 'allTenants' + Invoke-CIPPDBTestsRun -TenantFilter 'allTenants' } } From 8f2fc705170ad1dbd2f3e45a1d9325e2cb1ecabe Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Thu, 22 Jan 2026 14:19:09 +0100 Subject: [PATCH 247/503] v up --- host.json | 2 +- version_latest.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/host.json b/host.json index 03a1061b6423..ff513b12d0ea 100644 --- a/host.json +++ b/host.json @@ -16,7 +16,7 @@ "distributedTracingEnabled": false, "version": "None" }, - "defaultVersion": "10.0.4", + "defaultVersion": "10.0.5", "versionMatchStrategy": "Strict", "versionFailureStrategy": "Fail" } diff --git a/version_latest.txt b/version_latest.txt index 6b48f258c7a4..2681b301aa6c 100644 --- a/version_latest.txt +++ b/version_latest.txt @@ -1 +1 @@ -10.0.4 +10.0.5 From 96db4765b3d85f64f4d2f21598c59dd9521c9fcd Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 22 Jan 2026 12:35:35 -0500 Subject: [PATCH 248/503] roll back memory exception testing --- .../Push-StoreMailboxPermissions.ps1 | 41 ++++--------------- .../Public/Set-CIPPDBCacheCASMailboxes.ps1 | 14 ++----- .../CIPPCore/Public/Set-CIPPDBCacheUsers.ps1 | 18 ++------ 3 files changed, 16 insertions(+), 57 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Mailbox Permissions/Push-StoreMailboxPermissions.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Mailbox Permissions/Push-StoreMailboxPermissions.ps1 index c7b22a576847..911745c1b06b 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Mailbox Permissions/Push-StoreMailboxPermissions.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Mailbox Permissions/Push-StoreMailboxPermissions.ps1 @@ -44,21 +44,15 @@ function Push-StoreMailboxPermissions { # Results are grouped by cmdlet name due to ReturnWithCommand if ($ActualResult['Get-MailboxPermission']) { Write-Information "Adding $($ActualResult['Get-MailboxPermission'].Count) mailbox permissions" - foreach ($perm in $ActualResult['Get-MailboxPermission']) { - $AllMailboxPermissions.Add($perm) - } + $AllMailboxPermissions.AddRange($ActualResult['Get-MailboxPermission']) } if ($ActualResult['Get-RecipientPermission']) { Write-Information "Adding $($ActualResult['Get-RecipientPermission'].Count) recipient permissions" - foreach ($perm in $ActualResult['Get-RecipientPermission']) { - $AllRecipientPermissions.Add($perm) - } + $AllRecipientPermissions.AddRange($ActualResult['Get-RecipientPermission']) } if ($ActualResult['Get-MailboxFolderPermission']) { Write-Information "Adding $($ActualResult['Get-MailboxFolderPermission'].Count) calendar permissions" - foreach ($perm in $ActualResult['Get-MailboxFolderPermission']) { - $AllCalendarPermissions.Add($perm) - } + $AllCalendarPermissions.AddRange($ActualResult['Get-MailboxFolderPermission']) } } else { Write-Information "Skipping non-hashtable result: $($ActualResult.GetType().Name)" @@ -67,47 +61,30 @@ function Push-StoreMailboxPermissions { # Combine all permissions (mailbox and recipient) into a single collection $AllPermissions = [System.Collections.Generic.List[object]]::new() - foreach ($perm in $AllMailboxPermissions) { - $AllPermissions.Add($perm) - } - foreach ($perm in $AllRecipientPermissions) { - $AllPermissions.Add($perm) - } + $AllPermissions.AddRange($AllMailboxPermissions) + $AllPermissions.AddRange($AllRecipientPermissions) Write-Information "Aggregated $($AllPermissions.Count) total permissions ($($AllMailboxPermissions.Count) mailbox + $($AllRecipientPermissions.Count) recipient)" Write-Information "Aggregated $($AllCalendarPermissions.Count) calendar permissions" # Store all permissions together as MailboxPermissions if ($AllPermissions.Count -gt 0) { - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'MailboxPermissions' -Data $AllPermissions.ToArray() - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'MailboxPermissions' -Data @{ Count = $AllPermissions.Count } -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'MailboxPermissions' -Data $AllPermissions + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'MailboxPermissions' -Data $AllPermissions -Count Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($AllPermissions.Count) mailbox permission records" -sev Info } else { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'No mailbox permissions found to cache' -sev Info } - # Clear to free memory before processing calendar permissions - $AllMailboxPermissions.Clear() - $AllRecipientPermissions.Clear() - $AllPermissions.Clear() - $AllMailboxPermissions = $null - $AllRecipientPermissions = $null - $AllPermissions = $null - # Store calendar permissions separately if ($AllCalendarPermissions.Count -gt 0) { - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'CalendarPermissions' -Data $AllCalendarPermissions.ToArray() - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'CalendarPermissions' -Data @{ Count = $AllCalendarPermissions.Count } -Count + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'CalendarPermissions' -Data $AllCalendarPermissions + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'CalendarPermissions' -Data $AllCalendarPermissions -Count Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($AllCalendarPermissions.Count) calendar permission records" -sev Info } else { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'No calendar permissions found to cache' -sev Info } - # Final cleanup - $AllCalendarPermissions.Clear() - $AllCalendarPermissions = $null - [System.GC]::Collect() - return } catch { diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheCASMailboxes.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheCASMailboxes.ps1 index 283b056f9ba6..64c44ac31556 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheCASMailboxes.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheCASMailboxes.ps1 @@ -16,19 +16,11 @@ function Set-CIPPDBCacheCASMailboxes { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching CAS mailboxes' -sev Debug # Use Generic List for better memory efficiency with large datasets - $CASMailboxList = [System.Collections.Generic.List[PSObject]]::new() - $CASMailboxesResponse = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-CasMailbox' - foreach ($Mailbox in $CASMailboxesResponse) { - $CASMailboxList.Add($Mailbox) - } + $CASMailboxList = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-CasMailbox' - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'CASMailbox' -Data $CASMailboxList.ToArray() - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'CASMailbox' -Data @{ Count = $CASMailboxList.Count } -Count - - $CASMailboxesResponse = $null - $CASMailboxList.Clear() + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'CASMailbox' -Data $CASMailboxList + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'CASMailbox' -Data $CASMailboxList -Count $CASMailboxList = $null - [System.GC]::Collect() Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached CAS mailboxes successfully' -sev Debug diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheUsers.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheUsers.ps1 index 3355fbdd9f33..a1ffd11ef337 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheUsers.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheUsers.ps1 @@ -15,23 +15,13 @@ function Set-CIPPDBCacheUsers { try { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching users' -sev Debug - $Users = [System.Collections.Generic.List[PSObject]]::new() - $UsersResponse = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/users?$top=999' -tenantid $TenantFilter - foreach ($User in $UsersResponse) { - $Users.Add($User) - } - - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Users' -Data $Users.ToArray() - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Users' -Data @{ Count = $Users.Count } -Count - - $Users.Clear() + $Users = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/users?$top=999' -tenantid $TenantFilter + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Users' -Data $Users + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Users' -Data $Users -Count $Users = $null - $UsersResponse = $null - [System.GC]::Collect() - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached users successfully' -sev Debug } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache users: $($_.Exception.Message)" -sev Error -LogData (Get-CippException -Exception $_) + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache users: $($_.Exception.Message)" -sev Error } } From 67df08f0d0936ccedbb50b278d98a027e74a5c56 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 22 Jan 2026 12:35:47 -0500 Subject: [PATCH 249/503] Optimize service principal and permission grants with bulk requests Refactors Add-CIPPApplicationPermission to batch-create missing service principals and apply app role assignments using Microsoft Graph bulk requests. This improves efficiency and error handling when processing multiple permissions and service principals. --- .../Public/Add-CIPPApplicationPermission.ps1 | 93 +++++++++++++++---- 1 file changed, 77 insertions(+), 16 deletions(-) diff --git a/Modules/CIPPCore/Public/Add-CIPPApplicationPermission.ps1 b/Modules/CIPPCore/Public/Add-CIPPApplicationPermission.ps1 index ddcbd3447e9e..4e1357df4b35 100644 --- a/Modules/CIPPCore/Public/Add-CIPPApplicationPermission.ps1 +++ b/Modules/CIPPCore/Public/Add-CIPPApplicationPermission.ps1 @@ -60,12 +60,16 @@ function Add-CIPPApplicationPermission { } - $ServicePrincipalList = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/servicePrincipals?`$select=AppId,id,displayName&`$top=999" -skipTokenCache $true -tenantid $TenantFilter -NoAuthCheck $true + $ServicePrincipalList = [System.Collections.Generic.List[object]]::new() + $SPList = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/servicePrincipals?`$select=AppId,id,displayName&`$top=999" -skipTokenCache $true -tenantid $TenantFilter -NoAuthCheck $true + foreach ($SP in $SPList) { $ServicePrincipalList.Add($SP) } $ourSVCPrincipal = $ServicePrincipalList | Where-Object -Property AppId -EQ $ApplicationId if (!$ourSVCPrincipal) { #Our Service Principal isn't available yet. We do a sleep and reexecute after 3 seconds. Start-Sleep -Seconds 5 - $ServicePrincipalList = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/servicePrincipals?`$select=AppId,id,displayName&`$top=999" -skipTokenCache $true -tenantid $TenantFilter -NoAuthCheck $true + $ServicePrincipalList.Clear() + $SPList = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/servicePrincipals?`$select=AppId,id,displayName&`$top=999" -skipTokenCache $true -tenantid $TenantFilter -NoAuthCheck $true + foreach ($SP in $SPList) { $ServicePrincipalList.Add($SP) } $ourSVCPrincipal = $ServicePrincipalList | Where-Object -Property AppId -EQ $ApplicationId } @@ -73,19 +77,53 @@ function Add-CIPPApplicationPermission { $CurrentRoles = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/servicePrincipals/$($ourSVCPrincipal.id)/appRoleAssignments" -tenantid $TenantFilter -skipTokenCache $true -NoAuthCheck $true - $Grants = foreach ($App in $RequiredResourceAccess) { + # Collect missing service principals and prepare bulk request + $MissingServicePrincipals = [System.Collections.Generic.List[object]]::new() + $AppIdToRequestId = @{} + $requestId = 1 + + foreach ($App in $RequiredResourceAccess) { $svcPrincipalId = $ServicePrincipalList | Where-Object -Property AppId -EQ $App.resourceAppId if (!$svcPrincipalId) { - try { - $Body = @{ - appId = $App.resourceAppId - } | ConvertTo-Json -Compress - $svcPrincipalId = New-GraphPOSTRequest -uri 'https://graph.microsoft.com/beta/servicePrincipals' -tenantid $TenantFilter -body $Body -type POST - } catch { - $Results.add("Failed to create service principal for $($App.resourceAppId): $(Get-NormalizedError -message $_.Exception.Message)") - continue + $Body = @{ + appId = $App.resourceAppId } + $MissingServicePrincipals.Add(@{ + id = $requestId.ToString() + method = 'POST' + url = '/servicePrincipals' + headers = @{ + 'Content-Type' = 'application/json' + } + body = $Body + }) + $AppIdToRequestId[$App.resourceAppId] = $requestId.ToString() + $requestId++ + } + } + + # Create missing service principals in bulk + if ($MissingServicePrincipals.Count -gt 0) { + try { + $BulkResults = New-GraphBulkRequest -Requests $MissingServicePrincipals -tenantid $TenantFilter -NoAuthCheck $true + foreach ($Result in $BulkResults) { + if ($Result.status -eq 201) { + $ServicePrincipalList.Add($Result.body) + } else { + $AppId = ($MissingServicePrincipals | Where-Object { $_.id -eq $Result.id }).body.appId + $Results.add("Failed to create service principal for $($AppId): $($Result.body.error.message)") + } + } + } catch { + $Results.add("Failed to create service principals in bulk: $(Get-NormalizedError -message $_.Exception.Message)") } + } + + # Build grants list + $Grants = foreach ($App in $RequiredResourceAccess) { + $svcPrincipalId = $ServicePrincipalList | Where-Object -Property AppId -EQ $App.resourceAppId + if (!$svcPrincipalId) { continue } + foreach ($SingleResource in $App.ResourceAccess | Where-Object -Property Type -EQ 'Role') { if ($SingleResource.id -in $CurrentRoles.appRoleId) { continue } [pscustomobject]@{ @@ -95,14 +133,37 @@ function Add-CIPPApplicationPermission { } } } + + # Apply grants in bulk $counter = 0 - foreach ($Grant in $Grants) { + if ($Grants.Count -gt 0) { + $GrantRequests = [System.Collections.Generic.List[object]]::new() + $requestId = 1 + foreach ($Grant in $Grants) { + $GrantRequests.Add(@{ + id = $requestId.ToString() + method = 'POST' + url = "/servicePrincipals/$($ourSVCPrincipal.id)/appRoleAssignedTo" + headers = @{ + 'Content-Type' = 'application/json' + } + body = $Grant + }) + $requestId++ + } + try { - $SettingsRequest = New-GraphPOSTRequest -body (ConvertTo-Json -InputObject $Grant -Depth 5) -uri "https://graph.microsoft.com/beta/servicePrincipals/$($ourSVCPrincipal.id)/appRoleAssignedTo" -tenantid $TenantFilter -type POST -NoAuthCheck $true - $counter++ + $BulkResults = New-GraphBulkRequest -Requests $GrantRequests -tenantid $TenantFilter -NoAuthCheck $true + foreach ($Result in $BulkResults) { + if ($Result.status -eq 201) { + $counter++ + } else { + $GrantRequest = $GrantRequests | Where-Object { $_.id -eq $Result.id } + $Results.add("Failed to grant $($GrantRequest.body.appRoleId) to $($GrantRequest.body.resourceId): $($Result.body.error.message)") + } + } } catch { - $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message - $Results.add("Failed to grant $($Grant.appRoleId) to $($Grant.resourceId): $ErrorMessage") + $Results.add("Failed to grant permissions in bulk: $(Get-NormalizedError -message $_.Exception.Message)") } } "Added $counter Application permissions to $($ourSVCPrincipal.displayName)" From 5b9007cabd270187ac318736c7a11fa2ae6db179 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 22 Jan 2026 12:42:40 -0500 Subject: [PATCH 250/503] Improve logging and response structure in core modules Added informational logging to Add-CIPPApplicationPermission for better traceability. Updated Invoke-ExecCPVRefresh to nest InstanceId under Metadata in the response body for improved response structure. --- Modules/CIPPCore/Public/Add-CIPPApplicationPermission.ps1 | 3 ++- .../HTTP Functions/CIPP/Core/Invoke-ExecCPVRefresh.ps1 | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/Modules/CIPPCore/Public/Add-CIPPApplicationPermission.ps1 b/Modules/CIPPCore/Public/Add-CIPPApplicationPermission.ps1 index 4e1357df4b35..b3f369b89236 100644 --- a/Modules/CIPPCore/Public/Add-CIPPApplicationPermission.ps1 +++ b/Modules/CIPPCore/Public/Add-CIPPApplicationPermission.ps1 @@ -59,6 +59,7 @@ function Add-CIPPApplicationPermission { } } + Write-Information "Adding application permissions to application $ApplicationId in tenant $TenantFilter" $ServicePrincipalList = [System.Collections.Generic.List[object]]::new() $SPList = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/servicePrincipals?`$select=AppId,id,displayName&`$top=999" -skipTokenCache $true -tenantid $TenantFilter -NoAuthCheck $true @@ -123,7 +124,7 @@ function Add-CIPPApplicationPermission { $Grants = foreach ($App in $RequiredResourceAccess) { $svcPrincipalId = $ServicePrincipalList | Where-Object -Property AppId -EQ $App.resourceAppId if (!$svcPrincipalId) { continue } - + foreach ($SingleResource in $App.ResourceAccess | Where-Object -Property Type -EQ 'Role') { if ($SingleResource.id -in $CurrentRoles.appRoleId) { continue } [pscustomobject]@{ diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ExecCPVRefresh.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ExecCPVRefresh.ps1 index 5459d1fab375..f8d2f9355c10 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ExecCPVRefresh.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ExecCPVRefresh.ps1 @@ -16,8 +16,10 @@ function Invoke-ExecCPVRefresh { return @{ StatusCode = [System.Net.HttpStatusCode]::OK Body = @{ - Results = 'CPV Refresh has been triggered' - InstanceId = $InstanceId + Results = 'CPV Refresh has been triggered' + Metadata = @{ + InstanceId = $InstanceId + } } } } From c17f29ef6b8989d119c24f37d59040bbbd1e7cd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Thu, 22 Jan 2026 18:48:30 +0100 Subject: [PATCH 251/503] chore: sync product names in ExcludeSkuList.JSON - Update product display names in ExcludeSkuList.JSON to match authoritative license data. - Implemented syncing logic in Update-LicenseSKUFiles.ps1 to automate updates. --- Config/ExcludeSkuList.JSON | 20 ++++++++++++-------- Tools/Update-LicenseSKUFiles.ps1 | 23 +++++++++++++++++++++++ 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/Config/ExcludeSkuList.JSON b/Config/ExcludeSkuList.JSON index ec0dfb32d4a9..7409a30962dd 100644 --- a/Config/ExcludeSkuList.JSON +++ b/Config/ExcludeSkuList.JSON @@ -5,15 +5,15 @@ }, { "GUID": "f30db892-07e9-47e9-837c-80727f46fd3d", - "Product_Display_Name": "MICROSOFT FLOW FREE" + "Product_Display_Name": "Microsoft Power Automate Free" }, { "GUID": "16ddbbfc-09ea-4de2-b1d7-312db6112d70", - "Product_Display_Name": "MICROSOFT TEAMS (FREE)" + "Product_Display_Name": "Microsoft Teams (Free)" }, { "GUID": "a403ebcc-fae0-4ca2-8c8c-7a907fd6c235", - "Product_Display_Name": "Power BI (free)" + "Product_Display_Name": "Microsoft Fabric (Free)" }, { "GUID": "61e6bd70-fbdb-4deb-82ea-912842f39431", @@ -25,7 +25,7 @@ }, { "GUID": "338148b6-1b11-4102-afb9-f92b6cdc0f8d", - "Product_Display_Name": "DYNAMICS 365 P1 TRIAL FOR INFORMATION WORKERS" + "Product_Display_Name": "Dynamics 365 P1 Tria for Information Workers" }, { "GUID": "fcecd1f9-a91e-488d-a918-a96cdb6ce2b0", @@ -41,19 +41,19 @@ }, { "GUID": "606b54a9-78d8-4298-ad8b-df6ef4481c80", - "Product_Display_Name": "Power Virtual Agents Viral Trial" + "Product_Display_Name": "Microsoft Copilot Studio Viral Trial" }, { "GUID": "1f2f344a-700d-42c9-9427-5cea1d5d7ba6", - "Product_Display_Name": "MICROSOFT STREAM" + "Product_Display_Name": "Microsoft Stream" }, { "GUID": "6470687e-a428-4b7a-bef2-8a291ad947c9", - "Product_Display_Name": "WINDOWS STORE FOR BUSINESS" + "Product_Display_Name": "Windows Store for Business" }, { "GUID": "710779e8-3d4a-4c88-adb9-386c958d1fdf", - "Product_Display_Name": "MICROSOFT TEAMS EXPLORATORY" + "Product_Display_Name": "Microsoft Teams Exploratory" }, { "GUID": "8c4ce438-32a7-4ac5-91a6-e22ae08d9c8b", @@ -94,5 +94,9 @@ { "GUID": "99049c9c-6011-4908-bf17-15f496e6519d", "Product_Display_Name": "Office 365 Extra File Storage" + }, + { + "GUID": "47794cd0-f0e5-45c5-9033-2eb6b5fc84e0", + "Product_Display_Name": "Communications Credits" } ] diff --git a/Tools/Update-LicenseSKUFiles.ps1 b/Tools/Update-LicenseSKUFiles.ps1 index e21817d57f18..2bcc8d8c1910 100644 --- a/Tools/Update-LicenseSKUFiles.ps1 +++ b/Tools/Update-LicenseSKUFiles.ps1 @@ -54,5 +54,28 @@ foreach ($File in $LicenseJSONFiles) { Write-Host "Updated $($File.FullName) with new license SKU data." -ForegroundColor Green } +# Sync ExcludeSkuList.JSON names with the authoritative license data +Set-Location $PSScriptRoot +$ExcludeSkuListPath = Join-Path $PSScriptRoot '..\Config\ExcludeSkuList.JSON' +if (Test-Path $ExcludeSkuListPath) { + Write-Host 'Syncing ExcludeSkuList.JSON product names...' -ForegroundColor Yellow + $GuidToName = @{} + foreach ($license in $LicenseData) { + if (-not $GuidToName.ContainsKey($license.GUID)) { + $GuidToName[$license.GUID] = $license.Product_Display_Name + } + } + $ExcludeSkuList = Get-Content -Path $ExcludeSkuListPath -Encoding utf8 | ConvertFrom-Json + $updatedCount = 0 + foreach ($entry in $ExcludeSkuList) { + if ($GuidToName.ContainsKey($entry.GUID) -and $entry.Product_Display_Name -cne $GuidToName[$entry.GUID]) { + $entry.Product_Display_Name = $GuidToName[$entry.GUID] + $updatedCount++ + } + } + $ExcludeSkuList | ConvertTo-Json -Depth 100 | Set-Content -Path $ExcludeSkuListPath -Encoding utf8 + Write-Host "Updated $updatedCount product names in ExcludeSkuList.JSON." -ForegroundColor Green +} + # Clean up the temporary license SKU CSV file Remove-Item -Path $TempLicenseDataFile -Force From dc6c7e7ffe0e7e1e80255173049c211aff237a21 Mon Sep 17 00:00:00 2001 From: rvdwegen Date: Fri, 23 Jan 2026 01:20:58 +0100 Subject: [PATCH 252/503] Make version better available on azure function app interaction --- profile.ps1 | 1 + 1 file changed, 1 insertion(+) diff --git a/profile.ps1 b/profile.ps1 index d26d55f264bd..a1ec3269f8d5 100644 --- a/profile.ps1 +++ b/profile.ps1 @@ -97,6 +97,7 @@ $CurrentVersion = (Get-Content -Path (Join-Path $PSScriptRoot 'version_latest.tx $Table = Get-CippTable -tablename 'Version' Write-Information "Function App: $($env:WEBSITE_SITE_NAME) | API Version: $CurrentVersion | PS Version: $($PSVersionTable.PSVersion)" $global:CippVersion = $CurrentVersion +$ENV:CurrentVersion = $CurrentVersion $LastStartup = Get-CIPPAzDataTableEntity @Table -Filter "PartitionKey eq 'Version' and RowKey eq '$($env:WEBSITE_SITE_NAME)'" if (!$LastStartup -or $CurrentVersion -ne $LastStartup.Version) { From 9b7e9951408014a34027d06b0cdce6e706155980 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 23 Jan 2026 17:33:08 -0500 Subject: [PATCH 253/503] Refactor Add-CIPPDbItem for pipeline streaming and batch efficiency Add-CIPPDbItem now supports pipeline input for memory-efficient streaming, improved batch processing, and automatic count recording via -AddCount. Updated related cache scripts to use streaming and batch features, reducing memory usage and simplifying code. Added Set-CIPPDbCacheTestData.ps1 for generating large test datasets. Enhanced Add-CIPPAzDataTableEntity with performance logging. --- .../Public/Add-CIPPAzDataTableEntity.ps1 | 20 ++ Modules/CIPPCore/Public/Add-CIPPDbItem.ps1 | 204 ++++++++++++------ .../Push-StoreMailboxPermissions.ps1 | 6 +- .../Public/Set-CIPPDBCacheCASMailboxes.ps1 | 9 +- .../Public/Set-CIPPDBCacheMailboxes.ps1 | 7 +- .../CIPPCore/Public/Set-CIPPDBCacheUsers.ps1 | 8 +- .../Public/Set-CIPPDbCacheTestData.ps1 | 84 ++++++++ 7 files changed, 257 insertions(+), 81 deletions(-) create mode 100644 Modules/CIPPCore/Public/Set-CIPPDbCacheTestData.ps1 diff --git a/Modules/CIPPCore/Public/Add-CIPPAzDataTableEntity.ps1 b/Modules/CIPPCore/Public/Add-CIPPAzDataTableEntity.ps1 index 260f8691f0f1..4a287b2415d0 100644 --- a/Modules/CIPPCore/Public/Add-CIPPAzDataTableEntity.ps1 +++ b/Modules/CIPPCore/Public/Add-CIPPAzDataTableEntity.ps1 @@ -40,6 +40,12 @@ function Add-CIPPAzDataTableEntity { $MaxRowSize = 500000 - 100 $MaxSize = 30kb + $startTime = Get-Date + $entityCount = @($Entity).Count + $totalValidationTime = 0 + $totalAddTime = 0 + Write-Information "[Add-CIPPAzDataTableEntity] Processing $entityCount entities" + foreach ($SingleEnt in @($Entity)) { try { # Skip null entities @@ -62,6 +68,7 @@ function Add-CIPPAzDataTableEntity { } # Additional validation for AzBobbyTables compatibility + $validationStart = Get-Date try { # Ensure all property values are not null for string properties if ($SingleEnt -is [hashtable]) { @@ -84,8 +91,15 @@ function Add-CIPPAzDataTableEntity { } catch { Write-Warning "Error during entity validation: $($_.Exception.Message)" } + $validationEnd = Get-Date + $validationDuration = ($validationEnd - $validationStart).TotalMilliseconds + $totalValidationTime += $validationDuration + $addStart = Get-Date Add-AzDataTableEntity @Parameters -Entity $SingleEnt -ErrorAction Stop + $addEnd = Get-Date + $addDuration = ($addEnd - $addStart).TotalMilliseconds + $totalAddTime += $addDuration } catch [System.Exception] { if ($_.Exception.ErrorCode -in @('PropertyValueTooLarge', 'EntityTooLarge', 'RequestBodyTooLarge')) { @@ -237,4 +251,10 @@ function Add-CIPPAzDataTableEntity { } } } + + $endTime = Get-Date + $totalDuration = [math]::Round(($endTime - $startTime).TotalSeconds, 2) + $avgValidation = [math]::Round($totalValidationTime / $entityCount, 2) + $avgAdd = [math]::Round($totalAddTime / $entityCount, 2) + Write-Debug "[Add-CIPPAzDataTableEntity] Completed $entityCount entities in ${totalDuration}s (avg validation: ${avgValidation}ms, avg add: ${avgAdd}ms)" } diff --git a/Modules/CIPPCore/Public/Add-CIPPDbItem.ps1 b/Modules/CIPPCore/Public/Add-CIPPDbItem.ps1 index bd9c4d733eff..e7384f339b12 100644 --- a/Modules/CIPPCore/Public/Add-CIPPDbItem.ps1 +++ b/Modules/CIPPCore/Public/Add-CIPPDbItem.ps1 @@ -4,7 +4,7 @@ function Add-CIPPDbItem { Add items to the CIPP Reporting database .DESCRIPTION - Adds items to the CippReportingDB table with support for bulk inserts and count mode + Adds items to the CippReportingDB table with support for bulk inserts, count mode, and pipeline streaming .PARAMETER TenantFilter The tenant domain or GUID (used as partition key) @@ -12,15 +12,22 @@ function Add-CIPPDbItem { .PARAMETER Type The type of data being stored (used in row key) - .PARAMETER Data - Array of items to add to the database + .PARAMETER InputObject + Items to add to the database. Accepts pipeline input for memory-efficient streaming. + Alias: Data (for backward compatibility) .PARAMETER Count - If specified, stores a single row with count of each object property as separate properties + If specified, stores a single row with count of items processed + + .PARAMETER AddCount + If specified, automatically records the total count after processing all items .EXAMPLE Add-CIPPDbItem -TenantFilter 'contoso.onmicrosoft.com' -Type 'Groups' -Data $GroupsData + .EXAMPLE + New-GraphGetRequest -uri '...' | Add-CIPPDbItem -TenantFilter 'contoso.onmicrosoft.com' -Type 'Users' -AddCount + .EXAMPLE Add-CIPPDbItem -TenantFilter 'contoso.onmicrosoft.com' -Type 'Groups' -Data $GroupsData -Count #> @@ -32,92 +39,167 @@ function Add-CIPPDbItem { [Parameter(Mandatory = $true)] [string]$Type, - [Parameter(Mandatory = $true)] + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Alias('Data')] [AllowEmptyCollection()] - [array]$Data, + $InputObject, + + [Parameter(Mandatory = $false)] + [switch]$Count, [Parameter(Mandatory = $false)] - [switch]$Count + [switch]$AddCount ) - try { + begin { + # Initialize pipeline processing with state hashtable for nested function access $Table = Get-CippTable -tablename 'CippReportingDB' + $BatchAccumulator = [System.Collections.Generic.List[hashtable]]::new(500) + $State = @{ + TotalProcessed = 0 + BatchNumber = 0 + } # Helper function to format RowKey values by removing disallowed characters function Format-RowKey { param([string]$RowKey) - - # Remove disallowed characters: / \ # ? and control characters (U+0000 to U+001F and U+007F to U+009F) $sanitized = $RowKey -replace '[/\\#?]', '_' -replace '[\u0000-\u001F\u007F-\u009F]', '' - return $sanitized } - if ($Count) { - $Entity = @{ - PartitionKey = $TenantFilter - RowKey = Format-RowKey "$Type-Count" - DataCount = [int]$Data.Count - } + # Function to flush current batch + function Invoke-FlushBatch { + param($State) + if ($BatchAccumulator.Count -eq 0) { return } + + $State.BatchNumber++ + $batchSize = $BatchAccumulator.Count + $MemoryBeforeGC = [System.GC]::GetTotalMemory($false) + $flushStart = Get-Date + + try { + # Entities are already in the accumulator, just write them + $writeStart = Get-Date + Add-CIPPAzDataTableEntity @Table -Entity $BatchAccumulator.ToArray() -Force | Out-Null + $writeEnd = Get-Date + $writeDuration = [math]::Round(($writeEnd - $writeStart).TotalSeconds, 2) + $State.TotalProcessed += $batchSize + + } finally { + # Clear and GC + $gcStart = Get-Date + $BatchAccumulator.Clear() + + # Single GC pass is sufficient - aggressive GC was causing slowdown + [System.GC]::Collect() - Add-CIPPAzDataTableEntity @Table -Entity $Entity -Force | Out-Null + $flushEnd = Get-Date + $gcDuration = [math]::Round(($flushEnd - $gcStart).TotalSeconds, 2) + $flushDuration = [math]::Round(($flushEnd - $flushStart).TotalSeconds, 2) + $MemoryAfterGC = [System.GC]::GetTotalMemory($false) + $FreedMB = [math]::Round(($MemoryBeforeGC - $MemoryAfterGC) / 1MB, 2) + $CurrentMemoryMB = [math]::Round($MemoryAfterGC / 1MB, 2) + Write-Debug "Batch $($State.BatchNumber): ${flushDuration}s total (write: ${writeDuration}s, gc: ${gcDuration}s) | Processed: $($State.TotalProcessed) | Memory: ${CurrentMemoryMB}MB | Freed: ${FreedMB}MB" + } + } - } else { - #Get the existing type entries and nuke them. This ensures we don't have stale data. + if (-not $Count.IsPresent) { + # Delete existing entries for this type $Filter = "PartitionKey eq '{0}' and RowKey ge '{1}-' and RowKey lt '{1}0'" -f $TenantFilter, $Type $ExistingEntities = Get-CIPPAzDataTableEntity @Table -Filter $Filter if ($ExistingEntities) { Remove-AzDataTableEntity @Table -Entity $ExistingEntities -Force | Out-Null } + $AllocatedMemoryMB = [math]::Round([System.GC]::GetTotalMemory($false) / 1MB, 2) + Write-Debug "Starting $Type import for $TenantFilter | Allocated Memory: ${AllocatedMemoryMB}MB | Batch Size: 500" + } + } - # Calculate batch size based on available memory - $AvailableMemory = [System.GC]::GetTotalMemory($false) - $AvailableMemoryMB = [math]::Round($AvailableMemory / 1MB, 2) + process { + # Process each item from pipeline + if ($null -eq $InputObject) { return } - # Estimate item size from first item (with fallback) - $EstimatedItemSizeBytes = 1KB # Default assumption - if ($Data.Count -gt 0) { - $SampleJson = $Data[0] | ConvertTo-Json -Depth 10 -Compress - $EstimatedItemSizeBytes = [System.Text.Encoding]::UTF8.GetByteCount($SampleJson) - } + # If Count mode and InputObject is an integer, use it directly as count + if ($Count.IsPresent -and $InputObject -is [int]) { + $State.TotalProcessed = $InputObject + return + } - # Use 25% of available memory for batch processing, with min/max bounds - $TargetBatchMemoryMB = [Math]::Max(50, $AvailableMemoryMB * 0.25) - $CalculatedBatchSize = [Math]::Floor(($TargetBatchMemoryMB * 1MB) / $EstimatedItemSizeBytes) - # Reduce max to 500 to prevent OOM with large datasets - $BatchSize = [Math]::Max(100, [Math]::Min(500, $CalculatedBatchSize)) - - $TotalCount = $Data.Count - $ProcessedCount = 0 - Write-Information "Adding $TotalCount items of type $Type to CIPP Reporting DB for tenant $TenantFilter | Available Memory: ${AvailableMemoryMB}MB | Target Memory: ${TargetBatchMemoryMB}MB | Calculated: $CalculatedBatchSize | Batch Size: $BatchSize (est. item size: $([math]::Round($EstimatedItemSizeBytes/1KB, 2))KB)" - for ($i = 0; $i -lt $TotalCount; $i += $BatchSize) { - $BatchEnd = [Math]::Min($i + $BatchSize, $TotalCount) - $Batch = $Data[$i..($BatchEnd - 1)] - - $Entities = foreach ($Item in $Batch) { - $ItemId = $Item.id ?? $Item.ExternalDirectoryObjectId ?? $Item.Identity ?? $Item.skuId - @{ - PartitionKey = $TenantFilter - RowKey = Format-RowKey "$Type-$ItemId" - Data = [string]($Item | ConvertTo-Json -Depth 10 -Compress) - Type = $Type - } - } + # Handle both single items and arrays (for backward compatibility) + $ItemsToProcess = if ($InputObject -is [array]) { + $InputObject + } else { + @($InputObject) + } - Add-CIPPAzDataTableEntity @Table -Entity $Entities -Force | Out-Null - $ProcessedCount += $Batch.Count + # If Count mode, just count items without processing + if ($Count.IsPresent) { + $itemCount = if ($ItemsToProcess -is [array]) { $ItemsToProcess.Count } else { 1 } + $State.TotalProcessed += $itemCount + return + } - # Clear batch variables to free memory - $Entities = $null - $Batch = $null - [System.GC]::Collect() + foreach ($Item in $ItemsToProcess) { + if ($null -eq $Item) { continue } + + # Convert to entity + $ItemId = $Item.ExternalDirectoryObjectId ?? $Item.id ?? $Item.Identity ?? $Item.skuId + $Entity = @{ + PartitionKey = $TenantFilter + RowKey = Format-RowKey "$Type-$ItemId" + Data = [string]($Item | ConvertTo-Json -Depth 10 -Compress) + Type = $Type } + $BatchAccumulator.Add($Entity) + + # Flush when batch reaches 500 items + if ($BatchAccumulator.Count -ge 500) { + Invoke-FlushBatch -State $State + } } - Write-LogMessage -API 'CIPPDbItem' -tenant $TenantFilter -message "Added $($Data.Count) items of type $Type$(if ($Count) { ' (count mode)' })" -sev Debug + } + + end { + try { + # Flush any remaining items in final partial batch + if ($BatchAccumulator.Count -gt 0) { + Invoke-FlushBatch -State $State + } + + if ($Count.IsPresent) { + # Store count record + $Entity = @{ + PartitionKey = $TenantFilter + RowKey = Format-RowKey "$Type-Count" + DataCount = [int]$State.TotalProcessed + } + Add-CIPPAzDataTableEntity @Table -Entity $Entity -Force | Out-Null + } - } catch { - Write-LogMessage -API 'CIPPDbItem' -tenant $TenantFilter -message "Failed to add items of type $Type : $($_.Exception.Message)" -sev Error -LogData (Get-CippException -Exception $_) - throw + Write-LogMessage -API 'CIPPDbItem' -tenant $TenantFilter ` + -message "Added $($State.TotalProcessed) items of type $Type$(if ($Count.IsPresent) { ' (count mode)' })" -sev Debug + + } catch { + Write-LogMessage -API 'CIPPDbItem' -tenant $TenantFilter ` + -message "Failed to add items of type $Type : $($_.Exception.Message)" -sev Error ` + -LogData (Get-CippException -Exception $_) + Write-Debug "[Add-CIPPDbItem] $TenantFilter - $(Get-CippException -Exception $_ | ConvertTo-Json -Depth 5 -Compress)" + throw + } finally { + # Record count if AddCount was specified + if ($AddCount.IsPresent -and $State.TotalProcessed -gt 0) { + try { + Add-CIPPDbItem -TenantFilter $TenantFilter -Type $Type -InputObject $State.TotalProcessed -Count + } catch { + Write-LogMessage -API 'CIPPDbItem' -tenant $TenantFilter ` + -message "Failed to record count for $Type : $($_.Exception.Message)" -sev Warning + } + } + + # Final cleanup + $BatchAccumulator = $null + [System.GC]::Collect() + } } } diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Mailbox Permissions/Push-StoreMailboxPermissions.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Mailbox Permissions/Push-StoreMailboxPermissions.ps1 index 911745c1b06b..42a4f93d60dc 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Mailbox Permissions/Push-StoreMailboxPermissions.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Mailbox Permissions/Push-StoreMailboxPermissions.ps1 @@ -69,8 +69,7 @@ function Push-StoreMailboxPermissions { # Store all permissions together as MailboxPermissions if ($AllPermissions.Count -gt 0) { - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'MailboxPermissions' -Data $AllPermissions - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'MailboxPermissions' -Data $AllPermissions -Count + $AllPermissions | Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'MailboxPermissions' -AddCount Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($AllPermissions.Count) mailbox permission records" -sev Info } else { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'No mailbox permissions found to cache' -sev Info @@ -78,8 +77,7 @@ function Push-StoreMailboxPermissions { # Store calendar permissions separately if ($AllCalendarPermissions.Count -gt 0) { - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'CalendarPermissions' -Data $AllCalendarPermissions - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'CalendarPermissions' -Data $AllCalendarPermissions -Count + $AllCalendarPermissions | Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'CalendarPermissions' -AddCount Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($AllCalendarPermissions.Count) calendar permission records" -sev Info } else { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'No calendar permissions found to cache' -sev Info diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheCASMailboxes.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheCASMailboxes.ps1 index 64c44ac31556..02c12125f6e4 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheCASMailboxes.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheCASMailboxes.ps1 @@ -15,12 +15,9 @@ function Set-CIPPDBCacheCASMailboxes { try { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching CAS mailboxes' -sev Debug - # Use Generic List for better memory efficiency with large datasets - $CASMailboxList = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-CasMailbox' - - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'CASMailbox' -Data $CASMailboxList - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'CASMailbox' -Data $CASMailboxList -Count - $CASMailboxList = $null + # Stream CAS mailboxes directly to batch processor + New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-CasMailbox' | + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'CASMailbox' -AddCount Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached CAS mailboxes successfully' -sev Debug diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheMailboxes.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheMailboxes.ps1 index 8c78abbdab4b..27fb33c5cbc6 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheMailboxes.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheMailboxes.ps1 @@ -44,13 +44,8 @@ function Set-CIPPDBCacheMailboxes { MessageCopyForSentAsEnabled)) } - $Mailboxes = $MailboxList.ToArray() - $RawMailboxes = $null - $MailboxList.Clear() - $MailboxList = $null + $Mailboxes | Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Mailboxes' -AddCount - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Mailboxes' -Data $Mailboxes - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Mailboxes' -Data $Mailboxes -Count Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($Mailboxes.Count) mailboxes successfully" -sev Debug # Start orchestrator to cache mailbox permissions in batches diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheUsers.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheUsers.ps1 index a1ffd11ef337..72f498142664 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheUsers.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheUsers.ps1 @@ -15,10 +15,10 @@ function Set-CIPPDBCacheUsers { try { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching users' -sev Debug - $Users = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/users?$top=999' -tenantid $TenantFilter - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Users' -Data $Users - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Users' -Data $Users -Count - $Users = $null + # Stream users directly from Graph API to batch processor + New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/users?$top=999' -tenantid $TenantFilter | + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Users' -AddCount + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached users successfully' -sev Debug } catch { diff --git a/Modules/CIPPCore/Public/Set-CIPPDbCacheTestData.ps1 b/Modules/CIPPCore/Public/Set-CIPPDbCacheTestData.ps1 new file mode 100644 index 000000000000..5a5f6b2094f1 --- /dev/null +++ b/Modules/CIPPCore/Public/Set-CIPPDbCacheTestData.ps1 @@ -0,0 +1,84 @@ +function Set-CIPPDbCacheTestData { + <# + .SYNOPSIS + Generates test data for cache performance testing + + .DESCRIPTION + Creates 50,000 test objects with ~3KB of data each to test streaming performance + + .PARAMETER TenantFilter + The tenant to use for test data + + .PARAMETER Count + Number of test objects to generate (default: 50000) + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter, + + [Parameter(Mandatory = $false)] + [int]$Count = 50000 + ) + + try { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Generating $Count test objects" -sev Debug + + # Generate sample data to reach ~3KB per object + $sampleText = @' +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. +Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. +Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. +Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. +'@ * 10 # Repeat to get ~3KB + + $startTime = Get-Date + + Write-Information "[Set-CIPPDbCacheTestData] Starting generation of $Count test objects for tenant $TenantFilter" + # Stream test objects directly to batch processor + 1..$Count | ForEach-Object { + [PSCustomObject]@{ + id = [guid]::NewGuid().ToString() + displayName = "Test User $_" + userPrincipalName = "testuser$_@$TenantFilter" + mail = "testuser$_@$TenantFilter" + givenName = 'Test' + surname = "User $_" + jobTitle = 'Test Engineer' + department = 'Testing Department' + officeLocation = "Test Office $_" + mobilePhone = "+1-555-000-$($_.ToString().PadLeft(4, '0'))" + businessPhones = @("+1-555-001-$($_.ToString().PadLeft(4, '0'))") + accountEnabled = $true + createdDateTime = (Get-Date).ToString('o') + lastSignInDateTime = (Get-Date).AddDays(-1).ToString('o') + description = $sampleText + companyName = 'Test Company' + country = 'United States' + city = 'Test City' + state = 'Test State' + postalCode = '12345' + streetAddress = '123 Test Street' + proxyAddresses = @("SMTP:testuser$_@$TenantFilter", "smtp:alias$_@$TenantFilter") + assignedLicenses = @( + @{ skuId = [guid]::NewGuid().ToString(); disabledPlans = @() } + ) + customAttribute1 = 'Custom Value 1' + customAttribute2 = 'Custom Value 2' + customAttribute3 = 'Custom Value 3' + additionalData = $sampleText + } + } | Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'TestData' -AddCount + + $endTime = Get-Date + $duration = ($endTime - $startTime).TotalSeconds + $objectsPerSecond = [math]::Round($Count / $duration, 2) + + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter ` + -message "Generated $Count test objects in $duration seconds ($objectsPerSecond objects/sec)" -sev Debug + + } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter ` + -message "Failed to generate test data: $($_.Exception.Message)" -sev Error + } +} From 5287714347c44756fb0f9b53cf460b2498e31e46 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 23 Jan 2026 17:53:25 -0500 Subject: [PATCH 254/503] Remove debug and timing logs from Add-CIPPAzDataTableEntity and Add-CIPPDbItem Eliminated detailed timing and debug output from Add-CIPPAzDataTableEntity.ps1 to reduce log verbosity and improve performance. Commented out Write-Debug statements in Add-CIPPDbItem.ps1 and added property selection to Get-CIPPAzDataTableEntity for efficiency. --- .../Public/Add-CIPPAzDataTableEntity.ps1 | 20 ------------------- Modules/CIPPCore/Public/Add-CIPPDbItem.ps1 | 8 ++++---- 2 files changed, 4 insertions(+), 24 deletions(-) diff --git a/Modules/CIPPCore/Public/Add-CIPPAzDataTableEntity.ps1 b/Modules/CIPPCore/Public/Add-CIPPAzDataTableEntity.ps1 index 4a287b2415d0..260f8691f0f1 100644 --- a/Modules/CIPPCore/Public/Add-CIPPAzDataTableEntity.ps1 +++ b/Modules/CIPPCore/Public/Add-CIPPAzDataTableEntity.ps1 @@ -40,12 +40,6 @@ function Add-CIPPAzDataTableEntity { $MaxRowSize = 500000 - 100 $MaxSize = 30kb - $startTime = Get-Date - $entityCount = @($Entity).Count - $totalValidationTime = 0 - $totalAddTime = 0 - Write-Information "[Add-CIPPAzDataTableEntity] Processing $entityCount entities" - foreach ($SingleEnt in @($Entity)) { try { # Skip null entities @@ -68,7 +62,6 @@ function Add-CIPPAzDataTableEntity { } # Additional validation for AzBobbyTables compatibility - $validationStart = Get-Date try { # Ensure all property values are not null for string properties if ($SingleEnt -is [hashtable]) { @@ -91,15 +84,8 @@ function Add-CIPPAzDataTableEntity { } catch { Write-Warning "Error during entity validation: $($_.Exception.Message)" } - $validationEnd = Get-Date - $validationDuration = ($validationEnd - $validationStart).TotalMilliseconds - $totalValidationTime += $validationDuration - $addStart = Get-Date Add-AzDataTableEntity @Parameters -Entity $SingleEnt -ErrorAction Stop - $addEnd = Get-Date - $addDuration = ($addEnd - $addStart).TotalMilliseconds - $totalAddTime += $addDuration } catch [System.Exception] { if ($_.Exception.ErrorCode -in @('PropertyValueTooLarge', 'EntityTooLarge', 'RequestBodyTooLarge')) { @@ -251,10 +237,4 @@ function Add-CIPPAzDataTableEntity { } } } - - $endTime = Get-Date - $totalDuration = [math]::Round(($endTime - $startTime).TotalSeconds, 2) - $avgValidation = [math]::Round($totalValidationTime / $entityCount, 2) - $avgAdd = [math]::Round($totalAddTime / $entityCount, 2) - Write-Debug "[Add-CIPPAzDataTableEntity] Completed $entityCount entities in ${totalDuration}s (avg validation: ${avgValidation}ms, avg add: ${avgAdd}ms)" } diff --git a/Modules/CIPPCore/Public/Add-CIPPDbItem.ps1 b/Modules/CIPPCore/Public/Add-CIPPDbItem.ps1 index e7384f339b12..7b96b16bc4cb 100644 --- a/Modules/CIPPCore/Public/Add-CIPPDbItem.ps1 +++ b/Modules/CIPPCore/Public/Add-CIPPDbItem.ps1 @@ -99,19 +99,19 @@ function Add-CIPPDbItem { $MemoryAfterGC = [System.GC]::GetTotalMemory($false) $FreedMB = [math]::Round(($MemoryBeforeGC - $MemoryAfterGC) / 1MB, 2) $CurrentMemoryMB = [math]::Round($MemoryAfterGC / 1MB, 2) - Write-Debug "Batch $($State.BatchNumber): ${flushDuration}s total (write: ${writeDuration}s, gc: ${gcDuration}s) | Processed: $($State.TotalProcessed) | Memory: ${CurrentMemoryMB}MB | Freed: ${FreedMB}MB" + #Write-Debug "Batch $($State.BatchNumber): ${flushDuration}s total (write: ${writeDuration}s, gc: ${gcDuration}s) | Processed: $($State.TotalProcessed) | Memory: ${CurrentMemoryMB}MB | Freed: ${FreedMB}MB" } } if (-not $Count.IsPresent) { # Delete existing entries for this type $Filter = "PartitionKey eq '{0}' and RowKey ge '{1}-' and RowKey lt '{1}0'" -f $TenantFilter, $Type - $ExistingEntities = Get-CIPPAzDataTableEntity @Table -Filter $Filter + $ExistingEntities = Get-CIPPAzDataTableEntity @Table -Filter $Filter -Property PartitionKey, RowKey, ETag if ($ExistingEntities) { Remove-AzDataTableEntity @Table -Entity $ExistingEntities -Force | Out-Null } $AllocatedMemoryMB = [math]::Round([System.GC]::GetTotalMemory($false) / 1MB, 2) - Write-Debug "Starting $Type import for $TenantFilter | Allocated Memory: ${AllocatedMemoryMB}MB | Batch Size: 500" + #Write-Debug "Starting $Type import for $TenantFilter | Allocated Memory: ${AllocatedMemoryMB}MB | Batch Size: 500" } } @@ -184,7 +184,7 @@ function Add-CIPPDbItem { Write-LogMessage -API 'CIPPDbItem' -tenant $TenantFilter ` -message "Failed to add items of type $Type : $($_.Exception.Message)" -sev Error ` -LogData (Get-CippException -Exception $_) - Write-Debug "[Add-CIPPDbItem] $TenantFilter - $(Get-CippException -Exception $_ | ConvertTo-Json -Depth 5 -Compress)" + #Write-Debug "[Add-CIPPDbItem] $TenantFilter - $(Get-CippException -Exception $_ | ConvertTo-Json -Depth 5 -Compress)" throw } finally { # Record count if AddCount was specified From a16affbee13acb979ae0c2ac8724c1256dda4815 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Sat, 24 Jan 2026 22:26:55 -0500 Subject: [PATCH 255/503] Enhance error handling and info retrieval in core modules Improved client secret retrieval logic and error messaging in Invoke-ExecTokenExchange. Added organization and user info to Invoke-ExecListAppId response. Implemented retry logic for Key Vault secret retrieval in Get-CippKeyVaultSecret. Enhanced error normalization for AADSTS650051 in Get-NormalizedError. Minor code style and comment cleanups. --- .../Public/Add-CIPPApplicationPermission.ps1 | 2 - .../CIPP/Setup/Invoke-ExecTokenExchange.ps1 | 19 ++++--- .../Entrypoints/Invoke-ExecListAppId.ps1 | 52 ++++++++++++++++--- .../Public/Get-CippKeyVaultSecret.ps1 | 36 +++++++++---- .../GraphHelper/Get-NormalizedError.ps1 | 8 ++- 5 files changed, 91 insertions(+), 26 deletions(-) diff --git a/Modules/CIPPCore/Public/Add-CIPPApplicationPermission.ps1 b/Modules/CIPPCore/Public/Add-CIPPApplicationPermission.ps1 index b3f369b89236..41681e8b5bba 100644 --- a/Modules/CIPPCore/Public/Add-CIPPApplicationPermission.ps1 +++ b/Modules/CIPPCore/Public/Add-CIPPApplicationPermission.ps1 @@ -7,12 +7,10 @@ function Add-CIPPApplicationPermission { $TenantFilter ) if ($ApplicationId -eq $env:ApplicationID -and $TenantFilter -eq $env:TenantID) { - #return @('Cannot modify application permissions for CIPP-SAM on partner tenant') $RequiredResourceAccess = 'CIPPDefaults' } Set-Location (Get-Item $PSScriptRoot).FullName if ($RequiredResourceAccess -eq 'CIPPDefaults') { - #$RequiredResourceAccess = (Get-Content '.\SAMManifest.json' | ConvertFrom-Json).requiredResourceAccess $Permissions = Get-CippSamPermissions -NoDiff $RequiredResourceAccess = [System.Collections.Generic.List[object]]::new() diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecTokenExchange.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecTokenExchange.ps1 index 6fd1a97bf94a..c53b7f1690b7 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecTokenExchange.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecTokenExchange.ps1 @@ -1,4 +1,4 @@ -Function Invoke-ExecTokenExchange { +function Invoke-ExecTokenExchange { <# .FUNCTIONALITY Entrypoint,AnyTenant @@ -32,24 +32,29 @@ Function Invoke-ExecTokenExchange { # Make sure we get the latest authentication $auth = Get-CIPPAuthentication - if ($env:AzureWebJobsStorage -eq 'UseDevelopmentStorage=true' -or $env:NonLocalHostAzurite -eq 'true') { + # Check if environment variable is already set and not the placeholder value + if ($auth -and $env:ApplicationSecret -and $env:ApplicationSecret -ne 'AppSecret') { + $ClientSecret = $env:ApplicationSecret + Write-LogMessage -API $APIName -message 'Using client secret from environment variable' -Sev 'Debug' + } elseif ($env:AzureWebJobsStorage -eq 'UseDevelopmentStorage=true' -or $env:NonLocalHostAzurite -eq 'true') { $DevSecretsTable = Get-CIPPTable -tablename 'DevSecrets' $Secret = Get-CIPPAzDataTableEntity @DevSecretsTable -Filter "PartitionKey eq 'Secret' and RowKey eq 'Secret'" $ClientSecret = $Secret.applicationsecret - Write-LogMessage -API $APIName -message 'Retrieved client secret from development secrets' -Sev 'Info' + Write-LogMessage -API $APIName -message 'Retrieved client secret from development secrets' -Sev 'Debug' } else { try { $ClientSecret = (Get-CippKeyVaultSecret -VaultName $kv -Name 'applicationsecret' -AsPlainText) - Write-LogMessage -API $APIName -message 'Retrieved client secret from key vault' -Sev 'Info' + Write-LogMessage -API $APIName -message 'Retrieved client secret from key vault' -Sev 'Debug' } catch { Write-LogMessage -API $APIName -message "Failed to retrieve client secret: $($_.Exception.Message)" -Sev 'Error' throw "Failed to retrieve client secret: $($_.Exception.Message)" } } - if (!$ClientSecret) { - Write-LogMessage -API $APIName -message 'Client secret is empty or null' -Sev 'Error' - throw 'Client secret is empty or null' + # Check if client secret is still the default placeholder value from ARM template + if (!$ClientSecret -or $ClientSecret -eq 'AppSecret') { + Write-LogMessage -API $APIName -message 'Client secret is not configured' -Sev 'Error' + throw 'Application secret has not been configured. Please complete the setup process first.' } # Convert token request to form data and add client secret diff --git a/Modules/CIPPCore/Public/Entrypoints/Invoke-ExecListAppId.ps1 b/Modules/CIPPCore/Public/Entrypoints/Invoke-ExecListAppId.ps1 index e3c8a7f62611..cf5e199893eb 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Invoke-ExecListAppId.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Invoke-ExecListAppId.ps1 @@ -1,4 +1,4 @@ -Function Invoke-ExecListAppId { +function Invoke-ExecListAppId { <# .FUNCTIONALITY Entrypoint @@ -28,14 +28,52 @@ Function Invoke-ExecListAppId { $env:TenantID = (Get-CippException -Exception $_) } } + + # Get organization info and authenticated user using bulk request + $AuthenticatedUserDisplayName = $null + $AuthenticatedUserPrincipalName = $null + $OrgInfo = $null + try { + $BulkRequests = @( + @{ + id = 'organization' + url = '/organization?$select=displayName,partnerTenantType' + method = 'GET' + }, + @{ + id = 'me' + url = '/me?$select=displayName,userPrincipalName' + method = 'GET' + } + ) + + $BulkResponse = New-GraphBulkRequest -Requests $BulkRequests -tenantid $env:TenantID -NoAuthCheck $true + $OrgResponse = $BulkResponse | Where-Object { $_.id -eq 'organization' } + $MeResponse = $BulkResponse | Where-Object { $_.id -eq 'me' } + if ($MeResponse.body) { + $AuthenticatedUserDisplayName = $MeResponse.body.displayName + $AuthenticatedUserPrincipalName = $MeResponse.body.userPrincipalName + } + if ($OrgResponse.body.value -and $OrgResponse.body.value.Count -gt 0) { + $OrgInfo = $OrgResponse.body.value[0] + } + } catch { + Write-LogMessage -message 'Failed to retrieve organization info and authenticated user' -LogData (Get-CippException -Exception $_) -Sev 'Warning' + } + $Results = @{ - applicationId = $env:ApplicationID - tenantId = $env:TenantID - refreshUrl = "https://login.microsoftonline.com/$env:TenantID/oauth2/v2.0/authorize?client_id=$env:ApplicationID&response_type=code&redirect_uri=$ResponseURL&response_mode=query&scope=https%3A%2F%2Fgraph.microsoft.com%2F.default+offline_access+profile+openid&state=1&prompt=select_account" + applicationId = $env:ApplicationID + tenantId = $env:TenantID + orgName = $OrgInfo.displayName + authenticatedUserDisplayName = $AuthenticatedUserDisplayName + authenticatedUserPrincipalName = $AuthenticatedUserPrincipalName + isPartnerTenant = !!$OrgInfo.partnerTenantType + partnerTenantType = $OrgInfo.partnerTenantType + refreshUrl = "https://login.microsoftonline.com/$env:TenantID/oauth2/v2.0/authorize?client_id=$env:ApplicationID&response_type=code&redirect_uri=$ResponseURL&response_mode=query&scope=https%3A%2F%2Fgraph.microsoft.com%2F.default+offline_access+profile+openid&state=1&prompt=select_account" } return [HttpResponseContext]@{ - StatusCode = [HttpStatusCode]::OK - Body = $Results - } + StatusCode = [HttpStatusCode]::OK + Body = $Results + } } diff --git a/Modules/CIPPCore/Public/Get-CippKeyVaultSecret.ps1 b/Modules/CIPPCore/Public/Get-CippKeyVaultSecret.ps1 index 6de9dc5c25a6..6455e07781b0 100644 --- a/Modules/CIPPCore/Public/Get-CippKeyVaultSecret.ps1 +++ b/Modules/CIPPCore/Public/Get-CippKeyVaultSecret.ps1 @@ -40,18 +40,36 @@ function Get-CippKeyVaultSecret { if ($env:WEBSITE_DEPLOYMENT_ID) { $VaultName = ($env:WEBSITE_DEPLOYMENT_ID -split '-')[0] } else { - throw "VaultName not provided and WEBSITE_DEPLOYMENT_ID environment variable not set" + throw 'VaultName not provided and WEBSITE_DEPLOYMENT_ID environment variable not set' } } # Get access token for Key Vault - $token = Get-CIPPAzIdentityToken -ResourceUrl "https://vault.azure.net" + $token = Get-CIPPAzIdentityToken -ResourceUrl 'https://vault.azure.net' - # Call Key Vault REST API + # Call Key Vault REST API with retry logic $uri = "https://$VaultName.vault.azure.net/secrets/$Name`?api-version=7.4" - $response = Invoke-RestMethod -Uri $uri -Headers @{ - Authorization = "Bearer $token" - } -Method Get -ErrorAction Stop + $maxRetries = 3 + $retryDelay = 2 + $response = $null + + for ($i = 0; $i -lt $maxRetries; $i++) { + try { + $response = Invoke-RestMethod -Uri $uri -Headers @{ + Authorization = "Bearer $token" + } -Method Get -ErrorAction Stop + break + } catch { + $lastError = $_ + if ($i -lt ($maxRetries - 1)) { + Start-Sleep -Seconds $retryDelay + $retryDelay *= 2 # Exponential backoff + } else { + Write-Error "Failed to retrieve secret '$Name' from vault '$VaultName' after $maxRetries attempts: $($_.Exception.Message)" + throw + } + } + } # Return based on AsPlainText switch if ($AsPlainText) { @@ -60,12 +78,12 @@ function Get-CippKeyVaultSecret { # Return object similar to Get-AzKeyVaultSecret for compatibility return @{ SecretValue = ($response.value | ConvertTo-SecureString -AsPlainText -Force) - Name = $Name - VaultName = $VaultName + Name = $Name + VaultName = $VaultName } } } catch { - Write-Error "Failed to retrieve secret '$Name' from vault '$VaultName': $($_.Exception.Message)" + # Error already handled in retry loop, just rethrow throw } } diff --git a/Modules/CIPPCore/Public/GraphHelper/Get-NormalizedError.ps1 b/Modules/CIPPCore/Public/GraphHelper/Get-NormalizedError.ps1 index 75b05c0fc184..13398c7fddee 100644 --- a/Modules/CIPPCore/Public/GraphHelper/Get-NormalizedError.ps1 +++ b/Modules/CIPPCore/Public/GraphHelper/Get-NormalizedError.ps1 @@ -49,7 +49,13 @@ function Get-NormalizedError { '*AADSTS50177' { 'AADSTS50177: The user you have used for your Secure Application Model is a guest in this tenant, or your are using GDAP and have not added the user to the correct group. Please delete the guest user to gain access to this tenant' } '*invalid or malformed*' { 'The request is malformed. Have you finished the Setup Wizard' } '*Windows Store repository apps feature is not supported for this tenant*' { 'This tenant does not have WinGet support available' } - '*AADSTS650051*' { 'The application does not exist yet. Try again in 30 seconds.' } + '*AADSTS650051*' { + if ($Message -like '*service principal name is already present*') { + 'The application service principal already exists in this tenant. This is expected and not an error.' + } else { + 'The application does not exist yet. Try again in 30 seconds.' + } + } '*AppLifecycle_2210*' { 'Failed to call Intune APIs: Does the tenant have a license available?' } '*One or more added object references already exist for the following modified properties:*' { 'This user is already a member of this group.' } '*Microsoft.Exchange.Management.Tasks.MemberAlreadyExistsException*' { 'This user is already a member of this group.' } From 1177ffd0b1daaa4ea406530f4ed4b61c8bdbc815 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Sat, 24 Jan 2026 23:03:10 -0500 Subject: [PATCH 256/503] Add dynamic redirect URI update for application This update retrieves the application's web redirect URIs and adds a new redirect URI based on the current request if it is not already present. The change ensures the application's redirect URIs are kept up to date automatically during execution. --- .../Entrypoints/Invoke-ExecListAppId.ps1 | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/Modules/CIPPCore/Public/Entrypoints/Invoke-ExecListAppId.ps1 b/Modules/CIPPCore/Public/Entrypoints/Invoke-ExecListAppId.ps1 index cf5e199893eb..3f0aac4c2ebf 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Invoke-ExecListAppId.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Invoke-ExecListAppId.ps1 @@ -45,11 +45,17 @@ function Invoke-ExecListAppId { url = '/me?$select=displayName,userPrincipalName' method = 'GET' } + @{ + id = 'application' + url = "/applications(appId='$($env:ApplicationID)')?`$select=id,web" + method = 'GET' + } ) $BulkResponse = New-GraphBulkRequest -Requests $BulkRequests -tenantid $env:TenantID -NoAuthCheck $true $OrgResponse = $BulkResponse | Where-Object { $_.id -eq 'organization' } $MeResponse = $BulkResponse | Where-Object { $_.id -eq 'me' } + $AppResponse = $BulkResponse | Where-Object { $_.id -eq 'application' } if ($MeResponse.body) { $AuthenticatedUserDisplayName = $MeResponse.body.displayName $AuthenticatedUserPrincipalName = $MeResponse.body.userPrincipalName @@ -57,6 +63,27 @@ function Invoke-ExecListAppId { if ($OrgResponse.body.value -and $OrgResponse.body.value.Count -gt 0) { $OrgInfo = $OrgResponse.body.value[0] } + + if ($AppResponse.body) { + $AppWeb = $AppResponse.body.web + if ($AppWeb.redirectUris -and $AppWeb.redirectUris.Count -gt 0) { + # construct new redirect uri with current + $URL = ($Request.headers.'x-ms-original-url').split('/api') | Select-Object -First 1 + $NewRedirectUri = "$($URL)/authredirect" + if ($AppWeb.redirectUris -notcontains $NewRedirectUri) { + $RedirectUris = [system.collections.generic.list[string]]::new() + $AppWeb.redirectUris | ForEach-Object { $RedirectUris.Add($_) } + $RedirectUris.Add($NewRedirectUri) + $AppUpdateBody = @{ + web = @{ + redirectUris = $RedirectUris + } + } | ConvertTo-Json -Depth 10 + Invoke-GraphRequest -Method PATCH -Url "https://graph.microsoft.com/v1.0/applications/$($AppResponse.body.id)" -Body $AppUpdateBody -tenantid $env:TenantID -NoAuthCheck $true + Write-LogMessage -message "Updated redirect URIs for application $($env:ApplicationID) to include $NewRedirectUri" -Sev 'Info' + } + } + } } catch { Write-LogMessage -message 'Failed to retrieve organization info and authenticated user' -LogData (Get-CippException -Exception $_) -Sev 'Warning' } From 185c02673a27f649a1fd9449fb71f62f39cd490d Mon Sep 17 00:00:00 2001 From: John Duprey Date: Sat, 24 Jan 2026 23:04:10 -0500 Subject: [PATCH 257/503] Add error handling for redirect URI update in ExecListAppId Wrapped the redirect URI update logic in a try/catch block to handle potential failures when updating application redirect URIs. Added logging for both successful and failed update attempts to improve troubleshooting and reliability. --- .../Entrypoints/Invoke-ExecListAppId.ps1 | 59 ++++++++++--------- 1 file changed, 31 insertions(+), 28 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/Invoke-ExecListAppId.ps1 b/Modules/CIPPCore/Public/Entrypoints/Invoke-ExecListAppId.ps1 index 3f0aac4c2ebf..61d2008900a5 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Invoke-ExecListAppId.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Invoke-ExecListAppId.ps1 @@ -71,36 +71,39 @@ function Invoke-ExecListAppId { $URL = ($Request.headers.'x-ms-original-url').split('/api') | Select-Object -First 1 $NewRedirectUri = "$($URL)/authredirect" if ($AppWeb.redirectUris -notcontains $NewRedirectUri) { - $RedirectUris = [system.collections.generic.list[string]]::new() - $AppWeb.redirectUris | ForEach-Object { $RedirectUris.Add($_) } - $RedirectUris.Add($NewRedirectUri) - $AppUpdateBody = @{ - web = @{ - redirectUris = $RedirectUris - } - } | ConvertTo-Json -Depth 10 - Invoke-GraphRequest -Method PATCH -Url "https://graph.microsoft.com/v1.0/applications/$($AppResponse.body.id)" -Body $AppUpdateBody -tenantid $env:TenantID -NoAuthCheck $true - Write-LogMessage -message "Updated redirect URIs for application $($env:ApplicationID) to include $NewRedirectUri" -Sev 'Info' + try { + $RedirectUris = [system.collections.generic.list[string]]::new() + $AppWeb.redirectUris | ForEach-Object { $RedirectUris.Add($_) } + $RedirectUris.Add($NewRedirectUri) + $AppUpdateBody = @{ + web = @{ + redirectUris = $RedirectUris + } + } | ConvertTo-Json -Depth 10 + Invoke-GraphRequest -Method PATCH -Url "https://graph.microsoft.com/v1.0/applications/$($AppResponse.body.id)" -Body $AppUpdateBody -tenantid $env:TenantID -NoAuthCheck $true + Write-LogMessage -message "Updated redirect URIs for application $($env:ApplicationID) to include $NewRedirectUri" -Sev 'Info' + } catch { + Write-LogMessage -message "Failed to update redirect URIs for application $($env:ApplicationID)" -LogData (Get-CippException -Exception $_) -Sev 'Warning' + } } } + } catch { + Write-LogMessage -message 'Failed to retrieve organization info and authenticated user' -LogData (Get-CippException -Exception $_) -Sev 'Warning' } - } catch { - Write-LogMessage -message 'Failed to retrieve organization info and authenticated user' -LogData (Get-CippException -Exception $_) -Sev 'Warning' - } - $Results = @{ - applicationId = $env:ApplicationID - tenantId = $env:TenantID - orgName = $OrgInfo.displayName - authenticatedUserDisplayName = $AuthenticatedUserDisplayName - authenticatedUserPrincipalName = $AuthenticatedUserPrincipalName - isPartnerTenant = !!$OrgInfo.partnerTenantType - partnerTenantType = $OrgInfo.partnerTenantType - refreshUrl = "https://login.microsoftonline.com/$env:TenantID/oauth2/v2.0/authorize?client_id=$env:ApplicationID&response_type=code&redirect_uri=$ResponseURL&response_mode=query&scope=https%3A%2F%2Fgraph.microsoft.com%2F.default+offline_access+profile+openid&state=1&prompt=select_account" - } - return [HttpResponseContext]@{ - StatusCode = [HttpStatusCode]::OK - Body = $Results - } + $Results = @{ + applicationId = $env:ApplicationID + tenantId = $env:TenantID + orgName = $OrgInfo.displayName + authenticatedUserDisplayName = $AuthenticatedUserDisplayName + authenticatedUserPrincipalName = $AuthenticatedUserPrincipalName + isPartnerTenant = !!$OrgInfo.partnerTenantType + partnerTenantType = $OrgInfo.partnerTenantType + refreshUrl = "https://login.microsoftonline.com/$env:TenantID/oauth2/v2.0/authorize?client_id=$env:ApplicationID&response_type=code&redirect_uri=$ResponseURL&response_mode=query&scope=https%3A%2F%2Fgraph.microsoft.com%2F.default+offline_access+profile+openid&state=1&prompt=select_account" + } + return [HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::OK + Body = $Results + } -} + } From f153a186f38bde30d7dccf5077779c432fe35f55 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Sat, 24 Jan 2026 23:11:43 -0500 Subject: [PATCH 258/503] Update Invoke-ExecListAppId.ps1 --- .../Entrypoints/Invoke-ExecListAppId.ps1 | 37 ++++++++++--------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/Invoke-ExecListAppId.ps1 b/Modules/CIPPCore/Public/Entrypoints/Invoke-ExecListAppId.ps1 index 61d2008900a5..6c0581ad0c8e 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Invoke-ExecListAppId.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Invoke-ExecListAppId.ps1 @@ -66,7 +66,7 @@ function Invoke-ExecListAppId { if ($AppResponse.body) { $AppWeb = $AppResponse.body.web - if ($AppWeb.redirectUris -and $AppWeb.redirectUris.Count -gt 0) { + if ($AppWeb.redirectUris) { # construct new redirect uri with current $URL = ($Request.headers.'x-ms-original-url').split('/api') | Select-Object -First 1 $NewRedirectUri = "$($URL)/authredirect" @@ -87,23 +87,24 @@ function Invoke-ExecListAppId { } } } - } catch { - Write-LogMessage -message 'Failed to retrieve organization info and authenticated user' -LogData (Get-CippException -Exception $_) -Sev 'Warning' - } - - $Results = @{ - applicationId = $env:ApplicationID - tenantId = $env:TenantID - orgName = $OrgInfo.displayName - authenticatedUserDisplayName = $AuthenticatedUserDisplayName - authenticatedUserPrincipalName = $AuthenticatedUserPrincipalName - isPartnerTenant = !!$OrgInfo.partnerTenantType - partnerTenantType = $OrgInfo.partnerTenantType - refreshUrl = "https://login.microsoftonline.com/$env:TenantID/oauth2/v2.0/authorize?client_id=$env:ApplicationID&response_type=code&redirect_uri=$ResponseURL&response_mode=query&scope=https%3A%2F%2Fgraph.microsoft.com%2F.default+offline_access+profile+openid&state=1&prompt=select_account" - } - return [HttpResponseContext]@{ - StatusCode = [HttpStatusCode]::OK - Body = $Results } + } catch { + Write-LogMessage -message 'Failed to retrieve organization info and authenticated user' -LogData (Get-CippException -Exception $_) -Sev 'Warning' + } + $Results = @{ + applicationId = $env:ApplicationID + tenantId = $env:TenantID + orgName = $OrgInfo.displayName + authenticatedUserDisplayName = $AuthenticatedUserDisplayName + authenticatedUserPrincipalName = $AuthenticatedUserPrincipalName + isPartnerTenant = !!$OrgInfo.partnerTenantType + partnerTenantType = $OrgInfo.partnerTenantType + refreshUrl = "https://login.microsoftonline.com/$env:TenantID/oauth2/v2.0/authorize?client_id=$env:ApplicationID&response_type=code&redirect_uri=$ResponseURL&response_mode=query&scope=https%3A%2F%2Fgraph.microsoft.com%2F.default+offline_access+profile+openid&state=1&prompt=select_account" + } + return [HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::OK + Body = $Results } + +} From 3cbaf96f4f9a4c5b6000e03f100fd2a8d0146475 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Sun, 25 Jan 2026 00:14:31 -0500 Subject: [PATCH 259/503] Optimize tenant info retrieval and trigger CPV refresh Replaces multiple Microsoft Graph API calls with a single batch request to retrieve organization and domain information when adding a tenant. Adds logic to trigger a CPV permissions refresh for the new tenant by starting the appropriate orchestrator. --- .../CIPP/Setup/Invoke-ExecAddTenant.ps1 | 55 +++++++++++++++++-- 1 file changed, 49 insertions(+), 6 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecAddTenant.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecAddTenant.ps1 index 5d9b59c9616a..6f5d6263cd9d 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecAddTenant.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecAddTenant.ps1 @@ -31,13 +31,35 @@ function Invoke-ExecAddTenant { } else { # Create new tenant entry try { - # Get tenant information from Microsoft Graph + # Get tenant information from Microsoft Graph using bulk request $headers = @{ Authorization = "Bearer $($request.body.accessToken)" } - $Organization = (Invoke-RestMethod -Uri 'https://graph.microsoft.com/v1.0/organization' -Headers $headers -Method GET -ContentType 'application/json' -ErrorAction Stop).value - $displayName = $Organization.displayName - $Domains = (Invoke-RestMethod -Uri 'https://graph.microsoft.com/v1.0/domains?$top=999' -Headers $headers -Method GET -ContentType 'application/json' -ErrorAction Stop).value - $defaultDomainName = ($Domains | Where-Object { $_.isDefault -eq $true }).id - $initialDomainName = ($Domains | Where-Object { $_.isInitial -eq $true }).id + + $BulkRequests = @( + @{ + id = 'organization' + method = 'GET' + url = '/organization?$select=id,displayName' + } + @{ + id = 'domains' + method = 'GET' + url = '/domains?$top=999' + } + ) + + $BulkBody = @{ + requests = $BulkRequests + } | ConvertTo-Json -Depth 10 + + $BulkResponse = Invoke-RestMethod -Uri 'https://graph.microsoft.com/v1.0/$batch' -Headers $headers -Method POST -Body $BulkBody -ContentType 'application/json' -ErrorAction Stop + + # Parse bulk response + $OrgResponse = ($BulkResponse.responses | Where-Object { $_.id -eq 'organization' }).body.value + $DomainsResponse = ($BulkResponse.responses | Where-Object { $_.id -eq 'domains' }).body.value + + $displayName = $OrgResponse.displayName + $defaultDomainName = ($DomainsResponse | Where-Object { $_.isDefault -eq $true }).id + $initialDomainName = ($DomainsResponse | Where-Object { $_.isInitial -eq $true }).id } catch { Write-LogMessage -API 'Add-Tenant' -message "Failed to get information for tenant $tenantId - $($_.Exception.Message)" -Sev 'Critical' throw "Failed to get information for tenant $tenantId. Make sure the tenant is properly authenticated." @@ -66,6 +88,27 @@ function Invoke-ExecAddTenant { Add-CIPPAzDataTableEntity @TenantsTable -Entity $NewTenant -Force | Out-Null $Results = @{'message' = "Successfully added tenant $displayName ($defaultDomainName) to the tenant list with Direct Tenant status."; 'severity' = 'success' } Write-LogMessage -tenant $defaultDomainName -tenantid $tenantId -API 'Add-Tenant' -message "Added tenant $displayName ($defaultDomainName) with Direct Tenant status." -Sev 'Info' + + # Trigger CPV refresh to push remaining permissions to this specific tenant + try { + $Queue = New-CippQueueEntry -Name "Update Permissions - $displayName" -TotalTasks 1 + $TenantBatch = @([PSCustomObject]@{ + defaultDomainName = $defaultDomainName + customerId = $tenantId + displayName = $displayName + FunctionName = 'UpdatePermissionsQueue' + QueueId = $Queue.RowKey + }) + $InputObject = [PSCustomObject]@{ + OrchestratorName = 'UpdatePermissionsOrchestrator' + Batch = @($TenantBatch) + } + Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Depth 5 -Compress) + Write-Information "Started permissions update orchestrator for $displayName" + } catch { + Write-Warning "Failed to start permissions orchestrator: $($_.Exception.Message)" + } + } } catch { $Results = @{'message' = "Failed to add tenant: $($_.Exception.Message)"; 'state' = 'error'; 'severity' = 'error' } From 09d5c7bc691233cbd8eb424f0b9f757998652a1d Mon Sep 17 00:00:00 2001 From: John Duprey Date: Sun, 25 Jan 2026 00:32:32 -0500 Subject: [PATCH 260/503] Update success message after adding tenant The success message now informs users that a permission refresh is queued and the tenant will be available shortly, providing clearer feedback after adding a tenant. --- .../HTTP Functions/CIPP/Setup/Invoke-ExecAddTenant.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecAddTenant.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecAddTenant.ps1 index 6f5d6263cd9d..ea5bcb8d9158 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecAddTenant.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecAddTenant.ps1 @@ -86,7 +86,7 @@ function Invoke-ExecAddTenant { # Add tenant to table Add-CIPPAzDataTableEntity @TenantsTable -Entity $NewTenant -Force | Out-Null - $Results = @{'message' = "Successfully added tenant $displayName ($defaultDomainName) to the tenant list with Direct Tenant status."; 'severity' = 'success' } + $Results = @{'message' = "Successfully added tenant $displayName ($defaultDomainName) to the tenant list with Direct Tenant status. Permission refresh queued, the tenant will be available shortly."; 'severity' = 'success' } Write-LogMessage -tenant $defaultDomainName -tenantid $tenantId -API 'Add-Tenant' -message "Added tenant $displayName ($defaultDomainName) with Direct Tenant status." -Sev 'Info' # Trigger CPV refresh to push remaining permissions to this specific tenant From 2b5360eab15dca1e942d45dadc294f7f20ebf4ec Mon Sep 17 00:00:00 2001 From: John Duprey Date: Sun, 25 Jan 2026 00:58:43 -0500 Subject: [PATCH 261/503] Set refresh token as environment variable after update After updating the refresh token, immediately set it as an environment variable to make it available for subsequent operations. This applies to both the main tenant and additional tenants, ensuring the new token is accessible without delay. --- .../CIPP/Setup/Invoke-ExecUpdateRefreshToken.ps1 | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecUpdateRefreshToken.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecUpdateRefreshToken.ps1 index 6a6dfae29ccc..567b5f89f31a 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecUpdateRefreshToken.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecUpdateRefreshToken.ps1 @@ -21,27 +21,35 @@ function Invoke-ExecUpdateRefreshToken { if ($env:TenantID -eq $Request.body.tenantId) { $Secret | Add-Member -MemberType NoteProperty -Name 'RefreshToken' -Value $Request.body.refreshtoken -Force + # Set environment variable to make it immediately available + Set-Item -Path env:RefreshToken -Value $Request.body.refreshtoken -Force } else { Write-Host "$($env:TenantID) does not match $($Request.body.tenantId)" $name = $Request.body.tenantId -replace '-', '_' $secret | Add-Member -MemberType NoteProperty -Name $name -Value $Request.body.refreshtoken -Force + # Set environment variable to make it immediately available + Set-Item -Path env:$name -Value $Request.body.refreshtoken -Force } Add-CIPPAzDataTableEntity @DevSecretsTable -Entity $Secret -Force } else { if ($env:TenantID -eq $Request.body.tenantId) { Set-CippKeyVaultSecret -VaultName $kv -Name 'RefreshToken' -SecretValue (ConvertTo-SecureString -String $Request.body.refreshtoken -AsPlainText -Force) + # Set environment variable to make it immediately available + Set-Item -Path env:RefreshToken -Value $Request.body.refreshtoken -Force + $InstanceId = Start-UpdatePermissionsOrchestrator #start the CPV refresh immediately while wizard still runs. } else { Write-Host "$($env:TenantID) does not match $($Request.body.tenantId) - we're adding a new secret for the tenant." $name = $Request.body.tenantId try { Set-CippKeyVaultSecret -VaultName $kv -Name $name -SecretValue (ConvertTo-SecureString -String $Request.body.refreshtoken -AsPlainText -Force) + # Set environment variable to make it immediately available + Set-Item -Path env:$name -Value $Request.body.refreshtoken -Force } catch { Write-Host "Failed to set secret $name in KeyVault. $($_.Exception.Message)" throw $_ } } } - $InstanceId = Start-UpdatePermissionsOrchestrator #start the CPV refresh immediately while wizard still runs. if ($request.body.tenantId -eq $env:TenantID) { $TenantName = 'your partner tenant' From 82655b3c4bacb3fb01b3eabba4ad4681786c3ba9 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Sun, 25 Jan 2026 01:03:24 -0500 Subject: [PATCH 262/503] Add Force switch to Get-CIPPAuthentication Introduces a -Force switch to the Get-CIPPAuthentication function, allowing environment variables to be overwritten even if they are already set. Without the switch, existing environment variables are skipped. This is to avoid rate limits with the key vault api when we are adding separate tenants. --- Modules/CIPPCore/Public/Get-CIPPAuthentication.ps1 | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Get-CIPPAuthentication.ps1 b/Modules/CIPPCore/Public/Get-CIPPAuthentication.ps1 index 1aecba454413..fa8e47ef838b 100644 --- a/Modules/CIPPCore/Public/Get-CIPPAuthentication.ps1 +++ b/Modules/CIPPCore/Public/Get-CIPPAuthentication.ps1 @@ -2,7 +2,8 @@ function Get-CIPPAuthentication { [CmdletBinding()] param ( - $APIName = 'Get Keyvault Authentication' + $APIName = 'Get Keyvault Authentication', + [switch]$Force ) $Variables = @('ApplicationID', 'ApplicationSecret', 'TenantID', 'RefreshToken') @@ -42,6 +43,12 @@ function Get-CIPPAuthentication { if ($tenants) { $tenants | ForEach-Object { $name = $_.customerId + + # if environment variable already set, skip unless force switch is set + if ((Get-Item -Path env:$name -ErrorAction SilentlyContinue) -and -not $Force.IsPresent) { + return + } + $secret = Get-CippKeyVaultSecret -VaultName $keyvaultname -Name $name -AsPlainText -ErrorAction Stop if ($secret) { Set-Item -Path env:$name -Value $secret -Force From 68dadd81e7653662ec56b5e7b97bc00e2adda872 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Sun, 25 Jan 2026 02:26:03 -0500 Subject: [PATCH 263/503] Trigger CPV refresh for partner tenant after token update Replaces the immediate call to Start-UpdatePermissionsOrchestrator with logic to queue and start the permissions update orchestrator specifically for the partner tenant after updating the refresh token. Adds error handling and logging for orchestrator startup. --- .../Setup/Invoke-ExecUpdateRefreshToken.ps1 | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecUpdateRefreshToken.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecUpdateRefreshToken.ps1 index 567b5f89f31a..8725c2c87de1 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecUpdateRefreshToken.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecUpdateRefreshToken.ps1 @@ -36,7 +36,26 @@ function Invoke-ExecUpdateRefreshToken { Set-CippKeyVaultSecret -VaultName $kv -Name 'RefreshToken' -SecretValue (ConvertTo-SecureString -String $Request.body.refreshtoken -AsPlainText -Force) # Set environment variable to make it immediately available Set-Item -Path env:RefreshToken -Value $Request.body.refreshtoken -Force - $InstanceId = Start-UpdatePermissionsOrchestrator #start the CPV refresh immediately while wizard still runs. + + # Trigger CPV refresh for partner tenant only + try { + $Queue = New-CippQueueEntry -Name 'Update Permissions - Partner Tenant' -TotalTasks 1 + $TenantBatch = @([PSCustomObject]@{ + defaultDomainName = 'PartnerTenant' + customerId = $env:TenantID + displayName = '*Partner Tenant' + FunctionName = 'UpdatePermissionsQueue' + QueueId = $Queue.RowKey + }) + $InputObject = [PSCustomObject]@{ + OrchestratorName = 'UpdatePermissionsOrchestrator' + Batch = @($TenantBatch) + } + Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Depth 5 -Compress) + Write-Information 'Started permissions update orchestrator for Partner Tenant' + } catch { + Write-Warning "Failed to start permissions orchestrator: $($_.Exception.Message)" + } } else { Write-Host "$($env:TenantID) does not match $($Request.body.tenantId) - we're adding a new secret for the tenant." $name = $Request.body.tenantId From d7bbf29427fc154381a2afbf6b7bbf435fa44980 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Mon, 26 Jan 2026 14:30:24 -0500 Subject: [PATCH 264/503] fix sort --- .../HTTP Functions/CIPP/Settings/Invoke-ExecCustomRole.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecCustomRole.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecCustomRole.ps1 index dc3a91b75e05..b4d91f8bffa6 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecCustomRole.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecCustomRole.ps1 @@ -178,7 +178,7 @@ function Invoke-ExecCustomRole { } } default { - $Body = Get-CIPPAzDataTableEntity @Table + $Body = Get-CIPPAzDataTableEntity @Table | Sort-Object -Property RowKey $EntraRoleGroups = Get-CIPPAzDataTableEntity @AccessRoleGroupTable $AccessIPRanges = Get-CIPPAzDataTableEntity @AccessIPRangeTable if (!$Body) { From 412b97e70e6465b2c977ee0b727cd1346336b69c Mon Sep 17 00:00:00 2001 From: Phillip Schjeldal Hansen Date: Tue, 27 Jan 2026 13:10:29 +0100 Subject: [PATCH 265/503] Added standard to enable windows diagnostic data settings in Intune --- ...ke-CIPPStandardIntuneWindowsDiagnostic.ps1 | 106 ++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardIntuneWindowsDiagnostic.ps1 diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardIntuneWindowsDiagnostic.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardIntuneWindowsDiagnostic.ps1 new file mode 100644 index 000000000000..aaa98a19957a --- /dev/null +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardIntuneWindowsDiagnostic.ps1 @@ -0,0 +1,106 @@ +Function Invoke-CIPPStandardIntuneWindowsDiagnostic { + <# + .FUNCTIONALITY + Internal + .COMPONENT + (APIName) IntuneWindowsDiagnostic + .SYNOPSIS + (Label) Set Intune Windows diagnostic data settings + .DESCRIPTION + (Helptext) **Some features require Windows E3 or equivalent licenses** Configures Windows diagnostic data settings for Intune. Enables features like Windows update reports, device readiness reports, and driver update reports. More information can be found in [Microsoft's documentation.](https://go.microsoft.com/fwlink/?linkid=2204384) + (DocsDescription) Enables Windows diagnostic data in processor configuration for your Intune tenant. This setting is required for several Intune features including Windows feature update device readiness reports, compatibility risk reports, driver update reports, and update policy alerts. When enabled, your organization becomes the controller of Windows diagnostic data collected from managed devices, allowing Intune to use this data for reporting and update management features. More information can be found in [Microsoft's documentation.](https://go.microsoft.com/fwlink/?linkid=2204384) + .NOTES + CAT + Intune Standards + TAG + EXECUTIVETEXT + Enables access to Windows Update reporting and compatibility analysis features in Intune by allowing the use of Windows diagnostic data. This unlocks important capabilities like device readiness reports for feature updates, driver update reports, and proactive alerts for update failures, helping IT teams plan and monitor Windows updates more effectively across the organization. + ADDEDCOMPONENT + {"type":"switch","name":"standards.IntuneWindowsDiagnostic.areDataProcessorServiceForWindowsFeaturesEnabled","label":"Enable Windows data","defaultValue":false} + {"type":"switch","name":"standards.IntuneWindowsDiagnostic.hasValidWindowsLicense","label":"Confirm ownership of the required Windows E3 or equivalent licenses (Enables Windows update app and driver compatibility reports)","defaultValue":false} + IMPACT + Low Impact + ADDEDDATE + 2026-01-27 + POWERSHELLEQUIVALENT + Graph API + RECOMMENDEDBY + UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block + .LINK + https://docs.cipp.app/user-documentation/tenant/standards/list-standards + #> + [CmdletBinding()] + param($Tenant, $Settings) + + $TestResult = Test-CIPPStandardLicense -StandardName 'IntuneWindowsDiagnostic' -TenantFilter $Tenant -RequiredCapabilities @('INTUNE_A', 'MDM_Services', 'EMS', 'SCCM', 'MICROSOFTINTUNEPLAN1') + + if ($TestResult -eq $false) { + Write-Host "We're exiting as the correct license is not present for this standard." + return $true + } + + write-host $Settings + write-host ($settings | ConvertTo-Json -Depth 10) + + # Example diagnostic logic for Intune Windows devices + try { + + $CurrentInfo = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/deviceManagement/dataProcessorServiceForWindowsFeaturesOnboarding" -tenantid $Tenant + $CurrentValue = $CurrentInfo | Select-Object -Property areDataProcessorServiceForWindowsFeaturesEnabled, hasValidWindowsLicense + + $StateIsCorrect = ($CurrentInfo.areDataProcessorServiceForWindowsFeaturesEnabled -eq $Settings.areDataProcessorServiceForWindowsFeaturesEnabled) -and ($CurrentInfo.hasValidWindowsLicense -eq $Settings.hasValidWindowsLicense) + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to retrieve Windows diagnostic data settings for Intune." -Sev 'Error' -LogData $ErrorMessage + throw "Failed to retrieve current Windows diagnostic data settings for Intune. Error: $($ErrorMessage)" + } + + if ($Settings.remediate -eq $true) { + if ($StateIsCorrect -eq $true) { + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Windows Diagnostic for Intune is already in the desired state." -sev Info + } + else { + $Body = [pscustomobject]@{ + value = @{ + areDataProcessorServiceForWindowsFeaturesEnabled = $Settings.areDataProcessorServiceForWindowsFeaturesEnabled + hasValidWindowsLicense = $Settings.hasValidWindowsLicense + } + } | ConvertTo-Json -Depth 10 -Compress + + try { + $null = New-GraphPostRequest -tenantid $tenant -Uri 'https://graph.microsoft.com/beta/deviceManagement/dataProcessorServiceForWindowsFeaturesOnboarding' -Type PATCH -Body $body -ContentType 'application/json' -AsApp $true + $CurrentInfo.areDataProcessorServiceForWindowsFeaturesEnabled = $Settings.areDataProcessorServiceForWindowsFeaturesEnabled + $CurrentInfo.hasValidWindowsLicense = $Settings.hasValidWindowsLicense + $StateIsCorrect = $true + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Successfully updated Windows Diagnostic settings for Intune." -sev Info + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to update Windows Diagnostic settings for Intune. Error: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + } + } + } + + if ($Settings.alert -eq $true) { + if ($StateIsCorrect -eq $true) { + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Windows Diagnostic for Intune is in the desired state." -sev Info + } else { + Write-StandardsAlert -message "Windows Diagnostic for Intune is not in the desired state." -object $CurrentValue -tenant $Tenant -standardName 'IntuneWindowsDiagnostic' -standardId $Settings.standardId + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Windows Diagnostic for Intune is not in the desired state." -sev Info + } + + } + + if ($Settings.report -eq $true) { + $CurrentValue = @{ + areDataProcessorServiceForWindowsFeaturesEnabled = $CurrentInfo.areDataProcessorServiceForWindowsFeaturesEnabled + hasValidWindowsLicense = $CurrentInfo.hasValidWindowsLicense + } + $ExpectedValue = @{ + areDataProcessorServiceForWindowsFeaturesEnabled = $Settings.areDataProcessorServiceForWindowsFeaturesEnabled + hasValidWindowsLicense = $Settings.hasValidWindowsLicense + } + Set-CIPPStandardsCompareField -FieldName 'standards.IntuneWindowsDiagnostic' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant + Add-CIPPBPAField -FieldName 'IntuneWindowsDiagnostic' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $Tenant + } +} From db3ce770f30b43769b8aed540bba68e75429f49b Mon Sep 17 00:00:00 2001 From: Phillip Schjeldal Hansen Date: Tue, 27 Jan 2026 13:10:46 +0100 Subject: [PATCH 266/503] Invoke-ListExtensionsConfig failed to run if table is empty --- .../CIPPCore/Public/Entrypoints/Invoke-ListExtensionsConfig.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/Invoke-ListExtensionsConfig.ps1 b/Modules/CIPPCore/Public/Entrypoints/Invoke-ListExtensionsConfig.ps1 index 45faae5f8224..eb94ced2c322 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Invoke-ListExtensionsConfig.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Invoke-ListExtensionsConfig.ps1 @@ -10,7 +10,7 @@ function Invoke-ListExtensionsConfig { $Table = Get-CIPPTable -TableName Extensionsconfig try { $Config = (Get-CIPPAzDataTableEntity @Table).config - if (Test-Json -Json $Config) { + if (Test-Json -Json $Config -ErrorAction SilentlyContinue) { $Body = $Config | ConvertFrom-Json -Depth 10 -ErrorAction Stop if ($Body.HaloPSA.TicketType -and !$Body.HaloPSA.TicketType.value) { # translate ticket type to autocomplete format From 7d9d89912ac14a683af472a542b2358a2f029035 Mon Sep 17 00:00:00 2001 From: Phillip Schjeldal Hansen Date: Tue, 27 Jan 2026 14:12:48 +0100 Subject: [PATCH 267/503] Oops, I forgot to remove some debug output lines --- .../Standards/Invoke-CIPPStandardIntuneWindowsDiagnostic.ps1 | 3 --- 1 file changed, 3 deletions(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardIntuneWindowsDiagnostic.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardIntuneWindowsDiagnostic.ps1 index aaa98a19957a..80410575de19 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardIntuneWindowsDiagnostic.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardIntuneWindowsDiagnostic.ps1 @@ -40,9 +40,6 @@ Function Invoke-CIPPStandardIntuneWindowsDiagnostic { return $true } - write-host $Settings - write-host ($settings | ConvertTo-Json -Depth 10) - # Example diagnostic logic for Intune Windows devices try { From 2e51ba58f492eed3bee3e9494ab41cb9f8ecf87c Mon Sep 17 00:00:00 2001 From: John Duprey Date: Tue, 27 Jan 2026 14:54:01 -0500 Subject: [PATCH 268/503] Refactor direct tenant secret loading for efficiency Removed eager loading of direct tenant secrets in Get-CIPPAuthentication and implemented lazy loading in Get-GraphToken. This change improves performance by only fetching secrets from storage or Key Vault when needed, rather than at authentication initialization. --- .../Public/Get-CIPPAuthentication.ps1 | 33 ------------------- .../Public/GraphHelper/Get-GraphToken.ps1 | 28 ++++++++++++++++ 2 files changed, 28 insertions(+), 33 deletions(-) diff --git a/Modules/CIPPCore/Public/Get-CIPPAuthentication.ps1 b/Modules/CIPPCore/Public/Get-CIPPAuthentication.ps1 index fa8e47ef838b..cc70d3e0a5b5 100644 --- a/Modules/CIPPCore/Public/Get-CIPPAuthentication.ps1 +++ b/Modules/CIPPCore/Public/Get-CIPPAuthentication.ps1 @@ -20,41 +20,8 @@ function Get-CIPPAuthentication { } } Write-Host "Got secrets from dev storage. ApplicationID: $env:ApplicationID" - #Get list of tenants that have 'directTenant' set to true - #get directtenants directly from table, avoid get-tenants due to performance issues - $TenantsTable = Get-CippTable -tablename 'Tenants' - $Filter = "PartitionKey eq 'Tenants' and delegatedPrivilegeStatus eq 'directTenant'" - $tenants = Get-CIPPAzDataTableEntity @TenantsTable -Filter $Filter - if ($tenants) { - $tenants | ForEach-Object { - $secretname = $_.customerId -replace '-', '_' - if ($secret.$secretname) { - $name = $_.customerId - Set-Item -Path env:$name -Value $secret.$secretname -Force - } - } - } } else { $keyvaultname = ($env:WEBSITE_DEPLOYMENT_ID -split '-')[0] - #Get list of tenants that have 'directTenant' set to true - $TenantsTable = Get-CippTable -tablename 'Tenants' - $Filter = "PartitionKey eq 'Tenants' and delegatedPrivilegeStatus eq 'directTenant'" - $tenants = Get-CIPPAzDataTableEntity @TenantsTable -Filter $Filter - if ($tenants) { - $tenants | ForEach-Object { - $name = $_.customerId - - # if environment variable already set, skip unless force switch is set - if ((Get-Item -Path env:$name -ErrorAction SilentlyContinue) -and -not $Force.IsPresent) { - return - } - - $secret = Get-CippKeyVaultSecret -VaultName $keyvaultname -Name $name -AsPlainText -ErrorAction Stop - if ($secret) { - Set-Item -Path env:$name -Value $secret -Force - } - } - } $Variables | ForEach-Object { Set-Item -Path env:$_ -Value (Get-CippKeyVaultSecret -VaultName $keyvaultname -Name $_ -AsPlainText -ErrorAction Stop) -Force } diff --git a/Modules/CIPPCore/Public/GraphHelper/Get-GraphToken.ps1 b/Modules/CIPPCore/Public/GraphHelper/Get-GraphToken.ps1 index 327052774f61..1548ebe869fc 100644 --- a/Modules/CIPPCore/Public/GraphHelper/Get-GraphToken.ps1 +++ b/Modules/CIPPCore/Public/GraphHelper/Get-GraphToken.ps1 @@ -24,6 +24,34 @@ function Get-GraphToken($tenantid, $scope, $AsApp, $AppID, $AppSecret, $refreshT if ($tenantid -ne $env:TenantID -and $clientType.delegatedPrivilegeStatus -eq 'directTenant') { Write-Host "Using direct tenant refresh token for $($clientType.customerId)" $ClientRefreshToken = Get-Item -Path "env:\$($clientType.customerId)" -ErrorAction SilentlyContinue + + if ($null -eq $ClientRefreshToken) { + # Lazy load the refresh token from Key Vault only when needed + Write-Host "Fetching refresh token for direct tenant $($clientType.customerId) from Key Vault" + try { + if ($env:AzureWebJobsStorage -eq 'UseDevelopmentStorage=true' -or $env:NonLocalHostAzurite -eq 'true') { + # Development environment - get from table storage + $Table = Get-CIPPTable -tablename 'DevSecrets' + $Secret = Get-AzDataTableEntity @Table -Filter "PartitionKey eq 'Secret' and RowKey eq 'Secret'" + $secretname = $clientType.customerId -replace '-', '_' + if ($Secret.$secretname) { + Set-Item -Path "env:\$($clientType.customerId)" -Value $Secret.$secretname -Force + $ClientRefreshToken = Get-Item -Path "env:\$($clientType.customerId)" -ErrorAction SilentlyContinue + } + } else { + # Production environment - get from Key Vault + $keyvaultname = ($env:WEBSITE_DEPLOYMENT_ID -split '-')[0] + $secret = Get-CippKeyVaultSecret -VaultName $keyvaultname -Name $clientType.customerId -AsPlainText -ErrorAction Stop + if ($secret) { + Set-Item -Path "env:\$($clientType.customerId)" -Value $secret -Force + $ClientRefreshToken = Get-Item -Path "env:\$($clientType.customerId)" -ErrorAction SilentlyContinue + } + } + } catch { + Write-Host "Failed to retrieve refresh token for direct tenant $($clientType.customerId): $($_.Exception.Message)" + } + } + $refreshToken = $ClientRefreshToken.Value } From f5b2783ca435a716fe2557e03b4d61bcce204927 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Tue, 27 Jan 2026 20:13:20 -0500 Subject: [PATCH 269/503] Return status object on test function failure Updated Push-CIPPTest to return @{ testRun = $false } when a test function is not found or an exception occurs, providing clearer feedback on test execution status. --- .../{Invoke-CIPPTestsRun.ps1 => Invoke-CIPPDBTestsRun.ps1} | 0 .../Entrypoints/Activity Triggers/Tests/Push-CIPPTest.ps1 | 3 ++- 2 files changed, 2 insertions(+), 1 deletion(-) rename Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Tests/{Invoke-CIPPTestsRun.ps1 => Invoke-CIPPDBTestsRun.ps1} (100%) diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Tests/Invoke-CIPPTestsRun.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Tests/Invoke-CIPPDBTestsRun.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Tests/Invoke-CIPPTestsRun.ps1 rename to Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Tests/Invoke-CIPPDBTestsRun.ps1 diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Tests/Push-CIPPTest.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Tests/Push-CIPPTest.ps1 index 844c8ff80695..5b2124a4dea0 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Tests/Push-CIPPTest.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Tests/Push-CIPPTest.ps1 @@ -17,7 +17,7 @@ function Push-CIPPTest { if (-not (Get-Command $FunctionName -ErrorAction SilentlyContinue)) { Write-LogMessage -API 'Tests' -tenant $TenantFilter -message "Test function not found: $FunctionName" -sev Error - return + return @{ testRun = $false } } Write-Information "Executing $FunctionName for $TenantFilter" @@ -28,5 +28,6 @@ function Push-CIPPTest { } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Tests' -tenant $TenantFilter -message "Failed to run test $TestId $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + return @{ testRun = $false } } } From f144e1c4f3b4fa29d627b95bd9e52eb3eb00b323 Mon Sep 17 00:00:00 2001 From: rvdwegen Date: Wed, 28 Jan 2026 10:59:34 +0100 Subject: [PATCH 270/503] Yeet Write-Hosts --- .../Standards/Invoke-CIPPStandardAddDKIM.ps1 | 1 - .../Invoke-CIPPStandardAntiPhishPolicy.ps1 | 1 - .../Invoke-CIPPStandardAntiSpamSafeList.ps1 | 2 -- ...e-CIPPStandardAssignmentFilterTemplate.ps1 | 1 - .../Invoke-CIPPStandardAtpPolicyForO365.ps1 | 1 - .../Standards/Invoke-CIPPStandardAuditLog.ps1 | 4 ---- ...Invoke-CIPPStandardAuthMethodsSettings.ps1 | 4 ---- .../Invoke-CIPPStandardAutoAddProxy.ps1 | 1 - .../Invoke-CIPPStandardAutoArchive.ps1 | 3 --- .../Invoke-CIPPStandardAutoArchiveMailbox.ps1 | 3 --- .../Invoke-CIPPStandardAutoExpandArchive.ps1 | 3 --- .../Invoke-CIPPStandardAutopilotProfile.ps1 | 1 - ...Invoke-CIPPStandardAutopilotStatusPage.ps1 | 1 - .../Standards/Invoke-CIPPStandardBookings.ps1 | 2 -- .../Invoke-CIPPStandardCloudMessageRecall.ps1 | 2 -- ...-CIPPStandardConditionalAccessTemplate.ps1 | 1 - ...e-CIPPStandardCustomBannedPasswordList.ps1 | 7 ------- ...IPPStandardDefaultPlatformRestrictions.ps1 | 1 - .../Invoke-CIPPStandardDefaultSharingLink.ps1 | 2 -- .../Invoke-CIPPStandardDelegateSentItems.ps1 | 5 ----- ...voke-CIPPStandardDeletedUserRentention.ps1 | 3 --- ...oke-CIPPStandardDeployContactTemplates.ps1 | 1 - .../Invoke-CIPPStandardDeployMailContact.ps1 | 1 - ...PStandardDisableAddShortcutsToOneDrive.ps1 | 3 --- ...ndardDisableAdditionalStorageProviders.ps1 | 1 - ...nvoke-CIPPStandardDisableBasicAuthSMTP.ps1 | 3 --- ...tandardDisableExchangeOnlinePowerShell.ps1 | 2 -- ...StandardDisableExternalCalendarSharing.ps1 | 1 - .../Invoke-CIPPStandardDisableGuests.ps1 | 1 - ...voke-CIPPStandardDisableM365GroupUsers.ps1 | 1 - ...nvoke-CIPPStandardDisableOutlookAddins.ps1 | 1 - .../Invoke-CIPPStandardDisableReshare.ps1 | 1 - ...oke-CIPPStandardDisableResourceMailbox.ps1 | 4 ---- ...IPPStandardDisableSharePointLegacyAuth.ps1 | 1 - .../Invoke-CIPPStandardDisableTNEF.ps1 | 3 --- ...voke-CIPPStandardDisableTenantCreation.ps1 | 1 - ...voke-CIPPStandardDisableUserSiteCreate.ps1 | 1 - .../Invoke-CIPPStandardDisableViva.ps1 | 2 -- .../Invoke-CIPPStandardEXODirectSend.ps1 | 2 -- ...e-CIPPStandardEXODisableAutoForwarding.ps1 | 1 - ...voke-CIPPStandardEXOOutboundSpamLimits.ps1 | 2 -- ...voke-CIPPStandardEnableCustomerLockbox.ps1 | 2 -- ...nvoke-CIPPStandardEnableLitigationHold.ps1 | 2 -- .../Invoke-CIPPStandardEnableMailTips.ps1 | 1 - ...voke-CIPPStandardEnableMailboxAuditing.ps1 | 2 -- ...ke-CIPPStandardEnableNamePronunciation.ps1 | 3 --- ...voke-CIPPStandardEnableOnlineArchiving.ps1 | 3 --- .../Invoke-CIPPStandardEnablePronouns.ps1 | 2 -- ...ntWindowsHelloForBusinessConfiguration.ps1 | 1 - ...-CIPPStandardExchangeConnectorTemplate.ps1 | 1 - .../Invoke-CIPPStandardExcludedfileExt.ps1 | 6 ------ .../Invoke-CIPPStandardExternalMFATrusted.ps1 | 2 -- .../Invoke-CIPPStandardFocusedInbox.ps1 | 3 --- ...ke-CIPPStandardFormsPhishingProtection.ps1 | 2 -- ...PStandardGlobalQuarantineNotifications.ps1 | 3 --- .../Invoke-CIPPStandardGroupTemplate.ps1 | 2 -- ...e-CIPPStandardIntuneComplianceSettings.ps1 | 1 - .../Invoke-CIPPStandardIntuneTemplate.ps1 | 20 ------------------- ...ke-CIPPStandardIntuneWindowsDiagnostic.ps1 | 4 ---- ...tandardMDMEnrollmentDuringRegistration.ps1 | 1 - .../Standards/Invoke-CIPPStandardMDMScope.ps1 | 1 - .../Invoke-CIPPStandardMailContacts.ps1 | 1 - ...oke-CIPPStandardMailboxRecipientLimits.ps1 | 1 - ...Invoke-CIPPStandardMalwareFilterPolicy.ps1 | 1 - .../Invoke-CIPPStandardMessageExpiration.ps1 | 2 -- .../Standards/Invoke-CIPPStandardNudgeMFA.ps1 | 2 +- ...-CIPPStandardOWAAttachmentRestrictions.ps1 | 1 - .../Invoke-CIPPStandardOauthConsent.ps1 | 1 - .../Invoke-CIPPStandardOutBoundSpamAlert.ps1 | 1 - ...CIPPStandardPWcompanionAppAllowedState.ps1 | 2 -- ...oke-CIPPStandardPasswordExpireDisabled.ps1 | 1 - .../Invoke-CIPPStandardPhishProtection.ps1 | 2 -- ...-CIPPStandardPhishSimSpoofIntelligence.ps1 | 2 -- ...Invoke-CIPPStandardPhishingSimulations.ps1 | 1 - .../Invoke-CIPPStandardProfilePhotos.ps1 | 7 ------- ...oke-CIPPStandardQuarantineRequestAlert.ps1 | 1 - .../Invoke-CIPPStandardQuarantineTemplate.ps1 | 1 - ...ndardRestrictThirdPartyStorageServices.ps1 | 3 --- .../Invoke-CIPPStandardRetentionPolicyTag.ps1 | 3 --- .../Invoke-CIPPStandardRotateDKIM.ps1 | 1 - .../Invoke-CIPPStandardSPAzureB2B.ps1 | 1 - .../Invoke-CIPPStandardSPDirectSharing.ps1 | 1 - ...e-CIPPStandardSPDisableLegacyWorkflows.ps1 | 1 - ...ke-CIPPStandardSPDisallowInfectedFiles.ps1 | 1 - .../Invoke-CIPPStandardSPEmailAttestation.ps1 | 1 - ...e-CIPPStandardSPExternalUserExpiration.ps1 | 1 - .../Invoke-CIPPStandardSPSyncButtonState.ps1 | 3 --- ...nvoke-CIPPStandardSafeAttachmentPolicy.ps1 | 1 - .../Invoke-CIPPStandardSafeLinksPolicy.ps1 | 1 - ...ke-CIPPStandardSafeLinksTemplatePolicy.ps1 | 1 - .../Invoke-CIPPStandardSafeSendersDisable.ps1 | 2 -- ...oke-CIPPStandardSecureScoreRemediation.ps1 | 2 -- .../Invoke-CIPPStandardSecurityDefaults.ps1 | 1 - .../Invoke-CIPPStandardSendFromAlias.ps1 | 1 - ...oke-CIPPStandardSendReceiveLimitTenant.ps1 | 4 ---- ...IPPStandardSharePointMassDeletionAlert.ps1 | 1 - .../Invoke-CIPPStandardShortenMeetings.ps1 | 5 +---- .../Invoke-CIPPStandardSpamFilterPolicy.ps1 | 1 - .../Invoke-CIPPStandardSpoofWarn.ps1 | 6 ------ .../Invoke-CIPPStandardStaleEntraDevices.ps1 | 3 --- ...Invoke-CIPPStandardTeamsChatProtection.ps1 | 1 - ...voke-CIPPStandardTeamsEmailIntegration.ps1 | 1 - .../Invoke-CIPPStandardTeamsEnrollUser.ps1 | 1 - ...-CIPPStandardTeamsExternalAccessPolicy.ps1 | 1 - ...IPPStandardTeamsExternalChatWithAnyone.ps1 | 1 - ...e-CIPPStandardTeamsExternalFileSharing.ps1 | 1 - ...PPStandardTeamsFederationConfiguration.ps1 | 1 - ...e-CIPPStandardTeamsGlobalMeetingPolicy.ps1 | 1 - .../Invoke-CIPPStandardTeamsGuestAccess.ps1 | 1 - ...tandardTeamsMeetingRecordingExpiration.ps1 | 2 -- ...e-CIPPStandardTeamsMeetingVerification.ps1 | 1 - ...oke-CIPPStandardTeamsMeetingsByDefault.ps1 | 2 -- ...nvoke-CIPPStandardTeamsMessagingPolicy.ps1 | 1 - ...voke-CIPPStandardTenantDefaultTimezone.ps1 | 1 - ...voke-CIPPStandardTransportRuleTemplate.ps1 | 5 ----- ...ke-CIPPStandardTwoClickEmailProtection.ps1 | 3 --- .../Invoke-CIPPStandardUserSubmissions.ps1 | 1 - .../Invoke-CIPPStandardcalDefault.ps1 | 6 ------ .../Invoke-CIPPStandarddisableMacSync.ps1 | 1 - ...voke-CIPPStandardintuneBrandingProfile.ps1 | 1 - .../Invoke-CIPPStandardintuneDeviceReg.ps1 | 1 - ...CIPPStandardintuneDeviceRetirementDays.ps1 | 1 - .../Invoke-CIPPStandardsharingCapability.ps1 | 3 --- ...e-CIPPStandardsharingDomainRestriction.ps1 | 4 ---- .../Invoke-CIPPStandardunmanagedSync.ps1 | 1 - 125 files changed, 2 insertions(+), 257 deletions(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAddDKIM.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAddDKIM.ps1 index fbaf8ce3251a..8bfce0573246 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAddDKIM.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAddDKIM.ps1 @@ -37,7 +37,6 @@ function Invoke-CIPPStandardAddDKIM { $TestResult = Test-CIPPStandardLicense -StandardName 'AddDKIM' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAntiPhishPolicy.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAntiPhishPolicy.ps1 index 79f02f28e15f..daeb2dfe9d27 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAntiPhishPolicy.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAntiPhishPolicy.ps1 @@ -55,7 +55,6 @@ function Invoke-CIPPStandardAntiPhishPolicy { $TestResult = Test-CIPPStandardLicense -StandardName 'AntiPhishPolicy' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'AntiPhishPolicy' diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAntiSpamSafeList.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAntiSpamSafeList.ps1 index 614a1a7f036c..e959f26818ea 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAntiSpamSafeList.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAntiSpamSafeList.ps1 @@ -35,7 +35,6 @@ function Invoke-CIPPStandardAntiSpamSafeList { $TestResult = Test-CIPPStandardLicense -StandardName 'AntiSpamSafeList' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'AntiSpamSafeList' @@ -69,7 +68,6 @@ function Invoke-CIPPStandardAntiSpamSafeList { } if ($Settings.remediate -eq $true) { - Write-Host 'Time to remediate' if ($StateIsCorrect -eq $false) { try { $null = New-ExoRequest -tenantid $Tenant -cmdlet 'Set-HostedConnectionFilterPolicy' -cmdParams @{ diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAssignmentFilterTemplate.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAssignmentFilterTemplate.ps1 index 8d371752daa4..5a35370671d4 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAssignmentFilterTemplate.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAssignmentFilterTemplate.ps1 @@ -50,7 +50,6 @@ function Invoke-CIPPStandardAssignmentFilterTemplate { $CurrentValue = if ($MissingFilters.Count -eq 0) { [PSCustomObject]@{'state' = 'Configured correctly' } } else { [PSCustomObject]@{'MissingFilters' = @($MissingFilters) } } if ($Settings.remediate -eq $true) { - Write-Host "Settings: $($Settings.TemplateList | ConvertTo-Json)" foreach ($Template in $AssignmentFilterTemplates) { Write-Information "Processing template: $($Template.displayName)" try { diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAtpPolicyForO365.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAtpPolicyForO365.ps1 index 3bf5fd8ba0d7..3cf2a1536f67 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAtpPolicyForO365.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAtpPolicyForO365.ps1 @@ -36,7 +36,6 @@ function Invoke-CIPPStandardAtpPolicyForO365 { ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'AtpPolicyForO365' if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. try { diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAuditLog.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAuditLog.ps1 index 4d1c5a4d1b68..21d7ff92f9fd 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAuditLog.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAuditLog.ps1 @@ -38,12 +38,10 @@ function Invoke-CIPPStandardAuditLog { $TestResult = Test-CIPPStandardLicense -StandardName 'AuditLog' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'AuditLog' - Write-Host ($Settings | ConvertTo-Json) $AuditLogEnabled = [bool](New-ExoRequest -tenantid $Tenant -cmdlet 'Get-AdminAuditLogConfig' -Select UnifiedAuditLogIngestionEnabled).UnifiedAuditLogIngestionEnabled $CurrentValue = [PSCustomObject]@{ @@ -54,8 +52,6 @@ function Invoke-CIPPStandardAuditLog { } if ($Settings.remediate -eq $true) { - Write-Host 'Time to remediate' - $DehydratedTenant = (New-ExoRequest -tenantid $Tenant -cmdlet 'Get-OrganizationConfig' -Select IsDehydrated).IsDehydrated if ($DehydratedTenant -eq $true) { try { diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAuthMethodsSettings.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAuthMethodsSettings.ps1 index cd0332008211..c9f92324fb17 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAuthMethodsSettings.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAuthMethodsSettings.ps1 @@ -54,8 +54,6 @@ function Invoke-CIPPStandardAuthMethodsSettings { $ValidStates = @('default', 'enabled', 'disabled') if (($Settings.remediate -eq $true -or $Settings.alert -eq $true) -and ($ReportSuspiciousActivityState -notin $ValidStates -or $SystemCredentialState -notin $ValidStates)) { - Write-Host "ReportSuspiciousActivity: $($ReportSuspiciousActivityState)" - Write-Host "SystemCredential: $($SystemCredentialState)" Write-LogMessage -API 'Standards' -tenant $tenant -message 'AuthMethodsPolicy: Invalid state parameter set' -sev Error return } @@ -75,7 +73,6 @@ function Invoke-CIPPStandardAuthMethodsSettings { $StateSetCorrectly = $ReportSuspiciousActivityCorrect -and $SystemCredentialCorrect if ($Settings.remediate -eq $true) { - Write-Host 'Time to remediate' if ($StateSetCorrectly -eq $false) { try { $body = [PSCustomObject]@{ @@ -85,7 +82,6 @@ function Invoke-CIPPStandardAuthMethodsSettings { $body.reportSuspiciousActivitySettings.state = $ReportSuspiciousActivityState $body.systemCredentialPreferences.state = $SystemCredentialState - Write-Host "Body: $($body | ConvertTo-Json -Depth 10 -Compress)" # Update settings $null = New-GraphPostRequest -tenantid $tenant -Uri 'https://graph.microsoft.com/beta/policies/authenticationMethodsPolicy' -AsApp $true -Type PATCH -Body ($body | ConvertTo-Json -Depth 10 -Compress) -ContentType 'application/json' Write-LogMessage -API 'Standards' -tenant $tenant -message "Successfully configured authentication methods policy settings: Report Suspicious Activity ($ReportSuspiciousActivityState), System Credential Preferences ($SystemCredentialState)" -sev Info diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAutoAddProxy.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAutoAddProxy.ps1 index 43b3a943ecf3..2fb418ff552a 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAutoAddProxy.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAutoAddProxy.ps1 @@ -107,7 +107,6 @@ function Invoke-CIPPStandardAutoAddProxy { $BatchResults | ForEach-Object { if ($_.error) { $ErrorMessage = Get-CippException -Exception $_.error - Write-Host "Failed to apply new email policy to $($_.target) Error: $($ErrorMessage.NormalizedError)" Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to apply proxy address to $($_.error.target) Error: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAutoArchive.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAutoArchive.ps1 index 971c4bf55e56..e8c0b7d6be5a 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAutoArchive.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAutoArchive.ps1 @@ -34,7 +34,6 @@ function Invoke-CIPPStandardAutoArchive { $TestResult = Test-CIPPStandardLicense -StandardName 'AutoArchive' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." return $true } @@ -65,8 +64,6 @@ function Invoke-CIPPStandardAutoArchive { } if ($Settings.remediate -eq $true) { - Write-Host 'Time to remediate' - if ($CorrectState) { Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Auto-archiving threshold is already set to $CurrentState%." -Sev Info } else { diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAutoArchiveMailbox.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAutoArchiveMailbox.ps1 index ab6d73d2d135..ef649fcedfbb 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAutoArchiveMailbox.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAutoArchiveMailbox.ps1 @@ -34,7 +34,6 @@ function Invoke-CIPPStandardAutoArchiveMailbox { $TestResult = Test-CIPPStandardLicense -StandardName 'AutoArchiveMailbox' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." return $true } @@ -65,8 +64,6 @@ function Invoke-CIPPStandardAutoArchiveMailbox { } if ($Settings.remediate -eq $true) { - Write-Host 'Time to remediate' - if ($CorrectState) { Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Auto enable archive mailbox is already set to $StateValue." -Sev Info } else { diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAutoExpandArchive.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAutoExpandArchive.ps1 index 72b4552da9bb..40dc7014d49e 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAutoExpandArchive.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAutoExpandArchive.ps1 @@ -33,7 +33,6 @@ function Invoke-CIPPStandardAutoExpandArchive { $TestResult = Test-CIPPStandardLicense -StandardName 'AutoExpandArchive' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'AutoExpandArchive' @@ -54,8 +53,6 @@ function Invoke-CIPPStandardAutoExpandArchive { } if ($Settings.remediate -eq $true) { - Write-Host 'Time to remediate' - if ($CurrentState) { Write-LogMessage -API 'Standards' -tenant $tenant -message 'Auto Expanding Archive is already enabled.' -sev Info } else { diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAutopilotProfile.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAutopilotProfile.ps1 index 98cda9ca282b..508e56635b2e 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAutopilotProfile.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAutopilotProfile.ps1 @@ -45,7 +45,6 @@ function Invoke-CIPPStandardAutopilotProfile { # Get the current configuration if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. try { diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAutopilotStatusPage.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAutopilotStatusPage.ps1 index 17ac191280b5..1fcf83165723 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAutopilotStatusPage.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAutopilotStatusPage.ps1 @@ -43,7 +43,6 @@ function Invoke-CIPPStandardAutopilotStatusPage { # Get current Autopilot enrollment status page configuration if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. try { diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardBookings.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardBookings.ps1 index 608aadf0e58c..95cb110b2076 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardBookings.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardBookings.ps1 @@ -34,7 +34,6 @@ function Invoke-CIPPStandardBookings { $TestResult = Test-CIPPStandardLicense -StandardName 'Bookings' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'Bookings' @@ -72,7 +71,6 @@ function Invoke-CIPPStandardBookings { return } if ($Settings.remediate -eq $true) { - Write-Host 'Time to remediate' if ($StateIsCorrect -eq $false) { try { $null = New-ExoRequest -tenantid $Tenant -cmdlet 'Set-OrganizationConfig' -cmdParams @{ BookingsEnabled = $WantedState } -useSystemMailbox $true diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardCloudMessageRecall.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardCloudMessageRecall.ps1 index 48feb07b2c45..28b2adefcbe5 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardCloudMessageRecall.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardCloudMessageRecall.ps1 @@ -34,7 +34,6 @@ function Invoke-CIPPStandardCloudMessageRecall { $TestResult = Test-CIPPStandardLicense -StandardName 'CloudMessageRecall' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'CloudMessageRecall' @@ -74,7 +73,6 @@ function Invoke-CIPPStandardCloudMessageRecall { if ($Settings.remediate -eq $true) { - Write-Host 'Time to remediate' if ($StateIsCorrect -eq $false) { try { $null = New-ExoRequest -tenantid $Tenant -cmdlet 'Set-OrganizationConfig' -cmdParams @{ MessageRecallEnabled = $WantedState } -useSystemMailbox $true diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardConditionalAccessTemplate.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardConditionalAccessTemplate.ps1 index 95765f0dd839..02def34b7681 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardConditionalAccessTemplate.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardConditionalAccessTemplate.ps1 @@ -42,7 +42,6 @@ function Invoke-CIPPStandardConditionalAccessTemplate { $settings.TemplateList | ForEach-Object { Set-CIPPStandardsCompareField -FieldName "standards.ConditionalAccessTemplate.$($_.value)" -FieldValue 'This tenant does not have the required license for this standard.' -Tenant $Tenant } - Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardCustomBannedPasswordList.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardCustomBannedPasswordList.ps1 index 1eb6f3370fcd..74d257a6387a 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardCustomBannedPasswordList.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardCustomBannedPasswordList.ps1 @@ -31,7 +31,6 @@ function Invoke-CIPPStandardCustomBannedPasswordList { #> param($Tenant, $Settings) - Write-Host "All params received: $Tenant, $tenant, $($Settings | ConvertTo-Json -Depth 10 -Compress)" $PasswordRuleTemplateId = '5cf42378-d67d-4f36-ba46-e8b86229381d' # Parse and validate banned words from input $BannedWordsInput = $Settings.BannedWords @@ -77,10 +76,7 @@ function Invoke-CIPPStandardCustomBannedPasswordList { } if ($Settings.remediate -eq $true) { - Write-Host 'Time to remediate Custom Banned Password List' - if ($null -eq $ExistingSettings) { - Write-Host 'No existing Custom Banned Password List found, creating new one' # Create new directory setting with default values if it doesn't exist try { $Body = @{ @@ -121,7 +117,6 @@ function Invoke-CIPPStandardCustomBannedPasswordList { Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to create Custom Banned Password List: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage } } else { - Write-Host 'Existing Custom Banned Password List found, updating it' # Update existing directory setting try { # Get the current passwords and check if all the new words are already in the list @@ -131,10 +126,8 @@ function Invoke-CIPPStandardCustomBannedPasswordList { # Check if the new words are already in the list $NewBannedWords = $BannedWordsList | Where-Object { $CurrentBannedWords -notcontains $_ } if ($NewBannedWords.Count -eq 0 -and ($ExistingSettings.values | Where-Object { $_.name -eq 'EnableBannedPasswordCheck' }).value -eq 'True') { - Write-Host 'No new words to add' Write-LogMessage -API 'Standards' -tenant $Tenant -message "Custom Banned Password List is already configured with $($CurrentBannedWords.Count) words." -sev Info } else { - Write-Host "$($NewBannedWords.Count) new words to add" $AllBannedWords = [System.Collections.Generic.List[string]]::new() $NewBannedWords | ForEach-Object { $AllBannedWords.Add($_) } $CurrentBannedWords | ForEach-Object { $AllBannedWords.Add($_) } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDefaultPlatformRestrictions.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDefaultPlatformRestrictions.ps1 index 23945c2b628b..349cb5b4aea3 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDefaultPlatformRestrictions.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDefaultPlatformRestrictions.ps1 @@ -44,7 +44,6 @@ function Invoke-CIPPStandardDefaultPlatformRestrictions { $TestResult = Test-CIPPStandardLicense -StandardName 'DefaultPlatformRestrictions' -TenantFilter $Tenant -RequiredCapabilities @('INTUNE_A', 'MDM_Services', 'EMS', 'SCCM', 'MICROSOFTINTUNEPLAN1') if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDefaultSharingLink.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDefaultSharingLink.ps1 index d3397e85af50..684f768cd70a 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDefaultSharingLink.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDefaultSharingLink.ps1 @@ -41,7 +41,6 @@ function Invoke-CIPPStandardDefaultSharingLink { # Determine the desired sharing link type (default to Internal if not specified) if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. $DesiredSharingLinkType = $Settings.SharingLinkType.value ?? 'Internal' @@ -84,7 +83,6 @@ function Invoke-CIPPStandardDefaultSharingLink { # Check if the current state matches the desired configuration $StateIsCorrect = ($CurrentState.DefaultSharingLinkType -eq $DesiredSharingLinkTypeValue) -and ($CurrentState.DefaultLinkPermission -eq 1) - Write-Host "currentstate: $($CurrentState.DefaultSharingLinkType), $($CurrentState.DefaultLinkPermission). Desired: $DesiredSharingLinkTypeValue, 1" if ($Settings.remediate -eq $true) { if ($StateIsCorrect -eq $true) { Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Default sharing link settings are already configured correctly (Type: $DesiredSharingLinkType, Permission: View)" -Sev Info diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDelegateSentItems.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDelegateSentItems.ps1 index ac95dcd2b1fd..177e070d6024 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDelegateSentItems.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDelegateSentItems.ps1 @@ -34,7 +34,6 @@ function Invoke-CIPPStandardDelegateSentItems { $TestResult = Test-CIPPStandardLicense -StandardName 'DelegateSentItems' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. #$Rerun -Type Standard -Tenant $Tenant -API 'DelegateSentItems' -Settings $Settings @@ -62,10 +61,7 @@ function Invoke-CIPPStandardDelegateSentItems { state = 'Configured correctly' } - Write-Host "Mailboxes: $($Mailboxes.Count)" if ($Settings.remediate -eq $true) { - Write-Host 'Time to remediate' - if ($Mailboxes) { try { $Request = $Mailboxes | ForEach-Object { @@ -80,7 +76,6 @@ function Invoke-CIPPStandardDelegateSentItems { $BatchResults | ForEach-Object { if ($_.error) { $ErrorMessage = Get-CippException -Exception $_.error - Write-Host "Failed to apply Delegate Sent Items Style to $($_.target) Error: $($ErrorMessage.NormalizedError)" Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to apply Delegate Sent Items Style to $($_.error.target) Error: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeletedUserRentention.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeletedUserRentention.ps1 index ccb08c900fe3..8a6ed59c11f8 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeletedUserRentention.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeletedUserRentention.ps1 @@ -35,7 +35,6 @@ function Invoke-CIPPStandardDeletedUserRentention { ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'DeletedUserRetention' if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. @@ -78,8 +77,6 @@ function Invoke-CIPPStandardDeletedUserRentention { $StateSetCorrectly = if ($CurrentInfo.deletedUserPersonalSiteRetentionPeriodInDays -eq $WantedState) { $true } else { $false } if ($Settings.remediate -eq $true) { - Write-Host 'Time to remediate' - if ($StateSetCorrectly -eq $false) { try { $body = [PSCustomObject]@{ diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployContactTemplates.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployContactTemplates.ps1 index 067feb5c567d..43a541db360c 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployContactTemplates.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployContactTemplates.ps1 @@ -37,7 +37,6 @@ function Invoke-CIPPStandardDeployContactTemplates { $TestResult = Test-CIPPStandardLicense -StandardName 'DeployContactTemplates' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployMailContact.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployMailContact.ps1 index 8dc3df011a96..0bc606b7adbf 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployMailContact.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployMailContact.ps1 @@ -38,7 +38,6 @@ function Invoke-CIPPStandardDeployMailContact { $TestResult = Test-CIPPStandardLicense -StandardName 'DeployMailContact' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableAddShortcutsToOneDrive.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableAddShortcutsToOneDrive.ps1 index 5b43e27dbee0..b27abf52a5df 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableAddShortcutsToOneDrive.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableAddShortcutsToOneDrive.ps1 @@ -35,7 +35,6 @@ function Invoke-CIPPStandardDisableAddShortcutsToOneDrive { ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'DisableAddShortcutsToOneDrive' if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. @@ -77,8 +76,6 @@ function Invoke-CIPPStandardDisableAddShortcutsToOneDrive { } if ($Settings.remediate -eq $true) { - Write-Host 'Time to remediate' - if ($StateIsCorrect -eq $false) { try { $CurrentState | Set-CIPPSPOTenant -Properties @{DisableAddToOneDrive = $WantedState } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableAdditionalStorageProviders.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableAdditionalStorageProviders.ps1 index 5a83612fc3b9..42e28232c593 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableAdditionalStorageProviders.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableAdditionalStorageProviders.ps1 @@ -36,7 +36,6 @@ function Invoke-CIPPStandardDisableAdditionalStorageProviders { $TestResult = Test-CIPPStandardLicense -StandardName 'DisableAdditionalStorageProviders' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'DisableAdditionalStorageProviders' diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableBasicAuthSMTP.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableBasicAuthSMTP.ps1 index d648b2d8589b..f11d03b8818d 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableBasicAuthSMTP.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableBasicAuthSMTP.ps1 @@ -37,7 +37,6 @@ function Invoke-CIPPStandardDisableBasicAuthSMTP { $TestResult = Test-CIPPStandardLicense -StandardName 'DisableBasicAuthSMTP' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'DisableBasicAuthSMTP' @@ -54,8 +53,6 @@ function Invoke-CIPPStandardDisableBasicAuthSMTP { } if ($Settings.remediate -eq $true) { - Write-Host 'Time to remediate' - if ($CurrentInfo.SmtpClientAuthenticationDisabled -and $SMTPusers.Count -eq 0) { Write-LogMessage -API 'Standards' -tenant $tenant -message 'SMTP Basic Authentication for tenant and all users is already disabled' -sev Info } else { diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableExchangeOnlinePowerShell.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableExchangeOnlinePowerShell.ps1 index ff49eefefeef..6efd8ddc3677 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableExchangeOnlinePowerShell.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableExchangeOnlinePowerShell.ps1 @@ -37,7 +37,6 @@ function Invoke-CIPPStandardDisableExchangeOnlinePowerShell { $TestResult = Test-CIPPStandardLicense -StandardName 'DisableExchangeOnlinePowerShell' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'DisableExchangeOnlinePowerShell' @@ -72,7 +71,6 @@ function Invoke-CIPPStandardDisableExchangeOnlinePowerShell { $BatchResults | ForEach-Object { if ($_.error) { $ErrorMessage = Get-NormalizedError -Message $_.error - Write-Host "Failed to disable Exchange Online PowerShell for $($_.target). Error: $ErrorMessage" Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to disable Exchange Online PowerShell for $($_.target). Error: $ErrorMessage" -sev Error } else { $SuccessCount++ diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableExternalCalendarSharing.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableExternalCalendarSharing.ps1 index d8bf42551e92..69b36bf7eba0 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableExternalCalendarSharing.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableExternalCalendarSharing.ps1 @@ -36,7 +36,6 @@ function Invoke-CIPPStandardDisableExternalCalendarSharing { $TestResult = Test-CIPPStandardLicense -StandardName 'DisableExternalCalendarSharing' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableGuests.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableGuests.ps1 index de4789652b77..ad00f5293547 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableGuests.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableGuests.ps1 @@ -40,7 +40,6 @@ function Invoke-CIPPStandardDisableGuests { $settings.TemplateList | ForEach-Object { Set-CIPPStandardsCompareField -FieldName 'standards.DisableGuests' -FieldValue 'This tenant does not have the required license for this standard.' -Tenant $Tenant } - Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableM365GroupUsers.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableM365GroupUsers.ps1 index 4919d502721f..79075e249008 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableM365GroupUsers.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableM365GroupUsers.ps1 @@ -34,7 +34,6 @@ function Invoke-CIPPStandardDisableM365GroupUsers { $TestResult = Test-CIPPStandardLicense -StandardName 'DisableM365GroupUsers' -TenantFilter $Tenant -RequiredCapabilities @('SHAREPOINTWAC', 'SHAREPOINTSTANDARD', 'SHAREPOINTENTERPRISE', 'SHAREPOINTENTERPRISE_EDU', 'ONEDRIVE_BASIC', 'ONEDRIVE_ENTERPRISE') if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableOutlookAddins.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableOutlookAddins.ps1 index 42c955f5186f..05cf2ab4403d 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableOutlookAddins.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableOutlookAddins.ps1 @@ -38,7 +38,6 @@ function Invoke-CIPPStandardDisableOutlookAddins { $TestResult = Test-CIPPStandardLicense -StandardName 'DisableOutlookAddins' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableReshare.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableReshare.ps1 index 3e29a5d81a3b..6f2fd8f23a03 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableReshare.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableReshare.ps1 @@ -38,7 +38,6 @@ function Invoke-CIPPStandardDisableReshare { $TestResult = Test-CIPPStandardLicense -StandardName 'DisableReshare' -TenantFilter $Tenant -RequiredCapabilities @('SHAREPOINTWAC', 'SHAREPOINTSTANDARD', 'SHAREPOINTENTERPRISE', 'SHAREPOINTENTERPRISE_EDU', 'ONEDRIVE_BASIC', 'ONEDRIVE_ENTERPRISE') if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableResourceMailbox.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableResourceMailbox.ps1 index ca70a1330275..d4d20c070e3a 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableResourceMailbox.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableResourceMailbox.ps1 @@ -36,7 +36,6 @@ function Invoke-CIPPStandardDisableResourceMailbox { $TestResult = Test-CIPPStandardLicense -StandardName 'DisableResourceMailbox' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. @@ -53,11 +52,8 @@ function Invoke-CIPPStandardDisableResourceMailbox { } if ($Settings.remediate -eq $true) { - Write-Host 'Time to remediate' - if ($ResourceMailboxList) { - Write-Host "Resource Mailboxes to disable: $($ResourceMailboxList.Count)" $ResourceMailboxList | ForEach-Object { try { New-GraphPOSTRequest -uri "https://graph.microsoft.com/v1.0/users/$($_.ExternalDirectoryObjectId)" -type PATCH -body '{"accountEnabled":"false"}' -tenantid $Tenant diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSharePointLegacyAuth.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSharePointLegacyAuth.ps1 index 88b113d8d554..4c09192f756f 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSharePointLegacyAuth.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSharePointLegacyAuth.ps1 @@ -40,7 +40,6 @@ function Invoke-CIPPStandardDisableSharePointLegacyAuth { $TestResult = Test-CIPPStandardLicense -StandardName 'DisableSharePointLegacyAuth' -TenantFilter $Tenant -RequiredCapabilities @('SHAREPOINTWAC', 'SHAREPOINTSTANDARD', 'SHAREPOINTENTERPRISE', 'SHAREPOINTENTERPRISE_EDU', 'ONEDRIVE_BASIC', 'ONEDRIVE_ENTERPRISE') if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableTNEF.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableTNEF.ps1 index e82a30d9de1a..7656fb4ebfa1 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableTNEF.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableTNEF.ps1 @@ -34,7 +34,6 @@ function Invoke-CIPPStandardDisableTNEF { $TestResult = Test-CIPPStandardLicense -StandardName 'DisableTNEF' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. @@ -47,8 +46,6 @@ function Invoke-CIPPStandardDisableTNEF { } if ($Settings.remediate -eq $true) { - Write-Host 'Time to remediate' - if ($CurrentState.TNEFEnabled -ne $false) { try { New-ExoRequest -tenantid $Tenant -cmdlet 'Set-RemoteDomain' -cmdParams @{Identity = 'Default'; TNEFEnabled = $false } -useSystemMailbox $true diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableTenantCreation.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableTenantCreation.ps1 index c68199c54554..688e76667111 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableTenantCreation.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableTenantCreation.ps1 @@ -45,7 +45,6 @@ function Invoke-CIPPStandardDisableTenantCreation { $StateIsCorrect = ($CurrentState.defaultUserRolePermissions.allowedToCreateTenants -eq $false) if ($Settings.remediate -eq $true) { - Write-Host "Time to remediate DisableTenantCreation standard for tenant $Tenant" if ($StateIsCorrect -eq $true) { Write-LogMessage -API 'Standards' -tenant $Tenant -message 'Users are already disabled from creating tenants.' -sev Info } else { diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableUserSiteCreate.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableUserSiteCreate.ps1 index 2eeda69ca0f1..2d0ab46cd39d 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableUserSiteCreate.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableUserSiteCreate.ps1 @@ -33,7 +33,6 @@ function Invoke-CIPPStandardDisableUserSiteCreate { $TestResult = Test-CIPPStandardLicense -StandardName 'DisableUserSiteCreate' -TenantFilter $Tenant -RequiredCapabilities @('SHAREPOINTWAC', 'SHAREPOINTSTANDARD', 'SHAREPOINTENTERPRISE', 'SHAREPOINTENTERPRISE_EDU', 'ONEDRIVE_BASIC', 'ONEDRIVE_ENTERPRISE') if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableViva.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableViva.ps1 index 4ae2623e8e1b..e32c4b4a2d5c 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableViva.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableViva.ps1 @@ -41,8 +41,6 @@ function Invoke-CIPPStandardDisableViva { } if ($Settings.remediate -eq $true) { - Write-Host 'Time to remediate' - if ($CurrentSetting.isEnabledInOrganization -eq $false) { Write-LogMessage -API 'Standards' -tenant $Tenant -message 'Viva is already disabled.' -sev Info } else { diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEXODirectSend.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEXODirectSend.ps1 index 81bfe45e8006..1fa581d7c933 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEXODirectSend.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEXODirectSend.ps1 @@ -54,8 +54,6 @@ function Invoke-CIPPStandardEXODirectSend { # Remediate if needed if ($Settings.remediate -eq $true) { - - Write-Host 'Time to remediate' if ($StateIsCorrect -eq $true) { Write-LogMessage -API 'Standards' -tenant $Tenant -message "Direct Send is already set to $DesiredStateName." -sev Info } else { diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEXODisableAutoForwarding.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEXODisableAutoForwarding.ps1 index f39c7de785f5..8c636b8201ae 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEXODisableAutoForwarding.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEXODisableAutoForwarding.ps1 @@ -40,7 +40,6 @@ function Invoke-CIPPStandardEXODisableAutoForwarding { $TestResult = Test-CIPPStandardLicense -StandardName 'EXODisableAutoForwarding' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEXOOutboundSpamLimits.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEXOOutboundSpamLimits.ps1 index 537bb0133407..6c7fa531bbc0 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEXOOutboundSpamLimits.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEXOOutboundSpamLimits.ps1 @@ -40,7 +40,6 @@ function Invoke-CIPPStandardEXOOutboundSpamLimits { $TestResult = Test-CIPPStandardLicense -StandardName 'EXOOutboundSpamLimits' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. @@ -87,7 +86,6 @@ function Invoke-CIPPStandardEXOOutboundSpamLimits { # Remediation if ($Settings.remediate -eq $true) { - Write-Host 'Time to remediate' if ($StateIsCorrect -eq $false) { try { $null = New-ExoRequest -tenantid $Tenant -cmdlet 'Set-HostedOutboundSpamFilterPolicy' -cmdParams @{ diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableCustomerLockbox.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableCustomerLockbox.ps1 index f790392524ba..1446e6695b9f 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableCustomerLockbox.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableCustomerLockbox.ps1 @@ -36,7 +36,6 @@ function Invoke-CIPPStandardEnableCustomerLockbox { $TestResult = Test-CIPPStandardLicense -StandardName 'EnableCustomerLockbox' -TenantFilter $Tenant -RequiredCapabilities @('CustomerLockbox') if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. @@ -49,7 +48,6 @@ function Invoke-CIPPStandardEnableCustomerLockbox { } if ($Settings.remediate -eq $true) { - Write-Host 'Time to remediate' try { if ($CustomerLockboxStatus) { diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableLitigationHold.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableLitigationHold.ps1 index 0b7f49ac521e..66bdbf9d537c 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableLitigationHold.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableLitigationHold.ps1 @@ -34,7 +34,6 @@ function Invoke-CIPPStandardEnableLitigationHold { $TestResult = Test-CIPPStandardLicense -StandardName 'EnableLitigationHold' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. @@ -70,7 +69,6 @@ function Invoke-CIPPStandardEnableLitigationHold { $BatchResults | ForEach-Object { if ($_.error) { $ErrorMessage = Get-NormalizedError -Message $_.error - Write-Host "Failed to Enable Litigation Hold for $($_.Target). Error: $ErrorMessage" Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to Enable Litigation Hold for $($_.Target). Error: $ErrorMessage" -sev Error } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableMailTips.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableMailTips.ps1 index c76f81dd6e3f..bfa1cc10f477 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableMailTips.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableMailTips.ps1 @@ -38,7 +38,6 @@ function Invoke-CIPPStandardEnableMailTips { $TestResult = Test-CIPPStandardLicense -StandardName 'EnableMailTips' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableMailboxAuditing.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableMailboxAuditing.ps1 index 5bfab677b1d9..6793ec94c69c 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableMailboxAuditing.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableMailboxAuditing.ps1 @@ -42,7 +42,6 @@ function Invoke-CIPPStandardEnableMailboxAuditing { $TestResult = Test-CIPPStandardLicense -StandardName 'EnableMailboxAuditing' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. @@ -110,7 +109,6 @@ function Invoke-CIPPStandardEnableMailboxAuditing { $BatchResults | ForEach-Object { if ($_.error) { $ErrorMessage = Get-NormalizedError -Message $_.error - Write-Host "Failed to disable mailbox audit bypass for $($_.target). Error: $ErrorMessage" Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to disable mailbox audit bypass for $($_.target). Error: $ErrorMessage" -sev Error } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableNamePronunciation.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableNamePronunciation.ps1 index 21d676aa2253..eeee70c6fd6f 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableNamePronunciation.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableNamePronunciation.ps1 @@ -38,11 +38,8 @@ function Invoke-CIPPStandardEnableNamePronunciation { Write-LogMessage -API 'Standards' -tenant $Tenant -message "Could not get CurrentState for Name Pronunciation. Error: $($ErrorMessage.NormalizedError)" -sev Error return } - Write-Host $CurrentState if ($Settings.remediate -eq $true) { - Write-Host 'Time to remediate' - if ($CurrentState.isEnabledInOrganization -eq $true) { Write-LogMessage -API 'Standards' -tenant $tenant -message 'Name Pronunciation is already enabled.' -sev Info } else { diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableOnlineArchiving.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableOnlineArchiving.ps1 index b69034289ed8..501493c84eb6 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableOnlineArchiving.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableOnlineArchiving.ps1 @@ -35,14 +35,12 @@ function Invoke-CIPPStandardEnableOnlineArchiving { $TestResult = Test-CIPPStandardLicense -StandardName 'EnableOnlineArchiving' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. $MailboxPlans = @( 'ExchangeOnline', 'ExchangeOnlineEnterprise' ) $MailboxesNoArchive = $MailboxPlans | ForEach-Object { New-ExoRequest -tenantid $Tenant -cmdlet 'Get-Mailbox' -cmdParams @{ MailboxPlan = $_; Filter = 'ArchiveGuid -Eq "00000000-0000-0000-0000-000000000000" -AND RecipientTypeDetails -Eq "UserMailbox"' } - Write-Host "Getting mailboxes without Online Archiving for plan $_" } if ($Settings.remediate -eq $true) { @@ -64,7 +62,6 @@ function Invoke-CIPPStandardEnableOnlineArchiving { $BatchResults | ForEach-Object { if ($_.error) { $ErrorMessage = Get-NormalizedError -Message $_.error - Write-Host "Failed to Enable Online Archiving for $($_.Target). Error: $ErrorMessage" Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to Enable Online Archiving for $($_.Target). Error: $ErrorMessage" -sev Error } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnablePronouns.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnablePronouns.ps1 index 1288147b8448..e57dc64edd9f 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnablePronouns.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnablePronouns.ps1 @@ -41,8 +41,6 @@ function Invoke-CIPPStandardEnablePronouns { } if ($Settings.remediate -eq $true) { - Write-Host 'Time to remediate' - if ($CurrentState.isEnabledInOrganization -eq $true) { Write-LogMessage -API 'Standards' -tenant $tenant -message 'Pronouns are already enabled.' -sev Info } else { diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnrollmentWindowsHelloForBusinessConfiguration.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnrollmentWindowsHelloForBusinessConfiguration.ps1 index 5115ee7463b8..80643e0d9937 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnrollmentWindowsHelloForBusinessConfiguration.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnrollmentWindowsHelloForBusinessConfiguration.ps1 @@ -45,7 +45,6 @@ function Invoke-CIPPStandardEnrollmentWindowsHelloForBusinessConfiguration { $TestResult = Test-CIPPStandardLicense -StandardName 'EnrollmentWindowsHelloForBusinessConfiguration' -TenantFilter $Tenant -RequiredCapabilities @('INTUNE_A', 'MDM_Services', 'EMS', 'SCCM', 'MICROSOFTINTUNEPLAN1') if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardExchangeConnectorTemplate.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardExchangeConnectorTemplate.ps1 index e40f07aabcc4..53d5b0db49ca 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardExchangeConnectorTemplate.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardExchangeConnectorTemplate.ps1 @@ -31,7 +31,6 @@ function Invoke-CIPPStandardExchangeConnectorTemplate { $TestResult = Test-CIPPStandardLicense -StandardName 'ExConnector' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardExcludedfileExt.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardExcludedfileExt.ps1 index 8271da894f31..5482feac8fa9 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardExcludedfileExt.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardExcludedfileExt.ps1 @@ -34,7 +34,6 @@ function Invoke-CIPPStandardExcludedfileExt { $TestResult = Test-CIPPStandardLicense -StandardName 'ExcludedfileExt' -TenantFilter $Tenant -RequiredCapabilities @('SHAREPOINTWAC', 'SHAREPOINTSTANDARD', 'SHAREPOINTENTERPRISE', 'SHAREPOINTENTERPRISE_EDU', 'ONEDRIVE_BASIC', 'ONEDRIVE_ENTERPRISE') if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. @@ -56,16 +55,11 @@ function Invoke-CIPPStandardExcludedfileExt { } } - Write-Host "MissingExclusions: $($MissingExclusions)" - - if ($Settings.remediate -eq $true) { # If the number of extensions in the settings does not match the number of extensions in the current settings, we need to update the settings $MissingExclusions = if ($Exts.Count -ne $CurrentInfo.excludedFileExtensionsForSyncApp.Count) { $true } else { $MissingExclusions } if ($MissingExclusions) { - Write-Host "CurrentInfo.excludedFileExtensionsForSyncApp: $($CurrentInfo.excludedFileExtensionsForSyncApp)" - Write-Host "Exts: $($Exts)" try { $body = ConvertTo-Json -InputObject @{ excludedFileExtensionsForSyncApp = @($Exts) } $null = New-GraphPostRequest -tenantid $tenant -Uri 'https://graph.microsoft.com/beta/admin/sharepoint/settings' -AsApp $true -Type patch -Body $body -ContentType 'application/json' diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardExternalMFATrusted.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardExternalMFATrusted.ps1 index f4d03b8edaeb..858e214b9bdd 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardExternalMFATrusted.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardExternalMFATrusted.ps1 @@ -52,8 +52,6 @@ function Invoke-CIPPStandardExternalMFATrusted { } if ($Settings.remediate -eq $true) { - - Write-Host 'Remediate External MFA Trusted' if ($ExternalMFATrusted.inboundTrust.isMfaAccepted -eq $WantedState ) { Write-LogMessage -API 'Standards' -tenant $Tenant -message "External MFA Trusted is already $StateMessage." -sev Info } else { diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardFocusedInbox.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardFocusedInbox.ps1 index 9c60877dd511..1eaeb003af7a 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardFocusedInbox.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardFocusedInbox.ps1 @@ -34,7 +34,6 @@ function Invoke-CIPPStandardFocusedInbox { $TestResult = Test-CIPPStandardLicense -StandardName 'FocusedInbox' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. @@ -59,8 +58,6 @@ function Invoke-CIPPStandardFocusedInbox { $StateIsCorrect = if ($CurrentState -eq $WantedState) { $true } else { $false } if ($Settings.remediate -eq $true) { - Write-Host 'Time to remediate' - if ($StateIsCorrect -eq $true) { Write-LogMessage -API 'Standards' -tenant $Tenant -message "Focused Inbox is already set to $state." -sev Info } else { diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardFormsPhishingProtection.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardFormsPhishingProtection.ps1 index 957c2b2d7059..8531ad4f56ee 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardFormsPhishingProtection.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardFormsPhishingProtection.ps1 @@ -48,8 +48,6 @@ function Invoke-CIPPStandardFormsPhishingProtection { } if ($Settings.remediate -eq $true) { - Write-Host 'Time to remediate Forms phishing protection' - # Check if phishing protection is already enabled if ($CurrentState -eq $true) { Write-LogMessage -API 'Standards' -tenant $Tenant -message 'Forms internal phishing protection is already enabled.' -sev Info diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardGlobalQuarantineNotifications.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardGlobalQuarantineNotifications.ps1 index e7a3b9f246ea..549409230064 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardGlobalQuarantineNotifications.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardGlobalQuarantineNotifications.ps1 @@ -31,7 +31,6 @@ function Invoke-CIPPStandardGlobalQuarantineNotifications { $TestResult = Test-CIPPStandardLicense -StandardName 'GlobalQuarantineNotifications' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. @@ -68,8 +67,6 @@ function Invoke-CIPPStandardGlobalQuarantineNotifications { } if ($Settings.remediate -eq $true) { - Write-Host 'Time to remediate' - if ($CurrentState.EndUserSpamNotificationFrequency -eq $WantedState) { Write-LogMessage -API 'Standards' -tenant $Tenant -message "Global Quarantine Notifications are already set to the desired value of $WantedState" -sev Info } else { diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardGroupTemplate.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardGroupTemplate.ps1 index a7539fe40fd1..151258c2b087 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardGroupTemplate.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardGroupTemplate.ps1 @@ -48,8 +48,6 @@ function Invoke-CIPPStandardGroupTemplate { if ($Settings.remediate -eq $true) { #Because the list name changed from TemplateList to groupTemplate by someone :@, we'll need to set it back to TemplateList - - Write-Host "Settings: $($Settings.TemplateList | ConvertTo-Json)" foreach ($Template in $GroupTemplates) { Write-Information "Processing template: $($Template.displayName)" try { diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardIntuneComplianceSettings.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardIntuneComplianceSettings.ps1 index 06ab67ebb055..8d785d6de7d7 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardIntuneComplianceSettings.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardIntuneComplianceSettings.ps1 @@ -35,7 +35,6 @@ function Invoke-CIPPStandardIntuneComplianceSettings { $TestResult = Test-CIPPStandardLicense -StandardName 'IntuneComplianceSettings' -TenantFilter $Tenant -RequiredCapabilities @('INTUNE_A', 'MDM_Services', 'EMS', 'SCCM', 'MICROSOFTINTUNEPLAN1') if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardIntuneTemplate.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardIntuneTemplate.ps1 index f3d7ccca4a0c..444fc3891dcf 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardIntuneTemplate.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardIntuneTemplate.ps1 @@ -43,55 +43,40 @@ function Invoke-CIPPStandardIntuneTemplate { $settings.TemplateList | ForEach-Object { Set-CIPPStandardsCompareField -FieldName "standards.IntuneTemplate.$($_.value)" -FieldValue 'This tenant does not have the required license for this standard.' -Tenant $Tenant } - Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. $Table = Get-CippTable -tablename 'templates' $Filter = "PartitionKey eq 'IntuneTemplate'" $Request = @{body = $null } - Write-Host "IntuneTemplate: Starting process. Settings are: $($Settings | ConvertTo-Json -Compress)" $CompareList = foreach ($Template in $Settings) { - Write-Host "IntuneTemplate: $($Template.TemplateList.value) - Trying to find template" $Request.body = (Get-CIPPAzDataTableEntity @Table -Filter $Filter | Where-Object -Property RowKey -Like "$($Template.TemplateList.value)*").JSON | ConvertFrom-Json -ErrorAction SilentlyContinue if ($null -eq $Request.body) { Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to find template $($Template.TemplateList.value). Has this Intune Template been deleted?" -sev 'Error' continue } - Write-Host "IntuneTemplate: $($Template.TemplateList.value) - Got template." $displayname = $request.body.Displayname $description = $request.body.Description $RawJSON = $Request.body.RawJSON try { - Write-Host "IntuneTemplate: $($Template.TemplateList.value) - Grabbing existing Policy" $ExistingPolicy = Get-CIPPIntunePolicy -tenantFilter $Tenant -DisplayName $displayname -TemplateType $Request.body.Type } catch { - Write-Host "IntuneTemplate: $($Template.TemplateList.value) - Failed to get existing." } if ($ExistingPolicy) { try { - Write-Host "IntuneTemplate: $($Template.TemplateList.value) - Found existing policy." $RawJSON = Get-CIPPTextReplacement -Text $RawJSON -TenantFilter $Tenant - Write-Host "IntuneTemplate: $($Template.TemplateList.value) - Grabbing JSON existing." $JSONExistingPolicy = $ExistingPolicy.cippconfiguration | ConvertFrom-Json - Write-Host "IntuneTemplate: $($Template.TemplateList.value) - Got existing JSON. Converting RawJSON to Template" $JSONTemplate = $RawJSON | ConvertFrom-Json - Write-Host "IntuneTemplate: $($Template.TemplateList.value) - Converted RawJSON to Template." - Write-Host "IntuneTemplate: $($Template.TemplateList.value) - Comparing JSON." $Compare = Compare-CIPPIntuneObject -ReferenceObject $JSONTemplate -DifferenceObject $JSONExistingPolicy -compareType $Request.body.Type -ErrorAction SilentlyContinue } catch { - Write-Host "The compare failed. The error was: $($_.Exception.Message)" } - Write-Host "IntuneTemplate: $($Template.TemplateList.value) - Compared JSON: $($Compare | ConvertTo-Json -Compress)" } else { - Write-Host "IntuneTemplate: $($Template.TemplateList.value) - No existing policy found." $compare = [pscustomobject]@{ MatchFailed = $true Difference = 'This policy does not exist in Intune.' } } if ($Compare) { - Write-Host "IntuneTemplate: $($Template.TemplateList.value) - Compare found differences." [PSCustomObject]@{ MatchFailed = $true displayname = $displayname @@ -111,7 +96,6 @@ function Invoke-CIPPStandardIntuneTemplate { assignmentFilterType = $Template.assignmentFilterType } } else { - Write-Host "IntuneTemplate: $($Template.TemplateList.value) - No differences found." [PSCustomObject]@{ MatchFailed = $false displayname = $displayname @@ -134,9 +118,7 @@ function Invoke-CIPPStandardIntuneTemplate { } if ($true -in $Settings.remediate) { - Write-Host 'starting template deploy' foreach ($TemplateFile in $CompareList | Where-Object -Property remediate -EQ $true) { - Write-Host "working on template deploy: $($TemplateFile.displayname)" try { $TemplateFile.customGroup ? ($TemplateFile.AssignTo = $TemplateFile.customGroup) : $null @@ -167,7 +149,6 @@ function Invoke-CIPPStandardIntuneTemplate { if ($true -in $Settings.alert) { foreach ($Template in $CompareList | Where-Object -Property alert -EQ $true) { - Write-Host "working on template alert: $($Template.displayname)" $AlertObj = $Template | Select-Object -Property displayname, description, compare, assignTo, excludeGroup, existingPolicyId if ($Template.compare) { Write-StandardsAlert -message "Template $($Template.displayname) does not match the expected configuration." -object $AlertObj -tenant $Tenant -standardName 'IntuneTemplate' -standardId $Settings.templateId @@ -185,7 +166,6 @@ function Invoke-CIPPStandardIntuneTemplate { if ($true -in $Settings.report) { foreach ($Template in $CompareList | Where-Object { $_.report -eq $true -or $_.remediate -eq $true }) { - Write-Host "working on template report: $($Template.displayname)" $id = $Template.templateId $CurrentValue = @{ diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardIntuneWindowsDiagnostic.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardIntuneWindowsDiagnostic.ps1 index aaa98a19957a..edea119a83d2 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardIntuneWindowsDiagnostic.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardIntuneWindowsDiagnostic.ps1 @@ -36,13 +36,9 @@ Function Invoke-CIPPStandardIntuneWindowsDiagnostic { $TestResult = Test-CIPPStandardLicense -StandardName 'IntuneWindowsDiagnostic' -TenantFilter $Tenant -RequiredCapabilities @('INTUNE_A', 'MDM_Services', 'EMS', 'SCCM', 'MICROSOFTINTUNEPLAN1') if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." return $true } - write-host $Settings - write-host ($settings | ConvertTo-Json -Depth 10) - # Example diagnostic logic for Intune Windows devices try { diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMDMEnrollmentDuringRegistration.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMDMEnrollmentDuringRegistration.ps1 index 250aaa273045..373be8b68738 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMDMEnrollmentDuringRegistration.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMDMEnrollmentDuringRegistration.ps1 @@ -34,7 +34,6 @@ $TestResult = Test-CIPPStandardLicense -StandardName 'MDMEnrollmentDuringRegistration' -TenantFilter $Tenant -RequiredCapabilities @('INTUNE_A', 'MDM_Services', 'EMS', 'SCCM', 'MICROSOFTINTUNEPLAN1') if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." return $true } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMDMScope.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMDMScope.ps1 index e01b2616abe1..7a02236ff7e9 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMDMScope.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMDMScope.ps1 @@ -35,7 +35,6 @@ function Invoke-CIPPStandardMDMScope { $TestResult = Test-CIPPStandardLicense -StandardName 'MDMScope' -TenantFilter $Tenant -RequiredCapabilities @('INTUNE_A', 'MDM_Services', 'EMS', 'SCCM', 'MICROSOFTINTUNEPLAN1') if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMailContacts.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMailContacts.ps1 index d45e345f556c..e804e605ce61 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMailContacts.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMailContacts.ps1 @@ -63,7 +63,6 @@ function Invoke-CIPPStandardMailContacts { { $Contacts.TechContact } { $body | Add-Member -NotePropertyName technicalNotificationMails -NotePropertyValue @($Contacts.TechContact) -ErrorAction SilentlyContinue } { $Contacts.GeneralContact } { $body | Add-Member -NotePropertyName privacyProfile -NotePropertyValue @{contactEmail = $Contacts.GeneralContact } } } - Write-Host (ConvertTo-Json -InputObject $body) New-GraphPostRequest -tenantid $tenant -Uri "https://graph.microsoft.com/v1.0/organization/$($TenantID.id)" -asApp $true -Type patch -Body (ConvertTo-Json -InputObject $body) -ContentType 'application/json' Write-LogMessage -API 'Standards' -tenant $tenant -message 'Contact emails set.' -sev Info } catch { diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMailboxRecipientLimits.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMailboxRecipientLimits.ps1 index a796462e2881..c549c568c054 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMailboxRecipientLimits.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMailboxRecipientLimits.ps1 @@ -35,7 +35,6 @@ function Invoke-CIPPStandardMailboxRecipientLimits { $TestResult = Test-CIPPStandardLicense -StandardName 'MailboxRecipientLimits' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMalwareFilterPolicy.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMalwareFilterPolicy.ps1 index 640db0445b43..1c91a09eca28 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMalwareFilterPolicy.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMalwareFilterPolicy.ps1 @@ -46,7 +46,6 @@ function Invoke-CIPPStandardMalwareFilterPolicy { $TestResult = Test-CIPPStandardLicense -StandardName 'MalwareFilterPolicy' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMessageExpiration.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMessageExpiration.ps1 index 489366c54f0a..07dc9be99dc0 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMessageExpiration.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMessageExpiration.ps1 @@ -31,7 +31,6 @@ function Invoke-CIPPStandardMessageExpiration { $TestResult = Test-CIPPStandardLicense -StandardName 'MessageExpiration' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. @@ -44,7 +43,6 @@ function Invoke-CIPPStandardMessageExpiration { } if ($Settings.remediate -eq $true) { - Write-Host 'Time to remediate' if ($MessageExpiration -ne '12:00:00') { try { New-ExoRequest -tenantid $Tenant -cmdlet 'Set-TransportConfig' -cmdParams @{MessageExpiration = '12:00:00' } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardNudgeMFA.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardNudgeMFA.ps1 index 169ba51d7586..93b3fe130970 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardNudgeMFA.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardNudgeMFA.ps1 @@ -32,7 +32,7 @@ function Invoke-CIPPStandardNudgeMFA { #> param($Tenant, $Settings) - Write-Host "NudgeMFA: $($Settings | ConvertTo-Json -Compress)" + # Get state value using null-coalescing operator $State = $Settings.state.value ?? $Settings.state diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardOWAAttachmentRestrictions.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardOWAAttachmentRestrictions.ps1 index 614df8aca6fe..9a2f38181609 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardOWAAttachmentRestrictions.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardOWAAttachmentRestrictions.ps1 @@ -39,7 +39,6 @@ function Invoke-CIPPStandardOWAAttachmentRestrictions { $TestResult = Test-CIPPStandardLicense -StandardName 'OWAAttachmentRestrictions' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardOauthConsent.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardOauthConsent.ps1 index adc90a5a342d..bf86be5f502e 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardOauthConsent.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardOauthConsent.ps1 @@ -69,7 +69,6 @@ function Invoke-CIPPStandardOauthConsent { foreach ($AllowedApp in $AllowedAppIdsForTenant) { if ($AllowedApp -and ($AllowedApp -notin $ExistingAppIds)) { - Write-Host "Adding missing approved app: $AllowedApp" New-GraphPostRequest -tenantid $tenant -Uri 'https://graph.microsoft.com/beta/policies/permissionGrantPolicies/cipp-consent-policy/includes' -Type POST -Body ('{"permissionType": "delegated","clientApplicationIds": ["' + $AllowedApp + '"]}') -ContentType 'application/json' New-GraphPostRequest -tenantid $tenant -Uri 'https://graph.microsoft.com/beta/policies/permissionGrantPolicies/cipp-consent-policy/includes' -Type POST -Body ('{ "permissionType": "Application", "clientApplicationIds": ["' + $AllowedApp + '"] }') -ContentType 'application/json' } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardOutBoundSpamAlert.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardOutBoundSpamAlert.ps1 index 746cb2632c46..0ae5d1e92eab 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardOutBoundSpamAlert.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardOutBoundSpamAlert.ps1 @@ -34,7 +34,6 @@ function Invoke-CIPPStandardOutBoundSpamAlert { $TestResult = Test-CIPPStandardLicense -StandardName 'OutBoundSpamAlert' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPWcompanionAppAllowedState.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPWcompanionAppAllowedState.ps1 index 474de9cf7e44..656e69bd5367 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPWcompanionAppAllowedState.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPWcompanionAppAllowedState.ps1 @@ -54,8 +54,6 @@ function Invoke-CIPPStandardPWcompanionAppAllowedState { } If ($Settings.remediate -eq $true) { - Write-Host "Remediating PWcompanionAppAllowedState for tenant $Tenant to $WantedState" - if ($AuthStateCorrect -eq $true) { Write-LogMessage -API 'Standards' -tenant $Tenant -message "companionAppAllowedState is already set to the desired state of $WantedState." -sev Info } else { diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPasswordExpireDisabled.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPasswordExpireDisabled.ps1 index 403f80236b2e..f207a6b69557 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPasswordExpireDisabled.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPasswordExpireDisabled.ps1 @@ -53,7 +53,6 @@ function Invoke-CIPPStandardPasswordExpireDisabled { try { if ( $null -eq $_.passwordNotificationWindowInDays ) { $Body = '{"passwordValidityPeriodInDays": 2147483647, "passwordNotificationWindowInDays": 14 }' - Write-Host "PasswordNotificationWindowInDays is null for $($_.id). Setting to the default of 14 days." } else { $Body = '{"passwordValidityPeriodInDays": 2147483647 }' } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPhishProtection.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPhishProtection.ps1 index fdf203641250..56247118b1f5 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPhishProtection.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPhishProtection.ps1 @@ -74,11 +74,9 @@ function Invoke-CIPPStandardPhishProtection { } } if ($currentBody -like "*$CSS*") { - Write-Host 'Logon Screen Phishing Protection system already active' Write-LogMessage -API 'Standards' -tenant $tenant -message 'Logon Screen Phishing Protection system already active' -sev Info } else { $currentBody = $currentBody + $CSS - Write-Host 'Creating Logon Screen Phishing Protection System' New-GraphPostRequest -tenantid $tenant -Uri "https://graph.microsoft.com/beta/organization/$($TenantId.customerId)/branding/localizations/0/customCSS" -ContentType 'text/css' -asApp $true -Type PUT -Body $currentBody Write-LogMessage -API 'Standards' -tenant $tenant -message 'Enabled Logon Screen Phishing Protection system' -sev Info diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPhishSimSpoofIntelligence.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPhishSimSpoofIntelligence.ps1 index 8374e9803f06..e2de19d4fd2e 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPhishSimSpoofIntelligence.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPhishSimSpoofIntelligence.ps1 @@ -33,7 +33,6 @@ function Invoke-CIPPStandardPhishSimSpoofIntelligence { $TestResult = Test-CIPPStandardLicense -StandardName 'PhishSimSpoofIntelligence' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. # Fetch current Phishing Simulations Spoof Intelligence domains and ensure it is correctly configured @@ -71,7 +70,6 @@ function Invoke-CIPPStandardPhishSimSpoofIntelligence { if ($Settings.RemoveExtraDomains -eq $true) { # Prepare removal requests if ($RemoveDomain.Count -gt 0) { - Write-Host "Removing $($RemoveDomain.Count) domains from Spoof Intelligence" $BulkRequests.Add(@{ CmdletInput = @{ CmdletName = 'Remove-TenantAllowBlockListSpoofItems' diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPhishingSimulations.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPhishingSimulations.ps1 index e5d64514d13c..978cd750c317 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPhishingSimulations.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPhishingSimulations.ps1 @@ -35,7 +35,6 @@ function Invoke-CIPPStandardPhishingSimulations { $TestResult = Test-CIPPStandardLicense -StandardName 'PhishingSimulations' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. $PolicyName = 'CIPPPhishSim' diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardProfilePhotos.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardProfilePhotos.ps1 index c802bc152a03..a7b462a40fdb 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardProfilePhotos.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardProfilePhotos.ps1 @@ -34,7 +34,6 @@ function Invoke-CIPPStandardProfilePhotos { $TestResult = Test-CIPPStandardLicense -StandardName 'ProfilePhotos' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. @@ -75,20 +74,15 @@ function Invoke-CIPPStandardProfilePhotos { $CurrentStatesCorrect = $GraphStateCorrect -eq $true -and $OWAStateCorrect -eq $true if ($Settings.remediate -eq $true) { - Write-Host 'Time to remediate' - if ($CurrentStatesCorrect -eq $false) { - Write-Host 'Settings are not correct' try { if ($StateValue -eq 'enabled') { - Write-Host 'Enabling' # Enable photo updates $null = New-ExoRequest -tenantid $Tenant -cmdlet 'Set-OwaMailboxPolicy' -cmdParams @{Identity = $CurrentOWAState.Identity; SetPhotoEnabled = $true } -useSystemMailbox $true $null = New-GraphPostRequest -uri $Uri -tenant $Tenant -type DELETE -AsApp $true Write-LogMessage -API 'Standards' -tenant $Tenant -message "Set Profile photo settings to $StateValue" -sev Info } else { - Write-Host 'Disabling' # Disable photo updates $null = New-ExoRequest -tenantid $Tenant -cmdlet 'Set-OwaMailboxPolicy' -cmdParams @{Identity = $CurrentOWAState.Identity; SetPhotoEnabled = $false } -useSystemMailbox $true @@ -108,7 +102,6 @@ function Invoke-CIPPStandardProfilePhotos { Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to set profile photo settings to $StateValue. Error: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage } } else { - Write-Host 'Settings are correct' Write-LogMessage -API 'Standards' -tenant $Tenant -message "Profile photo settings are already set to the desired state: $StateValue" -sev Info } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardQuarantineRequestAlert.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardQuarantineRequestAlert.ps1 index 8e22526e6b29..e82e2b5e1b26 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardQuarantineRequestAlert.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardQuarantineRequestAlert.ps1 @@ -34,7 +34,6 @@ function Invoke-CIPPStandardQuarantineRequestAlert { $TestResult = Test-CIPPStandardLicense -StandardName 'QuarantineRequestAlert' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. $PolicyName = 'CIPP User requested to release a quarantined message' diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardQuarantineTemplate.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardQuarantineTemplate.ps1 index a89ce3829c0a..c490d80e062d 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardQuarantineTemplate.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardQuarantineTemplate.ps1 @@ -45,7 +45,6 @@ function Invoke-CIPPStandardQuarantineTemplate { $TestResult = Test-CIPPStandardLicense -StandardName 'QuarantineTemplate' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardRestrictThirdPartyStorageServices.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardRestrictThirdPartyStorageServices.ps1 index d140c767a1da..517045ae0169 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardRestrictThirdPartyStorageServices.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardRestrictThirdPartyStorageServices.ps1 @@ -35,7 +35,6 @@ function Invoke-CIPPStandardRestrictThirdPartyStorageServices { $TestResult = Test-CIPPStandardLicense -StandardName 'ThirdPartyStorageServicesRestricted' -TenantFilter $Tenant -RequiredCapabilities @('SHAREPOINTWAC', 'SHAREPOINTSTANDARD', 'SHAREPOINTENTERPRISE', 'SHAREPOINTENTERPRISE_EDU', 'ONEDRIVE_BASIC', 'ONEDRIVE_ENTERPRISE') if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. @@ -51,8 +50,6 @@ function Invoke-CIPPStandardRestrictThirdPartyStorageServices { } if ($Settings.remediate -eq $true) { - Write-Host 'Time to remediate third-party storage services restriction' - # Check if service principal is already disabled if ($CurrentState.accountEnabled -eq $false) { Write-LogMessage -API 'Standards' -tenant $Tenant -message 'Third-party storage services are already restricted (service principal is disabled).' -sev Info diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardRetentionPolicyTag.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardRetentionPolicyTag.ps1 index cb992130b094..f8e204371c98 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardRetentionPolicyTag.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardRetentionPolicyTag.ps1 @@ -35,7 +35,6 @@ function Invoke-CIPPStandardRetentionPolicyTag { $TestResult = Test-CIPPStandardLicense -StandardName 'RetentionPolicyTag' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. @@ -61,8 +60,6 @@ function Invoke-CIPPStandardRetentionPolicyTag { ($PolicyState.RetentionPolicyTagLinks -contains $PolicyName) if ($Settings.remediate -eq $true) { - Write-Host 'Time to remediate' - if ($StateIsCorrect -eq $true) { Write-LogMessage -API 'Standards' -tenant $Tenant -message 'Retention policy tag already correctly configured' -sev Info } else { diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardRotateDKIM.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardRotateDKIM.ps1 index 9e9e96d44c85..8b148dfc92dc 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardRotateDKIM.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardRotateDKIM.ps1 @@ -36,7 +36,6 @@ function Invoke-CIPPStandardRotateDKIM { $TestResult = Test-CIPPStandardLicense -StandardName 'RotateDKIM' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPAzureB2B.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPAzureB2B.ps1 index 6a4d323ed5a1..c6ef5d3a2862 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPAzureB2B.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPAzureB2B.ps1 @@ -35,7 +35,6 @@ function Invoke-CIPPStandardSPAzureB2B { $TestResult = Test-CIPPStandardLicense -StandardName 'SPAzureB2B' -TenantFilter $Tenant -RequiredCapabilities @('SHAREPOINTWAC', 'SHAREPOINTSTANDARD', 'SHAREPOINTENTERPRISE', 'SHAREPOINTENTERPRISE_EDU', 'ONEDRIVE_BASIC', 'ONEDRIVE_ENTERPRISE') if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPDirectSharing.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPDirectSharing.ps1 index a2ac80b497d4..f8f894c18a3d 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPDirectSharing.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPDirectSharing.ps1 @@ -35,7 +35,6 @@ function Invoke-CIPPStandardSPDirectSharing { $TestResult = Test-CIPPStandardLicense -StandardName 'SPDirectSharing' -TenantFilter $Tenant -RequiredCapabilities @('SHAREPOINTWAC', 'SHAREPOINTSTANDARD', 'SHAREPOINTENTERPRISE', 'SHAREPOINTENTERPRISE_EDU', 'ONEDRIVE_BASIC', 'ONEDRIVE_ENTERPRISE') if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPDisableLegacyWorkflows.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPDisableLegacyWorkflows.ps1 index 5612ce7c034e..92e648c9cf88 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPDisableLegacyWorkflows.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPDisableLegacyWorkflows.ps1 @@ -32,7 +32,6 @@ function Invoke-CIPPStandardSPDisableLegacyWorkflows { $TestResult = Test-CIPPStandardLicense -StandardName 'SPDisableLegacyWorkflows' -TenantFilter $Tenant -RequiredCapabilities @('SHAREPOINTWAC', 'SHAREPOINTSTANDARD', 'SHAREPOINTENTERPRISE', 'SHAREPOINTENTERPRISE_EDU', 'ONEDRIVE_BASIC', 'ONEDRIVE_ENTERPRISE') if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPDisallowInfectedFiles.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPDisallowInfectedFiles.ps1 index 650647f6a0e0..969c329052ce 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPDisallowInfectedFiles.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPDisallowInfectedFiles.ps1 @@ -38,7 +38,6 @@ function Invoke-CIPPStandardSPDisallowInfectedFiles { $TestResult = Test-CIPPStandardLicense -StandardName 'SPDisallowInfectedFiles' -TenantFilter $Tenant -RequiredCapabilities @('SHAREPOINTWAC', 'SHAREPOINTSTANDARD', 'SHAREPOINTENTERPRISE', 'SHAREPOINTENTERPRISE_EDU','ONEDRIVE_BASIC', 'ONEDRIVE_ENTERPRISE') if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPEmailAttestation.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPEmailAttestation.ps1 index 965b5bf90d5c..154451f56a8a 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPEmailAttestation.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPEmailAttestation.ps1 @@ -38,7 +38,6 @@ function Invoke-CIPPStandardSPEmailAttestation { $TestResult = Test-CIPPStandardLicense -StandardName 'SPEmailAttestation' -TenantFilter $Tenant -RequiredCapabilities @('SHAREPOINTWAC', 'SHAREPOINTSTANDARD', 'SHAREPOINTENTERPRISE', 'SHAREPOINTENTERPRISE_EDU', 'ONEDRIVE_BASIC', 'ONEDRIVE_ENTERPRISE') if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPExternalUserExpiration.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPExternalUserExpiration.ps1 index f5595d9263f1..8070afb1b523 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPExternalUserExpiration.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPExternalUserExpiration.ps1 @@ -37,7 +37,6 @@ function Invoke-CIPPStandardSPExternalUserExpiration { $TestResult = Test-CIPPStandardLicense -StandardName 'SPExternalUserExpiration' -TenantFilter $Tenant -RequiredCapabilities @('SHAREPOINTWAC', 'SHAREPOINTSTANDARD', 'SHAREPOINTENTERPRISE', 'SHAREPOINTENTERPRISE_EDU', 'ONEDRIVE_BASIC', 'ONEDRIVE_ENTERPRISE') if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPSyncButtonState.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPSyncButtonState.ps1 index 0c39478cd85b..24d175063862 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPSyncButtonState.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPSyncButtonState.ps1 @@ -34,7 +34,6 @@ function Invoke-CIPPStandardSPSyncButtonState { $TestResult = Test-CIPPStandardLicense -StandardName 'SPSyncButtonState' -TenantFilter $Tenant -RequiredCapabilities @('SHAREPOINTWAC', 'SHAREPOINTSTANDARD', 'SHAREPOINTENTERPRISE', 'SHAREPOINTENTERPRISE_EDU', 'ONEDRIVE_BASIC', 'ONEDRIVE_ENTERPRISE') if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. @@ -59,8 +58,6 @@ function Invoke-CIPPStandardSPSyncButtonState { $HumanReadableState = if ($WantedState -eq $true) { 'disabled' } else { 'enabled' } if ($Settings.remediate -eq $true) { - Write-Host 'Time to remediate' - if ($StateIsCorrect -eq $false) { try { $CurrentState | Set-CIPPSPOTenant -Properties @{HideSyncButtonOnDocLib = $WantedState } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeAttachmentPolicy.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeAttachmentPolicy.ps1 index f23855cbbeee..4e8ccfe8d6ba 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeAttachmentPolicy.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeAttachmentPolicy.ps1 @@ -42,7 +42,6 @@ function Invoke-CIPPStandardSafeAttachmentPolicy { $TestResult = Test-CIPPStandardLicense -StandardName 'SafeAttachmentPolicy' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeLinksPolicy.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeLinksPolicy.ps1 index bd7fadbfbbb4..4293c173c9bd 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeLinksPolicy.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeLinksPolicy.ps1 @@ -41,7 +41,6 @@ function Invoke-CIPPStandardSafeLinksPolicy { $TestResult = Test-CIPPStandardLicense -StandardName 'SafeLinksPolicy' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeLinksTemplatePolicy.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeLinksTemplatePolicy.ps1 index f3a91c2a43e6..7cf312e6d678 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeLinksTemplatePolicy.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeLinksTemplatePolicy.ps1 @@ -34,7 +34,6 @@ function Invoke-CIPPStandardSafeLinksTemplatePolicy { $TestResult = Test-CIPPStandardLicense -StandardName 'SafeLinksTemplatePolicy' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeSendersDisable.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeSendersDisable.ps1 index b879e3250c8d..6196df255398 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeSendersDisable.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeSendersDisable.ps1 @@ -36,7 +36,6 @@ function Invoke-CIPPStandardSafeSendersDisable { $TestResult = Test-CIPPStandardLicense -StandardName 'SafeSendersDisable' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. @@ -59,7 +58,6 @@ function Invoke-CIPPStandardSafeSendersDisable { $BatchResults | ForEach-Object { if ($_.error) { $ErrorMessage = Get-NormalizedError -Message $_.error - Write-Host "Failed to Disable SafeSenders for $($_.target). Error: $ErrorMessage" Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to Disable SafeSenders for $($_.target). Error: $ErrorMessage" -sev Error } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSecureScoreRemediation.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSecureScoreRemediation.ps1 index 4534d08d73ef..9dbfee7f6eb1 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSecureScoreRemediation.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSecureScoreRemediation.ps1 @@ -95,8 +95,6 @@ function Invoke-CIPPStandardSecureScoreRemediation { } if ($Settings.remediate -eq $true) { - Write-Host 'Processing Secure Score control updates' - foreach ($Control in $ControlsToUpdate) { # Skip if this is a Defender control (starts with scid_) if ($Control.ControlName -match '^scid_') { diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSecurityDefaults.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSecurityDefaults.ps1 index bc5c73b053b5..e774f32756e7 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSecurityDefaults.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSecurityDefaults.ps1 @@ -44,7 +44,6 @@ function Invoke-CIPPStandardSecurityDefaults { if ($SecureDefaultsState.IsEnabled -ne $true) { try { - Write-Host "Secure Defaults is disabled. Enabling for $tenant" -ForegroundColor Yellow $body = '{ "isEnabled": true }' $null = New-GraphPostRequest -tenantid $tenant -Uri 'https://graph.microsoft.com/beta/policies/identitySecurityDefaultsEnforcementPolicy' -Type patch -Body $body -ContentType 'application/json' diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSendFromAlias.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSendFromAlias.ps1 index 4f2d624577b9..8dc6a4af027b 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSendFromAlias.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSendFromAlias.ps1 @@ -34,7 +34,6 @@ function Invoke-CIPPStandardSendFromAlias { $TestResult = Test-CIPPStandardLicense -StandardName 'SendFromAlias' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSendReceiveLimitTenant.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSendReceiveLimitTenant.ps1 index be9fe0909549..0911e5d2634d 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSendReceiveLimitTenant.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSendReceiveLimitTenant.ps1 @@ -35,7 +35,6 @@ function Invoke-CIPPStandardSendReceiveLimitTenant { $TestResult = Test-CIPPStandardLicense -StandardName 'SendReceiveLimitTenant' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. @@ -78,10 +77,7 @@ function Invoke-CIPPStandardSendReceiveLimitTenant { } if ($Settings.remediate -eq $true) { - Write-Host "Time to remediate. Our Settings are $($Settings.SendLimit)MB and $($Settings.ReceiveLimit)MB" - if ($NotSetCorrectly.Count -gt 0) { - Write-Host "Found $($NotSetCorrectly.Count) Mailbox Plans that are not set correctly. Setting them to $($Settings.SendLimit)MB and $($Settings.ReceiveLimit)MB" try { foreach ($MailboxPlan in $NotSetCorrectly) { New-ExoRequest -tenantid $Tenant -cmdlet 'Set-MailboxPlan' -cmdParams @{Identity = $MailboxPlan.GUID; MaxSendSize = $MaxSendSize; MaxReceiveSize = $MaxReceiveSize } -useSystemMailbox $true diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSharePointMassDeletionAlert.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSharePointMassDeletionAlert.ps1 index 4891f990cf02..6913b882617e 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSharePointMassDeletionAlert.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSharePointMassDeletionAlert.ps1 @@ -36,7 +36,6 @@ function Invoke-CIPPStandardSharePointMassDeletionAlert { $TestResult = Test-CIPPStandardLicense -StandardName 'DeletedUserRentention' -TenantFilter $Tenant -RequiredCapabilities @('RMS_S_PREMIUM2') if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardShortenMeetings.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardShortenMeetings.ps1 index b05f3abb32db..64b420cfa7c3 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardShortenMeetings.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardShortenMeetings.ps1 @@ -36,10 +36,9 @@ function Invoke-CIPPStandardShortenMeetings { $TestResult = Test-CIPPStandardLicense -StandardName 'ShortenMeetings' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. - Write-Host "ShortenMeetings: $($Settings | ConvertTo-Json -Compress)" + # Get state value using null-coalescing operator $scopeDefault = $Settings.ShortenEventScopeDefault.value ? $Settings.ShortenEventScopeDefault.value : $Settings.ShortenEventScopeDefault @@ -57,8 +56,6 @@ function Invoke-CIPPStandardShortenMeetings { $CurrentState.DefaultMinutesToReduceLongEventsBy -eq $Settings.DefaultMinutesToReduceLongEventsBy) { $true } else { $false } if ($Settings.remediate -eq $true) { - Write-Host 'Time to remediate' - if ($CorrectState -eq $true) { Write-LogMessage -API 'Standards' -tenant $Tenant -message 'Shorten meetings settings are already in the correct state. ' -sev Info } else { diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSpamFilterPolicy.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSpamFilterPolicy.ps1 index 32a9cb7f22b1..4c76216889a7 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSpamFilterPolicy.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSpamFilterPolicy.ps1 @@ -55,7 +55,6 @@ function Invoke-CIPPStandardSpamFilterPolicy { $TestResult = Test-CIPPStandardLicense -StandardName 'SpamFilterPolicy' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSpoofWarn.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSpoofWarn.ps1 index fc1989311104..3da3cec7fe99 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSpoofWarn.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSpoofWarn.ps1 @@ -38,7 +38,6 @@ function Invoke-CIPPStandardSpoofWarn { $TestResult = Test-CIPPStandardLicense -StandardName 'SpoofWarn' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. @@ -60,16 +59,12 @@ function Invoke-CIPPStandardSpoofWarn { $AllowListCorrect = $true if ($AllowListAdd -eq $null -or $AllowListAdd.Count -eq 0) { - Write-Host 'No AllowList entries provided, skipping AllowList check.' $AllowListAdd = @{'@odata.type' = '#Exchange.GenericHashTable'; Add = @() } } else { $AllowListAddEntries = foreach ($entry in $AllowListAdd) { if ($CurrentInfo.AllowList -notcontains $entry) { $AllowListCorrect = $false - Write-Host "AllowList entry $entry not found in current AllowList" $entry - } else { - Write-Host "AllowList entry $entry found in current AllowList." } } $AllowListAdd = @{'@odata.type' = '#Exchange.GenericHashTable'; Add = $AllowListAddEntries } @@ -86,7 +81,6 @@ function Invoke-CIPPStandardSpoofWarn { } if ($Settings.remediate -eq $true) { - Write-Host 'Time to remediate!' $status = if ($Settings.enable -and $Settings.disable) { # Handle pre standards v2.0 legacy settings when this was 2 separate standards Write-LogMessage -API 'Standards' -tenant $Tenant -message 'You cannot both enable and disable the Spoof Warnings setting' -sev Error diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardStaleEntraDevices.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardStaleEntraDevices.ps1 index 88deaae2c234..fb789d4036e6 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardStaleEntraDevices.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardStaleEntraDevices.ps1 @@ -41,7 +41,6 @@ function Invoke-CIPPStandardStaleEntraDevices { # Get all Entra devices if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. @@ -57,8 +56,6 @@ function Invoke-CIPPStandardStaleEntraDevices { $StaleDevices = $AllDevices | Where-Object { $_.approximateLastSignInDateTime -lt $Date } if ($Settings.remediate -eq $true) { - - Write-Host 'Remediation not implemented yet' # TODO: Implement remediation. For others in the future that want to try this: # Good MS guide on what to watch out for https://learn.microsoft.com/en-us/entra/identity/devices/manage-stale-devices#clean-up-stale-devices # https://learn.microsoft.com/en-us/graph/api/device-list?view=graph-rest-beta&tabs=http diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsChatProtection.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsChatProtection.ps1 index 06bd0ed34f07..6c21fe2f03c4 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsChatProtection.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsChatProtection.ps1 @@ -36,7 +36,6 @@ function Invoke-CIPPStandardTeamsChatProtection { $TestResult = Test-CIPPStandardLicense -StandardName 'TeamsChatProtection' -TenantFilter $Tenant -RequiredCapabilities @('MCOSTANDARD', 'MCOEV', 'MCOIMP', 'TEAMS1', 'Teams_Room_Standard') if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsEmailIntegration.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsEmailIntegration.ps1 index 6cf019c7353e..df9b629223f3 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsEmailIntegration.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsEmailIntegration.ps1 @@ -36,7 +36,6 @@ function Invoke-CIPPStandardTeamsEmailIntegration { $TestResult = Test-CIPPStandardLicense -StandardName 'TeamsEmailIntegration' -TenantFilter $Tenant -RequiredCapabilities @('MCOSTANDARD', 'MCOEV', 'MCOIMP', 'TEAMS1', 'Teams_Room_Standard') if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsEnrollUser.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsEnrollUser.ps1 index ac908df686cb..bc64d8317237 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsEnrollUser.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsEnrollUser.ps1 @@ -36,7 +36,6 @@ function Invoke-CIPPStandardTeamsEnrollUser { # Get EnrollUserOverride value using null-coalescing operator if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. $enrollUserOverride = $Settings.EnrollUserOverride.value ?? $Settings.EnrollUserOverride diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsExternalAccessPolicy.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsExternalAccessPolicy.ps1 index 3b09e9240683..78d77b893356 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsExternalAccessPolicy.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsExternalAccessPolicy.ps1 @@ -35,7 +35,6 @@ function Invoke-CIPPStandardTeamsExternalAccessPolicy { $TestResult = Test-CIPPStandardLicense -StandardName 'TeamsExternalAccessPolicy' -TenantFilter $Tenant -RequiredCapabilities @('MCOSTANDARD', 'MCOEV', 'MCOIMP', 'TEAMS1', 'Teams_Room_Standard') if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsExternalChatWithAnyone.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsExternalChatWithAnyone.ps1 index 659a46c5d73a..2a83ae8a4a76 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsExternalChatWithAnyone.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsExternalChatWithAnyone.ps1 @@ -35,7 +35,6 @@ function Invoke-CIPPStandardTeamsExternalChatWithAnyone { $TestResult = Test-CIPPStandardLicense -StandardName 'TeamsExternalChatWithAnyone' -TenantFilter $Tenant -RequiredCapabilities @('MCOSTANDARD', 'MCOEV', 'MCOIMP', 'TEAMS1', 'Teams_Room_Standard') if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." return $true } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsExternalFileSharing.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsExternalFileSharing.ps1 index a24252704227..296160fd73fb 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsExternalFileSharing.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsExternalFileSharing.ps1 @@ -40,7 +40,6 @@ function Invoke-CIPPStandardTeamsExternalFileSharing { $TestResult = Test-CIPPStandardLicense -StandardName 'TeamsExternalFileSharing' -TenantFilter $Tenant -RequiredCapabilities @('MCOSTANDARD', 'MCOEV', 'MCOIMP', 'TEAMS1', 'Teams_Room_Standard') if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsFederationConfiguration.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsFederationConfiguration.ps1 index ee526b9037bb..2ac7b7ae4898 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsFederationConfiguration.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsFederationConfiguration.ps1 @@ -36,7 +36,6 @@ function Invoke-CIPPStandardTeamsFederationConfiguration { $TestResult = Test-CIPPStandardLicense -StandardName 'TeamsFederationConfiguration' -TenantFilter $Tenant -RequiredCapabilities @('MCOSTANDARD', 'MCOEV', 'MCOIMP', 'TEAMS1','Teams_Room_Standard') if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsGlobalMeetingPolicy.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsGlobalMeetingPolicy.ps1 index 163e626da422..7c30eb607332 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsGlobalMeetingPolicy.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsGlobalMeetingPolicy.ps1 @@ -44,7 +44,6 @@ function Invoke-CIPPStandardTeamsGlobalMeetingPolicy { $TestResult = Test-CIPPStandardLicense -StandardName 'TeamsGlobalMeetingPolicy' -TenantFilter $Tenant -RequiredCapabilities @('MCOSTANDARD', 'MCOEV', 'MCOIMP', 'TEAMS1', 'Teams_Room_Standard') if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsGuestAccess.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsGuestAccess.ps1 index 3c5c3d6336f9..14ea444c35b1 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsGuestAccess.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsGuestAccess.ps1 @@ -34,7 +34,6 @@ function Invoke-CIPPStandardTeamsGuestAccess { $TestResult = Test-CIPPStandardLicense -StandardName 'TeamsGuestAccess' -TenantFilter $Tenant -RequiredCapabilities @('MCOSTANDARD', 'MCOEV', 'MCOIMP', 'TEAMS1', 'Teams_Room_Standard') if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsMeetingRecordingExpiration.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsMeetingRecordingExpiration.ps1 index 9004c3de2b30..9738625eb568 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsMeetingRecordingExpiration.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsMeetingRecordingExpiration.ps1 @@ -36,7 +36,6 @@ function Invoke-CIPPStandardTeamsMeetingRecordingExpiration { # Input validation if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. $ExpirationDays = try { [int64]$Settings.ExpirationDays } catch { Write-Warning "Invalid ExpirationDays value provided: $($Settings.ExpirationDays)"; return } @@ -56,7 +55,6 @@ function Invoke-CIPPStandardTeamsMeetingRecordingExpiration { $StateIsCorrect = if ($CurrentExpirationDays -eq $ExpirationDays) { $true } else { $false } if ($Settings.remediate -eq $true) { - Write-Host 'Time to remediate' if ($StateIsCorrect -eq $true) { Write-LogMessage -API 'Standards' -tenant $Tenant -message "Teams Meeting Recording Expiration Policy already set to $ExpirationDays days." -sev Info } else { diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsMeetingVerification.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsMeetingVerification.ps1 index 119e3a0af58a..c9ac9f3a5db9 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsMeetingVerification.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsMeetingVerification.ps1 @@ -35,7 +35,6 @@ function Invoke-CIPPStandardTeamsMeetingVerification { $TestResult = Test-CIPPStandardLicense -StandardName 'TeamsMeetingVerification' -TenantFilter $Tenant -RequiredCapabilities @('MCOSTANDARD', 'MCOEV', 'MCOIMP', 'TEAMS1', 'Teams_Room_Standard') if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsMeetingsByDefault.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsMeetingsByDefault.ps1 index 38cb28d5efae..187764549419 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsMeetingsByDefault.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsMeetingsByDefault.ps1 @@ -34,7 +34,6 @@ function Invoke-CIPPStandardTeamsMeetingsByDefault { $TestResult = Test-CIPPStandardLicense -StandardName 'TeamsMeetingsByDefault' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. @@ -59,7 +58,6 @@ function Invoke-CIPPStandardTeamsMeetingsByDefault { } if ($Settings.remediate -eq $true) { - Write-Host 'Time to remediate' if ($StateIsCorrect -eq $false) { try { $null = New-ExoRequest -tenantid $Tenant -cmdlet 'Set-OrganizationConfig' -cmdParams @{ OnlineMeetingsByDefaultEnabled = $WantedState } -useSystemMailbox $true diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsMessagingPolicy.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsMessagingPolicy.ps1 index d2c938e26366..27aa5e7a4f8f 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsMessagingPolicy.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsMessagingPolicy.ps1 @@ -42,7 +42,6 @@ function Invoke-CIPPStandardTeamsMessagingPolicy { $TestResult = Test-CIPPStandardLicense -StandardName 'TeamsMessagingPolicy' -TenantFilter $Tenant -RequiredCapabilities @('MCOSTANDARD', 'MCOEV', 'MCOIMP', 'TEAMS1', 'Teams_Room_Standard') if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTenantDefaultTimezone.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTenantDefaultTimezone.ps1 index 265ff6116e17..7463f6e7e773 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTenantDefaultTimezone.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTenantDefaultTimezone.ps1 @@ -34,7 +34,6 @@ function Invoke-CIPPStandardTenantDefaultTimezone { $TestResult = Test-CIPPStandardLicense -StandardName 'TenantDefaultTimezone' -TenantFilter $Tenant -RequiredCapabilities @('SHAREPOINTWAC', 'SHAREPOINTSTANDARD', 'SHAREPOINTENTERPRISE', 'SHAREPOINTENTERPRISE_EDU', 'ONEDRIVE_BASIC', 'ONEDRIVE_ENTERPRISE') if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTransportRuleTemplate.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTransportRuleTemplate.ps1 index d129d05ab124..1387dc113b7d 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTransportRuleTemplate.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTransportRuleTemplate.ps1 @@ -31,16 +31,13 @@ function Invoke-CIPPStandardTransportRuleTemplate { $TestResult = Test-CIPPStandardLicense -StandardName 'TransportRuleTemplate' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. $existingRules = New-ExoRequest -ErrorAction SilentlyContinue -tenantid $Tenant -cmdlet 'Get-TransportRule' -useSystemMailbox $true if ($Settings.remediate -eq $true) { - Write-Host "Settings: $($Settings | ConvertTo-Json)" $Settings.transportRuleTemplate ? ($Settings | Add-Member -NotePropertyName 'TemplateList' -NotePropertyValue $Settings.transportRuleTemplate) : $null foreach ($Template in $Settings.TemplateList) { - Write-Host "working on $($Template.value)" $Table = Get-CippTable -tablename 'templates' $Filter = "PartitionKey eq 'TransportTemplate' and RowKey eq '$($Template.value)'" $RequestParams = (Get-AzDataTableEntity @Table -Filter $Filter).JSON | ConvertFrom-Json @@ -48,7 +45,6 @@ function Invoke-CIPPStandardTransportRuleTemplate { try { if ($Existing) { - Write-Host 'Found existing' if ($Settings.overwrite) { $RequestParams | Add-Member -NotePropertyValue $RequestParams.name -NotePropertyName Identity $GraphRequest = New-ExoRequest -tenantid $Tenant -cmdlet 'Set-TransportRule' -cmdParams ($RequestParams | Select-Object -Property * -ExcludeProperty GUID, Comments, HasSenderOverride, ExceptIfHasSenderOverride, ExceptIfMessageContainsDataClassifications, MessageContainsDataClassifications, UseLegacyRegex) -useSystemMailbox $true @@ -57,7 +53,6 @@ function Invoke-CIPPStandardTransportRuleTemplate { Write-LogMessage -API 'Standards' -tenant $tenant -message "Skipping transport rule for $tenant as it already exists" -sev 'Info' } } else { - Write-Host 'Creating new' $GraphRequest = New-ExoRequest -tenantid $Tenant -cmdlet 'New-TransportRule' -cmdParams ($RequestParams | Select-Object -Property * -ExcludeProperty GUID, Comments, HasSenderOverride, ExceptIfHasSenderOverride, ExceptIfMessageContainsDataClassifications, MessageContainsDataClassifications, UseLegacyRegex) -useSystemMailbox $true Write-LogMessage -API 'Standards' -tenant $tenant -message "Successfully created transport rule for $tenant" -sev 'Info' } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTwoClickEmailProtection.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTwoClickEmailProtection.ps1 index 3368eaa5be6c..cd23ae592974 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTwoClickEmailProtection.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTwoClickEmailProtection.ps1 @@ -34,7 +34,6 @@ function Invoke-CIPPStandardTwoClickEmailProtection { $TestResult = Test-CIPPStandardLicense -StandardName 'TwoClickEmailProtection' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. @@ -59,8 +58,6 @@ function Invoke-CIPPStandardTwoClickEmailProtection { $StateIsCorrect = $CurrentState -eq $WantedState ? $true : $false if ($Settings.remediate -eq $true) { - Write-Host 'Time to remediate two-click email protection' - if ($StateIsCorrect -eq $true) { Write-LogMessage -API 'Standards' -tenant $Tenant -message "Two-click email protection is already set to $State." -sev Info } else { diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardUserSubmissions.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardUserSubmissions.ps1 index d0653e4d2c8f..265959e7ce6a 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardUserSubmissions.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardUserSubmissions.ps1 @@ -35,7 +35,6 @@ function Invoke-CIPPStandardUserSubmissions { $TestResult = Test-CIPPStandardLicense -StandardName 'UserSubmissions' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardcalDefault.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardcalDefault.ps1 index 193addccbdc1..ed12a9730c35 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardcalDefault.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardcalDefault.ps1 @@ -37,7 +37,6 @@ function Invoke-CIPPStandardcalDefault { $TestResult = Test-CIPPStandardLicense -StandardName 'calDefault' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') #No Foundation because that does not allow powershell access if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. @@ -68,9 +67,6 @@ function Invoke-CIPPStandardcalDefault { $SuccessCounter = if ($startIndex -eq 0) { 0 } else { [int64]$LastRun.currentSuccessCount } $processedMailboxes = $startIndex $Mailboxes = $Mailboxes[$startIndex..($TotalMailboxes - 1)] - Write-Host "CalDefaults Starting at index $startIndex" - Write-Host "CalDefaults success counter starting at $SuccessCounter" - Write-Host "CalDefaults Processing $($Mailboxes.Count) mailboxes" $Mailboxes | ForEach-Object { $Mailbox = $_ try { @@ -82,7 +78,6 @@ function Invoke-CIPPStandardcalDefault { $SuccessCounter++ } catch { $ErrorMessage = Get-CippException -Exception $_ - Write-Host "Setting cal failed: $ErrorMessage" Write-LogMessage -API 'Standards' -tenant $Tenant -message "Could not set default calendar permissions for $($Mailbox.UserPrincipalName). Error: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage } } @@ -100,7 +95,6 @@ function Invoke-CIPPStandardcalDefault { currentSuccessCount = $SuccessCounter } Add-CIPPAzDataTableEntity @LastRunTable -Entity $LastRun -Force - Write-Host "Processed $processedMailboxes mailboxes" } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandarddisableMacSync.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandarddisableMacSync.ps1 index df8b04ecfdeb..47f420c9ac3f 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandarddisableMacSync.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandarddisableMacSync.ps1 @@ -33,7 +33,6 @@ function Invoke-CIPPStandarddisableMacSync { $TestResult = Test-CIPPStandardLicense -StandardName 'disableMacSync' -TenantFilter $Tenant -RequiredCapabilities @('SHAREPOINTWAC', 'SHAREPOINTSTANDARD', 'SHAREPOINTENTERPRISE', 'SHAREPOINTENTERPRISE_EDU', 'ONEDRIVE_BASIC', 'ONEDRIVE_ENTERPRISE') if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardintuneBrandingProfile.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardintuneBrandingProfile.ps1 index cd36588ae4ad..66ed49f285ec 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardintuneBrandingProfile.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardintuneBrandingProfile.ps1 @@ -43,7 +43,6 @@ function Invoke-CIPPStandardintuneBrandingProfile { $TestResult = Test-CIPPStandardLicense -StandardName 'intuneBrandingProfile' -TenantFilter $Tenant -RequiredCapabilities @('INTUNE_A', 'MDM_Services', 'EMS', 'SCCM', 'MICROSOFTINTUNEPLAN1') if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardintuneDeviceReg.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardintuneDeviceReg.ps1 index 24948cf10bf7..aa31d08e889b 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardintuneDeviceReg.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardintuneDeviceReg.ps1 @@ -35,7 +35,6 @@ function Invoke-CIPPStandardintuneDeviceReg { $TestResult = Test-CIPPStandardLicense -StandardName 'intuneDeviceReg' -TenantFilter $Tenant -RequiredCapabilities @('INTUNE_A', 'MDM_Services', 'EMS', 'SCCM', 'MICROSOFTINTUNEPLAN1') if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardintuneDeviceRetirementDays.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardintuneDeviceRetirementDays.ps1 index b980c034ddd0..6441d9ff09bc 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardintuneDeviceRetirementDays.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardintuneDeviceRetirementDays.ps1 @@ -35,7 +35,6 @@ function Invoke-CIPPStandardintuneDeviceRetirementDays { $TestResult = Test-CIPPStandardLicense -StandardName 'intuneDeviceRetirementDays' -TenantFilter $Tenant -RequiredCapabilities @('INTUNE_A', 'MDM_Services', 'EMS', 'SCCM', 'MICROSOFTINTUNEPLAN1') if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardsharingCapability.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardsharingCapability.ps1 index 054066c38646..f19d3dba95b4 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardsharingCapability.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardsharingCapability.ps1 @@ -39,7 +39,6 @@ function Invoke-CIPPStandardsharingCapability { $TestResult = Test-CIPPStandardLicense -StandardName 'sharingCapability' -TenantFilter $Tenant -RequiredCapabilities @('SHAREPOINTWAC', 'SHAREPOINTSTANDARD', 'SHAREPOINTENTERPRISE', 'SHAREPOINTENTERPRISE_EDU', 'ONEDRIVE_BASIC', 'ONEDRIVE_ENTERPRISE') if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. @@ -67,10 +66,8 @@ function Invoke-CIPPStandardsharingCapability { if ($Settings.remediate -eq $true) { if ($CurrentInfo.sharingCapability -eq $level) { - Write-Host "Sharing level is already set to $level" Write-LogMessage -API 'Standards' -tenant $Tenant -message "Sharing level is already set to $level" -sev Info } else { - Write-Host "Setting sharing level to $level from $($CurrentInfo.sharingCapability)" try { $body = @{ sharingCapability = $level diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardsharingDomainRestriction.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardsharingDomainRestriction.ps1 index 6aae7a5b8dcd..f0bd6cff3c37 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardsharingDomainRestriction.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardsharingDomainRestriction.ps1 @@ -38,7 +38,6 @@ function Invoke-CIPPStandardsharingDomainRestriction { $TestResult = Test-CIPPStandardLicense -StandardName 'sharingDomainRestriction' -TenantFilter $Tenant -RequiredCapabilities @('SHAREPOINTWAC', 'SHAREPOINTSTANDARD', 'SHAREPOINTENTERPRISE', 'SHAREPOINTENTERPRISE_EDU', 'ONEDRIVE_BASIC', 'ONEDRIVE_ENTERPRISE') if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. @@ -65,7 +64,6 @@ function Invoke-CIPPStandardsharingDomainRestriction { ($mode -eq 'blockList' -and ([string[]]($CurrentBlockedDomains | Sort-Object) -join ',') -eq ([string[]]($SelectedDomains | Sort-Object) -join ',')) ) } - Write-Host "StateIsCorrect: $StateIsCorrect" if ($Settings.remediate -eq $true) { if ($StateIsCorrect -eq $true) { @@ -89,8 +87,6 @@ function Invoke-CIPPStandardsharingDomainRestriction { body = ($Body | ConvertTo-Json) } - Write-Host ($cmdParams | ConvertTo-Json -Depth 5) - try { $null = New-GraphPostRequest @cmdParams Write-LogMessage -API 'Standards' -tenant $tenant -message 'Successfully updated Sharing Domain Restriction settings' -sev Info diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardunmanagedSync.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardunmanagedSync.ps1 index 18b75b21b643..fdf2bd41931f 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardunmanagedSync.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardunmanagedSync.ps1 @@ -38,7 +38,6 @@ function Invoke-CIPPStandardunmanagedSync { $TestResult = Test-CIPPStandardLicense -StandardName 'unmanagedSync' -TenantFilter $Tenant -RequiredCapabilities @('INTUNE_A', 'MDM_Services', 'EMS', 'SCCM', 'MICROSOFTINTUNEPLAN1') if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." return $true } #we're done. From b8333476e83ce8a58d8e3a4243b5e94db042d423 Mon Sep 17 00:00:00 2001 From: rvdwegen Date: Wed, 28 Jan 2026 11:09:11 +0100 Subject: [PATCH 271/503] Remove use of foreach-object from standards --- .../Standards/Invoke-CIPPStandardAddDKIM.ps1 | 34 ++++++------- .../Invoke-CIPPStandardAddDMARCToMOERA.ps1 | 12 ++--- .../Invoke-CIPPStandardAutoAddProxy.ps1 | 8 +-- ...-CIPPStandardConditionalAccessTemplate.ps1 | 4 +- ...e-CIPPStandardCustomBannedPasswordList.ps1 | 4 +- .../Invoke-CIPPStandardDelegateSentItems.ps1 | 12 ++--- ...nvoke-CIPPStandardDisableBasicAuthSMTP.ps1 | 8 +-- ...tandardDisableExchangeOnlinePowerShell.ps1 | 12 ++--- ...StandardDisableExternalCalendarSharing.ps1 | 8 +-- .../Invoke-CIPPStandardDisableGuests.ps1 | 6 +-- ...nvoke-CIPPStandardDisableOutlookAddins.ps1 | 5 +- ...oke-CIPPStandardDisableResourceMailbox.ps1 | 8 +-- ...CIPPStandardDisableSelfServiceLicenses.ps1 | 17 +++---- ...nvoke-CIPPStandardDisableSharedMailbox.ps1 | 6 +-- ...nvoke-CIPPStandardEnableLitigationHold.ps1 | 12 ++--- ...voke-CIPPStandardEnableMailboxAuditing.ps1 | 12 ++--- ...voke-CIPPStandardEnableOnlineArchiving.ps1 | 16 +++--- .../Invoke-CIPPStandardIntuneTemplate.ps1 | 4 +- ...oke-CIPPStandardMailboxRecipientLimits.ps1 | 51 +++++++++---------- .../Invoke-CIPPStandardOauthConsentLowSec.ps1 | 6 +-- ...oke-CIPPStandardPasswordExpireDisabled.ps1 | 10 ++-- .../Invoke-CIPPStandardRotateDKIM.ps1 | 6 +-- .../Invoke-CIPPStandardSafeSendersDisable.ps1 | 12 ++--- .../Invoke-CIPPStandardcalDefault.ps1 | 49 +++++++++--------- 24 files changed, 160 insertions(+), 162 deletions(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAddDKIM.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAddDKIM.ps1 index 8bfce0573246..e4831bb8f50e 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAddDKIM.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAddDKIM.ps1 @@ -60,10 +60,10 @@ function Invoke-CIPPStandardAddDKIM { # Check for errors in the batch results. Cannot continue if there are errors. $ErrorCounter = 0 $ErrorMessages = [System.Collections.Generic.List[string]]::new() - $BatchResults | ForEach-Object { - if ($_.error) { + foreach ($Result in $BatchResults) { + if ($Result.error) { $ErrorCounter++ - $ErrorMessage = Get-NormalizedError -Message $_.error + $ErrorMessage = Get-NormalizedError -Message $Result.error $ErrorMessages.Add($ErrorMessage) } } @@ -88,8 +88,8 @@ function Invoke-CIPPStandardAddDKIM { '*.teams-sbc.dk' ) - $AllDomains = ($BatchResults | Where-Object { $_.DomainName }).DomainName | ForEach-Object { - $Domain = $_ + $AllDomains = foreach ($DomainName in ($BatchResults | Where-Object { $_.DomainName }).DomainName) { + $Domain = $DomainName foreach ($ExclusionDomain in $ExclusionDomains) { if ($Domain -like $ExclusionDomain) { $Domain = $null @@ -97,8 +97,8 @@ function Invoke-CIPPStandardAddDKIM { } if ($null -ne $Domain) { $Domain } } - $DKIM = $BatchResults | Where-Object { $_.Domain } | Select-Object Domain, Enabled, Status | ForEach-Object { - $Domain = $_ + $DKIM = foreach ($DkimConfig in ($BatchResults | Where-Object { $_.Domain } | Select-Object Domain, Enabled, Status)) { + $Domain = $DkimConfig foreach ($ExclusionDomain in $ExclusionDomains) { if ($Domain.Domain -like $ExclusionDomain) { $Domain = $null @@ -131,37 +131,37 @@ function Invoke-CIPPStandardAddDKIM { Write-LogMessage -API 'Standards' -tenant $Tenant -message "Trying to enable DKIM for:$($NewDomains -join ', ' ) $($SetDomains.Domain -join ', ')" -sev Info # New-domains - $Request = $NewDomains | ForEach-Object { + $Request = foreach ($Domain in $NewDomains) { @{ CmdletInput = @{ CmdletName = 'New-DkimSigningConfig' - Parameters = @{ KeySize = 2048; DomainName = $_; Enabled = $true } + Parameters = @{ KeySize = 2048; DomainName = $Domain; Enabled = $true } } } } if ($null -ne $Request) { $BatchResults = New-ExoBulkRequest -tenantid $Tenant -cmdletArray @($Request) -useSystemMailbox $true } - $BatchResults | ForEach-Object { - if ($_.error) { + foreach ($Result in $BatchResults) { + if ($Result.error) { $ErrorCounter ++ - $ErrorMessage = Get-NormalizedError -Message $_.error + $ErrorMessage = Get-NormalizedError -Message $Result.error Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to enable DKIM. Error: $ErrorMessage" -sev Error } } # Set-domains - $Request = $SetDomains | ForEach-Object { + $Request = foreach ($Domain in $SetDomains) { @{ CmdletInput = @{ CmdletName = 'Set-DkimSigningConfig' - Parameters = @{ Identity = $_.Domain; Enabled = $true } + Parameters = @{ Identity = $Domain.Domain; Enabled = $true } } } } if ($null -ne $Request) { $BatchResults = New-ExoBulkRequest -tenantid $Tenant -cmdletArray @($Request) -useSystemMailbox $true } - $BatchResults | ForEach-Object { - if ($_.error) { + foreach ($Result in $BatchResults) { + if ($Result.error) { $ErrorCounter ++ - $ErrorMessage = Get-NormalizedError -Message $_.error + $ErrorMessage = Get-NormalizedError -Message $Result.error Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to set DKIM. Error: $ErrorMessage" -sev Error } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAddDMARCToMOERA.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAddDMARCToMOERA.ps1 index 288053375351..63fa43bc6ece 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAddDMARCToMOERA.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAddDMARCToMOERA.ps1 @@ -49,17 +49,17 @@ function Invoke-CIPPStandardAddDMARCToMOERA { try { $Domains = New-GraphGetRequest -scope 'https://admin.microsoft.com/.default' -TenantID $Tenant -Uri 'https://admin.microsoft.com/admin/api/Domains/List' | Where-Object -Property Name -Like '*.onmicrosoft.com' - $CurrentInfo = $Domains | ForEach-Object { + $CurrentInfo = foreach ($Domain in $Domains) { # Get current DNS records that matches _dmarc hostname and TXT type - $RecordsResponse = New-GraphGetRequest -scope 'https://admin.microsoft.com/.default' -TenantID $Tenant -Uri "https://admin.microsoft.com/admin/api/Domains/Records?domainName=$($_.Name)" + $RecordsResponse = New-GraphGetRequest -scope 'https://admin.microsoft.com/.default' -TenantID $Tenant -Uri "https://admin.microsoft.com/admin/api/Domains/Records?domainName=$($Domain.Name)" $AllRecords = $RecordsResponse | Select-Object -ExpandProperty DnsRecords $CurrentRecords = $AllRecords | Where-Object { $_.HostName -eq '_dmarc' -and $_.Type -eq 'TXT' } - Write-Information "Found $($CurrentRecords.count) DMARC records for domain $($_.Name)" + Write-Information "Found $($CurrentRecords.count) DMARC records for domain $($Domain.Name)" if ($CurrentRecords.count -eq 0) { #record not found, return a model with Match set to false [PSCustomObject]@{ - DomainName = $_.Name + DomainName = $Domain.Name Match = $false CurrentRecord = $null } @@ -76,13 +76,13 @@ function Invoke-CIPPStandardAddDMARCToMOERA { # Compare the current record with the expected record model if (!(Compare-Object -ReferenceObject $RecordModel -DifferenceObject $CurrentRecordModel -Property HostName, TtlValue, Type, Value)) { [PSCustomObject]@{ - DomainName = $_.Name + DomainName = $Domain.Name Match = $true CurrentRecord = $CurrentRecord } } else { [PSCustomObject]@{ - DomainName = $_.Name + DomainName = $Domain.Name Match = $false CurrentRecord = $CurrentRecord } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAutoAddProxy.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAutoAddProxy.ps1 index 2fb418ff552a..5549e79b9bc1 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAutoAddProxy.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAutoAddProxy.ps1 @@ -104,10 +104,10 @@ function Invoke-CIPPStandardAutoAddProxy { } } $BatchResults = New-ExoBulkRequest -tenantid $Tenant -cmdletArray @($bulkRequest) - $BatchResults | ForEach-Object { - if ($_.error) { - $ErrorMessage = Get-CippException -Exception $_.error - Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to apply proxy address to $($_.error.target) Error: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + foreach ($Result in $BatchResults) { + if ($Result.error) { + $ErrorMessage = Get-CippException -Exception $Result.error + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to apply proxy address to $($Result.error.target) Error: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage } } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardConditionalAccessTemplate.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardConditionalAccessTemplate.ps1 index 02def34b7681..07da79a1fc70 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardConditionalAccessTemplate.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardConditionalAccessTemplate.ps1 @@ -39,8 +39,8 @@ function Invoke-CIPPStandardConditionalAccessTemplate { $TestP2 = Test-CIPPStandardLicense -StandardName 'ConditionalAccessTemplate_p2' -TenantFilter $Tenant -RequiredCapabilities @('AAD_PREMIUM_P2') -SkipLog if ($TestResult -eq $false) { #writing to each item that the license is not present. - $settings.TemplateList | ForEach-Object { - Set-CIPPStandardsCompareField -FieldName "standards.ConditionalAccessTemplate.$($_.value)" -FieldValue 'This tenant does not have the required license for this standard.' -Tenant $Tenant + foreach ($Template in $settings.TemplateList) { + Set-CIPPStandardsCompareField -FieldName "standards.ConditionalAccessTemplate.$($Template.value)" -FieldValue 'This tenant does not have the required license for this standard.' -Tenant $Tenant } return $true } #we're done. diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardCustomBannedPasswordList.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardCustomBannedPasswordList.ps1 index 74d257a6387a..26a6cf65ae36 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardCustomBannedPasswordList.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardCustomBannedPasswordList.ps1 @@ -129,8 +129,8 @@ function Invoke-CIPPStandardCustomBannedPasswordList { Write-LogMessage -API 'Standards' -tenant $Tenant -message "Custom Banned Password List is already configured with $($CurrentBannedWords.Count) words." -sev Info } else { $AllBannedWords = [System.Collections.Generic.List[string]]::new() - $NewBannedWords | ForEach-Object { $AllBannedWords.Add($_) } - $CurrentBannedWords | ForEach-Object { $AllBannedWords.Add($_) } + foreach ($Word in $NewBannedWords) { $AllBannedWords.Add($Word) } + foreach ($Word in $CurrentBannedWords) { $AllBannedWords.Add($Word) } $AllBannedWords = $AllBannedWords | Select-Object -Unique -First 1000 | Where-Object { $_ -ne $null } $Body = @{ diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDelegateSentItems.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDelegateSentItems.ps1 index 177e070d6024..561f1420bbf1 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDelegateSentItems.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDelegateSentItems.ps1 @@ -64,19 +64,19 @@ function Invoke-CIPPStandardDelegateSentItems { if ($Settings.remediate -eq $true) { if ($Mailboxes) { try { - $Request = $Mailboxes | ForEach-Object { + $Request = foreach ($Mailbox in $Mailboxes) { @{ CmdletInput = @{ CmdletName = 'Set-Mailbox' - Parameters = @{Identity = $_.UserPrincipalName ; MessageCopyForSendOnBehalfEnabled = $true; MessageCopyForSentAsEnabled = $true } + Parameters = @{Identity = $Mailbox.UserPrincipalName ; MessageCopyForSendOnBehalfEnabled = $true; MessageCopyForSentAsEnabled = $true } } } } $BatchResults = New-ExoBulkRequest -tenantid $Tenant -cmdletArray @($Request) - $BatchResults | ForEach-Object { - if ($_.error) { - $ErrorMessage = Get-CippException -Exception $_.error - Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to apply Delegate Sent Items Style to $($_.error.target) Error: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + foreach ($Result in $BatchResults) { + if ($Result.error) { + $ErrorMessage = Get-CippException -Exception $Result.error + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to apply Delegate Sent Items Style to $($Result.error.target) Error: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage } } Write-LogMessage -API 'Standards' -tenant $Tenant -message "Delegate Sent Items Style applied for $($Mailboxes.Count - $BatchResults.Error.Count) mailboxes" -sev Info diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableBasicAuthSMTP.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableBasicAuthSMTP.ps1 index f11d03b8818d..2ce079633872 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableBasicAuthSMTP.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableBasicAuthSMTP.ps1 @@ -66,13 +66,13 @@ function Invoke-CIPPStandardDisableBasicAuthSMTP { } # Disable SMTP Basic Authentication for all users - $SMTPusers | ForEach-Object { + foreach ($User in $SMTPusers) { try { - New-ExoRequest -tenantid $Tenant -cmdlet 'Set-CASMailbox' -cmdParams @{ Identity = $_.Guid; SmtpClientAuthenticationDisabled = $null } -UseSystemMailbox $true - Write-LogMessage -API 'Standards' -tenant $tenant -message "Disabled SMTP Basic Authentication for $($_.DisplayName), $($_.PrimarySmtpAddress)" -sev Info + New-ExoRequest -tenantid $Tenant -cmdlet 'Set-CASMailbox' -cmdParams @{ Identity = $User.Guid; SmtpClientAuthenticationDisabled = $null } -UseSystemMailbox $true + Write-LogMessage -API 'Standards' -tenant $tenant -message "Disabled SMTP Basic Authentication for $($User.DisplayName), $($User.PrimarySmtpAddress)" -sev Info } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message - Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to disable SMTP Basic Authentication for $($_.DisplayName), $($_.PrimarySmtpAddress). Error: $ErrorMessage" -sev Error + Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to disable SMTP Basic Authentication for $($User.DisplayName), $($User.PrimarySmtpAddress). Error: $ErrorMessage" -sev Error } } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableExchangeOnlinePowerShell.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableExchangeOnlinePowerShell.ps1 index 6efd8ddc3677..62cce48dcb8f 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableExchangeOnlinePowerShell.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableExchangeOnlinePowerShell.ps1 @@ -57,21 +57,21 @@ function Invoke-CIPPStandardDisableExchangeOnlinePowerShell { if ($PowerShellEnabledCount -gt 0) { Write-LogMessage -API 'Standards' -tenant $Tenant -message "Started disabling Exchange Online PowerShell for $PowerShellEnabledCount users." -sev Info - $Request = $UsersWithPowerShell | ForEach-Object { + $Request = foreach ($User in $UsersWithPowerShell) { @{ CmdletInput = @{ CmdletName = 'Set-User' - Parameters = @{Identity = $_.Guid; RemotePowerShellEnabled = $false } + Parameters = @{Identity = $User.Guid; RemotePowerShellEnabled = $false } } } } $BatchResults = New-ExoBulkRequest -tenantid $tenant -cmdletArray @($Request) $SuccessCount = 0 - $BatchResults | ForEach-Object { - if ($_.error) { - $ErrorMessage = Get-NormalizedError -Message $_.error - Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to disable Exchange Online PowerShell for $($_.target). Error: $ErrorMessage" -sev Error + foreach ($Result in $BatchResults) { + if ($Result.error) { + $ErrorMessage = Get-NormalizedError -Message $Result.error + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to disable Exchange Online PowerShell for $($Result.target). Error: $ErrorMessage" -sev Error } else { $SuccessCount++ } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableExternalCalendarSharing.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableExternalCalendarSharing.ps1 index 69b36bf7eba0..cc6364f7101e 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableExternalCalendarSharing.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableExternalCalendarSharing.ps1 @@ -50,13 +50,13 @@ function Invoke-CIPPStandardDisableExternalCalendarSharing { if ($Settings.remediate -eq $true) { if ($CurrentInfo.Enabled) { - $CurrentInfo | ForEach-Object { + foreach ($Policy in $CurrentInfo) { try { - New-ExoRequest -tenantid $Tenant -cmdlet 'Set-SharingPolicy' -cmdParams @{ Identity = $_.Id ; Enabled = $false } -UseSystemMailbox $true - Write-LogMessage -API 'Standards' -tenant $tenant -message "Successfully disabled external calendar sharing for the policy $($_.Name)" -sev Info + New-ExoRequest -tenantid $Tenant -cmdlet 'Set-SharingPolicy' -cmdParams @{ Identity = $Policy.Id ; Enabled = $false } -UseSystemMailbox $true + Write-LogMessage -API 'Standards' -tenant $tenant -message "Successfully disabled external calendar sharing for the policy $($Policy.Name)" -sev Info } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message - Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to disable external calendar sharing for the policy $($_.Name). Error: $ErrorMessage" -sev Error + Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to disable external calendar sharing for the policy $($Policy.Name). Error: $ErrorMessage" -sev Error } } } else { diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableGuests.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableGuests.ps1 index ad00f5293547..7c5eb29a2f2b 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableGuests.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableGuests.ps1 @@ -37,7 +37,7 @@ function Invoke-CIPPStandardDisableGuests { if ($TestResult -eq $false) { #writing to each item that the license is not present. - $settings.TemplateList | ForEach-Object { + foreach ($Template in $settings.TemplateList) { Set-CIPPStandardsCompareField -FieldName 'standards.DisableGuests' -FieldValue 'This tenant does not have the required license for this standard.' -Tenant $Tenant } return $true @@ -57,8 +57,8 @@ function Invoke-CIPPStandardDisableGuests { return } - $RecentlyReactivatedUsers = (New-GraphGetRequest -uri "https://graph.microsoft.com/beta/auditLogs/directoryAudits?`$filter=activityDisplayName eq 'Enable account' and activityDateTime ge $AuditLookup" -scope 'https://graph.microsoft.com/.default' -tenantid $Tenant | - ForEach-Object { $_.targetResources[0].id } | Select-Object -Unique) + $AuditResults = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/auditLogs/directoryAudits?`$filter=activityDisplayName eq 'Enable account' and activityDateTime ge $AuditLookup" -scope 'https://graph.microsoft.com/.default' -tenantid $Tenant + $RecentlyReactivatedUsers = foreach ($AuditEntry in $AuditResults) { $AuditEntry.targetResources[0].id } | Select-Object -Unique $GraphRequest = $GraphRequest | Where-Object { -not ($RecentlyReactivatedUsers -contains $_.id) } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableOutlookAddins.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableOutlookAddins.ps1 index 05cf2ab4403d..5def2a8ca32d 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableOutlookAddins.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableOutlookAddins.ps1 @@ -62,8 +62,9 @@ function Invoke-CIPPStandardDisableOutlookAddins { foreach ($Role in $RolesToRemove) { try { - New-ExoRequest -tenantid $Tenant -cmdlet 'Get-ManagementRoleAssignment' -cmdParams @{ RoleAssignee = $CurrentInfo.Identity; Role = $Role } | ForEach-Object { - New-ExoRequest -tenantid $Tenant -cmdlet 'Remove-ManagementRoleAssignment' -cmdParams @{ Identity = $_.Guid; Confirm = $false } -UseSystemMailbox $true + $RoleAssignments = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-ManagementRoleAssignment' -cmdParams @{ RoleAssignee = $CurrentInfo.Identity; Role = $Role } + foreach ($Assignment in $RoleAssignments) { + New-ExoRequest -tenantid $Tenant -cmdlet 'Remove-ManagementRoleAssignment' -cmdParams @{ Identity = $Assignment.Guid; Confirm = $false } -UseSystemMailbox $true Write-LogMessage -API 'Standards' -tenant $tenant -message "Disabled Outlook add-in role: $Role" -sev Debug } } catch { diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableResourceMailbox.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableResourceMailbox.ps1 index d4d20c070e3a..f2be7ce4edf4 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableResourceMailbox.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableResourceMailbox.ps1 @@ -54,13 +54,13 @@ function Invoke-CIPPStandardDisableResourceMailbox { if ($Settings.remediate -eq $true) { if ($ResourceMailboxList) { - $ResourceMailboxList | ForEach-Object { + foreach ($Mailbox in $ResourceMailboxList) { try { - New-GraphPOSTRequest -uri "https://graph.microsoft.com/v1.0/users/$($_.ExternalDirectoryObjectId)" -type PATCH -body '{"accountEnabled":"false"}' -tenantid $Tenant - Write-LogMessage -API 'Standards' -tenant $Tenant -message "Entra account for $($_.RecipientTypeDetails), $($_.DisplayName), $($_.UserPrincipalName) disabled." -sev Info + New-GraphPOSTRequest -uri "https://graph.microsoft.com/v1.0/users/$($Mailbox.ExternalDirectoryObjectId)" -type PATCH -body '{"accountEnabled":"false"}' -tenantid $Tenant + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Entra account for $($Mailbox.RecipientTypeDetails), $($Mailbox.DisplayName), $($Mailbox.UserPrincipalName) disabled." -sev Info } catch { $ErrorMessage = Get-CippException -Exception $_ - Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to disable Entra account for $($_.RecipientTypeDetails), $($_.DisplayName), $($_.UserPrincipalName). Error: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to disable Entra account for $($Mailbox.RecipientTypeDetails), $($Mailbox.DisplayName), $($Mailbox.UserPrincipalName). Error: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage } } } else { diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSelfServiceLicenses.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSelfServiceLicenses.ps1 index 41dc31365e43..f06f7e0091f0 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSelfServiceLicenses.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSelfServiceLicenses.ps1 @@ -52,32 +52,31 @@ function Invoke-CIPPStandardDisableSelfServiceLicenses { $exclusions = $settings.Exclusions -split (',') } - $selfServiceItems | ForEach-Object { + foreach ($Item in $selfServiceItems) { $body = $null - if ($_.policyValue -eq 'Enabled' -AND ($_.productId -in $exclusions)) { + if ($Item.policyValue -eq 'Enabled' -AND ($Item.productId -in $exclusions)) { # Self service is enabled on product and productId is in exclusions, skip } - if ($_.policyValue -eq 'Disabled' -AND ($_.productId -in $exclusions)) { + if ($Item.policyValue -eq 'Disabled' -AND ($Item.productId -in $exclusions)) { # Self service is disabled on product and productId is in exclusions, enable $body = '{ "policyValue": "Enabled" }' } - if ($_.policyValue -eq 'Enabled' -AND ($_.productId -notin $exclusions)) { + if ($Item.policyValue -eq 'Enabled' -AND ($Item.productId -notin $exclusions)) { # Self service is enabled on product and productId is NOT in exclusions, disable $body = '{ "policyValue": "Disabled" }' } - if ($_.policyValue -eq 'Disabled' -AND ($_.productId -notin $exclusions)) { + if ($Item.policyValue -eq 'Disabled' -AND ($Item.productId -notin $exclusions)) { # Self service is disabled on product and productId is NOT in exclusions, skip } try { if ($body) { - $product = $_ - New-GraphPOSTRequest -scope 'aeb86249-8ea3-49e2-900b-54cc8e308f85/.default' -uri "https://licensing.m365.microsoft.com/v1.0/policies/AllowSelfServicePurchase/products/$($product.productId)" -tenantid $Tenant -body $body -type PUT + New-GraphPOSTRequest -scope 'aeb86249-8ea3-49e2-900b-54cc8e308f85/.default' -uri "https://licensing.m365.microsoft.com/v1.0/policies/AllowSelfServicePurchase/products/$($Item.productId)" -tenantid $Tenant -body $body -type PUT } } catch { - Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to set product status for $($product.productId) with body $($body) for reason: $($_.Exception.Message)" -sev Error - #Write-Error "Failed to disable product $($product.productName):$($_.Exception.Message)" + Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to set product status for $($Item.productId) with body $($body) for reason: $($_.Exception.Message)" -sev Error + #Write-Error "Failed to disable product $($Item.productName):$($_.Exception.Message)" } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSharedMailbox.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSharedMailbox.ps1 index 01b501aec319..9d13e4604ab6 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSharedMailbox.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSharedMailbox.ps1 @@ -48,10 +48,10 @@ function Invoke-CIPPStandardDisableSharedMailbox { if ($Settings.remediate -eq $true) { if ($SharedMailboxList) { - $SharedMailboxList | ForEach-Object { + foreach ($Mailbox in $SharedMailboxList) { try { - New-GraphPOSTRequest -uri "https://graph.microsoft.com/v1.0/users/$($_.ObjectKey)" -type PATCH -body '{"accountEnabled":"false"}' -tenantid $Tenant - Write-LogMessage -API 'Standards' -tenant $Tenant -message "Entra account for shared mailbox $($_.DisplayName) disabled." -sev Info + New-GraphPOSTRequest -uri "https://graph.microsoft.com/v1.0/users/$($Mailbox.ObjectKey)" -type PATCH -body '{"accountEnabled":"false"}' -tenantid $Tenant + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Entra account for shared mailbox $($Mailbox.DisplayName) disabled." -sev Info } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to disable Entra account for shared mailbox. Error: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableLitigationHold.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableLitigationHold.ps1 index 66bdbf9d537c..35f68f3350d9 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableLitigationHold.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableLitigationHold.ps1 @@ -51,11 +51,11 @@ function Invoke-CIPPStandardEnableLitigationHold { Write-LogMessage -API 'Standards' -tenant $Tenant -message 'Litigation Hold already enabled for all accounts' -sev Info } else { try { - $Request = $MailboxesNoLitHold | ForEach-Object { + $Request = foreach ($Mailbox in $MailboxesNoLitHold) { $params = @{ CmdletInput = @{ CmdletName = 'Set-Mailbox' - Parameters = @{ Identity = $_.UserPrincipalName; LitigationHoldEnabled = $true } + Parameters = @{ Identity = $Mailbox.UserPrincipalName; LitigationHoldEnabled = $true } } } if ($null -ne $Settings.days) { @@ -66,10 +66,10 @@ function Invoke-CIPPStandardEnableLitigationHold { $BatchResults = New-ExoBulkRequest -tenantid $Tenant -cmdletArray @($Request) - $BatchResults | ForEach-Object { - if ($_.error) { - $ErrorMessage = Get-NormalizedError -Message $_.error - Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to Enable Litigation Hold for $($_.Target). Error: $ErrorMessage" -sev Error + foreach ($Result in $BatchResults) { + if ($Result.error) { + $ErrorMessage = Get-NormalizedError -Message $Result.error + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to Enable Litigation Hold for $($Result.Target). Error: $ErrorMessage" -sev Error } } } catch { diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableMailboxAuditing.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableMailboxAuditing.ps1 index 6793ec94c69c..17eee3e1b987 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableMailboxAuditing.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableMailboxAuditing.ps1 @@ -96,20 +96,20 @@ function Invoke-CIPPStandardEnableMailboxAuditing { # Disable audit bypass for all mailboxes that have it enabled $BypassMailboxes = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-MailboxAuditBypassAssociation' -select 'GUID, AuditBypassEnabled, Name' -useSystemMailbox $true | Where-Object { $_.AuditBypassEnabled -eq $true } - $Request = $BypassMailboxes | ForEach-Object { + $Request = foreach ($Mailbox in $BypassMailboxes) { @{ CmdletInput = @{ CmdletName = 'Set-MailboxAuditBypassAssociation' - Parameters = @{Identity = $_.Guid; AuditBypassEnabled = $false } + Parameters = @{Identity = $Mailbox.Guid; AuditBypassEnabled = $false } } } } $BatchResults = New-ExoBulkRequest -tenantid $tenant -cmdletArray @($Request) - $BatchResults | ForEach-Object { - if ($_.error) { - $ErrorMessage = Get-NormalizedError -Message $_.error - Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to disable mailbox audit bypass for $($_.target). Error: $ErrorMessage" -sev Error + foreach ($Result in $BatchResults) { + if ($Result.error) { + $ErrorMessage = Get-NormalizedError -Message $Result.error + Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to disable mailbox audit bypass for $($Result.target). Error: $ErrorMessage" -sev Error } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableOnlineArchiving.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableOnlineArchiving.ps1 index 501493c84eb6..152f5847d3e0 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableOnlineArchiving.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableOnlineArchiving.ps1 @@ -39,8 +39,8 @@ function Invoke-CIPPStandardEnableOnlineArchiving { } #we're done. $MailboxPlans = @( 'ExchangeOnline', 'ExchangeOnlineEnterprise' ) - $MailboxesNoArchive = $MailboxPlans | ForEach-Object { - New-ExoRequest -tenantid $Tenant -cmdlet 'Get-Mailbox' -cmdParams @{ MailboxPlan = $_; Filter = 'ArchiveGuid -Eq "00000000-0000-0000-0000-000000000000" -AND RecipientTypeDetails -Eq "UserMailbox"' } + $MailboxesNoArchive = foreach ($Plan in $MailboxPlans) { + New-ExoRequest -tenantid $Tenant -cmdlet 'Get-Mailbox' -cmdParams @{ MailboxPlan = $Plan; Filter = 'ArchiveGuid -Eq "00000000-0000-0000-0000-000000000000" -AND RecipientTypeDetails -Eq "UserMailbox"' } } if ($Settings.remediate -eq $true) { @@ -49,20 +49,20 @@ function Invoke-CIPPStandardEnableOnlineArchiving { Write-LogMessage -API 'Standards' -tenant $Tenant -message 'Online Archiving already enabled for all accounts' -sev Info } else { try { - $Request = $MailboxesNoArchive | ForEach-Object { + $Request = foreach ($Mailbox in $MailboxesNoArchive) { @{ CmdletInput = @{ CmdletName = 'Enable-Mailbox' - Parameters = @{ Identity = $_.UserPrincipalName; Archive = $true } + Parameters = @{ Identity = $Mailbox.UserPrincipalName; Archive = $true } } } } $BatchResults = New-ExoBulkRequest -tenantid $tenant -cmdletArray @($Request) - $BatchResults | ForEach-Object { - if ($_.error) { - $ErrorMessage = Get-NormalizedError -Message $_.error - Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to Enable Online Archiving for $($_.Target). Error: $ErrorMessage" -sev Error + foreach ($Result in $BatchResults) { + if ($Result.error) { + $ErrorMessage = Get-NormalizedError -Message $Result.error + Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to Enable Online Archiving for $($Result.Target). Error: $ErrorMessage" -sev Error } } } catch { diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardIntuneTemplate.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardIntuneTemplate.ps1 index 444fc3891dcf..50dc65afc821 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardIntuneTemplate.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardIntuneTemplate.ps1 @@ -40,8 +40,8 @@ function Invoke-CIPPStandardIntuneTemplate { if ($TestResult -eq $false) { #writing to each item that the license is not present. - $settings.TemplateList | ForEach-Object { - Set-CIPPStandardsCompareField -FieldName "standards.IntuneTemplate.$($_.value)" -FieldValue 'This tenant does not have the required license for this standard.' -Tenant $Tenant + foreach ($Template in $settings.TemplateList) { + Set-CIPPStandardsCompareField -FieldName "standards.IntuneTemplate.$($Template.value)" -FieldValue 'This tenant does not have the required license for this standard.' -Tenant $Tenant } return $true } #we're done. diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMailboxRecipientLimits.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMailboxRecipientLimits.ps1 index c549c568c054..ae62b19cd6eb 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMailboxRecipientLimits.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMailboxRecipientLimits.ps1 @@ -81,11 +81,9 @@ function Invoke-CIPPStandardMailboxRecipientLimits { if ($null -ne $Mailboxes -and @($Mailboxes).Count -gt 0) { # Process mailboxes and categorize them based on their plan limits - $MailboxResults = @($Mailboxes) | ForEach-Object { - - $Mailbox = $_ + $MailboxResults = foreach ($Mailbox in @($Mailboxes)) { if ($Mailbox.UserPrincipalName -like 'DiscoverySearchMailbox*' -or $Mailbox.UserPrincipalName -like 'SystemMailbox*') { - return + continue } # Safe hashtable lookup - check if MailboxPlanId exists and is not null $Plan = $null @@ -127,12 +125,13 @@ function Invoke-CIPPStandardMailboxRecipientLimits { # Separate mailboxes into their respective categories only if we have results $MailboxesToUpdate = $MailboxResults | Where-Object { $_.Type -eq 'ToUpdate' } | Select-Object -ExpandProperty Mailbox - $MailboxesWithPlanIssues = $MailboxResults | Where-Object { $_.Type -eq 'PlanIssue' } | ForEach-Object { + $PlanIssueResults = $MailboxResults | Where-Object { $_.Type -eq 'PlanIssue' } + $MailboxesWithPlanIssues = foreach ($Issue in $PlanIssueResults) { [PSCustomObject]@{ - Identity = $_.Mailbox.Identity - CurrentLimit = $_.CurrentLimit - PlanLimit = $_.PlanLimit - PlanName = $_.PlanName + Identity = $Issue.Mailbox.Identity + CurrentLimit = $Issue.CurrentLimit + PlanLimit = $Issue.PlanLimit + PlanName = $Issue.PlanName } } } @@ -149,12 +148,12 @@ function Invoke-CIPPStandardMailboxRecipientLimits { if ($MailboxesToUpdate.Count -gt 0) { try { # Create detailed log data for audit trail - $MailboxChanges = $MailboxesToUpdate | ForEach-Object { - $CurrentLimit = if ($_.RecipientLimits -eq 'Unlimited') { 'Unlimited' } else { $_.RecipientLimits } + $MailboxChanges = foreach ($Mailbox in $MailboxesToUpdate) { + $CurrentLimit = if ($Mailbox.RecipientLimits -eq 'Unlimited') { 'Unlimited' } else { $Mailbox.RecipientLimits } @{ - Identity = $_.Identity - DisplayName = $_.DisplayName - PrimarySmtpAddress = $_.PrimarySmtpAddress + Identity = $Mailbox.Identity + DisplayName = $Mailbox.DisplayName + PrimarySmtpAddress = $Mailbox.PrimarySmtpAddress CurrentLimit = $CurrentLimit NewLimit = $Settings.RecipientLimit } @@ -163,12 +162,12 @@ function Invoke-CIPPStandardMailboxRecipientLimits { Write-LogMessage -API 'Standards' -tenant $Tenant -message "Updating recipient limits to $($Settings.RecipientLimit) for $($MailboxesToUpdate.Count) mailboxes" -sev Info -LogData $MailboxChanges # Create batch requests for mailbox updates - $UpdateRequests = $MailboxesToUpdate | ForEach-Object { + $UpdateRequests = foreach ($Mailbox in $MailboxesToUpdate) { @{ CmdletInput = @{ CmdletName = 'Set-Mailbox' Parameters = @{ - Identity = $_.Identity + Identity = $Mailbox.Identity RecipientLimits = $Settings.RecipientLimit } } @@ -204,12 +203,12 @@ function Invoke-CIPPStandardMailboxRecipientLimits { # Add mailboxes that need updating if ($MailboxesToUpdate.Count -gt 0) { - $AlertData.MailboxesToUpdate = $MailboxesToUpdate | ForEach-Object { - $CurrentLimit = if ($_.RecipientLimits -eq 'Unlimited') { 'Unlimited' } else { $_.RecipientLimits } + $AlertData.MailboxesToUpdate = foreach ($Mailbox in $MailboxesToUpdate) { + $CurrentLimit = if ($Mailbox.RecipientLimits -eq 'Unlimited') { 'Unlimited' } else { $Mailbox.RecipientLimits } @{ - Identity = $_.Identity - DisplayName = $_.DisplayName - PrimarySmtpAddress = $_.PrimarySmtpAddress + Identity = $Mailbox.Identity + DisplayName = $Mailbox.DisplayName + PrimarySmtpAddress = $Mailbox.PrimarySmtpAddress CurrentLimit = $CurrentLimit RequiredLimit = $Settings.RecipientLimit } @@ -222,12 +221,12 @@ function Invoke-CIPPStandardMailboxRecipientLimits { # Add mailboxes with plan issues if ($MailboxesWithPlanIssues.Count -gt 0) { - $AlertData.MailboxesWithPlanIssues = $MailboxesWithPlanIssues | ForEach-Object { + $AlertData.MailboxesWithPlanIssues = foreach ($Issue in $MailboxesWithPlanIssues) { @{ - Identity = $_.Identity - CurrentLimit = $_.CurrentLimit - PlanLimit = $_.PlanLimit - PlanName = $_.PlanName + Identity = $Issue.Identity + CurrentLimit = $Issue.CurrentLimit + PlanLimit = $Issue.PlanLimit + PlanName = $Issue.PlanName RequestedLimit = $Settings.RecipientLimit } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardOauthConsentLowSec.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardOauthConsentLowSec.ps1 index 2ba180c1a0ca..2bce9422a74b 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardOauthConsentLowSec.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardOauthConsentLowSec.ps1 @@ -76,19 +76,19 @@ function Invoke-CIPPStandardOauthConsentLowSec { Write-LogMessage -API 'Standards' -tenant $tenant -message 'All permissions for Application Consent already assigned.' -sev Info } else { try { - $missingPermissions | ForEach-Object { + foreach ($Permission in $missingPermissions) { $GraphParam = @{ tenantid = $tenant Uri = "https://graph.microsoft.com/beta/servicePrincipals(appId='00000003-0000-0000-c000-000000000000')/delegatedPermissionClassifications" Type = 'POST' Body = @{ - permissionName = $_ + permissionName = $Permission classification = 'low' } | ConvertTo-Json ContentType = 'application/json' } $null = New-GraphPostRequest @GraphParam - Write-LogMessage -API 'Standards' -tenant $tenant -message "Permission $_ has been added to low Application Consent" -sev Info + Write-LogMessage -API 'Standards' -tenant $tenant -message "Permission $Permission has been added to low Application Consent" -sev Info } } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPasswordExpireDisabled.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPasswordExpireDisabled.ps1 index f207a6b69557..2caaa9e00f7f 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPasswordExpireDisabled.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPasswordExpireDisabled.ps1 @@ -49,18 +49,18 @@ function Invoke-CIPPStandardPasswordExpireDisabled { if ($Settings.remediate -eq $true) { if ($DomainsWithoutPassExpire) { - $DomainsWithoutPassExpire | ForEach-Object { + foreach ($Domain in $DomainsWithoutPassExpire) { try { - if ( $null -eq $_.passwordNotificationWindowInDays ) { + if ( $null -eq $Domain.passwordNotificationWindowInDays ) { $Body = '{"passwordValidityPeriodInDays": 2147483647, "passwordNotificationWindowInDays": 14 }' } else { $Body = '{"passwordValidityPeriodInDays": 2147483647 }' } - New-GraphPostRequest -type Patch -tenantid $Tenant -uri "https://graph.microsoft.com/v1.0/domains/$($_.id)" -body $Body - Write-LogMessage -API 'Standards' -tenant $tenant -message "Disabled Password Expiration for $($_.id)." -sev Info + New-GraphPostRequest -type Patch -tenantid $Tenant -uri "https://graph.microsoft.com/v1.0/domains/$($Domain.id)" -body $Body + Write-LogMessage -API 'Standards' -tenant $tenant -message "Disabled Password Expiration for $($Domain.id)." -sev Info } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message - Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to disable Password Expiration for $($_.id). Error: $ErrorMessage" -sev Error + Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to disable Password Expiration for $($Domain.id). Error: $ErrorMessage" -sev Error } } } else { diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardRotateDKIM.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardRotateDKIM.ps1 index 8b148dfc92dc..11759ab03809 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardRotateDKIM.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardRotateDKIM.ps1 @@ -50,10 +50,10 @@ function Invoke-CIPPStandardRotateDKIM { if ($Settings.remediate -eq $true) { if ($DKIM) { - $DKIM | ForEach-Object { + foreach ($DkimConfig in $DKIM) { try { - (New-ExoRequest -tenantid $tenant -cmdlet 'Rotate-DkimSigningConfig' -cmdParams @{ KeySize = 2048; Identity = $_.Identity } -useSystemMailbox $true) - Write-LogMessage -API 'Standards' -tenant $tenant -message "Rotated DKIM for $($_.Identity)" -sev Info + (New-ExoRequest -tenantid $tenant -cmdlet 'Rotate-DkimSigningConfig' -cmdParams @{ KeySize = 2048; Identity = $DkimConfig.Identity } -useSystemMailbox $true) + Write-LogMessage -API 'Standards' -tenant $tenant -message "Rotated DKIM for $($DkimConfig.Identity)" -sev Info } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to rotate DKIM Error: $ErrorMessage" -sev Error diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeSendersDisable.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeSendersDisable.ps1 index 6196df255398..cd172bd96070 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeSendersDisable.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeSendersDisable.ps1 @@ -42,12 +42,12 @@ function Invoke-CIPPStandardSafeSendersDisable { if ($Settings.remediate -eq $true) { try { $Mailboxes = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-Mailbox' -select 'UserPrincipalName' - $Request = $Mailboxes | ForEach-Object { + $Request = foreach ($Mailbox in $Mailboxes) { @{ CmdletInput = @{ CmdletName = 'Set-MailboxJunkEmailConfiguration' Parameters = @{ - Identity = $_.UserPrincipalName + Identity = $Mailbox.UserPrincipalName TrustedRecipientsAndDomains = $null } } @@ -55,10 +55,10 @@ function Invoke-CIPPStandardSafeSendersDisable { } $BatchResults = New-ExoBulkRequest -tenantid $tenant -cmdletArray @($Request) - $BatchResults | ForEach-Object { - if ($_.error) { - $ErrorMessage = Get-NormalizedError -Message $_.error - Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to Disable SafeSenders for $($_.target). Error: $ErrorMessage" -sev Error + foreach ($Result in $BatchResults) { + if ($Result.error) { + $ErrorMessage = Get-NormalizedError -Message $Result.error + Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to Disable SafeSenders for $($Result.target). Error: $ErrorMessage" -sev Error } } Write-LogMessage -API 'Standards' -tenant $tenant -message 'Safe Senders disabled' -sev Info diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardcalDefault.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardcalDefault.ps1 index ed12a9730c35..e039625d35ec 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardcalDefault.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardcalDefault.ps1 @@ -67,36 +67,35 @@ function Invoke-CIPPStandardcalDefault { $SuccessCounter = if ($startIndex -eq 0) { 0 } else { [int64]$LastRun.currentSuccessCount } $processedMailboxes = $startIndex $Mailboxes = $Mailboxes[$startIndex..($TotalMailboxes - 1)] - $Mailboxes | ForEach-Object { - $Mailbox = $_ + foreach ($Mailbox in $Mailboxes) { try { - New-ExoRequest -tenantid $Tenant -cmdlet 'Get-MailboxFolderStatistics' -cmdParams @{identity = $Mailbox.UserPrincipalName; FolderScope = 'Calendar' } -Anchor $Mailbox.UserPrincipalName | Where-Object { $_.FolderType -eq 'Calendar' } | - ForEach-Object { - try { - New-ExoRequest -tenantid $Tenant -cmdlet 'Set-MailboxFolderPermission' -cmdParams @{Identity = "$($Mailbox.UserPrincipalName):$($_.FolderId)"; User = 'Default'; AccessRights = $permissionLevel } -Anchor $Mailbox.UserPrincipalName - Write-LogMessage -API 'Standards' -tenant $Tenant -message "Set default folder permission for $($Mailbox.UserPrincipalName):\$($_.Name) to $permissionLevel" -sev Debug - $SuccessCounter++ - } catch { - $ErrorMessage = Get-CippException -Exception $_ - Write-LogMessage -API 'Standards' -tenant $Tenant -message "Could not set default calendar permissions for $($Mailbox.UserPrincipalName). Error: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - } + $CalendarFolders = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-MailboxFolderStatistics' -cmdParams @{identity = $Mailbox.UserPrincipalName; FolderScope = 'Calendar' } -Anchor $Mailbox.UserPrincipalName | Where-Object { $_.FolderType -eq 'Calendar' } + foreach ($Folder in $CalendarFolders) { + try { + New-ExoRequest -tenantid $Tenant -cmdlet 'Set-MailboxFolderPermission' -cmdParams @{Identity = "$($Mailbox.UserPrincipalName):$($Folder.FolderId)"; User = 'Default'; AccessRights = $permissionLevel } -Anchor $Mailbox.UserPrincipalName + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Set default folder permission for $($Mailbox.UserPrincipalName):\$($Folder.Name) to $permissionLevel" -sev Debug + $SuccessCounter++ + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Could not set default calendar permissions for $($Mailbox.UserPrincipalName). Error: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage } - } catch { - $ErrorMessage = Get-CippException -Exception $_ - Write-LogMessage -API 'Standards' -tenant $Tenant -message "Could not set default calendar permissions for $($Mailbox.UserPrincipalName). Error: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage } - $processedMailboxes++ - if ($processedMailboxes % 25 -eq 0) { - $LastRun = @{ - RowKey = 'calDefaults' - PartitionKey = $Tenant - totalMailboxes = $TotalMailboxes - processedMailboxes = $processedMailboxes - currentSuccessCount = $SuccessCounter - } - Add-CIPPAzDataTableEntity @LastRunTable -Entity $LastRun -Force + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Could not set default calendar permissions for $($Mailbox.UserPrincipalName). Error: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + } + $processedMailboxes++ + if ($processedMailboxes % 25 -eq 0) { + $LastRun = @{ + RowKey = 'calDefaults' + PartitionKey = $Tenant + totalMailboxes = $TotalMailboxes + processedMailboxes = $processedMailboxes + currentSuccessCount = $SuccessCounter } + Add-CIPPAzDataTableEntity @LastRunTable -Entity $LastRun -Force } + } $LastRun = @{ RowKey = 'calDefaults' From 88eddb970aa97c02787298fe36025326a91fad4e Mon Sep 17 00:00:00 2001 From: rvdwegen Date: Wed, 28 Jan 2026 11:20:42 +0100 Subject: [PATCH 272/503] Replace foreach New-ExoRequest with New-ExoBulkRequest --- ...nvoke-CIPPStandardDisableBasicAuthSMTP.ps1 | 28 +++++++++++++------ ...StandardDisableExternalCalendarSharing.ps1 | 24 +++++++++++----- 2 files changed, 37 insertions(+), 15 deletions(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableBasicAuthSMTP.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableBasicAuthSMTP.ps1 index 2ce079633872..6c54c0b78c1b 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableBasicAuthSMTP.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableBasicAuthSMTP.ps1 @@ -65,14 +65,26 @@ function Invoke-CIPPStandardDisableBasicAuthSMTP { Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to disable SMTP Basic Authentication. Error: $ErrorMessage" -sev Error } - # Disable SMTP Basic Authentication for all users - foreach ($User in $SMTPusers) { - try { - New-ExoRequest -tenantid $Tenant -cmdlet 'Set-CASMailbox' -cmdParams @{ Identity = $User.Guid; SmtpClientAuthenticationDisabled = $null } -UseSystemMailbox $true - Write-LogMessage -API 'Standards' -tenant $tenant -message "Disabled SMTP Basic Authentication for $($User.DisplayName), $($User.PrimarySmtpAddress)" -sev Info - } catch { - $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message - Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to disable SMTP Basic Authentication for $($User.DisplayName), $($User.PrimarySmtpAddress). Error: $ErrorMessage" -sev Error + # Disable SMTP Basic Authentication for all users using bulk request + if ($SMTPusers.Count -gt 0) { + $BulkRequest = foreach ($User in $SMTPusers) { + @{ + CmdletInput = @{ + CmdletName = 'Set-CASMailbox' + Parameters = @{ Identity = $User.Guid; SmtpClientAuthenticationDisabled = $null } + } + } + } + $BatchResults = New-ExoBulkRequest -tenantid $Tenant -cmdletArray @($BulkRequest) -useSystemMailbox $true + foreach ($Result in $BatchResults) { + if ($Result.error) { + $ErrorMessage = Get-NormalizedError -Message $Result.error + Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to disable SMTP Basic Authentication for $($Result.target). Error: $ErrorMessage" -sev Error + } + } + $SuccessCount = ($BatchResults | Where-Object { -not $_.error }).Count + if ($SuccessCount -gt 0) { + Write-LogMessage -API 'Standards' -tenant $tenant -message "Disabled SMTP Basic Authentication for $SuccessCount users" -sev Info } } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableExternalCalendarSharing.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableExternalCalendarSharing.ps1 index cc6364f7101e..480bf6a37c6c 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableExternalCalendarSharing.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableExternalCalendarSharing.ps1 @@ -50,15 +50,25 @@ function Invoke-CIPPStandardDisableExternalCalendarSharing { if ($Settings.remediate -eq $true) { if ($CurrentInfo.Enabled) { - foreach ($Policy in $CurrentInfo) { - try { - New-ExoRequest -tenantid $Tenant -cmdlet 'Set-SharingPolicy' -cmdParams @{ Identity = $Policy.Id ; Enabled = $false } -UseSystemMailbox $true - Write-LogMessage -API 'Standards' -tenant $tenant -message "Successfully disabled external calendar sharing for the policy $($Policy.Name)" -sev Info - } catch { - $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message - Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to disable external calendar sharing for the policy $($Policy.Name). Error: $ErrorMessage" -sev Error + $BulkRequest = foreach ($Policy in $CurrentInfo) { + @{ + CmdletInput = @{ + CmdletName = 'Set-SharingPolicy' + Parameters = @{ Identity = $Policy.Id ; Enabled = $false } + } } } + $BatchResults = New-ExoBulkRequest -tenantid $Tenant -cmdletArray @($BulkRequest) -useSystemMailbox $true + foreach ($Result in $BatchResults) { + if ($Result.error) { + $ErrorMessage = Get-NormalizedError -Message $Result.error + Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to disable external calendar sharing. Error: $ErrorMessage" -sev Error + } + } + $SuccessCount = ($BatchResults | Where-Object { -not $_.error }).Count + if ($SuccessCount -gt 0) { + Write-LogMessage -API 'Standards' -tenant $tenant -message "Successfully disabled external calendar sharing for $SuccessCount policies" -sev Info + } } else { Write-LogMessage -API 'Standards' -tenant $tenant -message 'External calendar sharing is already disabled' -sev Info From 7eab604fcdb45b66463272ff86aaca9d59994aab Mon Sep 17 00:00:00 2001 From: rvdwegen Date: Wed, 28 Jan 2026 11:28:05 +0100 Subject: [PATCH 273/503] Fix BannedWordsList count logic --- .../Invoke-CIPPStandardCustomBannedPasswordList.ps1 | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardCustomBannedPasswordList.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardCustomBannedPasswordList.ps1 index 26a6cf65ae36..6f823787f04f 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardCustomBannedPasswordList.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardCustomBannedPasswordList.ps1 @@ -42,12 +42,6 @@ function Invoke-CIPPStandardCustomBannedPasswordList { # Split input by commas, newlines, or semicolons and clean up $BannedWordsList = $BannedWordsInput -split '[,;\r\n]+' | ForEach-Object { ($_.Trim()) } | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -Unique - # Validate word count - if ($BannedWordsList.Count -gt 1000) { - Write-LogMessage -API 'Standards' -tenant $tenant -message "CustomBannedPasswordList: Too many banned words provided ($($BannedWordsList.Count)). Maximum allowed is 1000." -sev Error - return - } - # Validate word length (4-16 characters), remove duplicates and invalid words $ValidBannedWordsList = [System.Collections.Generic.List[string]]::new() $InvalidWords = [System.Collections.Generic.List[string]]::new() @@ -66,6 +60,12 @@ function Invoke-CIPPStandardCustomBannedPasswordList { Write-LogMessage -API 'Standards' -tenant $tenant -message "CustomBannedPasswordList: Invalid words found in input (must be 4-16 characters). Please remove the following words: $($InvalidWords -join ', ')" -sev Warning } + # Validate word count after filtering + if ($BannedWordsList.Count -gt 1000) { + Write-LogMessage -API 'Standards' -tenant $tenant -message "CustomBannedPasswordList: Too many valid banned words ($($BannedWordsList.Count)). Maximum allowed is 1000." -sev Error + return + } + # Get existing directory settings for password rules try { $ExistingSettings = New-GraphGetRequest -Uri 'https://graph.microsoft.com/beta/settings' -tenantid $Tenant | Where-Object { $_.templateId -eq $PasswordRuleTemplateId } From f96738887d9545471ba9928a1e6de4b5c5f15487 Mon Sep 17 00:00:00 2001 From: rvdwegen Date: Wed, 28 Jan 2026 11:41:51 +0100 Subject: [PATCH 274/503] Fix duplicate exo data retrieval in Invoke-CIPPStandardMalwareFilterPolicy --- ...Invoke-CIPPStandardMalwareFilterPolicy.ps1 | 34 ++++++++++++------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMalwareFilterPolicy.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMalwareFilterPolicy.ps1 index 1c91a09eca28..f7c72a9f8675 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMalwareFilterPolicy.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMalwareFilterPolicy.ps1 @@ -49,10 +49,26 @@ function Invoke-CIPPStandardMalwareFilterPolicy { return $true } #we're done. + try { + $AllMalwareFilterPolicies = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-MalwareFilterPolicy' + } catch { + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the MalwareFilterPolicy state for $Tenant. Error: $ErrorMessage" -Sev Error + return + } + + try { + $AllMalwareFilterRules = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-MalwareFilterRule' + } catch { + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the Get-MalwareFilterRule state for $Tenant. Error: $ErrorMessage" -Sev Error + return + } + # Use custom name if provided, otherwise use default for backward compatibility $PolicyName = if ($Settings.name) { $Settings.name } else { 'CIPP Default Malware Policy' } $PolicyList = @($PolicyName, 'CIPP Default Malware Policy', 'Default Malware Policy') - $ExistingPolicy = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-MalwareFilterPolicy' | Where-Object -Property Name -In $PolicyList | Select-Object -First 1 + $ExistingPolicy = $AllMalwareFilterPolicies | Where-Object -Property Name -In $PolicyList | Select-Object -First 1 if ($null -eq $ExistingPolicy.Name) { # No existing policy - use the configured/default name $PolicyName = if ($Settings.name) { $Settings.name } else { 'CIPP Default Malware Policy' } @@ -63,7 +79,7 @@ function Invoke-CIPPStandardMalwareFilterPolicy { # Derive rule name from policy name, but check for old names for backward compatibility $DesiredRuleName = "$PolicyName Rule" $RuleList = @($DesiredRuleName, 'CIPP Default Malware Rule', 'CIPP Default Malware Policy') - $ExistingRule = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-MalwareFilterRule' | Where-Object -Property Name -In $RuleList | Select-Object -First 1 + $ExistingRule = $AllMalwareFilterRules | Where-Object -Property Name -In $RuleList | Select-Object -First 1 if ($null -eq $ExistingRule.Name) { # No existing rule - use the derived name $RuleName = $DesiredRuleName @@ -72,15 +88,9 @@ function Invoke-CIPPStandardMalwareFilterPolicy { $RuleName = $ExistingRule.Name } - try { - $CurrentState = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-MalwareFilterPolicy' | - Where-Object -Property Name -EQ $PolicyName | - Select-Object Name, EnableFileFilter, FileTypeAction, FileTypes, ZapEnabled, QuarantineTag, EnableInternalSenderAdminNotifications, InternalSenderAdminAddress, EnableExternalSenderAdminNotifications, ExternalSenderAdminAddress - } catch { - $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message - Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the MalwareFilterPolicy state for $Tenant. Error: $ErrorMessage" -Sev Error - return - } + $CurrentState = $AllMalwareFilterPolicies | + Where-Object -Property Name -EQ $PolicyName | + Select-Object Name, EnableFileFilter, FileTypeAction, FileTypes, ZapEnabled, QuarantineTag, EnableInternalSenderAdminNotifications, InternalSenderAdminAddress, EnableExternalSenderAdminNotifications, ExternalSenderAdminAddress $DefaultFileTypes = @('ace', 'ani', 'apk', 'app', 'appx', 'arj', 'bat', 'cab', 'cmd', 'com', 'deb', 'dex', 'dll', 'docm', 'elf', 'exe', 'hta', 'img', 'iso', 'jar', 'jnlp', 'kext', 'lha', 'lib', 'library', 'lnk', 'lzh', 'macho', 'msc', 'msi', 'msix', 'msp', 'mst', 'pif', 'ppa', 'ppam', 'reg', 'rev', 'scf', 'scr', 'sct', 'sys', 'uif', 'vb', 'vbe', 'vbs', 'vxd', 'wsc', 'wsf', 'wsh', 'xll', 'xz', 'z') @@ -105,7 +115,7 @@ function Invoke-CIPPStandardMalwareFilterPolicy { $AcceptedDomains = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-AcceptedDomain' - $RuleState = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-MalwareFilterRule' | + $RuleState = $AllMalwareFilterRules | Where-Object -Property Name -EQ $RuleName | Select-Object Name, MalwareFilterPolicy, Priority, RecipientDomainIs From 0fe392dc060f276a72f73222540a19abb327116c Mon Sep 17 00:00:00 2001 From: rvdwegen Date: Wed, 28 Jan 2026 11:54:37 +0100 Subject: [PATCH 275/503] Fix duplicate exo data retrieval in Invoke-CIPPStandardSafeAttachmentPolicy --- ...nvoke-CIPPStandardSafeAttachmentPolicy.ps1 | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeAttachmentPolicy.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeAttachmentPolicy.ps1 index 4e8ccfe8d6ba..94664b645409 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeAttachmentPolicy.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeAttachmentPolicy.ps1 @@ -50,10 +50,19 @@ function Invoke-CIPPStandardSafeAttachmentPolicy { $MDOLicensed = $ServicePlans -contains 'ATP_ENTERPRISE' if ($MDOLicensed) { + # Cache all Safe Attachment Policies to avoid duplicate API calls + try { + $AllSafeAttachmentPolicies = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-SafeAttachmentPolicy' + } catch { + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the SafeAttachmentPolicy state for $Tenant. Error: $ErrorMessage" -Sev Error + return + } + # Use custom name if provided, otherwise use default for backward compatibility $PolicyName = if ($Settings.name) { $Settings.name } else { 'CIPP Default Safe Attachment Policy' } $PolicyList = @($PolicyName, 'CIPP Default Safe Attachment Policy', 'Default Safe Attachment Policy') - $ExistingPolicy = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-SafeAttachmentPolicy' | Where-Object -Property Name -In $PolicyList | Select-Object -First 1 + $ExistingPolicy = $AllSafeAttachmentPolicies | Where-Object -Property Name -In $PolicyList | Select-Object -First 1 if ($null -eq $ExistingPolicy.Name) { # No existing policy - use the configured/default name $PolicyName = if ($Settings.name) { $Settings.name } else { 'CIPP Default Safe Attachment Policy' } @@ -73,15 +82,9 @@ function Invoke-CIPPStandardSafeAttachmentPolicy { $RuleName = $ExistingRule.Name } - try { - $CurrentState = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-SafeAttachmentPolicy' | + $CurrentState = $AllSafeAttachmentPolicies | Where-Object -Property Name -EQ $PolicyName | Select-Object Name, Enable, Action, QuarantineTag, Redirect, RedirectAddress - } catch { - $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message - Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the SafeAttachmentPolicy state for $Tenant. Error: $ErrorMessage" -Sev Error - return - } $StateIsCorrect = ($CurrentState.Name -eq $PolicyName) -and ($CurrentState.Enable -eq $true) -and From d14d72df1d347c2f8d06a8a1e33f3d9451395e22 Mon Sep 17 00:00:00 2001 From: rvdwegen Date: Wed, 28 Jan 2026 12:10:07 +0100 Subject: [PATCH 276/503] Fix duplicate exo data retrieval for Invoke-CIPPStandardSafeLinksPolicy --- .../Invoke-CIPPStandardSafeLinksPolicy.ps1 | 34 ++++++++++++------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeLinksPolicy.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeLinksPolicy.ps1 index 4293c173c9bd..fc74053c34b7 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeLinksPolicy.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeLinksPolicy.ps1 @@ -49,10 +49,26 @@ function Invoke-CIPPStandardSafeLinksPolicy { $MDOLicensed = $ServicePlans -contains 'ATP_ENTERPRISE' if ($MDOLicensed) { + # Single data retrieval calls with error handling + try { + $AllSafeLinksPolicy = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-SafeLinksPolicy' + } catch { + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the SafeLinksPolicy state for $Tenant. Error: $ErrorMessage" -Sev Error + return + } + try { + $AllSafeLinksRule = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-SafeLinksRule' + } catch { + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the SafeLinksRule state for $Tenant. Error: $ErrorMessage" -Sev Error + return + } + # Use custom name if provided, otherwise use default for backward compatibility $PolicyName = if ($Settings.name) { $Settings.name } else { 'CIPP Default SafeLinks Policy' } $PolicyList = @($PolicyName, 'CIPP Default SafeLinks Policy', 'Default SafeLinks Policy') - $ExistingPolicy = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-SafeLinksPolicy' | Where-Object -Property Name -In $PolicyList | Select-Object -First 1 + $ExistingPolicy = $AllSafeLinksPolicy | Where-Object -Property Name -In $PolicyList | Select-Object -First 1 if ($null -eq $ExistingPolicy.Name) { # No existing policy - use the configured/default name $PolicyName = if ($Settings.name) { $Settings.name } else { 'CIPP Default SafeLinks Policy' } @@ -63,7 +79,7 @@ function Invoke-CIPPStandardSafeLinksPolicy { # Derive rule name from policy name, but check for old names for backward compatibility $DesiredRuleName = "$PolicyName Rule" $RuleList = @($DesiredRuleName, 'CIPP Default SafeLinks Rule', 'CIPP Default SafeLinks Policy') - $ExistingRule = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-SafeLinksRule' | Where-Object -Property Name -In $RuleList | Select-Object -First 1 + $ExistingRule = $AllSafeLinksRule | Where-Object -Property Name -In $RuleList | Select-Object -First 1 if ($null -eq $ExistingRule.Name) { # No existing rule - use the derived name $RuleName = $DesiredRuleName @@ -72,15 +88,9 @@ function Invoke-CIPPStandardSafeLinksPolicy { $RuleName = $ExistingRule.Name } - try { - $CurrentState = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-SafeLinksPolicy' | - Where-Object -Property Name -EQ $PolicyName | - Select-Object Name, EnableSafeLinksForEmail, EnableSafeLinksForTeams, EnableSafeLinksForOffice, TrackClicks, AllowClickThrough, ScanUrls, EnableForInternalSenders, DeliverMessageAfterScan, DisableUrlRewrite, EnableOrganizationBranding, DoNotRewriteUrls - } catch { - $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message - Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the SafeLinksPolicy state for $Tenant. Error: $ErrorMessage" -Sev Error - return - } + $CurrentState = $AllSafeLinksPolicy | + Where-Object -Property Name -EQ $PolicyName | + Select-Object Name, EnableSafeLinksForEmail, EnableSafeLinksForTeams, EnableSafeLinksForOffice, TrackClicks, AllowClickThrough, ScanUrls, EnableForInternalSenders, DeliverMessageAfterScan, DisableUrlRewrite, EnableOrganizationBranding, DoNotRewriteUrls $StateIsCorrect = ($CurrentState.Name -eq $PolicyName) -and ($CurrentState.EnableSafeLinksForEmail -eq $true) -and @@ -97,7 +107,7 @@ function Invoke-CIPPStandardSafeLinksPolicy { $AcceptedDomains = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-AcceptedDomain' - $RuleState = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-SafeLinksRule' | + $RuleState = $AllSafeLinksRule | Where-Object -Property Name -EQ $RuleName | Select-Object Name, SafeLinksPolicy, Priority, RecipientDomainIs From 18734482c362e6f650ad2bbd197d2089cbc92a3c Mon Sep 17 00:00:00 2001 From: rvdwegen Date: Wed, 28 Jan 2026 12:15:55 +0100 Subject: [PATCH 277/503] Fix duplicate exo data retrieval in Invoke-CIPPStandardAntiPhishPolicy --- .../Invoke-CIPPStandardAntiPhishPolicy.ps1 | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAntiPhishPolicy.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAntiPhishPolicy.ps1 index daeb2dfe9d27..059e7c1d3e69 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAntiPhishPolicy.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAntiPhishPolicy.ps1 @@ -64,6 +64,15 @@ function Invoke-CIPPStandardAntiPhishPolicy { $MDOLicensed = $ServicePlans -contains "ATP_ENTERPRISE" Write-Information "MDOLicensed: $MDOLicensed" + # Single data retrieval for Get-AntiPhishRule (used twice) with error handling + try { + $AllAntiPhishRule = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-AntiPhishRule' + } catch { + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the AntiPhishRule state for $Tenant. Error: $ErrorMessage" -Sev Error + return + } + # Use custom name if provided, otherwise use default for backward compatibility $PolicyName = if ($Settings.name) { $Settings.name } else { 'CIPP Default Anti-Phishing Policy' } $PolicyList = @($PolicyName, 'CIPP Default Anti-Phishing Policy','Default Anti-Phishing Policy') @@ -78,7 +87,7 @@ function Invoke-CIPPStandardAntiPhishPolicy { # Derive rule name from policy name, but check for old names for backward compatibility $DesiredRuleName = "$PolicyName Rule" $RuleList = @($DesiredRuleName, 'CIPP Default Anti-Phishing Rule','CIPP Default Anti-Phishing Policy') - $ExistingRule = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-AntiPhishRule' | Where-Object -Property Name -In $RuleList | Select-Object -First 1 + $ExistingRule = $AllAntiPhishRule | Where-Object -Property Name -In $RuleList | Select-Object -First 1 if ($null -eq $ExistingRule.Name) { # No existing rule - use the derived name $RuleName = $DesiredRuleName @@ -165,7 +174,7 @@ function Invoke-CIPPStandardAntiPhishPolicy { $AcceptedDomains = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-AcceptedDomain' - $RuleState = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-AntiPhishRule' | + $RuleState = $AllAntiPhishRule | Where-Object -Property Name -EQ $RuleName | Select-Object Name, AntiPhishPolicy, Priority, RecipientDomainIs From af0b19cf9a7be706b2b502f5f5669844242cc827 Mon Sep 17 00:00:00 2001 From: rvdwegen Date: Wed, 28 Jan 2026 12:18:59 +0100 Subject: [PATCH 278/503] Use cached capability data instead of making a new call --- .../Public/Standards/Invoke-CIPPStandardAntiPhishPolicy.ps1 | 5 ++--- .../Standards/Invoke-CIPPStandardSafeAttachmentPolicy.ps1 | 5 ++--- .../Public/Standards/Invoke-CIPPStandardSafeLinksPolicy.ps1 | 5 ++--- .../Standards/Invoke-CIPPStandardSafeLinksTemplatePolicy.ps1 | 5 ++--- 4 files changed, 8 insertions(+), 12 deletions(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAntiPhishPolicy.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAntiPhishPolicy.ps1 index 059e7c1d3e69..253c88731a29 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAntiPhishPolicy.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAntiPhishPolicy.ps1 @@ -59,9 +59,8 @@ function Invoke-CIPPStandardAntiPhishPolicy { } #we're done. ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'AntiPhishPolicy' - $ServicePlans = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/subscribedSkus?$select=servicePlans' -tenantid $Tenant - $ServicePlans = $ServicePlans.servicePlans.servicePlanName - $MDOLicensed = $ServicePlans -contains "ATP_ENTERPRISE" + $TenantCapabilities = Get-CIPPTenantCapabilities -TenantFilter $Tenant + $MDOLicensed = $TenantCapabilities.ATP_ENTERPRISE -eq $true Write-Information "MDOLicensed: $MDOLicensed" # Single data retrieval for Get-AntiPhishRule (used twice) with error handling diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeAttachmentPolicy.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeAttachmentPolicy.ps1 index 94664b645409..79c6d309ee85 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeAttachmentPolicy.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeAttachmentPolicy.ps1 @@ -45,9 +45,8 @@ function Invoke-CIPPStandardSafeAttachmentPolicy { return $true } #we're done. - $ServicePlans = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/subscribedSkus?$select=servicePlans' -tenantid $Tenant - $ServicePlans = $ServicePlans.servicePlans.servicePlanName - $MDOLicensed = $ServicePlans -contains 'ATP_ENTERPRISE' + $TenantCapabilities = Get-CIPPTenantCapabilities -TenantFilter $Tenant + $MDOLicensed = $TenantCapabilities.ATP_ENTERPRISE -eq $true if ($MDOLicensed) { # Cache all Safe Attachment Policies to avoid duplicate API calls diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeLinksPolicy.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeLinksPolicy.ps1 index fc74053c34b7..d4db33c46fd8 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeLinksPolicy.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeLinksPolicy.ps1 @@ -44,9 +44,8 @@ function Invoke-CIPPStandardSafeLinksPolicy { return $true } #we're done. - $ServicePlans = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/subscribedSkus?$select=servicePlans' -tenantid $Tenant - $ServicePlans = $ServicePlans.servicePlans.servicePlanName - $MDOLicensed = $ServicePlans -contains 'ATP_ENTERPRISE' + $TenantCapabilities = Get-CIPPTenantCapabilities -TenantFilter $Tenant + $MDOLicensed = $TenantCapabilities.ATP_ENTERPRISE -eq $true if ($MDOLicensed) { # Single data retrieval calls with error handling diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeLinksTemplatePolicy.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeLinksTemplatePolicy.ps1 index 7cf312e6d678..800bd87a62ef 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeLinksTemplatePolicy.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeLinksTemplatePolicy.ps1 @@ -68,9 +68,8 @@ function Invoke-CIPPStandardSafeLinksTemplatePolicy { function Test-MDOLicense { param($Tenant, $Settings) - $ServicePlans = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/subscribedSkus?$select=servicePlans' -tenantid $Tenant - $ServicePlans = $ServicePlans.servicePlans.servicePlanName - $MDOLicensed = $ServicePlans -contains 'ATP_ENTERPRISE' + $TenantCapabilities = Get-CIPPTenantCapabilities -TenantFilter $Tenant + $MDOLicensed = $TenantCapabilities.ATP_ENTERPRISE -eq $true if (-not $MDOLicensed) { $Message = 'Tenant does not have Microsoft Defender for Office 365 license' From 3dee3dc6795b7254586eb9f2245ddc85ec44747d Mon Sep 17 00:00:00 2001 From: rvdwegen Date: Wed, 28 Jan 2026 12:28:42 +0100 Subject: [PATCH 279/503] Remove redundant select-object usage --- .../Standards/Invoke-CIPPStandardQuarantineRequestAlert.ps1 | 3 +-- .../Invoke-CIPPStandardSPDisableLegacyWorkflows.ps1 | 3 +-- .../Invoke-CIPPStandardSharePointMassDeletionAlert.ps1 | 3 +-- .../Standards/Invoke-CIPPStandardSpamFilterPolicy.ps1 | 6 ++---- 4 files changed, 5 insertions(+), 10 deletions(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardQuarantineRequestAlert.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardQuarantineRequestAlert.ps1 index e82e2b5e1b26..2e4a10d64eda 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardQuarantineRequestAlert.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardQuarantineRequestAlert.ps1 @@ -40,8 +40,7 @@ function Invoke-CIPPStandardQuarantineRequestAlert { try { $CurrentState = New-ExoRequest -TenantId $Tenant -cmdlet 'Get-ProtectionAlert' -Compliance | - Where-Object { $_.Name -eq $PolicyName } | - Select-Object -Property * + Where-Object { $_.Name -eq $PolicyName } } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the QuarantineRequestAlert state for $Tenant. Error: $ErrorMessage" -Sev Error diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPDisableLegacyWorkflows.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPDisableLegacyWorkflows.ps1 index 92e648c9cf88..0ca4672a15bb 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPDisableLegacyWorkflows.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPDisableLegacyWorkflows.ps1 @@ -36,8 +36,7 @@ function Invoke-CIPPStandardSPDisableLegacyWorkflows { } #we're done. try { - $CurrentState = Get-CIPPSPOTenant -TenantFilter $Tenant | - Select-Object -Property * + $CurrentState = Get-CIPPSPOTenant -TenantFilter $Tenant } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the SPDisableLegacyWorkflows state for $Tenant. Error: $ErrorMessage" -Sev Error diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSharePointMassDeletionAlert.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSharePointMassDeletionAlert.ps1 index 6913b882617e..682d24b2c0c2 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSharePointMassDeletionAlert.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSharePointMassDeletionAlert.ps1 @@ -43,8 +43,7 @@ function Invoke-CIPPStandardSharePointMassDeletionAlert { try { $CurrentState = New-ExoRequest -TenantId $Tenant -cmdlet 'Get-ProtectionAlert' -Compliance | - Where-Object { $_.Name -eq $PolicyName } | - Select-Object -Property * + Where-Object { $_.Name -eq $PolicyName } } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the sharingCapability state for $Tenant. Error: $ErrorMessage" -Sev Error diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSpamFilterPolicy.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSpamFilterPolicy.ps1 index 4c76216889a7..37dd46c5c4a4 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSpamFilterPolicy.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSpamFilterPolicy.ps1 @@ -63,8 +63,7 @@ function Invoke-CIPPStandardSpamFilterPolicy { try { $CurrentState = New-ExoRequest -TenantId $Tenant -cmdlet 'Get-HostedContentFilterPolicy' | - Where-Object -Property Name -EQ $PolicyName | - Select-Object -Property * + 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 @@ -135,8 +134,7 @@ 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 | - Select-Object -Property * + Where-Object -Property Name -EQ $PolicyName $RuleStateIsCorrect = ($RuleState.Name -eq $PolicyName) -and ($RuleState.HostedContentFilterPolicy -eq $PolicyName) -and From 133df3f678d923b36401851680aebdb965d2f9d8 Mon Sep 17 00:00:00 2001 From: rvdwegen Date: Wed, 28 Jan 2026 12:43:17 +0100 Subject: [PATCH 280/503] Batch graph disable guests --- .../Invoke-CIPPStandardDisableGuests.ps1 | 39 ++++++++++++++----- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableGuests.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableGuests.ps1 index 7c5eb29a2f2b..dd7d018f5413 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableGuests.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableGuests.ps1 @@ -58,25 +58,46 @@ function Invoke-CIPPStandardDisableGuests { } $AuditResults = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/auditLogs/directoryAudits?`$filter=activityDisplayName eq 'Enable account' and activityDateTime ge $AuditLookup" -scope 'https://graph.microsoft.com/.default' -tenantid $Tenant - $RecentlyReactivatedUsers = foreach ($AuditEntry in $AuditResults) { $AuditEntry.targetResources[0].id } | Select-Object -Unique + $RecentlyReactivatedUsers = @(foreach ($AuditEntry in $AuditResults) { $AuditEntry.targetResources[0].id }) | Select-Object -Unique $GraphRequest = $GraphRequest | Where-Object { -not ($RecentlyReactivatedUsers -contains $_.id) } if ($Settings.remediate -eq $true) { if ($GraphRequest.Count -gt 0) { - foreach ($guest in $GraphRequest) { - try { - $null = New-GraphPostRequest -type Patch -tenantid $tenant -uri "https://graph.microsoft.com/beta/users/$($guest.id)" -body '{"accountEnabled":"false"}' - Write-LogMessage -API 'Standards' -tenant $tenant -message "Disabling guest $($guest.UserPrincipalName) ($($guest.id)). Last sign-in: $($guest.signInActivity.lastSuccessfulSignInDateTime)" -sev Info - } catch { - $ErrorMessage = Get-CippException -Exception $_ - Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to disable guest $($guest.UserPrincipalName) ($($guest.id)): $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + $int = 0 + $BulkRequests = foreach ($guest in $GraphRequest) { + @{ + id = $int++ + method = 'PATCH' + url = "users/$($guest.id)" + body = @{ accountEnabled = $false } + 'headers' = @{ + 'Content-Type' = 'application/json' + } } } + + try { + $BulkResults = New-GraphBulkRequest -tenantid $tenant -Requests @($BulkRequests) + + for ($i = 0; $i -lt $BulkResults.Count; $i++) { + $result = $BulkResults[$i] + $guest = $GraphRequest[$i] + + if ($result.status -eq 200 -or $result.status -eq 204) { + Write-LogMessage -API 'Standards' -tenant $tenant -message "Disabled guest $($guest.UserPrincipalName) ($($guest.id)). Last sign-in: $($guest.signInActivity.lastSuccessfulSignInDateTime)" -sev Info + } else { + $errorMsg = if ($result.body.error.message) { $result.body.error.message } else { "Unknown error (Status: $($result.status))" } + Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to disable guest $($guest.UserPrincipalName) ($($guest.id)): $errorMsg" -sev Error + } + } + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to process bulk disable guests request: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + } } else { Write-LogMessage -API 'Standards' -tenant $tenant -message "No guests accounts with a login longer than $checkDays days ago." -sev Info } - } if ($Settings.alert -eq $true) { From 19315cc5c21f7dd8237dfceb0df814b4db1e0f34 Mon Sep 17 00:00:00 2001 From: rvdwegen Date: Wed, 28 Jan 2026 12:57:27 +0100 Subject: [PATCH 281/503] Optimize retrieval of potentially large graph data sets --- .../CIPPCore/Public/Standards/Invoke-CIPPStandardAppDeploy.ps1 | 2 +- .../Standards/Invoke-CIPPStandardDisableResourceMailbox.ps1 | 2 +- .../Standards/Invoke-CIPPStandardDisableSharedMailbox.ps1 | 2 +- .../Public/Standards/Invoke-CIPPStandardGroupTemplate.ps1 | 2 +- .../Public/Standards/Invoke-CIPPStandardStaleEntraDevices.ps1 | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAppDeploy.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAppDeploy.ps1 index c01aade26629..528cf962773e 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAppDeploy.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAppDeploy.ps1 @@ -36,7 +36,7 @@ function Invoke-CIPPStandardAppDeploy { Write-Information "Running AppDeploy standard for tenant $($Tenant)." $AppsToAdd = $Settings.appids -split ',' - $AppExists = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/servicePrincipals?$top=999' -tenantid $Tenant + $AppExists = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/servicePrincipals?$top=999&$select=id,appId,displayName,applicationTemplateId' -tenantid $Tenant $Mode = $Settings.mode ?? 'copy' $ExpectedValue = [PSCustomObject]@{ state = 'Configured correctly' } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableResourceMailbox.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableResourceMailbox.ps1 index f2be7ce4edf4..b3800168c36d 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableResourceMailbox.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableResourceMailbox.ps1 @@ -41,7 +41,7 @@ function Invoke-CIPPStandardDisableResourceMailbox { # Get all users that are able to be try { - $UserList = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/users?$top=999&$filter=accountEnabled eq true and onPremisesSyncEnabled ne true and assignedLicenses/$count eq 0&$count=true' -Tenantid $Tenant -ComplexFilter | + $UserList = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/users?$top=999&$filter=accountEnabled eq true and onPremisesSyncEnabled ne true and assignedLicenses/$count eq 0&$count=true&$select=id,userPrincipalName,userType' -Tenantid $Tenant -ComplexFilter | Where-Object { $_.userType -eq 'Member' } $ResourceMailboxList = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-Mailbox' -cmdParams @{ Filter = "RecipientTypeDetails -eq 'RoomMailbox' -or RecipientTypeDetails -eq 'EquipmentMailbox'" } -Select 'UserPrincipalName,DisplayName,RecipientTypeDetails,ExternalDirectoryObjectId' | Where-Object { $_.ExternalDirectoryObjectId -in $UserList.id } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSharedMailbox.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSharedMailbox.ps1 index 9d13e4604ab6..bc93242ebd8a 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSharedMailbox.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSharedMailbox.ps1 @@ -37,7 +37,7 @@ function Invoke-CIPPStandardDisableSharedMailbox { param($Tenant, $Settings) try { - $UserList = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/users?$top=999&$filter=accountEnabled eq true and onPremisesSyncEnabled ne true&$count=true' -Tenantid $Tenant -ComplexFilter + $UserList = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/users?$top=999&$filter=accountEnabled eq true and onPremisesSyncEnabled ne true&$count=true&$select=id,userPrincipalName' -Tenantid $Tenant -ComplexFilter $SharedMailboxList = (New-GraphGetRequest -uri "https://outlook.office365.com/adminapi/beta/$($Tenant)/Mailbox" -Tenantid $Tenant -scope ExchangeOnline | Where-Object { $_.RecipientTypeDetails -eq 'SharedMailbox' -or $_.RecipientTypeDetails -eq 'SchedulingMailbox' -and $_.UserPrincipalName -in $UserList.UserPrincipalName }) } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardGroupTemplate.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardGroupTemplate.ps1 index 151258c2b087..ed52d13b0b4b 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardGroupTemplate.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardGroupTemplate.ps1 @@ -31,7 +31,7 @@ function Invoke-CIPPStandardGroupTemplate { #> param($Tenant, $Settings) - $existingGroups = New-GraphGETRequest -uri 'https://graph.microsoft.com/beta/groups?$top=999' -tenantid $tenant + $existingGroups = New-GraphGETRequest -uri 'https://graph.microsoft.com/beta/groups?$top=999&$select=id,displayName,description,membershipRule' -tenantid $tenant $TestResult = Test-CIPPStandardLicense -StandardName 'GroupTemplate' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') -SkipLog diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardStaleEntraDevices.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardStaleEntraDevices.ps1 index fb789d4036e6..bc202c1360ad 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardStaleEntraDevices.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardStaleEntraDevices.ps1 @@ -45,7 +45,7 @@ function Invoke-CIPPStandardStaleEntraDevices { } #we're done. try { - $AllDevices = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/devices' -tenantid $Tenant | Where-Object { $null -ne $_.approximateLastSignInDateTime } + $AllDevices = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/devices?$select=id,displayName,approximateLastSignInDateTime,accountEnabled,enrollmentProfileName,operatingSystem,managementType,profileType' -tenantid $Tenant | Where-Object { $null -ne $_.approximateLastSignInDateTime } } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the StaleEntraDevices state for $Tenant. Error: $ErrorMessage" -Sev Error From 03229d6cc53292aa6729904dd3307b7528a20b6d Mon Sep 17 00:00:00 2001 From: rvdwegen Date: Wed, 28 Jan 2026 13:06:24 +0100 Subject: [PATCH 282/503] Add missing license checks to prevent impossible runs --- .../Invoke-CIPPStandardAssignmentFilterTemplate.ps1 | 8 +++++++- .../Public/Standards/Invoke-CIPPStandardBranding.ps1 | 7 +++++++ .../Invoke-CIPPStandardCustomBannedPasswordList.ps1 | 7 +++++++ .../Standards/Invoke-CIPPStandardPhishProtection.ps1 | 6 ++++++ 4 files changed, 27 insertions(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAssignmentFilterTemplate.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAssignmentFilterTemplate.ps1 index 5a35370671d4..c0147bddaa93 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAssignmentFilterTemplate.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAssignmentFilterTemplate.ps1 @@ -31,8 +31,14 @@ function Invoke-CIPPStandardAssignmentFilterTemplate { #> param($Tenant, $Settings) + $TestResult = Test-CIPPStandardLicense -StandardName 'AssignmentFilterTemplate' -TenantFilter $Tenant -RequiredCapabilities @('INTUNE_A', 'MDM_Services', 'EMS', 'SCCM', 'MICROSOFTINTUNEPLAN1') + + if ($TestResult -eq $false) { + return $true + } #we're done. + ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'AssignmentFilterTemplate' - $existingFilters = New-GraphGETRequest -uri 'https://graph.microsoft.com/beta/deviceManagement/assignmentFilters' -tenantid $tenant + $existingFilters = New-GraphGETRequest -uri 'https://graph.microsoft.com/beta/deviceManagement/assignmentFilters?$select=id,displayName,description,platform,rule,assignmentFilterManagementType' -tenantid $tenant $Settings.assignmentFilterTemplate ? ($Settings | Add-Member -NotePropertyName 'TemplateList' -NotePropertyValue $Settings.assignmentFilterTemplate) : $null diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardBranding.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardBranding.ps1 index 467279f5838d..a0efe5f4ea5b 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardBranding.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardBranding.ps1 @@ -36,6 +36,13 @@ function Invoke-CIPPStandardBranding { #> param($Tenant, $Settings) + + $TestResult = Test-CIPPStandardLicense -StandardName 'Branding' -TenantFilter $Tenant -RequiredCapabilities @('AAD_PREMIUM', 'AAD_PREMIUM_P2') + + if ($TestResult -eq $false) { + return $true + } #we're done. + ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'Branding' $TenantId = Get-Tenants | Where-Object -Property defaultDomainName -EQ $Tenant diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardCustomBannedPasswordList.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardCustomBannedPasswordList.ps1 index 6f823787f04f..9ff4c6c9ebcd 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardCustomBannedPasswordList.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardCustomBannedPasswordList.ps1 @@ -31,6 +31,13 @@ function Invoke-CIPPStandardCustomBannedPasswordList { #> param($Tenant, $Settings) + + $TestResult = Test-CIPPStandardLicense -StandardName 'CustomBannedPasswordList' -TenantFilter $Tenant -RequiredCapabilities @('AAD_PREMIUM', 'AAD_PREMIUM_P2') + + if ($TestResult -eq $false) { + return $true + } #we're done. + $PasswordRuleTemplateId = '5cf42378-d67d-4f36-ba46-e8b86229381d' # Parse and validate banned words from input $BannedWordsInput = $Settings.BannedWords diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPhishProtection.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPhishProtection.ps1 index 56247118b1f5..8f79c8d5a850 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPhishProtection.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPhishProtection.ps1 @@ -34,6 +34,12 @@ function Invoke-CIPPStandardPhishProtection { param($Tenant, $Settings) + $TestResult = Test-CIPPStandardLicense -StandardName 'PhishProtection' -TenantFilter $Tenant -RequiredCapabilities @('AAD_PREMIUM', 'AAD_PREMIUM_P2') + + if ($TestResult -eq $false) { + return $true + } #we're done. + $TenantId = Get-Tenants | Where-Object -Property defaultDomainName -EQ $tenant $Table = Get-CIPPTable -TableName Config From 032124797ed6b170eac8927c0ee01971ad211abb Mon Sep 17 00:00:00 2001 From: rvdwegen Date: Wed, 28 Jan 2026 13:17:53 +0100 Subject: [PATCH 283/503] Sequential contact processing likely provides sufficient natural propagation time for larger batches. --- .../Standards/Invoke-CIPPStandardDeployContactTemplates.ps1 | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployContactTemplates.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployContactTemplates.ps1 index 43a541db360c..f92a0742d070 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployContactTemplates.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployContactTemplates.ps1 @@ -253,8 +253,10 @@ function Invoke-CIPPStandardDeployContactTemplates { if ($ProcessedCount -gt 0) { Write-LogMessage -API $APIName -tenant $Tenant -message "DeployContactTemplate: Successfully processed $ProcessedCount contacts" -sev Info - # Wait for contacts to propagate before updating additional fields - Start-Sleep -Seconds 1 + # Wait for contacts to propagate before updating additional fields (only needed for small batches) + if ($ProcessedCount -le 3) { + Start-Sleep -Seconds 1 + } # Second pass: Update contacts with additional fields (only if needed) $UpdateFailures = 0 From 45bf2dc2c279d7359aadb1bd214022912581bac8 Mon Sep 17 00:00:00 2001 From: rvdwegen Date: Wed, 28 Jan 2026 13:25:28 +0100 Subject: [PATCH 284/503] New-GraphBulkRequest --- ...nvoke-CIPPStandardDisableSharedMailbox.ps1 | 37 +++++++++++++++---- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSharedMailbox.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSharedMailbox.ps1 index bc93242ebd8a..8f2c04bb3b89 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSharedMailbox.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSharedMailbox.ps1 @@ -46,16 +46,37 @@ function Invoke-CIPPStandardDisableSharedMailbox { } if ($Settings.remediate -eq $true) { + if ($SharedMailboxList.Count -gt 0) { + $int = 0 + $BulkRequests = foreach ($Mailbox in $SharedMailboxList) { + @{ + id = $int++ + method = 'PATCH' + url = "users/$($Mailbox.ObjectKey)" + body = @{ accountEnabled = $false } + 'headers' = @{ + 'Content-Type' = 'application/json' + } + } + } - if ($SharedMailboxList) { - foreach ($Mailbox in $SharedMailboxList) { - try { - New-GraphPOSTRequest -uri "https://graph.microsoft.com/v1.0/users/$($Mailbox.ObjectKey)" -type PATCH -body '{"accountEnabled":"false"}' -tenantid $Tenant - Write-LogMessage -API 'Standards' -tenant $Tenant -message "Entra account for shared mailbox $($Mailbox.DisplayName) disabled." -sev Info - } catch { - $ErrorMessage = Get-CippException -Exception $_ - Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to disable Entra account for shared mailbox. Error: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + try { + $BulkResults = New-GraphBulkRequest -tenantid $Tenant -Requests @($BulkRequests) + + for ($i = 0; $i -lt $BulkResults.Count; $i++) { + $result = $BulkResults[$i] + $Mailbox = $SharedMailboxList[$i] + + if ($result.status -eq 200 -or $result.status -eq 204) { + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Entra account for shared mailbox $($Mailbox.DisplayName) ($($Mailbox.ObjectKey)) disabled." -sev Info + } else { + $errorMsg = if ($result.body.error.message) { $result.body.error.message } else { "Unknown error (Status: $($result.status))" } + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to disable Entra account for shared mailbox $($Mailbox.DisplayName) ($($Mailbox.ObjectKey)): $errorMsg" -sev Error + } } + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to process bulk disable shared mailboxes request: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage } } else { Write-LogMessage -API 'Standards' -tenant $Tenant -message 'All Entra accounts for shared mailboxes are already disabled.' -sev Info From 919bdfc6d005d267b243859524d862846c98f4a6 Mon Sep 17 00:00:00 2001 From: rvdwegen Date: Wed, 28 Jan 2026 13:27:04 +0100 Subject: [PATCH 285/503] New-GraphBulkRequest --- ...oke-CIPPStandardDisableResourceMailbox.ps1 | 37 +++++++++++++++---- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableResourceMailbox.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableResourceMailbox.ps1 index b3800168c36d..f41ca24c6f13 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableResourceMailbox.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableResourceMailbox.ps1 @@ -52,16 +52,37 @@ function Invoke-CIPPStandardDisableResourceMailbox { } if ($Settings.remediate -eq $true) { + if ($ResourceMailboxList.Count -gt 0) { + $int = 0 + $BulkRequests = foreach ($Mailbox in $ResourceMailboxList) { + @{ + id = $int++ + method = 'PATCH' + url = "users/$($Mailbox.ExternalDirectoryObjectId)" + body = @{ accountEnabled = $false } + 'headers' = @{ + 'Content-Type' = 'application/json' + } + } + } - if ($ResourceMailboxList) { - foreach ($Mailbox in $ResourceMailboxList) { - try { - New-GraphPOSTRequest -uri "https://graph.microsoft.com/v1.0/users/$($Mailbox.ExternalDirectoryObjectId)" -type PATCH -body '{"accountEnabled":"false"}' -tenantid $Tenant - Write-LogMessage -API 'Standards' -tenant $Tenant -message "Entra account for $($Mailbox.RecipientTypeDetails), $($Mailbox.DisplayName), $($Mailbox.UserPrincipalName) disabled." -sev Info - } catch { - $ErrorMessage = Get-CippException -Exception $_ - Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to disable Entra account for $($Mailbox.RecipientTypeDetails), $($Mailbox.DisplayName), $($Mailbox.UserPrincipalName). Error: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + try { + $BulkResults = New-GraphBulkRequest -tenantid $Tenant -Requests @($BulkRequests) + + for ($i = 0; $i -lt $BulkResults.Count; $i++) { + $result = $BulkResults[$i] + $Mailbox = $ResourceMailboxList[$i] + + if ($result.status -eq 200 -or $result.status -eq 204) { + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Entra account for $($Mailbox.RecipientTypeDetails), $($Mailbox.DisplayName), $($Mailbox.UserPrincipalName) disabled." -sev Info + } else { + $errorMsg = if ($result.body.error.message) { $result.body.error.message } else { "Unknown error (Status: $($result.status))" } + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to disable Entra account for $($Mailbox.RecipientTypeDetails), $($Mailbox.DisplayName), $($Mailbox.UserPrincipalName): $errorMsg" -sev Error + } } + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to process bulk disable resource mailboxes request: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage } } else { Write-LogMessage -API 'Standards' -tenant $Tenant -message 'All Entra accounts for resource mailboxes are already disabled.' -sev Info From 1647d3af7cc9a78638540b2deb57cd73ddfb841d Mon Sep 17 00:00:00 2001 From: rvdwegen Date: Wed, 28 Jan 2026 15:52:15 +0100 Subject: [PATCH 286/503] Project root cleanup --- CIPP-Permissions.json | 814 ------------------ .../GitHub/Invoke-ListCommunityRepos.ps1 | 2 +- .../Public/Get-CIPPTimerFunctions.ps1 | 2 +- .../Public/GraphHelper/New-passwordString.ps1 | 2 +- .../Gradient/New-GradientServiceSyncRun.ps1 | 4 +- .../Public/Hudu/Invoke-HuduExtensionSync.ps1 | 2 +- CIPPTimers.json => Resources/CIPPTimers.json | 0 .../CommunityRepos.json | 0 .../ConversionTable.csv | 0 .../TemplateEmail.html | 0 .../intuneCollection.json | 0 words.txt => Resources/words.txt | 0 .../Test-AllZTNATests.ps1 | 0 13 files changed, 6 insertions(+), 820 deletions(-) delete mode 100644 CIPP-Permissions.json rename CIPPTimers.json => Resources/CIPPTimers.json (100%) rename CommunityRepos.json => Resources/CommunityRepos.json (100%) rename ConversionTable.csv => Resources/ConversionTable.csv (100%) rename TemplateEmail.html => Resources/TemplateEmail.html (100%) rename intuneCollection.json => Resources/intuneCollection.json (100%) rename words.txt => Resources/words.txt (100%) rename Test-AllZTNATests.ps1 => Tools/Test-AllZTNATests.ps1 (100%) diff --git a/CIPP-Permissions.json b/CIPP-Permissions.json deleted file mode 100644 index ad1e52cb5388..000000000000 --- a/CIPP-Permissions.json +++ /dev/null @@ -1,814 +0,0 @@ -[ - { - "AppId": "aeb86249-8ea3-49e2-900b-54cc8e308f85", - "DisplayName": "M365 License Manager", - "DelegatedPermissions": [ - { - "Id": "fc946a4f-bc4d-413b-a090-b2c86113ec4f", - "Name": "LicenseManager.AccessAsUser", - "Description": "Allows the application to impersonate the signed-in user when communicating with the M365 License Manager service." - } - ], - "ApplicationPermissions": [] - }, - { - "AppId": "00000003-0000-0000-c000-000000000000", - "DisplayName": "Microsoft Graph", - "DelegatedPermissions": [ - { - "Id": "bdfbf15f-ee85-4955-8675-146e8e5296b5", - "Name": "Application.ReadWrite.All", - "Description": "Allows the app to create, read, update and delete applications and service principals on your behalf. Does not allow management of consent grants." - }, - { - "Id": "84bccea3-f856-4a8a-967b-dbe0a3d53a64", - "Name": "AppRoleAssignment.ReadWrite.All", - "Description": "Allows the app to manage permission grants for application permissions to any API (including Microsoft Graph) and application assignments for any app, on your behalf." - }, - { - "Id": "e4c9e354-4dc5-45b8-9e7c-e1393b0b1a20", - "Name": "AuditLog.Read.All", - "Description": "Allows the app to read and query your audit log activities, on your behalf." - }, - { - "Id": "b27a61ec-b99c-4d6a-b126-c4375d08ae30", - "Name": "BitlockerKey.Read.All", - "Description": "Allows the app to read BitLocker keys for your owned devices. Allows read of the recovery key." - }, - { - "Id": "101147cf-4178-4455-9d58-02b5c164e759", - "Name": "Channel.Create", - "Description": "Create channels in any team, on your behalf." - }, - { - "Id": "cc83893a-e232-4723-b5af-bd0b01bcfe65", - "Name": "Channel.Delete.All", - "Description": "Delete channels in any team, on your behalf." - }, - { - "Id": "9d8982ae-4365-4f57-95e9-d6032a4c0b87", - "Name": "Channel.ReadBasic.All", - "Description": "Read channel names and channel descriptions, on your behalf." - }, - { - "Id": "2eadaff8-0bce-4198-a6b9-2cfc35a30075", - "Name": "ChannelMember.Read.All", - "Description": "Read the members of channels, on your behalf." - }, - { - "Id": "0c3e411a-ce45-4cd1-8f30-f99a3efa7b11", - "Name": "ChannelMember.ReadWrite.All", - "Description": "Add and remove members from channels, on your behalf. Also allows changing a member's role, for example from owner to non-owner." - }, - { - "Id": "2b61aa8a-6d36-4b2f-ac7b-f29867937c53", - "Name": "ChannelMessage.Edit", - "Description": "Allows the app to edit channel messages in Microsoft Teams, on your behalf." - }, - { - "Id": "767156cb-16ae-4d10-8f8b-41b657c8c8c8", - "Name": "ChannelMessage.Read.All", - "Description": "Allows the app to read a channel's messages in Microsoft Teams, on your behalf." - }, - { - "Id": "ebf0f66e-9fb1-49e4-a278-222f76911cf4", - "Name": "ChannelMessage.Send", - "Description": "Allows the app to send channel messages in Microsoft Teams, on your behalf." - }, - { - "Id": "233e0cf1-dd62-48bc-b65b-b38fe87fcf8e", - "Name": "ChannelSettings.Read.All", - "Description": "Read all channel names, channel descriptions, and channel settings, on your behalf." - }, - { - "Id": "d649fb7c-72b4-4eec-b2b4-b15acf79e378", - "Name": "ChannelSettings.ReadWrite.All", - "Description": "Read and write the names, descriptions, and settings of all channels, on your behalf." - }, - { - "Id": "f3bfad56-966e-4590-a536-82ecf548ac1e", - "Name": "ConsentRequest.Read.All", - "Description": "Allows the app to read consent requests and approvals, on your behalf." - }, - { - "Id": "885f682f-a990-4bad-a642-36736a74b0c7", - "Name": "DelegatedAdminRelationship.ReadWrite.All", - "Description": "Allows the app to manage (create-update-terminate) Delegated Admin relationships with customers and role assignments to security groups for active Delegated Admin relationships on your behalf." - }, - { - "Id": "41ce6ca6-6826-4807-84f1-1c82854f7ee5", - "Name": "DelegatedPermissionGrant.ReadWrite.All", - "Description": "Allows the app to manage permission grants for delegated permissions exposed by any API (including Microsoft Graph), on your behalf." - }, - { - "Id": "bac3b9c2-b516-4ef4-bd3b-c2ef73d8d804", - "Name": "Device.Command", - "Description": "Allows the app to launch another app or communicate with another app on a device that you own." - }, - { - "Id": "11d4cd79-5ba5-460f-803f-e22c8ab85ccd", - "Name": "Device.Read", - "Description": "Allows the app to see your list of devices." - }, - { - "Id": "951183d1-1a61-466f-a6d1-1fde911bfd95", - "Name": "Device.Read.All", - "Description": "Allows the app to read devices' configuration information on your behalf." - }, - { - "Id": "280b3b69-0437-44b1-bc20-3b2fca1ee3e9", - "Name": "DeviceLocalCredential.Read.All", - "Description": "Allows the app to read device local credential properties including passwords, on your behalf." - }, - { - "Id": "7b3f05d5-f68c-4b8d-8c59-a2ecd12f24af", - "Name": "DeviceManagementApps.ReadWrite.All", - "Description": "Allows the app to read and write the properties, group assignments and status of apps, app configurations and app protection policies managed by Microsoft Intune." - }, - { - "Id": "0883f392-0a7a-443d-8c76-16a6d39c7b63", - "Name": "DeviceManagementConfiguration.ReadWrite.All", - "Description": "Allows the app to read and write properties of Microsoft Intune-managed device configuration and device compliance policies and their assignment to groups." - }, - { - "Id": "3404d2bf-2b13-457e-a330-c24615765193", - "Name": "DeviceManagementManagedDevices.PrivilegedOperations.All", - "Description": "Allows the app to perform remote high impact actions such as wiping the device or resetting the passcode on devices managed by Microsoft Intune." - }, - { - "Id": "44642bfe-8385-4adc-8fc6-fe3cb2c375c3", - "Name": "DeviceManagementManagedDevices.ReadWrite.All", - "Description": "Allows the app to read and write the properties of devices managed by Microsoft Intune. Does not allow high impact operations such as remote wipe and password reset on the device’s owner." - }, - { - "Id": "0c5e8a55-87a6-4556-93ab-adc52c4d862d", - "Name": "DeviceManagementRBAC.ReadWrite.All", - "Description": "Allows the app to read and write the properties relating to the Microsoft Intune Role-Based Access Control (RBAC) settings." - }, - { - "Id": "662ed50a-ac44-4eef-ad86-62eed9be2a29", - "Name": "DeviceManagementServiceConfig.ReadWrite.All", - "Description": "Allows the app to read and write Microsoft Intune service properties including device enrollment and third party service connection configuration." - }, - { - "Id": "0e263e50-5827-48a4-b97c-d940288653c7", - "Name": "Directory.AccessAsUser.All", - "Description": "Allows the app to have the same access to information in your work or school directory as you do." - }, - { - "Id": "c5366453-9fb0-48a5-a156-24f0c49a4b84", - "Name": "Directory.ReadWrite.All", - "Description": "Allows the app to read and write data in your organization's directory, such as other users, groups. It does not allow the app to delete users or groups, or reset user passwords." - }, - { - "Id": "2f9ee017-59c1-4f1d-9472-bd5529a7b311", - "Name": "Domain.Read.All", - "Description": "Allows the app to read all domain properties on your behalf." - }, - { - "Id": "4e46008b-f24c-477d-8fff-7bb4ec7aafe0", - "Name": "Group.ReadWrite.All", - "Description": "Allows the app to create groups and read all group properties and memberships on your behalf. Additionally allows the app to manage your groups and to update group content for groups you are a member of." - }, - { - "Id": "f81125ac-d3b7-4573-a3b2-7099cc39df9e", - "Name": "GroupMember.ReadWrite.All", - "Description": "Allows the app to list groups, read basic properties, read and update the membership of your groups. Group properties and owners cannot be updated and groups cannot be deleted." - }, - { - "Id": "9e4862a5-b68f-479e-848a-4e07e25c9916", - "Name": "IdentityRiskEvent.ReadWrite.All", - "Description": "Allows the app to read and update identity risk event information for all users in your organization on your behalf. Update operations include confirming risk event detections. " - }, - { - "Id": "bb6f654c-d7fd-4ae3-85c3-fc380934f515", - "Name": "IdentityRiskyServicePrincipal.ReadWrite.All", - "Description": "Allows the app to read and update identity risky service principal information for all service principals in your organization, on your behalf. Update operations include dismissing risky service principals." - }, - { - "Id": "e0a7cdbb-08b0-4697-8264-0069786e9674", - "Name": "IdentityRiskyUser.ReadWrite.All", - "Description": "Allows the app to read and update identity risky user information for all users in your organization on your behalf. Update operations include dismissing risky users." - }, - { - "Id": "e383f46e-2787-4529-855e-0e479a3ffac0", - "Name": "Mail.Send", - "Description": "Allows the app to send mail as you." - }, - { - "Id": "a367ab51-6b49-43bf-a716-a1fb06d2a174", - "Name": "Mail.Send.Shared", - "Description": "Allows the app to send mail as you or on-behalf of someone else." - }, - { - "Id": "818c620a-27a9-40bd-a6a5-d96f7d610b4b", - "Name": "MailboxSettings.ReadWrite", - "Description": "Allows the app to read, update, create, and delete your mailbox settings." - }, - { - "Id": "f6a3db3e-f7e8-4ed2-a414-557c8c9830be", - "Name": "Member.Read.Hidden", - "Description": "Allows the app to read the memberships of hidden groups or administrative units on your behalf, for those hidden groups or adminstrative units that you have access to." - }, - { - "Id": "7427e0e9-2fba-42fe-b0c0-848c9e6a8182", - "Name": "offline_access", - "Description": "Allows the app to see and update the data you gave it access to, even when you are not currently using the app. This does not give the app any additional permissions." - }, - { - "Id": "37f7f235-527c-4136-accd-4a02d197296e", - "Name": "openid", - "Description": "Allows you to sign in to the app with your work or school account and allows the app to read your basic profile information." - }, - { - "Id": "46ca0847-7e6b-426e-9775-ea810a948356", - "Name": "Organization.ReadWrite.All", - "Description": "Allows the app to read and write the organization and related resources, on your behalf. Related resources include things like subscribed skus and tenant branding information." - }, - { - "Id": "346c19ff-3fb2-4e81-87a0-bac9e33990c1", - "Name": "OrgSettings-Forms.ReadWrite.All", - "Description": "Allows the app to read and write organization-wide Microsoft Forms settings on your behalf." - }, - { - "Id": "e67e6727-c080-415e-b521-e3f35d5248e9", - "Name": "PeopleSettings.ReadWrite.All", - "Description": "Allows the application to read and write tenant-wide people settings on your behalf." - }, - { - "Id": "4c06a06a-098a-4063-868e-5dfee3827264", - "Name": "Place.ReadWrite.All", - "Description": "Allows the app to manage organization places (conference rooms and room lists) for calendar events and other applications, on your behalf." - }, - { - "Id": "572fea84-0151-49b2-9301-11cb16974376", - "Name": "Policy.Read.All", - "Description": "Allows the app to read your organization's policies on your behalf." - }, - { - "Id": "b27add92-efb2-4f16-84f5-8108ba77985c", - "Name": "Policy.ReadWrite.ApplicationConfiguration", - "Description": "Allows the app to read and write your organization's application configuration policies on your behalf. This includes policies such as activityBasedTimeoutPolicy, claimsMappingPolicy, homeRealmDiscoveryPolicy, tokenIssuancePolicy and tokenLifetimePolicy." - }, - { - "Id": "edb72de9-4252-4d03-a925-451deef99db7", - "Name": "Policy.ReadWrite.AuthenticationFlows", - "Description": "Allows the app to read and write the authentication flow policies for your tenant, on your behalf." - }, - { - "Id": "7e823077-d88e-468f-a337-e18f1f0e6c7c", - "Name": "Policy.ReadWrite.AuthenticationMethod", - "Description": "Allows the app to read and write the authentication method policies for your tenant, on your behalf." - }, - { - "Id": "edd3c878-b384-41fd-95ad-e7407dd775be", - "Name": "Policy.ReadWrite.Authorization", - "Description": "Allows the app to read and write your organization's authorization policy on your behalf. For example, authorization policies can control some of the permissions that the out-of-the-box user role has by default." - }, - { - "Id": "ad902697-1014-4ef5-81ef-2b4301988e8c", - "Name": "Policy.ReadWrite.ConditionalAccess", - "Description": "Allows the app to read and write your organization's conditional access policies on your behalf." - }, - { - "Id": "4d135e65-66b8-41a8-9f8b-081452c91774", - "Name": "Policy.ReadWrite.ConsentRequest", - "Description": "Allows the app to read and write your organization's consent request policy on your behalf." - }, - { - "Id": "40b534c3-9552-4550-901b-23879c90bcf9", - "Name": "Policy.ReadWrite.DeviceConfiguration", - "Description": "Allows the app to read and write your organization's device configuration policies on your behalf. For example, device registration policy can limit initial provisioning controls using quota restrictions, additional authentication and authorization checks." - }, - { - "Id": "a8ead177-1889-4546-9387-f25e658e2a79", - "Name": "Policy.ReadWrite.MobilityManagement", - "Description": "Allows the app to read and write your organization's mobility management policies on your behalf. For example, a mobility management policy can set the enrollment scope for a given mobility management application." - }, - { - "Id": "1d89d70c-dcac-4248-b214-903c457af83a", - "Name": "PrivilegedAccess.Read.AzureResources", - "Description": "Allows the app to read time-based assignment and just-in-time elevation of Azure resources (like your subscriptions, resource groups, storage, compute) on your behalf." - }, - { - "Id": "a84a9652-ffd3-496e-a991-22ba5529156a", - "Name": "PrivilegedAccess.ReadWrite.AzureResources", - "Description": "Allows the app to request and manage time-based assignment and just-in-time elevation of user privileges to manage  your Azure resources (like your subscriptions, resource groups, storage, compute) on your behalf." - }, - { - "Id": "14dad69e-099b-42c9-810b-d002981feec1", - "Name": "profile", - "Description": "Allows the app to see your basic profile (e.g., name, picture, user name, email address)" - }, - { - "Id": "02e97553-ed7b-43d0-ab3c-f8bace0d040c", - "Name": "Reports.Read.All", - "Description": "Allows an app to read all service usage reports on your behalf. Services that provide usage reports include Office 365 and Azure Active Directory." - }, - { - "Id": "b955410e-7715-4a88-a940-dfd551018df3", - "Name": "ReportSettings.ReadWrite.All", - "Description": "Allows the app to read and update admin report settings, such as whether to display concealed information in reports, on your behalf." - }, - { - "Id": "d01b97e9-cbc0-49fe-810a-750afd5527a3", - "Name": "RoleManagement.ReadWrite.Directory", - "Description": "Allows the app to read and manage the role-based access control (RBAC) settings for your company's directory, on your behalf. This includes instantiating directory roles and managing directory role membership, and reading directory role templates, directory roles and memberships." - }, - { - "Id": "dc38509c-b87d-4da0-bd92-6bec988bac4a", - "Name": "SecurityActions.ReadWrite.All", - "Description": "Allows the app to read and update security actions, on your behalf." - }, - { - "Id": "6aedf524-7e1c-45a7-bd76-ded8cab8d0fc", - "Name": "SecurityEvents.ReadWrite.All", - "Description": "Allows the app to read your organization’s security events on your behalf. Also allows you to update editable properties in security events." - }, - { - "Id": "128ca929-1a19-45e6-a3b8-435ec44a36ba", - "Name": "SecurityIncident.ReadWrite.All", - "Description": "Allows the app to read and write to all security incidents that you have access to." - }, - { - "Id": "55896846-df78-47a7-aa94-8d3d4442ca7f", - "Name": "ServiceHealth.Read.All", - "Description": "Allows the app to read your tenant's service health information on your behalf.Health information may include service issues or service health overviews." - }, - { - "Id": "eda39fa6-f8cf-4c3c-a909-432c683e4c9b", - "Name": "ServiceMessage.Read.All", - "Description": "Allows the app to read your tenant's service announcement messages on your behalf. Messages may include information about new or changed features." - }, - { - "Id": "aa07f155-3612-49b8-a147-6c590df35536", - "Name": "SharePointTenantSettings.ReadWrite.All", - "Description": "Allows the application to read and change the tenant-level settings of SharePoint and OneDrive on your behalf." - }, - { - "Id": "89fe6a52-be36-487e-b7d8-d061c450a026", - "Name": "Sites.ReadWrite.All", - "Description": "Allow the application to edit or delete documents and list items in all site collections on your behalf." - }, - { - "Id": "7825d5d6-6049-4ce7-bdf6-3b8d53f4bcd0", - "Name": "Team.Create", - "Description": "Allows the app to create teams on your behalf. " - }, - { - "Id": "485be79e-c497-4b35-9400-0e3fa7f2a5d4", - "Name": "Team.ReadBasic.All", - "Description": "Read the names and descriptions of teams, on your behalf." - }, - { - "Id": "4a06efd2-f825-4e34-813e-82a57b03d1ee", - "Name": "TeamMember.ReadWrite.All", - "Description": "Add and remove members from teams, on your behalf. Also allows changing a member's role, for example from owner to non-owner." - }, - { - "Id": "2104a4db-3a2f-4ea0-9dba-143d457dc666", - "Name": "TeamMember.ReadWriteNonOwnerRole.All", - "Description": "Add and remove members from all teams, on your behalf. Does not allow adding or removing a member with the owner role. Additionally, does not allow the app to elevate an existing member to the owner role." - }, - { - "Id": "0e755559-83fb-4b44-91d0-4cc721b9323e", - "Name": "TeamsActivity.Read", - "Description": "Allows the app to read your teamwork activity feed." - }, - { - "Id": "48638b3c-ad68-4383-8ac4-e6880ee6ca57", - "Name": "TeamSettings.Read.All", - "Description": "Read all teams' settings, on your behalf." - }, - { - "Id": "39d65650-9d3e-4223-80db-a335590d027e", - "Name": "TeamSettings.ReadWrite.All", - "Description": "Read and change all teams' settings, on your behalf." - }, - { - "Id": "a9ff19c2-f369-4a95-9a25-ba9d460efc8e", - "Name": "TeamsTab.Create", - "Description": "Allows the app to create tabs in any team in Microsoft Teams, on your behalf. This does not grant the ability to read, modify or delete tabs after they are created, or give access to the content inside the tabs." - }, - { - "Id": "b98bfd41-87c6-45cc-b104-e2de4f0dafb9", - "Name": "TeamsTab.ReadWrite.All", - "Description": "Read and write tabs in any team in Microsoft Teams, on your behalf. This does not give access to the content inside the tabs." - }, - { - "Id": "cac97e40-6730-457d-ad8d-4852fddab7ad", - "Name": "ThreatAssessment.ReadWrite.All", - "Description": "Allows an app to read your organization's threat assessment requests on your behalf. Also allows the app to create new requests to assess threats received by your organization on your behalf." - }, - { - "Id": "73e75199-7c3e-41bb-9357-167164dbb415", - "Name": "UnifiedGroupMember.Read.AsGuest", - "Description": "Allows the app to read basic unified group properties, memberships and owners of the group you are a member of." - }, - { - "Id": "637d7bec-b31e-4deb-acc9-24275642a2c9", - "Name": "User.ManageIdentities.All", - "Description": "Allows the app to read, update and delete identities that are associated with a user's account that you have access to. This controls the identities users can sign-in with." - }, - { - "Id": "204e0828-b5ca-4ad8-b9f3-f32a958e7cc4", - "Name": "User.ReadWrite.All", - "Description": "Allows the app to read and write the full set of profile properties, reports, and managers of other users in your organization, on your behalf." - }, - { - "Id": "aec28ec7-4d02-4e8c-b864-50163aea77eb", - "Name": "UserAuthenticationMethod.Read.All", - "Description": "Allows the app to read authentication methods of all users you have access to in your organization. Authentication methods include things like a user’s phone numbers and Authenticator app settings. This does not allow the app to see secret information like passwords, or to sign-in or otherwise use the authentication methods." - }, - { - "Id": "48971fc1-70d7-4245-af77-0beb29b53ee2", - "Name": "UserAuthenticationMethod.ReadWrite", - "Description": "Allows the app to read and write your authentication methods, including phone numbers and Authenticator app settings.This does not allow the app to see secret information like your passwords, or to sign-in or otherwise use your authentication methods." - }, - { - "Id": "424b07a8-1209-4d17-9fe4-9018a93a1024", - "Name": "TeamsTelephoneNumber.ReadWrite.All", - "Description": "Allows the app to read and modify your tenant's acquired telephone number details on behalf of the signed-in admin user. Acquired telephone numbers may include attributes related to assigned object, emergency location, network site, etc." - }, - { - "Id": "b7887744-6746-4312-813d-72daeaee7e2d", - "Name": "UserAuthenticationMethod.ReadWrite.All", - "Description": "Allows the app to read and write authentication methods of all users you have access to in your organization. Authentication methods include things like a user’s phone numbers and Authenticator app settings. This does not allow the app to see secret information like passwords, or to sign-in or otherwise use the authentication methods." - } - ], - "ApplicationPermissions": [ - { - "Id": "1bfefb4e-e0b5-418b-a88f-73c46d2cc8e9", - "Name": "Application.ReadWrite.All", - "Description": "Allows the app to create, read, update and delete applications and service principals without a signed-in user. Does not allow management of consent grants." - }, - { - "Id": "b0afded3-3588-46d8-8b3d-9842eff778da", - "Name": "AuditLog.Read.All", - "Description": "Allows the app to read and query your audit log activities, without a signed-in user." - }, - { - "Id": "5e1e9171-754d-478c-812c-f1755a9a4c2d", - "Name": "AuditLogsQuery.Read.All", - "Description": "Allows the app to read and query audit logs from all services." - }, - { - "Id": "f3a65bd4-b703-46df-8f7e-0174fea562aa", - "Name": "Channel.Create", - "Description": "Create channels in any team, without a signed-in user." - }, - { - "Id": "59a6b24b-4225-4393-8165-ebaec5f55d7a", - "Name": "Channel.ReadBasic.All", - "Description": "Read all channel names and channel descriptions, without a signed-in user." - }, - { - "Id": "3b55498e-47ec-484f-8136-9013221c06a9", - "Name": "ChannelMember.Read.All", - "Description": "Read the members of all channels, without a signed-in user." - }, - { - "Id": "35930dcf-aceb-4bd1-b99a-8ffed403c974", - "Name": "ChannelMember.ReadWrite.All", - "Description": "Add and remove members from all channels, without a signed-in user. Also allows changing a member's role, for example from owner to non-owner." - }, - { - "Id": "cac88765-0581-4025-9725-5ebc13f729ee", - "Name": "CrossTenantInformation.ReadBasic.All", - "Description": "Allows the application to obtain basic tenant information about another target tenant within the Azure AD ecosystem without a signed-in user." - }, - { - "Id": "1138cb37-bd11-4084-a2b7-9f71582aeddb", - "Name": "Device.ReadWrite.All", - "Description": "Allows the app to read and write all device properties without a signed in user. Does not allow device creation, device deletion or update of device alternative security identifiers." - }, - { - "Id": "78145de6-330d-4800-a6ce-494ff2d33d07", - "Name": "DeviceManagementApps.ReadWrite.All", - "Description": "Allows the app to read and write the properties, group assignments and status of apps, app configurations and app protection policies managed by Microsoft Intune, without a signed-in user." - }, - { - "Id": "9241abd9-d0e6-425a-bd4f-47ba86e767a4", - "Name": "DeviceManagementConfiguration.ReadWrite.All", - "Description": "Allows the app to read and write properties of Microsoft Intune-managed device configuration and device compliance policies and their assignment to groups, without a signed-in user." - }, - { - "Id": "5b07b0dd-2377-4e44-a38d-703f09a0dc3c", - "Name": "DeviceManagementManagedDevices.PrivilegedOperations.All", - "Description": "Allows the app to perform remote high impact actions such as wiping the device or resetting the passcode on devices managed by Microsoft Intune, without a signed-in user." - }, - { - "Id": "2f51be20-0bb4-4fed-bf7b-db946066c75e", - "Name": "DeviceManagementManagedDevices.Read.All", - "Description": "Allows the app to read the properties of devices managed by Microsoft Intune, without a signed-in user." - }, - { - "Id": "243333ab-4d21-40cb-a475-36241daa0842", - "Name": "DeviceManagementManagedDevices.ReadWrite.All", - "Description": "Allows the app to read and write the properties of devices managed by Microsoft Intune, without a signed-in user. Does not allow high impact operations such as remote wipe and password reset on the device’s owner" - }, - { - "Id": "58ca0d9a-1575-47e1-a3cb-007ef2e4583b", - "Name": "DeviceManagementRBAC.Read.All", - "Description": "Allows the app to read the properties relating to the Microsoft Intune Role-Based Access Control (RBAC) settings, without a signed-in user." - }, - { - "Id": "e330c4f0-4170-414e-a55a-2f022ec2b57b", - "Name": "DeviceManagementRBAC.ReadWrite.All", - "Description": "Allows the app to read and write the properties relating to the Microsoft Intune Role-Based Access Control (RBAC) settings, without a signed-in user." - }, - { - "Id": "9255e99d-faf5-445e-bbf7-cb71482737c4", - "Name": "DeviceManagementScripts.ReadWrite.All", - "Description": "Allows the app to read and write Microsoft Intune device compliance scripts, device management scripts, device shell scripts, device custom attribute shell scripts and device health scripts, without a signed-in user." - }, - { - "Id": "06a5fe6d-c49d-46a7-b082-56b1b14103c7", - "Name": "DeviceManagementServiceConfig.Read.All", - "Description": "Allows the app to read Microsoft Intune service properties including device enrollment and third party service connection configuration, without a signed-in user." - }, - { - "Id": "5ac13192-7ace-4fcf-b828-1a26f28068ee", - "Name": "DeviceManagementServiceConfig.ReadWrite.All", - "Description": "Allows the app to read and write Microsoft Intune service properties including device enrollment and third party service connection configuration, without a signed-in user." - }, - { - "Id": "7ab1d382-f21e-4acd-a863-ba3e13f7da61", - "Name": "Directory.Read.All", - "Description": "Allows the app to read data in your organization's directory, such as users, groups and apps, without a signed-in user." - }, - { - "Id": "19dbc75e-c2e2-444c-a770-ec69d8559fc7", - "Name": "Directory.ReadWrite.All", - "Description": "Allows the app to read and write data in your organization's directory, such as users, and groups, without a signed-in user. Does not allow user or group deletion." - }, - { - "Id": "dbb9058a-0e50-45d7-ae91-66909b5d4664", - "Name": "Domain.Read.All", - "Description": "Allows the app to read all domain properties without a signed-in user." - }, - { - "Id": "75359482-378d-4052-8f01-80520e7db3cd", - "Name": "Files.ReadWrite.All", - "Description": "Allows the app to read, create, update and delete all files in all site collections without a signed in user." - }, - { - "Id": "bf7b1a76-6e77-406b-b258-bf5c7720e98f", - "Name": "Group.Create", - "Description": "Allows the app to create groups without a signed-in user." - }, - { - "Id": "5b567255-7703-4780-807c-7be8301ae99b", - "Name": "Group.Read.All", - "Description": "Allows the app to read group properties and memberships, and read conversations for all groups, without a signed-in user." - }, - { - "Id": "62a82d76-70ea-41e2-9197-370581804d09", - "Name": "Group.ReadWrite.All", - "Description": "Allows the app to create groups, read all group properties and memberships, update group properties and memberships, and delete groups. Also allows the app to read and write conversations. All of these operations can be performed by the app without a signed-in user." - }, - { - "Id": "dbaae8cf-10b5-4b86-a4a1-f871c94c6695", - "Name": "GroupMember.ReadWrite.All", - "Description": "Allows the app to list groups, read basic properties, read and update the membership of the groups this app has access to without a signed-in user. Group properties and owners cannot be updated and groups cannot be deleted." - }, - { - "Id": "19da66cb-0fb0-4390-b071-ebc76a349482", - "Name": "InformationProtectionPolicy.Read.All", - "Description": "Allows an app to read published sensitivity labels and label policy settings for the entire organization or a specific user, without a signed in user." - }, - { - "Id": "6931bccd-447a-43d1-b442-00a195474933", - "Name": "MailboxSettings.ReadWrite", - "Description": "Allows the app to create, read, update, and delete user's mailbox settings without a signed-in user. Does not include permission to send mail." - }, - { - "Id": "292d869f-3427-49a8-9dab-8c70152b74e9", - "Name": "Organization.ReadWrite.All", - "Description": "Allows the app to read and write the organization and related resources, without a signed-in user. Related resources include things like subscribed skus and tenant branding information." - }, - { - "Id": "2cb92fee-97a3-4034-8702-24a6f5d0d1e9", - "Name": "OrgSettings-Forms.ReadWrite.All", - "Description": "Allows the app to read and write organization-wide Microsoft Forms settings, without a signed-in user." - }, - { - "Id": "b6890674-9dd5-4e42-bb15-5af07f541ae1", - "Name": "PeopleSettings.ReadWrite.All", - "Description": "Allows the application to read and write tenant-wide people settings without a signed-in user." - }, - { - "Id": "913b9306-0ce1-42b8-9137-6a7df690a760", - "Name": "Place.Read.All", - "Description": "Allows the app to read company places (conference rooms and room lists) for calendar events and other applications, without a signed-in user." - }, - { - "Id": "246dd0d5-5bd0-4def-940b-0421030a5b68", - "Name": "Policy.Read.All", - "Description": "Allows the app to read all your organization's policies without a signed in user." - }, - { - "Id": "be74164b-cff1-491c-8741-e671cb536e13", - "Name": "Policy.ReadWrite.ApplicationConfiguration", - "Description": "Allows the app to read and write your organization's application configuration policies, without a signed-in user. This includes policies such as activityBasedTimeoutPolicy, claimsMappingPolicy, homeRealmDiscoveryPolicy, tokenIssuancePolicy and tokenLifetimePolicy." - }, - { - "Id": "25f85f3c-f66c-4205-8cd5-de92dd7f0cec", - "Name": "Policy.ReadWrite.AuthenticationFlows", - "Description": "Allows the app to read and write all authentication flow policies for the tenant, without a signed-in user." - }, - { - "Id": "29c18626-4985-4dcd-85c0-193eef327366", - "Name": "Policy.ReadWrite.AuthenticationMethod", - "Description": "Allows the app to read and write all authentication method policies for the tenant, without a signed-in user. " - }, - { - "Id": "01c0a623-fc9b-48e9-b794-0756f8e8f067", - "Name": "Policy.ReadWrite.ConditionalAccess", - "Description": "Allows the app to read and write your organization's conditional access policies, without a signed-in user." - }, - { - "Id": "999f8c63-0a38-4f1b-91fd-ed1947bdd1a9", - "Name": "Policy.ReadWrite.ConsentRequest", - "Description": "Allows the app to read and write your organization's consent requests policy without a signed-in user." - }, - { - "Id": "338163d7-f101-4c92-94ba-ca46fe52447c", - "Name": "Policy.ReadWrite.CrossTenantAccess", - "Description": "Allows the app to read and write your organization's cross tenant access policies without a signed-in user." - }, - { - "Id": "2f6817f8-7b12-4f0f-bc18-eeaf60705a9e", - "Name": "PrivilegedAccess.ReadWrite.AzureADGroup", - "Description": "Allows the app to request and manage time-based assignment and just-in-time elevation (including scheduled elevation) of Azure AD groups in your organization, without a signed-in user." - }, - { - "Id": "230c1aed-a721-4c5d-9cb4-a90514e508ef", - "Name": "Reports.Read.All", - "Description": "Allows an app to read all service usage reports without a signed-in user. Services that provide usage reports include Office 365 and Azure Active Directory." - }, - { - "Id": "2a60023f-3219-47ad-baa4-40e17cd02a1d", - "Name": "ReportSettings.ReadWrite.All", - "Description": "Allows the app to read and update all admin report settings, such as whether to display concealed information in reports, without a signed-in user." - }, - { - "Id": "025d3225-3f02-4882-b4c0-cd5b541a4e80", - "Name": "RoleManagement.ReadWrite.Exchange", - "Description": "Allows the app to read and manage the role-based access control (RBAC) settings for your organization's Exchange Online service, without a signed-in user. This includes reading, creating, updating, and deleting Exchange management role definitions, role groups, role group membership, role assignments, management scopes, and role assignment policies." - }, - { - "Id": "04c55753-2244-4c25-87fc-704ab82a4f69", - "Name": "SecurityAnalyzedMessage.ReadWrite.All", - "Description": "Read email metadata and security detection details, and execute remediation actions like deleting an email, without a signed-in user." - }, - { - "Id": "bf394140-e372-4bf9-a898-299cfc7564e5", - "Name": "SecurityEvents.Read.All", - "Description": "Allows the app to read your organization’s security events without a signed-in user." - }, - { - "Id": "45cc0394-e837-488b-a098-1918f48d186c", - "Name": "SecurityIncident.Read.All", - "Description": "Allows the app to read all security incidents, without a signed-in user." - }, - { - "Id": "34bf0e97-1971-4929-b999-9e2442d941d7", - "Name": "SecurityIncident.ReadWrite.All", - "Description": "Allows the app to read and write to all security incidents, without a signed-in user." - }, - { - "Id": "19b94e34-907c-4f43-bde9-38b1909ed408", - "Name": "SharePointTenantSettings.ReadWrite.All", - "Description": "Allows the application to read and change the tenant-level settings of SharePoint and OneDrive, without a signed-in user." - }, - { - "Id": "a82116e5-55eb-4c41-a434-62fe8a61c773", - "Name": "Sites.FullControl.All", - "Description": "Allows the app to have full control of all site collections without a signed in user." - }, - { - "Id": "0121dc95-1b9f-4aed-8bac-58c5ac466691", - "Name": "TeamMember.ReadWrite.All", - "Description": "Add and remove members from all teams, without a signed-in user. Also allows changing a team member's role, for example from owner to non-owner." - }, - { - "Id": "4437522e-9a86-4a41-a7da-e380edd4a97d", - "Name": "TeamMember.ReadWriteNonOwnerRole.All", - "Description": "Add and remove members from all teams, without a signed-in user. Does not allow adding or removing a member with the owner role. Additionally, does not allow the app to elevate an existing member to the owner role." - }, - { - "Id": "741f803b-c850-494e-b5df-cde7c675a1ca", - "Name": "User.ReadWrite.All", - "Description": "Allows the app to read and update user profiles without a signed in user." - }, - { - "Id": "0a42382f-155c-4eb1-9bdc-21548ccaa387", - "Name": "TeamsTelephoneNumber.ReadWrite.All", - "Description": "Allows the app to read your tenant's acquired telephone number details, without a signed-in user. Acquired telephone numbers may include attributes related to assigned object, emergency location, network site, etc." - }, - { - "Id": "50483e42-d915-4231-9639-7fdb7fd190e5", - "Name": "UserAuthenticationMethod.ReadWrite.All", - "Description": "Allows the application to read and write authentication methods of all users in your organization, without a signed-in user. Authentication methods include things like a user’s phone numbers and Authenticator app settings. This does not allow the app to see secret information like passwords, or to sign-in or otherwise use the authentication methods" - } - ] - }, - { - "AppId": "fa3d9a0c-3fb0-42cc-9193-47c7ecd2edbd", - "DisplayName": "Microsoft Partner Center", - "DelegatedPermissions": [ - { - "Id": "1cebfa2a-fb4d-419e-b5f9-839b4383e05a", - "Name": "user_impersonation", - "Description": "Allow the application to access Partner Center on your behalf" - } - ], - "ApplicationPermissions": [] - }, - { - "AppId": "00000002-0000-0ff1-ce00-000000000000", - "DisplayName": "Office 365 Exchange Online", - "DelegatedPermissions": [ - { - "Id": "ab4f2b77-0b06-4fc1-a9de-02113fc2ab7c", - "Name": "Exchange.Manage", - "Description": "Allows the app to manage your organization's Exchange environment, such as mailboxes, groups, and other configuration objects. To enable management actions, an admin must assign you the appropriate roles." - }, - { - "Id": "bbd1ca91-75e0-4814-ad94-9c5dbbae3415", - "Name": "Calendars.ReadWrite.All", - "Description": "Allows the app to read, update, create and delete events in all calendars in your organization you have permissions to access. This includes delegate and shared calendars. " - }, - { - "Id": "2e83d72d-8895-4b66-9eea-abb43449ab8b", - "Name": "MailboxSettings.ReadWrite", - "Description": "Allows the app to read, update, create, and delete your mailbox settings." - } - ], - "ApplicationPermissions": [ - { - "Id": "dc50a0fb-09a3-484d-be87-e023b12c6440", - "Name": "Exchange.ManageAsApp", - "Description": "Allows the app to manage the organization's Exchange environment without any user interaction. This includes mailboxes, groups, and other configuration objects. To enable management actions, an admin must assign the appropriate roles directly to the app." - }, - { - "Id": "ef54d2bf-783f-4e0f-bca1-3210c0444d99", - "Name": "Calendars.ReadWrite.All", - "Description": "Allows the app to create, read, update, and delete events of all calendars without a signed-in user." - }, - { - "Id": "f9156939-25cd-4ba8-abfe-7fabcf003749", - "Name": "MailboxSettings.ReadWrite", - "Description": "Allows the app to create, read, update, and delete user's mailbox settings without a signed-in user. Does not include permission to send mail." - } - ] - }, - { - "AppId": "00000003-0000-0ff1-ce00-000000000000", - "DisplayName": "Office 365 SharePoint Online", - "DelegatedPermissions": [ - { - "Id": "56680e0d-d2a3-4ae1-80d8-3c4f2100e3d0", - "Name": "AllSites.FullControl", - "Description": "Allows the app to have full control of all site collections on your behalf." - }, - { - "Id": "AllProfiles.Manage", - "Name": "AllProfiles.Manage", - "Description": "Manually added" - } - ], - "ApplicationPermissions": [] - }, - { - "AppId": "48ac35b8-9aa8-4d74-927d-1f4a14a0b239", - "DisplayName": "Skype and Teams Tenant Admin API", - "DelegatedPermissions": [ - { - "Id": "e60370c1-e451-437e-aa6e-d76df38e5f15", - "Name": "user_impersonation", - "Description": "Access Microsoft Teams and Skype for Business data based on the user's role membership" - } - ], - "ApplicationPermissions": [] - }, - { - "AppId": "fc780465-2017-40d4-a0c5-307022471b92", - "DisplayName": "WindowsDefenderATP", - "DelegatedPermissions": [ - { - "Id": "63a677ce-818c-4409-9d12-5c6d2e2a6bfe", - "Name": "Vulnerability.Read", - "Description": "Allows the app to read Threat and Vulnerability Management vulnerability information on behalf of the signed-in user" - } - ], - "ApplicationPermissions": [ - { - "Id": "41269fc5-d04d-4bfd-bce7-43a51cea049a", - "Name": "Vulnerability.Read.All", - "Description": "Allows the app to read any Threat and Vulnerability Management vulnerability information" - } - ] - } -] diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tools/GitHub/Invoke-ListCommunityRepos.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tools/GitHub/Invoke-ListCommunityRepos.ps1 index 79256990242f..8c33f1ed2d2a 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tools/GitHub/Invoke-ListCommunityRepos.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tools/GitHub/Invoke-ListCommunityRepos.ps1 @@ -24,7 +24,7 @@ function Invoke-ListCommunityRepos { if (!$Request.Query.WriteAccess) { $CIPPRoot = (Get-Item (Get-Module -Name CIPPCore).ModuleBase).Parent.Parent.FullName - $CommunityRepos = Join-Path -Path $CIPPRoot -ChildPath 'CommunityRepos.json' + $CommunityRepos = Join-Path -Path $CIPPRoot -ChildPath 'Resources\CommunityRepos.json' $DefaultCommunityRepos = Get-Content -Path $CommunityRepos -Raw | ConvertFrom-Json $DefaultsMissing = $false diff --git a/Modules/CIPPCore/Public/Get-CIPPTimerFunctions.ps1 b/Modules/CIPPCore/Public/Get-CIPPTimerFunctions.ps1 index d60ca8ed40cf..529cd3f3aa84 100644 --- a/Modules/CIPPCore/Public/Get-CIPPTimerFunctions.ps1 +++ b/Modules/CIPPCore/Public/Get-CIPPTimerFunctions.ps1 @@ -40,7 +40,7 @@ function Get-CIPPTimerFunctions { } $CIPPRoot = (Get-Item $CIPPCoreModuleRoot).Parent.Parent - $CippTimers = Get-Content -Path $CIPPRoot\CIPPTimers.json + $CippTimers = Get-Content -Path $CIPPRoot\Resources\CIPPTimers.json if ($ListAllTasks) { $Orchestrators = $CippTimers | ConvertFrom-Json | Sort-Object -Property Priority diff --git a/Modules/CIPPCore/Public/GraphHelper/New-passwordString.ps1 b/Modules/CIPPCore/Public/GraphHelper/New-passwordString.ps1 index c8393455c326..89545efd1229 100644 --- a/Modules/CIPPCore/Public/GraphHelper/New-passwordString.ps1 +++ b/Modules/CIPPCore/Public/GraphHelper/New-passwordString.ps1 @@ -10,7 +10,7 @@ function New-passwordString { $SettingsTable = Get-CippTable -tablename 'Settings' $PasswordType = (Get-CIPPAzDataTableEntity @SettingsTable).passwordType if ($PasswordType -eq 'Correct-Battery-Horse') { - $Words = Get-Content .\words.txt + $Words = Get-Content .\Resources\words.txt (Get-Random -InputObject $words -Count 4) -join '-' } else { # Generate a complex password with a maximum of 100 tries diff --git a/Modules/CippExtensions/Public/Gradient/New-GradientServiceSyncRun.ps1 b/Modules/CippExtensions/Public/Gradient/New-GradientServiceSyncRun.ps1 index cb85b1b60c74..b3e32933987b 100644 --- a/Modules/CippExtensions/Public/Gradient/New-GradientServiceSyncRun.ps1 +++ b/Modules/CippExtensions/Public/Gradient/New-GradientServiceSyncRun.ps1 @@ -27,8 +27,8 @@ function New-GradientServiceSyncRun { } - Set-Location (Get-Item $PSScriptRoot).Parent.FullName - $ConvertTable = Import-Csv ConversionTable.csv + Set-Location (Get-Item $PSScriptRoot).Parent.Parent.Parent.Parent.FullName + $ConvertTable = Import-Csv Resources\ConversionTable.csv $Table = Get-CIPPTable -TableName cachelicenses $LicenseTable = Get-CIPPTable -TableName ExcludedLicenses $ExcludedSkuList = Get-CIPPAzDataTableEntity @LicenseTable diff --git a/Modules/CippExtensions/Public/Hudu/Invoke-HuduExtensionSync.ps1 b/Modules/CippExtensions/Public/Hudu/Invoke-HuduExtensionSync.ps1 index 19b1e8c13def..61b68e9e3b02 100644 --- a/Modules/CippExtensions/Public/Hudu/Invoke-HuduExtensionSync.ps1 +++ b/Modules/CippExtensions/Public/Hudu/Invoke-HuduExtensionSync.ps1 @@ -34,7 +34,7 @@ function Invoke-HuduExtensionSync { # Import license mapping Set-Location (Get-Item $PSScriptRoot).Parent.Parent.Parent.Parent.FullName - $LicTable = Import-Csv ConversionTable.csv + $LicTable = Import-Csv Resources\ConversionTable.csv $CompanyResult.Logs.Add('Starting Hudu Extension Sync') diff --git a/CIPPTimers.json b/Resources/CIPPTimers.json similarity index 100% rename from CIPPTimers.json rename to Resources/CIPPTimers.json diff --git a/CommunityRepos.json b/Resources/CommunityRepos.json similarity index 100% rename from CommunityRepos.json rename to Resources/CommunityRepos.json diff --git a/ConversionTable.csv b/Resources/ConversionTable.csv similarity index 100% rename from ConversionTable.csv rename to Resources/ConversionTable.csv diff --git a/TemplateEmail.html b/Resources/TemplateEmail.html similarity index 100% rename from TemplateEmail.html rename to Resources/TemplateEmail.html diff --git a/intuneCollection.json b/Resources/intuneCollection.json similarity index 100% rename from intuneCollection.json rename to Resources/intuneCollection.json diff --git a/words.txt b/Resources/words.txt similarity index 100% rename from words.txt rename to Resources/words.txt diff --git a/Test-AllZTNATests.ps1 b/Tools/Test-AllZTNATests.ps1 similarity index 100% rename from Test-AllZTNATests.ps1 rename to Tools/Test-AllZTNATests.ps1 From 383c90778d882789b8ba7e12ceb68fb992bf04d4 Mon Sep 17 00:00:00 2001 From: rvdwegen Date: Wed, 28 Jan 2026 15:55:47 +0100 Subject: [PATCH 287/503] root cleanup --- Modules/CIPPCore/Public/New-CIPPAlertTemplate.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/New-CIPPAlertTemplate.ps1 b/Modules/CIPPCore/Public/New-CIPPAlertTemplate.ps1 index 67340eaf3ab5..7ca6e0d74716 100644 --- a/Modules/CIPPCore/Public/New-CIPPAlertTemplate.ps1 +++ b/Modules/CIPPCore/Public/New-CIPPAlertTemplate.ps1 @@ -13,7 +13,7 @@ function New-CIPPAlertTemplate { $AlertComment ) $Appname = '[{"Application Name":"ACOM Azure Website","Application IDs":"23523755-3a2b-41ca-9315-f81f3f566a95"},{"Application Name":"AEM-DualAuth","Application IDs":"69893ee3-dd10-4b1c-832d-4870354be3d8"},{"Application Name":"ASM Campaign Servicing","Application IDs":"0cb7b9ec-5336-483b-bc31-b15b5788de71"},{"Application Name":"Azure Advanced Threat Protection","Application IDs":"7b7531ad-5926-4f2d-8a1d-38495ad33e17"},{"Application Name":"Azure Data Lake","Application IDs":"e9f49c6b-5ce5-44c8-925d-015017e9f7ad"},{"Application Name":"Azure Lab Services Portal","Application IDs":"835b2a73-6e10-4aa5-a979-21dfda45231c"},{"Application Name":"Azure Portal","Application IDs":"c44b4083-3bb0-49c1-b47d-974e53cbdf3c"},{"Application Name":"AzureSupportCenter","Application IDs":"37182072-3c9c-4f6a-a4b3-b3f91cacffce"},{"Application Name":"Bing","Application IDs":"9ea1ad79-fdb6-4f9a-8bc3-2b70f96e34c7"},{"Application Name":"CPIM Service","Application IDs":"bb2a2e3a-c5e7-4f0a-88e0-8e01fd3fc1f4"},{"Application Name":"CRM Power BI Integration","Application IDs":"e64aa8bc-8eb4-40e2-898b-cf261a25954f"},{"Application Name":"Dataverse","Application IDs":"00000007-0000-0000-c000-000000000000"},{"Application Name":"Enterprise Roaming and Backup","Application IDs":"60c8bde5-3167-4f92-8fdb-059f6176dc0f"},{"Application Name":"IAM Supportability","Application IDs":"a57aca87-cbc0-4f3c-8b9e-dc095fdc8978"},{"Application Name":"IrisSelectionFrontDoor","Application IDs":"16aeb910-ce68-41d1-9ac3-9e1673ac9575"},{"Application Name":"MCAPI Authorization Prod","Application IDs":"d73f4b35-55c9-48c7-8b10-651f6f2acb2e"},{"Application Name":"Media Analysis and Transformation Service","Application IDs":"944f0bd1-117b-4b1c-af26-804ed95e767e
0cd196ee-71bf-4fd6-a57c-b491ffd4fb1e"},{"Application Name":"Microsoft 365 Support Service","Application IDs":"ee272b19-4411-433f-8f28-5c13cb6fd407"},{"Application Name":"Microsoft App Access Panel","Application IDs":"0000000c-0000-0000-c000-000000000000"},{"Application Name":"Microsoft Approval Management","Application IDs":"65d91a3d-ab74-42e6-8a2f-0add61688c74
38049638-cc2c-4cde-abe4-4479d721ed44"},{"Application Name":"Microsoft Authentication Broker","Application IDs":"29d9ed98-a469-4536-ade2-f981bc1d605e"},{"Application Name":"Microsoft Azure CLI","Application IDs":"04b07795-8ddb-461a-bbee-02f9e1bf7b46"},{"Application Name":"Microsoft Azure PowerShell","Application IDs":"1950a258-227b-4e31-a9cf-717495945fc2"},{"Application Name":"Microsoft Bing Search","Application IDs":"cf36b471-5b44-428c-9ce7-313bf84528de"},{"Application Name":"Microsoft Bing Search for Microsoft Edge","Application IDs":"2d7f3606-b07d-41d1-b9d2-0d0c9296a6e8"},{"Application Name":"Microsoft Bing Default Search Engine","Application IDs":"1786c5ed-9644-47b2-8aa0-7201292175b6"},{"Application Name":"Microsoft Defender for Cloud Apps","Application IDs":"3090ab82-f1c1-4cdf-af2c-5d7a6f3e2cc7"},{"Application Name":"Microsoft Docs","Application IDs":"18fbca16-2224-45f6-85b0-f7bf2b39b3f3"},{"Application Name":"Microsoft Dynamics ERP","Application IDs":"00000015-0000-0000-c000-000000000000"},{"Application Name":"Microsoft Edge Insider Addons Prod","Application IDs":"6253bca8-faf2-4587-8f2f-b056d80998a7"},{"Application Name":"Microsoft Exchange Online Protection","Application IDs":"00000007-0000-0ff1-ce00-000000000000"},{"Application Name":"Microsoft Forms","Application IDs":"c9a559d2-7aab-4f13-a6ed-e7e9c52aec87"},{"Application Name":"Microsoft Graph","Application IDs":"00000003-0000-0000-c000-000000000000"},{"Application Name":"Microsoft Intune Web Company Portal","Application IDs":"74bcdadc-2fdc-4bb3-8459-76d06952a0e9"},{"Application Name":"Microsoft Intune Windows Agent","Application IDs":"fc0f3af4-6835-4174-b806-f7db311fd2f3"},{"Application Name":"Microsoft Learn","Application IDs":"18fbca16-2224-45f6-85b0-f7bf2b39b3f3"},{"Application Name":"Microsoft Office","Application IDs":"d3590ed6-52b3-4102-aeff-aad2292ab01c"},{"Application Name":"Microsoft Office 365 Portal","Application IDs":"00000006-0000-0ff1-ce00-000000000000"},{"Application Name":"Microsoft Office Web Apps Service","Application IDs":"67e3df25-268a-4324-a550-0de1c7f97287"},{"Application Name":"Microsoft Online Syndication Partner Portal","Application IDs":"d176f6e7-38e5-40c9-8a78-3998aab820e7"},{"Application Name":"Microsoft password reset service","Application IDs":"93625bc8-bfe2-437a-97e0-3d0060024faa"},{"Application Name":"Microsoft Power BI","Application IDs":"871c010f-5e61-4fb1-83ac-98610a7e9110"},{"Application Name":"Microsoft Storefronts","Application IDs":"28b567f6-162c-4f54-99a0-6887f387bbcc"},{"Application Name":"Microsoft Stream Portal","Application IDs":"cf53fce8-def6-4aeb-8d30-b158e7b1cf83"},{"Application Name":"Microsoft Substrate Management","Application IDs":"98db8bd6-0cc0-4e67-9de5-f187f1cd1b41"},{"Application Name":"Microsoft Support","Application IDs":"fdf9885b-dd37-42bf-82e5-c3129ef5a302"},{"Application Name":"Microsoft Teams","Application IDs":"1fec8e78-bce4-4aaf-ab1b-5451cc387264"},{"Application Name":"Microsoft Teams Services","Application IDs":"cc15fd57-2c6c-4117-a88c-83b1d56b4bbe"},{"Application Name":"Microsoft Teams Web Client","Application IDs":"5e3ce6c0-2b1f-4285-8d4b-75ee78787346"},{"Application Name":"Microsoft Whiteboard Services","Application IDs":"95de633a-083e-42f5-b444-a4295d8e9314"},{"Application Name":"O365 Suite UX","Application IDs":"4345a7b9-9a63-4910-a426-35363201d503"},{"Application Name":"Office 365 Exchange Online","Application IDs":"00000002-0000-0ff1-ce00-000000000000"},{"Application Name":"Office 365 Management","Application IDs":"00b41c95-dab0-4487-9791-b9d2c32c80f2"},{"Application Name":"Office 365 Search Service","Application IDs":"66a88757-258c-4c72-893c-3e8bed4d6899"},{"Application Name":"Office 365 SharePoint Online","Application IDs":"00000003-0000-0ff1-ce00-000000000000"},{"Application Name":"Office Delve","Application IDs":"94c63fef-13a3-47bc-8074-75af8c65887a"},{"Application Name":"Office Online Add-in SSO","Application IDs":"93d53678-613d-4013-afc1-62e9e444a0a5"},{"Application Name":"Office Online Client AAD- Augmentation Loop","Application IDs":"2abdc806-e091-4495-9b10-b04d93c3f040"},{"Application Name":"Office Online Client AAD- Loki","Application IDs":"b23dd4db-9142-4734-867f-3577f640ad0c"},{"Application Name":"Office Online Client AAD- Maker","Application IDs":"17d5e35f-655b-4fb0-8ae6-86356e9a49f5"},{"Application Name":"Office Online Client MSA- Loki","Application IDs":"b6e69c34-5f1f-4c34-8cdf-7fea120b8670"},{"Application Name":"Office Online Core SSO","Application IDs":"243c63a3-247d-41c5-9d83-7788c43f1c43"},{"Application Name":"Office Online Search","Application IDs":"a9b49b65-0a12-430b-9540-c80b3332c127"},{"Application Name":"Office.com","Application IDs":"4b233688-031c-404b-9a80-a4f3f2351f90"},{"Application Name":"Office365 Shell WCSS-Client","Application IDs":"89bee1f7-5e6e-4d8a-9f3d-ecd601259da7"},{"Application Name":"OfficeClientService","Application IDs":"0f698dd4-f011-4d23-a33e-b36416dcb1e6"},{"Application Name":"OfficeHome","Application IDs":"4765445b-32c6-49b0-83e6-1d93765276ca"},{"Application Name":"OfficeShredderWacClient","Application IDs":"4d5c2d63-cf83-4365-853c-925fd1a64357"},{"Application Name":"OMSOctopiPROD","Application IDs":"62256cef-54c0-4cb4-bcac-4c67989bdc40"},{"Application Name":"OneDrive SyncEngine","Application IDs":"ab9b8c07-8f02-4f72-87fa-80105867a763"},{"Application Name":"OneNote","Application IDs":"2d4d3d8e-2be3-4bef-9f87-7875a61c29de"},{"Application Name":"Outlook Mobile","Application IDs":"27922004-5251-4030-b22d-91ecd9a37ea4"},{"Application Name":"Partner Customer Delegated Admin Offline Processor","Application IDs":"a3475900-ccec-4a69-98f5-a65cd5dc5306"},{"Application Name":"Password Breach Authenticator","Application IDs":"bdd48c81-3a58-4ea9-849c-ebea7f6b6360"},{"Application Name":"Power BI Service","Application IDs":"00000009-0000-0000-c000-000000000000"},{"Application Name":"SharedWithMe","Application IDs":"ffcb16e8-f789-467c-8ce9-f826a080d987"},{"Application Name":"SharePoint Online Web Client Extensibility","Application IDs":"08e18876-6177-487e-b8b5-cf950c1e598c"},{"Application Name":"Signup","Application IDs":"b4bddae8-ab25-483e-8670-df09b9f1d0ea"},{"Application Name":"Skype for Business Online","Application IDs":"00000004-0000-0ff1-ce00-000000000000"},{"Application Name":"Sway","Application IDs":"905fcf26-4eb7-48a0-9ff0-8dcc7194b5ba"},{"Application Name":"Universal Store Native Client","Application IDs":"268761a2-03f3-40df-8a8b-c3db24145b6b"},{"Application Name":"Vortex [wsfed enabled]","Application IDs":"5572c4c0-d078-44ce-b81c-6cbf8d3ed39e"},{"Application Name":"Windows Azure Active Directory","Application IDs":"00000002-0000-0000-c000-000000000000"},{"Application Name":"Windows Azure Service Management API","Application IDs":"797f4846-ba00-4fd7-ba43-dac1f8f63013"},{"Application Name":"WindowsDefenderATP Portal","Application IDs":"a3b79187-70b2-4139-83f9-6016c58cd27b"},{"Application Name":"Windows Search","Application IDs":"26a7ee05-5602-4d76-a7ba-eae8b7b67941"},{"Application Name":"Windows Spotlight","Application IDs":"1b3c667f-cde3-4090-b60b-3d2abd0117f0"},{"Application Name":"Windows Store for Business","Application IDs":"45a330b1-b1ec-4cc1-9161-9f03992aa49f"},{"Application Name":"Yammer","Application IDs":"00000005-0000-0ff1-ce00-000000000000"},{"Application Name":"Yammer Web","Application IDs":"c1c74fed-04c9-4704-80dc-9f79a2e515cb"},{"Application Name":"Yammer Web Embed","Application IDs":"e1ef36fd-b883-4dbf-97f0-9ece4b576fc6"}]' | ConvertFrom-Json | Where-Object -Property 'Application IDs' -EQ $data.applicationId - $HTMLTemplate = Get-Content 'TemplateEmail.html' -Raw | Out-String + $HTMLTemplate = Get-Content 'Resources\TemplateEmail.html' -Raw | Out-String $Title = '' $IntroText = '' $ButtonUrl = '' From a1f755e7d463a69180083ed86069bf25092891de Mon Sep 17 00:00:00 2001 From: rvdwegen Date: Wed, 28 Jan 2026 16:06:53 +0100 Subject: [PATCH 288/503] Clean up obsolete location switches --- Modules/CIPPCore/Public/Add-CIPPApplicationPermission.ps1 | 1 - Modules/CIPPCore/Public/Add-CIPPDelegatedPermission.ps1 | 1 - .../Tenant/Administration/Alerts/Invoke-PublicWebhooks.ps1 | 2 -- Modules/CIPPCore/Public/Test-CIPPAccessPermissions.ps1 | 1 - Modules/CippEntrypoints/CippEntrypoints.psm1 | 2 -- .../Public/NinjaOne/Invoke-NinjaOneTenantSync.ps1 | 3 --- profile.ps1 | 1 - 7 files changed, 11 deletions(-) diff --git a/Modules/CIPPCore/Public/Add-CIPPApplicationPermission.ps1 b/Modules/CIPPCore/Public/Add-CIPPApplicationPermission.ps1 index 41681e8b5bba..c7963cc5a196 100644 --- a/Modules/CIPPCore/Public/Add-CIPPApplicationPermission.ps1 +++ b/Modules/CIPPCore/Public/Add-CIPPApplicationPermission.ps1 @@ -9,7 +9,6 @@ function Add-CIPPApplicationPermission { if ($ApplicationId -eq $env:ApplicationID -and $TenantFilter -eq $env:TenantID) { $RequiredResourceAccess = 'CIPPDefaults' } - Set-Location (Get-Item $PSScriptRoot).FullName if ($RequiredResourceAccess -eq 'CIPPDefaults') { $Permissions = Get-CippSamPermissions -NoDiff diff --git a/Modules/CIPPCore/Public/Add-CIPPDelegatedPermission.ps1 b/Modules/CIPPCore/Public/Add-CIPPDelegatedPermission.ps1 index 2202e25147f9..26ece527aa48 100644 --- a/Modules/CIPPCore/Public/Add-CIPPDelegatedPermission.ps1 +++ b/Modules/CIPPCore/Public/Add-CIPPDelegatedPermission.ps1 @@ -8,7 +8,6 @@ function Add-CIPPDelegatedPermission { $TenantFilter ) Write-Host 'Adding Delegated Permissions' - Set-Location (Get-Item $PSScriptRoot).FullName if ($ApplicationId -eq $env:ApplicationID -and $TenantFilter -eq $env:TenantID) { #return @('Cannot modify delgated permissions for CIPP-SAM on partner tenant') diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-PublicWebhooks.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-PublicWebhooks.ps1 index af575369f8f4..43289dc9cd39 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-PublicWebhooks.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-PublicWebhooks.ps1 @@ -8,8 +8,6 @@ function Invoke-PublicWebhooks { param($Request, $TriggerMetadata) $Headers = $Request.Headers - - Set-Location (Get-Item $PSScriptRoot).Parent.FullName $WebhookTable = Get-CIPPTable -TableName webhookTable $WebhookIncoming = Get-CIPPTable -TableName WebhookIncoming $Webhooks = Get-CIPPAzDataTableEntity @WebhookTable diff --git a/Modules/CIPPCore/Public/Test-CIPPAccessPermissions.ps1 b/Modules/CIPPCore/Public/Test-CIPPAccessPermissions.ps1 index e95cdb074f8d..9843f4c7de09 100644 --- a/Modules/CIPPCore/Public/Test-CIPPAccessPermissions.ps1 +++ b/Modules/CIPPCore/Public/Test-CIPPAccessPermissions.ps1 @@ -25,7 +25,6 @@ function Test-CIPPAccessPermissions { } $Success = $true try { - Set-Location (Get-Item $PSScriptRoot).FullName $null = Get-CIPPAuthentication $GraphToken = Get-GraphToken -returnRefresh $true -SkipCache $true if ($GraphToken) { diff --git a/Modules/CippEntrypoints/CippEntrypoints.psm1 b/Modules/CippEntrypoints/CippEntrypoints.psm1 index cda294332f1a..1f521af36c66 100644 --- a/Modules/CippEntrypoints/CippEntrypoints.psm1 +++ b/Modules/CippEntrypoints/CippEntrypoints.psm1 @@ -39,7 +39,6 @@ function Receive-CippHttpTrigger { # Convert the request to a PSCustomObject because the httpContext is case sensitive since 7.3 $Request = $Request | ConvertTo-Json -Depth 100 | ConvertFrom-Json - Set-Location (Get-Item $PSScriptRoot).Parent.Parent.FullName if ($Request.Params.CIPPEndpoint -eq '$batch') { # Implement batch processing in the style of graph api $batch @@ -291,7 +290,6 @@ function Receive-CippActivityTrigger { Write-Warning "Hey Boo, the activity function is running. Here's some info: $($Item | ConvertTo-Json -Depth 10 -Compress)" try { $Output = $null - Set-Location (Get-Item $PSScriptRoot).Parent.Parent.FullName $metric = @{ Kind = 'CIPPCommandStart' InvocationId = "$($ExecutionContext.InvocationId)" diff --git a/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneTenantSync.ps1 b/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneTenantSync.ps1 index 7d2b1a4b9aab..ca67d3b360e3 100644 --- a/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneTenantSync.ps1 +++ b/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneTenantSync.ps1 @@ -1846,9 +1846,6 @@ function Invoke-NinjaOneTenantSync { ### CIPP Applied Standards Cards Write-Information 'Applied Standards' - $ModuleBase = Get-Module CIPPExtensions | Select-Object -ExpandProperty ModuleBase - $CIPPRoot = (Get-Item $ModuleBase).Parent.Parent.FullName - Set-Location $CIPPRoot try { $StandardsDefinitions = Invoke-RestMethod -Uri 'https://raw.githubusercontent.com/KelvinTegelaar/CIPP/refs/heads/main/src/data/standards.json' diff --git a/profile.ps1 b/profile.ps1 index a1ec3269f8d5..3925ffbf3f9d 100644 --- a/profile.ps1 +++ b/profile.ps1 @@ -8,7 +8,6 @@ if ($env:APPLICATIONINSIGHTS_CONNECTION_STRING -or $env:APPINSIGHTS_INSTRUMENTAT $hasAppInsights = $true } if ($hasAppInsights) { - Set-Location -Path $PSScriptRoot $SwAppInsights = [System.Diagnostics.Stopwatch]::StartNew() try { $AppInsightsDllPath = Join-Path $PSScriptRoot 'Shared\AppInsights\Microsoft.ApplicationInsights.dll' From 46cf0c1ed4348c353eed6bfeac2c0cce1f3dc1a5 Mon Sep 17 00:00:00 2001 From: rvdwegen Date: Wed, 28 Jan 2026 16:08:45 +0100 Subject: [PATCH 289/503] clean up root --- ExampleReportTemplate.ps1 => Tools/ExampleReportTemplate.ps1 | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename ExampleReportTemplate.ps1 => Tools/ExampleReportTemplate.ps1 (100%) diff --git a/ExampleReportTemplate.ps1 b/Tools/ExampleReportTemplate.ps1 similarity index 100% rename from ExampleReportTemplate.ps1 rename to Tools/ExampleReportTemplate.ps1 From bf37b8d918c2ade9e3b2a90f9c3a9cd1e1398920 Mon Sep 17 00:00:00 2001 From: rvdwegen Date: Wed, 28 Jan 2026 16:12:16 +0100 Subject: [PATCH 290/503] gitignore all ps1 files except profile.ps1. Messy bastards. --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index ec6b5ec8902e..073300712d7e 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,7 @@ yarn.lock # Cursor IDE .cursor/rules + +# Ignore all root PowerShell files except profile.ps1 +/*.ps1 +!/profile.ps1 From ad98672028709f822e3cd1f9530eac32562cb6e6 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Wed, 28 Jan 2026 11:20:00 -0500 Subject: [PATCH 291/503] Update log messages for new tenant onboarding Changed API log identifiers to 'NewTenant' in Push-ExecOnboardTenantQueue.ps1 and Invoke-ExecAddTenant.ps1 for consistency and improved log tracking when onboarding or adding new tenants. fixes https://github.com/KelvinTegelaar/CIPP/issues/5278 --- .../Activity Triggers/Push-ExecOnboardTenantQueue.ps1 | 1 + .../HTTP Functions/CIPP/Setup/Invoke-ExecAddTenant.ps1 | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecOnboardTenantQueue.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecOnboardTenantQueue.ps1 index f7cd1a3dc2a0..23ca76c1adeb 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecOnboardTenantQueue.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecOnboardTenantQueue.ps1 @@ -412,6 +412,7 @@ function Push-ExecOnboardTenantQueue { $TenantOnboarding.Logs = [string](ConvertTo-Json -InputObject @($Logs) -Compress) Add-CIPPAzDataTableEntity @OnboardTable -Entity $TenantOnboarding -Force -ErrorAction Stop Write-LogMessage -API 'Onboarding' -message "Tenant onboarding succeeded for $($Relationship.customer.displayName)" -Sev 'Info' + Write-LogMessage -API 'NewTenant' -message "New tenant onboarded: $($Relationship.customer.displayName) ($($Relationship.customer.id))" -Sev 'Info' } else { $Logs.Add([PSCustomObject]@{ Date = (Get-Date).ToUniversalTime(); Log = 'API Test failed: {0}' -f $ApiError }) $OnboardingSteps.Step5.Status = 'failed' diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecAddTenant.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecAddTenant.ps1 index ea5bcb8d9158..f1345a069ea6 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecAddTenant.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecAddTenant.ps1 @@ -87,7 +87,7 @@ function Invoke-ExecAddTenant { # Add tenant to table Add-CIPPAzDataTableEntity @TenantsTable -Entity $NewTenant -Force | Out-Null $Results = @{'message' = "Successfully added tenant $displayName ($defaultDomainName) to the tenant list with Direct Tenant status. Permission refresh queued, the tenant will be available shortly."; 'severity' = 'success' } - Write-LogMessage -tenant $defaultDomainName -tenantid $tenantId -API 'Add-Tenant' -message "Added tenant $displayName ($defaultDomainName) with Direct Tenant status." -Sev 'Info' + Write-LogMessage -tenant $defaultDomainName -tenantid $tenantId -API 'NewTenant' -message "Added tenant $displayName ($defaultDomainName) with Direct Tenant status." -Sev 'Info' # Trigger CPV refresh to push remaining permissions to this specific tenant try { From 77514262aac053bf48f5306582c176e313fdb032 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Wed, 28 Jan 2026 15:42:40 -0500 Subject: [PATCH 292/503] Refactor test orchestration to per-tenant lists Move test enumeration out of Invoke-CIPPDBTestsRun into a new Push-CIPPTestsList activity. Invoke-CIPPDBTestsRun now builds a batch of per-tenant CIPPTestsList activities and starts a 'TestsList' orchestrator, simplifying orchestration responsibility. Added Push-CIPPTestsList which enumerates Invoke-CippTest* functions, verifies tenant DB data, builds per-tenant test batches, and starts a per-tenant 'TestsRun_{Tenant}' orchestrator. Also updated logging/messages to reflect the new flow. --- .DS_Store | Bin 0 -> 10244 bytes .../Tests/Invoke-CIPPDBTestsRun.ps1 | 32 +++------ .../Tests/Push-CIPPTestsList.ps1 | 67 ++++++++++++++++++ 3 files changed, 78 insertions(+), 21 deletions(-) create mode 100644 .DS_Store create mode 100644 Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Tests/Push-CIPPTestsList.ps1 diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..d193e2e23923124fe3d63a92b84181b4ef94c6c1 GIT binary patch literal 10244 zcmeHMU2GIp6u#fIzzicY)RsS4c4Y-Bl(3ZYCkoqc8~JJZ+m`I7vGc zgb@fM5Jn)3Kp25A0(U|LsAlsf^ZFgOFalu&!U$X+0sel7(Zy*ppkspiuMWETTL7Zf zB=#HiHQpf_3urQ+V}jBMH3(Cp!W6+41Hzo-u|O{w&@n-UIYY4dK(I1`9SZ!_Y5th6 z&X5!|yoV78BQQAvy!KbI95b1n8@hh~Zkeu^NL++eRXt_uGeBb4N}?PZQ8ms)Z!Fv-SutLVn9}8C8Nz9 z8EIVC)DT;}zG<`}HnO2H(GY7|y>@g|k?Z0sH}1(Cu?B4CxNrc$OMuOo;LwS2Z*(_r z<>=|SM&O|3mQvz?qAHym6b@c0JE%-o)ftiQp5DHF`!kwWbd5cRmG#V$(VF!PH>1sT z$=|Jc$KF*iO5|sE-m!`UHjT{6XRUmZl#OP~N}Ep#?R>$ptX+<4dZuId_ISoIkB{+* zDu#OcT&Lir{KVe6)SxNFGNz5YH5vF(|Un?^xVW}%_e&JGwCYUa+HfB%9=MCgp$ zlxOKF=FFo-(;L>}vMkn!EQ%|#c6z@~PIfptpHcTmB9TQ)WqBW$rwRt$g;iY|gL((Zd@_w7t6|JPw3)n`NaZ?+gr(Gp>+D zQmwMAeRYs8+@5v4or8vb?U7PjWVz4n&lZfGLxjSzMY}BfKO@PxNtqnUQeB;3<)AmA^@%fO2#mx*rtvjNfZV;oN=1Xo*D z3@ER25td*rY#Tes2H7w>!Cq!>v5(nlc7~l}-?EGBGW&)7%6?gy) z6!mxrjaZE)tVIg#=)ey2ArBn}4#PqbB@AI0PvIFni|23xFX1)3j+1y3Z{vM@fDiF0 zKF1e0hc9svKjJce!WI09tN2S&q#9{~6pC}xAE&AA&t=qP@-?54I!@WGU?yIevJtww!*~+H%k2YTepulSx zqSr&;M|(w%_-OYLF%hYL4yp^IU6c;SvGF7nNm))sZ5mHo`2yE|K#fPWDU>jRU82@U zwW*XWf?Xb65!EEhQNbpnjjC2f8L0TSYoaSu4YFW1s2f$SnsQOFo79D>#uUM{L|da8 z&n)>?Vf<@$o_$Xkzd{)Q2UUw!G?pQ zC}0Gmgz+&vjpKM8FW^PIf>#OYC-Da1{2jcD_wW%;;|$K?6Cd&4;5%Hx&l51*QHf#v zF^=ISp{Qv*k`i$Y? zVFbbm+}a4Bx;5R}LTlXYEzQr`V{{#&i#M(}Ca7;hw~9Xkh~dZa^>lIF;3&lJCj;4- dpuRcj_~u0ar#}Pi|2k+3_y6Jk|L)%Z{|g1diCzEz literal 0 HcmV?d00001 diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Tests/Invoke-CIPPDBTestsRun.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Tests/Invoke-CIPPDBTestsRun.ps1 index 7c30210d90a3..e4ecb119bdcb 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Tests/Invoke-CIPPDBTestsRun.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Tests/Invoke-CIPPDBTestsRun.ps1 @@ -25,16 +25,6 @@ function Invoke-CIPPDBTestsRun { return $true } try { - $AllTests = Get-Command -Name 'Invoke-CippTest*' -Module CIPPCore | Select-Object -ExpandProperty Name | ForEach-Object { - $_ -replace '^Invoke-CippTest', '' - } - - if ($AllTests.Count -eq 0) { - Write-LogMessage -API 'Tests' -message 'No test functions found.' -sev Error - return - } - - Write-Information "Found $($AllTests.Count) test functions to run" $AllTenantsList = if ($TenantFilter -eq 'allTenants') { $DbCounts = Get-CIPPDbItem -CountsOnly -TenantFilter 'allTenants' $TenantsWithData = $DbCounts | Where-Object { $_.Count -gt 0 } | Select-Object -ExpandProperty PartitionKey -Unique @@ -55,31 +45,31 @@ function Invoke-CIPPDBTestsRun { return } - # Build batch: all tests for all tenants + # Build batch of per-tenant list activities + # Each activity will start its own orchestrator for that tenant's tests $Batch = foreach ($Tenant in $AllTenantsList) { - foreach ($Test in $AllTests) { - @{ - FunctionName = 'CIPPTest' - TenantFilter = $Tenant - TestId = $Test - } + @{ + FunctionName = 'CIPPTestsList' + TenantFilter = $Tenant } } - Write-Information "Built batch of $($Batch.Count) test activities ($($AllTests.Count) tests x $($AllTenantsList.Count) tenants)" + Write-Information "Built batch of $($Batch.Count) tenant test list activities" + # Start orchestrator to dispatch per-tenant test orchestrators $InputObject = [PSCustomObject]@{ - OrchestratorName = 'TestsRun' + OrchestratorName = 'TestsList' Batch = @($Batch) SkipLog = $true } + Write-Information "InputObject: $($InputObject | ConvertTo-Json -Depth 5 -Compress)" $InstanceId = Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Depth 5 -Compress) - Write-Information "Started tests orchestration with ID = '$InstanceId'" + Write-Information "Started tests list orchestration with ID = '$InstanceId'" return @{ InstanceId = $InstanceId - Message = "Tests orchestration started: $($AllTests.Count) tests for $($AllTenantsList.Count) tenants" + Message = "Tests orchestration started: $($AllTenantsList.Count) tenant orchestrators will be created" } } catch { $ErrorMessage = Get-CippException -Exception $_ diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Tests/Push-CIPPTestsList.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Tests/Push-CIPPTestsList.ps1 new file mode 100644 index 000000000000..dc5f57c87f4a --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Tests/Push-CIPPTestsList.ps1 @@ -0,0 +1,67 @@ +function Push-CIPPTestsList { + <# + .FUNCTIONALITY + Entrypoint + #> + param($Item) + + $TenantFilter = $Item.TenantFilter + + try { + Write-Information "Building test list for tenant: $TenantFilter" + + # Get all test functions + $AllTests = Get-Command -Name 'Invoke-CippTest*' -Module CIPPCore | Select-Object -ExpandProperty Name | ForEach-Object { + $_ -replace '^Invoke-CippTest', '' + } + + if ($AllTests.Count -eq 0) { + Write-Information 'No test functions found' + return @() + } + + # Check if tenant has data + $DbCounts = Get-CIPPDbItem -TenantFilter $TenantFilter -CountsOnly + if (($DbCounts | Measure-Object -Property DataCount -Sum).Sum -eq 0) { + Write-Information "Tenant $TenantFilter has no data in database. Skipping tests." + return @() + } + + # Build test batch for this tenant + $TestBatch = foreach ($Test in $AllTests) { + [PSCustomObject]@{ + FunctionName = 'CIPPTest' + TenantFilter = $TenantFilter + TestId = $Test + } + } + + Write-Information "Built $($TestBatch.Count) test activities for tenant $TenantFilter" + + # Start orchestrator for this tenant's tests + $InputObject = [PSCustomObject]@{ + OrchestratorName = "TestsRun_$TenantFilter" + Batch = @($TestBatch) + SkipLog = $true + } + + $InstanceId = Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Depth 5 -Compress) + Write-Information "Started tests orchestrator for tenant $TenantFilter with ID = '$InstanceId'" + + return @{ + Success = $true + Tenant = $TenantFilter + InstanceId = $InstanceId + TestCount = $TestBatch.Count + } + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Tests' -tenant $TenantFilter -message "Failed to start tests for tenant: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + return @{ + Success = $false + Tenant = $TenantFilter + Error = $ErrorMessage.NormalizedError + } + } +} From c3187cfe65c385be8ac7d6fcb19af208ea15af44 Mon Sep 17 00:00:00 2001 From: rvdwegen Date: Wed, 28 Jan 2026 21:47:45 +0100 Subject: [PATCH 293/503] Standards: replace users graph calls with cippdb --- .../Invoke-CIPPStandardDisableGuests.ps1 | 18 +++++++++++++++--- ...voke-CIPPStandardDisableResourceMailbox.ps1 | 16 ++++++++++++++-- ...Invoke-CIPPStandardDisableSharedMailbox.ps1 | 13 ++++++++++++- .../Invoke-CIPPStandardPerUserMFA.ps1 | 14 +++++++++++++- ...nvoke-CIPPStandardUserPreferredLanguage.ps1 | 14 +++++++++++++- 5 files changed, 67 insertions(+), 8 deletions(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableGuests.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableGuests.ps1 index dd7d018f5413..baa729c653b2 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableGuests.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableGuests.ps1 @@ -45,12 +45,17 @@ function Invoke-CIPPStandardDisableGuests { $checkDays = if ($Settings.days) { $Settings.days } else { 90 } # Default to 90 days if not set. Pre v8.5.0 compatibility $Days = (Get-Date).AddDays(-$checkDays).ToUniversalTime() - $Lookup = $Days.ToString('o') $AuditLookup = (Get-Date).AddDays(-7).ToUniversalTime().ToString('o') try { - $GraphRequest = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/users?`$filter=createdDateTime le $Lookup and userType eq 'Guest' and accountEnabled eq true &`$select=id,UserPrincipalName,signInActivity,mail,userType,accountEnabled,createdDateTime,externalUserState" -scope 'https://graph.microsoft.com/.default' -tenantid $Tenant | - Where-Object { $_.signInActivity.lastSuccessfulSignInDateTime -le $Days } + $AllUsers = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Users' + + $GraphRequest = $AllUsers | Where-Object { + $_.userType -eq 'Guest' -and + $_.accountEnabled -eq $true -and + ($null -ne $_.createdDateTime -and [DateTime]$_.createdDateTime -le $Days) -and + ($null -eq $_.signInActivity -or $null -eq $_.signInActivity.lastSuccessfulSignInDateTime -or [DateTime]$_.signInActivity.lastSuccessfulSignInDateTime -le $Days) + } } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the DisableGuests state for $Tenant. Error: $ErrorMessage" -Sev Error @@ -95,6 +100,13 @@ function Invoke-CIPPStandardDisableGuests { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to process bulk disable guests request: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage } + + # Refresh user cache after remediation + try { + Set-CIPPDBCacheUsers -TenantFilter $Tenant + } catch { + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to refresh user cache after remediation: $($_.Exception.Message)" -sev Warning + } } else { Write-LogMessage -API 'Standards' -tenant $tenant -message "No guests accounts with a login longer than $checkDays days ago." -sev Info } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableResourceMailbox.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableResourceMailbox.ps1 index f41ca24c6f13..a15bcae71e8a 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableResourceMailbox.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableResourceMailbox.ps1 @@ -41,8 +41,13 @@ function Invoke-CIPPStandardDisableResourceMailbox { # Get all users that are able to be try { - $UserList = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/users?$top=999&$filter=accountEnabled eq true and onPremisesSyncEnabled ne true and assignedLicenses/$count eq 0&$count=true&$select=id,userPrincipalName,userType' -Tenantid $Tenant -ComplexFilter | - Where-Object { $_.userType -eq 'Member' } + $AllUsers = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Users' + $UserList = $AllUsers | Where-Object { + $_.accountEnabled -eq $true -and + $_.onPremisesSyncEnabled -ne $true -and + ($null -eq $_.assignedLicenses -or $_.assignedLicenses.Count -eq 0) -and + $_.userType -eq 'Member' + } $ResourceMailboxList = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-Mailbox' -cmdParams @{ Filter = "RecipientTypeDetails -eq 'RoomMailbox' -or RecipientTypeDetails -eq 'EquipmentMailbox'" } -Select 'UserPrincipalName,DisplayName,RecipientTypeDetails,ExternalDirectoryObjectId' | Where-Object { $_.ExternalDirectoryObjectId -in $UserList.id } } catch { @@ -84,6 +89,13 @@ function Invoke-CIPPStandardDisableResourceMailbox { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to process bulk disable resource mailboxes request: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage } + + # Refresh user cache after remediation + try { + Set-CIPPDBCacheUsers -TenantFilter $Tenant + } catch { + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to refresh user cache after remediation: $($_.Exception.Message)" -sev Warning + } } else { Write-LogMessage -API 'Standards' -tenant $Tenant -message 'All Entra accounts for resource mailboxes are already disabled.' -sev Info } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSharedMailbox.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSharedMailbox.ps1 index 8f2c04bb3b89..1f1202c0a164 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSharedMailbox.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSharedMailbox.ps1 @@ -37,7 +37,11 @@ function Invoke-CIPPStandardDisableSharedMailbox { param($Tenant, $Settings) try { - $UserList = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/users?$top=999&$filter=accountEnabled eq true and onPremisesSyncEnabled ne true&$count=true&$select=id,userPrincipalName' -Tenantid $Tenant -ComplexFilter + $AllUsers = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Users' + $UserList = $AllUsers | Where-Object { + $_.accountEnabled -eq $true -and + $_.onPremisesSyncEnabled -ne $true + } $SharedMailboxList = (New-GraphGetRequest -uri "https://outlook.office365.com/adminapi/beta/$($Tenant)/Mailbox" -Tenantid $Tenant -scope ExchangeOnline | Where-Object { $_.RecipientTypeDetails -eq 'SharedMailbox' -or $_.RecipientTypeDetails -eq 'SchedulingMailbox' -and $_.UserPrincipalName -in $UserList.UserPrincipalName }) } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message @@ -78,6 +82,13 @@ function Invoke-CIPPStandardDisableSharedMailbox { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to process bulk disable shared mailboxes request: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage } + + # Refresh user cache after remediation + try { + Set-CIPPDBCacheUsers -TenantFilter $Tenant + } catch { + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to refresh user cache after remediation: $($_.Exception.Message)" -sev Warning + } } else { Write-LogMessage -API 'Standards' -tenant $Tenant -message 'All Entra accounts for shared mailboxes are already disabled.' -sev Info } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPerUserMFA.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPerUserMFA.ps1 index e4ea97dc4389..98d5a2fcb762 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPerUserMFA.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPerUserMFA.ps1 @@ -41,7 +41,12 @@ function Invoke-CIPPStandardPerUserMFA { param($Tenant, $Settings) try { - $GraphRequest = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/users?`$top=999&`$select=userPrincipalName,displayName,accountEnabled,perUserMfaState&`$filter=userType eq 'Member' and accountEnabled eq true and displayName ne 'On-Premises Directory Synchronization Service Account'&`$count=true" -tenantid $Tenant -ComplexFilter + $AllUsers = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Users' + $GraphRequest = $AllUsers | Where-Object { + $_.userType -eq 'Member' -and + $_.accountEnabled -eq $true -and + $_.displayName -ne 'On-Premises Directory Synchronization Service Account' + } } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the PerUserMFA state for $Tenant. Error: $ErrorMessage" -Sev Error @@ -58,6 +63,13 @@ function Invoke-CIPPStandardPerUserMFA { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to enforce MFA for all users: $ErrorMessage" -sev Error } + + # Refresh user cache after remediation + try { + Set-CIPPDBCacheUsers -TenantFilter $Tenant + } catch { + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to refresh user cache after remediation: $($_.Exception.Message)" -sev Warning + } } } if ($Settings.alert -eq $true) { diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardUserPreferredLanguage.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardUserPreferredLanguage.ps1 index c124c14d352e..08c7dfd53772 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardUserPreferredLanguage.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardUserPreferredLanguage.ps1 @@ -33,7 +33,12 @@ function Invoke-CIPPStandardUserPreferredLanguage { $preferredLanguage = $Settings.preferredLanguage.value try { - $IncorrectUsers = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/users?`$top=999&`$select=userPrincipalName,displayName,preferredLanguage,userType,onPremisesSyncEnabled&`$filter=preferredLanguage ne '$preferredLanguage' and userType eq 'Member' and onPremisesSyncEnabled ne true&`$count=true" -tenantid $Tenant -ComplexFilter + $AllUsers = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Users' + $IncorrectUsers = $AllUsers | Where-Object { + ($null -eq $_.preferredLanguage -or $_.preferredLanguage -ne $preferredLanguage) -and + $_.userType -eq 'Member' -and + $_.onPremisesSyncEnabled -ne $true + } } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the UserPreferredLanguage state for $Tenant. Error: $ErrorMessage" -Sev Error @@ -61,6 +66,13 @@ function Invoke-CIPPStandardUserPreferredLanguage { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to set preferred language to $preferredLanguage for all users." -sev Error -LogData $ErrorMessage } + + # Refresh user cache after remediation + try { + Set-CIPPDBCacheUsers -TenantFilter $Tenant + } catch { + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to refresh user cache after remediation: $($_.Exception.Message)" -sev Warning + } } } From 64740a9d61393e310dfc06c19e7d73224c0cab3c Mon Sep 17 00:00:00 2001 From: rvdwegen Date: Wed, 28 Jan 2026 22:08:28 +0100 Subject: [PATCH 294/503] Standards: replace servicePrincipals graph call with cippdb --- .../Public/Standards/Invoke-CIPPStandardAppDeploy.ps1 | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAppDeploy.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAppDeploy.ps1 index 528cf962773e..318e751bfd2e 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAppDeploy.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAppDeploy.ps1 @@ -36,7 +36,7 @@ function Invoke-CIPPStandardAppDeploy { Write-Information "Running AppDeploy standard for tenant $($Tenant)." $AppsToAdd = $Settings.appids -split ',' - $AppExists = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/servicePrincipals?$top=999&$select=id,appId,displayName,applicationTemplateId' -tenantid $Tenant + $AppExists = New-CIPPDbRequest -TenantFilter $Tenant -Type 'ServicePrincipals' $Mode = $Settings.mode ?? 'copy' $ExpectedValue = [PSCustomObject]@{ state = 'Configured correctly' } @@ -271,6 +271,13 @@ function Invoke-CIPPStandardAppDeploy { } } } + + # Refresh service principals cache after remediation + try { + Set-CIPPDBCacheServicePrincipals -TenantFilter $Tenant + } catch { + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to refresh service principals cache after remediation: $($_.Exception.Message)" -sev Warning + } } if ($Settings.alert) { From 3185d0b2d8f33d12262a00cb93e8a82b066ba796 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Wed, 28 Jan 2026 22:55:26 +0100 Subject: [PATCH 295/503] feat: add new calendar properties to room functions - Added 'AddOrganizerToSubject', 'DeleteSubject', and 'RemoveCanceledMeetings' to the calendar properties in Invoke-EditRoomMailbox and Invoke-ListRooms functions for enhanced functionality. --- .../Email-Exchange/Resources/Invoke-EditRoomMailbox.ps1 | 3 ++- .../Email-Exchange/Resources/Invoke-ListRooms.ps1 | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Resources/Invoke-EditRoomMailbox.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Resources/Invoke-EditRoomMailbox.ps1 index 7b976f0b9861..652ca7868e3b 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Resources/Invoke-EditRoomMailbox.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Resources/Invoke-EditRoomMailbox.ps1 @@ -59,7 +59,8 @@ Function Invoke-EditRoomMailbox { $CalendarProperties = @( 'AllowConflicts', 'AllowRecurringMeetings', 'BookingWindowInDays', 'MaximumDurationInMinutes', 'ProcessExternalMeetingMessages', 'EnforceCapacity', - 'ForwardRequestsToDelegates', 'ScheduleOnlyDuringWorkHours ', 'AutomateProcessing' + 'ForwardRequestsToDelegates', 'ScheduleOnlyDuringWorkHours ', 'AutomateProcessing', + 'AddOrganizerToSubject', 'DeleteSubject', 'RemoveCanceledMeetings' ) foreach ($prop in $CalendarProperties) { diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Resources/Invoke-ListRooms.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Resources/Invoke-ListRooms.ps1 index 4218cd7a19ba..d8756f9b6a6e 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Resources/Invoke-ListRooms.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Resources/Invoke-ListRooms.ps1 @@ -84,6 +84,9 @@ Function Invoke-ListRooms { ForwardRequestsToDelegates = $CalendarProperties.ForwardRequestsToDelegates ScheduleOnlyDuringWorkHours = $CalendarProperties.ScheduleOnlyDuringWorkHours AutomateProcessing = $CalendarProperties.AutomateProcessing + AddOrganizerToSubject = $CalendarProperties.AddOrganizerToSubject + DeleteSubject = $CalendarProperties.DeleteSubject + RemoveCanceledMeetings = $CalendarProperties.RemoveCanceledMeetings # Calendar Configuration Properties WorkDays = if ([string]::IsNullOrWhiteSpace($CalendarConfigurationProperties.WorkDays)) { $null } else { $CalendarConfigurationProperties.WorkDays } From 404f5fb68268d48b37cfa11dd76490cc53bf67c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Wed, 28 Jan 2026 23:58:32 +0100 Subject: [PATCH 296/503] feat: enhance inactive user alert functionality - Added support for dynamic inactive days based on input. --- .../Get-CIPPAlertInactiveLicensedUsers.ps1 | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertInactiveLicensedUsers.ps1 b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertInactiveLicensedUsers.ps1 index 964e60d5a375..09288d3fee13 100644 --- a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertInactiveLicensedUsers.ps1 +++ b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertInactiveLicensedUsers.ps1 @@ -15,10 +15,26 @@ function Get-CIPPAlertInactiveLicensedUsers { try { try { - $Lookup = (Get-Date).AddDays(-90).ToUniversalTime() + $inactiveDays = 90 + $excludeDisabled = $false + if ($InputValue -is [hashtable] -or $InputValue -is [pscustomobject]) { + $excludeDisabled = [bool]$InputValue.ExcludeDisabled + if ($null -ne $InputValue.DaysSinceLastLogin -and $InputValue.DaysSinceLastLogin -ne '') { + $parsedDays = 0 + if ([int]::TryParse($InputValue.DaysSinceLastLogin.ToString(), [ref]$parsedDays) -and $parsedDays -gt 0) { + $inactiveDays = $parsedDays + } + } + } elseif ($InputValue -eq $true) { + # Backwards compatibility: legacy single-input boolean means exclude disabled users + $excludeDisabled = $true + } + + $Lookup = (Get-Date).AddDays(-$inactiveDays).ToUniversalTime() + Write-Host "Checking for users inactive since $Lookup (excluding disabled: $excludeDisabled)" # Build base filter - cannot filter assignedLicenses server-side - $BaseFilter = if ($InputValue -eq $true) { 'accountEnabled eq true' } else { '' } + $BaseFilter = if ($excludeDisabled) { 'accountEnabled eq true' } else { '' } $Uri = if ($BaseFilter) { "https://graph.microsoft.com/beta/users?`$filter=$BaseFilter&`$select=id,UserPrincipalName,signInActivity,mail,userType,accountEnabled,assignedLicenses" From 99156181a2db127649cc76527279770f207b0650 Mon Sep 17 00:00:00 2001 From: rvdwegen Date: Thu, 29 Jan 2026 00:20:47 +0100 Subject: [PATCH 297/503] Standards: replace exo mailboxstats with DB --- .../Invoke-CIPPStandardcalDefault.ps1 | 104 +++++++++--------- 1 file changed, 53 insertions(+), 51 deletions(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardcalDefault.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardcalDefault.ps1 index e039625d35ec..2edce2a10d35 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardcalDefault.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardcalDefault.ps1 @@ -50,63 +50,65 @@ function Invoke-CIPPStandardcalDefault { } if ($Settings.remediate -eq $true) { - $Mailboxes = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-Mailbox' | Sort-Object UserPrincipalName - $TotalMailboxes = $Mailboxes.Count - Write-LogMessage -API 'Standards' -tenant $Tenant -message "Started setting default calendar permissions for $($TotalMailboxes) mailboxes." -sev Info - - # Retrieve the last run status - $LastRunTable = Get-CIPPTable -Table StandardsLastRun - $Filter = "RowKey eq 'calDefaults' and PartitionKey eq '{0}'" -f $tenant - $LastRun = Get-CIPPAzDataTableEntity @LastRunTable -Filter $Filter - - $startIndex = 0 - if ($LastRun -and $LastRun.processedMailboxes -lt $LastRun.totalMailboxes ) { - $startIndex = $LastRun.processedMailboxes - } + try { + # Get calendar permissions from cache - this contains the calendar Identity we need + $CalendarPermissions = New-CIPPDbRequest -TenantFilter $Tenant -Type 'CalendarPermissions' - $SuccessCounter = if ($startIndex -eq 0) { 0 } else { [int64]$LastRun.currentSuccessCount } - $processedMailboxes = $startIndex - $Mailboxes = $Mailboxes[$startIndex..($TotalMailboxes - 1)] - foreach ($Mailbox in $Mailboxes) { - try { - $CalendarFolders = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-MailboxFolderStatistics' -cmdParams @{identity = $Mailbox.UserPrincipalName; FolderScope = 'Calendar' } -Anchor $Mailbox.UserPrincipalName | Where-Object { $_.FolderType -eq 'Calendar' } - foreach ($Folder in $CalendarFolders) { - try { - New-ExoRequest -tenantid $Tenant -cmdlet 'Set-MailboxFolderPermission' -cmdParams @{Identity = "$($Mailbox.UserPrincipalName):$($Folder.FolderId)"; User = 'Default'; AccessRights = $permissionLevel } -Anchor $Mailbox.UserPrincipalName - Write-LogMessage -API 'Standards' -tenant $Tenant -message "Set default folder permission for $($Mailbox.UserPrincipalName):\$($Folder.Name) to $permissionLevel" -sev Debug - $SuccessCounter++ - } catch { - $ErrorMessage = Get-CippException -Exception $_ - Write-LogMessage -API 'Standards' -tenant $Tenant -message "Could not set default calendar permissions for $($Mailbox.UserPrincipalName). Error: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage - } - } - } catch { - $ErrorMessage = Get-CippException -Exception $_ - Write-LogMessage -API 'Standards' -tenant $Tenant -message "Could not set default calendar permissions for $($Mailbox.UserPrincipalName). Error: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + if (-not $CalendarPermissions) { + Write-LogMessage -API 'Standards' -tenant $Tenant -message 'No cached calendar permissions found. Please ensure the mailbox cache has been populated.' -sev Error + return + } + + # Filter to only Default user permissions that don't match target level + $DefaultPermissions = $CalendarPermissions | Where-Object { $_.User -eq 'Default' } + $NeedsUpdate = $DefaultPermissions | Where-Object { + $currentRights = if ($_.AccessRights -is [array]) { $_.AccessRights -join ',' } else { $_.AccessRights } + $currentRights -ne $permissionLevel + } + + $TotalCalendars = $DefaultPermissions.Count + $CalendarsToUpdate = $NeedsUpdate.Count + + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Found $TotalCalendars calendars. $CalendarsToUpdate need permission update to $permissionLevel." -sev Info + + if ($CalendarsToUpdate -eq 0) { + Write-LogMessage -API 'Standards' -tenant $Tenant -message 'All calendars already have the correct default permission level.' -sev Info + return } - $processedMailboxes++ - if ($processedMailboxes % 25 -eq 0) { - $LastRun = @{ - RowKey = 'calDefaults' - PartitionKey = $Tenant - totalMailboxes = $TotalMailboxes - processedMailboxes = $processedMailboxes - currentSuccessCount = $SuccessCounter + + # Set permissions for each calendar that needs updating + $SuccessCounter = 0 + $ErrorCounter = 0 + + foreach ($Calendar in $NeedsUpdate) { + try { + New-ExoRequest -tenantid $Tenant -cmdlet 'Set-MailboxFolderPermission' -cmdParams @{ + Identity = $Calendar.Identity + User = 'Default' + AccessRights = $permissionLevel + } + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Set default calendar permission for $($Calendar.Identity) to $permissionLevel" -sev Debug + $SuccessCounter++ + } catch { + $ErrorCounter++ + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to set calendar permission for $($Calendar.Identity): $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage } - Add-CIPPAzDataTableEntity @LastRunTable -Entity $LastRun -Force } - } - $LastRun = @{ - RowKey = 'calDefaults' - PartitionKey = $Tenant - totalMailboxes = $TotalMailboxes - processedMailboxes = $processedMailboxes - currentSuccessCount = $SuccessCounter + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Successfully set default calendar permissions for $SuccessCounter calendars. $ErrorCounter failed." -sev Info + + # Refresh calendar permissions cache after remediation + try { + Set-CIPPDBCacheMailboxes -TenantFilter $Tenant + } catch { + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to refresh mailbox cache after remediation: $($_.Exception.Message)" -sev Warning } - Add-CIPPAzDataTableEntity @LastRunTable -Entity $LastRun -Force - Write-LogMessage -API 'Standards' -tenant $Tenant -message "Successfully set default calendar permissions for $SuccessCounter out of $TotalMailboxes mailboxes." -sev Info + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Could not set default calendar permissions. Error: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage } - } + +} From 3a3d3786d548ff166e056e98b767a820f1b2fad4 Mon Sep 17 00:00:00 2001 From: rvdwegen Date: Thu, 29 Jan 2026 12:47:43 +0100 Subject: [PATCH 298/503] Limit params on Search-CIPPDbData --- .../Invoke-ExecUniversalSearchV2.ps1 | 22 +++++++++++++++++ Modules/CIPPCore/Public/Search-CIPPDbData.ps1 | 24 +++++++++++++++---- 2 files changed, 41 insertions(+), 5 deletions(-) create mode 100644 Modules/CIPPCore/Public/Entrypoints/Invoke-ExecUniversalSearchV2.ps1 diff --git a/Modules/CIPPCore/Public/Entrypoints/Invoke-ExecUniversalSearchV2.ps1 b/Modules/CIPPCore/Public/Entrypoints/Invoke-ExecUniversalSearchV2.ps1 new file mode 100644 index 000000000000..25cb9e964f4c --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/Invoke-ExecUniversalSearchV2.ps1 @@ -0,0 +1,22 @@ +function Invoke-ExecUniversalSearchV2 { + <# + .FUNCTIONALITY + Entrypoint,AnyTenant + .ROLE + CIPP.Core.Read + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $TenantFilter = $Request.Query.tenantFilter + $SearchTerms = $Request.Query.searchTerms + $Limit = if ($Request.Query.limit) { [int]$Request.Query.limit } else { 10 } + + $Results = Search-CIPPDbData -TenantFilter $TenantFilter -SearchTerms $SearchTerms -Types 'Users' -Limit $Limit + + return [HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::OK + Body = @($Results) + } + +} diff --git a/Modules/CIPPCore/Public/Search-CIPPDbData.ps1 b/Modules/CIPPCore/Public/Search-CIPPDbData.ps1 index 1ef40a996260..a782821fa989 100644 --- a/Modules/CIPPCore/Public/Search-CIPPDbData.ps1 +++ b/Modules/CIPPCore/Public/Search-CIPPDbData.ps1 @@ -27,6 +27,9 @@ function Search-CIPPDbData { .PARAMETER MaxResultsPerType Maximum number of results to return per type. Default is unlimited (0) + .PARAMETER Limit + Maximum total number of results to return across all types. Default is unlimited (0) + .EXAMPLE Search-CIPPDbData -TenantFilter 'contoso.onmicrosoft.com' -SearchTerms 'john.doe' -Types 'Users', 'Groups' @@ -63,7 +66,10 @@ function Search-CIPPDbData { [switch]$MatchAll, [Parameter(Mandatory = $false)] - [int]$MaxResultsPerType = 0 + [int]$MaxResultsPerType = 0, + + [Parameter(Mandatory = $false)] + [int]$Limit = 0 ) try { @@ -91,9 +97,9 @@ function Search-CIPPDbData { } # Process each data type - foreach ($Type in $Types) { + :typeLoop foreach ($Type in $Types) { Write-Verbose "Searching type: $Type" - $TypeResults = [System.Collections.Generic.List[object]]::new() + $TypeResultCount = 0 # Search across all tenants foreach ($Tenant in $TenantsToSearch) { @@ -145,10 +151,18 @@ function Search-CIPPDbData { Timestamp = $Item.Timestamp } $Results.Add($ResultItem) + $TypeResultCount++ + + # Check total limit first + if ($Limit -gt 0 -and $Results.Count -ge $Limit) { + Write-Verbose "Reached total limit of $Limit results" + break typeLoop + } # Check max results per type - if ($MaxResultsPerType -gt 0 -and $Results.Count -ge $MaxResultsPerType) { - break + if ($MaxResultsPerType -gt 0 -and $TypeResultCount -ge $MaxResultsPerType) { + Write-Verbose "Reached max results per type ($MaxResultsPerType) for type '$Type'" + continue typeLoop } } catch { Write-Verbose "Failed to parse JSON for $($Item.RowKey): $($_.Exception.Message)" From 33561960dbf15fa866a433bbe1f4e096ea4f2f98 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Thu, 29 Jan 2026 13:57:21 +0100 Subject: [PATCH 299/503] remove quad9 as valid resolved --- .../Domain Analyser/Push-DomainAnalyserDomain.ps1 | 2 +- .../HTTP Functions/CIPP/Settings/Invoke-ExecDnsConfig.ps1 | 3 +-- .../Tenant/Standards/Invoke-ListDomainHealth.ps1 | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Domain Analyser/Push-DomainAnalyserDomain.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Domain Analyser/Push-DomainAnalyserDomain.ps1 index ed24471113d9..8643b34e1084 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Domain Analyser/Push-DomainAnalyserDomain.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Domain Analyser/Push-DomainAnalyserDomain.ps1 @@ -13,7 +13,7 @@ function Push-DomainAnalyserDomain { $Filter = "PartitionKey eq 'Domains' and RowKey eq 'Domains'" $Config = Get-CIPPAzDataTableEntity @ConfigTable -Filter $Filter - $ValidResolvers = @('Google', 'CloudFlare', 'Quad9') + $ValidResolvers = @('Google', 'CloudFlare') if ($ValidResolvers -contains $Config.Resolver) { $Resolver = $Config.Resolver } else { diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecDnsConfig.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecDnsConfig.ps1 index 46d80f061f29..6ad0becf4381 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecDnsConfig.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecDnsConfig.ps1 @@ -1,4 +1,4 @@ -Function Invoke-ExecDnsConfig { +function Invoke-ExecDnsConfig { <# .FUNCTIONALITY Entrypoint @@ -13,7 +13,6 @@ Function Invoke-ExecDnsConfig { $ValidResolvers = @( 'Google' 'Cloudflare' - 'Quad9' ) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ListDomainHealth.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ListDomainHealth.ps1 index 6930f2cce975..8a87edb67943 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ListDomainHealth.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ListDomainHealth.ps1 @@ -16,7 +16,7 @@ function Invoke-ListDomainHealth { $Filter = "PartitionKey eq 'Domains' and RowKey eq 'Domains'" $Config = Get-CIPPAzDataTableEntity @ConfigTable -Filter $Filter - $ValidResolvers = @('Google', 'CloudFlare', 'Quad9') + $ValidResolvers = @('Google', 'CloudFlare') if ($ValidResolvers -contains $Config.Resolver) { $Resolver = $Config.Resolver } else { From 4875b28b890297ad9cdc9a6e74816e9f5a6ced74 Mon Sep 17 00:00:00 2001 From: James Tarran Date: Thu, 29 Jan 2026 13:36:49 +0000 Subject: [PATCH 300/503] Update Get-CIPPAlertSmtpAuthSuccess.ps1 Fix Get-CIPPAlertSmtpAuthSuccess by changing filter to 'Authenticated SMTP' instead of 'SMTP' --- Modules/CIPPCore/Public/Alerts/Get-CIPPAlertSmtpAuthSuccess.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertSmtpAuthSuccess.ps1 b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertSmtpAuthSuccess.ps1 index 8edc398ff3cc..c7c8e57f4ea6 100644 --- a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertSmtpAuthSuccess.ps1 +++ b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertSmtpAuthSuccess.ps1 @@ -13,7 +13,7 @@ function Get-CIPPAlertSmtpAuthSuccess { try { # Graph API endpoint for sign-ins - $uri = "https://graph.microsoft.com/v1.0/auditLogs/signIns?`$filter=clientAppUsed eq 'SMTP' and status/errorCode eq 0" + $uri = "https://graph.microsoft.com/v1.0/auditLogs/signIns?`$filter=clientAppUsed eq 'Authenticated SMTP' and status/errorCode eq 0" # Call Graph API for the given tenant $SignIns = New-GraphGetRequest -uri $uri -tenantid $TenantFilter From 46eeb04da8846f07bca581d2680bfe85b11d928c Mon Sep 17 00:00:00 2001 From: James Tarran Date: Thu, 29 Jan 2026 13:40:07 +0000 Subject: [PATCH 301/503] Revert "Update identity-openapispec.json" This reverts commit 8fbe5c4a3423447fa3e8894650765d8b177f86d3. --- Tools/identity-openapispec.json | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/Tools/identity-openapispec.json b/Tools/identity-openapispec.json index 96c5c2b5d28a..72e8871a8913 100644 --- a/Tools/identity-openapispec.json +++ b/Tools/identity-openapispec.json @@ -178,13 +178,9 @@ "UserEmail": { "type": "string", "description": "User Principal Name" - }, - "tenantFilter": { - "type": "string", - "description": "Tenant to filter by" } }, - "required": ["UserEmail","tenantFilter"] + "required": ["UserEmail"] } } } From dd14fa7abdec5056006a8fc5edbcd4d3a966d9c0 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Thu, 29 Jan 2026 15:03:59 +0100 Subject: [PATCH 302/503] performance improvements for securescore --- ...oke-CIPPStandardSecureScoreRemediation.ps1 | 65 ++++++++++++++----- 1 file changed, 47 insertions(+), 18 deletions(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSecureScoreRemediation.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSecureScoreRemediation.ps1 index 9dbfee7f6eb1..d96f9702e39e 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSecureScoreRemediation.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSecureScoreRemediation.ps1 @@ -36,7 +36,7 @@ function Invoke-CIPPStandardSecureScoreRemediation { # Get current secure score controls try { - $CurrentControls = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/security/secureScoreControlProfiles' -tenantid $Tenant + $CurrentControls = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/security/secureScoreControlProfiles?$top=999' -tenantid $Tenant } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -tenant $Tenant -message "Could not retrieve Secure Score controls for $Tenant. Error: $ErrorMessage" -sev Error @@ -95,37 +95,66 @@ function Invoke-CIPPStandardSecureScoreRemediation { } if ($Settings.remediate -eq $true) { + $ControlsNeedingUpdate = [System.Collections.Generic.List[object]]::new() + foreach ($Control in $ControlsToUpdate) { # Skip if this is a Defender control (starts with scid_) if ($Control.ControlName -match '^scid_') { + Write-Host 'scid' Write-LogMessage -API 'Standards' -tenant $tenant -message "Skipping Defender control $($Control.ControlName) - cannot be updated via this API" -sev Info continue } - # Build the request body - $Body = @{ - state = $Control.State - comment = $Control.Reason - vendorInformation = @{ - vendor = 'Microsoft' - provider = 'SecureScore' + $CurrentControl = $CurrentControls | Where-Object { $_.id -eq $Control.ControlName } + + # Check if already in desired state + if ($CurrentControl.state -eq $Control.State) { + Write-Host 'Already in state' + Write-LogMessage -API 'Standards' -tenant $tenant -message "Control $($Control.ControlName) is already in state $($Control.State)" -sev Info + } else { + $ControlsNeedingUpdate.Add($Control) + } + } + + # Build bulk requests for all controls that need updating + if ($ControlsNeedingUpdate.Count -gt 0) { + $int = 1 + $BulkRequests = foreach ($Control in $ControlsNeedingUpdate) { + @{ + id = $int++ + method = 'PATCH' + url = "security/secureScoreControlProfiles/$($Control.ControlName)" + body = @{ + state = $Control.State + comment = $Control.Reason + vendorInformation = @{ + vendor = 'Microsoft' + provider = 'SecureScore' + } + } + headers = @{ + 'Content-Type' = 'application/json' + } } } try { - $CurrentControl = $CurrentControls | Where-Object { $_.id -eq $Control.ControlName } - - # Check if already in desired state - if ($CurrentControl.state -eq $Control.State) { - Write-LogMessage -API 'Standards' -tenant $tenant -message "Control $($Control.ControlName) is already in state $($Control.State)" -sev Info - } else { - # Update the control - $null = New-GraphPostRequest -uri "https://graph.microsoft.com/beta/security/secureScoreControlProfiles/$($Control.ControlName)" -tenantid $Tenant -type PATCH -Body (ConvertTo-Json -InputObject $Body -Compress) - Write-LogMessage -API 'Standards' -tenant $tenant -message "Successfully set control $($Control.ControlName) to $($Control.State)" -sev Info + $BulkResults = New-GraphBulkRequest -tenantid $Tenant -Requests @($BulkRequests) + + for ($i = 0; $i -lt $BulkResults.Count; $i++) { + $result = $BulkResults[$i] + $Control = $ControlsNeedingUpdate[$i] + + if ($result.status -eq 200 -or $result.status -eq 204) { + Write-LogMessage -API 'Standards' -tenant $tenant -message "Successfully set control $($Control.ControlName) to $($Control.State)" -sev Info + } else { + $errorMsg = if ($result.body.error.message) { $result.body.error.message } else { "Unknown error (Status: $($result.status))" } + Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to set control $($Control.ControlName) to $($Control.State). Error: $errorMsg" -sev Error + } } } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message - Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to set control $($Control.ControlName) to $($Control.State). Error: $ErrorMessage" -sev Error + Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to update secure score controls in bulk. Error: $ErrorMessage" -sev Error } } } From 8d76ec67aad4f45d8457b5de4ff7e54f07f95c21 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Thu, 29 Jan 2026 15:21:32 +0100 Subject: [PATCH 303/503] line breaks --- .../Standards/Invoke-CIPPStandardEnableMailboxAuditing.ps1 | 2 +- .../Standards/Invoke-CIPPStandardQuarantineRequestAlert.ps1 | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableMailboxAuditing.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableMailboxAuditing.ps1 index 17eee3e1b987..6f63e7477bbd 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableMailboxAuditing.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableMailboxAuditing.ps1 @@ -95,7 +95,7 @@ function Invoke-CIPPStandardEnableMailboxAuditing { # Disable audit bypass for all mailboxes that have it enabled - $BypassMailboxes = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-MailboxAuditBypassAssociation' -select 'GUID, AuditBypassEnabled, Name' -useSystemMailbox $true | Where-Object { $_.AuditBypassEnabled -eq $true } + #$BypassMailboxes = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-MailboxAuditBypassAssociation' -select 'GUID, AuditBypassEnabled, Name' -useSystemMailbox $true | Where-Object { $_.AuditBypassEnabled -eq $true } $Request = foreach ($Mailbox in $BypassMailboxes) { @{ CmdletInput = @{ diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardQuarantineRequestAlert.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardQuarantineRequestAlert.ps1 index 2e4a10d64eda..2638eae4e1de 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardQuarantineRequestAlert.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardQuarantineRequestAlert.ps1 @@ -39,8 +39,7 @@ function Invoke-CIPPStandardQuarantineRequestAlert { $PolicyName = 'CIPP User requested to release a quarantined message' try { - $CurrentState = New-ExoRequest -TenantId $Tenant -cmdlet 'Get-ProtectionAlert' -Compliance | - Where-Object { $_.Name -eq $PolicyName } + $CurrentState = New-ExoRequest -TenantId $Tenant -cmdlet 'Get-ProtectionAlert' -Compliance | Where-Object { $_.Name -eq $PolicyName } } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the QuarantineRequestAlert state for $Tenant. Error: $ErrorMessage" -Sev Error From e42e92a21d2ad20db463735639a66d244aed2298 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 29 Jan 2026 11:14:18 -0500 Subject: [PATCH 304/503] DNSHealth: bump to 1.1.2 and update providers/logic Bump DNSHealth module to 1.1.2 and migrate MailProviders into the new version folder. Replace $PSScriptRoot usages with the module base ($MyInvocation.MyCommand.Module.ModuleBase) for MailProviders file access. Add DMARC-aware handling for SPF soft-fail (~all) in Read-SpfRecord (accept when DMARC p=reject at 100%, otherwise recommend -all). Remove Quad9 DNS-over-HTTPS resolver support from Resolve-DnsHttpsQuery and Set-DnsResolver. Update Microsoft365 MX pattern to include mail.eo.outlook.com. Rename and update Barracuda provider JSON (new name/links). Refresh PSGetModuleInfo metadata to reflect version, dates and file list. --- .../1.1.0/MailProviders/BarracudaESS.json | 10 - .../DNSHealth/{1.1.0 => 1.1.2}/DNSHealth.psd1 | 2 +- .../DNSHealth/{1.1.0 => 1.1.2}/DNSHealth.psm1 | 53 ++-- .../MailProviders/AppRiver.json | 0 .../1.1.2/MailProviders/BarracudaESS.json | 10 + .../MailProviders/Google.json | 0 .../MailProviders/HornetSecurity.json | 0 .../MailProviders/Intermedia.json | 0 .../MailProviders/Microsoft365.json | 2 +- .../MailProviders/Mimecast.json | 0 .../{1.1.0 => 1.1.2}/MailProviders/Null.json | 0 .../MailProviders/Proofpoint.json | 0 .../MailProviders/Reflexion.json | 0 .../MailProviders/Sophos.json | 0 .../MailProviders/SpamTitan.json | 0 .../MailProviders/SymantecCloud.json | 0 .../MailProviders/_template.json | 0 .../{1.1.0 => 1.1.2}/PSGetModuleInfo.xml | 288 +++++++++--------- 18 files changed, 191 insertions(+), 174 deletions(-) delete mode 100644 Modules/DNSHealth/1.1.0/MailProviders/BarracudaESS.json rename Modules/DNSHealth/{1.1.0 => 1.1.2}/DNSHealth.psd1 (99%) rename Modules/DNSHealth/{1.1.0 => 1.1.2}/DNSHealth.psm1 (98%) rename Modules/DNSHealth/{1.1.0 => 1.1.2}/MailProviders/AppRiver.json (100%) create mode 100644 Modules/DNSHealth/1.1.2/MailProviders/BarracudaESS.json rename Modules/DNSHealth/{1.1.0 => 1.1.2}/MailProviders/Google.json (100%) rename Modules/DNSHealth/{1.1.0 => 1.1.2}/MailProviders/HornetSecurity.json (100%) rename Modules/DNSHealth/{1.1.0 => 1.1.2}/MailProviders/Intermedia.json (100%) rename Modules/DNSHealth/{1.1.0 => 1.1.2}/MailProviders/Microsoft365.json (88%) rename Modules/DNSHealth/{1.1.0 => 1.1.2}/MailProviders/Mimecast.json (100%) rename Modules/DNSHealth/{1.1.0 => 1.1.2}/MailProviders/Null.json (100%) rename Modules/DNSHealth/{1.1.0 => 1.1.2}/MailProviders/Proofpoint.json (100%) rename Modules/DNSHealth/{1.1.0 => 1.1.2}/MailProviders/Reflexion.json (100%) rename Modules/DNSHealth/{1.1.0 => 1.1.2}/MailProviders/Sophos.json (100%) rename Modules/DNSHealth/{1.1.0 => 1.1.2}/MailProviders/SpamTitan.json (100%) rename Modules/DNSHealth/{1.1.0 => 1.1.2}/MailProviders/SymantecCloud.json (100%) rename Modules/DNSHealth/{1.1.0 => 1.1.2}/MailProviders/_template.json (100%) rename Modules/DNSHealth/{1.1.0 => 1.1.2}/PSGetModuleInfo.xml (79%) diff --git a/Modules/DNSHealth/1.1.0/MailProviders/BarracudaESS.json b/Modules/DNSHealth/1.1.0/MailProviders/BarracudaESS.json deleted file mode 100644 index cc7349d1435d..000000000000 --- a/Modules/DNSHealth/1.1.0/MailProviders/BarracudaESS.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "Name": "Barracuda Email Security Service", - "_MxComment": "https://campus.barracuda.com/product/essentials/doc/86545480/step-2-configure-office-365-for-inbound-and-outbound-mail/", - "MxMatch": "ess(?.[a-z]{2})?.barracudanetworks.com", - "_SpfComment": "https://campus.barracuda.com/product/essentials/doc/73702276/sender-policy-framework-for-outbound-mail", - "SpfInclude": "spf.ess{0}.barracudanetworks.com", - "SpfReplace": ["Country"], - "_DkimComment": "No configuration found", - "Selectors": [""] -} \ No newline at end of file diff --git a/Modules/DNSHealth/1.1.0/DNSHealth.psd1 b/Modules/DNSHealth/1.1.2/DNSHealth.psd1 similarity index 99% rename from Modules/DNSHealth/1.1.0/DNSHealth.psd1 rename to Modules/DNSHealth/1.1.2/DNSHealth.psd1 index 651cc2720db7..b6f33b295966 100644 --- a/Modules/DNSHealth/1.1.0/DNSHealth.psd1 +++ b/Modules/DNSHealth/1.1.2/DNSHealth.psd1 @@ -12,7 +12,7 @@ RootModule = 'DNSHealth.psm1' # Version number of this module. - ModuleVersion = '1.1.0' + ModuleVersion = '1.1.2' # Supported PSEditions # CompatiblePSEditions = @() diff --git a/Modules/DNSHealth/1.1.0/DNSHealth.psm1 b/Modules/DNSHealth/1.1.2/DNSHealth.psm1 similarity index 98% rename from Modules/DNSHealth/1.1.0/DNSHealth.psm1 rename to Modules/DNSHealth/1.1.2/DNSHealth.psm1 index cbefac309733..26e008d67c06 100644 --- a/Modules/DNSHealth/1.1.0/DNSHealth.psm1 +++ b/Modules/DNSHealth/1.1.2/DNSHealth.psm1 @@ -1155,13 +1155,13 @@ function Read-MXRecord { elseif ($Result.Status -ne 0 -or -not ($Result.Answer)) { if ($Result.Status -eq 3) { $ValidationFails.Add($NoMxValidation) | Out-Null - $MXResults.MailProvider = Get-Content "$PSScriptRoot\MailProviders\Null.json" | ConvertFrom-Json + $MXResults.MailProvider = Get-Content "$($MyInvocation.MyCommand.Module.ModuleBase)\MailProviders\Null.json" | ConvertFrom-Json $MXResults.Selectors = $MXRecords.MailProvider.Selectors } else { $ValidationFails.Add($NoMxValidation) | Out-Null - $MXResults.MailProvider = Get-Content "$PSScriptRoot\MailProviders\Null.json" | ConvertFrom-Json + $MXResults.MailProvider = Get-Content "$($MyInvocation.MyCommand.Module.ModuleBase)\MailProviders\Null.json" | ConvertFrom-Json $MXResults.Selectors = $MXRecords.MailProvider.Selectors } $MXRecords = $null @@ -1183,17 +1183,17 @@ function Read-MXRecord { $MXRecords = $MXRecords | Sort-Object -Property Priority # Attempt to identify mail provider based on MX record - if (Test-Path "$PSScriptRoot\MailProviders") { + if (Test-Path "$($MyInvocation.MyCommand.Module.ModuleBase)\MailProviders") { $ReservedVariables = @{ 'DomainNameDashNotation' = $Domain -replace '\.', '-' } if ($MXRecords.Hostname -eq '') { $ValidationFails.Add($NoMxValidation) | Out-Null - $MXResults.MailProvider = Get-Content "$PSScriptRoot\MailProviders\Null.json" | ConvertFrom-Json + $MXResults.MailProvider = Get-Content "$($MyInvocation.MyCommand.Module.ModuleBase)\MailProviders\Null.json" | ConvertFrom-Json } else { - $ProviderList = Get-ChildItem "$PSScriptRoot\MailProviders" -Exclude '_template.json' | ForEach-Object { + $ProviderList = Get-ChildItem "$($MyInvocation.MyCommand.Module.ModuleBase)\MailProviders" -Exclude '_template.json' | ForEach-Object { try { Get-Content $_ | ConvertFrom-Json -ErrorAction Stop } catch { Write-Verbose $_.Exception.Message } } @@ -1345,7 +1345,7 @@ function Read-SpfRecord { Author: John Duprey #> [CmdletBinding(DefaultParameterSetName = 'Lookup')] - Param( + param( [Parameter(Mandatory = $true, ParameterSetName = 'Lookup')] [Parameter(ParameterSetName = 'Manual')] [string]$Domain, @@ -1774,6 +1774,30 @@ function Read-SpfRecord { $ValidationPasses.Add('The SPF record ends with a hard fail qualifier (-all). This is best practice and will instruct recipients to discard unauthorized senders.') | Out-Null } + elseif ($AllMechanism -eq '~all') { + # Check DMARC policy for soft fail + $DmarcRejectPolicy = $false + try { + $DmarcPolicy = Read-DmarcPolicy -Domain $Domain -ErrorAction Stop + if ($DmarcPolicy.Policy -eq 'reject' -and ($DmarcPolicy.Percent -eq 100 -or $null -eq $DmarcPolicy.Percent)) { + $DmarcRejectPolicy = $true + } + } catch { + Write-Verbose "Unable to read DMARC policy: $($_.Exception.Message)" + } + + if ($DmarcRejectPolicy) { + $ValidationPasses.Add('The SPF record ends with a soft fail qualifier (~all). With DMARC p=reject at 100%, this is acceptable as DMARC will enforce rejection.') | Out-Null + } else { + $ValidationFails.Add('The SPF record should end in -all to prevent spamming.') | Out-Null + $Recommendations.Add([PSCustomObject]@{ + Message = "Replace '~all' with '-all' to make a SPF failure result in a hard fail." + Match = '~all' + Replace = '-all' + }) | Out-Null + } + } + elseif ($Record -ne '') { $ValidationFails.Add('The SPF record should end in -all to prevent spamming.') | Out-Null $Recommendations.Add([PSCustomObject]@{ @@ -1865,7 +1889,7 @@ function Read-SpfRecord { # Output SpfResults object $SpfResults } -#EndRegion './Public/Records/Read-SPFRecord.ps1' 553 +#EndRegion './Public/Records/Read-SPFRecord.ps1' 577 #Region './Public/Records/Read-TlsRptRecord.ps1' -1 function Read-TlsRptRecord { @@ -2269,7 +2293,7 @@ function Resolve-DnsHttpsQuery { if (!$Results) { throw 'Exception querying resolver {0}: {1}' -f $Resolver.Resolver, $Exception.Exception.Message } if ($RecordType -eq 'txt' -and $Results.Answer) { - if ($Resolver -eq 'Cloudflare' -or $Resolver -eq 'Quad9') { + if ($Resolver -eq 'Cloudflare') { $Results.Answer | ForEach-Object { $_.data = $_.data -replace '" "' -replace '"', '' } @@ -2284,9 +2308,9 @@ function Resolve-DnsHttpsQuery { function Set-DnsResolver { [CmdletBinding(SupportsShouldProcess)] - Param( + param( [Parameter()] - [ValidateSet('Google', 'Cloudflare', 'Quad9')] + [ValidateSet('Google', 'Cloudflare')] [string]$Resolver = 'Google' ) @@ -2306,17 +2330,10 @@ function Set-DnsResolver { QueryTemplate = '{0}?name={1}&type={2}' } } - 'Quad9' { - [PSCustomObject]@{ - Resolver = $Resolver - BaseUri = 'https://dns.quad9.net:5053/dns-query' - QueryTemplate = '{0}?name={1}&type={2}' - } - } } } } -#EndRegion './Public/Resolver/Set-DnsResolver.ps1' 35 +#EndRegion './Public/Resolver/Set-DnsResolver.ps1' 28 #Region './Public/Tests/Test-DNSSEC.ps1' -1 function Test-DNSSEC { diff --git a/Modules/DNSHealth/1.1.0/MailProviders/AppRiver.json b/Modules/DNSHealth/1.1.2/MailProviders/AppRiver.json similarity index 100% rename from Modules/DNSHealth/1.1.0/MailProviders/AppRiver.json rename to Modules/DNSHealth/1.1.2/MailProviders/AppRiver.json diff --git a/Modules/DNSHealth/1.1.2/MailProviders/BarracudaESS.json b/Modules/DNSHealth/1.1.2/MailProviders/BarracudaESS.json new file mode 100644 index 000000000000..febadc8c686a --- /dev/null +++ b/Modules/DNSHealth/1.1.2/MailProviders/BarracudaESS.json @@ -0,0 +1,10 @@ +{ + "Name": "Barracuda Email Gateway Defense", + "_MxComment": "https://campus.barracuda.com/product/emailgatewaydefense/doc/167976430/step-2-configure-microsoft-365-for-inbound-and-outbound-mail", + "MxMatch": "ess(?.[a-z]{2})?.barracudanetworks.com", + "_SpfComment": "https://campus.barracuda.com/product/emailgatewaydefense/doc/167976840/sender-policy-framework-for-outbound-mail", + "SpfInclude": "spf.ess{0}.barracudanetworks.com", + "SpfReplace": ["Country"], + "_DkimComment": "No configuration found", + "Selectors": [""] +} \ No newline at end of file diff --git a/Modules/DNSHealth/1.1.0/MailProviders/Google.json b/Modules/DNSHealth/1.1.2/MailProviders/Google.json similarity index 100% rename from Modules/DNSHealth/1.1.0/MailProviders/Google.json rename to Modules/DNSHealth/1.1.2/MailProviders/Google.json diff --git a/Modules/DNSHealth/1.1.0/MailProviders/HornetSecurity.json b/Modules/DNSHealth/1.1.2/MailProviders/HornetSecurity.json similarity index 100% rename from Modules/DNSHealth/1.1.0/MailProviders/HornetSecurity.json rename to Modules/DNSHealth/1.1.2/MailProviders/HornetSecurity.json diff --git a/Modules/DNSHealth/1.1.0/MailProviders/Intermedia.json b/Modules/DNSHealth/1.1.2/MailProviders/Intermedia.json similarity index 100% rename from Modules/DNSHealth/1.1.0/MailProviders/Intermedia.json rename to Modules/DNSHealth/1.1.2/MailProviders/Intermedia.json diff --git a/Modules/DNSHealth/1.1.0/MailProviders/Microsoft365.json b/Modules/DNSHealth/1.1.2/MailProviders/Microsoft365.json similarity index 88% rename from Modules/DNSHealth/1.1.0/MailProviders/Microsoft365.json rename to Modules/DNSHealth/1.1.2/MailProviders/Microsoft365.json index 35fb2c66b9df..13d02769ff7f 100644 --- a/Modules/DNSHealth/1.1.0/MailProviders/Microsoft365.json +++ b/Modules/DNSHealth/1.1.2/MailProviders/Microsoft365.json @@ -1,6 +1,6 @@ { "Name": "Microsoft 365", - "MxMatch": "mail.protection.outlook.com|mx.microsoft", + "MxMatch": "mail.protection.outlook.com|mx.microsoft|mail.eo.outlook.com", "SpfInclude": "spf.protection.outlook.com", "Selectors": ["selector1", "selector2"], "MinimumSelectorPass": 1, diff --git a/Modules/DNSHealth/1.1.0/MailProviders/Mimecast.json b/Modules/DNSHealth/1.1.2/MailProviders/Mimecast.json similarity index 100% rename from Modules/DNSHealth/1.1.0/MailProviders/Mimecast.json rename to Modules/DNSHealth/1.1.2/MailProviders/Mimecast.json diff --git a/Modules/DNSHealth/1.1.0/MailProviders/Null.json b/Modules/DNSHealth/1.1.2/MailProviders/Null.json similarity index 100% rename from Modules/DNSHealth/1.1.0/MailProviders/Null.json rename to Modules/DNSHealth/1.1.2/MailProviders/Null.json diff --git a/Modules/DNSHealth/1.1.0/MailProviders/Proofpoint.json b/Modules/DNSHealth/1.1.2/MailProviders/Proofpoint.json similarity index 100% rename from Modules/DNSHealth/1.1.0/MailProviders/Proofpoint.json rename to Modules/DNSHealth/1.1.2/MailProviders/Proofpoint.json diff --git a/Modules/DNSHealth/1.1.0/MailProviders/Reflexion.json b/Modules/DNSHealth/1.1.2/MailProviders/Reflexion.json similarity index 100% rename from Modules/DNSHealth/1.1.0/MailProviders/Reflexion.json rename to Modules/DNSHealth/1.1.2/MailProviders/Reflexion.json diff --git a/Modules/DNSHealth/1.1.0/MailProviders/Sophos.json b/Modules/DNSHealth/1.1.2/MailProviders/Sophos.json similarity index 100% rename from Modules/DNSHealth/1.1.0/MailProviders/Sophos.json rename to Modules/DNSHealth/1.1.2/MailProviders/Sophos.json diff --git a/Modules/DNSHealth/1.1.0/MailProviders/SpamTitan.json b/Modules/DNSHealth/1.1.2/MailProviders/SpamTitan.json similarity index 100% rename from Modules/DNSHealth/1.1.0/MailProviders/SpamTitan.json rename to Modules/DNSHealth/1.1.2/MailProviders/SpamTitan.json diff --git a/Modules/DNSHealth/1.1.0/MailProviders/SymantecCloud.json b/Modules/DNSHealth/1.1.2/MailProviders/SymantecCloud.json similarity index 100% rename from Modules/DNSHealth/1.1.0/MailProviders/SymantecCloud.json rename to Modules/DNSHealth/1.1.2/MailProviders/SymantecCloud.json diff --git a/Modules/DNSHealth/1.1.0/MailProviders/_template.json b/Modules/DNSHealth/1.1.2/MailProviders/_template.json similarity index 100% rename from Modules/DNSHealth/1.1.0/MailProviders/_template.json rename to Modules/DNSHealth/1.1.2/MailProviders/_template.json diff --git a/Modules/DNSHealth/1.1.0/PSGetModuleInfo.xml b/Modules/DNSHealth/1.1.2/PSGetModuleInfo.xml similarity index 79% rename from Modules/DNSHealth/1.1.0/PSGetModuleInfo.xml rename to Modules/DNSHealth/1.1.2/PSGetModuleInfo.xml index e64f6f169372..85f904f8990e 100644 --- a/Modules/DNSHealth/1.1.0/PSGetModuleInfo.xml +++ b/Modules/DNSHealth/1.1.2/PSGetModuleInfo.xml @@ -1,144 +1,144 @@ - - - - Microsoft.PowerShell.Commands.PSRepositoryItemInfo - System.Management.Automation.PSCustomObject - System.Object - - - DNSHealth - 1.1.0 - Module - CIPP DNS Health Check Module - John Duprey - johnduprey - 2023 John Duprey -
2025-05-27T23:15:27-04:00
- - - - https://github.com/johnduprey/DNSHealth - - - - System.Object[] - System.Array - System.Object - - - PSModule - - - - - System.Collections.Hashtable - System.Object - - - - Command - - - - Read-DmarcPolicy - Read-MtaStsPolicy - Read-DkimRecord - Read-MtaStsRecord - Read-MXRecord - Read-NSRecord - Read-SPFRecord - Read-TlsRptRecord - Read-WhoisRecord - Resolve-DnsHttpsQuery - Set-DnsResolver - Test-DNSSEC - Test-HttpsCertificate - Test-MtaSts - - - - - Function - - - - Read-DmarcPolicy - Read-MtaStsPolicy - Read-DkimRecord - Read-MtaStsRecord - Read-MXRecord - Read-NSRecord - Read-SPFRecord - Read-TlsRptRecord - Read-WhoisRecord - Resolve-DnsHttpsQuery - Set-DnsResolver - Test-DNSSEC - Test-HttpsCertificate - Test-MtaSts - - - - - Workflow - - - - - - - RoleCapability - - - - DscResource - - - - Cmdlet - - - - - - - - - - - https://www.powershellgallery.com/api/v2 - PSGallery - NuGet - - - System.Management.Automation.PSCustomObject - System.Object - - - 2023 John Duprey - CIPP DNS Health Check Module - False - True - True - 0 - 290 - 28670 - 5/27/2025 11:15:27 PM -04:00 - 5/27/2025 11:15:27 PM -04:00 - 5/27/2025 11:15:27 PM -04:00 - PSModule PSFunction_Read-DmarcPolicy PSCommand_Read-DmarcPolicy PSFunction_Read-MtaStsPolicy PSCommand_Read-MtaStsPolicy PSFunction_Read-DkimRecord PSCommand_Read-DkimRecord PSFunction_Read-MtaStsRecord PSCommand_Read-MtaStsRecord PSFunction_Read-MXRecord PSCommand_Read-MXRecord PSFunction_Read-NSRecord PSCommand_Read-NSRecord PSFunction_Read-SPFRecord PSCommand_Read-SPFRecord PSFunction_Read-TlsRptRecord PSCommand_Read-TlsRptRecord PSFunction_Read-WhoisRecord PSCommand_Read-WhoisRecord PSFunction_Resolve-DnsHttpsQuery PSCommand_Resolve-DnsHttpsQuery PSFunction_Set-DnsResolver PSCommand_Set-DnsResolver PSFunction_Test-DNSSEC PSCommand_Test-DNSSEC PSFunction_Test-HttpsCertificate PSCommand_Test-HttpsCertificate PSFunction_Test-MtaSts PSCommand_Test-MtaSts PSIncludes_Function - False - 2025-05-27T23:15:27Z - 1.1.0 - John Duprey - false - Module - DNSHealth.nuspec|MailProviders\Google.json|MailProviders\BarracudaESS.json|MailProviders\_template.json|MailProviders\Null.json|MailProviders\HornetSecurity.json|DNSHealth.psm1|MailProviders\Intermedia.json|MailProviders\Proofpoint.json|MailProviders\Reflexion.json|DNSHealth.psd1|MailProviders\SpamTitan.json|MailProviders\Mimecast.json|MailProviders\Sophos.json|MailProviders\Microsoft365.json|MailProviders\AppRiver.json|MailProviders\SymantecCloud.json - a300d2b0-d468-46d1-88a3-e442a76b655b - 7.0 - - - C:\GitHub\CIPP Workspace\CIPP-API\Modules\DNSHealth\1.1.0 -
-
-
+ + + + Microsoft.PowerShell.Commands.PSRepositoryItemInfo + System.Management.Automation.PSCustomObject + System.Object + + + DNSHealth + 1.1.2 + Module + CIPP DNS Health Check Module + John Duprey + johnduprey + 2023 John Duprey +
2026-01-29T15:52:42-05:00
+ + + + https://github.com/johnduprey/DNSHealth + + + + System.Object[] + System.Array + System.Object + + + PSModule + + + + + System.Collections.Hashtable + System.Object + + + + Command + + + + Read-DmarcPolicy + Read-MtaStsPolicy + Read-DkimRecord + Read-MtaStsRecord + Read-MXRecord + Read-NSRecord + Read-SPFRecord + Read-TlsRptRecord + Read-WhoisRecord + Resolve-DnsHttpsQuery + Set-DnsResolver + Test-DNSSEC + Test-HttpsCertificate + Test-MtaSts + + + + + Workflow + + + + + + + RoleCapability + + + + DscResource + + + + Cmdlet + + + + Function + + + + Read-DmarcPolicy + Read-MtaStsPolicy + Read-DkimRecord + Read-MtaStsRecord + Read-MXRecord + Read-NSRecord + Read-SPFRecord + Read-TlsRptRecord + Read-WhoisRecord + Resolve-DnsHttpsQuery + Set-DnsResolver + Test-DNSSEC + Test-HttpsCertificate + Test-MtaSts + + + + + + + + + + + + https://www.powershellgallery.com/api/v2 + PSGallery + NuGet + + + System.Management.Automation.PSCustomObject + System.Object + + + 2023 John Duprey + CIPP DNS Health Check Module + False + True + True + 0 + 392 + 28878 + 1/29/2026 3:52:42 PM -05:00 + 1/29/2026 3:52:42 PM -05:00 + 1/29/2026 3:52:42 PM -05:00 + PSModule PSFunction_Read-DmarcPolicy PSCommand_Read-DmarcPolicy PSFunction_Read-MtaStsPolicy PSCommand_Read-MtaStsPolicy PSFunction_Read-DkimRecord PSCommand_Read-DkimRecord PSFunction_Read-MtaStsRecord PSCommand_Read-MtaStsRecord PSFunction_Read-MXRecord PSCommand_Read-MXRecord PSFunction_Read-NSRecord PSCommand_Read-NSRecord PSFunction_Read-SPFRecord PSCommand_Read-SPFRecord PSFunction_Read-TlsRptRecord PSCommand_Read-TlsRptRecord PSFunction_Read-WhoisRecord PSCommand_Read-WhoisRecord PSFunction_Resolve-DnsHttpsQuery PSCommand_Resolve-DnsHttpsQuery PSFunction_Set-DnsResolver PSCommand_Set-DnsResolver PSFunction_Test-DNSSEC PSCommand_Test-DNSSEC PSFunction_Test-HttpsCertificate PSCommand_Test-HttpsCertificate PSFunction_Test-MtaSts PSCommand_Test-MtaSts PSIncludes_Function + False + 2026-01-29T15:52:42Z + 1.1.2 + John Duprey + false + Module + DNSHealth.nuspec|DNSHealth.psm1|MailProviders\SpamTitan.json|MailProviders\Proofpoint.json|MailProviders\Sophos.json|DNSHealth.psd1|MailProviders\Google.json|MailProviders\BarracudaESS.json|MailProviders\AppRiver.json|MailProviders\SymantecCloud.json|MailProviders\Microsoft365.json|MailProviders\HornetSecurity.json|MailProviders\_template.json|MailProviders\Mimecast.json|MailProviders\Intermedia.json|MailProviders\Null.json|MailProviders\Reflexion.json + a300d2b0-d468-46d1-88a3-e442a76b655b + 7.0 + + + /Users/johnduprey/Documents/GitHub/CIPP Workspace/CIPP-API/Modules/DNSHealth/1.1.2 +
+
+
From 5baa05721828efd13c6a32fcb2a02e643d9410de Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Thu, 29 Jan 2026 17:32:31 +0100 Subject: [PATCH 305/503] clean up legacy stuff around intune template management. --- .../Invoke-CIPPStandardIntuneTemplate.ps1 | 209 ++++++++---------- 1 file changed, 88 insertions(+), 121 deletions(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardIntuneTemplate.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardIntuneTemplate.ps1 index 50dc65afc821..ca5d082d5895 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardIntuneTemplate.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardIntuneTemplate.ps1 @@ -36,150 +36,117 @@ function Invoke-CIPPStandardIntuneTemplate { https://docs.cipp.app/user-documentation/tenant/standards/list-standards #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'IntuneTemplate_general' -TenantFilter $Tenant -RequiredCapabilities @('INTUNE_A', 'MDM_Services', 'EMS', 'SCCM', 'MICROSOFTINTUNEPLAN1') - if ($TestResult -eq $false) { - #writing to each item that the license is not present. - foreach ($Template in $settings.TemplateList) { - Set-CIPPStandardsCompareField -FieldName "standards.IntuneTemplate.$($Template.value)" -FieldValue 'This tenant does not have the required license for this standard.' -Tenant $Tenant - } - return $true - } #we're done. $Table = Get-CippTable -tablename 'templates' $Filter = "PartitionKey eq 'IntuneTemplate'" - $Request = @{body = $null } - $CompareList = foreach ($Template in $Settings) { - $Request.body = (Get-CIPPAzDataTableEntity @Table -Filter $Filter | Where-Object -Property RowKey -Like "$($Template.TemplateList.value)*").JSON | ConvertFrom-Json -ErrorAction SilentlyContinue - if ($null -eq $Request.body) { - Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to find template $($Template.TemplateList.value). Has this Intune Template been deleted?" -sev 'Error' - continue - } - $displayname = $request.body.Displayname - $description = $request.body.Description - $RawJSON = $Request.body.RawJSON + $Template = (Get-CIPPAzDataTableEntity @Table -Filter $Filter | Where-Object -Property RowKey -Like "$($Settings.TemplateList.value)*").JSON | ConvertFrom-Json -ErrorAction SilentlyContinue + if ($null -eq $Template) { + Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to find template $($Settings.TemplateList.value). Has this Intune Template been deleted?" -sev 'Error' + return $true + } + + $displayname = $Template.Displayname + $description = $Template.Description + $RawJSON = $Template.RawJSON + $TemplateType = $Template.Type + + try { + $ExistingPolicy = Get-CIPPIntunePolicy -tenantFilter $Tenant -DisplayName $displayname -TemplateType $TemplateType + } catch { + $ExistingPolicy = $null + } + + if ($ExistingPolicy) { try { - $ExistingPolicy = Get-CIPPIntunePolicy -tenantFilter $Tenant -DisplayName $displayname -TemplateType $Request.body.Type + $RawJSON = Get-CIPPTextReplacement -Text $RawJSON -TenantFilter $Tenant + $JSONExistingPolicy = $ExistingPolicy.cippconfiguration | ConvertFrom-Json + $JSONTemplate = $RawJSON | ConvertFrom-Json + #This might be a slow one. + $Compare = Compare-CIPPIntuneObject -ReferenceObject $JSONTemplate -DifferenceObject $JSONExistingPolicy -compareType $TemplateType -ErrorAction SilentlyContinue } catch { } - if ($ExistingPolicy) { - try { - $RawJSON = Get-CIPPTextReplacement -Text $RawJSON -TenantFilter $Tenant - $JSONExistingPolicy = $ExistingPolicy.cippconfiguration | ConvertFrom-Json - $JSONTemplate = $RawJSON | ConvertFrom-Json - $Compare = Compare-CIPPIntuneObject -ReferenceObject $JSONTemplate -DifferenceObject $JSONExistingPolicy -compareType $Request.body.Type -ErrorAction SilentlyContinue - } catch { - } - } else { - $compare = [pscustomobject]@{ - MatchFailed = $true - Difference = 'This policy does not exist in Intune.' - } - } - if ($Compare) { - [PSCustomObject]@{ - MatchFailed = $true - displayname = $displayname - description = $description - compare = $Compare - rawJSON = $RawJSON - body = $Request.body - assignTo = $Template.AssignTo - excludeGroup = $Template.excludeGroup - remediate = $Template.remediate - alert = $Template.alert - report = $Template.report - existingPolicyId = $ExistingPolicy.id - templateId = $Template.TemplateList.value - customGroup = $Template.customGroup - assignmentFilter = $Template.assignmentFilter - assignmentFilterType = $Template.assignmentFilterType - } - } else { - [PSCustomObject]@{ - MatchFailed = $false - displayname = $displayname - description = $description - compare = $false - rawJSON = $RawJSON - body = $Request.body - assignTo = $Template.AssignTo - excludeGroup = $Template.excludeGroup - remediate = $Template.remediate - alert = $Template.alert - report = $Template.report - existingPolicyId = $ExistingPolicy.id - templateId = $Template.TemplateList.value - customGroup = $Template.customGroup - assignmentFilter = $Template.assignmentFilter - assignmentFilterType = $Template.assignmentFilterType - } + } else { + $compare = [pscustomobject]@{ + MatchFailed = $true + Difference = 'This policy does not exist in Intune.' } } + $CompareResult = [PSCustomObject]@{ + MatchFailed = [bool]$Compare + displayname = $displayname + description = $description + compare = $Compare + rawJSON = $RawJSON + templateType = $TemplateType + assignTo = $Settings.AssignTo + excludeGroup = $Settings.excludeGroup + remediate = $Settings.remediate + alert = $Settings.alert + report = $Settings.report + existingPolicyId = $ExistingPolicy.id + templateId = $Settings.TemplateList.value + customGroup = $Settings.customGroup + assignmentFilter = $Settings.assignmentFilter + assignmentFilterType = $Settings.assignmentFilterType + } - if ($true -in $Settings.remediate) { - foreach ($TemplateFile in $CompareList | Where-Object -Property remediate -EQ $true) { - try { - $TemplateFile.customGroup ? ($TemplateFile.AssignTo = $TemplateFile.customGroup) : $null - - $PolicyParams = @{ - TemplateType = $TemplateFile.body.Type - Description = $TemplateFile.description - DisplayName = $TemplateFile.displayname - RawJSON = $templateFile.rawJSON - AssignTo = $TemplateFile.AssignTo - ExcludeGroup = $TemplateFile.excludeGroup - tenantFilter = $Tenant - } + if ($Settings.remediate) { + try { + $CompareResult.customGroup ? ($CompareResult.AssignTo = $CompareResult.customGroup) : $null - # Add assignment filter if specified - if ($TemplateFile.assignmentFilter) { - $PolicyParams.AssignmentFilterName = $TemplateFile.assignmentFilter - $PolicyParams.AssignmentFilterType = $TemplateFile.assignmentFilterType ?? 'include' - } + $PolicyParams = @{ + TemplateType = $CompareResult.templateType + Description = $CompareResult.description + DisplayName = $CompareResult.displayname + RawJSON = $CompareResult.rawJSON + AssignTo = $CompareResult.AssignTo + ExcludeGroup = $CompareResult.excludeGroup + tenantFilter = $Tenant + } - Set-CIPPIntunePolicy @PolicyParams - } catch { - $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message - Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to create or update Intune Template $($TemplateFile.displayname), Error: $ErrorMessage" -sev 'Error' + # Add assignment filter if specified + if ($CompareResult.assignmentFilter) { + $PolicyParams.AssignmentFilterName = $CompareResult.assignmentFilter + $PolicyParams.AssignmentFilterType = $CompareResult.assignmentFilterType ?? 'include' } - } + Set-CIPPIntunePolicy @PolicyParams + } catch { + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to create or update Intune Template $($CompareResult.displayname), Error: $ErrorMessage" -sev 'Error' + } } - if ($true -in $Settings.alert) { - foreach ($Template in $CompareList | Where-Object -Property alert -EQ $true) { - $AlertObj = $Template | Select-Object -Property displayname, description, compare, assignTo, excludeGroup, existingPolicyId - if ($Template.compare) { - Write-StandardsAlert -message "Template $($Template.displayname) does not match the expected configuration." -object $AlertObj -tenant $Tenant -standardName 'IntuneTemplate' -standardId $Settings.templateId - Write-LogMessage -API 'Standards' -tenant $Tenant -message "Template $($Template.displayname) does not match the expected configuration. We've generated an alert" -sev info + if ($Settings.alert) { + $AlertObj = $CompareResult | Select-Object -Property displayname, description, compare, assignTo, excludeGroup, existingPolicyId + if ($CompareResult.compare) { + Write-StandardsAlert -message "Template $($CompareResult.displayname) does not match the expected configuration." -object $AlertObj -tenant $Tenant -standardName 'IntuneTemplate' -standardId $Settings.templateId + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Template $($CompareResult.displayname) does not match the expected configuration. We've generated an alert" -sev info + } else { + if ($CompareResult.ExistingPolicyId) { + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Template $($CompareResult.displayname) has the correct configuration." -sev Info } else { - if ($Template.ExistingPolicyId) { - Write-LogMessage -API 'Standards' -tenant $Tenant -message "Template $($Template.displayname) has the correct configuration." -sev Info - } else { - Write-StandardsAlert -message "Template $($Template.displayname) is missing." -object $AlertObj -tenant $Tenant -standardName 'IntuneTemplate' -standardId $Settings.templateId - Write-LogMessage -API 'Standards' -tenant $Tenant -message "Template $($Template.displayname) is missing." -sev info - } + Write-StandardsAlert -message "Template $($CompareResult.displayname) is missing." -object $AlertObj -tenant $Tenant -standardName 'IntuneTemplate' -standardId $Settings.templateId + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Template $($CompareResult.displayname) is missing." -sev info } } } - if ($true -in $Settings.report) { - foreach ($Template in $CompareList | Where-Object { $_.report -eq $true -or $_.remediate -eq $true }) { - $id = $Template.templateId + if ($Settings.report -or $Settings.remediate) { + $id = $CompareResult.templateId - $CurrentValue = @{ - displayName = $Template.displayname - description = $Template.description - isCompliant = if ($Template.compare) { $false } else { $true } - } - $ExpectedValue = @{ - displayName = $Template.displayname - description = $Template.description - isCompliant = $true - } - Set-CIPPStandardsCompareField -FieldName "standards.IntuneTemplate.$id" -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant + $CurrentValue = @{ + displayName = $CompareResult.displayname + description = $CompareResult.description + isCompliant = if ($CompareResult.compare) { $false } else { $true } + } + $ExpectedValue = @{ + displayName = $CompareResult.displayname + description = $CompareResult.description + isCompliant = $true } + Set-CIPPStandardsCompareField -FieldName "standards.IntuneTemplate.$id" -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant #Add-CIPPBPAField -FieldName "policy-$id" -FieldValue $Compare -StoreAs bool -Tenant $tenant } } From bd391d6bdd34a968a1cbd8629adc03a0842a6337 Mon Sep 17 00:00:00 2001 From: redanthrax Date: Thu, 29 Jan 2026 08:43:06 -0800 Subject: [PATCH 306/503] fix: Hudu sync creating duplicate users and devices MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This fix resolves issue #5257 where Hudu sync was creating thousands of duplicate user and device entries. Root Cause: - The $People and $HuduDevices collections were fetched once at the start of the sync process - When new users/devices were created in Hudu during the sync, they were not added to these in-memory collections - Subsequent iterations or sync runs would not find the newly created assets in the stale collections and create them again, leading to duplicates Changes: - Converted $People and $HuduDevices from static arrays to System.Collections.Generic.List[object] for efficient mutation - Added newly created users to $People collection after creation - Added newly created devices to $HuduDevices collection after creation - This ensures the collections stay up-to-date during the sync process and prevents duplicate creation Fixes: KelvinTegelaar/CIPP#5257 💘 Generated with Crush Assisted-by: Claude Sonnet 4.5 via Crush --- .../Public/Hudu/Invoke-HuduExtensionSync.ps1 | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/Modules/CippExtensions/Public/Hudu/Invoke-HuduExtensionSync.ps1 b/Modules/CippExtensions/Public/Hudu/Invoke-HuduExtensionSync.ps1 index 19b1e8c13def..fc68634b5f29 100644 --- a/Modules/CippExtensions/Public/Hudu/Invoke-HuduExtensionSync.ps1 +++ b/Modules/CippExtensions/Public/Hudu/Invoke-HuduExtensionSync.ps1 @@ -66,18 +66,19 @@ function Invoke-HuduExtensionSync { $CreateUsers = $Configuration.CreateMissingUsers $PeopleLayout = Get-HuduAssetLayouts -Id $PeopleLayoutId if ($PeopleLayout.id) { - $People = Get-HuduAssets -CompanyId $company_id -AssetLayoutId $PeopleLayout.id + $PeopleArray = Get-HuduAssets -CompanyId $company_id -AssetLayoutId $PeopleLayout.id + $People = [System.Collections.Generic.List[object]]::new($PeopleArray) } else { $CreateUsers = $false - $People = @() + $People = [System.Collections.Generic.List[object]]::new() } } else { $CreateUsers = $false - $People = @() + $People = [System.Collections.Generic.List[object]]::new() } } catch { $CreateUsers = $false - $People = @() + $People = [System.Collections.Generic.List[object]]::new() $CompanyResult.Errors.add("Company: Unable to fetch People $_") Write-Host "Hudu People - Error: $_" } @@ -91,18 +92,18 @@ function Invoke-HuduExtensionSync { $DesktopsLayout = Get-HuduAssetLayouts -Id $DeviceLayoutId if ($DesktopsLayout.id) { $HuduDesktopDevices = Get-HuduAssets -CompanyId $company_id -AssetLayoutId $DesktopsLayout.id - $HuduDevices = $HuduDesktopDevices + $HuduDevices = [System.Collections.Generic.List[object]]::new($HuduDesktopDevices) } else { $CreateDevices = $false - $HuduDevices = @() + $HuduDevices = [System.Collections.Generic.List[object]]::new() } } else { $CreateDevices = $false - $HuduDevices = @() + $HuduDevices = [System.Collections.Generic.List[object]]::new() } } catch { $CreateDevices = $false - $HuduDevices = @() + $HuduDevices = [System.Collections.Generic.List[object]]::new() $CompanyResult.Errors.add("Company: Unable to fetch Devices $_") Write-Host "Hudu Devices - Error: $_" } @@ -753,6 +754,8 @@ function Invoke-HuduExtensionSync { Hash = [string]$NewHash } Add-CIPPAzDataTableEntity @HuduAssetCache -Entity $AssetCache -Force + # Add newly created user to the People collection to prevent duplicates + $People.Add($CreateHuduUser) } } } else { @@ -997,6 +1000,8 @@ function Invoke-HuduExtensionSync { Hash = [string]$NewHash } Add-CIPPAzDataTableEntity @HuduAssetCache -Entity $AssetCache -Force + # Add newly created device to the HuduDevices collection to prevent duplicates + $HuduDevices.Add($CreateHuduDevice) $RelHuduUser = $People | Where-Object { $_.primary_mail -eq $Device.userPrincipalName -or ($_.cards.integrator_name -eq 'cw_manage' -and $_.cards.data.communicationItems.communicationType -eq 'Email' -and $_.cards.data.communicationItems.value -eq $Device.userPrincipalName) } if ($RelHuduUser) { From 74e95eedf9a06d02ec5adab31c3d7778a9253acc Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Thu, 29 Jan 2026 18:02:53 +0100 Subject: [PATCH 307/503] Fix Intune Standards rerun prevention/filtering --- .../Activity Triggers/Standards/Push-CIPPStandardsList.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Standards/Push-CIPPStandardsList.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Standards/Push-CIPPStandardsList.ps1 index 9cd477e3b68a..50088efac3e4 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Standards/Push-CIPPStandardsList.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Standards/Push-CIPPStandardsList.ps1 @@ -170,7 +170,7 @@ function Push-CIPPStandardsList { # Remove if both unchanged if (-not $PolicyChanged -and -not $StandardTemplateChanged) { -x [void]$ComputedStandards.Remove($Key) + [void]$ComputedStandards.Remove($Key) } } } catch { From 4bc014304e4fbc5dee07dc3be5db3b4e9efa8bf3 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Thu, 29 Jan 2026 18:03:52 +0100 Subject: [PATCH 308/503] add some logging --- .../Activity Triggers/Standards/Push-CIPPStandardsList.ps1 | 1 + 1 file changed, 1 insertion(+) diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Standards/Push-CIPPStandardsList.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Standards/Push-CIPPStandardsList.ps1 index 50088efac3e4..01acec8da950 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Standards/Push-CIPPStandardsList.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Standards/Push-CIPPStandardsList.ps1 @@ -170,6 +170,7 @@ function Push-CIPPStandardsList { # Remove if both unchanged if (-not $PolicyChanged -and -not $StandardTemplateChanged) { + Write-Host "NO INTUNE CHANGE: Filtering out $key for $($TenantFilter)" [void]$ComputedStandards.Remove($Key) } } From 4461f555d33dad7db0c43230f8ca822254129fdf Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Thu, 29 Jan 2026 18:07:28 +0100 Subject: [PATCH 309/503] add "INTUNETEMPLATERUN" as AppInsights tracker. --- .../Public/Standards/Invoke-CIPPStandardIntuneTemplate.ps1 | 2 +- Resources/CIPPTimers.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardIntuneTemplate.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardIntuneTemplate.ps1 index ca5d082d5895..a47738ecaee2 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardIntuneTemplate.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardIntuneTemplate.ps1 @@ -36,7 +36,7 @@ function Invoke-CIPPStandardIntuneTemplate { https://docs.cipp.app/user-documentation/tenant/standards/list-standards #> param($Tenant, $Settings) - + Write-Host 'INTUNETEMPLATERUN' $Table = Get-CippTable -tablename 'templates' $Filter = "PartitionKey eq 'IntuneTemplate'" diff --git a/Resources/CIPPTimers.json b/Resources/CIPPTimers.json index f76dba8941e2..95fa03a4b5d8 100644 --- a/Resources/CIPPTimers.json +++ b/Resources/CIPPTimers.json @@ -72,10 +72,10 @@ "RunOnProcessor": true }, { - "Id": "9b0c8e50-f798-49db-9a8b-dbcc0fcadeea", + "Id": "9b0c8e50-f798-49db-9a8b-dbcc0faadeea", "Command": "Start-StandardsOrchestrator", "Description": "Orchestrator to process standards", - "Cron": "0 0 */4 * * *", + "Cron": "0 * * * * *", "Priority": 4, "RunOnProcessor": true, "PreferredProcessor": "standards" From d33efc4bc1afc55c6116ef008a8dca8ec290f6ce Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 29 Jan 2026 12:46:45 -0500 Subject: [PATCH 310/503] revert timer --- Resources/CIPPTimers.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Resources/CIPPTimers.json b/Resources/CIPPTimers.json index 95fa03a4b5d8..f76dba8941e2 100644 --- a/Resources/CIPPTimers.json +++ b/Resources/CIPPTimers.json @@ -72,10 +72,10 @@ "RunOnProcessor": true }, { - "Id": "9b0c8e50-f798-49db-9a8b-dbcc0faadeea", + "Id": "9b0c8e50-f798-49db-9a8b-dbcc0fcadeea", "Command": "Start-StandardsOrchestrator", "Description": "Orchestrator to process standards", - "Cron": "0 * * * * *", + "Cron": "0 0 */4 * * *", "Priority": 4, "RunOnProcessor": true, "PreferredProcessor": "standards" From f960820649edd4347456b36b36162a9862d15937 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 29 Jan 2026 13:12:39 -0500 Subject: [PATCH 311/503] Bump version to 10.0.6 --- host.json | 2 +- version_latest.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/host.json b/host.json index ff513b12d0ea..aa8f581b4e1f 100644 --- a/host.json +++ b/host.json @@ -16,7 +16,7 @@ "distributedTracingEnabled": false, "version": "None" }, - "defaultVersion": "10.0.5", + "defaultVersion": "10.0.6", "versionMatchStrategy": "Strict", "versionFailureStrategy": "Fail" } diff --git a/version_latest.txt b/version_latest.txt index 2681b301aa6c..80f86ac0c358 100644 --- a/version_latest.txt +++ b/version_latest.txt @@ -1 +1 @@ -10.0.5 +10.0.6 From 64dd20564601a5d48de6aed9c26e4af5452ae78f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Thu, 29 Jan 2026 20:12:21 +0100 Subject: [PATCH 312/503] fix: remove top to fix far too few results returning --- .../HTTP Functions/Endpoint/MEM/Invoke-ListDefenderTVM.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListDefenderTVM.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListDefenderTVM.ps1 index ed88689e9578..3d6a59a25d46 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListDefenderTVM.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListDefenderTVM.ps1 @@ -1,4 +1,4 @@ -Function Invoke-ListDefenderTVM { +function Invoke-ListDefenderTVM { <# .FUNCTIONALITY Entrypoint @@ -10,7 +10,7 @@ Function Invoke-ListDefenderTVM { $TenantFilter = $Request.Query.tenantFilter # Interact with query parameters or the body of the request. try { - $GraphRequest = New-GraphGetRequest -tenantid $TenantFilter -uri "https://api.securitycenter.microsoft.com/api/machines/SoftwareVulnerabilitiesByMachine?`$top=999" -scope 'https://api.securitycenter.microsoft.com/.default' | Group-Object cveId + $GraphRequest = New-GraphGetRequest -tenantid $TenantFilter -uri 'https://api.securitycenter.microsoft.com/api/machines/SoftwareVulnerabilitiesByMachine' -scope 'https://api.securitycenter.microsoft.com/.default' | Group-Object cveId $GroupObj = foreach ($cve in $GraphRequest) { # Start with base properties $obj = [ordered]@{ From ca4be7246682cfd923bc1b1acbde34d6f345223a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Thu, 29 Jan 2026 20:14:23 +0100 Subject: [PATCH 313/503] feat: enhance vulnerability alert configuration Updated the "Vulnerabilities" alert to support multiple inputs for age, CVSS severity, and exploitability levels. This allows for more granular control over vulnerability monitoring. Adjusted the description for clarity. --- .../Alerts/Get-CIPPAlertVulnerabilities.ps1 | 45 ++++++++++++++++++- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertVulnerabilities.ps1 b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertVulnerabilities.ps1 index 068a039086ff..f6cb1d37f0b8 100644 --- a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertVulnerabilities.ps1 +++ b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertVulnerabilities.ps1 @@ -11,8 +11,36 @@ function Get-CIPPAlertVulnerabilities { $TenantFilter ) + # Extract filter parameters from InputValue + $ExploitabilityLevels = [System.Collections.Generic.List[string]]::new() + if ($InputValue -is [hashtable] -or $InputValue -is [PSCustomObject]) { + # Number inputs are stored directly + $AgeThresholdHours = if ($InputValue.VulnerabilityAgeHours) { [int]$InputValue.VulnerabilityAgeHours } else { 0 } + # Autocomplete inputs store value in .value subproperty + $CVSSSeverity = if ($InputValue.CVSSSeverity.value) { $InputValue.CVSSSeverity.value } else { 'low' } + # Multi-select autocomplete returns array of objects with .value + if ($InputValue.ExploitabilityLevels) { + foreach ($level in $InputValue.ExploitabilityLevels) { + $ExploitabilityLevels.Add($(if ($level.value) { $level.value } else { $level })) + } + } + } else { + # Backward compatibility: simple value = hours threshold + $AgeThresholdHours = if ($InputValue) { [int]$InputValue } else { 0 } + $CVSSSeverity = 'low' + } + + # Convert CVSS severity to minimum score + $CVSSMinScore = switch ($CVSSSeverity.ToLower()) { + 'critical' { 9.0 } + 'high' { 7.0 } + 'medium' { 4.0 } + 'low' { 0.0 } + default { 0.0 } + } + try { - $VulnerabilityRequest = New-GraphGetRequest -tenantid $TenantFilter -uri "https://api.securitycenter.microsoft.com/api/machines/SoftwareVulnerabilitiesByMachine?`$top=999&`$filter=cveId ne null" -scope 'https://api.securitycenter.microsoft.com/.default' + $VulnerabilityRequest = New-GraphGetRequest -tenantid $TenantFilter -uri 'https://api.securitycenter.microsoft.com/api/machines/SoftwareVulnerabilitiesByMachine' -scope 'https://api.securitycenter.microsoft.com/.default' if ($VulnerabilityRequest) { $AlertData = [System.Collections.Generic.List[PSCustomObject]]::new() @@ -25,10 +53,23 @@ function Get-CIPPAlertVulnerabilities { $HoursOld = [math]::Round(((Get-Date) - [datetime]$FirstVuln.firstSeenTimestamp).TotalHours) # Skip if vulnerability is not old enough - if ($HoursOld -lt [int]$InputValue) { + if ($HoursOld -lt $AgeThresholdHours) { continue } + # Skip if CVSS score is below minimum threshold + $VulnCVSS = if ($null -ne $FirstVuln.cvssScore) { [double]$FirstVuln.cvssScore } else { 0 } + if ($VulnCVSS -lt $CVSSMinScore) { + continue + } + + # Skip if exploitability level doesn't match filter (unless "All" is selected) + if ($ExploitabilityLevels.Count -gt 0 -and 'All' -notin $ExploitabilityLevels) { + if ($FirstVuln.exploitabilityLevel -notin $ExploitabilityLevels) { + continue + } + } + $DaysOld = [math]::Round(((Get-Date) - [datetime]$FirstVuln.firstSeenTimestamp).TotalDays) $AffectedDevices = ($Group.Group | Select-Object -ExpandProperty deviceName -Unique) -join ', ' From b374cbd9169326e9b6ab6d55d34c56bdc18e8795 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Thu, 29 Jan 2026 20:59:23 +0100 Subject: [PATCH 314/503] feat: enhance incident alert with severity filtering - Updated the logic to filter incidents based on severity. - Added additional incident properties: CreatedAt, IncidentID, and IncidentUrl. --- .../Alerts/Get-CIPPAlertDefenderIncidents.ps1 | 28 ++++++++++++++----- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertDefenderIncidents.ps1 b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertDefenderIncidents.ps1 index 49be9d9a153a..398a0875ca20 100644 --- a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertDefenderIncidents.ps1 +++ b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertDefenderIncidents.ps1 @@ -11,15 +11,29 @@ function Get-CIPPAlertDefenderIncidents { $InputValue, $TenantFilter ) + + $IncidentSeverities = $InputValue.IncidentSeverities.value -as [System.Collections.Generic.List[string]] try { - $AlertData = New-GraphGetRequest -uri "https://graph.microsoft.com/v1.0/security/incidents?`$top=50&`$filter=status eq 'active'" -tenantid $TenantFilter | ForEach-Object { + $Incidents = New-GraphGetRequest -uri "https://graph.microsoft.com/v1.0/security/incidents?`$top=50&`$filter=status eq 'active'" -tenantid $TenantFilter + $AlertData = foreach ($Incident in $Incidents) { + # Skip if severity doesn't match filter (unless "All" is selected or no filter) + if ($IncidentSeverities.Count -gt 0 -and 'All' -notin $IncidentSeverities) { + if ($Incident.severity -notin $IncidentSeverities) { + continue + } + } + [PSCustomObject]@{ - IncidentID = $_.id - CreatedAt = $_.createdDateTime - Severity = $_.severity - IncidentName = $_.displayName - IncidentUrl = $_.incidentWebUrl - Tenant = $TenantFilter + IncidentName = $Incident.displayName + Severity = $Incident.severity + Classification = $Incident.classification + Determination = $Incident.determination + Summary = $Incident.summary + AssignedTo = $Incident.assignedTo + CreatedAt = $Incident.createdDateTime + IncidentID = $Incident.id + IncidentUrl = $Incident.incidentWebUrl + Tenant = $TenantFilter } } Write-AlertTrace -cmdletName $MyInvocation.MyCommand -tenantFilter $TenantFilter -data $AlertData From 7c2386722181424835ccd98651995f969290e802 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Thu, 29 Jan 2026 21:42:13 +0100 Subject: [PATCH 315/503] feat: add DefenderAlerts with severity filtering options Added a new alert configuration for DefenderAlerts that includes a recommended run interval of 4 hours and allows users to filter alerts by severity. The input options include All Severities, High, Medium, Low, and Informational. --- .../Alerts/Get-CIPPAlertDefenderAlerts.ps1 | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 Modules/CIPPCore/Public/Alerts/Get-CIPPAlertDefenderAlerts.ps1 diff --git a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertDefenderAlerts.ps1 b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertDefenderAlerts.ps1 new file mode 100644 index 000000000000..76a42423395b --- /dev/null +++ b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertDefenderAlerts.ps1 @@ -0,0 +1,58 @@ + +function Get-CIPPAlertDefenderAlerts { + <# + .FUNCTIONALITY + Entrypoint + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $false)] + [Alias('input')] + $InputValue, + $TenantFilter + ) + + $AlertSeverities = $InputValue.AlertSeverities.value -as [System.Collections.Generic.List[string]] + try { + $DefenderAlerts = New-GraphGetRequest -uri "https://graph.microsoft.com/v1.0/security/alerts_v2?`$top=50&`$filter=status eq 'new'" -tenantid $TenantFilter + $AlertData = foreach ($Alert in $DefenderAlerts) { + # Skip if severity doesn't match filter (unless "All" is selected or no filter) + if ($AlertSeverities.Count -gt 0 -and 'All' -notin $AlertSeverities) { + if ($Alert.severity -notin $AlertSeverities) { + continue + } + } + + [PSCustomObject]@{ + Title = $Alert.title + Description = $Alert.description + Severity = $Alert.severity + Category = $Alert.category + ServiceSource = $Alert.serviceSource + ProductName = $Alert.productName + DetectionSource = $Alert.detectionSource + Classification = $Alert.classification + Determination = $Alert.determination + ThreatDisplayName = $Alert.threatDisplayName + ThreatFamilyName = $Alert.threatFamilyName + ActorDisplayName = $Alert.actorDisplayName + MitreTechniques = ($Alert.mitreTechniques -join ', ') + AssignedTo = $Alert.assignedTo + FirstActivityDateTime = $Alert.firstActivityDateTime + LastActivityDateTime = $Alert.lastActivityDateTime + CreatedAt = $Alert.createdDateTime + RecommendedActions = $Alert.recommendedActions + AlertID = $Alert.id + IncidentID = $Alert.incidentId + AlertUrl = $Alert.alertWebUrl + IncidentUrl = $Alert.incidentWebUrl + Tenant = $TenantFilter + } + } + Write-AlertTrace -cmdletName $MyInvocation.MyCommand -tenantFilter $TenantFilter -data $AlertData + + } catch { + # Commented out due to potential licensing spam + # Write-AlertMessage -tenant $($TenantFilter) -message "Could not get Defender alerts for $($TenantFilter): $(Get-NormalizedError -message $_.Exception.message)" + } +} From a0a865b02957b4bb64969afc4fbd4e6ac5b7f951 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 29 Jan 2026 16:57:06 -0500 Subject: [PATCH 316/503] Replace 'exit 0' with returns in scripts Replace abrupt 'exit 0' calls with return statements to avoid terminating the host/process and let callers handle early exits. Changes: New-CIPPAuditLogSearchResultsCache.ps1 (two exits -> return $false), Push-BPACollectData.ps1 (exit -> return), Push-CIPPStandard.ps1 (exit -> return), Push-AuditLogTenantDownload.ps1 (two exits -> return $false). Returns with $false are used where a failure signal is appropriate. --- .../Public/AuditLogs/New-CIPPAuditLogSearchResultsCache.ps1 | 4 ++-- .../Entrypoints/Activity Triggers/BPA/Push-BPACollectData.ps1 | 2 +- .../Activity Triggers/Standards/Push-CIPPStandard.ps1 | 2 +- .../Webhooks/Push-AuditLogTenantDownload.ps1 | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Modules/CIPPCore/Public/AuditLogs/New-CIPPAuditLogSearchResultsCache.ps1 b/Modules/CIPPCore/Public/AuditLogs/New-CIPPAuditLogSearchResultsCache.ps1 index 76701ac05736..f5c1766b59a9 100644 --- a/Modules/CIPPCore/Public/AuditLogs/New-CIPPAuditLogSearchResultsCache.ps1 +++ b/Modules/CIPPCore/Public/AuditLogs/New-CIPPAuditLogSearchResultsCache.ps1 @@ -22,7 +22,7 @@ function New-CIPPAuditLogSearchResultsCache { $message = "Skipping search ID: $SearchId for tenant: $TenantFilter - Previous attempt failed within the last 4 hours" Write-LogMessage -API 'AuditLog' -tenant $TenantFilter -message $message -Sev 'Info' Write-Information $message - exit 0 + return $false } } catch { Write-Information "Error checking for failed downloads: $($_.Exception.Message)" @@ -36,7 +36,7 @@ function New-CIPPAuditLogSearchResultsCache { $searchEntity = Get-CIPPAzDataTableEntity @CacheWebhooksTable -Filter "PartitionKey eq '$TenantFilter' and SearchId eq '$SearchId'" if ($searchEntity) { Write-Information "Search ID: $SearchId already cached for tenant: $TenantFilter" - exit 0 + return $false } # Record this attempt in the FailedAuditLogDownloads table BEFORE starting the download diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/BPA/Push-BPACollectData.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/BPA/Push-BPACollectData.ps1 index 0cc163058e14..db57af632bde 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/BPA/Push-BPACollectData.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/BPA/Push-BPACollectData.ps1 @@ -23,7 +23,7 @@ function Push-BPACollectData { $Rerun = Test-CIPPRerun -Type 'BPA' -Tenant $Item.Tenant -API $Item.Template if ($Rerun) { Write-Host 'Detected rerun for BPA. Exiting cleanly' - exit 0 + return } Write-Host "Working on BPA for $($TenantName.defaultDomainName) with GUID $($TenantName.customerId) - Report ID $($Item.Template)" $Template = $Templates | Where-Object -Property Name -EQ -Value $Item.Template diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Standards/Push-CIPPStandard.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Standards/Push-CIPPStandard.ps1 index 996c224490d9..e3944c21a4d2 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Standards/Push-CIPPStandard.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Standards/Push-CIPPStandard.ps1 @@ -24,7 +24,7 @@ function Push-CIPPStandard { $Rerun = Test-CIPPRerun -Type Standard -Tenant $Tenant -API $API if ($Rerun) { Write-Information 'Detected rerun. Exiting cleanly' - exit 0 + return } else { Write-Information "Rerun is set to false. We'll be running $FunctionName" } diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Webhooks/Push-AuditLogTenantDownload.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Webhooks/Push-AuditLogTenantDownload.ps1 index f7171d1cdac2..e80608d97d6a 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Webhooks/Push-AuditLogTenantDownload.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Webhooks/Push-AuditLogTenantDownload.ps1 @@ -70,11 +70,11 @@ function Push-AuditLogTenantDownload { } } catch { Write-Information ('Audit Log search: Error {0} line {1} - {2}' -f $_.InvocationInfo.ScriptName, $_.InvocationInfo.ScriptLineNumber, $_.Exception.Message) - exit 0 + return $false } } catch { Write-Information ('Push-AuditLogTenant: Error {0} line {1} - {2}' -f $_.InvocationInfo.ScriptName, $_.InvocationInfo.ScriptLineNumber, $_.Exception.Message) - exit 0 + return $false } } From f3eb700f3ff367d7c8a7d766c2d6831310955046 Mon Sep 17 00:00:00 2001 From: rvdwegen Date: Fri, 30 Jan 2026 11:05:32 +0100 Subject: [PATCH 317/503] Switch DisableGuests back to graph for now --- .../Invoke-CIPPStandardDisableGuests.ps1 | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableGuests.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableGuests.ps1 index baa729c653b2..dd7d018f5413 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableGuests.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableGuests.ps1 @@ -45,17 +45,12 @@ function Invoke-CIPPStandardDisableGuests { $checkDays = if ($Settings.days) { $Settings.days } else { 90 } # Default to 90 days if not set. Pre v8.5.0 compatibility $Days = (Get-Date).AddDays(-$checkDays).ToUniversalTime() + $Lookup = $Days.ToString('o') $AuditLookup = (Get-Date).AddDays(-7).ToUniversalTime().ToString('o') try { - $AllUsers = New-CIPPDbRequest -TenantFilter $Tenant -Type 'Users' - - $GraphRequest = $AllUsers | Where-Object { - $_.userType -eq 'Guest' -and - $_.accountEnabled -eq $true -and - ($null -ne $_.createdDateTime -and [DateTime]$_.createdDateTime -le $Days) -and - ($null -eq $_.signInActivity -or $null -eq $_.signInActivity.lastSuccessfulSignInDateTime -or [DateTime]$_.signInActivity.lastSuccessfulSignInDateTime -le $Days) - } + $GraphRequest = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/users?`$filter=createdDateTime le $Lookup and userType eq 'Guest' and accountEnabled eq true &`$select=id,UserPrincipalName,signInActivity,mail,userType,accountEnabled,createdDateTime,externalUserState" -scope 'https://graph.microsoft.com/.default' -tenantid $Tenant | + Where-Object { $_.signInActivity.lastSuccessfulSignInDateTime -le $Days } } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the DisableGuests state for $Tenant. Error: $ErrorMessage" -Sev Error @@ -100,13 +95,6 @@ function Invoke-CIPPStandardDisableGuests { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to process bulk disable guests request: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage } - - # Refresh user cache after remediation - try { - Set-CIPPDBCacheUsers -TenantFilter $Tenant - } catch { - Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to refresh user cache after remediation: $($_.Exception.Message)" -sev Warning - } } else { Write-LogMessage -API 'Standards' -tenant $tenant -message "No guests accounts with a login longer than $checkDays days ago." -sev Info } From a972740aa668e2c8d49f4ca66f97e245c1da8a6a Mon Sep 17 00:00:00 2001 From: rvdwegen Date: Fri, 30 Jan 2026 11:11:11 +0100 Subject: [PATCH 318/503] Move template email file back --- Modules/CIPPCore/Public/New-CIPPAlertTemplate.ps1 | 2 +- Resources/TemplateEmail.html => TemplateEmail.html | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename Resources/TemplateEmail.html => TemplateEmail.html (100%) diff --git a/Modules/CIPPCore/Public/New-CIPPAlertTemplate.ps1 b/Modules/CIPPCore/Public/New-CIPPAlertTemplate.ps1 index 7ca6e0d74716..67340eaf3ab5 100644 --- a/Modules/CIPPCore/Public/New-CIPPAlertTemplate.ps1 +++ b/Modules/CIPPCore/Public/New-CIPPAlertTemplate.ps1 @@ -13,7 +13,7 @@ function New-CIPPAlertTemplate { $AlertComment ) $Appname = '[{"Application Name":"ACOM Azure Website","Application IDs":"23523755-3a2b-41ca-9315-f81f3f566a95"},{"Application Name":"AEM-DualAuth","Application IDs":"69893ee3-dd10-4b1c-832d-4870354be3d8"},{"Application Name":"ASM Campaign Servicing","Application IDs":"0cb7b9ec-5336-483b-bc31-b15b5788de71"},{"Application Name":"Azure Advanced Threat Protection","Application IDs":"7b7531ad-5926-4f2d-8a1d-38495ad33e17"},{"Application Name":"Azure Data Lake","Application IDs":"e9f49c6b-5ce5-44c8-925d-015017e9f7ad"},{"Application Name":"Azure Lab Services Portal","Application IDs":"835b2a73-6e10-4aa5-a979-21dfda45231c"},{"Application Name":"Azure Portal","Application IDs":"c44b4083-3bb0-49c1-b47d-974e53cbdf3c"},{"Application Name":"AzureSupportCenter","Application IDs":"37182072-3c9c-4f6a-a4b3-b3f91cacffce"},{"Application Name":"Bing","Application IDs":"9ea1ad79-fdb6-4f9a-8bc3-2b70f96e34c7"},{"Application Name":"CPIM Service","Application IDs":"bb2a2e3a-c5e7-4f0a-88e0-8e01fd3fc1f4"},{"Application Name":"CRM Power BI Integration","Application IDs":"e64aa8bc-8eb4-40e2-898b-cf261a25954f"},{"Application Name":"Dataverse","Application IDs":"00000007-0000-0000-c000-000000000000"},{"Application Name":"Enterprise Roaming and Backup","Application IDs":"60c8bde5-3167-4f92-8fdb-059f6176dc0f"},{"Application Name":"IAM Supportability","Application IDs":"a57aca87-cbc0-4f3c-8b9e-dc095fdc8978"},{"Application Name":"IrisSelectionFrontDoor","Application IDs":"16aeb910-ce68-41d1-9ac3-9e1673ac9575"},{"Application Name":"MCAPI Authorization Prod","Application IDs":"d73f4b35-55c9-48c7-8b10-651f6f2acb2e"},{"Application Name":"Media Analysis and Transformation Service","Application IDs":"944f0bd1-117b-4b1c-af26-804ed95e767e
0cd196ee-71bf-4fd6-a57c-b491ffd4fb1e"},{"Application Name":"Microsoft 365 Support Service","Application IDs":"ee272b19-4411-433f-8f28-5c13cb6fd407"},{"Application Name":"Microsoft App Access Panel","Application IDs":"0000000c-0000-0000-c000-000000000000"},{"Application Name":"Microsoft Approval Management","Application IDs":"65d91a3d-ab74-42e6-8a2f-0add61688c74
38049638-cc2c-4cde-abe4-4479d721ed44"},{"Application Name":"Microsoft Authentication Broker","Application IDs":"29d9ed98-a469-4536-ade2-f981bc1d605e"},{"Application Name":"Microsoft Azure CLI","Application IDs":"04b07795-8ddb-461a-bbee-02f9e1bf7b46"},{"Application Name":"Microsoft Azure PowerShell","Application IDs":"1950a258-227b-4e31-a9cf-717495945fc2"},{"Application Name":"Microsoft Bing Search","Application IDs":"cf36b471-5b44-428c-9ce7-313bf84528de"},{"Application Name":"Microsoft Bing Search for Microsoft Edge","Application IDs":"2d7f3606-b07d-41d1-b9d2-0d0c9296a6e8"},{"Application Name":"Microsoft Bing Default Search Engine","Application IDs":"1786c5ed-9644-47b2-8aa0-7201292175b6"},{"Application Name":"Microsoft Defender for Cloud Apps","Application IDs":"3090ab82-f1c1-4cdf-af2c-5d7a6f3e2cc7"},{"Application Name":"Microsoft Docs","Application IDs":"18fbca16-2224-45f6-85b0-f7bf2b39b3f3"},{"Application Name":"Microsoft Dynamics ERP","Application IDs":"00000015-0000-0000-c000-000000000000"},{"Application Name":"Microsoft Edge Insider Addons Prod","Application IDs":"6253bca8-faf2-4587-8f2f-b056d80998a7"},{"Application Name":"Microsoft Exchange Online Protection","Application IDs":"00000007-0000-0ff1-ce00-000000000000"},{"Application Name":"Microsoft Forms","Application IDs":"c9a559d2-7aab-4f13-a6ed-e7e9c52aec87"},{"Application Name":"Microsoft Graph","Application IDs":"00000003-0000-0000-c000-000000000000"},{"Application Name":"Microsoft Intune Web Company Portal","Application IDs":"74bcdadc-2fdc-4bb3-8459-76d06952a0e9"},{"Application Name":"Microsoft Intune Windows Agent","Application IDs":"fc0f3af4-6835-4174-b806-f7db311fd2f3"},{"Application Name":"Microsoft Learn","Application IDs":"18fbca16-2224-45f6-85b0-f7bf2b39b3f3"},{"Application Name":"Microsoft Office","Application IDs":"d3590ed6-52b3-4102-aeff-aad2292ab01c"},{"Application Name":"Microsoft Office 365 Portal","Application IDs":"00000006-0000-0ff1-ce00-000000000000"},{"Application Name":"Microsoft Office Web Apps Service","Application IDs":"67e3df25-268a-4324-a550-0de1c7f97287"},{"Application Name":"Microsoft Online Syndication Partner Portal","Application IDs":"d176f6e7-38e5-40c9-8a78-3998aab820e7"},{"Application Name":"Microsoft password reset service","Application IDs":"93625bc8-bfe2-437a-97e0-3d0060024faa"},{"Application Name":"Microsoft Power BI","Application IDs":"871c010f-5e61-4fb1-83ac-98610a7e9110"},{"Application Name":"Microsoft Storefronts","Application IDs":"28b567f6-162c-4f54-99a0-6887f387bbcc"},{"Application Name":"Microsoft Stream Portal","Application IDs":"cf53fce8-def6-4aeb-8d30-b158e7b1cf83"},{"Application Name":"Microsoft Substrate Management","Application IDs":"98db8bd6-0cc0-4e67-9de5-f187f1cd1b41"},{"Application Name":"Microsoft Support","Application IDs":"fdf9885b-dd37-42bf-82e5-c3129ef5a302"},{"Application Name":"Microsoft Teams","Application IDs":"1fec8e78-bce4-4aaf-ab1b-5451cc387264"},{"Application Name":"Microsoft Teams Services","Application IDs":"cc15fd57-2c6c-4117-a88c-83b1d56b4bbe"},{"Application Name":"Microsoft Teams Web Client","Application IDs":"5e3ce6c0-2b1f-4285-8d4b-75ee78787346"},{"Application Name":"Microsoft Whiteboard Services","Application IDs":"95de633a-083e-42f5-b444-a4295d8e9314"},{"Application Name":"O365 Suite UX","Application IDs":"4345a7b9-9a63-4910-a426-35363201d503"},{"Application Name":"Office 365 Exchange Online","Application IDs":"00000002-0000-0ff1-ce00-000000000000"},{"Application Name":"Office 365 Management","Application IDs":"00b41c95-dab0-4487-9791-b9d2c32c80f2"},{"Application Name":"Office 365 Search Service","Application IDs":"66a88757-258c-4c72-893c-3e8bed4d6899"},{"Application Name":"Office 365 SharePoint Online","Application IDs":"00000003-0000-0ff1-ce00-000000000000"},{"Application Name":"Office Delve","Application IDs":"94c63fef-13a3-47bc-8074-75af8c65887a"},{"Application Name":"Office Online Add-in SSO","Application IDs":"93d53678-613d-4013-afc1-62e9e444a0a5"},{"Application Name":"Office Online Client AAD- Augmentation Loop","Application IDs":"2abdc806-e091-4495-9b10-b04d93c3f040"},{"Application Name":"Office Online Client AAD- Loki","Application IDs":"b23dd4db-9142-4734-867f-3577f640ad0c"},{"Application Name":"Office Online Client AAD- Maker","Application IDs":"17d5e35f-655b-4fb0-8ae6-86356e9a49f5"},{"Application Name":"Office Online Client MSA- Loki","Application IDs":"b6e69c34-5f1f-4c34-8cdf-7fea120b8670"},{"Application Name":"Office Online Core SSO","Application IDs":"243c63a3-247d-41c5-9d83-7788c43f1c43"},{"Application Name":"Office Online Search","Application IDs":"a9b49b65-0a12-430b-9540-c80b3332c127"},{"Application Name":"Office.com","Application IDs":"4b233688-031c-404b-9a80-a4f3f2351f90"},{"Application Name":"Office365 Shell WCSS-Client","Application IDs":"89bee1f7-5e6e-4d8a-9f3d-ecd601259da7"},{"Application Name":"OfficeClientService","Application IDs":"0f698dd4-f011-4d23-a33e-b36416dcb1e6"},{"Application Name":"OfficeHome","Application IDs":"4765445b-32c6-49b0-83e6-1d93765276ca"},{"Application Name":"OfficeShredderWacClient","Application IDs":"4d5c2d63-cf83-4365-853c-925fd1a64357"},{"Application Name":"OMSOctopiPROD","Application IDs":"62256cef-54c0-4cb4-bcac-4c67989bdc40"},{"Application Name":"OneDrive SyncEngine","Application IDs":"ab9b8c07-8f02-4f72-87fa-80105867a763"},{"Application Name":"OneNote","Application IDs":"2d4d3d8e-2be3-4bef-9f87-7875a61c29de"},{"Application Name":"Outlook Mobile","Application IDs":"27922004-5251-4030-b22d-91ecd9a37ea4"},{"Application Name":"Partner Customer Delegated Admin Offline Processor","Application IDs":"a3475900-ccec-4a69-98f5-a65cd5dc5306"},{"Application Name":"Password Breach Authenticator","Application IDs":"bdd48c81-3a58-4ea9-849c-ebea7f6b6360"},{"Application Name":"Power BI Service","Application IDs":"00000009-0000-0000-c000-000000000000"},{"Application Name":"SharedWithMe","Application IDs":"ffcb16e8-f789-467c-8ce9-f826a080d987"},{"Application Name":"SharePoint Online Web Client Extensibility","Application IDs":"08e18876-6177-487e-b8b5-cf950c1e598c"},{"Application Name":"Signup","Application IDs":"b4bddae8-ab25-483e-8670-df09b9f1d0ea"},{"Application Name":"Skype for Business Online","Application IDs":"00000004-0000-0ff1-ce00-000000000000"},{"Application Name":"Sway","Application IDs":"905fcf26-4eb7-48a0-9ff0-8dcc7194b5ba"},{"Application Name":"Universal Store Native Client","Application IDs":"268761a2-03f3-40df-8a8b-c3db24145b6b"},{"Application Name":"Vortex [wsfed enabled]","Application IDs":"5572c4c0-d078-44ce-b81c-6cbf8d3ed39e"},{"Application Name":"Windows Azure Active Directory","Application IDs":"00000002-0000-0000-c000-000000000000"},{"Application Name":"Windows Azure Service Management API","Application IDs":"797f4846-ba00-4fd7-ba43-dac1f8f63013"},{"Application Name":"WindowsDefenderATP Portal","Application IDs":"a3b79187-70b2-4139-83f9-6016c58cd27b"},{"Application Name":"Windows Search","Application IDs":"26a7ee05-5602-4d76-a7ba-eae8b7b67941"},{"Application Name":"Windows Spotlight","Application IDs":"1b3c667f-cde3-4090-b60b-3d2abd0117f0"},{"Application Name":"Windows Store for Business","Application IDs":"45a330b1-b1ec-4cc1-9161-9f03992aa49f"},{"Application Name":"Yammer","Application IDs":"00000005-0000-0ff1-ce00-000000000000"},{"Application Name":"Yammer Web","Application IDs":"c1c74fed-04c9-4704-80dc-9f79a2e515cb"},{"Application Name":"Yammer Web Embed","Application IDs":"e1ef36fd-b883-4dbf-97f0-9ece4b576fc6"}]' | ConvertFrom-Json | Where-Object -Property 'Application IDs' -EQ $data.applicationId - $HTMLTemplate = Get-Content 'Resources\TemplateEmail.html' -Raw | Out-String + $HTMLTemplate = Get-Content 'TemplateEmail.html' -Raw | Out-String $Title = '' $IntroText = '' $ButtonUrl = '' diff --git a/Resources/TemplateEmail.html b/TemplateEmail.html similarity index 100% rename from Resources/TemplateEmail.html rename to TemplateEmail.html From 68e6c1db1a1cd7d46be025d0ced2af67292900eb Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Fri, 30 Jan 2026 11:16:11 +0100 Subject: [PATCH 319/503] up version --- host.json | 2 +- version_latest.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/host.json b/host.json index aa8f581b4e1f..a581a9789068 100644 --- a/host.json +++ b/host.json @@ -16,7 +16,7 @@ "distributedTracingEnabled": false, "version": "None" }, - "defaultVersion": "10.0.6", + "defaultVersion": "10.0.7", "versionMatchStrategy": "Strict", "versionFailureStrategy": "Fail" } diff --git a/version_latest.txt b/version_latest.txt index 80f86ac0c358..9380cfccb8c7 100644 --- a/version_latest.txt +++ b/version_latest.txt @@ -1 +1 @@ -10.0.6 +10.0.7 From 3d3626c82debeef499c4c5d558b657d3651c88a7 Mon Sep 17 00:00:00 2001 From: rvdwegen Date: Fri, 30 Jan 2026 11:31:47 +0100 Subject: [PATCH 320/503] move words.txt back to root --- Modules/CIPPCore/Public/GraphHelper/New-passwordString.ps1 | 2 +- Resources/words.txt => words.txt | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename Resources/words.txt => words.txt (100%) diff --git a/Modules/CIPPCore/Public/GraphHelper/New-passwordString.ps1 b/Modules/CIPPCore/Public/GraphHelper/New-passwordString.ps1 index 89545efd1229..c8393455c326 100644 --- a/Modules/CIPPCore/Public/GraphHelper/New-passwordString.ps1 +++ b/Modules/CIPPCore/Public/GraphHelper/New-passwordString.ps1 @@ -10,7 +10,7 @@ function New-passwordString { $SettingsTable = Get-CippTable -tablename 'Settings' $PasswordType = (Get-CIPPAzDataTableEntity @SettingsTable).passwordType if ($PasswordType -eq 'Correct-Battery-Horse') { - $Words = Get-Content .\Resources\words.txt + $Words = Get-Content .\words.txt (Get-Random -InputObject $words -Count 4) -join '-' } else { # Generate a complex password with a maximum of 100 tries diff --git a/Resources/words.txt b/words.txt similarity index 100% rename from Resources/words.txt rename to words.txt From 05aacb2b65a8fd187ab9cb72fb01429ffb5a6352 Mon Sep 17 00:00:00 2001 From: rvdwegen Date: Fri, 30 Jan 2026 11:35:44 +0100 Subject: [PATCH 321/503] more stuff back to root --- Resources/CIPPTimers.json => CIPPTimers.json | 0 Resources/CommunityRepos.json => CommunityRepos.json | 0 Resources/ConversionTable.csv => ConversionTable.csv | 0 .../HTTP Functions/Tools/GitHub/Invoke-ListCommunityRepos.ps1 | 2 +- Modules/CIPPCore/Public/Get-CIPPTimerFunctions.ps1 | 2 +- .../Public/Gradient/New-GradientServiceSyncRun.ps1 | 4 ++-- .../CippExtensions/Public/Hudu/Invoke-HuduExtensionSync.ps1 | 2 +- 7 files changed, 5 insertions(+), 5 deletions(-) rename Resources/CIPPTimers.json => CIPPTimers.json (100%) rename Resources/CommunityRepos.json => CommunityRepos.json (100%) rename Resources/ConversionTable.csv => ConversionTable.csv (100%) diff --git a/Resources/CIPPTimers.json b/CIPPTimers.json similarity index 100% rename from Resources/CIPPTimers.json rename to CIPPTimers.json diff --git a/Resources/CommunityRepos.json b/CommunityRepos.json similarity index 100% rename from Resources/CommunityRepos.json rename to CommunityRepos.json diff --git a/Resources/ConversionTable.csv b/ConversionTable.csv similarity index 100% rename from Resources/ConversionTable.csv rename to ConversionTable.csv diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tools/GitHub/Invoke-ListCommunityRepos.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tools/GitHub/Invoke-ListCommunityRepos.ps1 index 8c33f1ed2d2a..79256990242f 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tools/GitHub/Invoke-ListCommunityRepos.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tools/GitHub/Invoke-ListCommunityRepos.ps1 @@ -24,7 +24,7 @@ function Invoke-ListCommunityRepos { if (!$Request.Query.WriteAccess) { $CIPPRoot = (Get-Item (Get-Module -Name CIPPCore).ModuleBase).Parent.Parent.FullName - $CommunityRepos = Join-Path -Path $CIPPRoot -ChildPath 'Resources\CommunityRepos.json' + $CommunityRepos = Join-Path -Path $CIPPRoot -ChildPath 'CommunityRepos.json' $DefaultCommunityRepos = Get-Content -Path $CommunityRepos -Raw | ConvertFrom-Json $DefaultsMissing = $false diff --git a/Modules/CIPPCore/Public/Get-CIPPTimerFunctions.ps1 b/Modules/CIPPCore/Public/Get-CIPPTimerFunctions.ps1 index 529cd3f3aa84..d60ca8ed40cf 100644 --- a/Modules/CIPPCore/Public/Get-CIPPTimerFunctions.ps1 +++ b/Modules/CIPPCore/Public/Get-CIPPTimerFunctions.ps1 @@ -40,7 +40,7 @@ function Get-CIPPTimerFunctions { } $CIPPRoot = (Get-Item $CIPPCoreModuleRoot).Parent.Parent - $CippTimers = Get-Content -Path $CIPPRoot\Resources\CIPPTimers.json + $CippTimers = Get-Content -Path $CIPPRoot\CIPPTimers.json if ($ListAllTasks) { $Orchestrators = $CippTimers | ConvertFrom-Json | Sort-Object -Property Priority diff --git a/Modules/CippExtensions/Public/Gradient/New-GradientServiceSyncRun.ps1 b/Modules/CippExtensions/Public/Gradient/New-GradientServiceSyncRun.ps1 index b3e32933987b..cb85b1b60c74 100644 --- a/Modules/CippExtensions/Public/Gradient/New-GradientServiceSyncRun.ps1 +++ b/Modules/CippExtensions/Public/Gradient/New-GradientServiceSyncRun.ps1 @@ -27,8 +27,8 @@ function New-GradientServiceSyncRun { } - Set-Location (Get-Item $PSScriptRoot).Parent.Parent.Parent.Parent.FullName - $ConvertTable = Import-Csv Resources\ConversionTable.csv + Set-Location (Get-Item $PSScriptRoot).Parent.FullName + $ConvertTable = Import-Csv ConversionTable.csv $Table = Get-CIPPTable -TableName cachelicenses $LicenseTable = Get-CIPPTable -TableName ExcludedLicenses $ExcludedSkuList = Get-CIPPAzDataTableEntity @LicenseTable diff --git a/Modules/CippExtensions/Public/Hudu/Invoke-HuduExtensionSync.ps1 b/Modules/CippExtensions/Public/Hudu/Invoke-HuduExtensionSync.ps1 index 61b68e9e3b02..19b1e8c13def 100644 --- a/Modules/CippExtensions/Public/Hudu/Invoke-HuduExtensionSync.ps1 +++ b/Modules/CippExtensions/Public/Hudu/Invoke-HuduExtensionSync.ps1 @@ -34,7 +34,7 @@ function Invoke-HuduExtensionSync { # Import license mapping Set-Location (Get-Item $PSScriptRoot).Parent.Parent.Parent.Parent.FullName - $LicTable = Import-Csv Resources\ConversionTable.csv + $LicTable = Import-Csv ConversionTable.csv $CompanyResult.Logs.Add('Starting Hudu Extension Sync') From a42df97a164c533849c1e7d8cc319474bf08cba3 Mon Sep 17 00:00:00 2001 From: rvdwegen Date: Fri, 30 Jan 2026 11:37:42 +0100 Subject: [PATCH 322/503] move intunecollection back --- Resources/intuneCollection.json => intuneCollection.json | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Resources/intuneCollection.json => intuneCollection.json (100%) diff --git a/Resources/intuneCollection.json b/intuneCollection.json similarity index 100% rename from Resources/intuneCollection.json rename to intuneCollection.json From 334b95fd5ebe0885f74ac897666e2f62fd0dbe8d Mon Sep 17 00:00:00 2001 From: rvdwegen Date: Fri, 30 Jan 2026 11:45:27 +0100 Subject: [PATCH 323/503] put set-locations back --- Modules/CIPPCore/Public/Add-CIPPApplicationPermission.ps1 | 1 + Modules/CIPPCore/Public/Add-CIPPDelegatedPermission.ps1 | 1 + .../Tenant/Administration/Alerts/Invoke-PublicWebhooks.ps1 | 1 + Modules/CIPPCore/Public/Test-CIPPAccessPermissions.ps1 | 1 + Modules/CippEntrypoints/CippEntrypoints.psm1 | 1 + .../Public/NinjaOne/Invoke-NinjaOneTenantSync.ps1 | 3 +++ 6 files changed, 8 insertions(+) diff --git a/Modules/CIPPCore/Public/Add-CIPPApplicationPermission.ps1 b/Modules/CIPPCore/Public/Add-CIPPApplicationPermission.ps1 index c7963cc5a196..e927820cb13f 100644 --- a/Modules/CIPPCore/Public/Add-CIPPApplicationPermission.ps1 +++ b/Modules/CIPPCore/Public/Add-CIPPApplicationPermission.ps1 @@ -11,6 +11,7 @@ function Add-CIPPApplicationPermission { } if ($RequiredResourceAccess -eq 'CIPPDefaults') { + Set-Location (Get-Item $PSScriptRoot).FullName $Permissions = Get-CippSamPermissions -NoDiff $RequiredResourceAccess = [System.Collections.Generic.List[object]]::new() diff --git a/Modules/CIPPCore/Public/Add-CIPPDelegatedPermission.ps1 b/Modules/CIPPCore/Public/Add-CIPPDelegatedPermission.ps1 index 26ece527aa48..2202e25147f9 100644 --- a/Modules/CIPPCore/Public/Add-CIPPDelegatedPermission.ps1 +++ b/Modules/CIPPCore/Public/Add-CIPPDelegatedPermission.ps1 @@ -8,6 +8,7 @@ function Add-CIPPDelegatedPermission { $TenantFilter ) Write-Host 'Adding Delegated Permissions' + Set-Location (Get-Item $PSScriptRoot).FullName if ($ApplicationId -eq $env:ApplicationID -and $TenantFilter -eq $env:TenantID) { #return @('Cannot modify delgated permissions for CIPP-SAM on partner tenant') diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-PublicWebhooks.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-PublicWebhooks.ps1 index 43289dc9cd39..e4318db2007e 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-PublicWebhooks.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-PublicWebhooks.ps1 @@ -8,6 +8,7 @@ function Invoke-PublicWebhooks { param($Request, $TriggerMetadata) $Headers = $Request.Headers + Set-Location (Get-Item $PSScriptRoot).Parent.FullName $WebhookTable = Get-CIPPTable -TableName webhookTable $WebhookIncoming = Get-CIPPTable -TableName WebhookIncoming $Webhooks = Get-CIPPAzDataTableEntity @WebhookTable diff --git a/Modules/CIPPCore/Public/Test-CIPPAccessPermissions.ps1 b/Modules/CIPPCore/Public/Test-CIPPAccessPermissions.ps1 index 9843f4c7de09..e95cdb074f8d 100644 --- a/Modules/CIPPCore/Public/Test-CIPPAccessPermissions.ps1 +++ b/Modules/CIPPCore/Public/Test-CIPPAccessPermissions.ps1 @@ -25,6 +25,7 @@ function Test-CIPPAccessPermissions { } $Success = $true try { + Set-Location (Get-Item $PSScriptRoot).FullName $null = Get-CIPPAuthentication $GraphToken = Get-GraphToken -returnRefresh $true -SkipCache $true if ($GraphToken) { diff --git a/Modules/CippEntrypoints/CippEntrypoints.psm1 b/Modules/CippEntrypoints/CippEntrypoints.psm1 index 1f521af36c66..bc94fb638e28 100644 --- a/Modules/CippEntrypoints/CippEntrypoints.psm1 +++ b/Modules/CippEntrypoints/CippEntrypoints.psm1 @@ -39,6 +39,7 @@ function Receive-CippHttpTrigger { # Convert the request to a PSCustomObject because the httpContext is case sensitive since 7.3 $Request = $Request | ConvertTo-Json -Depth 100 | ConvertFrom-Json + Set-Location (Get-Item $PSScriptRoot).Parent.Parent.FullName if ($Request.Params.CIPPEndpoint -eq '$batch') { # Implement batch processing in the style of graph api $batch diff --git a/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneTenantSync.ps1 b/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneTenantSync.ps1 index ca67d3b360e3..7d2b1a4b9aab 100644 --- a/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneTenantSync.ps1 +++ b/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneTenantSync.ps1 @@ -1846,6 +1846,9 @@ function Invoke-NinjaOneTenantSync { ### CIPP Applied Standards Cards Write-Information 'Applied Standards' + $ModuleBase = Get-Module CIPPExtensions | Select-Object -ExpandProperty ModuleBase + $CIPPRoot = (Get-Item $ModuleBase).Parent.Parent.FullName + Set-Location $CIPPRoot try { $StandardsDefinitions = Invoke-RestMethod -Uri 'https://raw.githubusercontent.com/KelvinTegelaar/CIPP/refs/heads/main/src/data/standards.json' From d8cbc433e2a8e5e40002f4662b2568d4c294a8ec Mon Sep 17 00:00:00 2001 From: rvdwegen Date: Fri, 30 Jan 2026 11:46:40 +0100 Subject: [PATCH 324/503] one more --- profile.ps1 | 1 + 1 file changed, 1 insertion(+) diff --git a/profile.ps1 b/profile.ps1 index 3925ffbf3f9d..a1ec3269f8d5 100644 --- a/profile.ps1 +++ b/profile.ps1 @@ -8,6 +8,7 @@ if ($env:APPLICATIONINSIGHTS_CONNECTION_STRING -or $env:APPINSIGHTS_INSTRUMENTAT $hasAppInsights = $true } if ($hasAppInsights) { + Set-Location -Path $PSScriptRoot $SwAppInsights = [System.Diagnostics.Stopwatch]::StartNew() try { $AppInsightsDllPath = Join-Path $PSScriptRoot 'Shared\AppInsights\Microsoft.ApplicationInsights.dll' From 32160569d548c56f6a2e89c7033b497af01e05b3 Mon Sep 17 00:00:00 2001 From: rvdwegen Date: Fri, 30 Jan 2026 11:59:21 +0100 Subject: [PATCH 325/503] Only run DB update if change was made --- .../Standards/Invoke-CIPPStandardAppDeploy.ps1 | 17 ++++++++++++----- ...nvoke-CIPPStandardDisableResourceMailbox.ps1 | 14 +++++++++----- .../Invoke-CIPPStandardDisableSharedMailbox.ps1 | 16 ++++++++++------ .../Standards/Invoke-CIPPStandardPerUserMFA.ps1 | 16 ++++++++++------ ...Invoke-CIPPStandardUserPreferredLanguage.ps1 | 14 +++++++++----- .../Standards/Invoke-CIPPStandardcalDefault.ps1 | 14 +++++++++----- 6 files changed, 59 insertions(+), 32 deletions(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAppDeploy.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAppDeploy.ps1 index 318e751bfd2e..547dca85d033 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAppDeploy.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAppDeploy.ps1 @@ -111,6 +111,7 @@ function Invoke-CIPPStandardAppDeploy { $CurrentValue = if ($MissingApps.Count -eq 0) { [PSCustomObject]@{'state' = 'Configured correctly' } } else { [PSCustomObject]@{'MissingApps' = $MissingApps } } if ($Settings.remediate -eq $true) { + $UpdateDB = $false if ($Mode -eq 'copy') { foreach ($App in $AppsToAdd) { $App = $App.Trim() @@ -121,6 +122,7 @@ function Invoke-CIPPStandardAppDeploy { try { New-CIPPApplicationCopy -App $App -Tenant $Tenant Write-LogMessage -API 'Standards' -tenant $tenant -message "Added application $($Application.displayName) ($App) to $Tenant and updated it's permissions" -sev Info + $UpdateDB = $true } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to add app $($Application.displayName) ($App). Error: $ErrorMessage" -sev Error @@ -175,6 +177,7 @@ function Invoke-CIPPStandardAppDeploy { if ($InstantiateResult.application.appId) { Write-LogMessage -API 'Standards' -tenant $tenant -message "Successfully deployed Gallery Template $($TemplateData.AppName) to tenant $Tenant. Application ID: $($InstantiateResult.application.appId)" -sev Info New-CIPPApplicationCopy -App $InstantiateResult.application.appId -Tenant $Tenant + $UpdateDB = $true } else { Write-LogMessage -API 'Standards' -tenant $tenant -message "Gallery Template deployment completed but application ID not returned for $($TemplateData.AppName) in tenant $Tenant" -sev Warning } @@ -243,6 +246,7 @@ function Invoke-CIPPStandardAppDeploy { Add-CIPPDelegatedPermission -RequiredResourceAccess $CreatedApp.requiredResourceAccess -ApplicationId $CreatedApp.appId -Tenantfilter $Tenant Add-CIPPApplicationPermission -RequiredResourceAccess $CreatedApp.requiredResourceAccess -ApplicationId $CreatedApp.appId -Tenantfilter $Tenant } + $UpdateDB = $true } else { Write-LogMessage -API 'Standards' -tenant $tenant -message "Application Manifest deployment failed - no application ID returned for $($TemplateData.AppName) in tenant $Tenant" -sev Error } @@ -263,6 +267,7 @@ function Invoke-CIPPStandardAppDeploy { Add-CIPPApplicationPermission -TemplateId $TemplateId -TenantFilter $Tenant Add-CIPPDelegatedPermission -TemplateId $TemplateId -TenantFilter $Tenant Write-LogMessage -API 'Standards' -tenant $tenant -message "Added application $($TemplateData.AppName) from Enterprise App template and updated its permissions" -sev Info + $UpdateDB = $true } } catch { @@ -272,11 +277,13 @@ function Invoke-CIPPStandardAppDeploy { } } - # Refresh service principals cache after remediation - try { - Set-CIPPDBCacheServicePrincipals -TenantFilter $Tenant - } catch { - Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to refresh service principals cache after remediation: $($_.Exception.Message)" -sev Warning + # Refresh service principals cache after remediation only if changes were made + if ($UpdateDB) { + try { + Set-CIPPDBCacheServicePrincipals -TenantFilter $Tenant + } catch { + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to refresh service principals cache after remediation: $($_.Exception.Message)" -sev Warning + } } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableResourceMailbox.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableResourceMailbox.ps1 index a15bcae71e8a..c0611aa41e34 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableResourceMailbox.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableResourceMailbox.ps1 @@ -57,6 +57,7 @@ function Invoke-CIPPStandardDisableResourceMailbox { } if ($Settings.remediate -eq $true) { + $UpdateDB = $false if ($ResourceMailboxList.Count -gt 0) { $int = 0 $BulkRequests = foreach ($Mailbox in $ResourceMailboxList) { @@ -80,6 +81,7 @@ function Invoke-CIPPStandardDisableResourceMailbox { if ($result.status -eq 200 -or $result.status -eq 204) { Write-LogMessage -API 'Standards' -tenant $Tenant -message "Entra account for $($Mailbox.RecipientTypeDetails), $($Mailbox.DisplayName), $($Mailbox.UserPrincipalName) disabled." -sev Info + $UpdateDB = $true } else { $errorMsg = if ($result.body.error.message) { $result.body.error.message } else { "Unknown error (Status: $($result.status))" } Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to disable Entra account for $($Mailbox.RecipientTypeDetails), $($Mailbox.DisplayName), $($Mailbox.UserPrincipalName): $errorMsg" -sev Error @@ -90,11 +92,13 @@ function Invoke-CIPPStandardDisableResourceMailbox { Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to process bulk disable resource mailboxes request: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage } - # Refresh user cache after remediation - try { - Set-CIPPDBCacheUsers -TenantFilter $Tenant - } catch { - Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to refresh user cache after remediation: $($_.Exception.Message)" -sev Warning + # Refresh user cache after remediation only if changes were made + if ($UpdateDB) { + try { + Set-CIPPDBCacheUsers -TenantFilter $Tenant + } catch { + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to refresh user cache after remediation: $($_.Exception.Message)" -sev Warning + } } } else { Write-LogMessage -API 'Standards' -tenant $Tenant -message 'All Entra accounts for resource mailboxes are already disabled.' -sev Info diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSharedMailbox.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSharedMailbox.ps1 index 1f1202c0a164..c1a1d4f9bb78 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSharedMailbox.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSharedMailbox.ps1 @@ -50,6 +50,7 @@ function Invoke-CIPPStandardDisableSharedMailbox { } if ($Settings.remediate -eq $true) { + $UpdateDB = $false if ($SharedMailboxList.Count -gt 0) { $int = 0 $BulkRequests = foreach ($Mailbox in $SharedMailboxList) { @@ -73,6 +74,7 @@ function Invoke-CIPPStandardDisableSharedMailbox { if ($result.status -eq 200 -or $result.status -eq 204) { Write-LogMessage -API 'Standards' -tenant $Tenant -message "Entra account for shared mailbox $($Mailbox.DisplayName) ($($Mailbox.ObjectKey)) disabled." -sev Info + $UpdateDB = $true } else { $errorMsg = if ($result.body.error.message) { $result.body.error.message } else { "Unknown error (Status: $($result.status))" } Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to disable Entra account for shared mailbox $($Mailbox.DisplayName) ($($Mailbox.ObjectKey)): $errorMsg" -sev Error @@ -82,12 +84,14 @@ function Invoke-CIPPStandardDisableSharedMailbox { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to process bulk disable shared mailboxes request: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage } - - # Refresh user cache after remediation - try { - Set-CIPPDBCacheUsers -TenantFilter $Tenant - } catch { - Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to refresh user cache after remediation: $($_.Exception.Message)" -sev Warning + + # Refresh user cache after remediation only if changes were made + if ($UpdateDB) { + try { + Set-CIPPDBCacheUsers -TenantFilter $Tenant + } catch { + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to refresh user cache after remediation: $($_.Exception.Message)" -sev Warning + } } } else { Write-LogMessage -API 'Standards' -tenant $Tenant -message 'All Entra accounts for shared mailboxes are already disabled.' -sev Info diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPerUserMFA.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPerUserMFA.ps1 index 98d5a2fcb762..60a44f4a4512 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPerUserMFA.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPerUserMFA.ps1 @@ -55,20 +55,24 @@ function Invoke-CIPPStandardPerUserMFA { $UsersWithoutMFA = $GraphRequest | Where-Object -Property perUserMfaState -NE 'enforced' | Select-Object -Property userPrincipalName, displayName, accountEnabled, perUserMfaState if ($Settings.remediate -eq $true) { + $UpdateDB = $false if (($UsersWithoutMFA | Measure-Object).Count -gt 0) { try { $MFAMessage = Set-CIPPPerUserMFA -TenantFilter $Tenant -userId @($UsersWithoutMFA.userPrincipalName) -State 'enforced' Write-LogMessage -API 'Standards' -tenant $tenant -message $MFAMessage -sev Info + $UpdateDB = $true } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to enforce MFA for all users: $ErrorMessage" -sev Error } - - # Refresh user cache after remediation - try { - Set-CIPPDBCacheUsers -TenantFilter $Tenant - } catch { - Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to refresh user cache after remediation: $($_.Exception.Message)" -sev Warning + + # Refresh user cache after remediation only if changes were made + if ($UpdateDB) { + try { + Set-CIPPDBCacheUsers -TenantFilter $Tenant + } catch { + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to refresh user cache after remediation: $($_.Exception.Message)" -sev Warning + } } } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardUserPreferredLanguage.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardUserPreferredLanguage.ps1 index 08c7dfd53772..9f015db38c4a 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardUserPreferredLanguage.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardUserPreferredLanguage.ps1 @@ -46,6 +46,7 @@ function Invoke-CIPPStandardUserPreferredLanguage { } if ($Settings.remediate -eq $true) { + $UpdateDB = $false if (($IncorrectUsers | Measure-Object).Count -gt 0) { try { foreach ($user in $IncorrectUsers) { @@ -61,17 +62,20 @@ function Invoke-CIPPStandardUserPreferredLanguage { } $null = New-GraphPOSTRequest @cmdParams Write-LogMessage -API 'Standards' -tenant $Tenant -message "Preferred language for $($user.userPrincipalName) has been set to $preferredLanguage" -sev Info + $UpdateDB = $true } } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to set preferred language to $preferredLanguage for all users." -sev Error -LogData $ErrorMessage } - # Refresh user cache after remediation - try { - Set-CIPPDBCacheUsers -TenantFilter $Tenant - } catch { - Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to refresh user cache after remediation: $($_.Exception.Message)" -sev Warning + # Refresh user cache after remediation only if changes were made + if ($UpdateDB) { + try { + Set-CIPPDBCacheUsers -TenantFilter $Tenant + } catch { + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to refresh user cache after remediation: $($_.Exception.Message)" -sev Warning + } } } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardcalDefault.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardcalDefault.ps1 index 2edce2a10d35..37873d00e0cd 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardcalDefault.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardcalDefault.ps1 @@ -50,6 +50,7 @@ function Invoke-CIPPStandardcalDefault { } if ($Settings.remediate -eq $true) { + $UpdateDB = $false try { # Get calendar permissions from cache - this contains the calendar Identity we need $CalendarPermissions = New-CIPPDbRequest -TenantFilter $Tenant -Type 'CalendarPermissions' @@ -89,6 +90,7 @@ function Invoke-CIPPStandardcalDefault { } Write-LogMessage -API 'Standards' -tenant $Tenant -message "Set default calendar permission for $($Calendar.Identity) to $permissionLevel" -sev Debug $SuccessCounter++ + $UpdateDB = $true } catch { $ErrorCounter++ $ErrorMessage = Get-CippException -Exception $_ @@ -98,11 +100,13 @@ function Invoke-CIPPStandardcalDefault { Write-LogMessage -API 'Standards' -tenant $Tenant -message "Successfully set default calendar permissions for $SuccessCounter calendars. $ErrorCounter failed." -sev Info - # Refresh calendar permissions cache after remediation - try { - Set-CIPPDBCacheMailboxes -TenantFilter $Tenant - } catch { - Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to refresh mailbox cache after remediation: $($_.Exception.Message)" -sev Warning + # Refresh calendar permissions cache after remediation only if changes were made + if ($UpdateDB) { + try { + Set-CIPPDBCacheMailboxes -TenantFilter $Tenant + } catch { + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to refresh mailbox cache after remediation: $($_.Exception.Message)" -sev Warning + } } } catch { From 703f9478da7f4668cb9e9ba74837ecf2387cb796 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Fri, 30 Jan 2026 13:28:41 +0100 Subject: [PATCH 326/503] fix: remove header parameter from exo request --- Modules/CIPPCore/Public/Set-CIPPMailboxRule.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Set-CIPPMailboxRule.ps1 b/Modules/CIPPCore/Public/Set-CIPPMailboxRule.ps1 index b7b0f4810c15..311a02400c6f 100644 --- a/Modules/CIPPCore/Public/Set-CIPPMailboxRule.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPMailboxRule.ps1 @@ -22,7 +22,7 @@ } try { - $null = New-ExoRequest -tenantid $TenantFilter -cmdlet "$State-InboxRule" -Anchor $Username -cmdParams @{Identity = $RuleId; mailbox = $UserId } -Headers $Headers + $null = New-ExoRequest -tenantid $TenantFilter -cmdlet "$State-InboxRule" -Anchor $Username -cmdParams @{Identity = $RuleId; Mailbox = $UserId } Write-LogMessage -headers $Headers -API $APIName -message "Successfully set mailbox rule $($RuleName) for $($Username) to $($State)d" -Sev 'Info' -tenant $TenantFilter return "Successfully set mailbox rule $($RuleName) for $($Username) to $($State)d" } catch { From c670995c823d4b1679d3a1a5e13dbed92d92fe56 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 30 Jan 2026 12:04:32 -0500 Subject: [PATCH 327/503] fix version check --- Modules/CIPPCore/Public/Assert-CippVersion.ps1 | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Modules/CIPPCore/Public/Assert-CippVersion.ps1 b/Modules/CIPPCore/Public/Assert-CippVersion.ps1 index ac61237dcb60..c96873ede537 100644 --- a/Modules/CIPPCore/Public/Assert-CippVersion.ps1 +++ b/Modules/CIPPCore/Public/Assert-CippVersion.ps1 @@ -10,8 +10,10 @@ function Assert-CippVersion { Local version of CIPP frontend #> - Param($CIPPVersion) - $APIVersion = (Get-Content 'version_latest.txt' -Raw).trim() + param($CIPPVersion) + $CIPPCoreModuleRoot = Get-Module -Name CIPPCore | Select-Object -ExpandProperty ModuleBase + $CIPPRoot = (Get-Item $CIPPCoreModuleRoot).Parent.Parent + $APIVersion = (Get-Content -Path $CIPPRoot\version_latest.txt).trim() $RemoteAPIVersion = (Invoke-RestMethod -Uri 'https://raw.githubusercontent.com/KelvinTegelaar/CIPP-API/master/version_latest.txt').trim() $RemoteCIPPVersion = (Invoke-RestMethod -Uri 'https://raw.githubusercontent.com/KelvinTegelaar/CIPP/main/public/version.json').version From b83297b646dd9dd18c9341a742532929c3e3cb45 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 30 Jan 2026 12:57:22 -0500 Subject: [PATCH 328/503] Update Invoke-ListExtensionsConfig.ps1 --- .../CIPPCore/Public/Entrypoints/Invoke-ListExtensionsConfig.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/Invoke-ListExtensionsConfig.ps1 b/Modules/CIPPCore/Public/Entrypoints/Invoke-ListExtensionsConfig.ps1 index eb94ced2c322..494b04b7b782 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Invoke-ListExtensionsConfig.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Invoke-ListExtensionsConfig.ps1 @@ -10,7 +10,7 @@ function Invoke-ListExtensionsConfig { $Table = Get-CIPPTable -TableName Extensionsconfig try { $Config = (Get-CIPPAzDataTableEntity @Table).config - if (Test-Json -Json $Config -ErrorAction SilentlyContinue) { + if ($Config -and (Test-Json -Json $Config -ErrorAction SilentlyContinue)) { $Body = $Config | ConvertFrom-Json -Depth 10 -ErrorAction Stop if ($Body.HaloPSA.TicketType -and !$Body.HaloPSA.TicketType.value) { # translate ticket type to autocomplete format From edac97e3804d436bbcd0f24900c1446c4b225cd3 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 30 Jan 2026 15:11:33 -0500 Subject: [PATCH 329/503] Optimize audit log processing and GUID lookups MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Improve audit log download and search orchestration and refactor GUID/user resolution for performance and reliability. Push-AuditLogTenantDownload: sort searches by start time, early-return when none ready, mark status updates and avoid returning unused download objects. Start-AuditLogSearchCreation: fix minor logging typo. Test-CIPPAuditLogRules: large refactor to precompile regexes, build O(1) hashtable lookups for users/groups/devices/service principals/partner users, validate and migrate cached lookup format (support legacy arrays and new hashtable JSON), cache hashtables to storage, and update Add-CIPPGuidMappings to use lookups—reducing O(n) scans and improving resiliency when cache is corrupted. Overall changes target performance, clearer logging, and safer cache handling. --- .../Webhooks/Push-AuditLogTenantDownload.ps1 | 11 +- .../Start-AuditLogSearchCreation.ps1 | 3 +- .../Webhooks/Test-CIPPAuditLogRules.ps1 | 306 +++++++++++++----- 3 files changed, 230 insertions(+), 90 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Webhooks/Push-AuditLogTenantDownload.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Webhooks/Push-AuditLogTenantDownload.ps1 index e80608d97d6a..54f1dc0f3c16 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Webhooks/Push-AuditLogTenantDownload.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Webhooks/Push-AuditLogTenantDownload.ps1 @@ -39,15 +39,19 @@ function Push-AuditLogTenantDownload { $LogSearchesTable = Get-CippTable -TableName 'AuditLogSearches' try { - $LogSearches = Get-CippAuditLogSearches -TenantFilter $TenantFilter -ReadyToProcess | Select-Object -First 10 - Write-Information ('Audit Logs: Found {0} searches, begin downloading' -f $LogSearches.Count) + $LogSearches = Get-CippAuditLogSearches -TenantFilter $TenantFilter -ReadyToProcess | Sort-Object -Property filterStartDateTime | Select-Object -First 10 + if ($LogSearches.Count -eq 0) { + Write-Information "Audit Logs: No searches ready to process for $TenantFilter" + return $true + } + Write-Information ('Audit Logs: Found {0} searches for {1}, begin downloading' -f $LogSearches.Count, $TenantFilter) foreach ($Search in $LogSearches) { $SearchEntity = Get-CIPPAzDataTableEntity @LogSearchesTable -Filter "Tenant eq '$($TenantFilter)' and RowKey eq '$($Search.id)'" $SearchEntity.CippStatus = 'Processing' Add-CIPPAzDataTableEntity @LogSearchesTable -Entity $SearchEntity -Force try { Write-Information "Audit Log search: Processing search ID: $($Search.id) for tenant: $TenantFilter" - $Downloads = New-CIPPAuditLogSearchResultsCache -TenantFilter $TenantFilter -searchId $Search.id + $null = New-CIPPAuditLogSearchResultsCache -TenantFilter $TenantFilter -searchId $Search.id $SearchEntity.CippStatus = 'Downloaded' } catch { if ($_.Exception.Message -match 'Request rate is large. More Request Units may be needed, so no changes were made. Please retry this request later.') { @@ -68,6 +72,7 @@ function Push-AuditLogTenantDownload { } Add-CIPPAzDataTableEntity @LogSearchesTable -Entity $SearchEntity -Force } + return $true } catch { Write-Information ('Audit Log search: Error {0} line {1} - {2}' -f $_.InvocationInfo.ScriptName, $_.InvocationInfo.ScriptLineNumber, $_.Exception.Message) return $false diff --git a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-AuditLogSearchCreation.ps1 b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-AuditLogSearchCreation.ps1 index c1ae93dd22a1..a8c64da33fec 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-AuditLogSearchCreation.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-AuditLogSearchCreation.ps1 @@ -45,7 +45,6 @@ function Start-AuditLogSearchCreation { } if (!$TenantInConfig) { - Write-Information "Tenant $($Tenant.defaultDomainName) has no configured audit log rules, skipping search creation." continue } @@ -67,7 +66,7 @@ function Start-AuditLogSearchCreation { SkipLog = $true } Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Depth 5 -Compress) - Write-Information "Started Audit Log search creation orchestratorwith $($Batch.Count) tenants" + Write-Information "Started Audit Log search creation orchestrator with $($Batch.Count) tenants" } else { Write-Information 'No tenants found for Audit Log search creation' } diff --git a/Modules/CIPPCore/Public/Webhooks/Test-CIPPAuditLogRules.ps1 b/Modules/CIPPCore/Public/Webhooks/Test-CIPPAuditLogRules.ps1 index cbd8229cf7fe..c33d4d006a53 100644 --- a/Modules/CIPPCore/Public/Webhooks/Test-CIPPAuditLogRules.ps1 +++ b/Modules/CIPPCore/Public/Webhooks/Test-CIPPAuditLogRules.ps1 @@ -8,97 +8,107 @@ function Test-CIPPAuditLogRules { ) try { - # Helper function to map GUIDs and partner UPNs to user objects + # Pre-compiled regex patterns for GUID matching (performance optimization) + $script:StandardGuidRegex = [regex]'^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$' + $script:PartnerUpnRegex = [regex]'user_([0-9a-f]{32})@([^@]+\.onmicrosoft\.com)' + $script:PartnerExchangeRegex = [regex]'([^\\]+\.onmicrosoft\.com)\\tenant:\s*([0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}),\s*object:\s*([0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})' + + # Helper function to map GUIDs and partner UPNs to user objects (Optimized with hashtable lookups) function Add-CIPPGuidMappings { param( [Parameter(Mandatory = $true)] $DataObject, [Parameter(Mandatory = $true)] - $Users, + $UserLookup, [Parameter(Mandatory = $true)] - $Groups, + $GroupLookup, [Parameter(Mandatory = $true)] - $Devices, + $DeviceLookup, [Parameter(Mandatory = $true)] - $ServicePrincipals, + $ServicePrincipalLookup, [Parameter(Mandatory = $true)] - $PartnerUsers, + $PartnerUserLookup, [Parameter(Mandatory = $false)] [string]$PropertyPrefix = '' ) $DataObject.PSObject.Properties | ForEach-Object { - # Check for standard GUID format OR partner UPN formats - if ($_.Value -match '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$' -or - $_.Value -match 'user_[0-9a-f]{32}@[^@]+\.onmicrosoft\.com' -or - $_.Value -match '[^\\]+\.onmicrosoft\.com\\tenant:\s*[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12},\s*object:\s*[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}') { - - # Use regex from guid-resolver hook to match various partner user formats - # Format 1: user_@.onmicrosoft.com - if ($_.Value -match 'user_([0-9a-f]{32})@([^@]+\.onmicrosoft\.com)') { - $hexId = $matches[1] - $tenantDomain = $matches[2] - if ($hexId.Length -eq 32) { - # Convert the 32-character hex string to GUID format - $guid = "$($hexId.Substring(0,8))-$($hexId.Substring(8,4))-$($hexId.Substring(12,4))-$($hexId.Substring(16,4))-$($hexId.Substring(20,12))" - Write-Information "Found partner UPN format: $($_.Value) with GUID: $guid and tenant: $tenantDomain" - - # Check partner users for this GUID - foreach ($PartnerUser in $PartnerUsers) { - if ($PartnerUser.id -eq $guid) { - $DataObject | Add-Member -NotePropertyName "$PropertyPrefix$($_.Name)" -NotePropertyValue $PartnerUser.userPrincipalName -Force -ErrorAction SilentlyContinue - Write-Information "Mapped Partner User UPN: $($PartnerUser.userPrincipalName) to $PropertyPrefix$($_.Name)" - return - } - } + $propValue = $_.Value + + # Quick type check - skip if not string or empty + if ([string]::IsNullOrEmpty($propValue) -or $propValue -isnot [string]) { + return + } + + # Check for partner UPN format 1: user_@.onmicrosoft.com + $match = $script:PartnerUpnRegex.Match($propValue) + if ($match.Success) { + $hexId = $match.Groups[1].Value + $tenantDomain = $match.Groups[2].Value + if ($hexId.Length -eq 32) { + # Convert hex string to GUID format + $guid = "$($hexId.Substring(0,8))-$($hexId.Substring(8,4))-$($hexId.Substring(12,4))-$($hexId.Substring(16,4))-$($hexId.Substring(20,12))" + Write-Information "Found partner UPN format: $propValue with GUID: $guid and tenant: $tenantDomain" + + # O(1) hashtable lookup instead of O(n) loop + if ($PartnerUserLookup.ContainsKey($guid)) { + $PartnerUser = $PartnerUserLookup[$guid] + $DataObject | Add-Member -NotePropertyName "$PropertyPrefix$($_.Name)" -NotePropertyValue $PartnerUser.userPrincipalName -Force -ErrorAction SilentlyContinue + Write-Information "Mapped Partner User UPN: $($PartnerUser.userPrincipalName) to $PropertyPrefix$($_.Name)" + return } } + } - # Format 2: TenantName.onmicrosoft.com\tenant: , object: - if ($_.Value -match '([^\\]+\.onmicrosoft\.com)\\tenant:\s*([0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}),\s*object:\s*([0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})') { - $customerTenantDomain = $matches[1] - $partnerTenantGuid = $matches[2] - $objectGuid = $matches[3] - Write-Information "Found partner exchange format: customer tenant $customerTenantDomain, partner tenant $partnerTenantGuid, object $objectGuid" - - # Check partner users for this object GUID - foreach ($PartnerUser in $PartnerUsers) { - if ($PartnerUser.id -eq $objectGuid) { - $DataObject | Add-Member -NotePropertyName "$PropertyPrefix$($_.Name)" -NotePropertyValue $PartnerUser.userPrincipalName -Force -ErrorAction SilentlyContinue - Write-Information "Mapped Partner User UPN: $($PartnerUser.userPrincipalName) to $PropertyPrefix$($_.Name)" - return - } - } + # Check for partner exchange format: TenantName.onmicrosoft.com\tenant: , object: + $match = $script:PartnerExchangeRegex.Match($propValue) + if ($match.Success) { + $customerTenantDomain = $match.Groups[1].Value + $partnerTenantGuid = $match.Groups[2].Value + $objectGuid = $match.Groups[3].Value + Write-Information "Found partner exchange format: customer tenant $customerTenantDomain, partner tenant $partnerTenantGuid, object $objectGuid" + + # O(1) hashtable lookup + if ($PartnerUserLookup.ContainsKey($objectGuid)) { + $PartnerUser = $PartnerUserLookup[$objectGuid] + $DataObject | Add-Member -NotePropertyName "$PropertyPrefix$($_.Name)" -NotePropertyValue $PartnerUser.userPrincipalName -Force -ErrorAction SilentlyContinue + Write-Information "Mapped Partner User UPN: $($PartnerUser.userPrincipalName) to $PropertyPrefix$($_.Name)" + return } + } - # Check standard directory objects (users, groups, devices, service principals) - foreach ($User in $Users) { - if ($User.id -eq $_.Value) { - $DataObject | Add-Member -NotePropertyName "$PropertyPrefix$($_.Name)" -NotePropertyValue $User.userPrincipalName -Force -ErrorAction SilentlyContinue - Write-Information "Mapped User: $($User.userPrincipalName) to $PropertyPrefix$($_.Name)" - return - } + # Check for standard GUID format + if ($script:StandardGuidRegex.IsMatch($propValue)) { + $guid = $propValue + + # O(1) hashtable lookups in priority order + if ($UserLookup.ContainsKey($guid)) { + $User = $UserLookup[$guid] + $DataObject | Add-Member -NotePropertyName "$PropertyPrefix$($_.Name)" -NotePropertyValue $User.userPrincipalName -Force -ErrorAction SilentlyContinue + Write-Information "Mapped User: $($User.userPrincipalName) to $PropertyPrefix$($_.Name)" + return } - foreach ($Group in $Groups) { - if ($Group.id -eq $_.Value) { - $DataObject | Add-Member -NotePropertyName "$PropertyPrefix$($_.Name)" -NotePropertyValue $Group -Force -ErrorAction SilentlyContinue - Write-Information "Mapped Group: $($Group.displayName) to $PropertyPrefix$($_.Name)" - return - } + + if ($GroupLookup.ContainsKey($guid)) { + $Group = $GroupLookup[$guid] + $DataObject | Add-Member -NotePropertyName "$PropertyPrefix$($_.Name)" -NotePropertyValue $Group -Force -ErrorAction SilentlyContinue + Write-Information "Mapped Group: $($Group.displayName) to $PropertyPrefix$($_.Name)" + return } - foreach ($Device in $Devices) { - if ($Device.id -eq $_.Value) { - $DataObject | Add-Member -NotePropertyName "$PropertyPrefix$($_.Name)" -NotePropertyValue $Device -Force -ErrorAction SilentlyContinue - Write-Information "Mapped Device: $($Device.displayName) to $PropertyPrefix$($_.Name)" - return - } + + if ($DeviceLookup.ContainsKey($guid)) { + $Device = $DeviceLookup[$guid] + $DataObject | Add-Member -NotePropertyName "$PropertyPrefix$($_.Name)" -NotePropertyValue $Device -Force -ErrorAction SilentlyContinue + Write-Information "Mapped Device: $($Device.displayName) to $PropertyPrefix$($_.Name)" + return } - foreach ($ServicePrincipal in $ServicePrincipals) { - if ($ServicePrincipal.id -eq $_.Value -or $ServicePrincipal.appId -eq $_.Value) { - $DataObject | Add-Member -NotePropertyName "$PropertyPrefix$($_.Name)" -NotePropertyValue $ServicePrincipal -Force -ErrorAction SilentlyContinue - Write-Information "Mapped Service Principal: $($ServicePrincipal.displayName) to $PropertyPrefix$($_.Name)" - return - } + + # ServicePrincipal indexed by both id and appId + if ($ServicePrincipalLookup.ContainsKey($guid)) { + $ServicePrincipal = $ServicePrincipalLookup[$guid] + $DataObject | Add-Member -NotePropertyName "$PropertyPrefix$($_.Name)" -NotePropertyValue $ServicePrincipal -Force -ErrorAction SilentlyContinue + Write-Information "Mapped Service Principal: $($ServicePrincipal.displayName) to $PropertyPrefix$($_.Name)" + return } } } @@ -151,7 +161,31 @@ function Test-CIPPAuditLogRules { $Table = Get-CIPPTable -tablename 'cacheauditloglookups' $1dayago = (Get-Date).AddDays(-1).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ') $Lookups = Get-CIPPAzDataTableEntity @Table -Filter "PartitionKey eq '$TenantFilter' and Timestamp gt datetime'$1dayago'" - if (!$Lookups) { + + # Check if cached data needs refresh (wrong format or corrupted) + $NeedsRefresh = $false + if ($Lookups) { + try { + # Test if we can parse the cached data + $TestUser = ($Lookups | Where-Object { $_.RowKey -eq 'users' }).Data + if (![string]::IsNullOrEmpty($TestUser)) { + $ParsedTest = $TestUser | ConvertFrom-Json -ErrorAction Stop + # Check if data is valid (either array for legacy or PSCustomObject for hashtable) + if ($null -eq $ParsedTest) { + Write-Warning 'Cached data is null after parsing, triggering refresh' + $NeedsRefresh = $true + } + } else { + Write-Warning 'Cached data is empty, triggering refresh' + $NeedsRefresh = $true + } + } catch { + Write-Warning "Error parsing cached data: $($_.Exception.Message), triggering refresh" + $NeedsRefresh = $true + } + } + + if (!$Lookups -or $NeedsRefresh) { # Collect bulk data for users/groups/devices/applications $Requests = @( @{ @@ -180,44 +214,145 @@ function Test-CIPPAuditLogRules { $Groups = ($Response | Where-Object { $_.id -eq 'groups' }).body.value ?? @() $Devices = ($Response | Where-Object { $_.id -eq 'devices' }).body.value ?? @() $ServicePrincipals = ($Response | Where-Object { $_.id -eq 'servicePrincipals' }).body.value ?? @() - # Cache the lookups for 1 day + + # Build hashtables for O(1) GUID lookups + Write-Information "Building hashtable lookups for tenant $TenantFilter" + $UserLookup = @{} + foreach ($User in $Users) { + if (![string]::IsNullOrEmpty($User.id)) { + $UserLookup[$User.id] = $User + } + } + + $GroupLookup = @{} + foreach ($Group in $Groups) { + if (![string]::IsNullOrEmpty($Group.id)) { + $GroupLookup[$Group.id] = $Group + } + } + + $DeviceLookup = @{} + foreach ($Device in $Devices) { + if (![string]::IsNullOrEmpty($Device.id)) { + $DeviceLookup[$Device.id] = $Device + } + } + + $ServicePrincipalLookup = @{} + foreach ($SP in $ServicePrincipals) { + if (![string]::IsNullOrEmpty($SP.id)) { + $ServicePrincipalLookup[$SP.id] = $SP + } + # Also index by appId for dual lookup capability + if (![string]::IsNullOrEmpty($SP.appId)) { + $ServicePrincipalLookup[$SP.appId] = $SP + } + } + Write-Information "Built hashtables: $($UserLookup.Count) users, $($GroupLookup.Count) groups, $($DeviceLookup.Count) devices, $($ServicePrincipalLookup.Count) service principals" + + # Cache the hashtable lookups for 1 day (storing as JSON) $Entities = @( @{ PartitionKey = $TenantFilter RowKey = 'users' - Data = [string]($Users | ConvertTo-Json -Compress) + Data = [string]($UserLookup | ConvertTo-Json -Compress) + Format = 'hashtable' } @{ PartitionKey = $TenantFilter RowKey = 'groups' - Data = [string]($Groups | ConvertTo-Json -Compress) + Data = [string]($GroupLookup | ConvertTo-Json -Compress) + Format = 'hashtable' } @{ PartitionKey = $TenantFilter RowKey = 'devices' - Data = [string]($Devices | ConvertTo-Json -Compress) + Data = [string]($DeviceLookup | ConvertTo-Json -Compress) + Format = 'hashtable' } @{ PartitionKey = $TenantFilter RowKey = 'servicePrincipals' - Data = [string]($ServicePrincipals | ConvertTo-Json -Compress) + Data = [string]($ServicePrincipalLookup | ConvertTo-Json -Compress) + Format = 'hashtable' } ) # Save the cached lookups Add-CIPPAzDataTableEntity @Table -Entity $Entities -Force - Write-Information "Cached directory lookups for tenant $TenantFilter" + Write-Information "Cached directory hashtable lookups for tenant $TenantFilter" } else { - # Use cached lookups - $Users = (($Lookups | Where-Object { $_.RowKey -eq 'users' }).Data | ConvertFrom-Json -ErrorAction SilentlyContinue) ?? @() - $Groups = (($Lookups | Where-Object { $_.RowKey -eq 'groups' }).Data | ConvertFrom-Json -ErrorAction SilentlyContinue) ?? @() - $Devices = (($Lookups | Where-Object { $_.RowKey -eq 'devices' }).Data | ConvertFrom-Json -ErrorAction SilentlyContinue) ?? @() - $ServicePrincipals = (($Lookups | Where-Object { $_.RowKey -eq 'servicePrincipals' }).Data | ConvertFrom-Json -ErrorAction SilentlyContinue) ?? @() - Write-Information "Using cached directory lookups for tenant $TenantFilter" + # Use cached lookups - check if they're already hashtables or need conversion + $UsersLookup = $Lookups | Where-Object { $_.RowKey -eq 'users' } + $GroupsLookup = $Lookups | Where-Object { $_.RowKey -eq 'groups' } + $DevicesLookup = $Lookups | Where-Object { $_.RowKey -eq 'devices' } + $ServicePrincipalsLookup = $Lookups | Where-Object { $_.RowKey -eq 'servicePrincipals' } + + # Check if cached data is already in hashtable format + $IsHashtableFormat = $UsersLookup.Format -eq 'hashtable' + + if ($IsHashtableFormat) { + # Load pre-built hashtables directly from cache + Write-Information "Loading pre-built hashtable lookups from cache for tenant $TenantFilter" + $UserLookup = ($UsersLookup.Data | ConvertFrom-Json -ErrorAction SilentlyContinue -AsHashtable) ?? @{} + $GroupLookup = ($GroupsLookup.Data | ConvertFrom-Json -ErrorAction SilentlyContinue -AsHashtable) ?? @{} + $DeviceLookup = ($DevicesLookup.Data | ConvertFrom-Json -ErrorAction SilentlyContinue -AsHashtable) ?? @{} + $ServicePrincipalLookup = ($ServicePrincipalsLookup.Data | ConvertFrom-Json -ErrorAction SilentlyContinue -AsHashtable) ?? @{} + Write-Information "Loaded hashtables: $($UserLookup.Count) users, $($GroupLookup.Count) groups, $($DeviceLookup.Count) devices, $($ServicePrincipalLookup.Count) service principals" + } else { + # Old format (array) - convert to hashtables + Write-Information "Converting legacy array cache to hashtables for tenant $TenantFilter" + $Users = ($UsersLookup.Data | ConvertFrom-Json -ErrorAction SilentlyContinue) ?? @() + $Groups = ($GroupsLookup.Data | ConvertFrom-Json -ErrorAction SilentlyContinue) ?? @() + $Devices = ($DevicesLookup.Data | ConvertFrom-Json -ErrorAction SilentlyContinue) ?? @() + $ServicePrincipals = ($ServicePrincipalsLookup.Data | ConvertFrom-Json -ErrorAction SilentlyContinue) ?? @() + + # Build hashtables + $UserLookup = @{} + foreach ($User in $Users) { + if (![string]::IsNullOrEmpty($User.id)) { + $UserLookup[$User.id] = $User + } + } + + $GroupLookup = @{} + foreach ($Group in $Groups) { + if (![string]::IsNullOrEmpty($Group.id)) { + $GroupLookup[$Group.id] = $Group + } + } + + $DeviceLookup = @{} + foreach ($Device in $Devices) { + if (![string]::IsNullOrEmpty($Device.id)) { + $DeviceLookup[$Device.id] = $Device + } + } + + $ServicePrincipalLookup = @{} + foreach ($SP in $ServicePrincipals) { + if (![string]::IsNullOrEmpty($SP.id)) { + $ServicePrincipalLookup[$SP.id] = $SP + } + if (![string]::IsNullOrEmpty($SP.appId)) { + $ServicePrincipalLookup[$SP.appId] = $SP + } + } + Write-Information "Built hashtables from legacy cache: $($UserLookup.Count) users, $($GroupLookup.Count) groups, $($DeviceLookup.Count) devices, $($ServicePrincipalLookup.Count) service principals" + } } # partner users $PartnerUsers = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/users?`$select=id,displayName,userPrincipalName,accountEnabled&`$top=999" -AsApp $true -NoAuthCheck $true + # Build partner user hashtable + $PartnerUserLookup = @{} + foreach ($PartnerUser in $PartnerUsers) { + if (![string]::IsNullOrEmpty($PartnerUser.id)) { + $PartnerUserLookup[$PartnerUser.id] = $PartnerUser + } + } + Write-Information "Built partner user hashtable: $($PartnerUserLookup.Count) partner users" + Write-Warning '## Audit Log Configuration ##' Write-Information ($Configuration | ConvertTo-Json -Depth 10) @@ -245,13 +380,13 @@ function Test-CIPPAuditLogRules { $RootProperties = $AuditRecord $Data = $AuditRecord.auditData | Select-Object *, CIPPAction, CIPPClause, CIPPGeoLocation, CIPPBadRepIP, CIPPHostedIP, CIPPIPDetected, CIPPLocationInfo, CIPPExtendedProperties, CIPPDeviceProperties, CIPPParameters, CIPPModifiedProperties, AuditRecord -ErrorAction SilentlyContinue try { - # Attempt to locate GUIDs in $Data and match them with their corresponding user, group, device, or service principal recursively by checking each key/value once located lets store these mapped values in a CIPP$KeyName property + # Attempt to locate GUIDs in $Data and match them with their corresponding user, group, device, or service principal using O(1) hashtable lookups Write-Information 'Checking Data for GUIDs to map to users, groups, devices, or service principals' - Add-CIPPGuidMappings -DataObject $Data -Users $Users -Groups $Groups -Devices $Devices -ServicePrincipals $ServicePrincipals -PartnerUsers $PartnerUsers -PropertyPrefix 'CIPP' + Add-CIPPGuidMappings -DataObject $Data -UserLookup $UserLookup -GroupLookup $GroupLookup -DeviceLookup $DeviceLookup -ServicePrincipalLookup $ServicePrincipalLookup -PartnerUserLookup $PartnerUserLookup -PropertyPrefix 'CIPP' # Also check root properties for GUIDs and partner UPNs Write-Information 'Checking RootProperties for GUIDs to map to users, groups, devices, or service principals' - Add-CIPPGuidMappings -DataObject $RootProperties -Users $Users -Groups $Groups -Devices $Devices -ServicePrincipals $ServicePrincipals -PartnerUsers $PartnerUsers + Add-CIPPGuidMappings -DataObject $RootProperties -UserLookup $UserLookup -GroupLookup $GroupLookup -DeviceLookup $DeviceLookup -ServicePrincipalLookup $ServicePrincipalLookup -PartnerUserLookup $PartnerUserLookup if ($Data.ExtendedProperties) { $Data.CIPPExtendedProperties = ($Data.ExtendedProperties | ConvertTo-Json -Compress) @@ -432,6 +567,7 @@ function Test-CIPPAuditLogRules { try { $ClauseStartTime = Get-Date Write-Warning "Webhook: Processing clause: $($clause.clause)" + Write-Information "Webhook: Available operations in data: $(($ProcessedData.Operation | Select-Object -Unique) -join ', ')" $ReturnedData = $ProcessedData | Where-Object { Invoke-Expression $clause.clause } if ($ReturnedData) { Write-Warning "Webhook: There is matching data: $(($ReturnedData.operation | Select-Object -Unique) -join ', ')" From 43db5a9adc7560437a475e725c6b791a88d4f329 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Sat, 31 Jan 2026 01:16:58 +0100 Subject: [PATCH 330/503] fix: GET to POST for domain analyser --- .../CIPP/Settings/Invoke-ExecDnsConfig.ps1 | 34 +++++++++++-------- .../Public/Get-CIPPDomainAnalyser.ps1 | 13 ++++--- 2 files changed, 29 insertions(+), 18 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecDnsConfig.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecDnsConfig.ps1 index 6ad0becf4381..04440b5926f7 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecDnsConfig.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecDnsConfig.ps1 @@ -9,6 +9,7 @@ function Invoke-ExecDnsConfig { param($Request, $TriggerMetadata) $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers # List of supported resolvers $ValidResolvers = @( 'Google' @@ -16,8 +17,11 @@ function Invoke-ExecDnsConfig { ) - $StatusCode = [HttpStatusCode]::OK + $Action = $Request.Query.Action ?? $Request.Body.Action + $Domain = $Request.Query.Domain ?? $Request.Body.Domain + $Resolver = $Request.Query.Resolver ?? $Request.Body.Resolver + $Selector = $Request.Query.Selector ?? $Request.Body.Selector try { $ConfigTable = Get-CippTable -tablename Config $Filter = "PartitionKey eq 'Domains' and RowKey eq 'Domains'" @@ -36,10 +40,9 @@ function Invoke-ExecDnsConfig { $updated = $false - switch ($Request.Query.Action) { + switch ($Action) { 'SetConfig' { - if ($Request.Body.Resolver) { - $Resolver = $Request.Body.Resolver + if ($Resolver) { if ($ValidResolvers -contains $Resolver) { try { $Config.Resolver = $Resolver @@ -53,7 +56,7 @@ function Invoke-ExecDnsConfig { } if ($updated) { Add-CIPPAzDataTableEntity @ConfigTable -Entity $Config -Force - Write-LogMessage -API $APINAME -tenant 'Global' -headers $Request.Headers -message 'DNS configuration updated' -Sev 'Info' + Write-LogMessage -API $APIName -tenant 'Global' -headers $Headers -message 'DNS configuration updated' -Sev 'Info' $body = [pscustomobject]@{'Results' = 'Success: DNS configuration updated.' } } else { $StatusCode = [HttpStatusCode]::BadRequest @@ -61,8 +64,8 @@ function Invoke-ExecDnsConfig { } } 'SetDkimConfig' { - $Domain = $Request.Query.Domain - $Selector = ($Request.Query.Selector).trim() -split '\s*,\s*' + $Domain = $Domain + $Selector = ($Selector).trim() -split '\s*,\s*' $DomainTable = Get-CIPPTable -Table 'Domains' $Filter = "RowKey eq '{0}'" -f $Domain $DomainInfo = Get-CIPPAzDataTableEntity @DomainTable -Filter $Filter @@ -71,7 +74,7 @@ function Invoke-ExecDnsConfig { $DomainInfo.DkimSelectors = $DkimSelectors } else { $DomainInfo = @{ - 'RowKey' = $Request.Query.Domain + 'RowKey' = $Domain 'PartitionKey' = 'ManualEntry' 'TenantId' = 'NoTenant' 'MailProviders' = '' @@ -81,22 +84,25 @@ function Invoke-ExecDnsConfig { } } Add-CIPPAzDataTableEntity @DomainTable -Entity $DomainInfo -Force + Write-LogMessage -API $APIName -tenant 'Global' -headers $Headers -message "Updated DKIM selectors for domain: $Domain - Selectors: $($Selector -join ', ')" -Sev 'Info' + $body = [pscustomobject]@{ 'Results' = "Success: DKIM selectors updated for $Domain. Selectors: $($Selector -join ', ')" } } 'GetConfig' { $body = [pscustomobject]$Config - Write-LogMessage -API $APINAME -tenant 'Global' -headers $Request.Headers -message 'Retrieved DNS configuration' -Sev 'Debug' + Write-LogMessage -API $APIName -tenant 'Global' -headers $Headers -message 'Retrieved DNS configuration' -Sev 'Debug' } 'RemoveDomain' { - $Filter = "RowKey eq '{0}'" -f $Request.Query.Domain + $Filter = "RowKey eq '{0}'" -f $Domain $DomainRow = Get-CIPPAzDataTableEntity @DomainTable -Filter $Filter -Property PartitionKey, RowKey Remove-AzDataTableEntity -Force @DomainTable -Entity $DomainRow - Write-LogMessage -API $APINAME -tenant 'Global' -headers $Request.Headers -message "Removed Domain - $($Request.Query.Domain) " -Sev 'Info' - $body = [pscustomobject]@{ 'Results' = "Domain removed - $($Request.Query.Domain)" } + Write-LogMessage -API $APIName -tenant 'Global' -headers $Headers -message "Removed Domain - $Domain " -Sev 'Info' + $body = [pscustomobject]@{ 'Results' = "Domain removed - $Domain" } } } } catch { - Write-LogMessage -API $APINAME -tenant $($name) -headers $Request.Headers -message "DNS Config API failed. $($_.Exception.Message)" -Sev 'Error' - $body = [pscustomobject]@{'Results' = "Failed. $($_.Exception.Message)" } + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API $APIName -tenant $($name) -headers $Headers -message "DNS Config API failed. $($ErrorMessage.NormalizedError)" -Sev 'Error' -LogData $ErrorMessage + $body = [pscustomobject]@{'Results' = "Failed. $($ErrorMessage.NormalizedError)" } $StatusCode = [HttpStatusCode]::BadRequest } diff --git a/Modules/CIPPCore/Public/Get-CIPPDomainAnalyser.ps1 b/Modules/CIPPCore/Public/Get-CIPPDomainAnalyser.ps1 index d201d8a5a41f..e1aed77dbd55 100644 --- a/Modules/CIPPCore/Public/Get-CIPPDomainAnalyser.ps1 +++ b/Modules/CIPPCore/Public/Get-CIPPDomainAnalyser.ps1 @@ -26,11 +26,16 @@ function Get-CIPPDomainAnalyser { } $Domains = Get-CIPPAzDataTableEntity @DomainTable | Where-Object { $_.TenantGUID -in $Tenants.customerId -or $TenantFilter -eq $_.TenantGUID } try { - # Extract json from table results - $Results = foreach ($DomainAnalyserResult in ($Domains).DomainAnalyser) { + # Extract json from table results and merge with DkimSelectors from the domain entity + $Results = foreach ($Domain in $Domains) { try { - if (![string]::IsNullOrEmpty($DomainAnalyserResult)) { - $Object = $DomainAnalyserResult | ConvertFrom-Json -ErrorAction SilentlyContinue + if (![string]::IsNullOrEmpty($Domain.DomainAnalyser)) { + $Object = $Domain.DomainAnalyser | ConvertFrom-Json -ErrorAction SilentlyContinue + # Add DkimSelectors from the domain entity if available + if (![string]::IsNullOrEmpty($Domain.DkimSelectors)) { + $Selectors = $Domain.DkimSelectors | ConvertFrom-Json -ErrorAction SilentlyContinue + $Object | Add-Member -NotePropertyName 'DkimSelectors' -NotePropertyValue ($Selectors) -Force + } $Object } } catch {} From bdd7592e580a73439f660d8dba9fe020793ae240 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 30 Jan 2026 23:51:22 -0500 Subject: [PATCH 331/503] Optimize user lookup and limit report output Replace repeated Where-Object scans with an accountEnabled user hashtable for O(1) lookups and iterate registration details directly to improve performance. Switch .Length to .Count where appropriate. Add a display limit (500) and truncate/summarize long user lists, showing phishable users first and then phish-resistant users up to the limit, with messages indicating omitted users. Add comments and minor formatting improvements to the generated markdown report. --- .../Identity/Invoke-CippTestZTNA21801.ps1 | 58 ++++++++++++++----- 1 file changed, 42 insertions(+), 16 deletions(-) diff --git a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21801.ps1 b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21801.ps1 index 1b3783d09304..d169ac04dc97 100644 --- a/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21801.ps1 +++ b/Modules/CIPPCore/Public/Tests/ZTNA/Identity/Invoke-CippTestZTNA21801.ps1 @@ -16,13 +16,21 @@ function Invoke-CippTestZTNA21801 { $PhishResistantMethods = @('passKeyDeviceBound', 'passKeyDeviceBoundAuthenticator', 'windowsHelloForBusiness') - $results = $UserRegistrationDetails | Where-Object { - $userId = $_.id - $matchingUser = $Users | Where-Object { $_.id -eq $userId -and $_.accountEnabled } - $matchingUser - } | ForEach-Object { - $regDetail = $_ - $matchingUser = $Users | Where-Object { $_.id -eq $regDetail.id } + # Create hashtable for O(1) user lookup instead of O(n) Where-Object searches + $UserLookup = @{} + foreach ($user in $Users) { + if ($user.accountEnabled) { + $UserLookup[$user.id] = $user + } + } + + $results = foreach ($regDetail in $UserRegistrationDetails) { + # Fast O(1) lookup instead of Where-Object + if (-not $UserLookup.ContainsKey($regDetail.id)) { + continue + } + + $matchingUser = $UserLookup[$regDetail.id] $hasPhishResistant = $false if ($regDetail.methodsRegistered) { @@ -42,11 +50,11 @@ function Invoke-CippTestZTNA21801 { } } - $totalUserCount = $results.Length + $totalUserCount = $results.Count $phishResistantUsers = $results | Where-Object { $_.phishResistantAuthMethod } $phishableUsers = $results | Where-Object { !$_.phishResistantAuthMethod } - $phishResistantUserCount = $phishResistantUsers.Length + $phishResistantUserCount = $phishResistantUsers.Count $passed = $totalUserCount -eq $phishResistantUserCount @@ -64,24 +72,42 @@ function Invoke-CippTestZTNA21801 { $mdInfo = "Found users that have not registered phishing resistant authentication methods.`n`n" } + # Limit output to prevent performance issues with large user sets + $maxUsersToDisplay = 500 + $mdInfo = $mdInfo + "| User | Last sign in | Phishing resistant method registered |`n" $mdInfo = $mdInfo + "| :--- | :--- | :---: |`n" $userLinkFormat = 'https://entra.microsoft.com/#view/Microsoft_AAD_UsersAndTenants/UserProfileMenuBlade/~/UserAuthMethods/userId/{0}/hidePreviewBanner~/true' - $mdLines = @($phishableUsers | Sort-Object displayName | ForEach-Object { + # Show phishable users first (up to limit) + $phishableUsersToShow = $phishableUsers | Sort-Object displayName | Select-Object -First $maxUsersToDisplay + $mdLines = @($phishableUsersToShow | ForEach-Object { $userLink = $userLinkFormat -f $_.id $lastSignInDate = if ($_.lastSuccessfulSignInDateTime) { (Get-Date $_.lastSuccessfulSignInDateTime -Format 'yyyy-MM-dd') } else { 'Never' } "|[$($_.displayName)]($userLink)| $lastSignInDate | ❌ |`n" }) $mdInfo = $mdInfo + ($mdLines -join '') - $mdLines = @($phishResistantUsers | Sort-Object displayName | ForEach-Object { - $userLink = $userLinkFormat -f $_.id - $lastSignInDate = if ($_.lastSuccessfulSignInDateTime) { (Get-Date $_.lastSuccessfulSignInDateTime -Format 'yyyy-MM-dd') } else { 'Never' } - "|[$($_.displayName)]($userLink)| $lastSignInDate | ✅ |`n" - }) - $mdInfo = $mdInfo + ($mdLines -join '') + if ($phishableUsers.Count -gt $maxUsersToDisplay) { + $mdInfo = $mdInfo + "|... and $($phishableUsers.Count - $maxUsersToDisplay) more users without phish-resistant methods|||`n" + } + + # Show phish-resistant users (up to remaining limit) + $remainingSlots = $maxUsersToDisplay - [Math]::Min($phishableUsers.Count, $maxUsersToDisplay) + if ($remainingSlots -gt 0) { + $phishResistantUsersToShow = $phishResistantUsers | Sort-Object displayName | Select-Object -First $remainingSlots + $mdLines = @($phishResistantUsersToShow | ForEach-Object { + $userLink = $userLinkFormat -f $_.id + $lastSignInDate = if ($_.lastSuccessfulSignInDateTime) { (Get-Date $_.lastSuccessfulSignInDateTime -Format 'yyyy-MM-dd') } else { 'Never' } + "|[$($_.displayName)]($userLink)| $lastSignInDate | ✅ |`n" + }) + $mdInfo = $mdInfo + ($mdLines -join '') + + if ($phishResistantUsers.Count -gt $remainingSlots) { + $mdInfo = $mdInfo + "|... and $($phishResistantUsers.Count - $remainingSlots) more users with phish-resistant methods|||`n" + } + } $testResultMarkdown = $testResultMarkdown -replace '%TestResult%', $mdInfo From 5b4aa9f9657ce48b7ab79aeba3384cc0289028c8 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 30 Jan 2026 23:59:36 -0500 Subject: [PATCH 332/503] Allow null for InputObject parameter Add the [AllowNull()] attribute to the InputObject parameter in Add-CIPPDbItem.ps1 so the function accepts $null values (from pipeline or explicit) in addition to existing [AllowEmptyCollection()]. This improves robustness when callers pass null input. --- Modules/CIPPCore/Public/Add-CIPPDbItem.ps1 | 1 + 1 file changed, 1 insertion(+) diff --git a/Modules/CIPPCore/Public/Add-CIPPDbItem.ps1 b/Modules/CIPPCore/Public/Add-CIPPDbItem.ps1 index 7b96b16bc4cb..be862eabe375 100644 --- a/Modules/CIPPCore/Public/Add-CIPPDbItem.ps1 +++ b/Modules/CIPPCore/Public/Add-CIPPDbItem.ps1 @@ -41,6 +41,7 @@ function Add-CIPPDbItem { [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [Alias('Data')] + [AllowNull()] [AllowEmptyCollection()] $InputObject, From 75451ee7cebba9d7562f6311e9042d510b4693e7 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Sat, 31 Jan 2026 00:11:43 -0500 Subject: [PATCH 333/503] Fix mailbox cache --- Modules/CIPPCore/Public/Set-CIPPDBCacheMailboxes.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheMailboxes.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheMailboxes.ps1 index 27fb33c5cbc6..3716fa9a5c06 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheMailboxes.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheMailboxes.ps1 @@ -24,11 +24,11 @@ function Set-CIPPDBCacheMailboxes { Select = $Select } # Use Generic List for better memory efficiency with large datasets - $MailboxList = [System.Collections.Generic.List[PSObject]]::new() + $Mailboxes = [System.Collections.Generic.List[PSObject]]::new() $RawMailboxes = New-ExoRequest @ExoRequest foreach ($Mailbox in $RawMailboxes) { - $MailboxList.Add(($Mailbox | Select-Object id, ExchangeGuid, ArchiveGuid, WhenSoftDeleted, + $Mailboxes.Add(($Mailbox | Select-Object id, ExchangeGuid, ArchiveGuid, WhenSoftDeleted, @{ Name = 'UPN'; Expression = { $_.'UserPrincipalName' } }, @{ Name = 'displayName'; Expression = { $_.'DisplayName' } }, @{ Name = 'primarySmtpAddress'; Expression = { $_.'PrimarySMTPAddress' } }, From f50cda4249cda257a4e712709ce03cc710409256 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Sat, 31 Jan 2026 20:43:36 -0500 Subject: [PATCH 334/503] Use tenant-based batching Remove SchedulerRateLimits.json and all rate-limit lookup logic from Start-UserTasksOrchestrator.ps1. Change batching strategy from command-based rate-limited groups to tenant-based groups so tasks are batched and queued per tenant. Performance and correctness improvements: cache Get-Command result to avoid repeated reflection calls, precompute whether a command supports TenantFilter, and clone TaskInfo objects to prevent shared reference mutation. Queue entries and orchestrator names are now tenant-scoped. --- Config/SchedulerRateLimits.json | 10 -- .../Start-UserTasksOrchestrator.ps1 | 96 ++++++------------- 2 files changed, 30 insertions(+), 76 deletions(-) delete mode 100644 Config/SchedulerRateLimits.json diff --git a/Config/SchedulerRateLimits.json b/Config/SchedulerRateLimits.json deleted file mode 100644 index 3d2c65716af0..000000000000 --- a/Config/SchedulerRateLimits.json +++ /dev/null @@ -1,10 +0,0 @@ -[ - { - "Command": "Sync-CIPPExtensionData", - "MaxRequests": 50 - }, - { - "Command": "Push-CIPPExtensionData", - "MaxRequests": 30 - } -] \ No newline at end of file diff --git a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-UserTasksOrchestrator.ps1 b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-UserTasksOrchestrator.ps1 index 272c78ee2627..b677afdd2071 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-UserTasksOrchestrator.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-UserTasksOrchestrator.ps1 @@ -17,33 +17,6 @@ function Start-UserTasksOrchestrator { $Filter = "PartitionKey eq 'ScheduledTask' and (TaskState eq 'Planned' or TaskState eq 'Failed - Planned' or (TaskState eq 'Pending' and Timestamp lt datetime'$30MinutesAgo') or (TaskState eq 'Running' and Timestamp lt datetime'$4HoursAgo'))" $tasks = Get-CIPPAzDataTableEntity @Table -Filter $Filter - $RateLimitTable = Get-CIPPTable -tablename 'SchedulerRateLimits' - $RateLimits = Get-CIPPAzDataTableEntity @RateLimitTable -Filter "PartitionKey eq 'SchedulerRateLimits'" - - $CIPPCoreModuleRoot = Get-Module -Name CIPPCore | Select-Object -ExpandProperty ModuleBase - $CIPPRoot = (Get-Item $CIPPCoreModuleRoot).Parent.Parent - $DefaultRateLimits = Get-Content -Path "$CIPPRoot/Config/SchedulerRateLimits.json" | ConvertFrom-Json - $NewRateLimits = foreach ($Limit in $DefaultRateLimits) { - if ($Limit.Command -notin $RateLimits.RowKey) { - @{ - PartitionKey = 'SchedulerRateLimits' - RowKey = $Limit.Command - MaxRequests = $Limit.MaxRequests - } - } - } - - if ($NewRateLimits) { - $null = Add-CIPPAzDataTableEntity @RateLimitTable -Entity $NewRateLimits -Force - $RateLimits = Get-CIPPAzDataTableEntity @RateLimitTable -Filter "PartitionKey eq 'SchedulerRateLimits'" - } - - # Create a hashtable for quick rate limit lookups - $RateLimitLookup = @{} - foreach ($limit in $RateLimits) { - $RateLimitLookup[$limit.RowKey] = $limit.MaxRequests - } - $Batch = [System.Collections.Generic.List[object]]::new() $TenantList = Get-Tenants -IncludeErrors foreach ($task in $tasks) { @@ -62,9 +35,12 @@ function Start-UserTasksOrchestrator { TaskState = 'Pending' } $task.Parameters = $task.Parameters | ConvertFrom-Json -AsHashtable - $task.AdditionalProperties = $task.AdditionalProperties | ConvertFrom-Json - if (!$task.Parameters) { $task.Parameters = @{} } + + # Cache Get-Command result to avoid repeated expensive reflection calls + $CommandInfo = Get-Command $task.Command + $HasTenantFilter = $CommandInfo.Parameters.ContainsKey('TenantFilter') + $ScheduledCommand = [pscustomobject]@{ Command = $task.Command Parameters = $task.Parameters @@ -77,13 +53,15 @@ function Start-UserTasksOrchestrator { Write-Host "Excluded Tenants from this task: $ExcludedTenants" $AllTenantCommands = foreach ($Tenant in $TenantList | Where-Object { $_.defaultDomainName -notin $ExcludedTenants }) { $NewParams = $task.Parameters.Clone() - if ((Get-Command $task.Command).Parameters.TenantFilter) { + if ($HasTenantFilter) { $NewParams.TenantFilter = $Tenant.defaultDomainName } + # Clone TaskInfo to prevent shared object references + $TaskInfoClone = $task.PSObject.Copy() [pscustomobject]@{ Command = $task.Command Parameters = $NewParams - TaskInfo = $task + TaskInfo = $TaskInfoClone FunctionName = 'ExecScheduledCommand' } } @@ -109,13 +87,15 @@ function Start-UserTasksOrchestrator { $GroupTenantCommands = foreach ($ExpandedTenant in $ExpandedTenants | Where-Object { $_.value -notin $ExcludedTenants }) { $NewParams = $task.Parameters.Clone() - if ((Get-Command $task.Command).Parameters.TenantFilter) { + if ($HasTenantFilter) { $NewParams.TenantFilter = $ExpandedTenant.value } + # Clone TaskInfo to prevent shared object references + $TaskInfoClone = $task.PSObject.Copy() [pscustomobject]@{ Command = $task.Command Parameters = $NewParams - TaskInfo = $task + TaskInfo = $TaskInfoClone FunctionName = 'ExecScheduledCommand' } } @@ -125,14 +105,14 @@ function Start-UserTasksOrchestrator { Write-LogMessage -API 'Scheduler_UserTasks' -tenant $tenant -message "Failed to expand tenant group for task $($task.Name): $($_.Exception.Message)" -sev Error # Fall back to treating as single tenant - if ((Get-Command $task.Command).Parameters.TenantFilter) { + if ($HasTenantFilter) { $ScheduledCommand.Parameters['TenantFilter'] = $task.Tenant } $Batch.Add($ScheduledCommand) } } else { # Handle single tenant - if ((Get-Command $task.Command).Parameters.TenantFilter) { + if ($HasTenantFilter) { $ScheduledCommand.Parameters['TenantFilter'] = $task.Tenant } $Batch.Add($ScheduledCommand) @@ -155,51 +135,35 @@ function Start-UserTasksOrchestrator { Write-Information 'Batching tasks for execution...' Write-Information "Total tasks to process: $($Batch.Count)" - if (($Batch | Measure-Object).Count -gt 0) { - # Group commands by type and apply rate limits - $CommandGroups = $Batch | Group-Object -Property Command + if ($Batch.Count -gt 0) { + # Group tasks by tenant instead of command type + $TenantGroups = $Batch | Group-Object -Property { $_.Parameters.TenantFilter } $ProcessedBatches = [System.Collections.Generic.List[object]]::new() - foreach ($CommandGroup in $CommandGroups) { - $CommandName = $CommandGroup.Name - $Commands = [System.Collections.Generic.List[object]]::new($CommandGroup.Group) - - # Get rate limit for this command (default to 100 if not found) - $MaxItemsPerBatch = if ($RateLimitLookup.ContainsKey($CommandName)) { - $RateLimitLookup[$CommandName] - } else { - 100 - } - - # Split into batches based on rate limit - while ($Commands.Count -gt 0) { - $BatchSize = [Math]::Min($Commands.Count, $MaxItemsPerBatch) - $CommandBatch = [System.Collections.Generic.List[object]]::new() - - for ($i = 0; $i -lt $BatchSize; $i++) { - $CommandBatch.Add($Commands[0]) - $Commands.RemoveAt(0) - } + foreach ($TenantGroup in $TenantGroups) { + $TenantName = $TenantGroup.Name + $TenantCommands = [System.Collections.Generic.List[object]]::new($TenantGroup.Group) - $ProcessedBatches.Add($CommandBatch) - } + Write-Information "Creating batch for tenant: $TenantName with $($TenantCommands.Count) tasks" + $ProcessedBatches.Add($TenantCommands) } - # Process each batch separately + # Process each tenant batch separately foreach ($ProcessedBatch in $ProcessedBatches) { - Write-Information "Processing batch with $($ProcessedBatch.Count) tasks..." + $TenantName = $ProcessedBatch[0].Parameters.TenantFilter + Write-Information "Processing batch for tenant: $TenantName with $($ProcessedBatch.Count) tasks..." Write-Information 'Tasks by command:' $ProcessedBatch | Group-Object -Property Command | ForEach-Object { Write-Information " - $($_.Name): $($_.Count)" } - # Create queue entry for each batch - $Queue = New-CippQueueEntry -Name "Scheduled Tasks - Batch #$($ProcessedBatches.IndexOf($ProcessedBatch) + 1) of $($ProcessedBatches.Count)" + # Create queue entry for each tenant batch + $Queue = New-CippQueueEntry -Name "Scheduled Tasks - $TenantName" $QueueId = $Queue.RowKey - $BatchWithQueue = $ProcessedBatch | Select-Object *, @{Name = 'QueueId'; Expression = { $QueueId } }, @{Name = 'QueueName'; Expression = { '{0} - {1}' -f $_.TaskInfo.Name, ($_.TaskInfo.Tenant -ne 'AllTenants' ? $_.TaskInfo.Tenant : $_.Parameters.TenantFilter) } } + $BatchWithQueue = $ProcessedBatch | Select-Object *, @{Name = 'QueueId'; Expression = { $QueueId } }, @{Name = 'QueueName'; Expression = { '{0} - {1}' -f $_.TaskInfo.Name, $TenantName } } $InputObject = [PSCustomObject]@{ - OrchestratorName = 'UserTaskOrchestrator' + OrchestratorName = "UserTaskOrchestrator_$TenantName" Batch = @($BatchWithQueue) SkipLog = $true } From 8429f252954c878a44bf770d7796b85eee658667 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Sun, 1 Feb 2026 16:01:32 +0100 Subject: [PATCH 335/503] Add timings --- .../Public/Alerts/Get-CIPPAlertQuarantineReleaseRequests.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertQuarantineReleaseRequests.ps1 b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertQuarantineReleaseRequests.ps1 index 56182ece6f32..3286eff0b41a 100644 --- a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertQuarantineReleaseRequests.ps1 +++ b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertQuarantineReleaseRequests.ps1 @@ -24,7 +24,7 @@ } try { - $RequestedReleases = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-QuarantineMessage' -cmdParams @{ PageSize = 1000; ReleaseStatus = 'Requested' } -ErrorAction Stop | Select-Object -ExcludeProperty *data.type* + $RequestedReleases = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-QuarantineMessage' -cmdParams @{ PageSize = 1000; ReleaseStatus = 'Requested'; StartReceivedDate = (Get-Date).AddHours(-6) } -ErrorAction Stop | Select-Object -ExcludeProperty *data.type* if ($RequestedReleases) { # Get the CIPP URL for the Quarantine link From 87edcf1676555dec3ac37c82ebb5501a110dc46a Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Sun, 1 Feb 2026 16:04:16 +0100 Subject: [PATCH 336/503] rerun for exchangemonitors --- .../Alerts/Get-CIPPAlertQuarantineReleaseRequests.ps1 | 7 ++++++- Modules/CIPPCore/Public/Test-CIPPRerun.ps1 | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertQuarantineReleaseRequests.ps1 b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertQuarantineReleaseRequests.ps1 index 3286eff0b41a..f0ca6d528e40 100644 --- a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertQuarantineReleaseRequests.ps1 +++ b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertQuarantineReleaseRequests.ps1 @@ -11,6 +11,11 @@ $TenantFilter ) + #Add rerun protection: This Monitor can only run once every hour. + $Rerun = Test-CIPPRerun -TenantFilter $TenantFilter -Type 'ExchangeMonitor' -API 'Get-CIPPAlertQuarantineReleaseRequests' + if ($Rerun) { + return $true + } $HasLicense = Test-CIPPStandardLicense -StandardName 'QuarantineReleaseRequests' -TenantFilter $TenantFilter -RequiredCapabilities @( 'EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', @@ -20,7 +25,7 @@ ) if (-not $HasLicense) { - return + return $true } try { diff --git a/Modules/CIPPCore/Public/Test-CIPPRerun.ps1 b/Modules/CIPPCore/Public/Test-CIPPRerun.ps1 index ac19ef9039e2..198cf87143f9 100644 --- a/Modules/CIPPCore/Public/Test-CIPPRerun.ps1 +++ b/Modules/CIPPCore/Public/Test-CIPPRerun.ps1 @@ -21,6 +21,7 @@ function Test-CIPPRerun { 'Standard' { 9800 } # 2 hours 45 minutes ish. 'BPA' { 85000 } # 24 hours ish. 'CippTests' { 85000 } # 24 hours ish. + 'Get-CIPPAlertQuarantineReleaseRequests' { 3500 } #about an hour default { throw "Unknown type: $Type" } } } From 3ae02bb45d69077a7c777be74a99070e0b2a6ea8 Mon Sep 17 00:00:00 2001 From: Phillip Schjeldal Hansen Date: Sun, 1 Feb 2026 22:14:12 +0100 Subject: [PATCH 337/503] Fixed reporting for standard RestrictThirdPartyStorageServices --- .../Invoke-CIPPStandardRestrictThirdPartyStorageServices.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardRestrictThirdPartyStorageServices.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardRestrictThirdPartyStorageServices.ps1 index 517045ae0169..7d6811020b46 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardRestrictThirdPartyStorageServices.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardRestrictThirdPartyStorageServices.ps1 @@ -90,7 +90,7 @@ function Invoke-CIPPStandardRestrictThirdPartyStorageServices { thirdPartyStorageRestricted = $CurrentState.accountEnabled -eq $false } $ExpectedValue = @{ - thirdPartyStorageRestricted = $false + thirdPartyStorageRestricted = $true } Set-CIPPStandardsCompareField -FieldName 'standards.RestrictThirdPartyStorageServices' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant From b70e851454bc8552236842d9fc3c882b49988cee Mon Sep 17 00:00:00 2001 From: Phillip Schjeldal Hansen Date: Mon, 2 Feb 2026 01:24:30 +0100 Subject: [PATCH 338/503] Added Report to DisableSelfServiceLicenses and refactored remidate to minimize requests --- ...CIPPStandardDisableSelfServiceLicenses.ps1 | 71 ++++++++++--------- 1 file changed, 38 insertions(+), 33 deletions(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSelfServiceLicenses.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSelfServiceLicenses.ps1 index f06f7e0091f0..7a242e22ae79 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSelfServiceLicenses.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSelfServiceLicenses.ps1 @@ -45,50 +45,52 @@ function Invoke-CIPPStandardDisableSelfServiceLicenses { throw $Message } - if ($settings.remediate) { - if ($settings.exclusions -like '*;*') { - $exclusions = $settings.Exclusions -split (';') - } else { - $exclusions = $settings.Exclusions -split (',') + + if ($settings.exclusions -like '*;*') { + $exclusions = $settings.Exclusions -split (';') + } else { + $exclusions = $settings.Exclusions -split (',') + } + + $ExpectedValues = [System.Collections.Generic.List[PSCustomObject]]::new() + foreach ($Item in $selfServiceItems) { + if ($Item.productId -in $exclusions) { + $Item.policyValue = "Enabled" + $ExpectedValues.add(($Item | Select-Object -Property productName, productId, policyValue)) + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Exclusion present for self-service license '$($Item.productName) - $($Item.productId)'" } + else { + $Item.policyValue = "Disabled" + $ExpectedValues.add(($Item | Select-Object -Property productName, productId, policyValue)) + } + } - foreach ($Item in $selfServiceItems) { - $body = $null + $CurrentValues = $selfServiceItems | Select-Object -Property productName, productId, policyValue - if ($Item.policyValue -eq 'Enabled' -AND ($Item.productId -in $exclusions)) { - # Self service is enabled on product and productId is in exclusions, skip - } - if ($Item.policyValue -eq 'Disabled' -AND ($Item.productId -in $exclusions)) { - # Self service is disabled on product and productId is in exclusions, enable - $body = '{ "policyValue": "Enabled" }' - } - if ($Item.policyValue -eq 'Enabled' -AND ($Item.productId -notin $exclusions)) { - # Self service is enabled on product and productId is NOT in exclusions, disable - $body = '{ "policyValue": "Disabled" }' - } - if ($Item.policyValue -eq 'Disabled' -AND ($Item.productId -notin $exclusions)) { - # Self service is disabled on product and productId is NOT in exclusions, skip - } + if ($settings.remediate) { + $Compare = Compare-Object -ReferenceObject $ExpectedValues -DifferenceObject $CurrentValues -Property productName, productId, policyValue - try { - if ($body) { + if (!$Compare) { + Write-LogMessage -API 'Standards' -tenant $Tenant -message 'self service licenses are already set correctly.' -sev Info + } + else { + $NeedsUpdate = $Compare | Where-Object {$_.SideIndicator -eq "<="} + foreach ($Item in $NeedsUpdate) { + try { + $body = @{policyValue=$Item.policyValue} | ConvertTo-Json -Compress New-GraphPOSTRequest -scope 'aeb86249-8ea3-49e2-900b-54cc8e308f85/.default' -uri "https://licensing.m365.microsoft.com/v1.0/policies/AllowSelfServicePurchase/products/$($Item.productId)" -tenantid $Tenant -body $body -type PUT + Write-LogMessage -API 'Standards' -tenant $tenant -message "Changed Self Service status for product '$($Item.productName) - $($Item.productId)' to '$($Item.policyValue)'" + } catch { + Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to set product status for '$($Item.productName) - $($Item.productId)' with body $($body) for reason: $($_.Exception.Message)" -sev Error } - } catch { - Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to set product status for $($Item.productId) with body $($body) for reason: $($_.Exception.Message)" -sev Error - #Write-Error "Failed to disable product $($Item.productName):$($_.Exception.Message)" } } - if (!$exclusions) { - Write-LogMessage -API 'Standards' -tenant $Tenant -message 'No exclusions set for self-service licenses, disabled all not excluded licenses for self-service.' -sev Info - } else { - Write-LogMessage -API 'Standards' -tenant $Tenant -message 'Exclusions present for self-service licenses, disabled all not excluded licenses for self-service.' -sev Info - } + $CurrentValues = (New-GraphGETRequest -scope 'aeb86249-8ea3-49e2-900b-54cc8e308f85/.default' -uri 'https://licensing.m365.microsoft.com/v1.0/policies/AllowSelfServicePurchase/products' -tenantid $Tenant).items | Select-Object -Property productName, productId, policyValue } if ($Settings.alert) { - $selfServiceItemsToAlert = $selfServiceItems | Where-Object { $_.policyValue -eq 'Enabled' } + $selfServiceItemsToAlert = $CurrentValues | Where-Object { $_.policyValue -eq 'Enabled' } if (!$selfServiceItemsToAlert) { Write-LogMessage -API 'Standards' -tenant $tenant -message 'All self-service licenses are disabled' -sev Info } else { @@ -98,6 +100,9 @@ function Invoke-CIPPStandardDisableSelfServiceLicenses { } if ($Settings.report -eq $true) { - #Add-CIPPBPAField -FieldName '????' -FieldValue "????" -StoreAs bool -Tenant $tenant + $StateIsCorrect = !(Compare-Object -ReferenceObject $ExpectedValues -DifferenceObject $CurrentValues -Property productName, productId, policyValue) + + Set-CIPPStandardsCompareField -FieldName 'standards.DisableSelfServiceLicenses' -CurrentValue $CurrentValues -ExpectedValue $ExpectedValues -TenantFilter $Tenant + Add-CIPPBPAField -FieldName 'DisableSelfServiceLicenses' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $Tenant } } From 546eb1451a6231d4dcb97638adeb17f8a40f8eff Mon Sep 17 00:00:00 2001 From: Phillip Schjeldal Hansen Date: Mon, 2 Feb 2026 12:07:41 +0100 Subject: [PATCH 339/503] StandardDisableSelfServiceLicenses Changed presentation of report values --- ...CIPPStandardDisableSelfServiceLicenses.ps1 | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSelfServiceLicenses.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSelfServiceLicenses.ps1 index 7a242e22ae79..03845d7a4d03 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSelfServiceLicenses.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSelfServiceLicenses.ps1 @@ -102,7 +102,25 @@ function Invoke-CIPPStandardDisableSelfServiceLicenses { if ($Settings.report -eq $true) { $StateIsCorrect = !(Compare-Object -ReferenceObject $ExpectedValues -DifferenceObject $CurrentValues -Property productName, productId, policyValue) - Set-CIPPStandardsCompareField -FieldName 'standards.DisableSelfServiceLicenses' -CurrentValue $CurrentValues -ExpectedValue $ExpectedValues -TenantFilter $Tenant + $ExpectedValuesHash = @{} + foreach ($Item in $ExpectedValues) { + $ExpectedValuesHash[$Item.productName] = [PSCustomObject]@{ + Id = $Item.productId + Value = $Item.policyValue + } + } + $ExpectedValue = [PSCustomObject]$ExpectedValuesHash + + $CurrentValuesHash = @{} + foreach ($Item in $CurrentValues) { + $CurrentValuesHash[$Item.productName] = [PSCustomObject]@{ + Id = $Item.productId + Value = $Item.policyValue + } + } + $CurrentValue = [PSCustomObject]$CurrentValuesHash + + Set-CIPPStandardsCompareField -FieldName 'standards.DisableSelfServiceLicenses' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant Add-CIPPBPAField -FieldName 'DisableSelfServiceLicenses' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $Tenant } } From 420ba3feefb0ae2e8f369faee86b1bbbd90408f6 Mon Sep 17 00:00:00 2001 From: rvdwegen Date: Wed, 4 Feb 2026 14:38:05 +0100 Subject: [PATCH 340/503] Temp location fix for email template --- Modules/CIPPCore/Public/New-CIPPAlertTemplate.ps1 | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/New-CIPPAlertTemplate.ps1 b/Modules/CIPPCore/Public/New-CIPPAlertTemplate.ps1 index 67340eaf3ab5..886a8f9201a8 100644 --- a/Modules/CIPPCore/Public/New-CIPPAlertTemplate.ps1 +++ b/Modules/CIPPCore/Public/New-CIPPAlertTemplate.ps1 @@ -13,7 +13,11 @@ function New-CIPPAlertTemplate { $AlertComment ) $Appname = '[{"Application Name":"ACOM Azure Website","Application IDs":"23523755-3a2b-41ca-9315-f81f3f566a95"},{"Application Name":"AEM-DualAuth","Application IDs":"69893ee3-dd10-4b1c-832d-4870354be3d8"},{"Application Name":"ASM Campaign Servicing","Application IDs":"0cb7b9ec-5336-483b-bc31-b15b5788de71"},{"Application Name":"Azure Advanced Threat Protection","Application IDs":"7b7531ad-5926-4f2d-8a1d-38495ad33e17"},{"Application Name":"Azure Data Lake","Application IDs":"e9f49c6b-5ce5-44c8-925d-015017e9f7ad"},{"Application Name":"Azure Lab Services Portal","Application IDs":"835b2a73-6e10-4aa5-a979-21dfda45231c"},{"Application Name":"Azure Portal","Application IDs":"c44b4083-3bb0-49c1-b47d-974e53cbdf3c"},{"Application Name":"AzureSupportCenter","Application IDs":"37182072-3c9c-4f6a-a4b3-b3f91cacffce"},{"Application Name":"Bing","Application IDs":"9ea1ad79-fdb6-4f9a-8bc3-2b70f96e34c7"},{"Application Name":"CPIM Service","Application IDs":"bb2a2e3a-c5e7-4f0a-88e0-8e01fd3fc1f4"},{"Application Name":"CRM Power BI Integration","Application IDs":"e64aa8bc-8eb4-40e2-898b-cf261a25954f"},{"Application Name":"Dataverse","Application IDs":"00000007-0000-0000-c000-000000000000"},{"Application Name":"Enterprise Roaming and Backup","Application IDs":"60c8bde5-3167-4f92-8fdb-059f6176dc0f"},{"Application Name":"IAM Supportability","Application IDs":"a57aca87-cbc0-4f3c-8b9e-dc095fdc8978"},{"Application Name":"IrisSelectionFrontDoor","Application IDs":"16aeb910-ce68-41d1-9ac3-9e1673ac9575"},{"Application Name":"MCAPI Authorization Prod","Application IDs":"d73f4b35-55c9-48c7-8b10-651f6f2acb2e"},{"Application Name":"Media Analysis and Transformation Service","Application IDs":"944f0bd1-117b-4b1c-af26-804ed95e767e
0cd196ee-71bf-4fd6-a57c-b491ffd4fb1e"},{"Application Name":"Microsoft 365 Support Service","Application IDs":"ee272b19-4411-433f-8f28-5c13cb6fd407"},{"Application Name":"Microsoft App Access Panel","Application IDs":"0000000c-0000-0000-c000-000000000000"},{"Application Name":"Microsoft Approval Management","Application IDs":"65d91a3d-ab74-42e6-8a2f-0add61688c74
38049638-cc2c-4cde-abe4-4479d721ed44"},{"Application Name":"Microsoft Authentication Broker","Application IDs":"29d9ed98-a469-4536-ade2-f981bc1d605e"},{"Application Name":"Microsoft Azure CLI","Application IDs":"04b07795-8ddb-461a-bbee-02f9e1bf7b46"},{"Application Name":"Microsoft Azure PowerShell","Application IDs":"1950a258-227b-4e31-a9cf-717495945fc2"},{"Application Name":"Microsoft Bing Search","Application IDs":"cf36b471-5b44-428c-9ce7-313bf84528de"},{"Application Name":"Microsoft Bing Search for Microsoft Edge","Application IDs":"2d7f3606-b07d-41d1-b9d2-0d0c9296a6e8"},{"Application Name":"Microsoft Bing Default Search Engine","Application IDs":"1786c5ed-9644-47b2-8aa0-7201292175b6"},{"Application Name":"Microsoft Defender for Cloud Apps","Application IDs":"3090ab82-f1c1-4cdf-af2c-5d7a6f3e2cc7"},{"Application Name":"Microsoft Docs","Application IDs":"18fbca16-2224-45f6-85b0-f7bf2b39b3f3"},{"Application Name":"Microsoft Dynamics ERP","Application IDs":"00000015-0000-0000-c000-000000000000"},{"Application Name":"Microsoft Edge Insider Addons Prod","Application IDs":"6253bca8-faf2-4587-8f2f-b056d80998a7"},{"Application Name":"Microsoft Exchange Online Protection","Application IDs":"00000007-0000-0ff1-ce00-000000000000"},{"Application Name":"Microsoft Forms","Application IDs":"c9a559d2-7aab-4f13-a6ed-e7e9c52aec87"},{"Application Name":"Microsoft Graph","Application IDs":"00000003-0000-0000-c000-000000000000"},{"Application Name":"Microsoft Intune Web Company Portal","Application IDs":"74bcdadc-2fdc-4bb3-8459-76d06952a0e9"},{"Application Name":"Microsoft Intune Windows Agent","Application IDs":"fc0f3af4-6835-4174-b806-f7db311fd2f3"},{"Application Name":"Microsoft Learn","Application IDs":"18fbca16-2224-45f6-85b0-f7bf2b39b3f3"},{"Application Name":"Microsoft Office","Application IDs":"d3590ed6-52b3-4102-aeff-aad2292ab01c"},{"Application Name":"Microsoft Office 365 Portal","Application IDs":"00000006-0000-0ff1-ce00-000000000000"},{"Application Name":"Microsoft Office Web Apps Service","Application IDs":"67e3df25-268a-4324-a550-0de1c7f97287"},{"Application Name":"Microsoft Online Syndication Partner Portal","Application IDs":"d176f6e7-38e5-40c9-8a78-3998aab820e7"},{"Application Name":"Microsoft password reset service","Application IDs":"93625bc8-bfe2-437a-97e0-3d0060024faa"},{"Application Name":"Microsoft Power BI","Application IDs":"871c010f-5e61-4fb1-83ac-98610a7e9110"},{"Application Name":"Microsoft Storefronts","Application IDs":"28b567f6-162c-4f54-99a0-6887f387bbcc"},{"Application Name":"Microsoft Stream Portal","Application IDs":"cf53fce8-def6-4aeb-8d30-b158e7b1cf83"},{"Application Name":"Microsoft Substrate Management","Application IDs":"98db8bd6-0cc0-4e67-9de5-f187f1cd1b41"},{"Application Name":"Microsoft Support","Application IDs":"fdf9885b-dd37-42bf-82e5-c3129ef5a302"},{"Application Name":"Microsoft Teams","Application IDs":"1fec8e78-bce4-4aaf-ab1b-5451cc387264"},{"Application Name":"Microsoft Teams Services","Application IDs":"cc15fd57-2c6c-4117-a88c-83b1d56b4bbe"},{"Application Name":"Microsoft Teams Web Client","Application IDs":"5e3ce6c0-2b1f-4285-8d4b-75ee78787346"},{"Application Name":"Microsoft Whiteboard Services","Application IDs":"95de633a-083e-42f5-b444-a4295d8e9314"},{"Application Name":"O365 Suite UX","Application IDs":"4345a7b9-9a63-4910-a426-35363201d503"},{"Application Name":"Office 365 Exchange Online","Application IDs":"00000002-0000-0ff1-ce00-000000000000"},{"Application Name":"Office 365 Management","Application IDs":"00b41c95-dab0-4487-9791-b9d2c32c80f2"},{"Application Name":"Office 365 Search Service","Application IDs":"66a88757-258c-4c72-893c-3e8bed4d6899"},{"Application Name":"Office 365 SharePoint Online","Application IDs":"00000003-0000-0ff1-ce00-000000000000"},{"Application Name":"Office Delve","Application IDs":"94c63fef-13a3-47bc-8074-75af8c65887a"},{"Application Name":"Office Online Add-in SSO","Application IDs":"93d53678-613d-4013-afc1-62e9e444a0a5"},{"Application Name":"Office Online Client AAD- Augmentation Loop","Application IDs":"2abdc806-e091-4495-9b10-b04d93c3f040"},{"Application Name":"Office Online Client AAD- Loki","Application IDs":"b23dd4db-9142-4734-867f-3577f640ad0c"},{"Application Name":"Office Online Client AAD- Maker","Application IDs":"17d5e35f-655b-4fb0-8ae6-86356e9a49f5"},{"Application Name":"Office Online Client MSA- Loki","Application IDs":"b6e69c34-5f1f-4c34-8cdf-7fea120b8670"},{"Application Name":"Office Online Core SSO","Application IDs":"243c63a3-247d-41c5-9d83-7788c43f1c43"},{"Application Name":"Office Online Search","Application IDs":"a9b49b65-0a12-430b-9540-c80b3332c127"},{"Application Name":"Office.com","Application IDs":"4b233688-031c-404b-9a80-a4f3f2351f90"},{"Application Name":"Office365 Shell WCSS-Client","Application IDs":"89bee1f7-5e6e-4d8a-9f3d-ecd601259da7"},{"Application Name":"OfficeClientService","Application IDs":"0f698dd4-f011-4d23-a33e-b36416dcb1e6"},{"Application Name":"OfficeHome","Application IDs":"4765445b-32c6-49b0-83e6-1d93765276ca"},{"Application Name":"OfficeShredderWacClient","Application IDs":"4d5c2d63-cf83-4365-853c-925fd1a64357"},{"Application Name":"OMSOctopiPROD","Application IDs":"62256cef-54c0-4cb4-bcac-4c67989bdc40"},{"Application Name":"OneDrive SyncEngine","Application IDs":"ab9b8c07-8f02-4f72-87fa-80105867a763"},{"Application Name":"OneNote","Application IDs":"2d4d3d8e-2be3-4bef-9f87-7875a61c29de"},{"Application Name":"Outlook Mobile","Application IDs":"27922004-5251-4030-b22d-91ecd9a37ea4"},{"Application Name":"Partner Customer Delegated Admin Offline Processor","Application IDs":"a3475900-ccec-4a69-98f5-a65cd5dc5306"},{"Application Name":"Password Breach Authenticator","Application IDs":"bdd48c81-3a58-4ea9-849c-ebea7f6b6360"},{"Application Name":"Power BI Service","Application IDs":"00000009-0000-0000-c000-000000000000"},{"Application Name":"SharedWithMe","Application IDs":"ffcb16e8-f789-467c-8ce9-f826a080d987"},{"Application Name":"SharePoint Online Web Client Extensibility","Application IDs":"08e18876-6177-487e-b8b5-cf950c1e598c"},{"Application Name":"Signup","Application IDs":"b4bddae8-ab25-483e-8670-df09b9f1d0ea"},{"Application Name":"Skype for Business Online","Application IDs":"00000004-0000-0ff1-ce00-000000000000"},{"Application Name":"Sway","Application IDs":"905fcf26-4eb7-48a0-9ff0-8dcc7194b5ba"},{"Application Name":"Universal Store Native Client","Application IDs":"268761a2-03f3-40df-8a8b-c3db24145b6b"},{"Application Name":"Vortex [wsfed enabled]","Application IDs":"5572c4c0-d078-44ce-b81c-6cbf8d3ed39e"},{"Application Name":"Windows Azure Active Directory","Application IDs":"00000002-0000-0000-c000-000000000000"},{"Application Name":"Windows Azure Service Management API","Application IDs":"797f4846-ba00-4fd7-ba43-dac1f8f63013"},{"Application Name":"WindowsDefenderATP Portal","Application IDs":"a3b79187-70b2-4139-83f9-6016c58cd27b"},{"Application Name":"Windows Search","Application IDs":"26a7ee05-5602-4d76-a7ba-eae8b7b67941"},{"Application Name":"Windows Spotlight","Application IDs":"1b3c667f-cde3-4090-b60b-3d2abd0117f0"},{"Application Name":"Windows Store for Business","Application IDs":"45a330b1-b1ec-4cc1-9161-9f03992aa49f"},{"Application Name":"Yammer","Application IDs":"00000005-0000-0ff1-ce00-000000000000"},{"Application Name":"Yammer Web","Application IDs":"c1c74fed-04c9-4704-80dc-9f79a2e515cb"},{"Application Name":"Yammer Web Embed","Application IDs":"e1ef36fd-b883-4dbf-97f0-9ece4b576fc6"}]' | ConvertFrom-Json | Where-Object -Property 'Application IDs' -EQ $data.applicationId - $HTMLTemplate = Get-Content 'TemplateEmail.html' -Raw | Out-String + # Get the function app root directory by navigating from the module location + $ModuleBase = Get-Module CIPPCore | Select-Object -ExpandProperty ModuleBase + $FunctionAppRoot = (Get-Item $ModuleBase).Parent.Parent.FullName + $TemplatePath = Join-Path $FunctionAppRoot 'TemplateEmail.html' + $HTMLTemplate = Get-Content $TemplatePath -Raw | Out-String $Title = '' $IntroText = '' $ButtonUrl = '' From 5fa89888094613ec5df89de28754efd192b397f4 Mon Sep 17 00:00:00 2001 From: James Tarran Date: Wed, 4 Feb 2026 15:01:50 +0000 Subject: [PATCH 341/503] Update Get-CIPPAlertSmtpAuthSuccess.ps1 Remove .value from $signins as this is done is new-GraphGetRequest already so $SignIns.value.value is null --- Modules/CIPPCore/Public/Alerts/Get-CIPPAlertSmtpAuthSuccess.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertSmtpAuthSuccess.ps1 b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertSmtpAuthSuccess.ps1 index c7c8e57f4ea6..f8e4b0f9b809 100644 --- a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertSmtpAuthSuccess.ps1 +++ b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertSmtpAuthSuccess.ps1 @@ -19,7 +19,7 @@ function Get-CIPPAlertSmtpAuthSuccess { $SignIns = New-GraphGetRequest -uri $uri -tenantid $TenantFilter # Select only the properties you care about - $AlertData = $SignIns.value | Select-Object userPrincipalName, createdDateTime, clientAppUsed, ipAddress, status, @{Name = 'Tenant'; Expression = { $TenantFilter } } + $AlertData = $SignIns | Select-Object userPrincipalName, createdDateTime, clientAppUsed, ipAddress, status, @{Name = 'Tenant'; Expression = { $TenantFilter } } # Write results into the alert pipeline Write-AlertTrace -cmdletName $MyInvocation.MyCommand -tenantFilter $TenantFilter -data $AlertData From 21bc0295aec12c1c8173254825b94884710ccad6 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Wed, 4 Feb 2026 10:34:24 -0800 Subject: [PATCH 342/503] bump version to 10.0.8 --- host.json | 2 +- version_latest.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/host.json b/host.json index a581a9789068..97957cbf1ed7 100644 --- a/host.json +++ b/host.json @@ -16,7 +16,7 @@ "distributedTracingEnabled": false, "version": "None" }, - "defaultVersion": "10.0.7", + "defaultVersion": "10.0.8", "versionMatchStrategy": "Strict", "versionFailureStrategy": "Fail" } diff --git a/version_latest.txt b/version_latest.txt index 9380cfccb8c7..5219a0df74ba 100644 --- a/version_latest.txt +++ b/version_latest.txt @@ -1 +1 @@ -10.0.7 +10.0.8 From 637688067f1c6b9bc34373af63588e2710d9233a Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Thu, 5 Feb 2026 04:37:49 -0800 Subject: [PATCH 343/503] Licence management granular control and optimisations Optimise getting users licences, making bulk graph requests making it much faster when updating more than 1 licence. --- .../Users/Invoke-ExecBulkLicense.ps1 | 91 ++++++--- .../CIPPCore/Public/Set-CIPPUserLicense.ps1 | 182 ++++++++++++++---- 2 files changed, 209 insertions(+), 64 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecBulkLicense.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecBulkLicense.ps1 index 69b8b3f169ff..58c4446912ba 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecBulkLicense.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecBulkLicense.ps1 @@ -23,7 +23,21 @@ function Invoke-ExecBulkLicense { foreach ($TenantGroup in $TenantGroups) { $TenantFilter = $TenantGroup.Name $TenantRequests = $TenantGroup.Group - $AllUsers = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/users?&`$select=id,userPrincipalName,assignedLicenses" -tenantid $TenantFilter + + # Initialize list for bulk license requests + $LicenseRequests = [System.Collections.Generic.List[object]]::new() + + # Get unique user IDs for this tenant + $UserIds = $TenantRequests.userIds | Select-Object -Unique + + # Build OData filter for specific users only + $UserIdFilters = $UserIds | ForEach-Object { "id eq '$_'" } + $FilterQuery = $UserIdFilters -join ' or ' + + # Fetch only the users we need with server-side filtering + $AllUsers = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/users?`$filter=$FilterQuery&`$select=id,userPrincipalName,assignedLicenses&top=999" -tenantid $TenantFilter + + # Create lookup for quick access $UserLookup = @{} foreach ($User in $AllUsers) { $UserLookup[$User.id] = $User @@ -31,37 +45,64 @@ function Invoke-ExecBulkLicense { # Process each user request foreach ($UserRequest in $TenantRequests) { - try { - $UserId = $UserRequest.userIds - $User = $UserLookup[$UserId] - $UserPrincipalName = $User.userPrincipalName - $LicenseOperation = $UserRequest.LicenseOperation - $RemoveAllLicenses = [bool]$UserRequest.RemoveAllLicenses - $Licenses = $UserRequest.Licenses | ForEach-Object { $_.value } - # Handle license operations - if ($LicenseOperation -eq 'Add' -or $LicenseOperation -eq 'Replace') { - $AddLicenses = $Licenses - } + $UserId = $UserRequest.userIds + $User = $UserLookup[$UserId] + $UserPrincipalName = $User.userPrincipalName + $LicenseOperation = $UserRequest.LicenseOperation + $RemoveAllLicenses = [bool]$UserRequest.RemoveAllLicenses + $ReplaceAllLicenses = [bool]$UserRequest.ReplaceAllLicenses + $Licenses = $UserRequest.Licenses | ForEach-Object { $_.value } + $LicensesToRemove = $UserRequest.LicensesToRemove | ForEach-Object { $_.value } + $LicensesToReplace = $UserRequest.LicensesToReplace | ForEach-Object { $_.value } - if ($LicenseOperation -eq 'Remove' -and $RemoveAllLicenses) { + # Handle license operations + if ($LicenseOperation -eq 'Add') { + $AddLicenses = $Licenses + $RemoveLicenses = @() + } elseif ($LicenseOperation -eq 'Remove') { + if ($RemoveAllLicenses) { $RemoveLicenses = $User.assignedLicenses.skuId - } elseif ($LicenseOperation -eq 'Remove') { - $RemoveLicenses = $Licenses - } elseif ($LicenseOperation -eq 'Replace') { - $RemoveReplace = $User.assignedLicenses.skuId - if ($RemoveReplace) { Set-CIPPUserLicense -UserId $UserId -TenantFilter $TenantFilter -RemoveLicenses $RemoveReplace -APIName $APIName -Headers $Headers } - } elseif ($RemoveAllLicenses) { + } else { + # Only remove licenses the user actually has + $RemoveLicenses = $LicensesToRemove | Where-Object { $_ -in $User.assignedLicenses.skuId } + } + $AddLicenses = @() + } elseif ($LicenseOperation -eq 'Replace') { + $AddLicenses = $Licenses + if ($ReplaceAllLicenses) { + # Replace all existing licenses with new ones $RemoveLicenses = $User.assignedLicenses.skuId + } else { + # Only replace licenses the user actually has + $RemoveLicenses = $LicensesToReplace | Where-Object { $_ -in $User.assignedLicenses.skuId } } - #todo: Actually build bulk support into set-cippuserlicense. - $TaskResults = Set-CIPPUserLicense -UserId $UserId -TenantFilter $TenantFilter -AddLicenses $AddLicenses -RemoveLicenses $RemoveLicenses -APIName $APIName -Headers $Headers + } - $Results.Add($TaskResults) - Write-LogMessage -API $APIName -tenant $TenantFilter -message "Successfully processed licenses for user $UserPrincipalName" -Sev 'Info' + # Add to processing list if there are licenses to add or remove + if ($AddLicenses.Count -gt 0 -or $RemoveLicenses.Count -gt 0) { + $LicenseRequests.Add([PSCustomObject]@{ + UserId = $UserId + UserPrincipalName = $UserPrincipalName + AddLicenses = $AddLicenses + RemoveLicenses = $RemoveLicenses + IsReplace = ($LicenseOperation -eq 'Replace' -and $ReplaceAllLicenses) + }) + } else { + $Results.Add("No license changes needed for user $UserPrincipalName") + } + } + + # Process all license changes in bulk + if ($LicenseRequests.Count -gt 0) { + try { + $BulkResults = Set-CIPPUserLicense -LicenseRequests $LicenseRequests -TenantFilter $TenantFilter -APIName $APIName -Headers $Headers + foreach ($Result in $BulkResults) { + $Results.Add($Result) + } } catch { $ErrorMessage = Get-CippException -Exception $_ - $Results.Add("Failed to process licenses for user $($UserRequest.userIds). Error: $($ErrorMessage.NormalizedError)") - Write-LogMessage -API $APIName -tenant $TenantFilter -message "Failed to process licenses for user $($UserRequest.userIds). Error: $($ErrorMessage.NormalizedError)" -Sev 'Error' -LogData $ErrorMessage + $Results.Add("Failed to process bulk license operation for tenant $TenantFilter. Error: $($ErrorMessage.NormalizedError)") + Write-LogMessage -API $APIName -tenant $TenantFilter -message "Failed to process bulk license operation. Error: $($ErrorMessage.NormalizedError)" -Sev 'Error' -LogData $ErrorMessage } } } diff --git a/Modules/CIPPCore/Public/Set-CIPPUserLicense.ps1 b/Modules/CIPPCore/Public/Set-CIPPUserLicense.ps1 index e72fb7b69701..587728681c63 100644 --- a/Modules/CIPPCore/Public/Set-CIPPUserLicense.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPUserLicense.ps1 @@ -1,59 +1,163 @@ function Set-CIPPUserLicense { [CmdletBinding()] param ( - [Parameter(Mandatory)][string]$UserId, + [Parameter(ParameterSetName = 'Single', Mandatory)][string]$UserId, + [Parameter(ParameterSetName = 'Single')][string]$UserPrincipalName, + [Parameter(ParameterSetName = 'Single')][array]$AddLicenses = @(), + [Parameter(ParameterSetName = 'Single')][array]$RemoveLicenses = @(), + [Parameter(ParameterSetName = 'Bulk', Mandatory)][System.Collections.Generic.List[object]]$LicenseRequests, [Parameter(Mandatory)][string]$TenantFilter, - [Parameter()][array]$AddLicenses = @(), - [Parameter()][array]$RemoveLicenses = @(), $Headers, $APIName = 'Set User License' ) - # Build the addLicenses array - $AddLicensesArray = foreach ($license in $AddLicenses) { + # Handle single user request (legacy support) + if ($PSCmdlet.ParameterSetName -eq 'Single') { + $LicenseRequests = [System.Collections.Generic.List[object]]::new() + $LicenseRequests.Add([PSCustomObject]@{ + UserId = $UserId + UserPrincipalName = $UserPrincipalName + AddLicenses = $AddLicenses + RemoveLicenses = $RemoveLicenses + IsReplace = $false + }) + } + + $Results = [System.Collections.Generic.List[string]]::new() + + # Get default usage location once for all users + $Table = Get-CippTable -tablename 'UserSettings' + $UserSettings = Get-CIPPAzDataTableEntity @Table -Filter "PartitionKey eq 'UserSettings' and RowKey eq 'allUsers'" + if ($UserSettings) { $DefaultUsageLocation = (ConvertFrom-Json $UserSettings.JSON -Depth 5 -ErrorAction SilentlyContinue).usageLocation.value } + $DefaultUsageLocation ??= 'US' + + # Process Replace operations first (remove all licenses) + $ReplaceRequests = $LicenseRequests | Where-Object { $_.IsReplace -and $_.RemoveLicenses.Count -gt 0 } + if ($ReplaceRequests.Count -gt 0) { + $RemoveBulkRequests = foreach ($Request in $ReplaceRequests) { + @{ + id = $Request.UserId + method = 'POST' + url = "/users/$($Request.UserId)/assignLicense" + body = @{ + 'addLicenses' = @() + 'removeLicenses' = @($Request.RemoveLicenses) + } + headers = @{ 'Content-Type' = 'application/json' } + } + } + + $RemoveResults = New-GraphBulkRequest -tenantid $TenantFilter -Requests @($RemoveBulkRequests) + + foreach ($Result in $RemoveResults) { + $Request = $ReplaceRequests | Where-Object { $_.UserId -eq $Result.id } + if ($Result.status -ge 200 -and $Result.status -le 299) { + Write-LogMessage -Headers $Headers -API $APIName -tenant $TenantFilter -message "Removed existing licenses for user $($Request.UserPrincipalName)" -Sev 'Info' + } else { + $Results.Add("Failed to remove licenses for user $($Request.UserPrincipalName): $($Result.body.error.message)") + Write-LogMessage -Headers $Headers -API $APIName -tenant $TenantFilter -message "Failed to remove licenses for user $($Request.UserPrincipalName): $($Result.body.error.message)" -Sev 'Error' + } + } + } + + # Build bulk requests for license assignment + $BulkRequests = foreach ($Request in $LicenseRequests) { + $AddLicensesArray = foreach ($license in $Request.AddLicenses) { + @{ 'disabledPlans' = @(); 'skuId' = $license } + } + @{ - 'disabledPlans' = @() - 'skuId' = $license + id = $Request.UserId + method = 'POST' + url = "/users/$($Request.UserId)/assignLicense" + body = @{ + 'addLicenses' = @($AddLicensesArray) + 'removeLicenses' = $Request.IsReplace ? @() : @($Request.RemoveLicenses) + } + headers = @{ 'Content-Type' = 'application/json' } } } - # Build the LicenseBody hashtable - $LicenseBody = @{ - 'addLicenses' = @($AddLicensesArray) - 'removeLicenses' = @($RemoveLicenses) ? @($RemoveLicenses) : @() + # Execute bulk request + $BulkResults = New-GraphBulkRequest -tenantid $TenantFilter -Requests @($BulkRequests) + + # Collect users with usage location errors + $UsageLocationErrors = [System.Collections.Generic.List[object]]::new() + + foreach ($Result in $BulkResults) { + $Request = $LicenseRequests | Where-Object { $_.UserId -eq $Result.id } + + if ($Result.status -ge 200 -and $Result.status -le 299) { + $Results.Add("Successfully set licenses for $($Request.UserPrincipalName). It may take 2–5 minutes before the changes become visible.") + Write-LogMessage -Headers $Headers -API $APIName -tenant $TenantFilter -message "Assigned licenses to user $($Request.UserPrincipalName). Added: $($Request.AddLicenses -join ', '); Removed: $($Request.RemoveLicenses -join ', ')" -Sev 'Info' + } elseif ($Result.body.error.message -like '*invalid usage location*' -or $Result.body.error.message -like '*UsageLocation*') { + $UsageLocationErrors.Add($Request) + } else { + $Results.Add("Failed to assign licenses for user $($Request.UserPrincipalName): $($Result.body.error.message)") + Write-LogMessage -Headers $Headers -API $APIName -tenant $TenantFilter -message "Failed to assign licenses for user $($Request.UserPrincipalName): $($Result.body.error.message)" -Sev 'Error' + } } - # Convert the LicenseBody to JSON - $LicenseBodyJson = ConvertTo-Json -InputObject $LicenseBody -Depth 10 -Compress - - Write-Host "License body JSON: $LicenseBodyJson" - - try { - try { - $null = New-GraphPostRequest -uri "https://graph.microsoft.com/beta/users/$UserId/assignLicense" -tenantid $TenantFilter -type POST -body $LicenseBodyJson -Verbose - } catch { - # Handle if the error is due to missing usage location - if ($_.Exception.Message -like '*invalid usage location*') { - $Table = Get-CippTable -tablename 'UserSettings' - $UserSettings = Get-CIPPAzDataTableEntity @Table -Filter "PartitionKey eq 'UserSettings' and RowKey eq 'allUsers'" - if ($UserSettings) { $DefaultUsageLocation = (ConvertFrom-Json $UserSettings.JSON -Depth 5 -ErrorAction SilentlyContinue).usageLocation.value } - $DefaultUsageLocation ??= 'US' # Fallback to US if not set - - $UsageLocationJson = ConvertTo-Json -InputObject @{'usageLocation' = $DefaultUsageLocation } -Depth 5 -Compress - $null = New-GraphPostRequest -uri "https://graph.microsoft.com/beta/users/$UserId" -tenantid $TenantFilter -type PATCH -body $UsageLocationJson -Verbose - Write-LogMessage -Headers $Headers -API $APIName -tenant $TenantFilter -message "Set usage location for user $UserId to $DefaultUsageLocation" -Sev 'Info' - # Retry assigning the license - $null = New-GraphPostRequest -uri "https://graph.microsoft.com/beta/users/$UserId/assignLicense" -tenantid $TenantFilter -type POST -body $LicenseBodyJson -Verbose + # Handle usage location errors + if ($UsageLocationErrors.Count -gt 0) { + # Set usage location for all users with errors + $UsageLocationRequests = foreach ($Request in $UsageLocationErrors) { + @{ + id = $Request.UserId + method = 'PATCH' + url = "/users/$($Request.UserId)" + body = @{ 'usageLocation' = $DefaultUsageLocation } + headers = @{ 'Content-Type' = 'application/json' } + } + } + + $UsageLocationResults = New-GraphBulkRequest -tenantid $TenantFilter -Requests @($UsageLocationRequests) + + # Log usage location updates + foreach ($Result in $UsageLocationResults) { + $Request = $UsageLocationErrors | Where-Object { $_.UserId -eq $Result.id } + if ($Result.status -ge 200 -and $Result.status -le 299) { + Write-LogMessage -Headers $Headers -API $APIName -tenant $TenantFilter -message "Set usage location for user $($Request.UserPrincipalName) to $DefaultUsageLocation" -Sev 'Info' + } + } + + # Retry license assignment for users with fixed usage location + $RetryBulkRequests = foreach ($Request in $UsageLocationErrors) { + $AddLicensesArray = foreach ($license in $Request.AddLicenses) { + @{ 'disabledPlans' = @(); 'skuId' = $license } + } + + @{ + id = $Request.UserId + method = 'POST' + url = "/users/$($Request.UserId)/assignLicense" + body = @{ + 'addLicenses' = @($AddLicensesArray) + 'removeLicenses' = $Request.IsReplace ? @() : @($Request.RemoveLicenses) + } + headers = @{ 'Content-Type' = 'application/json' } + } + } + + $RetryResults = New-GraphBulkRequest -tenantid $TenantFilter -Requests @($RetryBulkRequests) + + foreach ($Result in $RetryResults) { + $Request = $UsageLocationErrors | Where-Object { $_.UserId -eq $Result.id } + + if ($Result.status -ge 200 -and $Result.status -le 299) { + $Results.Add("Successfully set licenses for $($Request.UserPrincipalName) after setting usage location. It may take 2–5 minutes before the changes become visible.") + Write-LogMessage -Headers $Headers -API $APIName -tenant $TenantFilter -message "Assigned licenses to user $($Request.UserPrincipalName) after usage location fix. Added: $($Request.AddLicenses -join ', '); Removed: $($Request.RemoveLicenses -join ', ')" -Sev 'Info' } else { - throw $_ + $Results.Add("Failed to assign licenses for user $($Request.UserPrincipalName) after setting usage location: $($Result.body.error.message)") + Write-LogMessage -Headers $Headers -API $APIName -tenant $TenantFilter -message "Failed to assign licenses for user $($Request.UserPrincipalName) after usage location fix: $($Result.body.error.message)" -Sev 'Error' } } - } catch { - $ErrorMessage = Get-CippException -Exception $_ - Write-LogMessage -Headers $Headers -API $APIName -tenant $TenantFilter -message "Failed to assign the license. Error: $($ErrorMessage.NormalizedError)" -Sev Error -LogData $ErrorMessage - throw "Failed to assign the license. $($ErrorMessage.NormalizedError)" } - Write-LogMessage -Headers $Headers -API $APIName -tenant $TenantFilter -message "Assigned licenses to user $UserId. Added: $AddLicenses; Removed: $RemoveLicenses" -Sev 'Info' - return "Successfully set licenses for $UserId. It may take 2–5 minutes before the changes become visible." + # Return single result for legacy support, or all results for bulk + if ($PSCmdlet.ParameterSetName -eq 'Single') { + return $Results[0] + } else { + return $Results + } } From 7e08cac01e0868d5beb70644922a3999f1e868b7 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 5 Feb 2026 09:22:10 -0800 Subject: [PATCH 344/503] fix exchange monitor --- Modules/CIPPCore/Public/Test-CIPPRerun.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Test-CIPPRerun.ps1 b/Modules/CIPPCore/Public/Test-CIPPRerun.ps1 index 198cf87143f9..57d8d9b3a603 100644 --- a/Modules/CIPPCore/Public/Test-CIPPRerun.ps1 +++ b/Modules/CIPPCore/Public/Test-CIPPRerun.ps1 @@ -21,7 +21,7 @@ function Test-CIPPRerun { 'Standard' { 9800 } # 2 hours 45 minutes ish. 'BPA' { 85000 } # 24 hours ish. 'CippTests' { 85000 } # 24 hours ish. - 'Get-CIPPAlertQuarantineReleaseRequests' { 3500 } #about an hour + 'ExchangeMonitor' { 3500 } #about an hour default { throw "Unknown type: $Type" } } } From 8c03182c144959a00da03e5f051cc8361d7cb59e Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 5 Feb 2026 09:33:56 -0800 Subject: [PATCH 345/503] remove agents prop from compare --- Modules/CIPPCore/Public/Compare-CIPPIntuneObject.ps1 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Compare-CIPPIntuneObject.ps1 b/Modules/CIPPCore/Public/Compare-CIPPIntuneObject.ps1 index 616f0ef600a9..01e0c0f8f82e 100644 --- a/Modules/CIPPCore/Public/Compare-CIPPIntuneObject.ps1 +++ b/Modules/CIPPCore/Public/Compare-CIPPIntuneObject.ps1 @@ -30,7 +30,8 @@ function Compare-CIPPIntuneObject { 'featureUpdatesPauseStartDate' 'wslDistributions', 'lastSuccessfulSyncDateTime', - 'tenantFilter' + 'tenantFilter', + 'agents' ) $excludeProps = $defaultExcludeProperties + $ExcludeProperties From dde937ec96ec97f2dc9e7b7453730c01b2f758a0 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 5 Feb 2026 10:05:29 -0800 Subject: [PATCH 346/503] bump version to 10.0.9 --- host.json | 2 +- version_latest.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/host.json b/host.json index 97957cbf1ed7..2af179e475df 100644 --- a/host.json +++ b/host.json @@ -16,7 +16,7 @@ "distributedTracingEnabled": false, "version": "None" }, - "defaultVersion": "10.0.8", + "defaultVersion": "10.0.9", "versionMatchStrategy": "Strict", "versionFailureStrategy": "Fail" } diff --git a/version_latest.txt b/version_latest.txt index 5219a0df74ba..82f3d338cfb6 100644 --- a/version_latest.txt +++ b/version_latest.txt @@ -1 +1 @@ -10.0.8 +10.0.9 From 72144629230f3f346220f6e41479631b34da08dc Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 5 Feb 2026 12:43:24 -0800 Subject: [PATCH 347/503] Fix nested group lookup and variable naming Accumulate nested group memberships into a typed List and use AddRange to correctly collect results from Graph queries. Rename loop variables to avoid shadowing ($RoleGroup / $ExpectedGroup) and update matching/log messages accordingly. Include nested groups in the returned Memberships so missing-group detection considers indirect membership; preserve AdminAgents as an error-level issue. --- .../Public/Test-CIPPGDAPRelationships.ps1 | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/Modules/CIPPCore/Public/Test-CIPPGDAPRelationships.ps1 b/Modules/CIPPCore/Public/Test-CIPPGDAPRelationships.ps1 index 18748ca7e5f4..44defc8c5b28 100644 --- a/Modules/CIPPCore/Public/Test-CIPPGDAPRelationships.ps1 +++ b/Modules/CIPPCore/Public/Test-CIPPGDAPRelationships.ps1 @@ -56,30 +56,31 @@ function Test-CIPPGDAPRelationships { 'M365 GDAP Privileged Authentication Administrator' ) $RoleAssignableGroups = $SAMUserMemberships | Where-Object { $_.isAssignableToRole } - $NestedGroups = foreach ($Group in $RoleAssignableGroups) { - Write-Information "Getting nested group memberships for $($Group.displayName)" - New-GraphGetRequest -uri "https://graph.microsoft.com/beta/groups/$($Group.id)/memberOf?`$select=id,displayName" -NoAuthCheck $true + $NestedGroups = [System.Collections.Generic.List[object]]::new() + foreach ($RoleGroup in $RoleAssignableGroups) { + Write-Information "Getting nested group memberships for $($RoleGroup.displayName)" + $NestedGroups.AddRange(@(New-GraphGetRequest -uri "https://graph.microsoft.com/beta/groups/$($RoleGroup.id)/memberOf?`$select=id,displayName" -NoAuthCheck $true)) } - foreach ($Group in $ExpectedGroups) { + foreach ($ExpectedGroup in $ExpectedGroups) { $GroupFound = $false foreach ($Membership in ($SAMUserMemberships + $NestedGroups)) { - if ($Membership.displayName -match $Group) { - Write-Information "Found $Group in group memberships" + if ($Membership.displayName -match $ExpectedGroup) { + Write-Information "Found $ExpectedGroup in group memberships" $GroupFound = $true } } if (-not $GroupFound) { - if ($Group -eq 'AdminAgents') { $Type = 'Error' } else { $Type = 'Warning' } + if ($ExpectedGroup -eq 'AdminAgents') { $Type = 'Error' } else { $Type = 'Warning' } $GDAPissues.add([PSCustomObject]@{ Type = $Type - Issue = "$($Group) is not assigned to the SAM user $me. If you have migrated outside of CIPP this is to be expected. Please perform an access check to make sure you have the correct set of permissions." + Issue = "$($ExpectedGroup) is not assigned to the SAM user $me. If you have migrated outside of CIPP this is to be expected. Please perform an access check to make sure you have the correct set of permissions." Tenant = '*Partner Tenant' Relationship = 'None' Link = 'https://docs.cipp.app/setup/gdap/troubleshooting#groups' }) | Out-Null $MissingGroups.Add([PSCustomObject]@{ - Name = $Group + Name = $ExpectedGroup Type = 'SAM User Membership' }) | Out-Null } @@ -103,7 +104,7 @@ function Test-CIPPGDAPRelationships { $GDAPRelationships = [PSCustomObject]@{ GDAPIssues = @($GDAPissues) MissingGroups = @($MissingGroups) - Memberships = @($SAMUserMemberships) + Memberships = @($SAMUserMemberships + $NestedGroups) CIPPGroupCount = $CIPPGroupCount } From 5d2549dd64362ead88c3d67dfc83e9a1cc43d3ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Fri, 6 Feb 2026 08:40:55 +0100 Subject: [PATCH 348/503] refactor: clean up guest invitation logic in Invoke-AddGuest --- .../Administration/Users/Invoke-AddGuest.ps1 | 49 ++++++++++--------- 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-AddGuest.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-AddGuest.ps1 index 34d1114ac9a0..79950f3571aa 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-AddGuest.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-AddGuest.ps1 @@ -1,4 +1,4 @@ -Function Invoke-AddGuest { +function Invoke-AddGuest { <# .FUNCTIONALITY Entrypoint @@ -13,33 +13,36 @@ Function Invoke-AddGuest { $TenantFilter = $Request.Body.tenantFilter - $UserObject = $Request.Body + $DisplayName = -not [string]::IsNullOrWhiteSpace($Request.Body.displayName) ? $Request.Body.displayName : $null + $EmailAddress = -not [string]::IsNullOrWhiteSpace($Request.Body.mail) ? $Request.Body.mail : $null + $Message = -not [string]::IsNullOrWhiteSpace($Request.Body.message) ? $Request.Body.message : $null + $RedirectURL = -not [string]::IsNullOrWhiteSpace($Request.Body.redirectUri) ? $Request.Body.redirectUri : 'https://myapps.microsoft.com' + $SendInvite = [System.Convert]::ToBoolean($Request.Body.sendInvite) ?? $true + + Write-Information -MessageData "Received request to add guest with email $EmailAddress to tenant filter $TenantFilter with display name $DisplayName. SendInvite is set to $SendInvite. Redirect URL is $RedirectURL. Message is $Message" try { - if ($UserObject.RedirectURL) { - $BodyToShip = [pscustomobject] @{ - 'InvitedUserDisplayName' = $UserObject.DisplayName - 'InvitedUserEmailAddress' = $($UserObject.mail) - 'inviteRedirectUrl' = $($UserObject.RedirectURL) - 'sendInvitationMessage' = [bool]$UserObject.SendInvite - } - } else { - $BodyToShip = [pscustomobject] @{ - 'InvitedUserDisplayName' = $UserObject.DisplayName - 'InvitedUserEmailAddress' = $($UserObject.mail) - 'sendInvitationMessage' = [bool]$UserObject.SendInvite - 'inviteRedirectUrl' = 'https://myapps.microsoft.com' - } + $BodyToShip = [pscustomobject] @{ + invitedUserDisplayName = $DisplayName + invitedUserEmailAddress = $EmailAddress + inviteRedirectUrl = $RedirectURL + sendInvitationMessage = $SendInvite } - $bodyToShip = ConvertTo-Json -Depth 10 -InputObject $BodyToShip -Compress - $null = New-GraphPostRequest -uri 'https://graph.microsoft.com/beta/invitations' -tenantid $TenantFilter -type POST -body $BodyToShip -Verbose - if ($UserObject.SendInvite -eq $true) { - $Result = "Invited Guest $($UserObject.DisplayName) with Email Invite" - Write-LogMessage -headers $Headers -API $APIName -tenant $($TenantFilter) -message $Result -Sev 'Info' + + if (-not [string]::IsNullOrWhiteSpace($Message)) { + $BodyToShip | Add-Member -MemberType NoteProperty -Name 'invitedUserMessageInfo' -Value ([pscustomobject]@{ + customizedMessageBody = $Message + }) + } + + $BodyToShipJson = ConvertTo-Json -Depth 5 -InputObject $BodyToShip + $null = New-GraphPostRequest -uri 'https://graph.microsoft.com/beta/invitations' -tenantid $TenantFilter -type POST -body $BodyToShipJson + if ($SendInvite -eq $true) { + $Result = "Invited Guest $($DisplayName) with Email Invite" } else { - $Result = "Invited Guest $($UserObject.DisplayName) with no Email Invite" - Write-LogMessage -headers $Headers -API $APIName -tenant $($TenantFilter) -message $Result -Sev 'Info' + $Result = "Invited Guest $($DisplayName) with no Email Invite" } + Write-LogMessage -headers $Headers -API $APIName -tenant $($TenantFilter) -message $Result -Sev 'Info' $StatusCode = [HttpStatusCode]::OK } catch { $ErrorMessage = Get-CippException -Exception $_ From 76354bf9db6c45a129958115d46486483f711018 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Fri, 6 Feb 2026 12:38:10 -0800 Subject: [PATCH 349/503] Fixes failing calls for licence lookups and moving to a new endpoint --- .../CIPP/Core/Invoke-ListAdminPortalLicenses.ps1 | 3 ++- Modules/CIPPCore/Public/Get-CIPPLicenseOverview.ps1 | 7 ++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ListAdminPortalLicenses.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ListAdminPortalLicenses.ps1 index dc1ae4139972..dbbd60370647 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ListAdminPortalLicenses.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ListAdminPortalLicenses.ps1 @@ -11,7 +11,8 @@ function Invoke-ListAdminPortalLicenses { $TenantFilter = $Request.Query.tenantFilter try { - $AdminPortalLicenses = New-GraphGetRequest -scope 'https://admin.microsoft.com/.default' -TenantID $TenantFilter -Uri 'https://admin.microsoft.com/admin/api/tenant/accountSkus' + $AdminPortalLicenses = New-GraphGetRequest -scope 'https://admin.microsoft.com/.default' -TenantID $TenantFilter -Uri 'https://admin.microsoft.com/fd/m365licensing/v3/licensedProducts?allotmentSourceOwnerType=User&allotmentSourceType=LowFrictionTrial&allotmentSourceState=Active,Deleted,Suspended,Lockout,Warning&displayNameLanguage=en-GB' + } catch { Write-Warning 'Failed to get Admin Portal Licenses' $AdminPortalLicenses = @() diff --git a/Modules/CIPPCore/Public/Get-CIPPLicenseOverview.ps1 b/Modules/CIPPCore/Public/Get-CIPPLicenseOverview.ps1 index 2866df56749f..2e838aa0a63a 100644 --- a/Modules/CIPPCore/Public/Get-CIPPLicenseOverview.ps1 +++ b/Modules/CIPPCore/Public/Get-CIPPLicenseOverview.ps1 @@ -37,9 +37,10 @@ function Get-CIPPLicenseOverview { ) try { - $AdminPortalLicenses = New-GraphGetRequest -scope 'https://admin.microsoft.com/.default' -TenantID $TenantFilter -Uri 'https://admin.microsoft.com/admin/api/tenant/accountSkus' + $AdminPortalLicenses = New-GraphGetRequest -scope 'https://admin.microsoft.com/.default' -TenantID $TenantFilter -Uri 'https://admin.microsoft.com/fd/m365licensing/v3/licensedProducts?allotmentSourceOwnerType=User&allotmentSourceType=LowFrictionTrial&allotmentSourceState=Active,Deleted,Suspended,Lockout,Warning&displayNameLanguage=en-GB' } catch { - Write-Warning 'Failed to get Admin Portal Licenses' + Write-Warning "Failed to get Admin Portal Licenses: $($_.Exception.Message)" + $AdminPortalLicenses = @() } $Results = New-GraphBulkRequest -Requests $Requests -TenantID $TenantFilter -asapp $true @@ -103,7 +104,7 @@ function Get-CIPPLicenseOverview { $skuId = $singleReq.Licenses foreach ($sku in $skuId) { if ($sku.skuId -in $ExcludedSkuList.GUID) { continue } - $PrettyNameAdmin = $AdminPortalLicenses | Where-Object { $_.SkuId -eq $sku.skuId } | Select-Object -ExpandProperty Name + $PrettyNameAdmin = $AdminPortalLicenses | Where-Object { $_.aadSkuId -eq $sku.skuId } | Select-Object -ExpandProperty displayName -First 1 $PrettyNameCSV = ($ConvertTable | Where-Object { $_.guid -eq $sku.skuid }).'Product_Display_Name' | Select-Object -Last 1 $PrettyName = $PrettyNameAdmin ?? $PrettyNameCSV ?? $sku.skuPartNumber From ea0a6a42abd3b6a99f4229d99ecdad46e7ae80e3 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Mon, 9 Feb 2026 07:42:56 +0100 Subject: [PATCH 350/503] deprecated std --- .../Standards/Invoke-CIPPStandardLegacyEmailReportAddins.ps1 | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardLegacyEmailReportAddins.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardLegacyEmailReportAddins.ps1 index 899677c80a05..627c88eabc0c 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardLegacyEmailReportAddins.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardLegacyEmailReportAddins.ps1 @@ -31,6 +31,8 @@ function Invoke-CIPPStandardLegacyEmailReportAddins { param($Tenant, $Settings) + #Deprecated, immmediate return + return $true # Define the legacy add-ins to remove $LegacyAddins = @( @{ From 923a9291cb8dff9f670b0721468280d30064f84c Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Mon, 9 Feb 2026 07:53:25 +0100 Subject: [PATCH 351/503] use db instead. --- .../Standards/Invoke-CIPPStandardDelegateSentItems.ps1 | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDelegateSentItems.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDelegateSentItems.ps1 index 561f1420bbf1..cb3591f04121 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDelegateSentItems.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDelegateSentItems.ps1 @@ -43,13 +43,11 @@ function Invoke-CIPPStandardDelegateSentItems { if ([string]::IsNullOrWhiteSpace($Settings.IncludeUserMailboxes)) { $Settings.IncludeUserMailboxes = $true } - + $Mailboxes = New-CippDbRequest -TenantFilter $Tenant -Type 'Mailboxes' if ($Settings.IncludeUserMailboxes -eq $true) { - $Mailboxes = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-Mailbox' -cmdParams @{ RecipientTypeDetails = @('UserMailbox', 'SharedMailbox') } -Select 'Identity,UserPrincipalName,MessageCopyForSendOnBehalfEnabled,MessageCopyForSentAsEnabled' | - Where-Object { $_.MessageCopyForSendOnBehalfEnabled -eq $false -or $_.MessageCopyForSentAsEnabled -eq $false } + $Mailboxes = $Mailboxes | Where-Object { $_.recipientTypeDetails -ne 'DiscoveryMailbox' -and ($_.MessageCopyForSendOnBehalfEnabled -eq $false -or $_.MessageCopyForSentAsEnabled -eq $false) } } else { - $Mailboxes = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-Mailbox' -cmdParams @{ RecipientTypeDetails = @('SharedMailbox') } -Select 'Identity,UserPrincipalName,MessageCopyForSendOnBehalfEnabled,MessageCopyForSentAsEnabled' | - Where-Object { $_.MessageCopyForSendOnBehalfEnabled -eq $false -or $_.MessageCopyForSentAsEnabled -eq $false } + $Mailboxes = $Mailboxes | Where-Object { $_.recipientTypeDetails -eq 'SharedMailbox' -and ($_.MessageCopyForSendOnBehalfEnabled -eq $false -or $_.MessageCopyForSentAsEnabled -eq $false) } } $CurrentValue = if (!$Mailboxes) { From 8373fe9fbbb4cfa9dc66fd082aa97b6ad30bb317 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Mon, 9 Feb 2026 08:00:20 +0100 Subject: [PATCH 352/503] Less exo requests --- .../Standards/Invoke-CIPPStandardDisableBasicAuthSMTP.ps1 | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableBasicAuthSMTP.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableBasicAuthSMTP.ps1 index 6c54c0b78c1b..4e2146a003d4 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableBasicAuthSMTP.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableBasicAuthSMTP.ps1 @@ -42,10 +42,8 @@ function Invoke-CIPPStandardDisableBasicAuthSMTP { ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'DisableBasicAuthSMTP' try { - $CurrentInfo = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-TransportConfig' - - $SMTPusers = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-CASMailbox' | - Where-Object { ($_.SmtpClientAuthenticationDisabled -eq $false) } + $CurrentInfo = New-CippDbRequest -TenantFilter $Tenant -Type 'Get-TransportConfig' + $SMTPusers = New-CippDbRequest -TenantFilter $Tenant -Type 'CASMailbox' | Where-Object { ($_.SmtpClientAuthenticationDisabled -eq $false) } } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the DisableBasicAuthSMTP state for $Tenant. Error: $ErrorMessage" -Sev Error From 11f1df417e1ff4880febdb02bfa1d465090742dc Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Mon, 9 Feb 2026 08:04:47 +0100 Subject: [PATCH 353/503] use db --- .../CIPPCore/Public/Standards/Invoke-CIPPStandardlaps.ps1 | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardlaps.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardlaps.ps1 index b08508db2e6e..b7fa8f3e7544 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardlaps.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardlaps.ps1 @@ -33,15 +33,14 @@ function Invoke-CIPPStandardlaps { param($Tenant, $Settings) try { - $PreviousSetting = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/policies/deviceRegistrationPolicy' -tenantid $Tenant - } - catch { + $PreviousSetting = New-CippDbRequest -TenantFilter $Tenant -Type 'DeviceRegistrationPolicy' + } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the DeviceRegistrationPolicy state for $Tenant. Error: $ErrorMessage" -Sev Error return } - If ($Settings.remediate -eq $true) { + if ($Settings.remediate -eq $true) { try { $PreviousSetting.localAdminPassword.isEnabled = $true $NewBody = ConvertTo-Json -Compress -InputObject $PreviousSetting -Depth 10 From 27e8f6f07ffb340c1899c62e484fa978cf93a223 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Mon, 9 Feb 2026 15:33:28 +0100 Subject: [PATCH 354/503] add some exclusions --- Modules/CIPPCore/Public/Compare-CIPPIntuneObject.ps1 | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Compare-CIPPIntuneObject.ps1 b/Modules/CIPPCore/Public/Compare-CIPPIntuneObject.ps1 index 01e0c0f8f82e..7a51151cbc90 100644 --- a/Modules/CIPPCore/Public/Compare-CIPPIntuneObject.ps1 +++ b/Modules/CIPPCore/Public/Compare-CIPPIntuneObject.ps1 @@ -31,7 +31,9 @@ function Compare-CIPPIntuneObject { 'wslDistributions', 'lastSuccessfulSyncDateTime', 'tenantFilter', - 'agents' + 'agents', + 'isSynced' + 'locationInfo' ) $excludeProps = $defaultExcludeProperties + $ExcludeProperties From 98b1c7ad761f911eb9a3906cb3a8a4174ac3f991 Mon Sep 17 00:00:00 2001 From: Logan Cook <2997336+MWG-Logan@users.noreply.github.com> Date: Mon, 9 Feb 2026 12:19:58 -0500 Subject: [PATCH 355/503] fix(standards): Update Intune Deploy reusable settings to match KelvinCode --- .../Invoke-CIPPStandardIntuneTemplate.ps1 | 245 +++++++----------- 1 file changed, 100 insertions(+), 145 deletions(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardIntuneTemplate.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardIntuneTemplate.ps1 index 595e536c0714..1d5be63ed1b6 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardIntuneTemplate.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardIntuneTemplate.ps1 @@ -37,172 +37,127 @@ function Invoke-CIPPStandardIntuneTemplate { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'IntuneTemplate_general' -TenantFilter $Tenant -RequiredCapabilities @('INTUNE_A', 'MDM_Services', 'EMS', 'SCCM', 'MICROSOFTINTUNEPLAN1') - ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'intuneTemplate' - - if ($TestResult -eq $false) { - #writing to each item that the license is not present. - $settings.TemplateList | ForEach-Object { - Set-CIPPStandardsCompareField -FieldName "standards.IntuneTemplate.$($_.value)" -FieldValue 'This tenant does not have the required license for this standard.' -Tenant $Tenant - } - Write-Host "We're exiting as the correct license is not present for this standard." - return $true - } #we're done. + Write-Host 'INTUNETEMPLATERUN' $Table = Get-CippTable -tablename 'templates' $Filter = "PartitionKey eq 'IntuneTemplate'" - $Request = @{body = $null } - Write-Host "IntuneTemplate: Starting process. Settings are: $($Settings | ConvertTo-Json -Compress)" - $CompareList = foreach ($Template in $Settings) { - Write-Host "IntuneTemplate: $($Template.TemplateList.value) - Trying to find template" - $Request.body = (Get-CIPPAzDataTableEntity @Table -Filter $Filter | Where-Object -Property RowKey -Like "$($Template.TemplateList.value)*").JSON | ConvertFrom-Json -ErrorAction SilentlyContinue - if ($null -eq $Request.body) { - Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to find template $($Template.TemplateList.value). Has this Intune Template been deleted?" -sev 'Error' - continue - } - try { - $reusableSync = Sync-CIPPReusablePolicySettings -TemplateInfo $Request.body -Tenant $Tenant -ErrorAction Stop - if ($null -ne $reusableSync -and $reusableSync.PSObject.Properties.Name -contains 'RawJSON' -and $reusableSync.RawJSON) { - $Request.body.RawJSON = $reusableSync.RawJSON - } - } catch { - Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to sync reusable policy settings for template $($Template.TemplateList.value): $($_.Exception.Message)" -sev 'Error' - Write-Host "IntuneTemplate: $($Template.TemplateList.value) - Failed to sync reusable policy settings. Skipping this template." - continue + + $Template = (Get-CIPPAzDataTableEntity @Table -Filter $Filter | Where-Object -Property RowKey -Like "$($Settings.TemplateList.value)*").JSON | ConvertFrom-Json -ErrorAction SilentlyContinue + if ($null -eq $Template) { + Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to find template $($Settings.TemplateList.value). Has this Intune Template been deleted?" -sev 'Error' + return $true + } + + try { + $reusableSync = Sync-CIPPReusablePolicySettings -TemplateInfo $Template -Tenant $Tenant -ErrorAction Stop + if ($null -ne $reusableSync -and $reusableSync.PSObject.Properties.Name -contains 'RawJSON' -and $reusableSync.RawJSON) { + $Template.RawJSON = $reusableSync.RawJSON } - Write-Host "IntuneTemplate: $($Template.TemplateList.value) - Got template." + } catch { + Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to sync reusable policy settings for template $($Settings.TemplateList.value): $($_.Exception.Message)" -sev 'Error' + Write-Host "IntuneTemplate: $($Settings.TemplateList.value) - Failed to sync reusable policy settings. Skipping this template." + return $true + } - $displayname = $request.body.Displayname - $description = $request.body.Description - $RawJSON = $Request.body.RawJSON + $displayname = $Template.Displayname + $description = $Template.Description + $RawJSON = $Template.RawJSON + $TemplateType = $Template.Type + + try { + $ExistingPolicy = Get-CIPPIntunePolicy -tenantFilter $Tenant -DisplayName $displayname -TemplateType $TemplateType + } catch { + $ExistingPolicy = $null + } + + if ($ExistingPolicy) { try { - Write-Host "IntuneTemplate: $($Template.TemplateList.value) - Grabbing existing Policy" - $ExistingPolicy = Get-CIPPIntunePolicy -tenantFilter $Tenant -DisplayName $displayname -TemplateType $Request.body.Type + $RawJSON = Get-CIPPTextReplacement -Text $RawJSON -TenantFilter $Tenant + $JSONExistingPolicy = $ExistingPolicy.cippconfiguration | ConvertFrom-Json + $JSONTemplate = $RawJSON | ConvertFrom-Json + #This might be a slow one. + $Compare = Compare-CIPPIntuneObject -ReferenceObject $JSONTemplate -DifferenceObject $JSONExistingPolicy -compareType $TemplateType -ErrorAction SilentlyContinue } catch { - Write-Host "IntuneTemplate: $($Template.TemplateList.value) - Failed to get existing." - } - if ($ExistingPolicy) { - try { - Write-Host "IntuneTemplate: $($Template.TemplateList.value) - Found existing policy." - $RawJSON = Get-CIPPTextReplacement -Text $RawJSON -TenantFilter $Tenant - Write-Host "IntuneTemplate: $($Template.TemplateList.value) - Grabbing JSON existing." - $JSONExistingPolicy = $ExistingPolicy.cippconfiguration | ConvertFrom-Json - Write-Host "IntuneTemplate: $($Template.TemplateList.value) - Got existing JSON. Converting RawJSON to Template" - $JSONTemplate = $RawJSON | ConvertFrom-Json - Write-Host "IntuneTemplate: $($Template.TemplateList.value) - Converted RawJSON to Template." - Write-Host "IntuneTemplate: $($Template.TemplateList.value) - Comparing JSON." - $Compare = Compare-CIPPIntuneObject -ReferenceObject $JSONTemplate -DifferenceObject $JSONExistingPolicy -compareType $Request.body.Type -ErrorAction SilentlyContinue - } catch { - Write-Host "The compare failed. The error was: $($_.Exception.Message)" - } - Write-Host "IntuneTemplate: $($Template.TemplateList.value) - Compared JSON: $($Compare | ConvertTo-Json -Compress)" - } else { - Write-Host "IntuneTemplate: $($Template.TemplateList.value) - No existing policy found." - $compare = [pscustomobject]@{ - MatchFailed = $true - Difference = 'This policy does not exist in Intune.' - } } - if ($Compare) { - Write-Host "IntuneTemplate: $($Template.TemplateList.value) - Compare found differences." - [PSCustomObject]@{ - MatchFailed = $true - displayname = $displayname - description = $description - compare = $Compare - rawJSON = $RawJSON - body = $Request.body - assignTo = $Template.AssignTo - excludeGroup = $Template.excludeGroup - remediate = $Template.remediate - alert = $Template.alert - report = $Template.report - existingPolicyId = $ExistingPolicy.id - templateId = $Template.TemplateList.value - customGroup = $Template.customGroup - assignmentFilter = $Template.assignmentFilter - assignmentFilterType = $Template.assignmentFilterType - } - } else { - Write-Host "IntuneTemplate: $($Template.TemplateList.value) - No differences found." - [PSCustomObject]@{ - MatchFailed = $false - displayname = $displayname - description = $description - compare = $false - rawJSON = $RawJSON - body = $Request.body - assignTo = $Template.AssignTo - excludeGroup = $Template.excludeGroup - remediate = $Template.remediate - alert = $Template.alert - report = $Template.report - existingPolicyId = $ExistingPolicy.id - templateId = $Template.TemplateList.value - customGroup = $Template.customGroup - assignmentFilter = $Template.assignmentFilter - assignmentFilterType = $Template.assignmentFilterType - } + } else { + $compare = [pscustomobject]@{ + MatchFailed = $true + Difference = 'This policy does not exist in Intune.' } } + $CompareResult = [PSCustomObject]@{ + MatchFailed = [bool]$Compare + displayname = $displayname + description = $description + compare = $Compare + rawJSON = $RawJSON + templateType = $TemplateType + assignTo = $Settings.AssignTo + excludeGroup = $Settings.excludeGroup + remediate = $Settings.remediate + alert = $Settings.alert + report = $Settings.report + existingPolicyId = $ExistingPolicy.id + templateId = $Settings.TemplateList.value + customGroup = $Settings.customGroup + assignmentFilter = $Settings.assignmentFilter + assignmentFilterType = $Settings.assignmentFilterType + } - if ($true -in $Settings.remediate) { - Write-Host 'starting template deploy' - foreach ($TemplateFile in $CompareList | Where-Object -Property remediate -EQ $true) { - Write-Host "working on template deploy: $($TemplateFile.displayname)" - try { - $TemplateFile.customGroup ? ($TemplateFile.AssignTo = $TemplateFile.customGroup) : $null - - $PolicyParams = @{ - TemplateType = $TemplateFile.body.Type - Description = $TemplateFile.description - DisplayName = $TemplateFile.displayname - RawJSON = $templateFile.rawJSON - AssignTo = $TemplateFile.AssignTo - ExcludeGroup = $TemplateFile.excludeGroup - tenantFilter = $Tenant - } - - # Add assignment filter if specified - if ($TemplateFile.assignmentFilter) { - $PolicyParams.AssignmentFilterName = $TemplateFile.assignmentFilter - $PolicyParams.AssignmentFilterType = $TemplateFile.assignmentFilterType ?? 'include' - } + if ($Settings.remediate) { + try { + $CompareResult.customGroup ? ($CompareResult.AssignTo = $CompareResult.customGroup) : $null + $PolicyParams = @{ + TemplateType = $CompareResult.templateType + Description = $CompareResult.description + DisplayName = $CompareResult.displayname + RawJSON = $CompareResult.rawJSON + AssignTo = $CompareResult.AssignTo + ExcludeGroup = $CompareResult.excludeGroup + tenantFilter = $Tenant + } - Set-CIPPIntunePolicy @PolicyParams - } catch { - $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message - Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to create or update Intune Template $($TemplateFile.displayname), Error: $ErrorMessage" -sev 'Error' + # Add assignment filter if specified + if ($CompareResult.assignmentFilter) { + $PolicyParams.AssignmentFilterName = $CompareResult.assignmentFilter + $PolicyParams.AssignmentFilterType = $CompareResult.assignmentFilterType ?? 'include' } - } + Set-CIPPIntunePolicy @PolicyParams + } catch { + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to create or update Intune Template $($CompareResult.displayname), Error: $ErrorMessage" -sev 'Error' + } } - if ($true -in $Settings.alert) { - foreach ($Template in $CompareList | Where-Object -Property alert -EQ $true) { - Write-Host "working on template alert: $($Template.displayname)" - $AlertObj = $Template | Select-Object -Property displayname, description, compare, assignTo, excludeGroup, existingPolicyId - if ($Template.compare) { - Write-StandardsAlert -message "Template $($Template.displayname) does not match the expected configuration." -object $AlertObj -tenant $Tenant -standardName 'IntuneTemplate' -standardId $Settings.templateId - Write-LogMessage -API 'Standards' -tenant $Tenant -message "Template $($Template.displayname) does not match the expected configuration. We've generated an alert" -sev info + if ($Settings.alert) { + $AlertObj = $CompareResult | Select-Object -Property displayname, description, compare, assignTo, excludeGroup, existingPolicyId + if ($CompareResult.compare) { + Write-StandardsAlert -message "Template $($CompareResult.displayname) does not match the expected configuration." -object $AlertObj -tenant $Tenant -standardName 'IntuneTemplate' -standardId $Settings.templateId + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Template $($CompareResult.displayname) does not match the expected configuration. We've generated an alert" -sev info + } else { + if ($CompareResult.ExistingPolicyId) { + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Template $($CompareResult.displayname) has the correct configuration." -sev Info } else { - if ($Template.ExistingPolicyId) { - Write-LogMessage -API 'Standards' -tenant $Tenant -message "Template $($Template.displayname) has the correct configuration." -sev Info - } else { - Write-StandardsAlert -message "Template $($Template.displayname) is missing." -object $AlertObj -tenant $Tenant -standardName 'IntuneTemplate' -standardId $Settings.templateId - Write-LogMessage -API 'Standards' -tenant $Tenant -message "Template $($Template.displayname) is missing." -sev info - } + Write-StandardsAlert -message "Template $($CompareResult.displayname) is missing." -object $AlertObj -tenant $Tenant -standardName 'IntuneTemplate' -standardId $Settings.templateId + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Template $($CompareResult.displayname) is missing." -sev info } } } - if ($true -in $Settings.report) { - foreach ($Template in $CompareList | Where-Object { $_.report -eq $true -or $_.remediate -eq $true }) { - Write-Host "working on template report: $($Template.displayname)" - $id = $Template.templateId - $CompareObj = $Template.compare - $state = $CompareObj ? $CompareObj : $true - Set-CIPPStandardsCompareField -FieldName "standards.IntuneTemplate.$id" -FieldValue $state -TenantFilter $Tenant + if ($Settings.report -or $Settings.remediate) { + $id = $CompareResult.templateId + + $CurrentValue = @{ + displayName = $CompareResult.displayname + description = $CompareResult.description + isCompliant = if ($CompareResult.compare) { $false } else { $true } + } + $ExpectedValue = @{ + displayName = $CompareResult.displayname + description = $CompareResult.description + isCompliant = $true } + Set-CIPPStandardsCompareField -FieldName "standards.IntuneTemplate.$id" -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant #Add-CIPPBPAField -FieldName "policy-$id" -FieldValue $Compare -StoreAs bool -Tenant $tenant } } From 733e1b98751da40e5673f2a710a22e47d1884901 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Mon, 9 Feb 2026 12:26:52 -0500 Subject: [PATCH 356/503] ensure bulk requests are always arrays --- .../Endpoint/MEM/Invoke-ListAppProtectionPolicies.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListAppProtectionPolicies.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListAppProtectionPolicies.ps1 index ef6b59fec416..749c2984ad75 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListAppProtectionPolicies.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListAppProtectionPolicies.ps1 @@ -34,7 +34,7 @@ } ) - $BulkResults = New-GraphBulkRequest -Requests $BulkRequests -tenantid $TenantFilter + $BulkResults = New-GraphBulkRequest -Requests @($BulkRequests) -tenantid $TenantFilter # Extract groups for resolving assignment names $Groups = ($BulkResults | Where-Object { $_.id -eq 'Groups' }).body.value @@ -66,7 +66,7 @@ } } - $ManagedAppPoliciesBulkResults = New-GraphBulkRequest -Requests $ManagedAppPoliciesBulkRequests -tenantid $TenantFilter + $ManagedAppPoliciesBulkResults = New-GraphBulkRequest -Requests @($ManagedAppPoliciesBulkRequests) -tenantid $TenantFilter # Do this horriblenes as a workaround, as the results dont return with a odata.type property $ManagedAppPolicies = $ManagedAppPoliciesBulkResults | ForEach-Object { $URLName = $_.id From c9b0b1e7ad1f1cd4ab8986e11eaa969ad4d91354 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Mon, 9 Feb 2026 13:16:15 -0500 Subject: [PATCH 357/503] Improve MX record change detection and logging Add robust handling for missing or empty MX data and wrap per-domain comparison in try/catch to prevent runtime errors. Normalize PreviousRecords and CurrentRecords to arrays, use Compare-Object for differences, and emit informational messages when records are newly added or removed. Also ensure CurrentRecords is an array when updating the cache. These changes prevent failures when ActualMXRecords/Hostname are absent and provide clearer diagnostics. --- .../Alerts/Get-CIPPAlertMXRecordChanged.ps1 | 31 +++++++++++++++---- 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertMXRecordChanged.ps1 b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertMXRecordChanged.ps1 index 31811b78ed68..8abf8bb43747 100644 --- a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertMXRecordChanged.ps1 +++ b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertMXRecordChanged.ps1 @@ -17,16 +17,35 @@ function Get-CIPPAlertMXRecordChanged { $PreviousResults = Get-CIPPAzDataTableEntity @CacheTable -Filter "PartitionKey eq '$TenantFilter'" $ChangedDomains = foreach ($Domain in $DomainData) { - $PreviousDomain = $PreviousResults | Where-Object { $_.Domain -eq $Domain.Domain } - $PreviousRecords = $PreviousDomain.ActualMXRecords -split ',' | Sort-Object - $CurrentRecords = $Domain.ActualMXRecords.Hostname | Sort-Object - if ($PreviousDomain -and $PreviousRecords -ne $CurrentRecords) { - "$($Domain.Domain): MX records changed from [$($PreviousRecords -join ', ')] to [$($CurrentRecords -join ', ')]" + try { + $PreviousDomain = $PreviousResults | Where-Object { $_.Domain -eq $Domain.Domain } + $PreviousRecords = if ($PreviousDomain.ActualMXRecords) { @($PreviousDomain.ActualMXRecords -split ',' | Sort-Object) } else { @() } + $CurrentRecords = if ($Domain.ActualMXRecords.Hostname) { @($Domain.ActualMXRecords.Hostname | Sort-Object) } else { @() } + + # Only compare if both have records + $Differences = $null + if ($PreviousRecords.Count -gt 0 -and $CurrentRecords.Count -gt 0) { + $Differences = Compare-Object -ReferenceObject $PreviousRecords -DifferenceObject $CurrentRecords + } + + if ($PreviousRecords.Count -eq 0 -and $CurrentRecords.Count -gt 0) { + Write-Information "New MX records detected for domain $($Domain.Domain): $($CurrentRecords -join ', ')" + $Differences = 'NewRecords' + } elseif ($PreviousRecords.Count -gt 0 -and $CurrentRecords.Count -eq 0) { + Write-Information "All MX records removed for domain $($Domain.Domain). Previous records were: $($PreviousRecords -join ', ')" + $Differences = 'RemovedRecords' + } + + if ($Differences) { + "$($Domain.Domain): MX records changed from [$($PreviousRecords -join ', ')] to [$($CurrentRecords -join ', ')]" + } + } catch { + Write-Information "Error checking domain $($Domain.Domain): $($_.Exception.Message)" } } # Update cache with current data foreach ($Domain in $DomainData) { - $CurrentRecords = $Domain.ActualMXRecords.Hostname | Sort-Object + $CurrentRecords = @($Domain.ActualMXRecords.Hostname | Sort-Object) $CacheEntity = @{ PartitionKey = [string]$TenantFilter RowKey = [string]$Domain.Domain From 79d7d7a89a3eb1e25e8ba8403491a8e87e374e48 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Mon, 9 Feb 2026 13:51:10 -0500 Subject: [PATCH 358/503] fix ca policy location lookup --- Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 b/Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 index 250b31296dd8..1f908ff4e0f2 100644 --- a/Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 +++ b/Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 @@ -189,9 +189,9 @@ function New-CIPPCAPolicy { Write-LogMessage -Tenant $TenantFilter -Headers $Headers -API $APINAME -message "Matched a CA policy with the existing Named Location: $($location.displayName)" -Sev 'Info' } [pscustomobject]@{ - id = $ExistingLocation.id - name = $ExistingLocation.displayName - templateId = $location.id + id = $ExistingLocation.id + name = $ExistingLocation.displayName + templateId = $location.id } } else { if ($location.countriesAndRegions) { $location.countriesAndRegions = @($location.countriesAndRegions) } @@ -224,7 +224,9 @@ function New-CIPPCAPolicy { if (!$lookup) { continue } Write-Information "Replacing named location - $location" $index = [array]::IndexOf($JSONobj.conditions.locations.includeLocations, $location) - $JSONobj.conditions.locations.includeLocations[$index] = $lookup.id + if ($lookup.id) { + $JSONobj.conditions.locations.includeLocations[$index] = $lookup.id + } } foreach ($location in $JSONobj.conditions.locations.excludeLocations) { @@ -233,7 +235,9 @@ function New-CIPPCAPolicy { if (!$lookup) { continue } Write-Information "Replacing named location - $location" $index = [array]::IndexOf($JSONobj.conditions.locations.excludeLocations, $location) - $JSONobj.conditions.locations.excludeLocations[$index] = $lookup.id + if ($lookup.id) { + $JSONobj.conditions.locations.excludeLocations[$index] = $lookup.id + } } switch ($ReplacePattern) { 'none' { From d8e734bcbab6fcfe13eaa883bc6ef797ce4e8160 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Mon, 9 Feb 2026 14:03:45 -0500 Subject: [PATCH 359/503] fix renaming templates --- .../HTTP Functions/CIPP/Core/Invoke-ExecEditTemplate.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ExecEditTemplate.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ExecEditTemplate.ps1 index 5b47b16aceeb..f26daa540b5b 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ExecEditTemplate.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ExecEditTemplate.ps1 @@ -31,7 +31,7 @@ function Invoke-ExecEditTemplate { if ($Request.Body.parsedRAWJson) { $RawJSON = ConvertTo-Json -Compress -Depth 100 -InputObject $Request.Body.parsedRAWJson } else { - $RawJSON = $OriginalJSON + $RawJSON = $TemplateData.RAWJson } $IntuneTemplate = @{ From 4781956207b866850770c2ee4f0afa41535ae0f9 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Mon, 9 Feb 2026 15:58:34 -0500 Subject: [PATCH 360/503] Clear rerun cache before scheduling task Invoke-AddScheduledItem: when an existing ScheduledTask is found, call Test-CIPPRerun with Clear=$true for that tenant/API before calling Add-CIPPScheduledTask -RunNow to ensure any stale rerun cache is cleared. Test-CIPPRerun: avoid computing the estimated interval when Clear/ClearAll flags are present; and build a tighter Azure Table filter by optionally scoping PartitionKey (unless AllTenants) and using a RowKey ge/le range (with '~' as high ascii) to restrict results to the specific Type_API. These changes prevent stale rerun entries from blocking immediate runs and reduce table query scope for better performance. --- .../Scheduler/Invoke-AddScheduledItem.ps1 | 8 +++++ Modules/CIPPCore/Public/Test-CIPPRerun.ps1 | 31 ++++++++++++------- 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Scheduler/Invoke-AddScheduledItem.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Scheduler/Invoke-AddScheduledItem.ps1 index 18aecda3ab90..b076401bab5d 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Scheduler/Invoke-AddScheduledItem.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Scheduler/Invoke-AddScheduledItem.ps1 @@ -20,7 +20,15 @@ function Invoke-AddScheduledItem { $Table = Get-CIPPTable -TableName 'ScheduledTasks' $Filter = "PartitionKey eq 'ScheduledTask' and RowKey eq '$($Request.Body.RowKey)'" $ExistingTask = (Get-CIPPAzDataTableEntity @Table -Filter $Filter) + if ($ExistingTask) { + $RerunParams = @{ + TenantFilter = $ExistingTask.Tenant + Type = 'ScheduledTask' + API = $Request.Body.RowKey + Clear = $true + } + $null = Test-CIPPRerun @RerunParams $Result = Add-CIPPScheduledTask -RowKey $Request.Body.RowKey -RunNow -Headers $Request.Headers } else { $Result = "Task with id $($Request.Body.RowKey) does not exist" diff --git a/Modules/CIPPCore/Public/Test-CIPPRerun.ps1 b/Modules/CIPPCore/Public/Test-CIPPRerun.ps1 index 57d8d9b3a603..f83f7e649a23 100644 --- a/Modules/CIPPCore/Public/Test-CIPPRerun.ps1 +++ b/Modules/CIPPCore/Public/Test-CIPPRerun.ps1 @@ -13,16 +13,18 @@ function Test-CIPPRerun { ) $RerunTable = Get-CIPPTable -tablename 'RerunCache' - # Use custom interval if provided, otherwise use type-based defaults - if ($Interval -gt 0) { - $EstimatedDifference = $Interval - } else { - $EstimatedDifference = switch ($Type) { - 'Standard' { 9800 } # 2 hours 45 minutes ish. - 'BPA' { 85000 } # 24 hours ish. - 'CippTests' { 85000 } # 24 hours ish. - 'ExchangeMonitor' { 3500 } #about an hour - default { throw "Unknown type: $Type" } + if (!$ClearAll.IsPresent -and !$Clear.IsPresent) { + # Use custom interval if provided, otherwise use type-based defaults + if ($Interval -gt 0) { + $EstimatedDifference = $Interval + } else { + $EstimatedDifference = switch ($Type) { + 'Standard' { 9800 } # 2 hours 45 minutes ish. + 'BPA' { 85000 } # 24 hours ish. + 'CippTests' { 85000 } # 24 hours ish. + 'ExchangeMonitor' { 3500 } #about an hour + default { throw "Unknown type: $Type" } + } } } @@ -31,7 +33,14 @@ function Test-CIPPRerun { $EstimatedNextRun = $CurrentUnixTime + $EstimatedDifference try { - $RerunData = Get-CIPPAzDataTableEntity @RerunTable -filter "PartitionKey eq '$($TenantFilter)'" | Where-Object { $_.RowKey -match "^$($Type)_$($API)" } + $Filters = [System.Collections.Generic.List[string]]::new() + if ($TenantFilter -ne 'AllTenants') { + $Filters.Add("PartitionKey eq '$TenantFilter'") + } + $Filters.Add("RowKey ge '$($Type)_$($API)' and RowKey le '$($Type)_$($API)~'") # ~ is the highest ascii character, this ensures we only get entries for this API. + $FilterString = [string]::Join(' and ', $Filters) + + $RerunData = Get-CIPPAzDataTableEntity @RerunTable -filter $FilterString if ($ClearAll.IsPresent) { $AllRerunData = Get-CIPPAzDataTableEntity @RerunTable if ($AllRerunData) { From ebe90179626cf3647cd3c4052c26145ba8fc70f7 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Mon, 9 Feb 2026 15:59:46 -0500 Subject: [PATCH 361/503] Use generated GUID and add JSON logging In Import-CommunityTemplate.ps1 generate a new GUID ($id) for imported entities and use it instead of Template.id when setting GUID/RowKey for Group, CATemplate and IntuneTemplate entries. Remove redundant assignments of $id from Template.id, exclude the 'templateId' property from template selection, and add Write-Information logs showing the raw JSON before ID replacement and the final entity for easier debugging. This ensures unique IDs for imported templates and improves traceability during import. --- .../Public/Tools/Import-CommunityTemplate.ps1 | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/Modules/CIPPCore/Public/Tools/Import-CommunityTemplate.ps1 b/Modules/CIPPCore/Public/Tools/Import-CommunityTemplate.ps1 index 9ea1c1b98a76..d099f2a280f8 100644 --- a/Modules/CIPPCore/Public/Tools/Import-CommunityTemplate.ps1 +++ b/Modules/CIPPCore/Public/Tools/Import-CommunityTemplate.ps1 @@ -68,26 +68,26 @@ function Import-CommunityTemplate { $Template | Add-Member -MemberType NoteProperty -Name Source -Value $Source -Force Add-CIPPAzDataTableEntity @Table -Entity $Template -Force } else { + $id = [guid]::NewGuid().ToString() if ($Template.mailNickname) { $Type = 'Group' } if ($Template.'@odata.type' -like '*conditionalAccessPolicy*') { $Type = 'ConditionalAccessPolicy' } Write-Host "The type is $Type" switch -Wildcard ($Type) { - '*Group*' { $RawJsonObj = [PSCustomObject]@{ Displayname = $Template.displayName Description = $Template.Description MembershipRules = $Template.membershipRule username = $Template.mailNickname - GUID = $Template.id + GUID = $id groupType = 'generic' } | ConvertTo-Json -Depth 100 $entity = @{ JSON = "$RawJsonObj" PartitionKey = 'GroupTemplate' SHA = $SHA - GUID = $Template.id - RowKey = $Template.id + GUID = $id + RowKey = $id Source = $Source } Add-CIPPAzDataTableEntity @Table -Entity $entity -Force @@ -99,8 +99,7 @@ function Import-CommunityTemplate { $NonEmptyProperties = $_.psobject.Properties | Where-Object { $null -ne $_.Value } | Select-Object -ExpandProperty Name $_ | Select-Object -Property $NonEmptyProperties } - $id = $Template.id - $Template = $Template | Select-Object * -ExcludeProperty lastModifiedDateTime, 'assignments', '#microsoft*', '*@odata.navigationLink', '*@odata.associationLink', '*@odata.context', 'ScopeTagIds', 'supportsScopeTags', 'createdDateTime', '@odata.id', '@odata.editLink', '*odata.type', 'roleScopeTagIds@odata.type', createdDateTime, 'createdDateTime@odata.type' + $Template = $Template | Select-Object * -ExcludeProperty lastModifiedDateTime, 'assignments', '#microsoft*', '*@odata.navigationLink', '*@odata.associationLink', '*@odata.context', 'ScopeTagIds', 'supportsScopeTags', 'createdDateTime', '@odata.id', '@odata.editLink', '*odata.type', 'roleScopeTagIds@odata.type', createdDateTime, 'createdDateTime@odata.type', 'templateId' Remove-ODataProperties -Object $Template $LocationInfo = [system.collections.generic.list[object]]::new() @@ -117,6 +116,8 @@ function Import-CommunityTemplate { } $RawJson = ConvertTo-Json -InputObject $Template -Depth 100 -Compress + + Write-Information "Raw JSON before ID replacement: $RawJson" #Replace the ids with the displayname by using the migration table, this is a simple find and replace each instance in the JSON. $MigrationTable.objects | ForEach-Object { if ($RawJson -match $_.ID) { @@ -128,10 +129,12 @@ function Import-CommunityTemplate { JSON = "$RawJson" PartitionKey = 'CATemplate' SHA = $SHA - GUID = $ID - RowKey = $ID + GUID = $id + RowKey = $id Source = $Source } + Write-Information "Final entity: $($entity | ConvertTo-Json -Depth 10)" + Add-CIPPAzDataTableEntity @Table -Entity $entity -Force break } @@ -145,7 +148,6 @@ function Import-CommunityTemplate { '*managedAppPolicies*' { 'AppProtection' } '*deviceAppManagement*' { 'AppProtection' } } - $id = $Template.id $RawJson = $Template | Select-Object * -ExcludeProperty id, lastModifiedDateTime, 'assignments', '#microsoft*', '*@odata.navigationLink', '*@odata.associationLink', '*@odata.context', 'ScopeTagIds', 'supportsScopeTags', 'createdDateTime', '@odata.id', '@odata.editLink', 'lastModifiedDateTime@odata.type', 'roleScopeTagIds@odata.type', createdDateTime, 'createdDateTime@odata.type' Remove-ODataProperties -Object $RawJson $RawJson = $RawJson | ConvertTo-Json -Depth 100 -Compress @@ -156,15 +158,15 @@ function Import-CommunityTemplate { Description = $Template.Description RAWJson = $RawJson Type = $URLName - GUID = $ID + GUID = $id } | ConvertTo-Json -Depth 100 -Compress $entity = @{ JSON = "$RawJsonObj" PartitionKey = 'IntuneTemplate' SHA = $SHA - GUID = $ID - RowKey = $ID + GUID = $id + RowKey = $id Source = $Source } From 8b30e70267f6099e47c45dc9d3b84a13014aa18d Mon Sep 17 00:00:00 2001 From: John Duprey Date: Mon, 9 Feb 2026 18:13:50 -0500 Subject: [PATCH 362/503] Handle guest users and userType in MFA state Include userType in Graph query and user objects, and add handling for policies that target guests or external users. Introduce GuestUserPolicies (and use generic lists) to collect guest-targeting policies, evaluate include/exclude guest settings (guestOrExternalUserTypes) and exclude groups when calculating conditional access coverage for Guest users. Also add defensive null-checks throughout, ensure group include/exclude checks are robust, and expose UserType in the returned user record. Minor cleanup: use single quotes for a license error message and normalize list initializations. --- Modules/CIPPCore/Public/Get-CIPPMFAState.ps1 | 96 ++++++++++++++++++-- 1 file changed, 87 insertions(+), 9 deletions(-) diff --git a/Modules/CIPPCore/Public/Get-CIPPMFAState.ps1 b/Modules/CIPPCore/Public/Get-CIPPMFAState.ps1 index 193a2b1ca532..ee6ee2ea96a1 100644 --- a/Modules/CIPPCore/Public/Get-CIPPMFAState.ps1 +++ b/Modules/CIPPCore/Public/Get-CIPPMFAState.ps1 @@ -6,7 +6,7 @@ function Get-CIPPMFAState { $Headers ) #$PerUserMFAState = Get-CIPPPerUserMFA -TenantFilter $TenantFilter -AllUsers $true - $users = foreach ($user in (New-GraphGetRequest -uri 'https://graph.microsoft.com/v1.0/users?$top=999&$select=id,UserPrincipalName,DisplayName,accountEnabled,assignedLicenses,perUserMfaState' -tenantid $TenantFilter)) { + $users = foreach ($user in (New-GraphGetRequest -uri 'https://graph.microsoft.com/v1.0/users?$top=999&$select=id,UserPrincipalName,DisplayName,accountEnabled,assignedLicenses,perUserMfaState,userType' -tenantid $TenantFilter)) { [PSCustomObject]@{ UserPrincipalName = $user.UserPrincipalName isLicensed = [boolean]$user.assignedLicenses.Count @@ -14,6 +14,7 @@ function Get-CIPPMFAState { DisplayName = $user.DisplayName ObjectId = $user.id perUserMfaState = $user.perUserMfaState + UserType = $user.userType } } @@ -22,7 +23,8 @@ function Get-CIPPMFAState { $CASuccess = $false $CAError = $null $PolicyTable = @{} - $AllUserPolicies = @() + $AllUserPolicies = [System.Collections.Generic.List[object]]::new() + $GuestUserPolicies = [System.Collections.Generic.List[object]]::new() $UserGroupMembership = @{} $UserExcludeGroupMembership = @{} $GroupNameLookup = @{} @@ -47,7 +49,7 @@ function Get-CIPPMFAState { } catch { $CAState.Add('Not Licensed for Conditional Access') | Out-Null $MFARegistration = $null - $CAError = "MFA registration not available - licensing required for Conditional Access reporting" + $CAError = 'MFA registration not available - licensing required for Conditional Access reporting' if ($_.Exception.Message -ne "Tenant is not a B2C tenant and doesn't have premium licenses") { $Errors.Add(@{Step = 'MFARegistration'; Message = $_.Exception.Message }) } @@ -76,7 +78,7 @@ function Get-CIPPMFAState { if ($RequiresMFA) { # Handle user assignments - if ($Policy.conditions.users.includeUsers -ne $null) { + if ($null -ne $Policy.conditions.users.includeUsers) { # Check if "All" is included if ($Policy.conditions.users.includeUsers -contains 'All') { $AllUserPolicies.Add($Policy) @@ -90,15 +92,20 @@ function Get-CIPPMFAState { } } + # Handle guest/external user assignments + if ($null -ne $Policy.conditions.users.includeGuestsOrExternalUsers) { + $GuestUserPolicies.Add($Policy) + } + # Collect groups to resolve - if ($Policy.conditions.users.includeGroups -ne $null -and $Policy.conditions.users.includeGroups.Count -gt 0) { + if ($null -ne $Policy.conditions.users.includeGroups -and $Policy.conditions.users.includeGroups.Count -gt 0) { foreach ($GroupId in $Policy.conditions.users.includeGroups) { [void]$GroupsToResolve.Add($GroupId) } } # Collect exclude groups to resolve - if ($Policy.conditions.users.excludeGroups -ne $null -and $Policy.conditions.users.excludeGroups.Count -gt 0) { + if ($null -ne $Policy.conditions.users.excludeGroups -and $Policy.conditions.users.excludeGroups.Count -gt 0) { foreach ($GroupId in $Policy.conditions.users.excludeGroups) { [void]$ExcludeGroupsToResolve.Add($GroupId) } @@ -181,7 +188,7 @@ function Get-CIPPMFAState { } # Now add policies to users based on group membership - foreach ($Policy in $CAPolicies | Where-Object { $_.conditions.users.includeGroups -ne $null -and $_.conditions.users.includeGroups.Count -gt 0 }) { + foreach ($Policy in $CAPolicies | Where-Object { $null -ne $_.conditions.users.includeGroups -and $_.conditions.users.includeGroups.Count -gt 0 }) { # Check if this policy requires MFA $RequiresMFA = $false if ($Policy.grantControls.builtInControls -contains 'mfa') { @@ -235,6 +242,65 @@ function Get-CIPPMFAState { $GraphRequest = $Users | ForEach-Object { $UserCAState = [System.Collections.Generic.List[object]]::new() + # Check if user is a guest and add guest-targeting policies + if ($_.UserType -eq 'Guest') { + foreach ($Policy in $GuestUserPolicies) { + $GuestConfig = $Policy.conditions.users.includeGuestsOrExternalUsers + $IsGuestIncluded = $false + + if ($null -ne $GuestConfig -and $null -ne $GuestConfig.guestOrExternalUserTypes) { + $GuestTypes = $GuestConfig.guestOrExternalUserTypes -split ',' + # Check if policy includes all guests or specifically internal guests + if ($GuestTypes -contains 'internalGuest') { + $IsGuestIncluded = $true + } + } + + # Check if guests are explicitly excluded from the policy + $ExcludeGuestConfig = $Policy.conditions.users.excludeGuestsOrExternalUsers + if ($null -ne $ExcludeGuestConfig -and $null -ne $ExcludeGuestConfig.guestOrExternalUserTypes) { + $ExcludeGuestTypes = $ExcludeGuestConfig.guestOrExternalUserTypes -split ',' + if ($ExcludeGuestTypes -contains 'internalGuest') { + $IsGuestIncluded = $false + } + } + + if ($IsGuestIncluded) { + # Check if user is excluded directly or via group + $IsExcluded = $Policy.conditions.users.excludeUsers -contains $_.ObjectId + $ExcludedViaGroup = $null + + # Check exclude groups + if (-not $IsExcluded -and $null -ne $Policy.conditions.users.excludeGroups -and $Policy.conditions.users.excludeGroups.Count -gt 0) { + if ($UserExcludeGroupMembership.ContainsKey($_.ObjectId)) { + foreach ($ExcludeGroupId in $Policy.conditions.users.excludeGroups) { + if ($UserExcludeGroupMembership[$_.ObjectId].Contains($ExcludeGroupId)) { + $IsExcluded = $true + $ExcludedViaGroup = if ($GroupNameLookup.ContainsKey($ExcludeGroupId)) { + $GroupNameLookup[$ExcludeGroupId] + } else { + $ExcludeGroupId + } + break + } + } + } + } + + $PolicyObj = [PSCustomObject]@{ + DisplayName = $Policy.displayName + UserIncluded = -not $IsExcluded + AllApps = ($Policy.conditions.applications.includeApplications -contains 'All') + PolicyState = $Policy.state + } + if ($ExcludedViaGroup) { + $PolicyObj | Add-Member -NotePropertyName 'ExcludedViaGroup' -NotePropertyValue $ExcludedViaGroup + } + $UserCAState.Add($PolicyObj) + } + } + } + # Add policies that apply to this specific user if ($PolicyTable.ContainsKey($_.ObjectId)) { foreach ($Policy in $PolicyTable[$_.ObjectId]) { @@ -243,7 +309,7 @@ function Get-CIPPMFAState { $ExcludedViaGroup = $null # Check exclude groups - if (-not $IsExcluded -and $Policy.conditions.users.excludeGroups -ne $null -and $Policy.conditions.users.excludeGroups.Count -gt 0) { + if (-not $IsExcluded -and $null -ne $Policy.conditions.users.excludeGroups -and $Policy.conditions.users.excludeGroups.Count -gt 0) { if ($UserExcludeGroupMembership.ContainsKey($_.ObjectId)) { foreach ($ExcludeGroupId in $Policy.conditions.users.excludeGroups) { if ($UserExcludeGroupMembership[$_.ObjectId].Contains($ExcludeGroupId)) { @@ -278,8 +344,19 @@ function Get-CIPPMFAState { $IsExcluded = $Policy.conditions.users.excludeUsers -contains $_.ObjectId $ExcludedViaGroup = $null + # Check if guests are excluded from this "All users" policy + if (-not $IsExcluded -and $_.UserType -eq 'Guest') { + $ExcludeGuestConfig = $Policy.conditions.users.excludeGuestsOrExternalUsers + if ($null -ne $ExcludeGuestConfig -and $null -ne $ExcludeGuestConfig.guestOrExternalUserTypes) { + $ExcludeGuestTypes = $ExcludeGuestConfig.guestOrExternalUserTypes -split ',' + if ($ExcludeGuestTypes -contains 'internalGuest') { + $IsExcluded = $true + } + } + } + # Check exclude groups - if (-not $IsExcluded -and $Policy.conditions.users.excludeGroups -ne $null -and $Policy.conditions.users.excludeGroups.Count -gt 0) { + if (-not $IsExcluded -and $null -ne $Policy.conditions.users.excludeGroups -and $Policy.conditions.users.excludeGroups.Count -gt 0) { if ($UserExcludeGroupMembership.ContainsKey($_.ObjectId)) { foreach ($ExcludeGroupId in $Policy.conditions.users.excludeGroups) { if ($UserExcludeGroupMembership[$_.ObjectId].Contains($ExcludeGroupId)) { @@ -344,6 +421,7 @@ function Get-CIPPMFAState { CAPolicies = @($UserCAState) CoveredBySD = $SecureDefaultsState IsAdmin = $IsAdmin + UserType = $_.UserType RowKey = [string]($_.UserPrincipalName).replace('#', '') PartitionKey = 'users' } From 7b08cc05724ee5694e19d3954aa38c090007ccb4 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Mon, 9 Feb 2026 16:37:54 -0800 Subject: [PATCH 363/503] Revive Legacy Report Addin --- ...ke-CIPPStandardLegacyEmailReportAddins.ps1 | 78 ++++++++++--------- 1 file changed, 40 insertions(+), 38 deletions(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardLegacyEmailReportAddins.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardLegacyEmailReportAddins.ps1 index 627c88eabc0c..0876db936f1a 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardLegacyEmailReportAddins.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardLegacyEmailReportAddins.ps1 @@ -31,8 +31,6 @@ function Invoke-CIPPStandardLegacyEmailReportAddins { param($Tenant, $Settings) - #Deprecated, immmediate return - return $true # Define the legacy add-ins to remove $LegacyAddins = @( @{ @@ -48,62 +46,60 @@ function Invoke-CIPPStandardLegacyEmailReportAddins { ) try { - $CurrentApps = New-GraphGetRequest -scope 'https://admin.microsoft.com/.default' -TenantID $Tenant -Uri 'https://admin.microsoft.com/fd/addins/api/apps?workloads=AzureActiveDirectory,WXPO,MetaOS,Teams,SharePoint' - $InstalledApps = $CurrentApps.apps + $CurrentApps = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/applications&select=addins" -TenantID $Tenant + + # Filter to only applications that have the legacy add-ins we're looking for + $LegacyProductIds = $LegacyAddins | ForEach-Object { $_.ProductId } + $InstalledApps = $CurrentApps | Where-Object { + $app = $_ + $app.addIns | Where-Object { $_.id -in $LegacyProductIds } + } + Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Retrieved $($InstalledApps.Count) applications with legacy add-ins" -Sev Info } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the installed add-ins for $Tenant. Error: $ErrorMessage" -Sev Error return } - # Check which legacy add-ins are currently installed - $AddinsToRemove = [System.Collections.Generic.List[PSCustomObject]]::new() $InstalledLegacyAddins = [System.Collections.Generic.List[string]]::new() - foreach ($LegacyAddin in $LegacyAddins) { - $InstalledAddin = $InstalledApps | Where-Object { $_.assetId -eq $LegacyAddin.AssetId -or $_.productId -eq $LegacyAddin.ProductId } - if ($InstalledAddin) { - $InstalledLegacyAddins.Add($LegacyAddin.Name) - $AddinsToRemove.Add([PSCustomObject]@{ - AppsourceAssetID = $LegacyAddin.AssetId - ProductID = $LegacyAddin.ProductId - Command = 'UNDEPLOY' - Workload = 'WXPO' - }) + foreach ($App in $InstalledApps) { + foreach ($Addin in $App.addIns) { + $LegacyAddin = $LegacyAddins | Where-Object { $_.ProductId -eq $Addin.id } + if ($LegacyAddin) { + $InstalledLegacyAddins.Add($LegacyAddin.Name) + } } } - $StateIsCorrect = ($AddinsToRemove.Count -eq 0) + $StateIsCorrect = ($InstalledApps.Count -eq 0) $RemediationPerformed = $false if ($Settings.remediate -eq $true) { if ($StateIsCorrect -eq $true) { Write-LogMessage -API 'Standards' -Tenant $Tenant -Message 'Legacy Email Report Add-ins are already removed.' -Sev Info } else { - foreach ($AddinToRemove in $AddinsToRemove) { + foreach ($App in $InstalledApps) { try { - $Body = @{ - Locale = 'en-US' - WorkloadManagementList = @($AddinToRemove) - } | ConvertTo-Json -Depth 10 -Compress - + # Delete the application object using Graph API $GraphRequest = @{ - tenantID = $Tenant - uri = 'https://admin.microsoft.com/fd/addins/api/apps' - scope = 'https://admin.microsoft.com/.default' - AsApp = $false - Type = 'POST' - ContentType = 'application/json; charset=utf-8' - Body = $Body + tenantID = $Tenant + uri = "https://graph.microsoft.com/beta/applications/$($App.id)" + Type = 'DELETE' } - $Response = New-GraphPostRequest @GraphRequest - $AddinName = ($LegacyAddins | Where-Object { $_.AssetId -eq $AddinToRemove.AppsourceAssetID }).Name - Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Successfully initiated removal of $AddinName add-in" -Sev Info + $null = New-GraphPostRequest @GraphRequest + + $RemovedAddins = foreach ($Addin in $App.addIns) { + $LegacyAddin = $LegacyAddins | Where-Object { $_.ProductId -eq $Addin.id } + if ($LegacyAddin) { $LegacyAddin.Name } + } + + $RemovedAddinsText = $RemovedAddins -join ', ' + Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Successfully removed legacy add-in(s): $RemovedAddinsText (deleted application $($App.displayName))" -Sev Info $RemediationPerformed = $true } catch { - $AddinName = ($LegacyAddins | Where-Object { $_.AssetId -eq $AddinToRemove.AppsourceAssetID }).Name - Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Failed to remove $AddinName add-in" -Sev Error -LogData $_ + Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Failed to remove application $($App.displayName)" -Sev Error -LogData $_ } } } @@ -112,13 +108,19 @@ function Invoke-CIPPStandardLegacyEmailReportAddins { # If we performed remediation and need to report/alert, get fresh state if ($RemediationPerformed -and ($Settings.alert -eq $true -or $Settings.report -eq $true)) { try { - $FreshApps = New-GraphGetRequest -scope 'https://admin.microsoft.com/.default' -TenantID $Tenant -Uri 'https://admin.microsoft.com/fd/addins/api/apps?workloads=AzureActiveDirectory,WXPO,MetaOS,Teams,SharePoint' - $FreshInstalledApps = $FreshApps.apps + $FreshApps = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/applications&select=addins" -TenantID $Tenant + $LegacyProductIds = $LegacyAddins | ForEach-Object { $_.ProductId } + $FreshInstalledApps = $FreshApps | Where-Object { + $app = $_ + $app.addIns | Where-Object { $_.id -in $LegacyProductIds } + } # Check fresh state $FreshInstalledLegacyAddins = [System.Collections.Generic.List[string]]::new() foreach ($LegacyAddin in $LegacyAddins) { - $InstalledAddin = $FreshInstalledApps | Where-Object { $_.assetId -eq $LegacyAddin.AssetId -or $_.productId -eq $LegacyAddin.ProductId } + $InstalledAddin = $FreshInstalledApps | Where-Object { + $_.addIns | Where-Object { $_.id -eq $LegacyAddin.ProductId } + } if ($InstalledAddin) { $FreshInstalledLegacyAddins.Add($LegacyAddin.Name) } From 4e8c7a9b1680cb4f9a8fb5ae34a3b22ab7c51351 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Mon, 9 Feb 2026 21:01:43 -0800 Subject: [PATCH 364/503] Update Get-CIPPAlertMFAAdmins.ps1 --- .../CIPPCore/Public/Alerts/Get-CIPPAlertMFAAdmins.ps1 | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertMFAAdmins.ps1 b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertMFAAdmins.ps1 index 32170d84b9d0..8cea32a93caa 100644 --- a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertMFAAdmins.ps1 +++ b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertMFAAdmins.ps1 @@ -20,6 +20,15 @@ function Get-CIPPAlertMFAAdmins { if (!$DuoActive) { $Users = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/reports/authenticationMethods/userRegistrationDetails?`$top=999&filter=IsAdmin eq true and isMfaRegistered eq false and userType eq 'member'&`$select=id,userDisplayName,userPrincipalName,lastUpdatedDateTime,isMfaRegistered,IsAdmin" -tenantid $($TenantFilter) -AsApp $true | Where-Object { $_.userDisplayName -ne 'On-Premises Directory Synchronization Service Account' } + + # Filter out JIT admins if any users were found + if ($Users) { + $Schema = Get-CIPPSchemaExtensions | Where-Object { $_.id -match '_cippUser' } | Select-Object -First 1 + $JITAdmins = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/users?`$select=id,$($Schema.id)&`$filter=$($Schema.id)/jitAdminEnabled eq true" -tenantid $TenantFilter -ComplexFilter + $JITAdminIds = $JITAdmins.id + $Users = $Users | Where-Object { $_.id -notin $JITAdminIds } + } + if ($Users.UserPrincipalName) { $AlertData = foreach ($user in $Users) { [PSCustomObject]@{ From cc5c2c8c2497c8a3147060fd990fc7f864335f64 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Tue, 10 Feb 2026 00:43:54 -0500 Subject: [PATCH 365/503] Add ParticipantGiveRequestControl to Teams policy Introduce the AllowParticipantGiveRequestControl setting to the Teams Global Meeting Policy standard. Updates include help/docs text, a new UI switch, inclusion in the Set-CsTeamsMeetingPolicy example, reading the property from Get-CsTeamsMeetingPolicy, adding it to the current/expected value maps, and including it in the remediation payload and comparison logic so the standard can correctly evaluate and enforce this setting. --- ...nvoke-CIPPStandardTeamsGlobalMeetingPolicy.ps1 | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsGlobalMeetingPolicy.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsGlobalMeetingPolicy.ps1 index 7c30eb607332..73b41f0eb62c 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsGlobalMeetingPolicy.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsGlobalMeetingPolicy.ps1 @@ -7,8 +7,8 @@ function Invoke-CIPPStandardTeamsGlobalMeetingPolicy { .SYNOPSIS (Label) Define Global Meeting Policy for Teams .DESCRIPTION - (Helptext) Defines the CIS recommended global meeting policy for Teams. This includes AllowAnonymousUsersToJoinMeeting, AllowAnonymousUsersToStartMeeting, AutoAdmittedUsers, AllowPSTNUsersToBypassLobby, MeetingChatEnabledType, DesignatedPresenterRoleMode, AllowExternalParticipantGiveRequestControl - (DocsDescription) Defines the CIS recommended global meeting policy for Teams. This includes AllowAnonymousUsersToJoinMeeting, AllowAnonymousUsersToStartMeeting, AutoAdmittedUsers, AllowPSTNUsersToBypassLobby, MeetingChatEnabledType, DesignatedPresenterRoleMode, AllowExternalParticipantGiveRequestControl + (Helptext) Defines the CIS recommended global meeting policy for Teams. This includes AllowAnonymousUsersToJoinMeeting, AllowAnonymousUsersToStartMeeting, AutoAdmittedUsers, AllowPSTNUsersToBypassLobby, MeetingChatEnabledType, DesignatedPresenterRoleMode, AllowExternalParticipantGiveRequestControl, AllowParticipantGiveRequestControl + (DocsDescription) Defines the CIS recommended global meeting policy for Teams. This includes AllowAnonymousUsersToJoinMeeting, AllowAnonymousUsersToStartMeeting, AutoAdmittedUsers, AllowPSTNUsersToBypassLobby, MeetingChatEnabledType, DesignatedPresenterRoleMode, AllowExternalParticipantGiveRequestControl, AllowParticipantGiveRequestControl .NOTES CAT Teams Standards @@ -27,12 +27,13 @@ function Invoke-CIPPStandardTeamsGlobalMeetingPolicy { {"type":"autoComplete","required":false,"multiple":false,"creatable":false,"name":"standards.TeamsGlobalMeetingPolicy.AutoAdmittedUsers","label":"Who can bypass the lobby?","helperText":"If left blank, the current value will not be changed.","options":[{"label":"Only organizers and co-organizers","value":"OrganizerOnly"},{"label":"People in organization excluding guests","value":"EveryoneInCompanyExcludingGuests"},{"label":"People who were invited","value":"InvitedUsers"}]} {"type":"autoComplete","required":true,"multiple":false,"creatable":false,"name":"standards.TeamsGlobalMeetingPolicy.MeetingChatEnabledType","label":"Meeting chat policy","options":[{"label":"On for everyone","value":"Enabled"},{"label":"On for everyone but anonymous users","value":"EnabledExceptAnonymous"},{"label":"Off for everyone","value":"Disabled"}]} {"type":"switch","name":"standards.TeamsGlobalMeetingPolicy.AllowExternalParticipantGiveRequestControl","label":"External participants can give or request control"} + {"type":"switch","name":"standards.TeamsGlobalMeetingPolicy.AllowParticipantGiveRequestControl","label":"Participants can give or request control"} IMPACT Low Impact ADDEDDATE 2024-11-12 POWERSHELLEQUIVALENT - Set-CsTeamsMeetingPolicy -AllowAnonymousUsersToJoinMeeting \$false -AllowAnonymousUsersToStartMeeting \$false -AutoAdmittedUsers \$AutoAdmittedUsers -AllowPSTNUsersToBypassLobby \$false -MeetingChatEnabledType EnabledExceptAnonymous -DesignatedPresenterRoleMode \$DesignatedPresenterRoleMode -AllowExternalParticipantGiveRequestControl \$false + Set-CsTeamsMeetingPolicy -AllowAnonymousUsersToJoinMeeting \$false -AllowAnonymousUsersToStartMeeting \$false -AutoAdmittedUsers \$AutoAdmittedUsers -AllowPSTNUsersToBypassLobby \$false -MeetingChatEnabledType EnabledExceptAnonymous -DesignatedPresenterRoleMode \$DesignatedPresenterRoleMode -AllowExternalParticipantGiveRequestControl \$false -AllowParticipantGiveRequestControl \$false RECOMMENDEDBY "CIS" UPDATECOMMENTBLOCK @@ -49,7 +50,7 @@ function Invoke-CIPPStandardTeamsGlobalMeetingPolicy { try { $CurrentState = New-TeamsRequest -TenantFilter $Tenant -Cmdlet 'Get-CsTeamsMeetingPolicy' -CmdParams @{Identity = 'Global' } | - Select-Object AllowAnonymousUsersToJoinMeeting, AllowAnonymousUsersToStartMeeting, AutoAdmittedUsers, AllowPSTNUsersToBypassLobby, MeetingChatEnabledType, DesignatedPresenterRoleMode, AllowExternalParticipantGiveRequestControl + Select-Object AllowAnonymousUsersToJoinMeeting, AllowAnonymousUsersToStartMeeting, AutoAdmittedUsers, AllowPSTNUsersToBypassLobby, MeetingChatEnabledType, DesignatedPresenterRoleMode, AllowExternalParticipantGiveRequestControl, AllowParticipantGiveRequestControl } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the TeamsGlobalMeetingPolicy state for $Tenant. Error: $ErrorMessage" -Sev Error @@ -66,7 +67,8 @@ function Invoke-CIPPStandardTeamsGlobalMeetingPolicy { ($CurrentState.AllowPSTNUsersToBypassLobby -eq $false) -and ($CurrentState.MeetingChatEnabledType -eq $MeetingChatEnabledType) -and ($CurrentState.DesignatedPresenterRoleMode -eq $DesignatedPresenterRoleMode) -and - ($CurrentState.AllowExternalParticipantGiveRequestControl -eq $Settings.AllowExternalParticipantGiveRequestControl) + ($CurrentState.AllowExternalParticipantGiveRequestControl -eq $Settings.AllowExternalParticipantGiveRequestControl) -and + ($CurrentState.AllowParticipantGiveRequestControl -eq $Settings.AllowParticipantGiveRequestControl) if ($Settings.remediate -eq $true) { @@ -82,6 +84,7 @@ function Invoke-CIPPStandardTeamsGlobalMeetingPolicy { MeetingChatEnabledType = $MeetingChatEnabledType DesignatedPresenterRoleMode = $DesignatedPresenterRoleMode AllowExternalParticipantGiveRequestControl = $Settings.AllowExternalParticipantGiveRequestControl + AllowParticipantGiveRequestControl = $Settings.AllowParticipantGiveRequestControl } try { @@ -113,6 +116,7 @@ function Invoke-CIPPStandardTeamsGlobalMeetingPolicy { MeetingChatEnabledType = $CurrentState.MeetingChatEnabledType DesignatedPresenterRoleMode = $CurrentState.DesignatedPresenterRoleMode AllowExternalParticipantGiveRequestControl = $CurrentState.AllowExternalParticipantGiveRequestControl + AllowParticipantGiveRequestControl = $CurrentState.AllowParticipantGiveRequestControl } $ExpectedValue = @{ AllowAnonymousUsersToJoinMeeting = $Settings.AllowAnonymousUsersToJoinMeeting @@ -122,6 +126,7 @@ function Invoke-CIPPStandardTeamsGlobalMeetingPolicy { MeetingChatEnabledType = $MeetingChatEnabledType DesignatedPresenterRoleMode = $DesignatedPresenterRoleMode AllowExternalParticipantGiveRequestControl = $Settings.AllowExternalParticipantGiveRequestControl + AllowParticipantGiveRequestControl = $Settings.AllowParticipantGiveRequestControl } Set-CIPPStandardsCompareField -FieldName 'standards.TeamsGlobalMeetingPolicy' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -Tenant $Tenant Add-CIPPBPAField -FieldName 'TeamsGlobalMeetingPolicy' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $Tenant From 7db9133064f6870cdc13e5e826c92bec88939ea7 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Mon, 9 Feb 2026 21:47:49 -0800 Subject: [PATCH 366/503] Use reporting DB for signin report insead of lighthouse making it possible for all users to access --- .../Reports/Invoke-ListInactiveAccounts.ps1 | 136 ++++++++++++++++-- .../CIPPCore/Public/Set-CIPPDBCacheUsers.ps1 | 3 +- 2 files changed, 128 insertions(+), 11 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Reports/Invoke-ListInactiveAccounts.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Reports/Invoke-ListInactiveAccounts.ps1 index a7f74435d3dc..382218496777 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Reports/Invoke-ListInactiveAccounts.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Reports/Invoke-ListInactiveAccounts.ps1 @@ -7,21 +7,48 @@ Function Invoke-ListInactiveAccounts { #> [CmdletBinding()] param($Request, $TriggerMetadata) - # Convert the TenantFilter parameter to a list of tenant IDs for AllTenants or a single tenant ID + + $APIName = 'ListInactiveAccounts' $TenantFilter = $Request.Query.tenantFilter - if ($TenantFilter -eq 'AllTenants') { - $TenantFilter = (Get-Tenants).customerId - } else { - $TenantFilter = (Get-Tenants -TenantFilter $TenantFilter).customerId - } + $InactiveDays = if ($Request.Query.InactiveDays) { [int]$Request.Query.InactiveDays } else { 180 } try { - $GraphRequest = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/tenantRelationships/managedTenants/inactiveUsers?`$count=true" -tenantid $env:TenantID | Where-Object { $_.tenantId -in $TenantFilter } + $Lookup = (Get-Date).AddDays(-$InactiveDays).ToUniversalTime() + + if ($TenantFilter -eq 'AllTenants') { + # Get all tenants that have user data + $AllUserItems = Get-CIPPDbItem -TenantFilter 'allTenants' -Type 'Users' + $Tenants = @($AllUserItems | Where-Object { $_.RowKey -ne 'Users-Count' } | Select-Object -ExpandProperty PartitionKey -Unique) + + $TenantList = Get-Tenants -IncludeErrors + $Tenants = $Tenants | Where-Object { $TenantList.defaultDomainName -contains $_ } + + $AllResults = [System.Collections.Generic.List[PSCustomObject]]::new() + + foreach ($Tenant in $Tenants) { + try { + Write-Information "Processing tenant: $Tenant" + $TenantResults = Get-InactiveUsersFromDB -TenantFilter $Tenant -InactiveDays $InactiveDays -Lookup $Lookup + + foreach ($Result in $TenantResults) { + $AllResults.Add($Result) + } + } catch { + Write-LogMessage -API $APIName -tenant $Tenant -message "Failed to get inactive users: $($_.Exception.Message)" -sev Warning + } + } + + $GraphRequest = @($AllResults) + } else { + $GraphRequest = Get-InactiveUsersFromDB -TenantFilter $TenantFilter -InactiveDays $InactiveDays -Lookup $Lookup + } + $StatusCode = [HttpStatusCode]::OK } catch { - $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message - $StatusCode = [HttpStatusCode]::Forbidden - $GraphRequest = "Could not connect to Azure Lighthouse API: $($ErrorMessage)" + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API $APIName -tenant $TenantFilter -message "Failed to retrieve inactive accounts: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + $StatusCode = [HttpStatusCode]::InternalServerError + $GraphRequest = @{ Error = $ErrorMessage.NormalizedError } } return ([HttpResponseContext]@{ @@ -29,3 +56,92 @@ Function Invoke-ListInactiveAccounts { Body = @($GraphRequest) }) } + +# Helper function to get inactive users from the database for a specific tenant +function Get-InactiveUsersFromDB { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter, + + [Parameter(Mandatory = $true)] + [int]$InactiveDays, + + [Parameter(Mandatory = $true)] + [DateTime]$Lookup + ) + + # Get users from database + $Users = New-CIPPDbRequest -TenantFilter $TenantFilter -Type 'Users' + + if (-not $Users) { + Write-Information "No user data found in database for tenant $TenantFilter" + return @() + } + + # Get tenant info for display name + $TenantInfo = Get-Tenants -TenantFilter $TenantFilter | Select-Object -First 1 + $TenantDisplayName = $TenantInfo.displayName ?? $TenantFilter + + $InactiveUsers = foreach ($User in $Users) { + # Skip disabled users by default + if ($User.accountEnabled -eq $false) { continue } + + # Skip guest users + if ($User.userType -eq 'Guest') { continue } + + # Determine last sign-in + $lastInteractive = $User.signInActivity.lastSignInDateTime + $lastNonInteractive = $User.signInActivity.lastNonInteractiveSignInDateTime + + $lastSignIn = $null + if ($lastInteractive -and $lastNonInteractive) { + $lastSignIn = if ([DateTime]$lastInteractive -gt [DateTime]$lastNonInteractive) { + $lastInteractive + } else { + $lastNonInteractive + } + } elseif ($lastInteractive) { + $lastSignIn = $lastInteractive + } elseif ($lastNonInteractive) { + $lastSignIn = $lastNonInteractive + } + + # Check if user is inactive + $isInactive = (-not $lastSignIn) -or ([DateTime]$lastSignIn -le $Lookup) + + if ($isInactive) { + # Calculate days since last sign-in + $daysSinceSignIn = if ($lastSignIn) { + [Math]::Round(((Get-Date) - [DateTime]$lastSignIn).TotalDays) + } else { + $null + } + + # Count assigned licenses + $numberOfAssignedLicenses = if ($User.assignedLicenses) { + $User.assignedLicenses.Count + } else { + 0 + } + + [PSCustomObject]@{ + tenantId = $TenantFilter + tenantDisplayName = $TenantDisplayName + azureAdUserId = $User.id + userPrincipalName = $User.userPrincipalName + displayName = $User.displayName + userType = $User.userType + createdDateTime = $User.createdDateTime + lastSignInDateTime = $lastInteractive + lastNonInteractiveSignInDateTime = $lastNonInteractive + lastRefreshedDateTime = (Get-Date).ToString('yyyy-MM-ddTHH:mm:ss.fffZ') + numberOfAssignedLicenses = $numberOfAssignedLicenses + daysSinceLastSignIn = $daysSinceSignIn + accountEnabled = $User.accountEnabled + } + } + } + + return @($InactiveUsers) +} diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheUsers.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheUsers.ps1 index 72f498142664..6fd9623adb45 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheUsers.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheUsers.ps1 @@ -16,7 +16,8 @@ function Set-CIPPDBCacheUsers { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching users' -sev Debug # Stream users directly from Graph API to batch processor - New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/users?$top=999' -tenantid $TenantFilter | + # Using $top=500 due to signInActivity limitation + New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/users?$top=500&$select=signInActivity' -tenantid $TenantFilter | Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Users' -AddCount Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached users successfully' -sev Debug From 34a76da47b37f929705d348387f106618aa53b3c Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Tue, 10 Feb 2026 09:24:52 +0100 Subject: [PATCH 367/503] added props --- Modules/CIPPCore/Public/Compare-CIPPIntuneObject.ps1 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Compare-CIPPIntuneObject.ps1 b/Modules/CIPPCore/Public/Compare-CIPPIntuneObject.ps1 index 7a51151cbc90..b974f4bafbe7 100644 --- a/Modules/CIPPCore/Public/Compare-CIPPIntuneObject.ps1 +++ b/Modules/CIPPCore/Public/Compare-CIPPIntuneObject.ps1 @@ -33,7 +33,8 @@ function Compare-CIPPIntuneObject { 'tenantFilter', 'agents', 'isSynced' - 'locationInfo' + 'locationInfo', + 'templateId' ) $excludeProps = $defaultExcludeProperties + $ExcludeProperties From 4fd9970c16b56e57f33cdb6eb88227ca1f6ae574 Mon Sep 17 00:00:00 2001 From: Steven van Beek Date: Tue, 10 Feb 2026 14:00:23 +0100 Subject: [PATCH 368/503] added new alerts --- .../Get-CIPPAlertInactiveGuestUsers.ps1 | 100 ++++++++++++++++++ .../Alerts/Get-CIPPAlertInactiveUsers.ps1 | 91 ++++++++++++++++ .../Alerts/Get-CIPPAlertStaleEntraDevices.ps1 | 93 ++++++++++++++++ 3 files changed, 284 insertions(+) create mode 100644 Modules/CIPPCore/Public/Alerts/Get-CIPPAlertInactiveGuestUsers.ps1 create mode 100644 Modules/CIPPCore/Public/Alerts/Get-CIPPAlertInactiveUsers.ps1 create mode 100644 Modules/CIPPCore/Public/Alerts/Get-CIPPAlertStaleEntraDevices.ps1 diff --git a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertInactiveGuestUsers.ps1 b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertInactiveGuestUsers.ps1 new file mode 100644 index 000000000000..1109cd5e5bc0 --- /dev/null +++ b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertInactiveGuestUsers.ps1 @@ -0,0 +1,100 @@ +function Get-CIPPAlertInactiveGuestUsers { + <# + .FUNCTIONALITY + Entrypoint + #> + [CmdletBinding()] + param ( + [Parameter(Mandatory = $false)] + [Alias('input')] + $InputValue, + [Parameter(Mandatory = $false)] + [switch]$IncludeNeverSignedIn, # Include users who have never signed in (default is to skip them), future use would allow this to be set in an alert configuration + $TenantFilter + ) + + try { + try { + $inactiveDays = 90 + $excludeDisabled = $false + + if ($InputValue -is [hashtable] -or $InputValue -is [pscustomobject]) { + $excludeDisabled = [bool]$InputValue.ExcludeDisabled + if ($null -ne $InputValue.DaysSinceLastLogin -and $InputValue.DaysSinceLastLogin -ne '') { + $parsedDays = 0 + if ([int]::TryParse($InputValue.DaysSinceLastLogin.ToString(), [ref]$parsedDays) -and $parsedDays -gt 0) { + $inactiveDays = $parsedDays + } + } + } + elseif ($InputValue -eq $true) { + # Backwards compatibility: legacy single-input boolean means exclude disabled users + $excludeDisabled = $true + } + + $Lookup = (Get-Date).AddDays(-$inactiveDays).ToUniversalTime() + Write-Host "Checking for guest users inactive since $Lookup (excluding disabled: $excludeDisabled)" + # Build base filter - cannot filter assignedLicenses server-side + $BaseFilter = if ($excludeDisabled) { 'accountEnabled eq true' } else { '' } + + $Uri = if ($BaseFilter) { + "https://graph.microsoft.com/beta/users?`$filter=$BaseFilter&`$select=id,UserPrincipalName,signInActivity,mail,userType,accountEnabled,assignedLicenses" + } + else { + "https://graph.microsoft.com/beta/users?`$select=id,UserPrincipalName,signInActivity,mail,userType,accountEnabled,assignedLicenses" + } + + $GraphRequest = New-GraphGetRequest -uri $Uri -scope 'https://graph.microsoft.com/.default' -tenantid $TenantFilter | + Where-Object { $_.userType -eq 'Guest' } + + $AlertData = foreach ($user in $GraphRequest) { + $lastInteractive = $user.signInActivity.lastSignInDateTime + $lastNonInteractive = $user.signInActivity.lastNonInteractiveSignInDateTime + + # Find most recent sign-in + $lastSignIn = $null + if ($lastInteractive -and $lastNonInteractive) { + $lastSignIn = if ([DateTime]$lastInteractive -gt [DateTime]$lastNonInteractive) { $lastInteractive } else { $lastNonInteractive } + } + elseif ($lastInteractive) { + $lastSignIn = $lastInteractive + } + elseif ($lastNonInteractive) { + $lastSignIn = $lastNonInteractive + } + + # Check if inactive + $isInactive = (-not $lastSignIn) -or ([DateTime]$lastSignIn -le $Lookup) + # Skip users who have never signed in by default (unless IncludeNeverSignedIn is specified) + if (-not $IncludeNeverSignedIn -and -not $lastSignIn) { continue } + # Only process inactive users + if ($isInactive) { + + if (-not $lastSignIn) { + $Message = 'Guest user {0} has never signed in.' -f $user.UserPrincipalName + } + else { + $daysSinceSignIn = [Math]::Round(((Get-Date) - [DateTime]$lastSignIn).TotalDays) + $Message = 'Guest user {0} has been inactive for {1} days. Last sign-in: {2}' -f $user.UserPrincipalName, $daysSinceSignIn, $lastSignIn + } + + + [PSCustomObject]@{ + UserPrincipalName = $user.UserPrincipalName + Id = $user.id + lastSignIn = $lastSignIn + DaysSinceLastSignIn = if ($daysSinceSignIn) { $daysSinceSignIn } else { 'N/A' } + Message = $Message + Tenant = $TenantFilter + } + } + } + + Write-AlertTrace -cmdletName $MyInvocation.MyCommand -tenantFilter $TenantFilter -data $AlertData + } + catch {} + } + catch { + Write-AlertMessage -tenant $($TenantFilter) -message "Failed to check inactive guest users for $($TenantFilter): $(Get-NormalizedError -message $_.Exception.message)" + } +} diff --git a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertInactiveUsers.ps1 b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertInactiveUsers.ps1 new file mode 100644 index 000000000000..0a42e8346cce --- /dev/null +++ b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertInactiveUsers.ps1 @@ -0,0 +1,91 @@ +function Get-CIPPAlertInactiveUsers { + <# + .FUNCTIONALITY + Entrypoint + #> + [CmdletBinding()] + param ( + [Parameter(Mandatory = $false)] + [Alias('input')] + $InputValue, + [Parameter(Mandatory = $false)] + [switch]$IncludeNeverSignedIn, # Include users who have never signed in (default is to skip them), future use would allow this to be set in an alert configuration + $TenantFilter + ) + + try { + try { + $inactiveDays = 90 + $excludeDisabled = $false + + if ($InputValue -is [hashtable] -or $InputValue -is [pscustomobject]) { + $excludeDisabled = [bool]$InputValue.ExcludeDisabled + if ($null -ne $InputValue.DaysSinceLastLogin -and $InputValue.DaysSinceLastLogin -ne '') { + $parsedDays = 0 + if ([int]::TryParse($InputValue.DaysSinceLastLogin.ToString(), [ref]$parsedDays) -and $parsedDays -gt 0) { + $inactiveDays = $parsedDays + } + } + } elseif ($InputValue -eq $true) { + # Backwards compatibility: legacy single-input boolean means exclude disabled users + $excludeDisabled = $true + } + + $Lookup = (Get-Date).AddDays(-$inactiveDays).ToUniversalTime() + Write-Host "Checking for users inactive since $Lookup (excluding disabled: $excludeDisabled)" + # Build base filter - cannot filter accountEnabled server-side + $BaseFilter = if ($excludeDisabled) { 'accountEnabled eq true' } else { '' } + + $Uri = if ($BaseFilter) { + "https://graph.microsoft.com/beta/users?`$filter=$BaseFilter&`$select=id,UserPrincipalName,signInActivity,mail,userType,accountEnabled,assignedLicenses" + } else { + "https://graph.microsoft.com/beta/users?`$select=id,UserPrincipalName,signInActivity,mail,userType,accountEnabled,assignedLicenses" + } + + $GraphRequest = New-GraphGetRequest -uri $Uri -scope 'https://graph.microsoft.com/.default' -tenantid $TenantFilter | + Where-Object { $_.userType -eq 'Member' } + + $AlertData = foreach ($user in $GraphRequest) { + $lastInteractive = $user.signInActivity.lastSignInDateTime + $lastNonInteractive = $user.signInActivity.lastNonInteractiveSignInDateTime + + # Find most recent sign-in + $lastSignIn = $null + if ($lastInteractive -and $lastNonInteractive) { + $lastSignIn = if ([DateTime]$lastInteractive -gt [DateTime]$lastNonInteractive) { $lastInteractive } else { $lastNonInteractive } + } elseif ($lastInteractive) { + $lastSignIn = $lastInteractive + } elseif ($lastNonInteractive) { + $lastSignIn = $lastNonInteractive + } + + # Check if inactive + $isInactive = (-not $lastSignIn) -or ([DateTime]$lastSignIn -le $Lookup) + # Skip users who have never signed in by default (unless IncludeNeverSignedIn is specified) + if (-not $IncludeNeverSignedIn -and -not $lastSignIn) { continue } + # Only process inactive users + if ($isInactive) { + if (-not $lastSignIn) { + $Message = 'User {0} has never signed in.' -f $user.UserPrincipalName + } else { + $daysSinceSignIn = [Math]::Round(((Get-Date) - [DateTime]$lastSignIn).TotalDays) + $Message = 'User {0} has been inactive for {1} days. Last sign-in: {2}' -f $user.UserPrincipalName, $daysSinceSignIn, $lastSignIn + } + + [PSCustomObject]@{ + UserPrincipalName = $user.UserPrincipalName + Id = $user.id + lastSignIn = $lastSignIn + DaysSinceLastSignIn = if ($daysSinceSignIn) { $daysSinceSignIn } else { 'N/A' } + Message = $Message + Tenant = $TenantFilter + } + } + } + + Write-AlertTrace -cmdletName $MyInvocation.MyCommand -tenantFilter $TenantFilter -data $AlertData + } catch {} + } catch { + Write-AlertMessage -tenant $($TenantFilter) -message "Failed to check inactive users with licenses for $($TenantFilter): $(Get-NormalizedError -message $_.Exception.message)" + } +} diff --git a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertStaleEntraDevices.ps1 b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertStaleEntraDevices.ps1 new file mode 100644 index 000000000000..2c308fb00e05 --- /dev/null +++ b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertStaleEntraDevices.ps1 @@ -0,0 +1,93 @@ +function Get-CIPPAlertStaleEntraDevices { + <# + .FUNCTIONALITY + Entrypoint + #> + [CmdletBinding()] + param ( + [Parameter(Mandatory = $false)] + [Alias('input')] + $InputValue, + $TenantFilter + ) + + try { + try { + $inactiveDays = 90 + + if ($InputValue -is [hashtable] -or $InputValue -is [pscustomobject]) { + $excludeDisabled = [bool]$InputValue.ExcludeDisabled + if ($null -ne $InputValue.DaysSinceLastActivity -and $InputValue.DaysSinceLastActivity -ne '') { + $parsedDays = 0 + if ([int]::TryParse($InputValue.DaysSinceLastActivity.ToString(), [ref]$parsedDays) -and $parsedDays -gt 0) { + $inactiveDays = $parsedDays + } + } + } + elseif ($InputValue -eq $true) { + # Backwards compatibility: legacy single-input boolean means exclude disabled users + $excludeDisabled = $true + } + + $Lookup = (Get-Date).AddDays(-$inactiveDays).ToUniversalTime() + Write-Host "Checking for inactive Entra devices since $Lookup (excluding disabled: $excludeDisabled)" + # Build base filter - cannot filter accountEnabled server-side + $BaseFilter = if ($excludeDisabled) { 'accountEnabled eq true' } else { '' } + + $Uri = if ($BaseFilter) { + "https://graph.microsoft.com/beta/devices?`$filter=$BaseFilter" + } + else { + "https://graph.microsoft.com/beta/devices" + } + + $GraphRequest = New-GraphGetRequest -uri $Uri -scope 'https://graph.microsoft.com/.default' -tenantid $TenantFilter + + $AlertData = foreach ($device in $GraphRequest) { + + $lastActivity = $device.approximateLastSignInDateTime + + $isInactive = (-not $lastActivity) -or ([DateTime]$lastActivity -le $Lookup) + # Only process stale Entra devices + if ($isInactive) { + + if (-not $lastActivity) { + + $Message = 'Device {0} has never been active' -f $device.displayName + } + else { + $daysSinceLastActivity = [Math]::Round(((Get-Date) - [DateTime]$lastActivity).TotalDays) + $Message = 'Device {0} has been inactive for {1} days. Last activity: {2}' -f $device.displayName, $daysSinceLastActivity, $lastActivity + } + + if ($device.TrustType -eq "Workplace") { $TrustType = "Entra registered" } + elseif ($device.TrustType -eq "AzureAd") { $TrustType = "Entra joined" } + elseif ($device.TrustType -eq "ServerAd") { $TrustType = "Entra hybrid joined" } + + [PSCustomObject]@{ + DeviceName = if ($device.displayName) { $device.displayName } else { 'N/A' } + Id = if ($device.id) { $device.id } else { 'N/A' } + deviceOwnership = if ($device.deviceOwnership) { $device.deviceOwnership } else { 'N/A' } + operatingSystem = if ($device.operatingSystem) { $device.operatingSystem } else { 'N/A' } + enrollmentType = if ($device.enrollmentType) { $device.enrollmentType } else { 'N/A' } + Enabled = if ($device.accountEnabled) { $device.accountEnabled } else { 'N/A' } + Managed = if ($device.isManaged) { $device.isManaged } else { 'N/A' } + Complaint = if ($device.isCompliant) { $device.isCompliant } else { 'N/A' } + JoinType = $TrustType + lastActivity = if ($lastActivity) { $lastActivity } else { 'N/A' } + DaysSinceLastActivity = if ($daysSinceLastActivity) { $daysSinceLastActivity } else { 'N/A' } + RegisteredDateTime = if ($device.createdDateTime) { $device.createdDateTime } else { 'N/A' } + Message = $Message + Tenant = $TenantFilter + } + } + } + + Write-AlertTrace -cmdletName $MyInvocation.MyCommand -tenantFilter $TenantFilter -data $AlertData + } + catch {} + } + catch { + Write-AlertMessage -tenant $($TenantFilter) -message "Failed to check inactive guest users for $($TenantFilter): $(Get-NormalizedError -message $_.Exception.message)" + } +} From c2de084839dbeb2f4d342a7dc747ca0c4a6d15df Mon Sep 17 00:00:00 2001 From: James Tarran Date: Tue, 10 Feb 2026 15:22:37 +0000 Subject: [PATCH 369/503] Update Search-GitHub.ps1 - Fixed forks not appearing by adding a switch to include them when building the query --- Modules/CippExtensions/Public/GitHub/Search-GitHub.ps1 | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Modules/CippExtensions/Public/GitHub/Search-GitHub.ps1 b/Modules/CippExtensions/Public/GitHub/Search-GitHub.ps1 index c630d524c149..c44b0037ea1d 100644 --- a/Modules/CippExtensions/Public/GitHub/Search-GitHub.ps1 +++ b/Modules/CippExtensions/Public/GitHub/Search-GitHub.ps1 @@ -5,6 +5,7 @@ function Search-GitHub { [string]$User, [string]$Org, [string]$Path, + [bool]$includeforks = $false, [string[]]$SearchTerm, [string]$Language, [ValidateSet('code', 'commits', 'issues', 'users', 'repositories', 'topics', 'labels')] @@ -46,6 +47,7 @@ function Search-GitHub { if ($Language) { $QueryParts.Add("language:$Language") } + $QueryParts.Add("fork:$($includeforks.ToString().ToLower())") $Query = $QueryParts -join ' ' Write-Information "Query: $Query" From facce9bf5a80658a65a0aa9bbeb57ff492fe3d9a Mon Sep 17 00:00:00 2001 From: James Tarran Date: Tue, 10 Feb 2026 16:02:43 +0000 Subject: [PATCH 370/503] Search-GitHub.ps1 - Fixed forks not appearing by adding a switch to include them when building the query From 916e41dc1400d9e2d6a58d7061b2f6fa5b0690d6 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Tue, 10 Feb 2026 11:35:59 -0500 Subject: [PATCH 371/503] Optimize github api calls for if extension is not enabled --- .../Tools/GitHub/Invoke-ExecGitHubAction.ps1 | 127 ++++++++---------- .../Public/GitHub/Search-GitHub.ps1 | 8 +- 2 files changed, 64 insertions(+), 71 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tools/GitHub/Invoke-ExecGitHubAction.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tools/GitHub/Invoke-ExecGitHubAction.ps1 index f03b641719dd..709e3a6b076f 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tools/GitHub/Invoke-ExecGitHubAction.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tools/GitHub/Invoke-ExecGitHubAction.ps1 @@ -22,85 +22,76 @@ function Invoke-ExecGitHubAction { $SplatParams = $Parameters | Select-Object -ExcludeProperty Action, TenantFilter | ConvertTo-Json | ConvertFrom-Json -AsHashtable - $Table = Get-CIPPTable -TableName Extensionsconfig - $Configuration = ((Get-CIPPAzDataTableEntity @Table).config | ConvertFrom-Json -ErrorAction SilentlyContinue).GitHub - - if (!$Configuration.Enabled) { - $Response = Invoke-RestMethod -Uri 'https://cippy.azurewebsites.net/api/ExecGitHubAction' -Method POST -Body ($Parameters | ConvertTo-Json -Depth 10) -ContentType 'application/json' - $Results = $Response.Results - $Metadata = $Response.Metadata - } else { - switch ($Action) { - 'Search' { - $SearchResults = Search-GitHub @SplatParams - $Results = @($SearchResults.items) - $Metadata = $SearchResults | Select-Object -Property total_count, incomplete_results - } - 'GetFileContents' { - $Results = Get-GitHubFileContents @SplatParams - } - 'GetBranches' { - $Results = @(Get-GitHubBranch @SplatParams) - } - 'GetOrgs' { - try { - $Orgs = Invoke-GitHubApiRequest -Path 'user/orgs' - $Results = @($Orgs) - } catch { - $Results = @{ - resultText = 'You may not have permission to view organizations, check your PAT scopes and try again - {0}' -f $_.Exception.Message - state = 'error' - } + switch ($Action) { + 'Search' { + $SearchResults = Search-GitHub @SplatParams + $Results = @($SearchResults.items) + $Metadata = $SearchResults | Select-Object -Property total_count, incomplete_results + } + 'GetFileContents' { + $Results = Get-GitHubFileContents @SplatParams + } + 'GetBranches' { + $Results = @(Get-GitHubBranch @SplatParams) + } + 'GetOrgs' { + try { + $Orgs = Invoke-GitHubApiRequest -Path 'user/orgs' + $Results = @($Orgs) + } catch { + $Results = @{ + resultText = 'You may not have permission to view organizations, check your PAT scopes and try again - {0}' -f $_.Exception.Message + state = 'error' } } - 'GetFileTree' { - $Files = (Get-GitHubFileTree @SplatParams).tree | Where-Object { $_.path -match '.json$' } | Select-Object *, @{n = 'html_url'; e = { "https://github.com/$($SplatParams.FullName)/tree/$($SplatParams.Branch)/$($_.path)" } } - $Results = @($Files) - } - 'ImportTemplate' { - $Results = Import-CommunityTemplate @SplatParams - } - 'CreateRepo' { - try { - Write-Information "Creating repository '$($SplatParams.Name)'" - $Repo = New-GitHubRepo @SplatParams - if ($Repo.id) { - $Table = Get-CIPPTable -TableName CommunityRepos - $RepoEntity = @{ - PartitionKey = 'CommunityRepos' - RowKey = [string]$Repo.id - Name = [string]($Repo.name -replace ' ', '-') - Description = [string]$Repo.description - URL = [string]$Repo.html_url - FullName = [string]$Repo.full_name - Owner = [string]$Repo.owner.login - Visibility = [string]$Repo.visibility - WriteAccess = [bool]$Repo.permissions.push - DefaultBranch = [string]$Repo.default_branch - Permissions = [string]($Repo.permissions | ConvertTo-Json -Compress) - } - Add-CIPPAzDataTableEntity @Table -Entity $RepoEntity -Force | Out-Null - - $Results = @{ - resultText = "Repository '$($Repo.name)' created" - state = 'success' - } + } + 'GetFileTree' { + $Files = (Get-GitHubFileTree @SplatParams).tree | Where-Object { $_.path -match '.json$' } | Select-Object *, @{n = 'html_url'; e = { "https://github.com/$($SplatParams.FullName)/tree/$($SplatParams.Branch)/$($_.path)" } } + $Results = @($Files) + } + 'ImportTemplate' { + $Results = Import-CommunityTemplate @SplatParams + } + 'CreateRepo' { + try { + Write-Information "Creating repository '$($SplatParams.Name)'" + $Repo = New-GitHubRepo @SplatParams + if ($Repo.id) { + $Table = Get-CIPPTable -TableName CommunityRepos + $RepoEntity = @{ + PartitionKey = 'CommunityRepos' + RowKey = [string]$Repo.id + Name = [string]($Repo.name -replace ' ', '-') + Description = [string]$Repo.description + URL = [string]$Repo.html_url + FullName = [string]$Repo.full_name + Owner = [string]$Repo.owner.login + Visibility = [string]$Repo.visibility + WriteAccess = [bool]$Repo.permissions.push + DefaultBranch = [string]$Repo.default_branch + Permissions = [string]($Repo.permissions | ConvertTo-Json -Compress) } - } catch { - Write-Information (Get-CippException -Exception $_ | ConvertTo-Json) + Add-CIPPAzDataTableEntity @Table -Entity $RepoEntity -Force | Out-Null + $Results = @{ - resultText = 'You may not have permission to create repositories, check your PAT scopes and try again - {0}' -f $_.Exception.Message - state = 'error' + resultText = "Repository '$($Repo.name)' created" + state = 'success' } } - } - default { + } catch { + Write-Information (Get-CippException -Exception $_ | ConvertTo-Json) $Results = @{ - resultText = "Unknown action '$Action'" + resultText = 'You may not have permission to create repositories, check your PAT scopes and try again - {0}' -f $_.Exception.Message state = 'error' } } } + default { + $Results = @{ + resultText = "Unknown action '$Action'" + state = 'error' + } + } } $Body = @{ diff --git a/Modules/CippExtensions/Public/GitHub/Search-GitHub.ps1 b/Modules/CippExtensions/Public/GitHub/Search-GitHub.ps1 index c44b0037ea1d..f85131a38154 100644 --- a/Modules/CippExtensions/Public/GitHub/Search-GitHub.ps1 +++ b/Modules/CippExtensions/Public/GitHub/Search-GitHub.ps1 @@ -1,11 +1,11 @@ function Search-GitHub { [CmdletBinding()] - Param ( + param ( [string[]]$Repository, [string]$User, [string]$Org, [string]$Path, - [bool]$includeforks = $false, + [switch]$IncludeForks, [string[]]$SearchTerm, [string]$Language, [ValidateSet('code', 'commits', 'issues', 'users', 'repositories', 'topics', 'labels')] @@ -47,7 +47,9 @@ function Search-GitHub { if ($Language) { $QueryParts.Add("language:$Language") } - $QueryParts.Add("fork:$($includeforks.ToString().ToLower())") + if ($IncludeForks.IsPresent) { + $QueryParts.Add('fork:true') + } $Query = $QueryParts -join ' ' Write-Information "Query: $Query" From 19f9d7f8a01ef29504606c9fa4ee7edb2ba4cccd Mon Sep 17 00:00:00 2001 From: mpressley-np Date: Tue, 10 Feb 2026 12:42:13 -0500 Subject: [PATCH 372/503] Fix for Issue#5340 https://github.com/KelvinTegelaar/CIPP/issues/5340 --- .../Public/Standards/Invoke-CIPPStandardOauthConsentLowSec.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardOauthConsentLowSec.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardOauthConsentLowSec.ps1 index 2bce9422a74b..e28c87dc27e9 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardOauthConsentLowSec.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardOauthConsentLowSec.ps1 @@ -119,7 +119,7 @@ function Invoke-CIPPStandardOauthConsentLowSec { } $ExpectedValue = @{ - permissionGrantPolicyIdsAssignedToDefaultUserRole = @('managePermissionGrantsForSelf.microsoft-user-default-low') + permissionGrantPolicyIdsAssignedToDefaultUserRole = @('ManagePermissionGrantsForSelf.microsoft-user-default-low') } Add-CIPPBPAField -FieldName 'OauthConsentLowSec' -FieldValue $State.permissionGrantPolicyIdsAssignedToDefaultUserRole -StoreAs bool -Tenant $tenant Set-CIPPStandardsCompareField -FieldName 'standards.OauthConsentLowSec' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -Tenant $tenant From abc4d992007eb4ee4fc98e47b4603abafc0dd79e Mon Sep 17 00:00:00 2001 From: John Duprey Date: Tue, 10 Feb 2026 12:53:19 -0500 Subject: [PATCH 373/503] Add queue tracking and mailbox rules report Add queue tracking support across CIPP DB cache flows and introduce mailbox rules reporting. Key changes: Update-CippQueueEntry: accept TotalTasks and IncrementTotalTasks to set or increment task counts. Propagate QueueId through Push-ExecCIPPDBCache, Invoke-ExecCIPPDBCache and orchestration batches (create New-CippQueueEntry with TotalTasks and include QueueId in batch/response). Include 'MailboxRules' in Push-CIPPDBCacheData list. Add Get-CIPPMailboxRulesReport to read mailbox rules from the reporting DB (supports AllTenants). Enhance Invoke-ListMailboxRules to optionally use the report DB via UseReportDB, restructure queue/orchestration logic and add error handling. Many Set-CIPPDBCache* functions updated to accept an optional QueueId parameter so cache jobs can report progress to the queue. --- .../CippQueue/Update-CippQueueEntry.ps1 | 17 +- .../CIPPDBCache/Push-ExecCIPPDBCache.ps1 | 13 +- .../Push-CIPPDBCacheData.ps1 | 1 + .../CIPP/Core/Invoke-ExecCIPPDBCache.ps1 | 28 +++- .../Invoke-ListMailboxRules.ps1 | 150 +++++++++++------- .../Public/Get-CIPPMailboxRulesReport.ps1 | 82 ++++++++++ ...t-CIPPDBCacheAdminConsentRequestPolicy.ps1 | 6 +- .../Set-CIPPDBCacheAppRoleAssignments.ps1 | 6 +- .../CIPPCore/Public/Set-CIPPDBCacheApps.ps1 | 6 +- ...t-CIPPDBCacheAuthenticationFlowsPolicy.ps1 | 6 +- ...CIPPDBCacheAuthenticationMethodsPolicy.ps1 | 6 +- .../Set-CIPPDBCacheAuthorizationPolicy.ps1 | 6 +- .../Set-CIPPDBCacheB2BManagementPolicy.ps1 | 6 +- .../Public/Set-CIPPDBCacheCASMailboxes.ps1 | 6 +- ...t-CIPPDBCacheConditionalAccessPolicies.ps1 | 6 +- ...CacheCredentialUserRegistrationDetails.ps1 | 6 +- ...Set-CIPPDBCacheCrossTenantAccessPolicy.ps1 | 6 +- ...-CIPPDBCacheDefaultAppManagementPolicy.ps1 | 6 +- ...et-CIPPDBCacheDeviceRegistrationPolicy.ps1 | 6 +- .../Public/Set-CIPPDBCacheDeviceSettings.ps1 | 6 +- .../Public/Set-CIPPDBCacheDevices.ps1 | 6 +- ...et-CIPPDBCacheDirectoryRecommendations.ps1 | 6 +- .../Public/Set-CIPPDBCacheDomains.ps1 | 6 +- .../Set-CIPPDBCacheExoAcceptedDomains.ps1 | 6 +- .../Set-CIPPDBCacheExoAdminAuditLogConfig.ps1 | 6 +- .../Set-CIPPDBCacheExoAntiPhishPolicies.ps1 | 6 +- .../Set-CIPPDBCacheExoAntiPhishPolicy.ps1 | 6 +- .../Set-CIPPDBCacheExoAtpPolicyForO365.ps1 | 6 +- .../Set-CIPPDBCacheExoDkimSigningConfig.ps1 | 6 +- ...IPPDBCacheExoHostedContentFilterPolicy.ps1 | 6 +- ...CacheExoHostedOutboundSpamFilterPolicy.ps1 | 6 +- ...et-CIPPDBCacheExoMalwareFilterPolicies.ps1 | 6 +- .../Set-CIPPDBCacheExoMalwareFilterPolicy.ps1 | 6 +- .../Set-CIPPDBCacheExoOrganizationConfig.ps1 | 6 +- ...Set-CIPPDBCacheExoPresetSecurityPolicy.ps1 | 6 +- .../Set-CIPPDBCacheExoQuarantinePolicy.ps1 | 6 +- .../Public/Set-CIPPDBCacheExoRemoteDomain.ps1 | 6 +- ...t-CIPPDBCacheExoSafeAttachmentPolicies.ps1 | 6 +- ...Set-CIPPDBCacheExoSafeAttachmentPolicy.ps1 | 6 +- .../Set-CIPPDBCacheExoSafeLinksPolicies.ps1 | 6 +- .../Set-CIPPDBCacheExoSafeLinksPolicy.ps1 | 6 +- .../Set-CIPPDBCacheExoSharingPolicy.ps1 | 6 +- ...Set-CIPPDBCacheExoTenantAllowBlockList.ps1 | 6 +- .../Set-CIPPDBCacheExoTransportRules.ps1 | 6 +- .../CIPPCore/Public/Set-CIPPDBCacheGroups.ps1 | 6 +- .../CIPPCore/Public/Set-CIPPDBCacheGuests.ps1 | 6 +- ...CIPPDBCacheIntuneAppProtectionPolicies.ps1 | 6 +- .../Public/Set-CIPPDBCacheIntunePolicies.ps1 | 6 +- .../Public/Set-CIPPDBCacheLicenseOverview.ps1 | 6 +- .../Public/Set-CIPPDBCacheMFAState.ps1 | 6 +- .../Public/Set-CIPPDBCacheMailboxRules.ps1 | 60 +++++++ .../Public/Set-CIPPDBCacheMailboxUsage.ps1 | 6 +- .../Public/Set-CIPPDBCacheMailboxes.ps1 | 20 ++- ...PPDBCacheManagedDeviceEncryptionStates.ps1 | 6 +- .../Public/Set-CIPPDBCacheManagedDevices.ps1 | 6 +- .../Set-CIPPDBCacheOAuth2PermissionGrants.ps1 | 6 +- .../Public/Set-CIPPDBCacheOneDriveUsage.ps1 | 6 +- .../Public/Set-CIPPDBCacheOrganization.ps1 | 6 +- .../Public/Set-CIPPDBCachePIMSettings.ps1 | 6 +- .../Public/Set-CIPPDBCacheRiskDetections.ps1 | 6 +- .../Set-CIPPDBCacheRiskyServicePrincipals.ps1 | 6 +- .../Public/Set-CIPPDBCacheRiskyUsers.ps1 | 6 +- ...DBCacheRoleAssignmentScheduleInstances.ps1 | 3 +- ...et-CIPPDBCacheRoleEligibilitySchedules.ps1 | 6 +- .../Set-CIPPDBCacheRoleManagementPolicies.ps1 | 6 +- .../CIPPCore/Public/Set-CIPPDBCacheRoles.ps1 | 6 +- .../Public/Set-CIPPDBCacheSecureScore.ps1 | 6 +- ...PDBCacheServicePrincipalRiskDetections.ps1 | 6 +- .../Set-CIPPDBCacheServicePrincipals.ps1 | 6 +- .../Public/Set-CIPPDBCacheSettings.ps1 | 6 +- ...Set-CIPPDBCacheUserRegistrationDetails.ps1 | 6 +- .../CIPPCore/Public/Set-CIPPDBCacheUsers.ps1 | 6 +- .../Public/Set-CIPPDbCacheTestData.ps1 | 4 + 73 files changed, 624 insertions(+), 132 deletions(-) create mode 100644 Modules/CIPPCore/Public/Get-CIPPMailboxRulesReport.ps1 create mode 100644 Modules/CIPPCore/Public/Set-CIPPDBCacheMailboxRules.ps1 diff --git a/Modules/CIPPCore/Public/CippQueue/Update-CippQueueEntry.ps1 b/Modules/CIPPCore/Public/CippQueue/Update-CippQueueEntry.ps1 index e0202b01baf3..8184320508a8 100644 --- a/Modules/CIPPCore/Public/CippQueue/Update-CippQueueEntry.ps1 +++ b/Modules/CIPPCore/Public/CippQueue/Update-CippQueueEntry.ps1 @@ -3,11 +3,13 @@ function Update-CippQueueEntry { .FUNCTIONALITY Internal #> - Param( + param( [Parameter(Mandatory = $true)] $RowKey, $Status, - $Name + $Name, + $TotalTasks, + [switch]$IncrementTotalTasks ) $CippQueue = Get-CippTable -TableName CippQueue @@ -22,6 +24,15 @@ function Update-CippQueueEntry { if ($Name) { $QueueEntry.Name = $Name } + if ($TotalTasks) { + if ($IncrementTotalTasks) { + # Increment the existing total + $QueueEntry.TotalTasks = [int]$QueueEntry.TotalTasks + [int]$TotalTasks + } else { + # Set the total directly + $QueueEntry.TotalTasks = $TotalTasks + } + } Add-CIPPAzDataTableEntity @CippQueue -Entity $QueueEntry -Force $QueueEntry } else { @@ -30,4 +41,4 @@ function Update-CippQueueEntry { } else { return $false } -} \ No newline at end of file +} diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/CIPPDBCache/Push-ExecCIPPDBCache.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/CIPPDBCache/Push-ExecCIPPDBCache.ps1 index a6a68cfc8510..7c4f04c772e0 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/CIPPDBCache/Push-ExecCIPPDBCache.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/CIPPDBCache/Push-ExecCIPPDBCache.ps1 @@ -14,6 +14,7 @@ function Push-ExecCIPPDBCache { $Name = $Item.Name $TenantFilter = $Item.TenantFilter + $QueueId = $Item.QueueId try { Write-Information "Collecting $Name for tenant $TenantFilter" @@ -27,8 +28,18 @@ function Push-ExecCIPPDBCache { throw "Function $FullFunctionName does not exist" } + # Build parameters for the cache function + $CacheFunctionParams = @{ + TenantFilter = $TenantFilter + } + + # Add QueueId if provided + if ($QueueId) { + $CacheFunctionParams.QueueId = $QueueId + } + # Execute the cache function - & $FullFunctionName -TenantFilter $TenantFilter + & $FullFunctionName @CacheFunctionParams Write-Information "Completed $Name for tenant $TenantFilter" return "Successfully executed $Name for tenant $TenantFilter" diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 index 6bdf8f889ddf..2f18e8e92b2d 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 @@ -99,6 +99,7 @@ function Push-CIPPDBCacheData { 'ExoPresetSecurityPolicy' 'ExoTenantAllowBlockList' 'Mailboxes' + 'MailboxRules' 'CASMailboxes' 'MailboxUsage' 'OneDriveUsage' diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ExecCIPPDBCache.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ExecCIPPDBCache.ps1 index 92e4249af55b..dd526d2a574b 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ExecCIPPDBCache.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ExecCIPPDBCache.ps1 @@ -32,31 +32,46 @@ function Invoke-ExecCIPPDBCache { Write-LogMessage -API $APIName -tenant $TenantFilter -message "Starting CIPP DB cache for $Name" -sev Info + # Create queue entry for tracking + $QueueName = if ($TenantFilter -eq 'AllTenants') { + "$Name Cache Sync (All Tenants)" + } else { + "$Name Cache Sync ($TenantFilter)" + } + # Handle AllTenants - create a batch for each tenant if ($TenantFilter -eq 'AllTenants') { $TenantList = Get-Tenants -IncludeErrors + $Queue = New-CippQueueEntry -Name $QueueName -TotalTasks ($TenantList | Measure-Object).Count + $Batch = $TenantList | ForEach-Object { [PSCustomObject]@{ FunctionName = 'ExecCIPPDBCache' + QueueName = "$Name Cache - $($_.defaultDomainName)" Name = $Name TenantFilter = $_.defaultDomainName + QueueId = $Queue.RowKey } } - + $InputObject = [PSCustomObject]@{ Batch = @($Batch) OrchestratorName = "CIPPDBCache_${Name}_AllTenants" SkipLog = $false } - + Write-LogMessage -API $APIName -tenant $TenantFilter -message "Starting CIPP DB cache for $Name across $($TenantList.Count) tenants" -sev Info } else { # Single tenant + $Queue = New-CippQueueEntry -Name $QueueName -TotalTasks 1 + $InputObject = [PSCustomObject]@{ Batch = @([PSCustomObject]@{ + QueueName = "$Name Cache - $TenantFilter" FunctionName = 'ExecCIPPDBCache' Name = $Name TenantFilter = $TenantFilter + QueueId = $Queue.RowKey }) OrchestratorName = "CIPPDBCache_${Name}_$TenantFilter" SkipLog = $false @@ -67,12 +82,19 @@ function Invoke-ExecCIPPDBCache { Write-LogMessage -API $APIName -tenant $TenantFilter -message "Started CIPP DB cache orchestrator for $Name with instance ID: $InstanceId" -sev Info + $ResultsMessage = if ($TenantFilter -eq 'AllTenants') { + "Successfully started cache operation for $Name for all tenants" + } else { + "Successfully started cache operation for $Name on tenant $TenantFilter" + } + $Body = [PSCustomObject]@{ - Results = "Successfully started cache operation for $Name$(if ($TenantFilter -eq 'AllTenants') { ' for all tenants' } else { " on tenant $TenantFilter" })" + Results = $ResultsMessage Metadata = @{ Name = $Name Tenant = $TenantFilter InstanceId = $InstanceId + QueueId = $Queue.RowKey } } $StatusCode = [HttpStatusCode]::OK diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListMailboxRules.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListMailboxRules.ps1 index 648d7251deee..c20d575be6bb 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListMailboxRules.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListMailboxRules.ps1 @@ -9,77 +9,107 @@ function Invoke-ListMailboxRules { param($Request, $TriggerMetadata) # Interact with query parameters or the body of the request. $TenantFilter = $Request.Query.tenantFilter + $UseReportDB = $Request.Query.UseReportDB - $Table = Get-CIPPTable -TableName cachembxrules - if ($TenantFilter -ne 'AllTenants') { - $Table.Filter = "PartitionKey eq 'MailboxRules' and Tenant eq '$TenantFilter'" - } else { - $Table.Filter = "PartitionKey eq 'MailboxRules'" - } - - Write-Information 'Getting cached mailbox rules' - $Rows = Get-CIPPAzDataTableEntity @Table | Where-Object -Property Timestamp -GT (Get-Date).AddHours(-1) - $PartitionKey = 'MailboxRules' - $QueueReference = '{0}-{1}' -f $TenantFilter, $PartitionKey - $RunningQueue = Invoke-ListCippQueue -Reference $QueueReference | Where-Object { $_.Status -notmatch 'Completed' -and $_.Status -notmatch 'Failed' } + try { + # If UseReportDB is specified, retrieve from report database + if ($UseReportDB -eq 'true') { + try { + $GraphRequest = Get-CIPPMailboxRulesReport -TenantFilter $TenantFilter -ErrorAction Stop + $StatusCode = [HttpStatusCode]::OK + } catch { + Write-Host "Error retrieving mailbox rules from report database: $($_.Exception.Message)" + $StatusCode = [HttpStatusCode]::InternalServerError + $GraphRequest = $_.Exception.Message + } - $Metadata = @{} - # If a queue is running, we will not start a new one - if ($RunningQueue -and !$Rows) { - Write-Information "Queue is already running for $TenantFilter" - $Metadata = [PSCustomObject]@{ - QueueMessage = "Still loading data for $TenantFilter. Please check back in a few more minutes" - QueueId = $RunningQueue.RowKey + return ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = @($GraphRequest) + }) } - } elseif ((!$Rows -and !$RunningQueue) -or ($TenantFilter -eq 'AllTenants' -and ($Rows | Measure-Object).Count -eq 1)) { - Write-Information "No cached mailbox rules found for $TenantFilter, starting new orchestration" - if ($TenantFilter -eq 'AllTenants') { - $Tenants = Get-Tenants -IncludeErrors | Select-Object defaultDomainName - $Type = 'All Tenants' + + # Original cache table logic + $Table = Get-CIPPTable -TableName cachembxrules + if ($TenantFilter -ne 'AllTenants') { + $Table.Filter = "PartitionKey eq 'MailboxRules' and Tenant eq '$TenantFilter'" } else { - $Tenants = @(@{ defaultDomainName = $TenantFilter }) - $Type = $TenantFilter - } - $Queue = New-CippQueueEntry -Name "Mailbox Rules ($Type)" -Reference $QueueReference -TotalTasks ($Tenants | Measure-Object).Count - # If no rows are found and no queue is running, we will start a new one - $Metadata = [PSCustomObject]@{ - QueueMessage = "Loading data for $TenantFilter. Please check back in 1 minute" - QueueId = $Queue.RowKey + $Table.Filter = "PartitionKey eq 'MailboxRules'" } - $Batch = $Tenants | Select-Object defaultDomainName, @{Name = 'FunctionName'; Expression = { 'ListMailboxRulesQueue' } }, @{Name = 'QueueName'; Expression = { $_.defaultDomainName } }, @{Name = 'QueueId'; Expression = { $Queue.RowKey } } - if (($Batch | Measure-Object).Count -gt 0) { - $InputObject = [PSCustomObject]@{ - OrchestratorName = 'ListMailboxRulesOrchestrator' - Batch = @($Batch) - SkipLog = $true + Write-Information 'Getting cached mailbox rules' + $Rows = Get-CIPPAzDataTableEntity @Table | Where-Object -Property Timestamp -GT (Get-Date).AddHours(-1) + $PartitionKey = 'MailboxRules' + $QueueReference = '{0}-{1}' -f $TenantFilter, $PartitionKey + $RunningQueue = Invoke-ListCippQueue -Reference $QueueReference | Where-Object { $_.Status -notmatch 'Completed' -and $_.Status -notmatch 'Failed' } + + $Metadata = @{} + # If a queue is running, we will not start a new one + if ($RunningQueue -and !$Rows) { + Write-Information "Queue is already running for $TenantFilter" + $Metadata = [PSCustomObject]@{ + QueueMessage = "Still loading data for $TenantFilter. Please check back in a few more minutes" + QueueId = $RunningQueue.RowKey + } + } elseif ((!$Rows -and !$RunningQueue) -or ($TenantFilter -eq 'AllTenants' -and ($Rows | Measure-Object).Count -eq 1)) { + Write-Information "No cached mailbox rules found for $TenantFilter, starting new orchestration" + if ($TenantFilter -eq 'AllTenants') { + $Tenants = Get-Tenants -IncludeErrors | Select-Object defaultDomainName + $Type = 'All Tenants' + } else { + $Tenants = @(@{ defaultDomainName = $TenantFilter }) + $Type = $TenantFilter + } + $Queue = New-CippQueueEntry -Name "Mailbox Rules ($Type)" -Reference $QueueReference -TotalTasks ($Tenants | Measure-Object).Count + # If no rows are found and no queue is running, we will start a new one + $Metadata = [PSCustomObject]@{ + QueueMessage = "Loading data for $TenantFilter. Please check back in 1 minute" + QueueId = $Queue.RowKey } - #Write-Host ($InputObject | ConvertTo-Json) - Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Depth 5 -Compress) - Write-Host "Started mailbox rules orchestration with ID = '$InstanceId'" - } - } else { - $Metadata = [PSCustomObject]@{ - QueueId = $RunningQueue.RowKey ?? $null + $Batch = $Tenants | Select-Object defaultDomainName, @{Name = 'FunctionName'; Expression = { 'ListMailboxRulesQueue' } }, @{Name = 'QueueName'; Expression = { $_.defaultDomainName } }, @{Name = 'QueueId'; Expression = { $Queue.RowKey } } + if (($Batch | Measure-Object).Count -gt 0) { + $InputObject = [PSCustomObject]@{ + OrchestratorName = 'ListMailboxRulesOrchestrator' + Batch = @($Batch) + SkipLog = $true + } + #Write-Host ($InputObject | ConvertTo-Json) + Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Depth 5 -Compress) + Write-Host "Started mailbox rules orchestration with ID = '$InstanceId'" + } + + } else { + $Metadata = [PSCustomObject]@{ + QueueId = $RunningQueue.RowKey ?? $null + } + $GraphRequest = $Rows | ForEach-Object { + $NewObj = $_.Rules | ConvertFrom-Json -ErrorAction SilentlyContinue + $NewObj | Add-Member -NotePropertyName 'Tenant' -NotePropertyValue $_.Tenant -Force + $NewObj + } } - $GraphRequest = $Rows | ForEach-Object { - $NewObj = $_.Rules | ConvertFrom-Json -ErrorAction SilentlyContinue - $NewObj | Add-Member -NotePropertyName 'Tenant' -NotePropertyValue $_.Tenant -Force - $NewObj + + # If no results are found, we will return an empty message to prevent null reference errors in the frontend + $GraphRequest = $GraphRequest ?? @() + $Body = @{ + Results = @($GraphRequest) + Metadata = $Metadata } - } - # If no results are found, we will return an empty message to prevent null reference errors in the frontend - $GraphRequest = $GraphRequest ?? @() - $Body = @{ - Results = @($GraphRequest) - Metadata = $Metadata - } + return ([HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::OK + Body = $Body + }) - return ([HttpResponseContext]@{ - StatusCode = [HttpStatusCode]::OK - Body = $Body - }) + } catch { + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + $StatusCode = [HttpStatusCode]::Forbidden + $GraphRequest = $ErrorMessage + return ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = @($GraphRequest) + }) + } } diff --git a/Modules/CIPPCore/Public/Get-CIPPMailboxRulesReport.ps1 b/Modules/CIPPCore/Public/Get-CIPPMailboxRulesReport.ps1 new file mode 100644 index 000000000000..641e97965452 --- /dev/null +++ b/Modules/CIPPCore/Public/Get-CIPPMailboxRulesReport.ps1 @@ -0,0 +1,82 @@ +function Get-CIPPMailboxRulesReport { + <# + .SYNOPSIS + Generates a mailbox rules report from the CIPP Reporting database + + .DESCRIPTION + Retrieves mailbox rules data for a tenant from the reporting database + + .PARAMETER TenantFilter + The tenant to generate the report for + + .EXAMPLE + Get-CIPPMailboxRulesReport -TenantFilter 'contoso.onmicrosoft.com' + Gets mailbox rules for all users in the tenant + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter + ) + + try { + + # Handle AllTenants + if ($TenantFilter -eq 'AllTenants') { + # Get all tenants that have mailbox rules data + $AllRulesItems = Get-CIPPDbItem -TenantFilter 'allTenants' -Type 'MailboxRules' + $Tenants = @($AllRulesItems | Where-Object { $_.RowKey -ne 'MailboxRules-Count' } | Select-Object -ExpandProperty PartitionKey -Unique) + + $TenantList = Get-Tenants -IncludeErrors + $Tenants = $Tenants | Where-Object { $TenantList.defaultDomainName -contains $_ } + + $AllResults = [System.Collections.Generic.List[PSCustomObject]]::new() + foreach ($Tenant in $Tenants) { + try { + $TenantResults = Get-CIPPMailboxRulesReport -TenantFilter $Tenant + foreach ($Result in $TenantResults) { + # Add Tenant property to each result if not already present + if (-not $Result.Tenant) { + $Result | Add-Member -NotePropertyName 'Tenant' -NotePropertyValue $Tenant -Force + } + $AllResults.Add($Result) + } + } catch { + Write-LogMessage -API 'MailboxRulesReport' -tenant $Tenant -message "Failed to get report for tenant: $($_.Exception.Message)" -sev Warning + } + } + return $AllResults + } + + # Get mailbox rules from reporting DB + $RulesItems = Get-CIPPDbItem -TenantFilter $TenantFilter -Type 'MailboxRules' | Where-Object { $_.RowKey -ne 'MailboxRules-Count' } + if (-not $RulesItems) { + throw 'No mailbox rules data found in reporting database. Sync the report data first.' + } + + # Get the most recent cache timestamp + $CacheTimestamp = ($RulesItems | Where-Object { $_.Timestamp } | Sort-Object Timestamp -Descending | Select-Object -First 1).Timestamp + + # Parse mailbox rules data + $AllRules = [System.Collections.Generic.List[PSCustomObject]]::new() + foreach ($Item in $RulesItems | Where-Object { $_.RowKey -ne 'MailboxRules-Count' }) { + $Rule = $Item.Data | ConvertFrom-Json + + # Add cache timestamp to the rule + $Rule | Add-Member -NotePropertyName 'CacheTimestamp' -NotePropertyValue $CacheTimestamp -Force -ErrorAction SilentlyContinue + + # Ensure Tenant property is set + if (-not $Rule.Tenant) { + $Rule | Add-Member -NotePropertyName 'Tenant' -NotePropertyValue $TenantFilter -Force -ErrorAction SilentlyContinue + } + + $AllRules.Add($Rule) + } + + return $AllRules + + } catch { + Write-LogMessage -API 'MailboxRulesReport' -tenant $TenantFilter -message "Failed to get mailbox rules report: $($_.Exception.Message)" -sev Error -LogData (Get-CippException -Exception $_) + throw $_ + } +} diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheAdminConsentRequestPolicy.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheAdminConsentRequestPolicy.ps1 index 945d69f854cd..32f3e523e6a8 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheAdminConsentRequestPolicy.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheAdminConsentRequestPolicy.ps1 @@ -5,11 +5,15 @@ function Set-CIPPDBCacheAdminConsentRequestPolicy { .PARAMETER TenantFilter The tenant to cache consent policy for + + .PARAMETER QueueId + The queue ID to update with total tasks (optional) #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] - [string]$TenantFilter + [string]$TenantFilter, + [string]$QueueId ) try { diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheAppRoleAssignments.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheAppRoleAssignments.ps1 index 3ba077a2cdeb..93433d6c27f9 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheAppRoleAssignments.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheAppRoleAssignments.ps1 @@ -5,11 +5,15 @@ function Set-CIPPDBCacheAppRoleAssignments { .PARAMETER TenantFilter The tenant to cache app role assignments for + + .PARAMETER QueueId + The queue ID to update with total tasks (optional) #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] - [string]$TenantFilter + [string]$TenantFilter, + [string]$QueueId ) try { diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheApps.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheApps.ps1 index a0f1c667be2f..ef8acc12acf4 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheApps.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheApps.ps1 @@ -5,11 +5,15 @@ function Set-CIPPDBCacheApps { .PARAMETER TenantFilter The tenant to cache applications for + + .PARAMETER QueueId + The queue ID to update with total tasks (optional) #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] - [string]$TenantFilter + [string]$TenantFilter, + [string]$QueueId ) try { diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheAuthenticationFlowsPolicy.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheAuthenticationFlowsPolicy.ps1 index 7b75a2b23eaa..da9738675345 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheAuthenticationFlowsPolicy.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheAuthenticationFlowsPolicy.ps1 @@ -5,11 +5,15 @@ function Set-CIPPDBCacheAuthenticationFlowsPolicy { .PARAMETER TenantFilter The tenant to cache authentication flows policy for + + .PARAMETER QueueId + The queue ID to update with total tasks (optional) #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] - [string]$TenantFilter + [string]$TenantFilter, + [string]$QueueId ) try { diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheAuthenticationMethodsPolicy.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheAuthenticationMethodsPolicy.ps1 index 98ea20dd05d7..a4146e9c6c69 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheAuthenticationMethodsPolicy.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheAuthenticationMethodsPolicy.ps1 @@ -5,11 +5,15 @@ function Set-CIPPDBCacheAuthenticationMethodsPolicy { .PARAMETER TenantFilter The tenant to cache authentication methods policy for + + .PARAMETER QueueId + The queue ID to update with total tasks (optional) #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] - [string]$TenantFilter + [string]$TenantFilter, + [string]$QueueId ) try { diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheAuthorizationPolicy.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheAuthorizationPolicy.ps1 index ca6c92bfe624..d416c3fb2dd9 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheAuthorizationPolicy.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheAuthorizationPolicy.ps1 @@ -5,11 +5,15 @@ function Set-CIPPDBCacheAuthorizationPolicy { .PARAMETER TenantFilter The tenant to cache authorization policy for + + .PARAMETER QueueId + The queue ID to update with total tasks (optional) #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] - [string]$TenantFilter + [string]$TenantFilter, + [string]$QueueId ) try { diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheB2BManagementPolicy.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheB2BManagementPolicy.ps1 index f00d7d4c8fc0..d970a12e2953 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheB2BManagementPolicy.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheB2BManagementPolicy.ps1 @@ -5,11 +5,15 @@ function Set-CIPPDBCacheB2BManagementPolicy { .PARAMETER TenantFilter The tenant to cache B2B management policy for + + .PARAMETER QueueId + The queue ID to update with total tasks (optional) #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] - [string]$TenantFilter + [string]$TenantFilter, + [string]$QueueId ) try { diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheCASMailboxes.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheCASMailboxes.ps1 index 02c12125f6e4..3912da85e7c3 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheCASMailboxes.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheCASMailboxes.ps1 @@ -5,11 +5,15 @@ function Set-CIPPDBCacheCASMailboxes { .PARAMETER TenantFilter The tenant to cache CAS mailboxes for + + .PARAMETER QueueId + The queue ID to update with total tasks (optional) #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] - [string]$TenantFilter + [string]$TenantFilter, + [string]$QueueId ) try { diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheConditionalAccessPolicies.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheConditionalAccessPolicies.ps1 index 729644ed5d40..bb516877c6c8 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheConditionalAccessPolicies.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheConditionalAccessPolicies.ps1 @@ -5,11 +5,15 @@ function Set-CIPPDBCacheConditionalAccessPolicies { .PARAMETER TenantFilter The tenant to cache CA policies for + + .PARAMETER QueueId + The queue ID to update with total tasks (optional) #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] - [string]$TenantFilter + [string]$TenantFilter, + [string]$QueueId ) try { diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheCredentialUserRegistrationDetails.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheCredentialUserRegistrationDetails.ps1 index 888c398ffea3..5242dffdbbf7 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheCredentialUserRegistrationDetails.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheCredentialUserRegistrationDetails.ps1 @@ -5,11 +5,15 @@ function Set-CIPPDBCacheCredentialUserRegistrationDetails { .PARAMETER TenantFilter The tenant to cache credential user registration details for + + .PARAMETER QueueId + The queue ID to update with total tasks (optional) #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] - [string]$TenantFilter + [string]$TenantFilter, + [string]$QueueId ) try { diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheCrossTenantAccessPolicy.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheCrossTenantAccessPolicy.ps1 index cc4203420b96..4fe87f882ee1 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheCrossTenantAccessPolicy.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheCrossTenantAccessPolicy.ps1 @@ -5,11 +5,15 @@ function Set-CIPPDBCacheCrossTenantAccessPolicy { .PARAMETER TenantFilter The tenant to cache policy for + + .PARAMETER QueueId + The queue ID to update with total tasks (optional) #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] - [string]$TenantFilter + [string]$TenantFilter, + [string]$QueueId ) try { diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheDefaultAppManagementPolicy.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheDefaultAppManagementPolicy.ps1 index c053f36435a4..b0730af22a26 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheDefaultAppManagementPolicy.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheDefaultAppManagementPolicy.ps1 @@ -5,11 +5,15 @@ function Set-CIPPDBCacheDefaultAppManagementPolicy { .PARAMETER TenantFilter The tenant to cache policy for + + .PARAMETER QueueId + The queue ID to update with total tasks (optional) #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] - [string]$TenantFilter + [string]$TenantFilter, + [string]$QueueId ) try { diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheDeviceRegistrationPolicy.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheDeviceRegistrationPolicy.ps1 index 3a9eada7c7a6..c9f445cd62d8 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheDeviceRegistrationPolicy.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheDeviceRegistrationPolicy.ps1 @@ -5,11 +5,15 @@ function Set-CIPPDBCacheDeviceRegistrationPolicy { .PARAMETER TenantFilter The tenant to cache device registration policy for + + .PARAMETER QueueId + The queue ID to update with total tasks (optional) #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] - [string]$TenantFilter + [string]$TenantFilter, + [string]$QueueId ) try { diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheDeviceSettings.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheDeviceSettings.ps1 index 7845f72ffa8a..5a0a3dbe9207 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheDeviceSettings.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheDeviceSettings.ps1 @@ -5,11 +5,15 @@ function Set-CIPPDBCacheDeviceSettings { .PARAMETER TenantFilter The tenant to cache device settings for + + .PARAMETER QueueId + The queue ID to update with total tasks (optional) #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] - [string]$TenantFilter + [string]$TenantFilter, + [string]$QueueId ) try { diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheDevices.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheDevices.ps1 index 63996ee51294..c0d056617209 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheDevices.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheDevices.ps1 @@ -5,11 +5,15 @@ function Set-CIPPDBCacheDevices { .PARAMETER TenantFilter The tenant to cache devices for + + .PARAMETER QueueId + The queue ID to update with total tasks (optional) #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] - [string]$TenantFilter + [string]$TenantFilter, + [string]$QueueId ) try { diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheDirectoryRecommendations.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheDirectoryRecommendations.ps1 index 616c7ab82503..1a1e973cd695 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheDirectoryRecommendations.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheDirectoryRecommendations.ps1 @@ -5,11 +5,15 @@ function Set-CIPPDBCacheDirectoryRecommendations { .PARAMETER TenantFilter The tenant to cache recommendations for + + .PARAMETER QueueId + The queue ID to update with total tasks (optional) #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] - [string]$TenantFilter + [string]$TenantFilter, + [string]$QueueId ) try { diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheDomains.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheDomains.ps1 index b546382b3103..d15be42d43fe 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheDomains.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheDomains.ps1 @@ -5,11 +5,15 @@ function Set-CIPPDBCacheDomains { .PARAMETER TenantFilter The tenant to cache domains for + + .PARAMETER QueueId + The queue ID to update with total tasks (optional) #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] - [string]$TenantFilter + [string]$TenantFilter, + [string]$QueueId ) try { diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoAcceptedDomains.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoAcceptedDomains.ps1 index cbe7bd854481..b2af3cdbe3b8 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoAcceptedDomains.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoAcceptedDomains.ps1 @@ -5,11 +5,15 @@ function Set-CIPPDBCacheExoAcceptedDomains { .PARAMETER TenantFilter The tenant to cache accepted domains for + + .PARAMETER QueueId + The queue ID to update with total tasks (optional) #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] - [string]$TenantFilter + [string]$TenantFilter, + [string]$QueueId ) try { diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoAdminAuditLogConfig.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoAdminAuditLogConfig.ps1 index 892e471647fe..2c0b2a10e863 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoAdminAuditLogConfig.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoAdminAuditLogConfig.ps1 @@ -5,11 +5,15 @@ function Set-CIPPDBCacheExoAdminAuditLogConfig { .PARAMETER TenantFilter The tenant to cache admin audit log config for + + .PARAMETER QueueId + The queue ID to update with total tasks (optional) #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] - [string]$TenantFilter + [string]$TenantFilter, + [string]$QueueId ) try { diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoAntiPhishPolicies.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoAntiPhishPolicies.ps1 index 98a525d1f0a0..f35ba843e566 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoAntiPhishPolicies.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoAntiPhishPolicies.ps1 @@ -5,11 +5,15 @@ function Set-CIPPDBCacheExoAntiPhishPolicies { .PARAMETER TenantFilter The tenant to cache Anti-Phishing data for + + .PARAMETER QueueId + The queue ID to update with total tasks (optional) #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] - [string]$TenantFilter + [string]$TenantFilter, + [string]$QueueId ) try { diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoAntiPhishPolicy.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoAntiPhishPolicy.ps1 index b65b084e9ae1..ea5ce3b211aa 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoAntiPhishPolicy.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoAntiPhishPolicy.ps1 @@ -5,11 +5,15 @@ function Set-CIPPDBCacheExoAntiPhishPolicy { .PARAMETER TenantFilter The tenant to cache Anti-Phish policy data for + + .PARAMETER QueueId + The queue ID to update with total tasks (optional) #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] - [string]$TenantFilter + [string]$TenantFilter, + [string]$QueueId ) try { diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoAtpPolicyForO365.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoAtpPolicyForO365.ps1 index ee0b2203fa0b..c48fd4baab3a 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoAtpPolicyForO365.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoAtpPolicyForO365.ps1 @@ -5,11 +5,15 @@ function Set-CIPPDBCacheExoAtpPolicyForO365 { .PARAMETER TenantFilter The tenant to cache ATP policy data for + + .PARAMETER QueueId + The queue ID to update with total tasks (optional) #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] - [string]$TenantFilter + [string]$TenantFilter, + [string]$QueueId ) try { diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoDkimSigningConfig.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoDkimSigningConfig.ps1 index fb72c35dec68..aa7ea3bd4561 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoDkimSigningConfig.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoDkimSigningConfig.ps1 @@ -5,11 +5,15 @@ function Set-CIPPDBCacheExoDkimSigningConfig { .PARAMETER TenantFilter The tenant to cache DKIM configuration for + + .PARAMETER QueueId + The queue ID to update with total tasks (optional) #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] - [string]$TenantFilter + [string]$TenantFilter, + [string]$QueueId ) try { diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoHostedContentFilterPolicy.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoHostedContentFilterPolicy.ps1 index b6bb5fd5c571..cd71f9a00bc5 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoHostedContentFilterPolicy.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoHostedContentFilterPolicy.ps1 @@ -5,11 +5,15 @@ function Set-CIPPDBCacheExoHostedContentFilterPolicy { .PARAMETER TenantFilter The tenant to cache Hosted Content Filter data for + + .PARAMETER QueueId + The queue ID to update with total tasks (optional) #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] - [string]$TenantFilter + [string]$TenantFilter, + [string]$QueueId ) try { diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoHostedOutboundSpamFilterPolicy.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoHostedOutboundSpamFilterPolicy.ps1 index 06201cd38b63..a550c9031c8e 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoHostedOutboundSpamFilterPolicy.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoHostedOutboundSpamFilterPolicy.ps1 @@ -5,11 +5,15 @@ function Set-CIPPDBCacheExoHostedOutboundSpamFilterPolicy { .PARAMETER TenantFilter The tenant to cache Hosted Outbound Spam Filter data for + + .PARAMETER QueueId + The queue ID to update with total tasks (optional) #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] - [string]$TenantFilter + [string]$TenantFilter, + [string]$QueueId ) try { diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoMalwareFilterPolicies.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoMalwareFilterPolicies.ps1 index f50d64d610b8..23f6b1f775ef 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoMalwareFilterPolicies.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoMalwareFilterPolicies.ps1 @@ -5,11 +5,15 @@ function Set-CIPPDBCacheExoMalwareFilterPolicies { .PARAMETER TenantFilter The tenant to cache Malware Filter data for + + .PARAMETER QueueId + The queue ID to update with total tasks (optional) #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] - [string]$TenantFilter + [string]$TenantFilter, + [string]$QueueId ) try { diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoMalwareFilterPolicy.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoMalwareFilterPolicy.ps1 index 194501e09e9a..ff2ba00a090d 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoMalwareFilterPolicy.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoMalwareFilterPolicy.ps1 @@ -5,11 +5,15 @@ function Set-CIPPDBCacheExoMalwareFilterPolicy { .PARAMETER TenantFilter The tenant to cache Malware Filter policy data for + + .PARAMETER QueueId + The queue ID to update with total tasks (optional) #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] - [string]$TenantFilter + [string]$TenantFilter, + [string]$QueueId ) try { diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoOrganizationConfig.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoOrganizationConfig.ps1 index 6138bd21a3e3..27a8c481a7da 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoOrganizationConfig.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoOrganizationConfig.ps1 @@ -5,11 +5,15 @@ function Set-CIPPDBCacheExoOrganizationConfig { .PARAMETER TenantFilter The tenant to cache organization configuration for + + .PARAMETER QueueId + The queue ID to update with total tasks (optional) #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] - [string]$TenantFilter + [string]$TenantFilter, + [string]$QueueId ) try { diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoPresetSecurityPolicy.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoPresetSecurityPolicy.ps1 index a10092fba9ce..3d9def8909c5 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoPresetSecurityPolicy.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoPresetSecurityPolicy.ps1 @@ -5,11 +5,15 @@ function Set-CIPPDBCacheExoPresetSecurityPolicy { .PARAMETER TenantFilter The tenant to cache preset security policies for + + .PARAMETER QueueId + The queue ID to update with total tasks (optional) #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] - [string]$TenantFilter + [string]$TenantFilter, + [string]$QueueId ) try { diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoQuarantinePolicy.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoQuarantinePolicy.ps1 index 2ef8bf63639a..b4bcdd7ac011 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoQuarantinePolicy.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoQuarantinePolicy.ps1 @@ -5,11 +5,15 @@ function Set-CIPPDBCacheExoQuarantinePolicy { .PARAMETER TenantFilter The tenant to cache Quarantine policy data for + + .PARAMETER QueueId + The queue ID to update with total tasks (optional) #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] - [string]$TenantFilter + [string]$TenantFilter, + [string]$QueueId ) try { diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoRemoteDomain.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoRemoteDomain.ps1 index 692ba803c6de..e9b32337a9ee 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoRemoteDomain.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoRemoteDomain.ps1 @@ -5,11 +5,15 @@ function Set-CIPPDBCacheExoRemoteDomain { .PARAMETER TenantFilter The tenant to cache Remote Domain data for + + .PARAMETER QueueId + The queue ID to update with total tasks (optional) #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] - [string]$TenantFilter + [string]$TenantFilter, + [string]$QueueId ) try { diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoSafeAttachmentPolicies.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoSafeAttachmentPolicies.ps1 index 172e86887e66..28d8f587afc3 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoSafeAttachmentPolicies.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoSafeAttachmentPolicies.ps1 @@ -5,11 +5,15 @@ function Set-CIPPDBCacheExoSafeAttachmentPolicies { .PARAMETER TenantFilter The tenant to cache Safe Attachment data for + + .PARAMETER QueueId + The queue ID to update with total tasks (optional) #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] - [string]$TenantFilter + [string]$TenantFilter, + [string]$QueueId ) try { diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoSafeAttachmentPolicy.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoSafeAttachmentPolicy.ps1 index 6ebe371e5291..fc68dea68d01 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoSafeAttachmentPolicy.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoSafeAttachmentPolicy.ps1 @@ -5,11 +5,15 @@ function Set-CIPPDBCacheExoSafeAttachmentPolicy { .PARAMETER TenantFilter The tenant to cache Safe Attachment policy data for + + .PARAMETER QueueId + The queue ID to update with total tasks (optional) #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] - [string]$TenantFilter + [string]$TenantFilter, + [string]$QueueId ) try { diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoSafeLinksPolicies.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoSafeLinksPolicies.ps1 index c06fb2f08971..f78468844344 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoSafeLinksPolicies.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoSafeLinksPolicies.ps1 @@ -5,11 +5,15 @@ function Set-CIPPDBCacheExoSafeLinksPolicies { .PARAMETER TenantFilter The tenant to cache Safe Links data for + + .PARAMETER QueueId + The queue ID to update with total tasks (optional) #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] - [string]$TenantFilter + [string]$TenantFilter, + [string]$QueueId ) try { diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoSafeLinksPolicy.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoSafeLinksPolicy.ps1 index a3252b245bcd..5498a9cb5385 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoSafeLinksPolicy.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoSafeLinksPolicy.ps1 @@ -5,11 +5,15 @@ function Set-CIPPDBCacheExoSafeLinksPolicy { .PARAMETER TenantFilter The tenant to cache Safe Links policy data for + + .PARAMETER QueueId + The queue ID to update with total tasks (optional) #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] - [string]$TenantFilter + [string]$TenantFilter, + [string]$QueueId ) try { diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoSharingPolicy.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoSharingPolicy.ps1 index bd4c28da68f5..ffaaed92ed0a 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoSharingPolicy.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoSharingPolicy.ps1 @@ -5,11 +5,15 @@ function Set-CIPPDBCacheExoSharingPolicy { .PARAMETER TenantFilter The tenant to cache sharing policies for + + .PARAMETER QueueId + The queue ID to update with total tasks (optional) #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] - [string]$TenantFilter + [string]$TenantFilter, + [string]$QueueId ) try { diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoTenantAllowBlockList.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoTenantAllowBlockList.ps1 index 62b4385a5c8a..3906bf3d55f8 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoTenantAllowBlockList.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoTenantAllowBlockList.ps1 @@ -5,11 +5,15 @@ function Set-CIPPDBCacheExoTenantAllowBlockList { .PARAMETER TenantFilter The tenant to cache tenant allow/block list for + + .PARAMETER QueueId + The queue ID to update with total tasks (optional) #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] - [string]$TenantFilter + [string]$TenantFilter, + [string]$QueueId ) try { diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoTransportRules.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoTransportRules.ps1 index 6f83273af842..f08860ac00c6 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheExoTransportRules.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheExoTransportRules.ps1 @@ -5,11 +5,15 @@ function Set-CIPPDBCacheExoTransportRules { .PARAMETER TenantFilter The tenant to cache Transport Rules for + + .PARAMETER QueueId + The queue ID to update with total tasks (optional) #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] - [string]$TenantFilter + [string]$TenantFilter, + [string]$QueueId ) try { diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheGroups.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheGroups.ps1 index d3be98c3f17e..b153085bc954 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheGroups.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheGroups.ps1 @@ -5,11 +5,15 @@ function Set-CIPPDBCacheGroups { .PARAMETER TenantFilter The tenant to cache groups for + + .PARAMETER QueueId + The queue ID to update with total tasks (optional) #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] - [string]$TenantFilter + [string]$TenantFilter, + [string]$QueueId ) try { diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheGuests.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheGuests.ps1 index 36abaabef11d..867d45b791cd 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheGuests.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheGuests.ps1 @@ -5,11 +5,15 @@ function Set-CIPPDBCacheGuests { .PARAMETER TenantFilter The tenant to cache guest users for + + .PARAMETER QueueId + The queue ID to update with total tasks (optional) #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] - [string]$TenantFilter + [string]$TenantFilter, + [string]$QueueId ) try { diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheIntuneAppProtectionPolicies.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheIntuneAppProtectionPolicies.ps1 index 1906b3bcdfda..c43e17e6f389 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheIntuneAppProtectionPolicies.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheIntuneAppProtectionPolicies.ps1 @@ -5,11 +5,15 @@ function Set-CIPPDBCacheIntuneAppProtectionPolicies { .PARAMETER TenantFilter The tenant to cache app protection policies for + + .PARAMETER QueueId + The queue ID to update with total tasks (optional) #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] - [string]$TenantFilter + [string]$TenantFilter, + [string]$QueueId ) try { diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheIntunePolicies.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheIntunePolicies.ps1 index 51c2642f1397..c20f90891d1e 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheIntunePolicies.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheIntunePolicies.ps1 @@ -5,11 +5,15 @@ function Set-CIPPDBCacheIntunePolicies { .PARAMETER TenantFilter The tenant to cache Intune policies for + + .PARAMETER QueueId + The queue ID to update with total tasks (optional) #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] - [string]$TenantFilter + [string]$TenantFilter, + [string]$QueueId ) try { diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheLicenseOverview.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheLicenseOverview.ps1 index 5ba1ef461f0b..a4034eac1c56 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheLicenseOverview.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheLicenseOverview.ps1 @@ -5,11 +5,15 @@ function Set-CIPPDBCacheLicenseOverview { .PARAMETER TenantFilter The tenant to cache license overview for + + .PARAMETER QueueId + The queue ID to update with total tasks (optional) #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] - [string]$TenantFilter + [string]$TenantFilter, + [string]$QueueId ) try { diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheMFAState.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheMFAState.ps1 index 40fd5bb12ddf..ccd489fe14ce 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheMFAState.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheMFAState.ps1 @@ -5,11 +5,15 @@ function Set-CIPPDBCacheMFAState { .PARAMETER TenantFilter The tenant to cache MFA state for + + .PARAMETER QueueId + The queue ID to update with total tasks (optional) #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] - [string]$TenantFilter + [string]$TenantFilter, + [string]$QueueId ) try { diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheMailboxRules.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheMailboxRules.ps1 new file mode 100644 index 000000000000..e99c47bdf7a4 --- /dev/null +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheMailboxRules.ps1 @@ -0,0 +1,60 @@ +function Set-CIPPDBCacheMailboxRules { + <# + .SYNOPSIS + Caches mailbox rules for a tenant + + .PARAMETER TenantFilter + The tenant to cache mailbox rules for + + .PARAMETER QueueId + The queue ID to update with total tasks (optional) + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter, + [string]$QueueId + ) + + try { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching mailbox rules' -sev Debug + + # Get mailboxes + $Mailboxes = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-Mailbox' -Select 'userPrincipalName,GUID' + $Request = $Mailboxes | ForEach-Object { + @{ + OperationGuid = $_.UserPrincipalName + CmdletInput = @{ + CmdletName = 'Get-InboxRule' + Parameters = @{ + Mailbox = $_.UserPrincipalName + } + } + } + } + + $Rules = New-ExoBulkRequest -tenantid $TenantFilter -cmdletArray @($Request) | Where-Object { $_.Identity } + + if (($Rules | Measure-Object).Count -gt 0) { + $MailboxRules = foreach ($Rule in $Rules) { + $Rule | Add-Member -NotePropertyName 'Tenant' -NotePropertyValue $TenantFilter -Force + $Rule | Add-Member -NotePropertyName 'UserPrincipalName' -NotePropertyValue $Rule.OperationGuid -Force + $Rule + } + + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'MailboxRules' -Data @($MailboxRules) + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'MailboxRules' -Data @($MailboxRules) -Count + + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($MailboxRules.Count) mailbox rules successfully" -sev Debug + } else { + # Cache empty result to indicate successful check with no rules + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'MailboxRules' -Data @() + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'MailboxRules' -Data @() -Count + + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'No mailbox rules found' -sev Debug + } + + } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache mailbox rules: $($_.Exception.Message)" -sev Error -LogData (Get-CippException -Exception $_) + } +} diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheMailboxUsage.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheMailboxUsage.ps1 index 0b8e91fbc176..bd13f0b2da02 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheMailboxUsage.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheMailboxUsage.ps1 @@ -5,11 +5,15 @@ function Set-CIPPDBCacheMailboxUsage { .PARAMETER TenantFilter The tenant to cache mailbox usage for + + .PARAMETER QueueId + The queue ID to update with total tasks (optional) #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] - [string]$TenantFilter + [string]$TenantFilter, + [string]$QueueId ) try { diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheMailboxes.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheMailboxes.ps1 index 3716fa9a5c06..e965fa7e1e7b 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheMailboxes.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheMailboxes.ps1 @@ -5,11 +5,15 @@ function Set-CIPPDBCacheMailboxes { .PARAMETER TenantFilter The tenant to cache mailboxes for + + .PARAMETER QueueId + The queue ID to update with total tasks #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] - [string]$TenantFilter + [string]$TenantFilter, + [string]$QueueId ) try { @@ -68,6 +72,7 @@ function Set-CIPPDBCacheMailboxes { # Add mailbox permissions batch $Batches.Add([PSCustomObject]@{ FunctionName = 'GetMailboxPermissionsBatch' + QueueName = "Mailbox Permissions Batch $BatchNumber/$TotalBatches - $TenantFilter" TenantFilter = $TenantFilter Mailboxes = $BatchMailboxUPNs BatchNumber = $BatchNumber @@ -77,6 +82,7 @@ function Set-CIPPDBCacheMailboxes { # Add calendar permissions batch for the same mailboxes $Batches.Add([PSCustomObject]@{ FunctionName = 'GetCalendarPermissionsBatch' + QueueName = "Calendar Permissions Batch $BatchNumber/$TotalBatches - $TenantFilter" TenantFilter = $TenantFilter Mailboxes = $BatchMailboxUPNs BatchNumber = $BatchNumber @@ -88,6 +94,12 @@ function Set-CIPPDBCacheMailboxes { $MailboxPermBatches = $Batches | Where-Object { $_.FunctionName -eq 'GetMailboxPermissionsBatch' } $CalendarPermBatches = $Batches | Where-Object { $_.FunctionName -eq 'GetCalendarPermissionsBatch' } + # Update queue with additional tasks if QueueId is provided + if ($QueueId) { + Update-CippQueueEntry -RowKey $QueueId -TotalTasks $Batches.Count -IncrementTotalTasks + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Updated queue $QueueId with $($Batches.Count) additional tasks" -sev Debug + } + # Start single orchestrator for both mailbox and calendar permissions $InputObject = [PSCustomObject]@{ Batch = @($Batches) @@ -99,6 +111,12 @@ function Set-CIPPDBCacheMailboxes { } } } + if ($QueueId) { + # Add QueueId to each batch item + foreach ($Batch in $Batches) { + $Batch | Add-Member -NotePropertyName 'QueueId' -NotePropertyValue $QueueId -Force + } + } Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Compress -Depth 5) Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Started mailbox and calendar permission caching orchestrator with $($Batches.Count) batches" -sev Debug } else { diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheManagedDeviceEncryptionStates.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheManagedDeviceEncryptionStates.ps1 index 4e4f78f33c2a..7dd8764854ff 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheManagedDeviceEncryptionStates.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheManagedDeviceEncryptionStates.ps1 @@ -5,11 +5,15 @@ function Set-CIPPDBCacheManagedDeviceEncryptionStates { .PARAMETER TenantFilter The tenant to cache managed device encryption states for + + .PARAMETER QueueId + The queue ID to update with total tasks (optional) #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] - [string]$TenantFilter + [string]$TenantFilter, + [string]$QueueId ) try { diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheManagedDevices.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheManagedDevices.ps1 index ee724f9343af..509a136a4fec 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheManagedDevices.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheManagedDevices.ps1 @@ -5,11 +5,15 @@ function Set-CIPPDBCacheManagedDevices { .PARAMETER TenantFilter The tenant to cache managed devices for + + .PARAMETER QueueId + The queue ID to update with total tasks (optional) #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] - [string]$TenantFilter + [string]$TenantFilter, + [string]$QueueId ) try { diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheOAuth2PermissionGrants.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheOAuth2PermissionGrants.ps1 index 78ea6366ae5d..6fe33e93d378 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheOAuth2PermissionGrants.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheOAuth2PermissionGrants.ps1 @@ -5,11 +5,15 @@ function Set-CIPPDBCacheOAuth2PermissionGrants { .PARAMETER TenantFilter The tenant to cache OAuth2 permission grants for + + .PARAMETER QueueId + The queue ID to update with total tasks (optional) #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] - [string]$TenantFilter + [string]$TenantFilter, + [string]$QueueId ) try { diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheOneDriveUsage.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheOneDriveUsage.ps1 index 4df2d347f02e..60753ef0aedc 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheOneDriveUsage.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheOneDriveUsage.ps1 @@ -5,11 +5,15 @@ function Set-CIPPDBCacheOneDriveUsage { .PARAMETER TenantFilter The tenant to cache OneDrive usage for + + .PARAMETER QueueId + The queue ID to update with total tasks (optional) #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] - [string]$TenantFilter + [string]$TenantFilter, + [string]$QueueId ) try { diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheOrganization.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheOrganization.ps1 index 4710caf76427..3860aad8fe1e 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheOrganization.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheOrganization.ps1 @@ -5,11 +5,15 @@ function Set-CIPPDBCacheOrganization { .PARAMETER TenantFilter The tenant to cache organization data for + + .PARAMETER QueueId + The queue ID to update with total tasks (optional) #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] - [string]$TenantFilter + [string]$TenantFilter, + [string]$QueueId ) try { diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCachePIMSettings.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCachePIMSettings.ps1 index 224d357389d6..768525f69e94 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCachePIMSettings.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCachePIMSettings.ps1 @@ -5,11 +5,15 @@ function Set-CIPPDBCachePIMSettings { .PARAMETER TenantFilter The tenant to cache PIM settings for + + .PARAMETER QueueId + The queue ID to update with total tasks (optional) #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] - [string]$TenantFilter + [string]$TenantFilter, + [string]$QueueId ) try { diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheRiskDetections.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheRiskDetections.ps1 index 2acc5fa2f099..f7878e2f9edb 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheRiskDetections.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheRiskDetections.ps1 @@ -5,11 +5,15 @@ function Set-CIPPDBCacheRiskDetections { .PARAMETER TenantFilter The tenant to cache risk detections for + + .PARAMETER QueueId + The queue ID to update with total tasks (optional) #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] - [string]$TenantFilter + [string]$TenantFilter, + [string]$QueueId ) try { diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheRiskyServicePrincipals.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheRiskyServicePrincipals.ps1 index 09092dd18716..4efcfce693a1 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheRiskyServicePrincipals.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheRiskyServicePrincipals.ps1 @@ -5,11 +5,15 @@ function Set-CIPPDBCacheRiskyServicePrincipals { .PARAMETER TenantFilter The tenant to cache risky service principals for + + .PARAMETER QueueId + The queue ID to update with total tasks (optional) #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] - [string]$TenantFilter + [string]$TenantFilter, + [string]$QueueId ) try { diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheRiskyUsers.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheRiskyUsers.ps1 index 813dff9da5ac..6e29032f0b03 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheRiskyUsers.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheRiskyUsers.ps1 @@ -5,11 +5,15 @@ function Set-CIPPDBCacheRiskyUsers { .PARAMETER TenantFilter The tenant to cache risky users for + + .PARAMETER QueueId + The queue ID to update with total tasks (optional) #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] - [string]$TenantFilter + [string]$TenantFilter, + [string]$QueueId ) try { diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheRoleAssignmentScheduleInstances.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheRoleAssignmentScheduleInstances.ps1 index 6d269f2cf17c..5900d0620871 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheRoleAssignmentScheduleInstances.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheRoleAssignmentScheduleInstances.ps1 @@ -2,7 +2,8 @@ function Set-CIPPDBCacheRoleAssignmentScheduleInstances { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] - [string]$TenantFilter + [string]$TenantFilter, + [string]$QueueId ) try { diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheRoleEligibilitySchedules.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheRoleEligibilitySchedules.ps1 index a4f559b5bd6b..f13c8def21ea 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheRoleEligibilitySchedules.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheRoleEligibilitySchedules.ps1 @@ -5,11 +5,15 @@ function Set-CIPPDBCacheRoleEligibilitySchedules { .PARAMETER TenantFilter The tenant to cache role eligibility schedules for + + .PARAMETER QueueId + The queue ID to update with total tasks (optional) #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] - [string]$TenantFilter + [string]$TenantFilter, + [string]$QueueId ) try { diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheRoleManagementPolicies.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheRoleManagementPolicies.ps1 index a2c3f97b99d5..e162b3659f7c 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheRoleManagementPolicies.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheRoleManagementPolicies.ps1 @@ -5,11 +5,15 @@ function Set-CIPPDBCacheRoleManagementPolicies { .PARAMETER TenantFilter The tenant to cache role management policies for + + .PARAMETER QueueId + The queue ID to update with total tasks (optional) #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] - [string]$TenantFilter + [string]$TenantFilter, + [string]$QueueId ) try { diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheRoles.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheRoles.ps1 index f9f6bd66c77d..bcc10c993b20 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheRoles.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheRoles.ps1 @@ -5,11 +5,15 @@ function Set-CIPPDBCacheRoles { .PARAMETER TenantFilter The tenant to cache role data for + + .PARAMETER QueueId + The queue ID to update with total tasks (optional) #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] - [string]$TenantFilter + [string]$TenantFilter, + [string]$QueueId ) try { diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheSecureScore.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheSecureScore.ps1 index de3eab54f0ed..03bb7565d471 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheSecureScore.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheSecureScore.ps1 @@ -5,11 +5,15 @@ function Set-CIPPDBCacheSecureScore { .PARAMETER TenantFilter The tenant to cache secure score for + + .PARAMETER QueueId + The queue ID to update with total tasks (optional) #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] - [string]$TenantFilter + [string]$TenantFilter, + [string]$QueueId ) try { diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheServicePrincipalRiskDetections.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheServicePrincipalRiskDetections.ps1 index a437723e0abd..bcd4cfba838e 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheServicePrincipalRiskDetections.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheServicePrincipalRiskDetections.ps1 @@ -5,11 +5,15 @@ function Set-CIPPDBCacheServicePrincipalRiskDetections { .PARAMETER TenantFilter The tenant to cache service principal risk detections for + + .PARAMETER QueueId + The queue ID to update with total tasks (optional) #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] - [string]$TenantFilter + [string]$TenantFilter, + [string]$QueueId ) try { diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheServicePrincipals.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheServicePrincipals.ps1 index b91941940e66..af887bf31342 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheServicePrincipals.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheServicePrincipals.ps1 @@ -5,11 +5,15 @@ function Set-CIPPDBCacheServicePrincipals { .PARAMETER TenantFilter The tenant to cache service principals for + + .PARAMETER QueueId + The queue ID to update with total tasks (optional) #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] - [string]$TenantFilter + [string]$TenantFilter, + [string]$QueueId ) try { diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheSettings.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheSettings.ps1 index 76c682ba3adb..5bb7cec33ca5 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheSettings.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheSettings.ps1 @@ -5,11 +5,15 @@ function Set-CIPPDBCacheSettings { .PARAMETER TenantFilter The tenant to cache settings for + + .PARAMETER QueueId + The queue ID to update with total tasks (optional) #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] - [string]$TenantFilter + [string]$TenantFilter, + [string]$QueueId ) try { diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheUserRegistrationDetails.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheUserRegistrationDetails.ps1 index 818a441dc0f3..64e596f669c9 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheUserRegistrationDetails.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheUserRegistrationDetails.ps1 @@ -5,11 +5,15 @@ function Set-CIPPDBCacheUserRegistrationDetails { .PARAMETER TenantFilter The tenant to cache user registration details for + + .PARAMETER QueueId + The queue ID to update with total tasks (optional) #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] - [string]$TenantFilter + [string]$TenantFilter, + [string]$QueueId ) try { diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheUsers.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheUsers.ps1 index 72f498142664..8462dbd75e7f 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheUsers.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheUsers.ps1 @@ -5,11 +5,15 @@ function Set-CIPPDBCacheUsers { .PARAMETER TenantFilter The tenant to cache users for + + .PARAMETER QueueId + The queue ID to update with total tasks (optional) #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] - [string]$TenantFilter + [string]$TenantFilter, + [string]$QueueId ) try { diff --git a/Modules/CIPPCore/Public/Set-CIPPDbCacheTestData.ps1 b/Modules/CIPPCore/Public/Set-CIPPDbCacheTestData.ps1 index 5a5f6b2094f1..10302dd69d17 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDbCacheTestData.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDbCacheTestData.ps1 @@ -9,6 +9,9 @@ function Set-CIPPDbCacheTestData { .PARAMETER TenantFilter The tenant to use for test data + .PARAMETER QueueId + The queue ID to update with total tasks (optional) + .PARAMETER Count Number of test objects to generate (default: 50000) #> @@ -16,6 +19,7 @@ function Set-CIPPDbCacheTestData { param( [Parameter(Mandatory = $true)] [string]$TenantFilter, + [string]$QueueId, [Parameter(Mandatory = $false)] [int]$Count = 50000 From ec8f481eb09d260e402d4e7136d6c5dab4d2b204 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Tue, 10 Feb 2026 15:05:13 -0500 Subject: [PATCH 374/503] Cache mailbox rules batching and DB update ops Introduce batched mailbox-rules caching and add DB item update/remove helpers. Adds Push-GetMailboxRulesBatch and Push-StoreMailboxRules activity functions to fetch and aggregate mailbox rules in batches, and new Update-CIPPDbItem / Remove-CIPPDbItem cmdlets to support partial updates and removals in the CippReportingDB. Enhance Set-CIPPDBCacheMailboxes to accept a Types parameter (Permissions, CalendarPermissions, Rules) and spawn separate orchestrators for permission and rules batches; remove the old Set-CIPPDBCacheMailboxRules implementation. Propagate Types through Invoke-ExecCIPPDBCache and Push-ExecCIPPDBCache, improve logging, and update Set-CIPPMailboxRule / Remove-CIPPMailboxRule to keep the cache in sync when rules are changed or deleted. --- .../CIPPDBCache/Push-ExecCIPPDBCache.ps1 | 8 + .../Push-CIPPDBCacheData.ps1 | 1 - .../Push-GetMailboxRulesBatch.ps1 | 55 ++++++ .../Push-StoreMailboxRules.ps1 | 68 +++++++ .../CIPP/Core/Invoke-ExecCIPPDBCache.ps1 | 39 ++-- Modules/CIPPCore/Public/Remove-CIPPDbItem.ps1 | 76 ++++++++ .../Public/Remove-CIPPMailboxRule.ps1 | 10 +- .../Public/Set-CIPPDBCacheMailboxRules.ps1 | 60 ------ .../Public/Set-CIPPDBCacheMailboxes.ps1 | 183 ++++++++++++------ .../CIPPCore/Public/Set-CIPPMailboxRule.ps1 | 11 ++ Modules/CIPPCore/Public/Update-CIPPDbItem.ps1 | 97 ++++++++++ 11 files changed, 467 insertions(+), 141 deletions(-) create mode 100644 Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-GetMailboxRulesBatch.ps1 create mode 100644 Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-StoreMailboxRules.ps1 create mode 100644 Modules/CIPPCore/Public/Remove-CIPPDbItem.ps1 delete mode 100644 Modules/CIPPCore/Public/Set-CIPPDBCacheMailboxRules.ps1 create mode 100644 Modules/CIPPCore/Public/Update-CIPPDbItem.ps1 diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/CIPPDBCache/Push-ExecCIPPDBCache.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/CIPPDBCache/Push-ExecCIPPDBCache.ps1 index 7c4f04c772e0..ac069351da91 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/CIPPDBCache/Push-ExecCIPPDBCache.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/CIPPDBCache/Push-ExecCIPPDBCache.ps1 @@ -15,6 +15,7 @@ function Push-ExecCIPPDBCache { $Name = $Item.Name $TenantFilter = $Item.TenantFilter $QueueId = $Item.QueueId + $Types = $Item.Types try { Write-Information "Collecting $Name for tenant $TenantFilter" @@ -38,6 +39,13 @@ function Push-ExecCIPPDBCache { $CacheFunctionParams.QueueId = $QueueId } + # Add Types if provided (for Mailboxes function) + if ($Types) { + $CacheFunctionParams.Types = $Types + } + + Write-Information "Executing $FullFunctionName with parameters: $(($CacheFunctionParams.GetEnumerator() | ForEach-Object { "$($_.Key)=$($_.Value)" }) -join ', '))" + # Execute the cache function & $FullFunctionName @CacheFunctionParams diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 index 2f18e8e92b2d..6bdf8f889ddf 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 @@ -99,7 +99,6 @@ function Push-CIPPDBCacheData { 'ExoPresetSecurityPolicy' 'ExoTenantAllowBlockList' 'Mailboxes' - 'MailboxRules' 'CASMailboxes' 'MailboxUsage' 'OneDriveUsage' diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-GetMailboxRulesBatch.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-GetMailboxRulesBatch.ps1 new file mode 100644 index 000000000000..8ba597b4e1db --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-GetMailboxRulesBatch.ps1 @@ -0,0 +1,55 @@ +function Push-GetMailboxRulesBatch { + <# + .SYNOPSIS + Caches mailbox rules for a batch of mailboxes + + .PARAMETER InputObject + The batch object containing TenantFilter and Mailboxes array + #> + [CmdletBinding()] + param($Item) + + $TenantFilter = $Item.TenantFilter + $Mailboxes = $Item.Mailboxes + $BatchNumber = $Item.BatchNumber + $TotalBatches = $Item.TotalBatches + $QueueId = $Item.QueueId + + try { + Write-Information "Processing mailbox rules batch $BatchNumber/$TotalBatches for tenant $TenantFilter with $($Mailboxes.Count) mailboxes" + + # Build bulk request for mailbox rules + $Request = $Mailboxes | ForEach-Object { + @{ + OperationGuid = $_ + CmdletInput = @{ + CmdletName = 'Get-InboxRule' + Parameters = @{ + Mailbox = $_ + } + } + } + } + + $Rules = New-ExoBulkRequest -tenantid $TenantFilter -cmdletArray @($Request) | Where-Object { $_.Identity } + + Write-Information "Retrieved $($Rules.Count) rules from batch $BatchNumber/$TotalBatches" + + # Add metadata and return for aggregation + if (($Rules | Measure-Object).Count -gt 0) { + $RulesWithMetadata = foreach ($Rule in $Rules) { + $Rule | Add-Member -NotePropertyName 'Tenant' -NotePropertyValue $TenantFilter -Force + $Rule | Add-Member -NotePropertyName 'UserPrincipalName' -NotePropertyValue $Rule.OperationGuid -Force + $Rule + } + return , $RulesWithMetadata + } else { + Write-Information "No rules found in batch $BatchNumber/$TotalBatches" + return , @() + } + + } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to process mailbox rules batch $BatchNumber/$TotalBatches : $($_.Exception.Message)" -sev Error -LogData (Get-CippException -Exception $_) + throw + } +} diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-StoreMailboxRules.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-StoreMailboxRules.ps1 new file mode 100644 index 000000000000..c6938db264cd --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-StoreMailboxRules.ps1 @@ -0,0 +1,68 @@ +function Push-StoreMailboxRules { + <# + .SYNOPSIS + Post-execution function to aggregate and store all mailbox rules + + .DESCRIPTION + Collects results from all batches and stores them in the reporting database + + .FUNCTIONALITY + Entrypoint + #> + [CmdletBinding()] + param($Item) + + $TenantFilter = $Item.Parameters.TenantFilter + $Results = $Item.Results + + try { + Write-Information "Storing mailbox rules for tenant $TenantFilter" + Write-Information "Received $($Results.Count) batch results" + + # Aggregate all rules from batches + $AllRules = [System.Collections.Generic.List[object]]::new() + + foreach ($BatchResult in $Results) { + # Activity functions may return an array + $ActualResult = $BatchResult + if ($BatchResult -is [array] -and $BatchResult.Count -gt 0) { + Write-Information "Result is array with $($BatchResult.Count) elements" + # If first element is array of rules, use it + if ($BatchResult[0] -is [array]) { + $ActualResult = $BatchResult[0] + } else { + $ActualResult = $BatchResult + } + } + + if ($ActualResult) { + if ($ActualResult -is [array]) { + Write-Information "Adding $($ActualResult.Count) rules from batch" + $AllRules.AddRange($ActualResult) + } else { + Write-Information 'Adding 1 rule from batch' + $AllRules.Add($ActualResult) + } + } + } + + Write-Information "Aggregated $($AllRules.Count) total mailbox rules" + + # Store all rules + if ($AllRules.Count -gt 0) { + $AllRules | Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'MailboxRules' -AddCount + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($AllRules.Count) mailbox rules" -sev Info + } else { + # Store empty result to indicate successful check with no rules + @() | Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'MailboxRules' -AddCount + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'No mailbox rules found to cache' -sev Info + } + + return + + } catch { + $ErrorMsg = "Failed to store mailbox rules for tenant $TenantFilter : $($_.Exception.Message)" + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message $ErrorMsg -sev Error + throw $ErrorMsg + } +} diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ExecCIPPDBCache.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ExecCIPPDBCache.ps1 index dd526d2a574b..34ef1b3e1029 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ExecCIPPDBCache.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ExecCIPPDBCache.ps1 @@ -11,8 +11,9 @@ function Invoke-ExecCIPPDBCache { $APIName = $Request.Params.CIPPEndpoint $TenantFilter = $Request.Query.TenantFilter $Name = $Request.Query.Name + $Types = $Request.Query.Types - Write-Information "ExecCIPPDBCache called with Name: '$Name', TenantFilter: '$TenantFilter'" + Write-Information "ExecCIPPDBCache called with Name: '$Name', TenantFilter: '$TenantFilter', Types: '$Types'" try { if ([string]::IsNullOrEmpty($Name)) { @@ -30,8 +31,6 @@ function Invoke-ExecCIPPDBCache { throw "Cache function '$FunctionName' not found" } - Write-LogMessage -API $APIName -tenant $TenantFilter -message "Starting CIPP DB cache for $Name" -sev Info - # Create queue entry for tracking $QueueName = if ($TenantFilter -eq 'AllTenants') { "$Name Cache Sync (All Tenants)" @@ -45,13 +44,18 @@ function Invoke-ExecCIPPDBCache { $Queue = New-CippQueueEntry -Name $QueueName -TotalTasks ($TenantList | Measure-Object).Count $Batch = $TenantList | ForEach-Object { - [PSCustomObject]@{ + $BatchItem = [PSCustomObject]@{ FunctionName = 'ExecCIPPDBCache' - QueueName = "$Name Cache - $($_.defaultDomainName)" Name = $Name + QueueName = "$Name Cache - $($_.defaultDomainName)" TenantFilter = $_.defaultDomainName QueueId = $Queue.RowKey } + # Add Types parameter if provided + if ($Types) { + $BatchItem | Add-Member -NotePropertyName 'Types' -NotePropertyValue @($Types -split ',') -Force + } + $BatchItem } $InputObject = [PSCustomObject]@{ @@ -60,28 +64,33 @@ function Invoke-ExecCIPPDBCache { SkipLog = $false } - Write-LogMessage -API $APIName -tenant $TenantFilter -message "Starting CIPP DB cache for $Name across $($TenantList.Count) tenants" -sev Info + Write-LogMessage -Headers $Request.Headers -API $APIName -tenant $TenantFilter -message "Starting CIPP DB cache for $Name across $($TenantList.Count) tenants" -sev Info } else { # Single tenant $Queue = New-CippQueueEntry -Name $QueueName -TotalTasks 1 + $BatchItem = [PSCustomObject]@{ + FunctionName = 'ExecCIPPDBCache' + Name = $Name + QueueName = "$Name Cache - $TenantFilter" + TenantFilter = $TenantFilter + QueueId = $Queue.RowKey + } + # Add Types parameter if provided + if ($Types) { + $BatchItem | Add-Member -NotePropertyName 'Types' -NotePropertyValue @($Types -split ',') -Force + } + $InputObject = [PSCustomObject]@{ - Batch = @([PSCustomObject]@{ - QueueName = "$Name Cache - $TenantFilter" - FunctionName = 'ExecCIPPDBCache' - Name = $Name - TenantFilter = $TenantFilter - QueueId = $Queue.RowKey - }) + Batch = @($BatchItem) OrchestratorName = "CIPPDBCache_${Name}_$TenantFilter" SkipLog = $false } + Write-LogMessage -Headers $Request.Headers -API $APIName -tenant $TenantFilter -message "Starting CIPP DB cache for $Name on tenant $TenantFilter" -sev Info } $InstanceId = Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Compress -Depth 5) - Write-LogMessage -API $APIName -tenant $TenantFilter -message "Started CIPP DB cache orchestrator for $Name with instance ID: $InstanceId" -sev Info - $ResultsMessage = if ($TenantFilter -eq 'AllTenants') { "Successfully started cache operation for $Name for all tenants" } else { diff --git a/Modules/CIPPCore/Public/Remove-CIPPDbItem.ps1 b/Modules/CIPPCore/Public/Remove-CIPPDbItem.ps1 new file mode 100644 index 000000000000..fcb2196e4a23 --- /dev/null +++ b/Modules/CIPPCore/Public/Remove-CIPPDbItem.ps1 @@ -0,0 +1,76 @@ +function Remove-CIPPDbItem { + <# + .SYNOPSIS + Remove an item from the CIPP Reporting database + + .DESCRIPTION + Removes a specific item from the CippReportingDB table using partition key (tenant) and row key (item ID) + + .PARAMETER TenantFilter + The tenant domain or GUID (partition key) + + .PARAMETER Type + The type of data being removed (used to find and update count) + + .PARAMETER ItemId + The item ID or identifier to remove (used in row key) + + .EXAMPLE + Remove-CIPPDbItem -TenantFilter 'contoso.onmicrosoft.com' -Type 'MailboxRules' -ItemId 'rule-id-123' + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter, + + [Parameter(Mandatory = $true)] + [string]$Type, + + [Parameter(Mandatory = $true)] + [string]$ItemId + ) + + try { + $Table = Get-CippTable -tablename 'CippReportingDB' + + # Sanitize the ItemId for RowKey (same as in Add-CIPPDbItem) + $SanitizedId = $ItemId -replace '[/\\#?]', '_' -replace '[\u0000-\u001F\u007F-\u009F]', '' + $RowKey = "$Type-$SanitizedId" + + # Try to get the entity + $Filter = "PartitionKey eq '$TenantFilter' and RowKey eq '$RowKey'" + $Entity = Get-CIPPAzDataTableEntity @Table -Filter $Filter + + if ($Entity) { + # Remove the entity + Remove-AzDataTableEntity @Table -Entity $Entity -Force + Write-LogMessage -API 'CIPPDbItem' -tenant $TenantFilter -message "Removed $Type item with ID: $ItemId" -sev Debug + + # Always decrement count + try { + $CountRowKey = "$Type-Count" + $CountFilter = "PartitionKey eq '$TenantFilter' and RowKey eq '$CountRowKey'" + $CountEntity = Get-CIPPAzDataTableEntity @Table -Filter $CountFilter + + if ($CountEntity -and $CountEntity.DataCount -gt 0) { + $CountEntity.DataCount = [int]$CountEntity.DataCount - 1 + Add-CIPPAzDataTableEntity @Table -Entity @{ + PartitionKey = $CountEntity.PartitionKey + RowKey = $CountEntity.RowKey + DataCount = $CountEntity.DataCount + ETag = $CountEntity.ETag + } -Force | Out-Null + Write-LogMessage -API 'CIPPDbItem' -tenant $TenantFilter -message "Decremented $Type count to $($CountEntity.DataCount)" -sev Debug + } + } catch { + Write-LogMessage -API 'CIPPDbItem' -tenant $TenantFilter -message "Failed to decrement count for $Type : $($_.Exception.Message)" -sev Warning + } + } else { + Write-LogMessage -API 'CIPPDbItem' -tenant $TenantFilter -message "Item not found for removal: $Type with ID $ItemId" -sev Debug + } + + } catch { + Write-LogMessage -API 'CIPPDbItem' -tenant $TenantFilter -message "Failed to remove $Type item: $($_.Exception.Message)" -sev Error -LogData (Get-CippException -Exception $_) + throw + } +} diff --git a/Modules/CIPPCore/Public/Remove-CIPPMailboxRule.ps1 b/Modules/CIPPCore/Public/Remove-CIPPMailboxRule.ps1 index 579c7f2d4801..1f470021a92b 100644 --- a/Modules/CIPPCore/Public/Remove-CIPPMailboxRule.ps1 +++ b/Modules/CIPPCore/Public/Remove-CIPPMailboxRule.ps1 @@ -22,7 +22,7 @@ function Remove-CIPPMailboxRule { Write-LogMessage -headers $Headers -API $APIName -message $Message -Sev 'Info' -tenant $TenantFilter return $Message } else { - ForEach ($rule in $Rules) { + foreach ($rule in $Rules) { $null = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Remove-InboxRule' -Anchor $Username -cmdParams @{Identity = $rule.Identity } } $Message = "Successfully deleted all rules for $($Username)" @@ -41,6 +41,14 @@ function Remove-CIPPMailboxRule { $null = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Remove-InboxRule' -Anchor $Username -cmdParams @{Identity = $RuleId } $Message = "Successfully deleted mailbox rule $($RuleName) for $($Username)" Write-LogMessage -headers $Headers -API $APIName -message "Deleted mailbox rule $($RuleName) for $($Username)" -Sev 'Info' -tenant $TenantFilter + + # Remove from cache if it exists + try { + Remove-CIPPDbItem -TenantFilter $TenantFilter -Type 'MailboxRules' -ItemId $RuleId + } catch { + Write-LogMessage -headers $Headers -API $APIName -message "Rule deleted but failed to remove from cache: $($_.Exception.Message)" -Sev 'Warning' -tenant $TenantFilter + } + return $Message } catch { $ErrorMessage = Get-CippException -Exception $_ diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheMailboxRules.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheMailboxRules.ps1 deleted file mode 100644 index e99c47bdf7a4..000000000000 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheMailboxRules.ps1 +++ /dev/null @@ -1,60 +0,0 @@ -function Set-CIPPDBCacheMailboxRules { - <# - .SYNOPSIS - Caches mailbox rules for a tenant - - .PARAMETER TenantFilter - The tenant to cache mailbox rules for - - .PARAMETER QueueId - The queue ID to update with total tasks (optional) - #> - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [string]$TenantFilter, - [string]$QueueId - ) - - try { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching mailbox rules' -sev Debug - - # Get mailboxes - $Mailboxes = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-Mailbox' -Select 'userPrincipalName,GUID' - $Request = $Mailboxes | ForEach-Object { - @{ - OperationGuid = $_.UserPrincipalName - CmdletInput = @{ - CmdletName = 'Get-InboxRule' - Parameters = @{ - Mailbox = $_.UserPrincipalName - } - } - } - } - - $Rules = New-ExoBulkRequest -tenantid $TenantFilter -cmdletArray @($Request) | Where-Object { $_.Identity } - - if (($Rules | Measure-Object).Count -gt 0) { - $MailboxRules = foreach ($Rule in $Rules) { - $Rule | Add-Member -NotePropertyName 'Tenant' -NotePropertyValue $TenantFilter -Force - $Rule | Add-Member -NotePropertyName 'UserPrincipalName' -NotePropertyValue $Rule.OperationGuid -Force - $Rule - } - - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'MailboxRules' -Data @($MailboxRules) - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'MailboxRules' -Data @($MailboxRules) -Count - - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($MailboxRules.Count) mailbox rules successfully" -sev Debug - } else { - # Cache empty result to indicate successful check with no rules - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'MailboxRules' -Data @() - Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'MailboxRules' -Data @() -Count - - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'No mailbox rules found' -sev Debug - } - - } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache mailbox rules: $($_.Exception.Message)" -sev Error -LogData (Get-CippException -Exception $_) - } -} diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheMailboxes.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheMailboxes.ps1 index e965fa7e1e7b..d42279de2ec3 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheMailboxes.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheMailboxes.ps1 @@ -1,19 +1,25 @@ function Set-CIPPDBCacheMailboxes { <# .SYNOPSIS - Caches all mailboxes, CAS mailboxes, and mailbox permissions for a tenant + Caches all mailboxes and optionally related data (permissions, rules) for a tenant .PARAMETER TenantFilter The tenant to cache mailboxes for .PARAMETER QueueId The queue ID to update with total tasks + + .PARAMETER Types + Optional array of types to cache. Valid values: 'All', 'Permissions', 'CalendarPermissions', 'Rules' + If not specified, defaults to 'All' which caches all types. #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$TenantFilter, - [string]$QueueId + [string]$QueueId, + [ValidateSet('All', 'Permissions', 'CalendarPermissions', 'Rules')] + [string[]]$Types = @('All') ) try { @@ -52,75 +58,123 @@ function Set-CIPPDBCacheMailboxes { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Cached $($Mailboxes.Count) mailboxes successfully" -sev Debug - # Start orchestrator to cache mailbox permissions in batches - $MailboxCount = ($Mailboxes | Measure-Object).Count - if ($MailboxCount -gt 0) { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Starting mailbox permission caching for $MailboxCount mailboxes" -sev Debug - - # Create batches of 10 mailboxes each for both mailbox and calendar permissions - $BatchSize = 10 - $Batches = [System.Collections.Generic.List[object]]::new() - $TotalBatches = [Math]::Ceiling($Mailboxes.Count / $BatchSize) - - for ($i = 0; $i -lt $Mailboxes.Count; $i += $BatchSize) { - $BatchMailboxes = $Mailboxes[$i..[Math]::Min($i + $BatchSize - 1, $Mailboxes.Count - 1)] - - # Only send UPN to batch function to reduce payload size - $BatchMailboxUPNs = $BatchMailboxes | Select-Object -ExpandProperty UPN - $BatchNumber = [Math]::Floor($i / $BatchSize) + 1 - - # Add mailbox permissions batch - $Batches.Add([PSCustomObject]@{ - FunctionName = 'GetMailboxPermissionsBatch' - QueueName = "Mailbox Permissions Batch $BatchNumber/$TotalBatches - $TenantFilter" - TenantFilter = $TenantFilter - Mailboxes = $BatchMailboxUPNs - BatchNumber = $BatchNumber - TotalBatches = $TotalBatches - }) - - # Add calendar permissions batch for the same mailboxes - $Batches.Add([PSCustomObject]@{ - FunctionName = 'GetCalendarPermissionsBatch' - QueueName = "Calendar Permissions Batch $BatchNumber/$TotalBatches - $TenantFilter" - TenantFilter = $TenantFilter - Mailboxes = $BatchMailboxUPNs - BatchNumber = $BatchNumber - TotalBatches = $TotalBatches - }) - } + # Expand 'All' to all available types + if ($Types -contains 'All') { + $Types = @('Permissions', 'CalendarPermissions', 'Rules') + } - # Split batches into mailbox and calendar permissions for separate post-execution - $MailboxPermBatches = $Batches | Where-Object { $_.FunctionName -eq 'GetMailboxPermissionsBatch' } - $CalendarPermBatches = $Batches | Where-Object { $_.FunctionName -eq 'GetCalendarPermissionsBatch' } + # Process additional types if specified + if ($Types -and $Types.Count -gt 0) { + $MailboxCount = ($Mailboxes | Measure-Object).Count + if ($MailboxCount -gt 0) { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Starting batch caching for types: $($Types -join ', ')" -sev Debug + Write-Information "Starting batch caching for types: $($Types -join ', ')" + + # Create batches based on selected types + $BatchSize = 10 + $TotalBatches = [Math]::Ceiling($Mailboxes.Count / $BatchSize) + + # Separate batches for permissions and rules + $PermissionBatches = [System.Collections.Generic.List[object]]::new() + $RuleBatches = [System.Collections.Generic.List[object]]::new() + + for ($i = 0; $i -lt $Mailboxes.Count; $i += $BatchSize) { + $BatchMailboxes = $Mailboxes[$i..[Math]::Min($i + $BatchSize - 1, $Mailboxes.Count - 1)] + $BatchMailboxUPNs = $BatchMailboxes | Select-Object -ExpandProperty UPN + $BatchNumber = [Math]::Floor($i / $BatchSize) + 1 + + # Add mailbox permissions batch if requested + if ($Types -contains 'Permissions') { + $PermissionBatches.Add([PSCustomObject]@{ + FunctionName = 'GetMailboxPermissionsBatch' + QueueName = "Mailbox Permissions Batch $BatchNumber/$TotalBatches - $TenantFilter" + TenantFilter = $TenantFilter + Mailboxes = $BatchMailboxUPNs + BatchNumber = $BatchNumber + TotalBatches = $TotalBatches + }) + } - # Update queue with additional tasks if QueueId is provided - if ($QueueId) { - Update-CippQueueEntry -RowKey $QueueId -TotalTasks $Batches.Count -IncrementTotalTasks - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Updated queue $QueueId with $($Batches.Count) additional tasks" -sev Debug - } + # Add calendar permissions batch if requested + if ($Types -contains 'CalendarPermissions') { + $PermissionBatches.Add([PSCustomObject]@{ + FunctionName = 'GetCalendarPermissionsBatch' + QueueName = "Calendar Permissions Batch $BatchNumber/$TotalBatches - $TenantFilter" + TenantFilter = $TenantFilter + Mailboxes = $BatchMailboxUPNs + BatchNumber = $BatchNumber + TotalBatches = $TotalBatches + }) + } - # Start single orchestrator for both mailbox and calendar permissions - $InputObject = [PSCustomObject]@{ - Batch = @($Batches) - OrchestratorName = "MailboxPermissions_$TenantFilter" - PostExecution = @{ - FunctionName = 'StoreMailboxPermissions' - Parameters = @{ - TenantFilter = $TenantFilter + # Add mailbox rules batch if requested + if ($Types -contains 'Rules') { + $RuleBatches.Add([PSCustomObject]@{ + FunctionName = 'GetMailboxRulesBatch' + QueueName = "Mailbox Rules Batch $BatchNumber/$TotalBatches - $TenantFilter" + TenantFilter = $TenantFilter + Mailboxes = $BatchMailboxUPNs + BatchNumber = $BatchNumber + TotalBatches = $TotalBatches + }) } } - } - if ($QueueId) { - # Add QueueId to each batch item - foreach ($Batch in $Batches) { - $Batch | Add-Member -NotePropertyName 'QueueId' -NotePropertyValue $QueueId -Force + + # Add QueueId to batch items if provided + if ($QueueId) { + foreach ($Batch in $PermissionBatches) { + $Batch | Add-Member -NotePropertyName 'QueueId' -NotePropertyValue $QueueId -Force + } + foreach ($Batch in $RuleBatches) { + $Batch | Add-Member -NotePropertyName 'QueueId' -NotePropertyValue $QueueId -Force + } } + + # Update queue with total additional tasks if QueueId is provided + $TotalBatchCount = $PermissionBatches.Count + $RuleBatches.Count + if ($QueueId -and $TotalBatchCount -gt 0) { + Update-CippQueueEntry -RowKey $QueueId -TotalTasks $TotalBatchCount -IncrementTotalTasks + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Updated queue $QueueId with $TotalBatchCount additional tasks" -sev Debug + Write-Information "Updated queue $QueueId with $TotalBatchCount additional tasks" + } + + # Start separate orchestrator for permissions if we have permission batches + if ($PermissionBatches.Count -gt 0) { + $PermissionInputObject = [PSCustomObject]@{ + Batch = @($PermissionBatches) + OrchestratorName = "MailboxPermissions_$TenantFilter" + PostExecution = @{ + FunctionName = 'StoreMailboxPermissions' + Parameters = @{ + TenantFilter = $TenantFilter + } + } + } + Write-Information "Starting permissions caching orchestrator with $($PermissionBatches.Count) batches" + Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($PermissionInputObject | ConvertTo-Json -Compress -Depth 5) + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Started permission caching orchestrator with $($PermissionBatches.Count) batches" -sev Debug + } + + # Start separate orchestrator for rules if we have rule batches + if ($RuleBatches.Count -gt 0) { + $RuleInputObject = [PSCustomObject]@{ + Batch = @($RuleBatches) + OrchestratorName = "MailboxRules_$TenantFilter" + PostExecution = @{ + FunctionName = 'StoreMailboxRules' + Parameters = @{ + TenantFilter = $TenantFilter + } + } + } + Write-Information "Starting rules caching orchestrator with $($RuleBatches.Count) batches" + Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($RuleInputObject | ConvertTo-Json -Compress -Depth 5) + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Started rules caching orchestrator with $($RuleBatches.Count) batches" -sev Debug + } + + } else { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'No mailboxes found to cache additional data for' -sev Debug } - Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Compress -Depth 5) - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Started mailbox and calendar permission caching orchestrator with $($Batches.Count) batches" -sev Debug - } else { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'No mailboxes found to cache permissions for' -sev Debug } # Clear mailbox data to free memory @@ -129,5 +183,6 @@ function Set-CIPPDBCacheMailboxes { } catch { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache mailboxes: $($_.Exception.Message)" -sev Error + Write-Information "Failed to cache mailboxes: $($_.Exception.Message)" } } diff --git a/Modules/CIPPCore/Public/Set-CIPPMailboxRule.ps1 b/Modules/CIPPCore/Public/Set-CIPPMailboxRule.ps1 index 311a02400c6f..e99ead09a19d 100644 --- a/Modules/CIPPCore/Public/Set-CIPPMailboxRule.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPMailboxRule.ps1 @@ -24,6 +24,17 @@ try { $null = New-ExoRequest -tenantid $TenantFilter -cmdlet "$State-InboxRule" -Anchor $Username -cmdParams @{Identity = $RuleId; Mailbox = $UserId } Write-LogMessage -headers $Headers -API $APIName -message "Successfully set mailbox rule $($RuleName) for $($Username) to $($State)d" -Sev 'Info' -tenant $TenantFilter + + # Update the cached rule if it exists (without calling Exchange again) + try { + $EnabledValue = $State -eq 'Enable' + Update-CIPPDbItem -TenantFilter $TenantFilter -Type 'MailboxRules' -ItemId $RuleId -PropertyUpdates @{ + Enabled = $EnabledValue + } + } catch { + Write-LogMessage -headers $Headers -API $APIName -message "Rule updated but failed to update cache: $($_.Exception.Message)" -Sev 'Warning' -tenant $TenantFilter + } + return "Successfully set mailbox rule $($RuleName) for $($Username) to $($State)d" } catch { $ErrorMessage = Get-CippException -Exception $_ diff --git a/Modules/CIPPCore/Public/Update-CIPPDbItem.ps1 b/Modules/CIPPCore/Public/Update-CIPPDbItem.ps1 new file mode 100644 index 000000000000..59928b4dd79a --- /dev/null +++ b/Modules/CIPPCore/Public/Update-CIPPDbItem.ps1 @@ -0,0 +1,97 @@ +function Update-CIPPDbItem { + <# + .SYNOPSIS + Update a single item in the CIPP Reporting database + + .DESCRIPTION + Updates a single item in the CippReportingDB table by finding it by ItemId and updating its Data field. + Supports full object replacement or partial property updates. + + .PARAMETER TenantFilter + The tenant domain or GUID (used as partition key) + + .PARAMETER Type + The type of data being stored (used in row key) + + .PARAMETER ItemId + The unique identifier for the item to update + + .PARAMETER InputObject + The updated object to store (will be converted to JSON). Used for full replacement. + + .PARAMETER PropertyUpdates + Hashtable of property names and values to update in the existing cached object. More efficient than full replacement. + + .EXAMPLE + Update-CIPPDbItem -TenantFilter 'contoso.onmicrosoft.com' -Type 'MailboxRules' -ItemId 'rule-guid' -InputObject $UpdatedRule + + .EXAMPLE + Update-CIPPDbItem -TenantFilter 'contoso.onmicrosoft.com' -Type 'MailboxRules' -ItemId 'rule-guid' -PropertyUpdates @{Enabled = $true} + #> + [CmdletBinding(DefaultParameterSetName = 'FullObject')] + param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter, + + [Parameter(Mandatory = $true)] + [string]$Type, + + [Parameter(Mandatory = $true)] + [string]$ItemId, + + [Parameter(Mandatory = $true, ParameterSetName = 'FullObject')] + $InputObject, + + [Parameter(Mandatory = $true, ParameterSetName = 'PartialUpdate')] + [hashtable]$PropertyUpdates + ) + + try { + $Table = Get-CippTable -tablename 'CippReportingDB' + + # Format RowKey + $RowKey = "$Type-$ItemId" -replace '[/\\#?]', '_' -replace '[\u0000-\u001F\u007F-\u009F]', '' + + # Get existing entity + $Filter = "PartitionKey eq '$TenantFilter' and RowKey eq '$RowKey'" + $ExistingEntity = Get-CIPPAzDataTableEntity @Table -Filter $Filter + + if (-not $ExistingEntity) { + Write-Information "[CIPPDbItem] Item not found for update: Tenant=$TenantFilter, Type=$Type, ItemId=$ItemId." + return + } + + # Determine the data to store + if ($PSCmdlet.ParameterSetName -eq 'PartialUpdate') { + # Parse existing data and update specific properties + $ExistingData = $ExistingEntity.Data | ConvertFrom-Json + foreach ($key in $PropertyUpdates.GetEnumerator()) { + $ExistingData.($key.Name) = $key.Value + } + $DataToStore = $ExistingData + } else { + # Full object replacement + $DataToStore = $InputObject + } + + # Update entity + $Entity = @{ + PartitionKey = $TenantFilter + RowKey = $RowKey + Data = [string]($DataToStore | ConvertTo-Json -Depth 10 -Compress) + Type = $Type + ETag = $ExistingEntity.ETag + } + + Add-CIPPAzDataTableEntity @Table -Entity $Entity -Force | Out-Null + + Write-LogMessage -API 'CIPPDbItem' -tenant $TenantFilter ` + -message "Updated cached item: $Type - $ItemId" -sev Debug + + } catch { + Write-LogMessage -API 'CIPPDbItem' -tenant $TenantFilter ` + -message "Failed to update item $Type - $ItemId : $($_.Exception.Message)" -sev Error ` + -LogData (Get-CippException -Exception $_) + throw + } +} From 5fb46b7beb54c22609df6980f2372bd29f045cc4 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Tue, 10 Feb 2026 15:57:55 -0500 Subject: [PATCH 375/503] make dev environment more cross platform friendly --- Tools/Initialize-DevEnvironment.ps1 | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/Tools/Initialize-DevEnvironment.ps1 b/Tools/Initialize-DevEnvironment.ps1 index bc8fd6193f91..10421bb5a64b 100644 --- a/Tools/Initialize-DevEnvironment.ps1 +++ b/Tools/Initialize-DevEnvironment.ps1 @@ -1,18 +1,22 @@ +Write-Host 'Initializing development environment...' -ForegroundColor Green $CippRoot = (Get-Item $PSScriptRoot).Parent.FullName ### Read the local.settings.json file and convert to a PowerShell object. $CIPPSettings = Get-Content (Join-Path $CippRoot 'local.settings.json') | ConvertFrom-Json | Select-Object -ExpandProperty Values ### Loop through the settings and set environment variables for each. -$ValidKeys = @('TenantID', 'ApplicationID', 'ApplicationSecret', 'RefreshToken', 'AzureWebJobsStorage', 'PartnerTenantAvailable', 'SetFromProfile') +$ValidKeys = @('AzureWebJobsStorage', 'PartnerTenantAvailable', 'SetFromProfile') foreach ($Key in $CIPPSettings.PSObject.Properties.Name) { if ($ValidKeys -contains $Key) { [Environment]::SetEnvironmentVariable($Key, $CippSettings.$Key) } } -$PowerShellWorkerRoot = Join-Path $env:ProgramFiles 'Microsoft\Azure Functions Core Tools\workers\powershell\7.4\Microsoft.Azure.Functions.PowerShellWorker.dll' -if ((Test-Path $PowerShellWorkerRoot) -and !('Microsoft.Azure.Functions.PowerShellWorker' -as [type])) { - Write-Information "Loading PowerShell Worker from $PowerShellWorkerRoot" - Add-Type -Path $PowerShellWorkerRoot +# if windows +if ($IsWindows) { + $PowerShellWorkerRoot = Join-Path $env:ProgramFiles 'Microsoft\Azure Functions Core Tools\workers\powershell\7.4\Microsoft.Azure.Functions.PowerShellWorker.dll' + if ((Test-Path $PowerShellWorkerRoot) -and !('Microsoft.Azure.Functions.PowerShellWorker' -as [type])) { + Write-Information "Loading PowerShell Worker from $PowerShellWorkerRoot" + Add-Type -Path $PowerShellWorkerRoot + } } # Remove previously loaded modules to force reloading if new code changes were made @@ -28,4 +32,9 @@ Import-Module ( Join-Path $CippRoot 'Modules\DNSHealth' ) Import-Module ( Join-Path $CippRoot 'Modules\CIPPCore' ) Import-Module ( Join-Path $CippRoot 'Modules\CippExtensions' ) -Get-CIPPAuthentication +$Auth = Get-CIPPAuthentication +if ($Auth) { + Write-Host 'Development environment initialized successfully!' -ForegroundColor Green +} else { + Write-Host 'Failed to initialize development environment. Please check the error messages above.' -ForegroundColor Red +} From 5ab450f42bb3a4323c083ca233ed9a9f7f5146a9 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Tue, 10 Feb 2026 16:01:17 -0500 Subject: [PATCH 376/503] Initialize excluded licenses if table empty Add a guard that detects when the ExcludedLicenses table is empty and initializes it. When no excluded SKUs are found, the code logs an informational message, calls Initialize-CIPPExcludedLicenses, and reloads the excluded SKU list so downstream processing has the expected data. --- Modules/CIPPCore/Public/Get-CIPPLicenseOverview.ps1 | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Modules/CIPPCore/Public/Get-CIPPLicenseOverview.ps1 b/Modules/CIPPCore/Public/Get-CIPPLicenseOverview.ps1 index 2e838aa0a63a..de1835ea1496 100644 --- a/Modules/CIPPCore/Public/Get-CIPPLicenseOverview.ps1 +++ b/Modules/CIPPCore/Public/Get-CIPPLicenseOverview.ps1 @@ -56,6 +56,13 @@ function Get-CIPPLicenseOverview { $LicenseTable = Get-CIPPTable -TableName ExcludedLicenses $ExcludedSkuList = Get-CIPPAzDataTableEntity @LicenseTable + # If no excluded licenses exist, initialize them + if ($ExcludedSkuList.Count -lt 1) { + Write-Information 'Excluded licenses table is empty. Initializing from config file.' + $null = Initialize-CIPPExcludedLicenses + $ExcludedSkuList = Get-CIPPAzDataTableEntity @Table + } + $AllLicensedUsers = @(($Results | Where-Object { $_.id -eq 'licensedUsers' }).body.value) $UsersBySku = @{} foreach ($User in $AllLicensedUsers) { From 1309dd466bf2ccdcf3b21c6e3db37016cd566cf5 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Tue, 10 Feb 2026 16:01:47 -0500 Subject: [PATCH 377/503] Update Get-CIPPLicenseOverview.ps1 --- Modules/CIPPCore/Public/Get-CIPPLicenseOverview.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Get-CIPPLicenseOverview.ps1 b/Modules/CIPPCore/Public/Get-CIPPLicenseOverview.ps1 index de1835ea1496..bfa4b519de08 100644 --- a/Modules/CIPPCore/Public/Get-CIPPLicenseOverview.ps1 +++ b/Modules/CIPPCore/Public/Get-CIPPLicenseOverview.ps1 @@ -60,7 +60,7 @@ function Get-CIPPLicenseOverview { if ($ExcludedSkuList.Count -lt 1) { Write-Information 'Excluded licenses table is empty. Initializing from config file.' $null = Initialize-CIPPExcludedLicenses - $ExcludedSkuList = Get-CIPPAzDataTableEntity @Table + $ExcludedSkuList = Get-CIPPAzDataTableEntity @LicenseTable } $AllLicensedUsers = @(($Results | Where-Object { $_.id -eq 'licensedUsers' }).body.value) From 1de577f45033dc86aa82c670111991ee3faf6282 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Tue, 10 Feb 2026 18:08:37 -0500 Subject: [PATCH 378/503] Add cleanup rule for CippStandardsReports table Insert a new CleanupRule into Start-TableCleanup to run TableCleanupTask against the CippStandardsReports table. The rule deletes entities with Timestamp older than 7 days (UTC) in batches up to 10,000, returning PartitionKey, RowKey and ETag for each item. This ensures old standards report entries are pruned automatically. --- .../Entrypoints/Timer Functions/Start-TableCleanup.ps1 | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-TableCleanup.ps1 b/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-TableCleanup.ps1 index 35a101109294..74f620e133e1 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-TableCleanup.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-TableCleanup.ps1 @@ -65,6 +65,16 @@ function Start-TableCleanup { Property = @('PartitionKey', 'RowKey', 'ETag') } } + @{ + FunctionName = 'TableCleanupTask' + Type = 'CleanupRule' + TableName = 'CippStandardsReports' + DataTableProps = @{ + Filter = "Timestamp lt datetime'$((Get-Date).AddDays(-7).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ'))'" + First = 10000 + Property = @('PartitionKey', 'RowKey', 'ETag') + } + } @{ FunctionName = 'TableCleanupTask' Type = 'DeleteTable' From c1d27e1c73705b5e7d5c854a8060f55e1bb0e0c2 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Tue, 10 Feb 2026 18:08:46 -0500 Subject: [PATCH 379/503] cleanup logging --- .../Orchestrator Functions/Start-UserTasksOrchestrator.ps1 | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-UserTasksOrchestrator.ps1 b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-UserTasksOrchestrator.ps1 index b677afdd2071..a9a2f4ab3da5 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-UserTasksOrchestrator.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-UserTasksOrchestrator.ps1 @@ -151,11 +151,6 @@ function Start-UserTasksOrchestrator { # Process each tenant batch separately foreach ($ProcessedBatch in $ProcessedBatches) { $TenantName = $ProcessedBatch[0].Parameters.TenantFilter - Write-Information "Processing batch for tenant: $TenantName with $($ProcessedBatch.Count) tasks..." - Write-Information 'Tasks by command:' - $ProcessedBatch | Group-Object -Property Command | ForEach-Object { - Write-Information " - $($_.Name): $($_.Count)" - } # Create queue entry for each tenant batch $Queue = New-CippQueueEntry -Name "Scheduled Tasks - $TenantName" From 36443a228519cea2c21a2c74f83517634c4994e4 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Tue, 10 Feb 2026 21:06:03 -0500 Subject: [PATCH 380/503] Add feature flag support and endpoints Introduce feature flag infrastructure and HTTP entrypoints. Adds Get-CIPPFeatureFlag and Set-CIPPFeatureFlag to load defaults from lib/data/FeatureFlags.json, initialize missing table entries, and persist state to the FeatureFlags table (Set respects AllowUserToggle and records LastModified). Adds Invoke-ListFeatureFlags and Invoke-ExecFeatureFlag HTTP handlers to list and get/set flags. Integrates feature flag checks into New-CippCoreRequest (returns 503 for disabled endpoints), Start-BPAOrchestrator (no-op when BestPracticeAnalyser is disabled), and Get-CIPPTimerFunctions (filters timers tied to disabled flags). Includes basic logging and error handling for the new flows. --- .../CIPP/Core/Invoke-ExecFeatureFlag.ps1 | 70 +++++++++ .../CIPP/Core/Invoke-ListFeatureFlags.ps1 | 31 ++++ .../HTTP Functions/New-CippCoreRequest.ps1 | 15 ++ .../Start-BPAOrchestrator.ps1 | 6 + .../CIPPCore/Public/Get-CIPPFeatureFlag.ps1 | 137 ++++++++++++++++++ .../Public/Get-CIPPTimerFunctions.ps1 | 9 +- .../CIPPCore/Public/Set-CIPPFeatureFlag.ps1 | 68 +++++++++ Modules/CIPPCore/lib/data/FeatureFlags.json | 25 ++++ 8 files changed, 360 insertions(+), 1 deletion(-) create mode 100644 Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ExecFeatureFlag.ps1 create mode 100644 Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ListFeatureFlags.ps1 create mode 100644 Modules/CIPPCore/Public/Get-CIPPFeatureFlag.ps1 create mode 100644 Modules/CIPPCore/Public/Set-CIPPFeatureFlag.ps1 create mode 100644 Modules/CIPPCore/lib/data/FeatureFlags.json diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ExecFeatureFlag.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ExecFeatureFlag.ps1 new file mode 100644 index 000000000000..b5961417a54c --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ExecFeatureFlag.ps1 @@ -0,0 +1,70 @@ +function Invoke-ExecFeatureFlag { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + CIPP.AppSettings.ReadWrite + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + try { + $Action = $Request.Body.Action + $Id = $Request.Body.Id + $Enabled = $Request.Body.Enabled + + Write-LogMessage -API 'ExecFeatureFlag' -message "Processing feature flag action: $Action for $Id" -sev 'Info' + + switch ($Action) { + 'Set' { + if ([string]::IsNullOrEmpty($Id)) { + throw 'Feature flag Id is required' + } + + if ($null -eq $Enabled) { + throw 'Enabled state is required' + } + + # Use Set-CIPPFeatureFlag to update the flag + $Result = Set-CIPPFeatureFlag -Id $Id -Enabled ([bool]$Enabled) + + if ($Result) { + Write-LogMessage -API 'ExecFeatureFlag' -message "Successfully updated feature flag $Id to $Enabled" -sev 'Info' + $StatusCode = [HttpStatusCode]::OK + $Body = @{ + Results = "Successfully updated feature flag '$Id' to Enabled=$Enabled" + } + } else { + throw "Failed to update feature flag '$Id'" + } + } + 'Get' { + if ([string]::IsNullOrEmpty($Id)) { + # Get all flags + $Flags = Get-CIPPFeatureFlag + } else { + # Get specific flag + $Flags = Get-CIPPFeatureFlag -Id $Id + } + + $StatusCode = [HttpStatusCode]::OK + $Body = $Flags + } + default { + throw "Invalid action: $Action. Valid actions are 'Set' or 'Get'" + } + } + } catch { + Write-LogMessage -API 'ExecFeatureFlag' -message "Failed to process feature flag: $($_.Exception.Message)" -sev 'Error' + $StatusCode = [HttpStatusCode]::BadRequest + $Body = @{ + error = $_.Exception.Message + details = $_.Exception + } + } + + return [HttpResponseContext]@{ + StatusCode = $StatusCode + Body = $Body + } +} diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ListFeatureFlags.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ListFeatureFlags.ps1 new file mode 100644 index 000000000000..3b236cafbb4d --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ListFeatureFlags.ps1 @@ -0,0 +1,31 @@ +function Invoke-ListFeatureFlags { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + CIPP.Core.Read + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + try { + Write-LogMessage -API 'ListFeatureFlags' -message 'Accessed feature flags list' -sev 'Debug' + + $FeatureFlags = Get-CIPPFeatureFlag + + $StatusCode = [HttpStatusCode]::OK + $Body = @($FeatureFlags) + } catch { + Write-LogMessage -API 'ListFeatureFlags' -message "Failed to retrieve feature flags: $($_.Exception.Message)" -sev 'Error' + $StatusCode = [HttpStatusCode]::InternalServerError + $Body = @{ + error = $_.Exception.Message + details = $_.Exception + } + } + + return [HttpResponseContext]@{ + StatusCode = $StatusCode + Body = $Body + } +} diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/New-CippCoreRequest.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/New-CippCoreRequest.ps1 index 70586323cd32..87fc325b6c1e 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/New-CippCoreRequest.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/New-CippCoreRequest.ps1 @@ -43,6 +43,21 @@ function New-CippCoreRequest { $FunctionName = 'Invoke-{0}' -f $Request.Params.CIPPEndpoint Write-Information "API Endpoint: $($Request.Params.CIPPEndpoint) | Frontend Version: $($Request.Headers.'X-CIPP-Version' ?? 'Not specified')" + # Check if endpoint is disabled via feature flags + $FeatureFlags = Get-CIPPFeatureFlag + $DisabledEndpoint = $FeatureFlags | Where-Object { + $_.Enabled -eq $false -and $_.Endpoints -contains $Request.Params.CIPPEndpoint + } | Select-Object -First 1 + + if ($DisabledEndpoint) { + Write-Information "Endpoint $($Request.Params.CIPPEndpoint) is disabled via feature flag: $($DisabledEndpoint.Name)" + $HttpTotalStopwatch.Stop() + return ([HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::ServiceUnavailable + Body = "This feature has been disabled: $($DisabledEndpoint.Description)" + }) + } + if ($Request.Headers.'X-CIPP-Version') { $Table = Get-CippTable -tablename 'Version' $FrontendVer = Get-CIPPAzDataTableEntity @Table -Filter "PartitionKey eq 'Version' and RowKey eq 'frontend'" diff --git a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-BPAOrchestrator.ps1 b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-BPAOrchestrator.ps1 index 71e79f07a75a..41dd759b0c36 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-BPAOrchestrator.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-BPAOrchestrator.ps1 @@ -14,6 +14,12 @@ function Start-BPAOrchestrator { ) try { + # Check feature flag + $FeatureFlag = Get-CIPPFeatureFlag -Id 'BestPracticeAnalyser' + if ($FeatureFlag -and $FeatureFlag.Enabled -eq $false) { + Write-LogMessage -API 'BestPracticeAnalyser' -message 'Best Practice Analyser is disabled via feature flag' -sev Info + return $false + } if ($TenantFilter -ne 'AllTenants') { Write-Verbose "TenantFilter: $TenantFilter" if ($TenantFilter -notmatch '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$') { diff --git a/Modules/CIPPCore/Public/Get-CIPPFeatureFlag.ps1 b/Modules/CIPPCore/Public/Get-CIPPFeatureFlag.ps1 new file mode 100644 index 000000000000..a2ce615f1219 --- /dev/null +++ b/Modules/CIPPCore/Public/Get-CIPPFeatureFlag.ps1 @@ -0,0 +1,137 @@ +function Get-CIPPFeatureFlag { + <# + .SYNOPSIS + Get the state of a feature flag or all feature flags + .DESCRIPTION + Retrieves the current state of a feature flag from the FeatureFlags table, falling back to the default state from JSON if not found. + If Id is not specified, returns all feature flags. + .PARAMETER Id + The ID of the feature flag to retrieve. If not specified, returns all feature flags. + .FUNCTIONALITY + Internal + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $false)] + [string]$Id + ) + + try { + # Get feature flags from JSON + $FeatureFlagsPath = Join-Path -Path $PSScriptRoot -ChildPath '../lib/data/FeatureFlags.json' + $FeatureFlags = Get-Content -Path $FeatureFlagsPath -Raw | ConvertFrom-Json + + # Get all table flags once + $Table = Get-CIPPTable -TableName 'FeatureFlags' + $TableFlags = Get-CIPPAzDataTableEntity @Table -Filter "PartitionKey eq 'FeatureFlag'" + + # If Id is specified, return single flag + if ($Id) { + $FeatureFlag = $FeatureFlags | Where-Object { $_.Id -eq $Id } + + if (-not $FeatureFlag) { + Write-Warning "Feature flag '$Id' not found in FeatureFlags.json" + return $null + } + + $TableFlag = $TableFlags | Where-Object { $_.RowKey -eq $Id } + + if ($TableFlag) { + # Return the table version with metadata from JSON + # Parse JSON arrays from table storage + $Timers = if ($TableFlag.Timers) { $TableFlag.Timers | ConvertFrom-Json } else { $FeatureFlag.Timers } + $Endpoints = if ($TableFlag.Endpoints) { $TableFlag.Endpoints | ConvertFrom-Json } else { $FeatureFlag.Endpoints } + $Pages = if ($TableFlag.Pages) { $TableFlag.Pages | ConvertFrom-Json } else { $FeatureFlag.Pages } + + return [PSCustomObject]@{ + Id = $TableFlag.RowKey + Name = $TableFlag.Name + Description = $TableFlag.Description + AllowUserToggle = $FeatureFlag.AllowUserToggle + Timers = $Timers + Endpoints = $Endpoints + Pages = $Pages + Enabled = $TableFlag.Enabled + } + } else { + # Insert feature flag into table with defaults from JSON + $Entity = @{ + PartitionKey = 'FeatureFlag' + RowKey = $FeatureFlag.Id + Enabled = $FeatureFlag.Enabled + Timers = [string]($FeatureFlag.Timers | ConvertTo-Json -Compress) + Endpoints = [string]($FeatureFlag.Endpoints | ConvertTo-Json -Compress) + Pages = [string]($FeatureFlag.Pages | ConvertTo-Json -Compress) + Name = [string]$FeatureFlag.Name + Description = [string]$FeatureFlag.Description + } + Add-CIPPAzDataTableEntity @Table -Entity $Entity -Force + + # Return the initialized feature flag + return [PSCustomObject]@{ + Id = $FeatureFlag.Id + Name = $FeatureFlag.Name + Description = $FeatureFlag.Description + AllowUserToggle = $FeatureFlag.AllowUserToggle + Timers = $FeatureFlag.Timers + Endpoints = $FeatureFlag.Endpoints + Pages = $FeatureFlag.Pages + Enabled = $FeatureFlag.Enabled + } + } + } else { + # Return all feature flags + $Results = foreach ($FeatureFlag in $FeatureFlags) { + $TableFlag = $TableFlags | Where-Object { $_.RowKey -eq $FeatureFlag.Id } + + if ($TableFlag) { + # Parse JSON arrays from table storage + $Timers = if ($TableFlag.Timers) { $TableFlag.Timers | ConvertFrom-Json } else { $FeatureFlag.Timers } + $Endpoints = if ($TableFlag.Endpoints) { $TableFlag.Endpoints | ConvertFrom-Json } else { $FeatureFlag.Endpoints } + $Pages = if ($TableFlag.Pages) { $TableFlag.Pages | ConvertFrom-Json } else { $FeatureFlag.Pages } + + [PSCustomObject]@{ + Id = $TableFlag.RowKey + Name = $TableFlag.Name + Description = $TableFlag.Description + AllowUserToggle = $FeatureFlag.AllowUserToggle + Timers = $Timers + Endpoints = $Endpoints + Pages = $Pages + Enabled = $TableFlag.Enabled + } + } else { + # Insert feature flag into table with defaults from JSON + $Entity = @{ + PartitionKey = 'FeatureFlag' + RowKey = $FeatureFlag.Id + Enabled = $FeatureFlag.Enabled + Timers = [string]($FeatureFlag.Timers | ConvertTo-Json -Compress) + Endpoints = [string]($FeatureFlag.Endpoints | ConvertTo-Json -Compress) + Pages = [string]($FeatureFlag.Pages | ConvertTo-Json -Compress) + Name = [string]$FeatureFlag.Name + Description = [string]$FeatureFlag.Description + } + Add-CIPPAzDataTableEntity @Table -Entity $Entity -Force + + # Return the initialized feature flag + [PSCustomObject]@{ + Id = $FeatureFlag.Id + Name = $FeatureFlag.Name + Description = $FeatureFlag.Description + AllowUserToggle = $FeatureFlag.AllowUserToggle + Timers = $FeatureFlag.Timers + Endpoints = $FeatureFlag.Endpoints + Pages = $FeatureFlag.Pages + Enabled = $FeatureFlag.Enabled + } + } + } + return $Results + } + } catch { + $ErrorMsg = if ($Id) { "'$Id'" } else { 'flags' } + Write-Error "Error retrieving feature $($ErrorMsg): $($_.Exception.Message)" + return $null + } +} diff --git a/Modules/CIPPCore/Public/Get-CIPPTimerFunctions.ps1 b/Modules/CIPPCore/Public/Get-CIPPTimerFunctions.ps1 index d60ca8ed40cf..51b17666167a 100644 --- a/Modules/CIPPCore/Public/Get-CIPPTimerFunctions.ps1 +++ b/Modules/CIPPCore/Public/Get-CIPPTimerFunctions.ps1 @@ -42,10 +42,17 @@ function Get-CIPPTimerFunctions { $CIPPRoot = (Get-Item $CIPPCoreModuleRoot).Parent.Parent $CippTimers = Get-Content -Path $CIPPRoot\CIPPTimers.json + # Get all feature flags to filter disabled features + $FeatureFlags = Get-CIPPFeatureFlag + $DisabledTimers = $FeatureFlags | Where-Object { $_.Enabled -eq $false } | ForEach-Object { $_.Timers } | Where-Object { $_ } + if ($ListAllTasks) { $Orchestrators = $CippTimers | ConvertFrom-Json | Sort-Object -Property Priority } else { - $Orchestrators = $CippTimers | ConvertFrom-Json | Where-Object { $_.RunOnProcessor -eq $RunOnProcessor } | Sort-Object -Property Priority + # Filter out timers associated with disabled feature flags + $Orchestrators = $CippTimers | ConvertFrom-Json | Where-Object { + $_.RunOnProcessor -eq $RunOnProcessor -and $_.Id -notin $DisabledTimers + } | Sort-Object -Property Priority } $Table = Get-CIPPTable -TableName 'CIPPTimers' $RunOnProcessorTxt = if ($RunOnProcessor) { 'true' } else { 'false' } diff --git a/Modules/CIPPCore/Public/Set-CIPPFeatureFlag.ps1 b/Modules/CIPPCore/Public/Set-CIPPFeatureFlag.ps1 new file mode 100644 index 000000000000..2a36291c7235 --- /dev/null +++ b/Modules/CIPPCore/Public/Set-CIPPFeatureFlag.ps1 @@ -0,0 +1,68 @@ +function Set-CIPPFeatureFlag { + <# + .SYNOPSIS + Set the state of a feature flag + .DESCRIPTION + Updates the state of a feature flag in the FeatureFlags table + .PARAMETER Id + The ID of the feature flag to update + .PARAMETER Enabled + The new enabled state for the feature flag (true/false) + .FUNCTIONALITY + Internal + #> + [CmdletBinding(SupportsShouldProcess = $true)] + param( + [Parameter(Mandatory = $true)] + [string]$Id, + + [Parameter(Mandatory = $true)] + [bool]$Enabled + ) + + try { + # Get feature flags from JSON to validate + $FeatureFlagsPath = Join-Path -Path $PSScriptRoot -ChildPath '../lib/data/FeatureFlags.json' + $FeatureFlags = Get-Content -Path $FeatureFlagsPath -Raw | ConvertFrom-Json + + # Find the requested feature flag in JSON + $FeatureFlag = $FeatureFlags | Where-Object { $_.Id -eq $Id } + + if (-not $FeatureFlag) { + Write-Error "Feature flag '$Id' not found in FeatureFlags.json" + return $false + } + + # Check if user toggle is allowed + if (-not $FeatureFlag.AllowUserToggle) { + Write-Warning "Feature flag '$Id' does not allow user toggling" + return $false + } + + if ($PSCmdlet.ShouldProcess($Id, "Set feature flag enabled to $Enabled")) { + # Update or create the table entry + $Table = Get-CIPPTable -TableName 'FeatureFlags' + + # Convert arrays to JSON strings for table storage + $Entity = @{ + PartitionKey = 'FeatureFlag' + RowKey = $Id + Enabled = $Enabled + Timers = [string]($FeatureFlag.Timers | ConvertTo-Json -Compress) + Endpoints = [string]($FeatureFlag.Endpoints | ConvertTo-Json -Compress) + Pages = [string]($FeatureFlag.Pages | ConvertTo-Json -Compress) + Name = [string]$FeatureFlag.Name + Description = [string]$FeatureFlag.Description + LastModified = (Get-Date).ToUniversalTime().ToString('o') + } + + $Result = Add-CIPPAzDataTableEntity @Table -Entity $Entity -Force + + Write-Information "Feature flag '$Id' set to $Enabled" + return $true + } + } catch { + Write-Error "Error setting feature flag '$Id': $($_.Exception.Message)" + return $false + } +} diff --git a/Modules/CIPPCore/lib/data/FeatureFlags.json b/Modules/CIPPCore/lib/data/FeatureFlags.json new file mode 100644 index 000000000000..5be983f636ed --- /dev/null +++ b/Modules/CIPPCore/lib/data/FeatureFlags.json @@ -0,0 +1,25 @@ +[ + { + "Id": "BestPracticeAnalyser", + "Name": "Best Practice Analyser", + "Type": "Orchestrator", + "Description": "Best Practice Analyser orchestrator (deprecated)", + "Enabled": false, + "AllowUserToggle": true, + "Timers": [ + "80070b4f-95ed-4e5f-be4c-9e339306d4aa" + ], + "Endpoints": [ + "BestPracticeAnalyser_List", + "ExecBPA", + "ListBPA", + "ListBPATemplates", + "RemoveBPATemplate" + ], + "Pages": [ + "/tenant/standards/bpa-report", + "/tenant/standards/bpa-report/builder", + "/tenant/standards/bpa-report/view" + ] + } +] From e6ba6fa132498059906ccacd16a9f7d2f8dd9585 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Wed, 11 Feb 2026 11:40:22 -0500 Subject: [PATCH 381/503] Persist only Enabled in FeatureFlags table Keep feature flag metadata in the static JSON and only store the enabled state in table storage. Get-CIPPFeatureFlag now returns Enabled from the table but sources Id/Name/Description/AllowUserToggle/Timers/Endpoints/Pages from FeatureFlags.json, and it inserts table entities with just RowKey and Enabled if missing. Set-CIPPFeatureFlag updates/creates table entries with only PartitionKey, RowKey, Enabled and LastModified (removed serialization of timers/endpoints/pages/name/description). Also update FeatureFlags.json for BestPracticeAnalyser: removed Type and clarified the deprecation description. --- .../CIPPCore/Public/Get-CIPPFeatureFlag.ps1 | 51 ++++++------------- .../CIPPCore/Public/Set-CIPPFeatureFlag.ps1 | 8 +-- Modules/CIPPCore/lib/data/FeatureFlags.json | 3 +- 3 files changed, 18 insertions(+), 44 deletions(-) diff --git a/Modules/CIPPCore/Public/Get-CIPPFeatureFlag.ps1 b/Modules/CIPPCore/Public/Get-CIPPFeatureFlag.ps1 index a2ce615f1219..b068d3d37cbe 100644 --- a/Modules/CIPPCore/Public/Get-CIPPFeatureFlag.ps1 +++ b/Modules/CIPPCore/Public/Get-CIPPFeatureFlag.ps1 @@ -37,33 +37,23 @@ function Get-CIPPFeatureFlag { $TableFlag = $TableFlags | Where-Object { $_.RowKey -eq $Id } if ($TableFlag) { - # Return the table version with metadata from JSON - # Parse JSON arrays from table storage - $Timers = if ($TableFlag.Timers) { $TableFlag.Timers | ConvertFrom-Json } else { $FeatureFlag.Timers } - $Endpoints = if ($TableFlag.Endpoints) { $TableFlag.Endpoints | ConvertFrom-Json } else { $FeatureFlag.Endpoints } - $Pages = if ($TableFlag.Pages) { $TableFlag.Pages | ConvertFrom-Json } else { $FeatureFlag.Pages } - + # Return feature flag with Enabled from table, everything else from JSON return [PSCustomObject]@{ - Id = $TableFlag.RowKey - Name = $TableFlag.Name - Description = $TableFlag.Description + Id = $FeatureFlag.Id + Name = $FeatureFlag.Name + Description = $FeatureFlag.Description AllowUserToggle = $FeatureFlag.AllowUserToggle - Timers = $Timers - Endpoints = $Endpoints - Pages = $Pages + Timers = $FeatureFlag.Timers + Endpoints = $FeatureFlag.Endpoints + Pages = $FeatureFlag.Pages Enabled = $TableFlag.Enabled } } else { - # Insert feature flag into table with defaults from JSON + # Insert feature flag into table with defaults from JSON (only RowKey and Enabled) $Entity = @{ PartitionKey = 'FeatureFlag' RowKey = $FeatureFlag.Id Enabled = $FeatureFlag.Enabled - Timers = [string]($FeatureFlag.Timers | ConvertTo-Json -Compress) - Endpoints = [string]($FeatureFlag.Endpoints | ConvertTo-Json -Compress) - Pages = [string]($FeatureFlag.Pages | ConvertTo-Json -Compress) - Name = [string]$FeatureFlag.Name - Description = [string]$FeatureFlag.Description } Add-CIPPAzDataTableEntity @Table -Entity $Entity -Force @@ -85,32 +75,23 @@ function Get-CIPPFeatureFlag { $TableFlag = $TableFlags | Where-Object { $_.RowKey -eq $FeatureFlag.Id } if ($TableFlag) { - # Parse JSON arrays from table storage - $Timers = if ($TableFlag.Timers) { $TableFlag.Timers | ConvertFrom-Json } else { $FeatureFlag.Timers } - $Endpoints = if ($TableFlag.Endpoints) { $TableFlag.Endpoints | ConvertFrom-Json } else { $FeatureFlag.Endpoints } - $Pages = if ($TableFlag.Pages) { $TableFlag.Pages | ConvertFrom-Json } else { $FeatureFlag.Pages } - + # Return feature flag with Enabled from table, everything else from JSON [PSCustomObject]@{ - Id = $TableFlag.RowKey - Name = $TableFlag.Name - Description = $TableFlag.Description + Id = $FeatureFlag.Id + Name = $FeatureFlag.Name + Description = $FeatureFlag.Description AllowUserToggle = $FeatureFlag.AllowUserToggle - Timers = $Timers - Endpoints = $Endpoints - Pages = $Pages + Timers = $FeatureFlag.Timers + Endpoints = $FeatureFlag.Endpoints + Pages = $FeatureFlag.Pages Enabled = $TableFlag.Enabled } } else { - # Insert feature flag into table with defaults from JSON + # Insert feature flag into table with defaults from JSON (only RowKey and Enabled) $Entity = @{ PartitionKey = 'FeatureFlag' RowKey = $FeatureFlag.Id Enabled = $FeatureFlag.Enabled - Timers = [string]($FeatureFlag.Timers | ConvertTo-Json -Compress) - Endpoints = [string]($FeatureFlag.Endpoints | ConvertTo-Json -Compress) - Pages = [string]($FeatureFlag.Pages | ConvertTo-Json -Compress) - Name = [string]$FeatureFlag.Name - Description = [string]$FeatureFlag.Description } Add-CIPPAzDataTableEntity @Table -Entity $Entity -Force diff --git a/Modules/CIPPCore/Public/Set-CIPPFeatureFlag.ps1 b/Modules/CIPPCore/Public/Set-CIPPFeatureFlag.ps1 index 2a36291c7235..d8cb088f0fcf 100644 --- a/Modules/CIPPCore/Public/Set-CIPPFeatureFlag.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPFeatureFlag.ps1 @@ -40,19 +40,13 @@ function Set-CIPPFeatureFlag { } if ($PSCmdlet.ShouldProcess($Id, "Set feature flag enabled to $Enabled")) { - # Update or create the table entry + # Update or create the table entry (only store RowKey and Enabled) $Table = Get-CIPPTable -TableName 'FeatureFlags' - # Convert arrays to JSON strings for table storage $Entity = @{ PartitionKey = 'FeatureFlag' RowKey = $Id Enabled = $Enabled - Timers = [string]($FeatureFlag.Timers | ConvertTo-Json -Compress) - Endpoints = [string]($FeatureFlag.Endpoints | ConvertTo-Json -Compress) - Pages = [string]($FeatureFlag.Pages | ConvertTo-Json -Compress) - Name = [string]$FeatureFlag.Name - Description = [string]$FeatureFlag.Description LastModified = (Get-Date).ToUniversalTime().ToString('o') } diff --git a/Modules/CIPPCore/lib/data/FeatureFlags.json b/Modules/CIPPCore/lib/data/FeatureFlags.json index 5be983f636ed..61a49d453458 100644 --- a/Modules/CIPPCore/lib/data/FeatureFlags.json +++ b/Modules/CIPPCore/lib/data/FeatureFlags.json @@ -2,8 +2,7 @@ { "Id": "BestPracticeAnalyser", "Name": "Best Practice Analyser", - "Type": "Orchestrator", - "Description": "Best Practice Analyser orchestrator (deprecated)", + "Description": "The Best Practice Analyser has been deprecated and will be removed in a future release.", "Enabled": false, "AllowUserToggle": true, "Timers": [ From fc23cd660c6196c44cf69fe54d8076fbfc0883b9 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Wed, 11 Feb 2026 17:21:18 -0500 Subject: [PATCH 382/503] Include Selector2 1024-bit DKIM in rotation filter Expand DKIM selection to include configs where either Selector1KeySize or Selector2KeySize equals 1024 and the config is enabled. Previously only Selector1KeySize was checked, which could miss keys needing rotation on Selector2. --- .../CIPPCore/Public/Standards/Invoke-CIPPStandardRotateDKIM.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardRotateDKIM.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardRotateDKIM.ps1 index 11759ab03809..8b64998f0f86 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardRotateDKIM.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardRotateDKIM.ps1 @@ -40,7 +40,7 @@ function Invoke-CIPPStandardRotateDKIM { } #we're done. try { - $DKIM = (New-ExoRequest -tenantid $tenant -cmdlet 'Get-DkimSigningConfig') | Where-Object { $_.Selector1KeySize -eq 1024 -and $_.Enabled -eq $true } + $DKIM = (New-ExoRequest -tenantid $tenant -cmdlet 'Get-DkimSigningConfig') | Where-Object { ($_.Selector1KeySize -eq 1024 -or $_.Selector2KeySize -eq 1024) -and $_.Enabled -eq $true } } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the DKIM state for $Tenant. Error: $ErrorMessage" -Sev Error From eda364ed5c4236e11c5a87ff392fe4ef64527711 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Wed, 11 Feb 2026 18:56:59 -0500 Subject: [PATCH 383/503] Allow overwriting app templates; fix scope lookup Add support for an Overwrite flag and logic to find & reuse existing templates/permission sets when creating app approval templates. Improve delegated permission handling by grouping multi-scope grants, preferring publishedPermissionScopes (with fallback to treat IDs as names), and adding diagnostics. Also adjust servicePrincipal fetch to request publishedPermissionScopes, reuse or generate PermissionSetId when updating, add stronger logging, and use -Force on table writes. --- .../Invoke-ExecCreateAppTemplate.ps1 | 127 +++++++++++++++--- 1 file changed, 105 insertions(+), 22 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Application Approval/Invoke-ExecCreateAppTemplate.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Application Approval/Invoke-ExecCreateAppTemplate.ps1 index 9e02c588bf36..23315d7eb700 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Application Approval/Invoke-ExecCreateAppTemplate.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Application Approval/Invoke-ExecCreateAppTemplate.ps1 @@ -18,6 +18,7 @@ function Invoke-ExecCreateAppTemplate { $AppId = $Request.Body.AppId $DisplayName = $Request.Body.DisplayName $Type = $Request.Body.Type # 'servicePrincipal' or 'application' + $Overwrite = $Request.Body.Overwrite -eq $true if ([string]::IsNullOrWhiteSpace($AppId)) { throw 'AppId is required' @@ -88,14 +89,21 @@ function Invoke-ExecCreateAppTemplate { $AppRoleAssignments = ($GrantsResults | Where-Object { $_.id -eq 'assignments' }).body.value $DelegateResourceAccess = $DelegatePermissionGrants | Group-Object -Property resourceId | ForEach-Object { + $resourceAccessList = [System.Collections.Generic.List[object]]::new() + foreach ($Grant in $_.Group) { + if (-not [string]::IsNullOrWhiteSpace($Grant.scope)) { + $scopeNames = $Grant.scope -split '\s+' | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } + foreach ($scopeName in $scopeNames) { + $resourceAccessList.Add([pscustomobject]@{ + id = $scopeName + type = 'Scope' + }) + } + } + } [pscustomobject]@{ resourceAppId = ($TenantInfo | Where-Object -Property id -EQ $_.Name).appId - resourceAccess = @($_.Group | ForEach-Object { - [pscustomobject]@{ - id = $_.scope - type = 'Scope' - } - }) + resourceAccess = @($resourceAccessList) } } @@ -229,11 +237,11 @@ function Invoke-ExecCreateAppTemplate { $RequestId = "sp-$RequestIndex" $AppIdToRequestId[$ResourceAppId] = $RequestId - # Use object ID to fetch full details with appRoles and oauth2PermissionScopes + # Use object ID to fetch full details with appRoles $BulkRequests.Add([PSCustomObject]@{ id = $RequestId method = 'GET' - url = "/servicePrincipals/$($ResourceSPInfo.id)?`$select=id,appId,displayName,appRoles,oauth2PermissionScopes" + url = "/servicePrincipals/$($ResourceSPInfo.id)?`$select=id,appId,displayName,appRoles,publishedPermissionScopes" }) $RequestIndex++ } else { @@ -270,6 +278,8 @@ function Invoke-ExecCreateAppTemplate { continue } + #Write-Information ($ResourceSP | ConvertTo-Json -Depth 10) + foreach ($Access in $Resource.resourceAccess) { if ($Access.type -eq 'Role') { # Look up application permission name from appRoles @@ -284,16 +294,27 @@ function Invoke-ExecCreateAppTemplate { Write-LogMessage -headers $Request.headers -API $APINAME -message "Application permission $($Access.id) not found in $ResourceAppId appRoles" -Sev 'Warning' } } elseif ($Access.type -eq 'Scope') { - # Look up delegated permission name from oauth2PermissionScopes - $PermissionScope = $ResourceSP.oauth2PermissionScopes | Where-Object { $_.id -eq $Access.id } | Select-Object -First 1 - if ($PermissionScope) { + Write-Information "Processing delegated permission with id $($Access.id) for resource appId $ResourceAppId" + # Try to look up the permission by ID in publishedPermissionScopes + $OAuth2Permission = $ResourceSP.publishedPermissionScopes | Where-Object { $_.id -eq $Access.id } | Select-Object -First 1 + $OAuth2PermissionValue = $ResourceSP.publishedPermissionScopes | Where-Object { $_.value -eq $Access.id } | Select-Object -First 1 + if ($OAuth2Permission) { + Write-Information "Found delegated permission in publishedPermissionScopes with value: $($OAuth2Permission.value)" + # Found the permission - use the value from the lookup $PermObj = [PSCustomObject]@{ id = $Access.id - value = $PermissionScope.value # Use the claim value name, not the GUID + value = $OAuth2Permission.value } [void]$DelegatedPerms.Add($PermObj) } else { - Write-LogMessage -headers $Request.headers -API $APINAME -message "Delegated permission $($Access.id) not found in $ResourceAppId oauth2PermissionScopes" -Sev 'Warning' + # Not found by ID - assume Access.id is already the permission name + Write-Information "Could not find delegated permission by ID - using provided ID as value: $($Access.id)" + Write-Information "OAuth2PermissionValueLookup: $($OAuth2PermissionValue | ConvertTo-Json -Depth 10)" + $PermObj = [PSCustomObject]@{ + id = $OAuth2PermissionValue.id ?? $Access.id + value = $Access.id + } + [void]$DelegatedPerms.Add($PermObj) } } } @@ -304,10 +325,75 @@ function Invoke-ExecCreateAppTemplate { } } - # Create the permission set in AppPermissions table + # Permission set ID will be determined after template lookup + $PermissionSetId = $null + } + + # Get permissions table reference (needed later) + $PermissionsTable = Get-CIPPTable -TableName 'AppPermissions' + + # Create the template + $Table = Get-CIPPTable -TableName 'templates' + + # Check if template already exists + # For servicePrincipal: match by AppId (immutable) + # For application: match by DisplayName (since AppId changes when copied) + $ExistingTemplate = $null + if ($Overwrite) { + try { + $Filter = "PartitionKey eq 'AppApprovalTemplate'" + $AllTemplates = Get-CIPPAzDataTableEntity @Table -Filter $Filter + $TemplateNameToMatch = "$DisplayName (Auto-created)" + + foreach ($Template in $AllTemplates) { + $TemplateData = $Template.JSON | ConvertFrom-Json + $IsMatch = $false + + if ($Type -eq 'servicePrincipal') { + # Match by AppId for service principals + $IsMatch = $TemplateData.AppId -eq $AppId + } else { + # Match by TemplateName for app registrations + $IsMatch = $TemplateData.TemplateName -eq $TemplateNameToMatch + } + + if ($IsMatch) { + $ExistingTemplate = $Template + # Reuse the existing permission set ID if it exists + if ($TemplateData.PermissionSetId) { + $PermissionSetId = $TemplateData.PermissionSetId + Write-LogMessage -headers $Request.headers -API $APINAME -message "Found existing permission set ID: $PermissionSetId in template" -Sev 'Info' + } else { + Write-LogMessage -headers $Request.headers -API $APINAME -message 'Existing template found but has no PermissionSetId' -Sev 'Warning' + } + break + } + } + } catch { + # Ignore lookup errors + Write-LogMessage -headers $Request.headers -API $APINAME -message "Error during template lookup: $($_.Exception.Message)" -Sev 'Warning' + } + } + + if ($ExistingTemplate) { + $TemplateId = $ExistingTemplate.RowKey + $MatchCriteria = if ($Type -eq 'servicePrincipal') { "AppId: $AppId" } else { "DisplayName: $DisplayName" } + Write-LogMessage -headers $Request.headers -API $APINAME -message "Overwriting existing template matched by $MatchCriteria (Template ID: $TemplateId)" -Sev 'Info' + if ($PermissionSetId) { + Write-LogMessage -headers $Request.headers -API $APINAME -message "Reusing permission set ID: $PermissionSetId" -Sev 'Info' + } + } else { + $TemplateId = (New-Guid).Guid + } + + # Create new permission set ID if we don't have one yet + if (-not $PermissionSetId) { $PermissionSetId = (New-Guid).Guid - $PermissionsTable = Get-CIPPTable -TableName 'AppPermissions' + Write-LogMessage -headers $Request.headers -API $APINAME -message "Creating new permission set ID: $PermissionSetId" -Sev 'Info' + } + # Now create/update the permission set entity with the determined ID + if ($Permissions -and $Permissions.Count -gt 0) { $PermissionEntity = @{ 'PartitionKey' = 'Templates' 'RowKey' = [string]$PermissionSetId @@ -317,13 +403,9 @@ function Invoke-ExecCreateAppTemplate { } Add-CIPPAzDataTableEntity @PermissionsTable -Entity $PermissionEntity -Force - Write-LogMessage -headers $Request.headers -API $APINAME -message "Permission set created with ID: $PermissionSetId for $($Permissions.Count) resource(s)" -Sev 'Info' + Write-LogMessage -headers $Request.headers -API $APINAME -message "Permission set saved with ID: $PermissionSetId for $($Permissions.Count) resource(s)" -Sev 'Info' } - # Create the template - $Table = Get-CIPPTable -TableName 'templates' - $TemplateId = (New-Guid).Guid - $TemplateJson = @{ TemplateName = "$DisplayName (Auto-created)" AppId = $AppId @@ -343,7 +425,7 @@ function Invoke-ExecCreateAppTemplate { PartitionKey = 'AppApprovalTemplate' } - Add-CIPPAzDataTableEntity @Table -Entity $Entity + Add-CIPPAzDataTableEntity @Table -Entity $Entity -Force $PermissionCount = 0 if ($CIPPPermissions -and $CIPPPermissions.Count -gt 0) { @@ -358,7 +440,8 @@ function Invoke-ExecCreateAppTemplate { } } - $Message = "Template created: $DisplayName with $PermissionCount permission(s)" + $Action = if ($ExistingTemplate) { 'updated' } else { 'created' } + $Message = "Template $($Action) - $DisplayName with $PermissionCount permission(s)" Write-LogMessage -headers $Request.headers -API $APINAME -message $Message -Sev 'Info' $Body = @{ From 9148b20b143092c7517a40f8fe9d0c627d3492cd Mon Sep 17 00:00:00 2001 From: Integrated Solutions Date: Thu, 12 Feb 2026 11:00:42 +1000 Subject: [PATCH 384/503] added action to "Deploy to Custom Group" for authentication methods --- .../Administration/Invoke-SetAuthMethod.ps1 | 39 ++++++++++++++++++- .../Public/Set-CIPPAuthenticationPolicy.ps1 | 35 +++++++++++++++++ 2 files changed, 73 insertions(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-SetAuthMethod.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-SetAuthMethod.ps1 index 8c7ab7fab111..5c696ca8e199 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-SetAuthMethod.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-SetAuthMethod.ps1 @@ -15,10 +15,47 @@ function Invoke-SetAuthMethod { $State = if ($Request.Body.state -eq 'enabled') { $true } else { $false } $TenantFilter = $Request.Body.tenantFilter $AuthenticationMethodId = $Request.Body.Id + $GroupIdsRaw = $Request.Body.GroupIds + + function Get-StandardizedList { + param($InputObject) + + if ($null -eq $InputObject) { return @() } + + if ($InputObject -is [string]) { + return @( + $InputObject -split ',' | + ForEach-Object { $_.Trim() } | + Where-Object { -not [string]::IsNullOrWhiteSpace($_) } + ) + } + + if ($InputObject -is [array] -or $InputObject -is [System.Collections.IEnumerable]) { + return @( + $InputObject | + ForEach-Object { "$_".Trim() } | + Where-Object { -not [string]::IsNullOrWhiteSpace($_) } + ) + } + + return @("$InputObject".Trim()) | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } + } + + $GroupIds = Get-StandardizedList -InputObject $GroupIdsRaw try { - $Result = Set-CIPPAuthenticationPolicy -Tenant $TenantFilter -APIName $APIName -AuthenticationMethodId $AuthenticationMethodId -Enabled $State -Headers $Headers + $Params = @{ + Tenant = $TenantFilter + APIName = $APIName + AuthenticationMethodId = $AuthenticationMethodId + Enabled = $State + Headers = $Headers + } + if (@($GroupIds).Count -gt 0) { + $Params.GroupIds = @($GroupIds) + } + $Result = Set-CIPPAuthenticationPolicy @Params $StatusCode = [HttpStatusCode]::OK } catch { $Result = $_.Exception.Message diff --git a/Modules/CIPPCore/Public/Set-CIPPAuthenticationPolicy.ps1 b/Modules/CIPPCore/Public/Set-CIPPAuthenticationPolicy.ps1 index d3335cc89f35..f4b1b0f9793b 100644 --- a/Modules/CIPPCore/Public/Set-CIPPAuthenticationPolicy.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPAuthenticationPolicy.ps1 @@ -10,6 +10,7 @@ function Set-CIPPAuthenticationPolicy { $TAPDefaultLifeTime = 60, #minutes $TAPDefaultLength = 8, #TAP password generated length in chars $TAPisUsableOnce = $true, + [Parameter()][string[]]$GroupIds, [Parameter()][ValidateRange(1, 395)]$QRCodeLifetimeInDays = 365, [Parameter()][ValidateRange(8, 20)]$QRCodePinLength = 8, $APIName = 'Set Authentication Policy', @@ -118,6 +119,40 @@ function Set-CIPPAuthenticationPolicy { throw "Somehow you hit the default case with an input of $AuthenticationMethodId . You probably made a typo in the input for AuthenticationMethodId. It`'s case sensitive." } } + + if ($PSBoundParameters.ContainsKey('GroupIds') -and @($GroupIds).Count -gt 0) { + $ResolvedGroupIds = @( + @($GroupIds) | + ForEach-Object { "$_".Trim() } | + Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | + Select-Object -Unique + ) + + if ($ResolvedGroupIds.Count -gt 0) { + $TargetTemplate = $null + if ($CurrentInfo.includeTargets -and @($CurrentInfo.includeTargets).Count -gt 0) { + $TargetTemplate = $CurrentInfo.includeTargets | Select-Object -First 1 + } + + $CurrentInfo.includeTargets = @( + foreach ($GroupId in $ResolvedGroupIds) { + $TargetProperties = [ordered]@{} + if ($TargetTemplate) { + foreach ($Property in $TargetTemplate.PSObject.Properties) { + if ($Property.Name -ne 'id' -and $Property.Name -ne 'targetType') { + $TargetProperties[$Property.Name] = $Property.Value + } + } + } + $TargetProperties.targetType = 'group' + $TargetProperties.id = $GroupId + [pscustomobject]$TargetProperties + } + ) + $OptionalLogMessage = "$OptionalLogMessage and targeted groups set to $($ResolvedGroupIds -join ', ')" + } + } + # Set state of the authentication method try { if ($PSCmdlet.ShouldProcess($AuthenticationMethodId, "Set state to $State $OptionalLogMessage")) { From e3f82840efcf8511b83a1fc619dd674488161734 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Thu, 12 Feb 2026 19:33:37 +0100 Subject: [PATCH 385/503] add totals from db. --- .../Timer Functions/Start-CIPPStatsTimer.ps1 | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-CIPPStatsTimer.ps1 b/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-CIPPStatsTimer.ps1 index e4060f1c0a37..3218c12a3491 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-CIPPStatsTimer.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-CIPPStatsTimer.ps1 @@ -6,7 +6,7 @@ function Start-CIPPStatsTimer { [CmdletBinding(SupportsShouldProcess = $true)] param() #These stats are sent to a central server to help us understand how many tenants are using the product, and how many are using the latest version, this information allows the CIPP team to make decisions about what features to support, and what features to deprecate. - #We will never ship any data that is related to your instance, all we care about is the number of tenants, and the version of the API you are running, and if you completed setup. + if ($PSCmdlet.ShouldProcess('Start-CIPPStatsTimer', 'Starting CIPP Stats Timer')) { if ($env:ApplicationID -ne 'LongApplicationID') { @@ -25,13 +25,19 @@ function Start-CIPPStatsTimer { } catch { $RawExt = @{} } - + $counts = Get-CIPPDbItem -TenantFilter AllTenants -CountsOnly + $userCount = ($counts | Where-Object { $_.RowKey -eq 'Users-Count' } | Measure-Object -Property DataCount -Sum).Sum + $deviceCount = ($counts | Where-Object { $_.RowKey -eq 'Devices-Count' } | Measure-Object -Property DataCount -Sum).Sum + $groupsCount = ($counts | Where-Object { $_.RowKey -eq 'Groups-Count' } | Measure-Object -Property DataCount -Sum).Sum $SendingObject = [PSCustomObject]@{ rgid = $env:WEBSITE_SITE_NAME SetupComplete = $SetupComplete RunningVersionAPI = $APIVersion.trim() CountOfTotalTenants = $tenantcount uid = $env:TenantID + UserCount = $userCount + DeviceCount = $deviceCount + GroupsCount = $groupsCount CIPPAPI = $RawExt.CIPPAPI.Enabled Hudu = $RawExt.Hudu.Enabled Sherweb = $RawExt.Sherweb.Enabled From df83265e6ec4c0b0bfb9a9d8c74e102b335b9078 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Thu, 12 Feb 2026 23:18:03 +0100 Subject: [PATCH 386/503] clean up AI fragments --- .../Get-CIPPAlertInactiveGuestUsers.ps1 | 39 ++++------- .../Alerts/Get-CIPPAlertInactiveUsers.ps1 | 28 ++++---- .../Alerts/Get-CIPPAlertStaleEntraDevices.ps1 | 64 ++++++++----------- 3 files changed, 52 insertions(+), 79 deletions(-) diff --git a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertInactiveGuestUsers.ps1 b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertInactiveGuestUsers.ps1 index 1109cd5e5bc0..839a0af97e37 100644 --- a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertInactiveGuestUsers.ps1 +++ b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertInactiveGuestUsers.ps1 @@ -18,19 +18,15 @@ function Get-CIPPAlertInactiveGuestUsers { $inactiveDays = 90 $excludeDisabled = $false - if ($InputValue -is [hashtable] -or $InputValue -is [pscustomobject]) { - $excludeDisabled = [bool]$InputValue.ExcludeDisabled - if ($null -ne $InputValue.DaysSinceLastLogin -and $InputValue.DaysSinceLastLogin -ne '') { - $parsedDays = 0 - if ([int]::TryParse($InputValue.DaysSinceLastLogin.ToString(), [ref]$parsedDays) -and $parsedDays -gt 0) { - $inactiveDays = $parsedDays - } + $excludeDisabled = [bool]$InputValue.ExcludeDisabled + if ($null -ne $InputValue.DaysSinceLastLogin -and $InputValue.DaysSinceLastLogin -ne '') { + $parsedDays = 0 + if ([int]::TryParse($InputValue.DaysSinceLastLogin.ToString(), [ref]$parsedDays) -and $parsedDays -gt 0) { + $inactiveDays = $parsedDays } } - elseif ($InputValue -eq $true) { - # Backwards compatibility: legacy single-input boolean means exclude disabled users - $excludeDisabled = $true - } + + $Lookup = (Get-Date).AddDays(-$inactiveDays).ToUniversalTime() Write-Host "Checking for guest users inactive since $Lookup (excluding disabled: $excludeDisabled)" @@ -39,13 +35,11 @@ function Get-CIPPAlertInactiveGuestUsers { $Uri = if ($BaseFilter) { "https://graph.microsoft.com/beta/users?`$filter=$BaseFilter&`$select=id,UserPrincipalName,signInActivity,mail,userType,accountEnabled,assignedLicenses" - } - else { + } else { "https://graph.microsoft.com/beta/users?`$select=id,UserPrincipalName,signInActivity,mail,userType,accountEnabled,assignedLicenses" } - $GraphRequest = New-GraphGetRequest -uri $Uri -scope 'https://graph.microsoft.com/.default' -tenantid $TenantFilter | - Where-Object { $_.userType -eq 'Guest' } + $GraphRequest = New-GraphGetRequest -uri $Uri-tenantid $TenantFilter | Where-Object { $_.userType -eq 'Guest' } $AlertData = foreach ($user in $GraphRequest) { $lastInteractive = $user.signInActivity.lastSignInDateTime @@ -55,11 +49,9 @@ function Get-CIPPAlertInactiveGuestUsers { $lastSignIn = $null if ($lastInteractive -and $lastNonInteractive) { $lastSignIn = if ([DateTime]$lastInteractive -gt [DateTime]$lastNonInteractive) { $lastInteractive } else { $lastNonInteractive } - } - elseif ($lastInteractive) { + } elseif ($lastInteractive) { $lastSignIn = $lastInteractive - } - elseif ($lastNonInteractive) { + } elseif ($lastNonInteractive) { $lastSignIn = $lastNonInteractive } @@ -72,8 +64,7 @@ function Get-CIPPAlertInactiveGuestUsers { if (-not $lastSignIn) { $Message = 'Guest user {0} has never signed in.' -f $user.UserPrincipalName - } - else { + } else { $daysSinceSignIn = [Math]::Round(((Get-Date) - [DateTime]$lastSignIn).TotalDays) $Message = 'Guest user {0} has been inactive for {1} days. Last sign-in: {2}' -f $user.UserPrincipalName, $daysSinceSignIn, $lastSignIn } @@ -91,10 +82,8 @@ function Get-CIPPAlertInactiveGuestUsers { } Write-AlertTrace -cmdletName $MyInvocation.MyCommand -tenantFilter $TenantFilter -data $AlertData - } - catch {} - } - catch { + } catch {} + } catch { Write-AlertMessage -tenant $($TenantFilter) -message "Failed to check inactive guest users for $($TenantFilter): $(Get-NormalizedError -message $_.Exception.message)" } } diff --git a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertInactiveUsers.ps1 b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertInactiveUsers.ps1 index 0a42e8346cce..037f37e501d5 100644 --- a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertInactiveUsers.ps1 +++ b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertInactiveUsers.ps1 @@ -18,17 +18,12 @@ function Get-CIPPAlertInactiveUsers { $inactiveDays = 90 $excludeDisabled = $false - if ($InputValue -is [hashtable] -or $InputValue -is [pscustomobject]) { - $excludeDisabled = [bool]$InputValue.ExcludeDisabled - if ($null -ne $InputValue.DaysSinceLastLogin -and $InputValue.DaysSinceLastLogin -ne '') { - $parsedDays = 0 - if ([int]::TryParse($InputValue.DaysSinceLastLogin.ToString(), [ref]$parsedDays) -and $parsedDays -gt 0) { - $inactiveDays = $parsedDays - } + $excludeDisabled = [bool]$InputValue.ExcludeDisabled + if ($null -ne $InputValue.DaysSinceLastLogin -and $InputValue.DaysSinceLastLogin -ne '') { + $parsedDays = 0 + if ([int]::TryParse($InputValue.DaysSinceLastLogin.ToString(), [ref]$parsedDays) -and $parsedDays -gt 0) { + $inactiveDays = $parsedDays } - } elseif ($InputValue -eq $true) { - # Backwards compatibility: legacy single-input boolean means exclude disabled users - $excludeDisabled = $true } $Lookup = (Get-Date).AddDays(-$inactiveDays).ToUniversalTime() @@ -42,8 +37,7 @@ function Get-CIPPAlertInactiveUsers { "https://graph.microsoft.com/beta/users?`$select=id,UserPrincipalName,signInActivity,mail,userType,accountEnabled,assignedLicenses" } - $GraphRequest = New-GraphGetRequest -uri $Uri -scope 'https://graph.microsoft.com/.default' -tenantid $TenantFilter | - Where-Object { $_.userType -eq 'Member' } + $GraphRequest = New-GraphGetRequest -uri $Uri -tenantid $TenantFilter | Where-Object { $_.userType -eq 'Member' } $AlertData = foreach ($user in $GraphRequest) { $lastInteractive = $user.signInActivity.lastSignInDateTime @@ -73,12 +67,12 @@ function Get-CIPPAlertInactiveUsers { } [PSCustomObject]@{ - UserPrincipalName = $user.UserPrincipalName - Id = $user.id - lastSignIn = $lastSignIn + UserPrincipalName = $user.UserPrincipalName + Id = $user.id + lastSignIn = $lastSignIn DaysSinceLastSignIn = if ($daysSinceSignIn) { $daysSinceSignIn } else { 'N/A' } - Message = $Message - Tenant = $TenantFilter + Message = $Message + Tenant = $TenantFilter } } } diff --git a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertStaleEntraDevices.ps1 b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertStaleEntraDevices.ps1 index 2c308fb00e05..29043c3288fd 100644 --- a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertStaleEntraDevices.ps1 +++ b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertStaleEntraDevices.ps1 @@ -15,19 +15,13 @@ function Get-CIPPAlertStaleEntraDevices { try { $inactiveDays = 90 - if ($InputValue -is [hashtable] -or $InputValue -is [pscustomobject]) { - $excludeDisabled = [bool]$InputValue.ExcludeDisabled - if ($null -ne $InputValue.DaysSinceLastActivity -and $InputValue.DaysSinceLastActivity -ne '') { - $parsedDays = 0 - if ([int]::TryParse($InputValue.DaysSinceLastActivity.ToString(), [ref]$parsedDays) -and $parsedDays -gt 0) { - $inactiveDays = $parsedDays - } + $excludeDisabled = [bool]$InputValue.ExcludeDisabled + if ($null -ne $InputValue.DaysSinceLastActivity -and $InputValue.DaysSinceLastActivity -ne '') { + $parsedDays = 0 + if ([int]::TryParse($InputValue.DaysSinceLastActivity.ToString(), [ref]$parsedDays) -and $parsedDays -gt 0) { + $inactiveDays = $parsedDays } } - elseif ($InputValue -eq $true) { - # Backwards compatibility: legacy single-input boolean means exclude disabled users - $excludeDisabled = $true - } $Lookup = (Get-Date).AddDays(-$inactiveDays).ToUniversalTime() Write-Host "Checking for inactive Entra devices since $Lookup (excluding disabled: $excludeDisabled)" @@ -36,12 +30,11 @@ function Get-CIPPAlertStaleEntraDevices { $Uri = if ($BaseFilter) { "https://graph.microsoft.com/beta/devices?`$filter=$BaseFilter" - } - else { - "https://graph.microsoft.com/beta/devices" + } else { + 'https://graph.microsoft.com/beta/devices' } - $GraphRequest = New-GraphGetRequest -uri $Uri -scope 'https://graph.microsoft.com/.default' -tenantid $TenantFilter + $GraphRequest = New-GraphGetRequest -uri $Uri -tenantid $TenantFilter $AlertData = foreach ($device in $GraphRequest) { @@ -54,40 +47,37 @@ function Get-CIPPAlertStaleEntraDevices { if (-not $lastActivity) { $Message = 'Device {0} has never been active' -f $device.displayName - } - else { + } else { $daysSinceLastActivity = [Math]::Round(((Get-Date) - [DateTime]$lastActivity).TotalDays) $Message = 'Device {0} has been inactive for {1} days. Last activity: {2}' -f $device.displayName, $daysSinceLastActivity, $lastActivity } - if ($device.TrustType -eq "Workplace") { $TrustType = "Entra registered" } - elseif ($device.TrustType -eq "AzureAd") { $TrustType = "Entra joined" } - elseif ($device.TrustType -eq "ServerAd") { $TrustType = "Entra hybrid joined" } + if ($device.TrustType -eq 'Workplace') { $TrustType = 'Entra registered' } + elseif ($device.TrustType -eq 'AzureAd') { $TrustType = 'Entra joined' } + elseif ($device.TrustType -eq 'ServerAd') { $TrustType = 'Entra hybrid joined' } [PSCustomObject]@{ - DeviceName = if ($device.displayName) { $device.displayName } else { 'N/A' } - Id = if ($device.id) { $device.id } else { 'N/A' } - deviceOwnership = if ($device.deviceOwnership) { $device.deviceOwnership } else { 'N/A' } - operatingSystem = if ($device.operatingSystem) { $device.operatingSystem } else { 'N/A' } - enrollmentType = if ($device.enrollmentType) { $device.enrollmentType } else { 'N/A' } - Enabled = if ($device.accountEnabled) { $device.accountEnabled } else { 'N/A' } - Managed = if ($device.isManaged) { $device.isManaged } else { 'N/A' } - Complaint = if ($device.isCompliant) { $device.isCompliant } else { 'N/A' } - JoinType = $TrustType - lastActivity = if ($lastActivity) { $lastActivity } else { 'N/A' } + DeviceName = if ($device.displayName) { $device.displayName } else { 'N/A' } + Id = if ($device.id) { $device.id } else { 'N/A' } + deviceOwnership = if ($device.deviceOwnership) { $device.deviceOwnership } else { 'N/A' } + operatingSystem = if ($device.operatingSystem) { $device.operatingSystem } else { 'N/A' } + enrollmentType = if ($device.enrollmentType) { $device.enrollmentType } else { 'N/A' } + Enabled = if ($device.accountEnabled) { $device.accountEnabled } else { 'N/A' } + Managed = if ($device.isManaged) { $device.isManaged } else { 'N/A' } + Complaint = if ($device.isCompliant) { $device.isCompliant } else { 'N/A' } + JoinType = $TrustType + lastActivity = if ($lastActivity) { $lastActivity } else { 'N/A' } DaysSinceLastActivity = if ($daysSinceLastActivity) { $daysSinceLastActivity } else { 'N/A' } - RegisteredDateTime = if ($device.createdDateTime) { $device.createdDateTime } else { 'N/A' } - Message = $Message - Tenant = $TenantFilter + RegisteredDateTime = if ($device.createdDateTime) { $device.createdDateTime } else { 'N/A' } + Message = $Message + Tenant = $TenantFilter } } } Write-AlertTrace -cmdletName $MyInvocation.MyCommand -tenantFilter $TenantFilter -data $AlertData - } - catch {} - } - catch { + } catch {} + } catch { Write-AlertMessage -tenant $($TenantFilter) -message "Failed to check inactive guest users for $($TenantFilter): $(Get-NormalizedError -message $_.Exception.message)" } } From d5b3ab0bb1337dc59558ea1943b4e66bee41adaf Mon Sep 17 00:00:00 2001 From: Integrated Solutions Date: Fri, 13 Feb 2026 14:46:37 +1000 Subject: [PATCH 387/503] when creating a Security group, mailNickname will populate with random string if empty --- Modules/CIPPCore/Public/New-CIPPGroup.ps1 | 32 ++++++++++++++++------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/Modules/CIPPCore/Public/New-CIPPGroup.ps1 b/Modules/CIPPCore/Public/New-CIPPGroup.ps1 index 1d4a199568e4..72dc499d5748 100644 --- a/Modules/CIPPCore/Public/New-CIPPGroup.ps1 +++ b/Modules/CIPPCore/Public/New-CIPPGroup.ps1 @@ -77,20 +77,32 @@ function New-CIPPGroup { $null } + # Determine if we should generate a random mailNickname: + # For Security/Generic groups WITHOUT a username filled in + $ShouldGenerateRandomMailNickname = ($NormalizedGroupType -in @('Generic', 'Security')) -and [string]::IsNullOrWhiteSpace($GroupObject.username) + # Extract local part of username if exists and remove special characters for mailNickname - if ($GroupObject.username -like '*@*') { - $MailNickname = ($GroupObject.username -split '@')[0] + if ($ShouldGenerateRandomMailNickname) { + # Generate a random alphanumeric mailNickname for security groups without a username + # Format: 8 hex characters + hyphen + 1 hex character (e.g., "450662e4-3") + $RandomPart1 = -join ((0..7) | ForEach-Object { (0..15 | ForEach-Object { '0123456789abcdef'[$_] } | Get-Random) }) + $RandomPart2 = (0..15 | ForEach-Object { '0123456789abcdef'[$_] } | Get-Random) + $MailNickname = "$RandomPart1-$RandomPart2" } else { - $MailNickname = $GroupObject.username - } + if ($GroupObject.username) { + $MailNickname = ($GroupObject.username -split '@')[0] + } else { + $MailNickname = $GroupObject.username + } - # Remove forbidden characters per Microsoft 365 mailNickname requirements: - # ASCII 0-127 only, excluding: @ () / [] ' ; : <> , SPACE and any non-ASCII - $MailNickname = $MailNickname -replace "[@()\[\]/'`;:<>,\s]|[^\x00-\x7F]", '' + # Remove forbidden characters per Microsoft 365 mailNickname requirements: + # ASCII 0-127 only, excluding: @ () / [] ' ; : <> , SPACE and any non-ASCII + $MailNickname = $MailNickname -replace "[@()\[\]/'`;:<>,\s]|[^\x00-\x7F]", '' - # Ensure max length of 64 characters - if ($MailNickname.Length -gt 64) { - $MailNickname = $MailNickname.Substring(0, 64) + # Ensure max length of 64 characters + if ($MailNickname.Length -gt 64) { + $MailNickname = $MailNickname.Substring(0, 64) + } } Write-LogMessage -API $APIName -tenant $TenantFilter -message "Creating group $($GroupObject.displayName) of type $NormalizedGroupType$(if ($NeedsEmail) { " with email $Email" })" -Sev Info From 334f5d3a2023c308360aa9f1d643291f4dfd273c Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Fri, 13 Feb 2026 09:52:52 +0100 Subject: [PATCH 388/503] POST Retry --- Modules/CIPPCore/Public/GraphHelper/New-GraphPOSTRequest.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/GraphHelper/New-GraphPOSTRequest.ps1 b/Modules/CIPPCore/Public/GraphHelper/New-GraphPOSTRequest.ps1 index ebc8a4efc2dd..074cc701e07e 100644 --- a/Modules/CIPPCore/Public/GraphHelper/New-GraphPOSTRequest.ps1 +++ b/Modules/CIPPCore/Public/GraphHelper/New-GraphPOSTRequest.ps1 @@ -17,7 +17,7 @@ function New-GraphPOSTRequest { $contentType, $IgnoreErrors = $false, $returnHeaders = $false, - $maxRetries = 1 + $maxRetries = 3 ) if ($NoAuthCheck -or (Get-AuthorisedRequest -Uri $uri -TenantID $tenantid)) { From 2647486eb4305f4d2e93b326d781a6d193fcd635 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Fri, 13 Feb 2026 12:18:05 +0100 Subject: [PATCH 389/503] first go at retry logic --- .../Public/GraphHelper/New-CIPPGraphRetry.ps1 | 94 +++++++++++++++++++ .../GraphHelper/New-GraphPOSTRequest.ps1 | 47 +++++++++- 2 files changed, 140 insertions(+), 1 deletion(-) create mode 100644 Modules/CIPPCore/Public/GraphHelper/New-CIPPGraphRetry.ps1 diff --git a/Modules/CIPPCore/Public/GraphHelper/New-CIPPGraphRetry.ps1 b/Modules/CIPPCore/Public/GraphHelper/New-CIPPGraphRetry.ps1 new file mode 100644 index 000000000000..4666d9b47d23 --- /dev/null +++ b/Modules/CIPPCore/Public/GraphHelper/New-CIPPGraphRetry.ps1 @@ -0,0 +1,94 @@ +function New-CIPPGraphRetry { + <# + .SYNOPSIS + Retries a failed Graph API request + .DESCRIPTION + This function is called by scheduled tasks when a Graph API request has exhausted retries. + It attempts to execute the request again with the original parameters. + .PARAMETER uri + The Graph API URI to call + .PARAMETER tenantid + The tenant ID for the request + .PARAMETER type + The HTTP method (POST, PATCH, DELETE, etc.) + .PARAMETER body + The request body + .PARAMETER scope + Optional OAuth scope + .PARAMETER AsApp + Whether to use application authentication + .PARAMETER NoAuthCheck + Whether to skip authorization check + .PARAMETER skipTokenCache + Whether to skip token cache + .PARAMETER AddedHeaders + Additional headers to include + .PARAMETER contentType + Content type for the request + .PARAMETER IgnoreErrors + Whether to ignore HTTP errors + .PARAMETER returnHeaders + Whether to return response headers + .PARAMETER maxRetries + Maximum number of retries + .FUNCTIONALITY + Internal + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$uri, + + [Parameter(Mandatory = $true)] + [string]$tenantid, + + [Parameter(Mandatory = $true)] + [string]$type, + + [string]$body, + [string]$scope, + [switch]$AsApp, + [switch]$NoAuthCheck, + [switch]$skipTokenCache, + [hashtable]$AddedHeaders, + [string]$contentType, + [bool]$IgnoreErrors, + [bool]$returnHeaders, + [int]$maxRetries = 3 + ) + + Write-Information "Retrying Graph API request for URI: $uri | Tenant: $tenantid" + + try { + # Build the parameter splat for New-GraphPOSTRequest + $GraphParams = @{ + uri = $uri + tenantid = $tenantid + type = $type + body = $body + maxRetries = $maxRetries + ScheduleRetry = $false # Do NOT schedule again if this retry fails + } + + # Add optional parameters if they were provided + if ($scope) { $GraphParams.scope = $scope } + if ($AsApp) { $GraphParams.AsApp = $AsApp } + if ($NoAuthCheck) { $GraphParams.NoAuthCheck = $NoAuthCheck } + if ($skipTokenCache) { $GraphParams.skipTokenCache = $skipTokenCache } + if ($AddedHeaders) { $GraphParams.AddedHeaders = $AddedHeaders } + if ($contentType) { $GraphParams.contentType = $contentType } + if ($IgnoreErrors) { $GraphParams.IgnoreErrors = $IgnoreErrors } + if ($returnHeaders) { $GraphParams.returnHeaders = $returnHeaders } + + # Execute the Graph request + $Result = New-GraphPOSTRequest @GraphParams + + Write-LogMessage -API 'GraphRetry' -message "Successfully retried Graph request for URI: $uri | Tenant: $tenantid" -Sev 'Info' -tenant $tenantid + + return $Result + } catch { + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + Write-LogMessage -API 'GraphRetry' -message "Failed to retry Graph request for URI: $uri | Tenant: $tenantid. Error: $ErrorMessage" -Sev 'Error' -tenant $tenantid + throw $ErrorMessage + } +} diff --git a/Modules/CIPPCore/Public/GraphHelper/New-GraphPOSTRequest.ps1 b/Modules/CIPPCore/Public/GraphHelper/New-GraphPOSTRequest.ps1 index 074cc701e07e..2bfdd9028f56 100644 --- a/Modules/CIPPCore/Public/GraphHelper/New-GraphPOSTRequest.ps1 +++ b/Modules/CIPPCore/Public/GraphHelper/New-GraphPOSTRequest.ps1 @@ -17,7 +17,8 @@ function New-GraphPOSTRequest { $contentType, $IgnoreErrors = $false, $returnHeaders = $false, - $maxRetries = 3 + $maxRetries = 3, + $ScheduleRetry = $false ) if ($NoAuthCheck -or (Get-AuthorisedRequest -Uri $uri -TenantID $tenantid)) { @@ -56,6 +57,50 @@ function New-GraphPOSTRequest { Start-Sleep -Seconds (2 * $x) } } while (($x -lt $maxRetries) -and ($success -eq $false)) + if (($maxRetries -and $success -eq $false) -and $ScheduleRetry -eq $true) { + #Create a scheduled task to retry the task later, when there is less pressure on the system, but only if ScheduledRetry is true. + try { + $TaskId = (New-Guid).Guid.ToString() + + # Prepare parameters for the retry + $RetryParameters = @{ + uri = $uri + tenantid = $tenantid + type = $type + body = $body + } + + # Add optional parameters if they were provided + if ($scope) { $RetryParameters.scope = $scope } + if ($AsApp) { $RetryParameters.AsApp = $AsApp } + if ($NoAuthCheck) { $RetryParameters.NoAuthCheck = $NoAuthCheck } + if ($skipTokenCache) { $RetryParameters.skipTokenCache = $skipTokenCache } + if ($AddedHeaders) { $RetryParameters.AddedHeaders = $AddedHeaders } + if ($contentType) { $RetryParameters.contentType = $contentType } + if ($IgnoreErrors) { $RetryParameters.IgnoreErrors = $IgnoreErrors } + if ($returnHeaders) { $RetryParameters.ReturnHeaders = $returnHeaders } + if ($maxRetries) { $RetryParameters.maxRetries = $maxRetries } + + # Create the scheduled task object + $TaskObject = [PSCustomObject]@{ + TenantFilter = $tenantid + Name = "Graph API Retry - $($uri -replace 'https://graph.microsoft.com/(beta|v1.0)/', '')" + Command = [PSCustomObject]@{ value = 'New-CIPPGraphRetry' } + Parameters = $RetryParameters + ScheduledTime = [int64](([datetime]::UtcNow.AddMinutes(15)) - (Get-Date '1/1/1970')).TotalSeconds + Recurrence = '0' + PostExecution = @{} + Reference = "GraphRetry-$TaskId" + } + + # Add the scheduled task (hidden = system task) + $null = Add-CIPPScheduledTask -Task $TaskObject -Hidden $true + + return @{Result = "Scheduled job with id $TaskId as Graph API was too busy to respond" } + } catch { + Write-Warning "Failed to schedule retry task: $($_.Exception.Message)" + } + } if ($success -eq $false) { throw $Message From cffa9bdc7828ea96342bf88acba1889d8b810f7f Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Fri, 13 Feb 2026 12:18:13 +0100 Subject: [PATCH 390/503] retry logic --- .../Public/GraphHelper/New-CIPPGraphRetry.ps1 | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Modules/CIPPCore/Public/GraphHelper/New-CIPPGraphRetry.ps1 b/Modules/CIPPCore/Public/GraphHelper/New-CIPPGraphRetry.ps1 index 4666d9b47d23..eae86faad9d5 100644 --- a/Modules/CIPPCore/Public/GraphHelper/New-CIPPGraphRetry.ps1 +++ b/Modules/CIPPCore/Public/GraphHelper/New-CIPPGraphRetry.ps1 @@ -38,13 +38,13 @@ function New-CIPPGraphRetry { param( [Parameter(Mandatory = $true)] [string]$uri, - + [Parameter(Mandatory = $true)] [string]$tenantid, - + [Parameter(Mandatory = $true)] [string]$type, - + [string]$body, [string]$scope, [switch]$AsApp, @@ -56,9 +56,9 @@ function New-CIPPGraphRetry { [bool]$returnHeaders, [int]$maxRetries = 3 ) - + Write-Information "Retrying Graph API request for URI: $uri | Tenant: $tenantid" - + try { # Build the parameter splat for New-GraphPOSTRequest $GraphParams = @{ @@ -69,7 +69,7 @@ function New-CIPPGraphRetry { maxRetries = $maxRetries ScheduleRetry = $false # Do NOT schedule again if this retry fails } - + # Add optional parameters if they were provided if ($scope) { $GraphParams.scope = $scope } if ($AsApp) { $GraphParams.AsApp = $AsApp } @@ -79,12 +79,12 @@ function New-CIPPGraphRetry { if ($contentType) { $GraphParams.contentType = $contentType } if ($IgnoreErrors) { $GraphParams.IgnoreErrors = $IgnoreErrors } if ($returnHeaders) { $GraphParams.returnHeaders = $returnHeaders } - + # Execute the Graph request $Result = New-GraphPOSTRequest @GraphParams - + Write-LogMessage -API 'GraphRetry' -message "Successfully retried Graph request for URI: $uri | Tenant: $tenantid" -Sev 'Info' -tenant $tenantid - + return $Result } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message From f164421e0e3c93b50017cd1fc6006bb0a9770cc4 Mon Sep 17 00:00:00 2001 From: rvdwegen Date: Fri, 13 Feb 2026 14:00:57 +0100 Subject: [PATCH 391/503] universal search --- .../Invoke-ExecUniversalSearchV2.ps1 | 7 +- Modules/CIPPCore/Public/Search-CIPPDbData.ps1 | 107 +++++++++++++++--- 2 files changed, 94 insertions(+), 20 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/Invoke-ExecUniversalSearchV2.ps1 b/Modules/CIPPCore/Public/Entrypoints/Invoke-ExecUniversalSearchV2.ps1 index 25cb9e964f4c..f110b9a4cab6 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Invoke-ExecUniversalSearchV2.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Invoke-ExecUniversalSearchV2.ps1 @@ -8,11 +8,14 @@ function Invoke-ExecUniversalSearchV2 { [CmdletBinding()] param($Request, $TriggerMetadata) - $TenantFilter = $Request.Query.tenantFilter $SearchTerms = $Request.Query.searchTerms $Limit = if ($Request.Query.limit) { [int]$Request.Query.limit } else { 10 } - $Results = Search-CIPPDbData -TenantFilter $TenantFilter -SearchTerms $SearchTerms -Types 'Users' -Limit $Limit + # Always search all tenants - do not pass TenantFilter parameter + $Results = Search-CIPPDbData -SearchTerms $SearchTerms -Types 'Users' -Limit $Limit -UserProperties 'id', 'userPrincipalName', 'displayName' + + + Write-Information "Results: $($Results | ConvertTo-Json -Depth 10)" return [HttpResponseContext]@{ StatusCode = [HttpStatusCode]::OK diff --git a/Modules/CIPPCore/Public/Search-CIPPDbData.ps1 b/Modules/CIPPCore/Public/Search-CIPPDbData.ps1 index a782821fa989..b5aca603896f 100644 --- a/Modules/CIPPCore/Public/Search-CIPPDbData.ps1 +++ b/Modules/CIPPCore/Public/Search-CIPPDbData.ps1 @@ -30,12 +30,24 @@ function Search-CIPPDbData { .PARAMETER Limit Maximum total number of results to return across all types. Default is unlimited (0) + .PARAMETER UserProperties + Array of property names to return for Users type. If not specified, all properties are returned. + Only applies when Types includes 'Users'. Valid properties include: id, accountEnabled, businessPhones, + city, createdDateTime, companyName, country, department, displayName, faxNumber, givenName, + isResourceAccount, jobTitle, mail, mailNickname, mobilePhone, onPremisesDistinguishedName, + officeLocation, onPremisesLastSyncDateTime, otherMails, postalCode, preferredDataLocation, + preferredLanguage, proxyAddresses, showInAddressList, state, streetAddress, surname, + usageLocation, userPrincipalName, userType, assignedLicenses, onPremisesSyncEnabled, signInActivity + .EXAMPLE Search-CIPPDbData -TenantFilter 'contoso.onmicrosoft.com' -SearchTerms 'john.doe' -Types 'Users', 'Groups' .EXAMPLE Search-CIPPDbData -SearchTerms 'admin' -Types 'Users' + .EXAMPLE + Search-CIPPDbData -SearchTerms 'admin' -Types 'Users' -UserProperties 'id', 'displayName', 'userPrincipalName', 'mail' + .EXAMPLE Search-CIPPDbData -SearchTerms 'SecurityDefaults', 'ConditionalAccess' -Types 'ConditionalAccessPolicies', 'Organization' @@ -69,7 +81,10 @@ function Search-CIPPDbData { [int]$MaxResultsPerType = 0, [Parameter(Mandatory = $false)] - [int]$Limit = 0 + [int]$Limit = 0, + + [Parameter(Mandatory = $false)] + [string[]]$UserProperties ) try { @@ -143,26 +158,82 @@ function Search-CIPPDbData { if ($IsMatch) { try { $Data = $Item.Data | ConvertFrom-Json - $ResultItem = [PSCustomObject]@{ - Tenant = $Item.PartitionKey - Type = $Type - RowKey = $Item.RowKey - Data = $Data - Timestamp = $Item.Timestamp - } - $Results.Add($ResultItem) - $TypeResultCount++ - # Check total limit first - if ($Limit -gt 0 -and $Results.Count -ge $Limit) { - Write-Verbose "Reached total limit of $Limit results" - break typeLoop + # For Users type with UserProperties, verify match is in target properties + $IsVerifiedMatch = $true + if ($Type -eq 'Users' -and $UserProperties -and $UserProperties.Count -gt 0) { + $IsVerifiedMatch = $false + + if ($MatchAll) { + # All search terms must match in target properties + $IsVerifiedMatch = $true + foreach ($SearchTerm in $SearchTerms) { + $SearchPattern = [regex]::Escape($SearchTerm) + $TermMatches = $false + foreach ($Property in $UserProperties) { + if ($Data.PSObject.Properties.Name -contains $Property -and + $null -ne $Data.$Property -and + $Data.$Property.ToString() -match $SearchPattern) { + $TermMatches = $true + break + } + } + if (-not $TermMatches) { + $IsVerifiedMatch = $false + break + } + } + } else { + # Any search term can match in target properties + foreach ($SearchTerm in $SearchTerms) { + $SearchPattern = [regex]::Escape($SearchTerm) + foreach ($Property in $UserProperties) { + if ($Data.PSObject.Properties.Name -contains $Property -and + $null -ne $Data.$Property -and + $Data.$Property.ToString() -match $SearchPattern) { + $IsVerifiedMatch = $true + break + } + } + if ($IsVerifiedMatch) { break } + } + } } - # Check max results per type - if ($MaxResultsPerType -gt 0 -and $TypeResultCount -ge $MaxResultsPerType) { - Write-Verbose "Reached max results per type ($MaxResultsPerType) for type '$Type'" - continue typeLoop + # Only add to results if verified (or not Users/UserProperties) + if ($IsVerifiedMatch) { + # Filter user properties if specified and type is Users + if ($Type -eq 'Users' -and $UserProperties -and $UserProperties.Count -gt 0) { + $FilteredData = [PSCustomObject]@{} + foreach ($Property in $UserProperties) { + if ($Data.PSObject.Properties.Name -contains $Property) { + $FilteredData | Add-Member -MemberType NoteProperty -Name $Property -Value $Data.$Property -Force + } + } + $Data = $FilteredData + } + + $ResultItem = [PSCustomObject]@{ + Tenant = $Item.PartitionKey + Type = $Type + RowKey = $Item.RowKey + Data = $Data + Timestamp = $Item.Timestamp + } + $Results.Add($ResultItem) + $TypeResultCount++ + + # Check total limit first (only for verified matches) + if ($Limit -gt 0 -and $Results.Count -ge $Limit) { + Write-Verbose "Reached total limit of $Limit results" + break typeLoop + } + + # Check max results per type (only for verified matches) + if ($MaxResultsPerType -gt 0 -and $TypeResultCount -ge $MaxResultsPerType) { + Write-Verbose "Reached max results per type ($MaxResultsPerType) for type '$Type'" + continue typeLoop + } } } catch { Write-Verbose "Failed to parse JSON for $($Item.RowKey): $($_.Exception.Message)" From cb04385a73a9c3246d2c0f200c330003e8ba91c2 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 13 Feb 2026 09:05:22 -0500 Subject: [PATCH 392/503] fix permission --- .../Administration/Contacts/Invoke-AddContactTemplates.ps1 | 2 +- .../Administration/Contacts/Invoke-EditContactTemplates.ps1 | 2 +- .../Administration/Contacts/Invoke-RemoveContactTemplates.ps1 | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-AddContactTemplates.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-AddContactTemplates.ps1 index af9b208dded7..afdc1d1b1d98 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-AddContactTemplates.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-AddContactTemplates.ps1 @@ -3,7 +3,7 @@ Function Invoke-AddContactTemplates { .FUNCTIONALITY Entrypoint,AnyTenant .ROLE - Exchange.ReadWrite + Exchange.Contact.ReadWrite #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-EditContactTemplates.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-EditContactTemplates.ps1 index 8edcaf294af2..87faed97031a 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-EditContactTemplates.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-EditContactTemplates.ps1 @@ -3,7 +3,7 @@ Function Invoke-EditContactTemplates { .FUNCTIONALITY Entrypoint,AnyTenant .ROLE - Exchange.ReadWrite + Exchange.Contact.ReadWrite #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-RemoveContactTemplates.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-RemoveContactTemplates.ps1 index de4e79bf2b70..43b43dff8917 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-RemoveContactTemplates.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-RemoveContactTemplates.ps1 @@ -3,7 +3,7 @@ function Invoke-RemoveContactTemplates { .FUNCTIONALITY Entrypoint,AnyTenant .ROLE - Exchange.ReadWrite + Exchange.Contact.ReadWrite #> [CmdletBinding()] param($Request, $TriggerMetadata) From 8cd1a81885ec2c463b304e398d85b37b1685fa64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Fri, 13 Feb 2026 14:05:15 +0100 Subject: [PATCH 393/503] chore: add .claude to .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 073300712d7e..93317828cfe5 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ yarn.lock # Cursor IDE .cursor/rules +.claude # Ignore all root PowerShell files except profile.ps1 /*.ps1 From 72260b35192e68681020a4495ab2d147f3e056a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Fri, 13 Feb 2026 16:47:09 +0100 Subject: [PATCH 394/503] fix: move AffectedDevices down in alert --- Modules/CIPPCore/Public/Alerts/Get-CIPPAlertVulnerabilities.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertVulnerabilities.ps1 b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertVulnerabilities.ps1 index f6cb1d37f0b8..7b6c374eecf7 100644 --- a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertVulnerabilities.ps1 +++ b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertVulnerabilities.ps1 @@ -81,7 +81,6 @@ function Get-CIPPAlertVulnerabilities { DaysOld = $DaysOld HoursOld = $HoursOld AffectedDeviceCount = $Group.Count - AffectedDevices = $AffectedDevices SoftwareName = $FirstVuln.softwareName SoftwareVendor = $FirstVuln.softwareVendor SoftwareVersion = $FirstVuln.softwareVersion @@ -90,6 +89,7 @@ function Get-CIPPAlertVulnerabilities { RecommendedUpdate = $FirstVuln.recommendedSecurityUpdate RecommendedUpdateId = $FirstVuln.recommendedSecurityUpdateId RecommendedUpdateUrl = $FirstVuln.recommendedSecurityUpdateUrl + AffectedDevices = $AffectedDevices Tenant = $TenantFilter } $AlertData.Add($VulnerabilityAlert) From 457f13d45862a98748129dca81a8b29417071974 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Fri, 13 Feb 2026 16:48:26 +0100 Subject: [PATCH 395/503] refactor: change property type of affectedDevices in Invoke-ListDefenderTVM Changed 'affectedDevices' to create an array of objects instead of joining device names with commas. This makes them look a lot nicer in the tables. --- .../HTTP Functions/Endpoint/MEM/Invoke-ListDefenderTVM.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListDefenderTVM.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListDefenderTVM.ps1 index 3d6a59a25d46..ad2065be4e42 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListDefenderTVM.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListDefenderTVM.ps1 @@ -25,8 +25,8 @@ function Invoke-ListDefenderTVM { # Add all properties from the group with appropriate processing foreach ($property in $allProperties) { if ($property -eq 'deviceName') { - # Special handling for deviceName - join with comma - $obj['affectedDevices'] = ($cve.group.$property -join ', ') + # Special handling for deviceName - create array of objects + $obj['affectedDevices'] = @($cve.group.$property | ForEach-Object { @{ $property = $_ } }) } else { # For all other properties, get unique values $obj[$property] = ($cve.group.$property | Sort-Object -Unique) | Select-Object -First 1 From eecda8baa6343fb4d3c5457abede2c37410b0f89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Fri, 13 Feb 2026 18:57:14 +0100 Subject: [PATCH 396/503] feat: add Invoke-ExecSyncDEP function for DEP sync --- .../Endpoint/MEM/Invoke-ExecSyncDEP.ps1 | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ExecSyncDEP.ps1 diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ExecSyncDEP.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ExecSyncDEP.ps1 new file mode 100644 index 000000000000..5672f6a984ee --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ExecSyncDEP.ps1 @@ -0,0 +1,50 @@ +function Invoke-ExecSyncDEP { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Endpoint.MEM.ReadWrite + .DESCRIPTION + Syncs devices from Apple Business Manager to Intune + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + + $TenantFilter = $Request.Body.tenantFilter + try { + $DepOnboardingSettings = @(New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/deviceManagement/depOnboardingSettings' -tenantid $TenantFilter) + + if ($null -eq $DepOnboardingSettings -or $DepOnboardingSettings.Count -eq 0) { + $Result = 'No Apple Business Manager connections found' + Write-LogMessage -Headers $Headers -API $APIName -tenant $TenantFilter -message $Result -Sev Info + } else { + $SyncCount = 0 + foreach ($DepSetting in $DepOnboardingSettings) { + if ($DepSetting.id) { + $null = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/deviceManagement/depOnboardingSettings/$($DepSetting.id)/syncWithAppleDeviceEnrollmentProgram" -tenantid $TenantFilter + $SyncCount++ + } + } + if ($SyncCount -eq 0) { + $Result = 'No Apple Business Manager connections found' + } else { + $Result = "Successfully started device sync for $SyncCount Apple Business Manager connection$(if ($SyncCount -gt 1) { 's' })" + } + Write-LogMessage -Headers $Headers -API $APIName -tenant $TenantFilter -message $Result -Sev Info + } + $StatusCode = [HttpStatusCode]::OK + } catch { + $ErrorMessage = Get-CippException -Exception $_ + $Result = 'Failed to start Apple Business Manager device sync' + Write-LogMessage -Headers $Headers -API $APIName -tenant $TenantFilter -message $Result -Sev Error -LogData $ErrorMessage + $StatusCode = [HttpStatusCode]::Forbidden + } + + return ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = @{ Results = $Result } + }) + +} From 5d5492e8a8c34311259f59ce2708d43398007162 Mon Sep 17 00:00:00 2001 From: Bobby <31723128+kris6673@users.noreply.github.com> Date: Fri, 13 Feb 2026 20:29:14 +0100 Subject: [PATCH 397/503] chore: remove some useless logging and an unneeded null check --- .../Endpoint/Applications/Invoke-ExecSyncVPP.ps1 | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-ExecSyncVPP.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-ExecSyncVPP.ps1 index 2dc3f4ed534a..d94b31cad906 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-ExecSyncVPP.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-ExecSyncVPP.ps1 @@ -9,9 +9,8 @@ function Invoke-ExecSyncVPP { param($Request, $TriggerMetadata) $APIName = $Request.Params.CIPPEndpoint $Headers = $Request.Headers - Write-LogMessage -Headers $Headers -API $APIName -message 'Accessed this API' -Sev Debug - $TenantFilter = $Request.Body.tenantFilter ?? $Request.Query.tenantFilter + $TenantFilter = $Request.Body.tenantFilter try { # Get all VPP tokens and sync them $VppTokens = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/deviceAppManagement/vppTokens' -tenantid $TenantFilter | Where-Object { $_.state -eq 'valid' } From ac2c0e150d1781a3cc429e5d8130c709a7b99867 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 13 Feb 2026 14:30:24 -0500 Subject: [PATCH 398/503] Import: dedupe templates and return status Enhance Import-CommunityTemplate to detect duplicate templates (GroupTemplate, CATemplate, IntuneTemplate), preserve existing GUID/RowKey when updating, and skip imports when SHA matches (unless -Force). Introduce a $StatusMessage, log informative messages for create/update/skip cases, preserve Package from duplicates, and return the status string. Update callers (Invoke-ExecCommunityRepo and New-CIPPTemplateRun) to capture and use the import result (write/log it and include it in results), and pass Source where needed. These changes add feedback and prevent creating duplicate template records. --- .../Tools/GitHub/Invoke-ExecCommunityRepo.ps1 | 4 +- .../CIPPCore/Public/New-CIPPTemplateRun.ps1 | 7 +- .../Public/Tools/Import-CommunityTemplate.ps1 | 123 ++++++++++++++++-- 3 files changed, 122 insertions(+), 12 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tools/GitHub/Invoke-ExecCommunityRepo.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tools/GitHub/Invoke-ExecCommunityRepo.ps1 index ffe14702ce63..fd7feed3c243 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tools/GitHub/Invoke-ExecCommunityRepo.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tools/GitHub/Invoke-ExecCommunityRepo.ps1 @@ -178,10 +178,10 @@ function Invoke-ExecCommunityRepo { (Get-GitHubFileContents -FullName $FullName -Branch $Branch -Path $Location.path).content | ConvertFrom-Json } } - Import-CommunityTemplate -Template $Content -SHA $Template.sha -MigrationTable $MigrationTable -LocationData $LocationData + $ImportResult = Import-CommunityTemplate -Template $Content -SHA $Template.sha -MigrationTable $MigrationTable -LocationData $LocationData -Source $FullName $Results = @{ - resultText = 'Template imported' + resultText = $ImportResult ?? 'Template imported' state = 'success' } } catch { diff --git a/Modules/CIPPCore/Public/New-CIPPTemplateRun.ps1 b/Modules/CIPPCore/Public/New-CIPPTemplateRun.ps1 index 76ff020eb9ef..530a45c29fe5 100644 --- a/Modules/CIPPCore/Public/New-CIPPTemplateRun.ps1 +++ b/Modules/CIPPCore/Public/New-CIPPTemplateRun.ps1 @@ -64,8 +64,11 @@ function New-CIPPTemplateRun { if (!$ExistingTemplate -or $UpdateNeeded) { $Template = (Get-GitHubFileContents -FullName $TemplateSettings.templateRepo.value -Branch $TemplateSettings.templateRepoBranch.value -Path $File.path).content | ConvertFrom-Json - Import-CommunityTemplate -Template $Template -SHA $File.sha -MigrationTable $MigrationTable -LocationData $LocationData -Source $TemplateSettings.templateRepo.value - if ($UpdateNeeded) { + $ImportResult = Import-CommunityTemplate -Template $Template -SHA $File.sha -MigrationTable $MigrationTable -LocationData $LocationData -Source $TemplateSettings.templateRepo.value + if ($ImportResult) { + Write-Information $ImportResult + $ImportResult + } elseif ($UpdateNeeded) { Write-Information "Template $($File.name) needs to be updated as the SHA is different" "Template $($File.name) updated" } else { diff --git a/Modules/CIPPCore/Public/Tools/Import-CommunityTemplate.ps1 b/Modules/CIPPCore/Public/Tools/Import-CommunityTemplate.ps1 index d099f2a280f8..603df66a4c96 100644 --- a/Modules/CIPPCore/Public/Tools/Import-CommunityTemplate.ps1 +++ b/Modules/CIPPCore/Public/Tools/Import-CommunityTemplate.ps1 @@ -14,6 +14,7 @@ function Import-CommunityTemplate { ) $Table = Get-CippTable -TableName 'templates' + $StatusMessage = $null try { if ($Template.RowKey) { @@ -67,11 +68,20 @@ function Import-CommunityTemplate { $Template | Add-Member -MemberType NoteProperty -Name SHA -Value $SHA -Force $Template | Add-Member -MemberType NoteProperty -Name Source -Value $Source -Force Add-CIPPAzDataTableEntity @Table -Entity $Template -Force + + if ($Existing -and $Existing.SHA -ne $SHA) { + $StatusMessage = "Updated template '$($Template.RowKey)' from source '$Source' (SHA changed)." + } elseif ($Existing) { + $StatusMessage = "Template '$($Template.RowKey)' from source '$Source' is already up to date." + } else { + $StatusMessage = "Created template '$($Template.RowKey)' from source '$Source'." + } } else { $id = [guid]::NewGuid().ToString() if ($Template.mailNickname) { $Type = 'Group' } if ($Template.'@odata.type' -like '*conditionalAccessPolicy*') { $Type = 'ConditionalAccessPolicy' } Write-Host "The type is $Type" + switch -Wildcard ($Type) { '*Group*' { $RawJsonObj = [PSCustomObject]@{ @@ -82,12 +92,42 @@ function Import-CommunityTemplate { GUID = $id groupType = 'generic' } | ConvertTo-Json -Depth 100 + + # Check for duplicate template + $DuplicateFilter = "PartitionKey eq 'GroupTemplate'" + $ExistingTemplates = Get-CIPPAzDataTableEntity @Table -Filter $DuplicateFilter -ErrorAction SilentlyContinue + $Duplicate = $ExistingTemplates | Where-Object { + try { + $ExistingJSON = if (Test-Json $_.JSON -ErrorAction SilentlyContinue) { + $_.JSON | ConvertFrom-Json + } else { + $_.JSON + } + $ExistingJSON.Displayname -eq $Template.displayName -and $_.Source -eq $Source + } catch { + $false + } + } | Select-Object -First 1 + + if ($Duplicate -and $Duplicate.SHA -eq $SHA -and -not $Force) { + $StatusMessage = "Group template '$($Template.displayName)' from source '$Source' is already up to date. Skipping import." + Write-Information $StatusMessage + break + } + + if ($Duplicate) { + $StatusMessage = "Updating Group template '$($Template.displayName)' from source '$Source' (SHA changed)." + Write-Information $StatusMessage + } else { + $StatusMessage = "Created Group template '$($Template.displayName)' from source '$Source'." + } + $entity = @{ JSON = "$RawJsonObj" PartitionKey = 'GroupTemplate' SHA = $SHA - GUID = $id - RowKey = $id + GUID = if ($Duplicate) { $Duplicate.GUID } else { $id } + RowKey = if ($Duplicate) { $Duplicate.RowKey } else { $id } Source = $Source } Add-CIPPAzDataTableEntity @Table -Entity $entity -Force @@ -125,12 +165,41 @@ function Import-CommunityTemplate { } } + # Check for duplicate template + $DuplicateFilter = "PartitionKey eq 'CATemplate'" + $ExistingTemplates = Get-CIPPAzDataTableEntity @Table -Filter $DuplicateFilter -ErrorAction SilentlyContinue + $Duplicate = $ExistingTemplates | Where-Object { + try { + $ExistingJSON = if (Test-Json $_.JSON -ErrorAction SilentlyContinue) { + $_.JSON | ConvertFrom-Json + } else { + $_.JSON + } + $ExistingJSON.displayName -eq $Template.displayName -and $_.Source -eq $Source + } catch { + $false + } + } | Select-Object -First 1 + + if ($Duplicate -and $Duplicate.SHA -eq $SHA -and -not $Force) { + $StatusMessage = "Conditional Access template '$($Template.displayName)' from source '$Source' is already up to date. Skipping import." + Write-Information $StatusMessage + break + } + + if ($Duplicate) { + $StatusMessage = "Updating Conditional Access template '$($Template.displayName)' from source '$Source' (SHA changed)." + Write-Information $StatusMessage + } else { + $StatusMessage = "Created Conditional Access template '$($Template.displayName)' from source '$Source'." + } + $entity = @{ JSON = "$RawJson" PartitionKey = 'CATemplate' SHA = $SHA - GUID = $id - RowKey = $id + GUID = if ($Duplicate) { $Duplicate.GUID } else { $id } + RowKey = if ($Duplicate) { $Duplicate.RowKey } else { $id } Source = $Source } Write-Information "Final entity: $($entity | ConvertTo-Json -Depth 10)" @@ -153,20 +222,51 @@ function Import-CommunityTemplate { $RawJson = $RawJson | ConvertTo-Json -Depth 100 -Compress #create a new template + $DisplayName = $Template.displayName ?? $template.Name + $RawJsonObj = [PSCustomObject]@{ - Displayname = $Template.displayName ?? $template.Name + Displayname = $DisplayName Description = $Template.Description RAWJson = $RawJson Type = $URLName GUID = $id } | ConvertTo-Json -Depth 100 -Compress + # Check for duplicate template + $DuplicateFilter = "PartitionKey eq 'IntuneTemplate'" + $ExistingTemplates = Get-CIPPAzDataTableEntity @Table -Filter $DuplicateFilter -ErrorAction SilentlyContinue + $Duplicate = $ExistingTemplates | Where-Object { + try { + $ExistingJSON = if (Test-Json $_.JSON -ErrorAction SilentlyContinue) { + $_.JSON | ConvertFrom-Json + } else { + $_.JSON + } + $ExistingJSON.Displayname -eq $DisplayName -and $_.Source -eq $Source + } catch { + $false + } + } | Select-Object -First 1 + + if ($Duplicate -and $Duplicate.SHA -eq $SHA -and -not $Force) { + $StatusMessage = "Intune template '$DisplayName' from source '$Source' is already up to date. Skipping import." + Write-Information $StatusMessage + return $StatusMessage + } + + if ($Duplicate) { + $StatusMessage = "Updating Intune template '$DisplayName' from source '$Source' (SHA changed)." + Write-Information $StatusMessage + } else { + $StatusMessage = "Created Intune template '$DisplayName' from source '$Source'." + } + $entity = @{ JSON = "$RawJsonObj" PartitionKey = 'IntuneTemplate' SHA = $SHA - GUID = $id - RowKey = $id + GUID = if ($Duplicate) { $Duplicate.GUID } else { $id } + RowKey = if ($Duplicate) { $Duplicate.RowKey } else { $id } Source = $Source } @@ -174,13 +274,20 @@ function Import-CommunityTemplate { $entity.Package = $Existing.Package } + if ($Duplicate -and $Duplicate.Package) { + $entity.Package = $Duplicate.Package + } + Add-CIPPAzDataTableEntity @Table -Entity $entity -Force } } } } catch { - Write-Warning "Community template import failed. Error: $($_.Exception.Message)" + $StatusMessage = "Community template import failed. Error: $($_.Exception.Message)" + Write-Warning $StatusMessage Write-Information $_.InvocationInfo.PositionMessage } + + return $StatusMessage } From 454154c43aa04ea475404ecfdeaa5b4e7e7633fa Mon Sep 17 00:00:00 2001 From: Logan Cook <2997336+MWG-Logan@users.noreply.github.com> Date: Fri, 13 Feb 2026 16:47:26 -0500 Subject: [PATCH 399/503] fix(reusable-settings): better normalize reusable setting metadata --- .../Get-CIPPReusableSettingsFromPolicy.ps1 | 64 +++++++++- .../Remove-CIPPReusableSettingMetadata.ps1 | 118 +++++++++++++++--- ...AddIntuneReusableSettingTemplate.Tests.ps1 | 34 ++++- 3 files changed, 194 insertions(+), 22 deletions(-) diff --git a/Modules/CIPPCore/Public/Get-CIPPReusableSettingsFromPolicy.ps1 b/Modules/CIPPCore/Public/Get-CIPPReusableSettingsFromPolicy.ps1 index 8e331a484f00..f7a8e8c32ce2 100644 --- a/Modules/CIPPCore/Public/Get-CIPPReusableSettingsFromPolicy.ps1 +++ b/Modules/CIPPCore/Public/Get-CIPPReusableSettingsFromPolicy.ps1 @@ -98,12 +98,26 @@ function Get-CIPPReusableSettingsFromPolicy { foreach ($settingId in $referencedReusableIds) { try { $setting = New-GraphGETRequest -Uri "https://graph.microsoft.com/beta/deviceManagement/reusablePolicySettings/$settingId" -tenantid $Tenant - if (-not $setting) { + if ($null -eq $setting) { Write-LogMessage -headers $Headers -API $APIName -message "Reusable setting $settingId not returned from Graph" -Sev 'Warn' continue } - $settingDisplayName = $setting.displayName + # Normalize Graph SDK objects into PSCustomObject to ensure cleanup works consistently + $settingNormalized = [ordered]@{} + foreach ($prop in $setting.PSObject.Properties) { + $settingNormalized[$prop.Name] = $prop.Value + } + + if ($settingNormalized.Count -eq 0) { + foreach ($prop in $setting.GetType().GetProperties()) { + $settingNormalized[$prop.Name] = $prop.GetValue($setting) + } + } + + $settingNormalized = $settingNormalized | ConvertTo-Json -Depth 100 -Compress | ConvertFrom-Json -Depth 100 + + $settingDisplayName = $setting.displayName ?? $settingNormalized.displayName if (-not $settingDisplayName) { Write-LogMessage -headers $Headers -API $APIName -message "Reusable setting $settingId missing displayName" -Sev 'Warn' continue @@ -112,9 +126,10 @@ function Get-CIPPReusableSettingsFromPolicy { $matchedTemplate = $existingReusableByName[$settingDisplayName] $templateGuid = $matchedTemplate.RowKey + $cleanSetting = Remove-CIPPReusableSettingMetadata -InputObject $settingNormalized + $sanitizedJson = $cleanSetting | ConvertTo-Json -Depth 100 -Compress + if (-not $templateGuid) { - $cleanSetting = Remove-CIPPReusableSettingMetadata -InputObject $setting - $sanitizedJson = $cleanSetting | ConvertTo-Json -Depth 100 -Compress $templateGuid = (New-Guid).Guid $reusableEntity = [pscustomobject]@{ DisplayName = $settingDisplayName @@ -139,7 +154,46 @@ function Get-CIPPReusableSettingsFromPolicy { Write-LogMessage -headers $Headers -API $APIName -message "Created reusable setting template $templateGuid for '$settingDisplayName'" -Sev 'Info' } else { - Write-LogMessage -headers $Headers -API $APIName -message "Reusing existing reusable setting template $templateGuid for '$settingDisplayName'" -Sev 'Info' + $existingRawJson = $matchedTemplate.RawJSON + if (-not $existingRawJson) { + $existingParsed = $matchedTemplate.JSON | ConvertFrom-Json -ErrorAction SilentlyContinue + $existingRawJson = $existingParsed.RawJSON + } + + $requiresNormalization = $false + if ($existingRawJson -and $existingRawJson -match '"children"\s*:\s*null') { + $requiresNormalization = $true + } + + if ($requiresNormalization) { + $reusableEntity = [pscustomobject]@{ + DisplayName = $settingDisplayName + Description = $setting.description + RawJSON = $sanitizedJson + GUID = $templateGuid + } | ConvertTo-Json -Depth 100 -Compress + + Add-CIPPAzDataTableEntity @templatesTableForAdd -Entity @{ + JSON = "$reusableEntity" + RowKey = "$templateGuid" + PartitionKey = 'IntuneReusableSettingTemplate' + GUID = "$templateGuid" + DisplayName = $settingDisplayName + Description = $setting.description + RawJSON = "$sanitizedJson" + } + + $existingReusableByName[$settingDisplayName] = [pscustomobject]@{ + RowKey = $templateGuid + DisplayName = $settingDisplayName + JSON = $reusableEntity + RawJSON = $sanitizedJson + } + + Write-LogMessage -headers $Headers -API $APIName -message "Normalized reusable setting template $templateGuid for '$settingDisplayName'" -Sev 'Info' + } else { + Write-LogMessage -headers $Headers -API $APIName -message "Reusing existing reusable setting template $templateGuid for '$settingDisplayName'" -Sev 'Info' + } } $result.ReusableSettings.Add([pscustomobject]@{ diff --git a/Modules/CIPPCore/Public/Remove-CIPPReusableSettingMetadata.ps1 b/Modules/CIPPCore/Public/Remove-CIPPReusableSettingMetadata.ps1 index 3a6fcba20866..07ba8b6da4ad 100644 --- a/Modules/CIPPCore/Public/Remove-CIPPReusableSettingMetadata.ps1 +++ b/Modules/CIPPCore/Public/Remove-CIPPReusableSettingMetadata.ps1 @@ -1,23 +1,113 @@ function Remove-CIPPReusableSettingMetadata { param($InputObject) - if ($null -eq $InputObject) { return $null } + $metadataFields = @( + 'id', + 'createdDateTime', + 'lastModifiedDateTime', + 'version', + '@odata.context', + '@odata.etag', + 'referencingConfigurationPolicyCount', + 'settingInstanceTemplateReference', + 'settingValueTemplateReference', + 'auditRuleInformation' + ) - if ($InputObject -is [System.Collections.IEnumerable] -and $InputObject -isnot [string]) { - $cleanArray = [System.Collections.Generic.List[object]]::new() - foreach ($item in $InputObject) { $cleanArray.Add((Remove-CIPPReusableSettingMetadata -InputObject $item)) } - return $cleanArray - } + function Normalize-Object { + param($Value) + + if ($null -eq $Value) { return $null } - if ($InputObject -is [psobject]) { - $output = [ordered]@{} - foreach ($prop in $InputObject.PSObject.Properties) { - if ($null -eq $prop.Value) { continue } - if ($prop.Name -in @('id','createdDateTime','lastModifiedDateTime','version','@odata.context','@odata.etag','referencingConfigurationPolicyCount','settingInstanceTemplateReference','settingValueTemplateReference','auditRuleInformation')) { continue } - $output[$prop.Name] = Remove-CIPPReusableSettingMetadata -InputObject $prop.Value + function Test-IsCollection { + param($Candidate) + return ( + $Candidate -is [System.Collections.IEnumerable] -and + $Candidate -isnot [string] -and + ( + $Candidate -is [System.Array] -or + $Candidate -is [System.Collections.IList] -or + $Candidate -is [System.Collections.ICollection] + ) + ) } - return [pscustomobject]$output + + function Normalize-Entries { + param($Entries) + + $output = [ordered]@{} + foreach ($entry in $Entries) { + $name = $entry.Name + $item = $entry.Value + + if ($name -ieq 'children') { + if ($null -eq $item) { + $output[$name] = @() + } elseif (Test-IsCollection -Candidate $item) { + $output[$name] = Normalize-Object -Value $item + } else { + $output[$name] = @(Normalize-Object -Value $item) + } + continue + } + + if ($name -ieq 'groupSettingCollectionValue') { + if ($null -eq $item) { + $output[$name] = @() + continue + } + + if (Test-IsCollection -Candidate $item) { + $output[$name] = Normalize-Object -Value $item + } else { + $output[$name] = @(Normalize-Object -Value $item) + } + continue + } + + if ($null -eq $item) { continue } + if ($name -in $metadataFields) { continue } + $output[$name] = Normalize-Object -Value $item + } + + if ($output.Contains('children') -and -not (Test-IsCollection -Candidate $output['children'])) { + $output['children'] = @($output['children']) + } + + if ( + $output.Contains('groupSettingCollectionValue') -and + -not (Test-IsCollection -Candidate $output['groupSettingCollectionValue']) + ) { + $output['groupSettingCollectionValue'] = @($output['groupSettingCollectionValue']) + } + + return [pscustomobject]$output + } + + if ($Value -is [System.Collections.IDictionary]) { + $entries = foreach ($key in $Value.Keys) { + [pscustomobject]@{ Name = $key; Value = $Value[$key] } + } + return Normalize-Entries -Entries $entries + } + + if ($Value -is [System.Collections.IEnumerable] -and $Value -isnot [string]) { + $cleanArray = [System.Collections.Generic.List[object]]::new() + foreach ($entry in $Value) { + $cleanArray.Add((Normalize-Object -Value $entry)) + } + return $cleanArray + } + + if ($Value -is [psobject]) { + $entries = foreach ($prop in $Value.PSObject.Properties) { + [pscustomobject]@{ Name = $prop.Name; Value = $prop.Value } + } + return Normalize-Entries -Entries $entries + } + + return $Value } - return $InputObject + return Normalize-Object -Value $InputObject } diff --git a/Tests/Endpoint/Invoke-AddIntuneReusableSettingTemplate.Tests.ps1 b/Tests/Endpoint/Invoke-AddIntuneReusableSettingTemplate.Tests.ps1 index 14a79de3fca1..d29a4530611f 100644 --- a/Tests/Endpoint/Invoke-AddIntuneReusableSettingTemplate.Tests.ps1 +++ b/Tests/Endpoint/Invoke-AddIntuneReusableSettingTemplate.Tests.ps1 @@ -4,6 +4,7 @@ BeforeAll { $RepoRoot = Split-Path -Parent (Split-Path -Parent (Split-Path -Parent $PSCommandPath)) $FunctionPath = Join-Path $RepoRoot 'Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-AddIntuneReusableSettingTemplate.ps1' + $MetadataPath = Join-Path $RepoRoot 'Modules/CIPPCore/Public/Remove-CIPPReusableSettingMetadata.ps1' class HttpResponseContext { [int]$StatusCode @@ -19,9 +20,7 @@ BeforeAll { [pscustomobject]@{ NormalizedError = $Exception } } - # Pass-through for metadata cleanup used in the function - function Remove-CIPPReusableSettingMetadata { param($InputObject) $InputObject } - + . $MetadataPath . $FunctionPath } @@ -72,4 +71,33 @@ Describe 'Invoke-AddIntuneReusableSettingTemplate' { $response.StatusCode | Should -Be ([System.Net.HttpStatusCode]::InternalServerError) $response.Body.Results | Should -Match 'RawJSON is not valid JSON' } + + It 'normalizes children null values in reusable setting templates' { + $request = [pscustomobject]@{ + Params = @{ CIPPEndpoint = 'AddIntuneReusableSettingTemplate' } + Headers = @{ Authorization = 'Bearer token' } + Body = [pscustomobject]@{ + displayName = 'Template With Children' + rawJSON = '{"displayName":"Template With Children","settingInstance":{"groupSettingCollectionValue":[{"children":[{"choiceSettingValue":{"children":null}}]}]}}' + GUID = 'template-children' + } + } + + $parsed = $request.Body.rawJSON | ConvertFrom-Json -Depth 100 + $clean = Remove-CIPPReusableSettingMetadata -InputObject $parsed + $clean.settingInstance.PSObject.Properties.Name | Should -Contain 'groupSettingCollectionValue' + $clean.settingInstance.groupSettingCollectionValue | Should -Not -BeNullOrEmpty + $clean.settingInstance.groupSettingCollectionValue.GetType().FullName | Should -Be 'System.Object[]' + ($clean.settingInstance.groupSettingCollectionValue -is [System.Collections.IEnumerable]) | Should -BeTrue + ($clean.settingInstance.groupSettingCollectionValue | Measure-Object).Count | Should -Be 1 + ($clean.settingInstance.groupSettingCollectionValue[0].children -is [System.Collections.IEnumerable]) | Should -BeTrue + ($clean.settingInstance.groupSettingCollectionValue[0].children | Measure-Object).Count | Should -Be 1 + ($clean.settingInstance.groupSettingCollectionValue[0].children[0].choiceSettingValue.children -is [System.Collections.IEnumerable]) | Should -BeTrue + + $response = Invoke-AddIntuneReusableSettingTemplate -Request $request -TriggerMetadata $null + + $response.StatusCode | Should -Be ([System.Net.HttpStatusCode]::OK) + $lastEntity.RawJSON | Should -Not -Match '"children":null' + $lastEntity.RawJSON | Should -Match '"children":\[\]' + } } From 4cad64883880824b19b0694fd1b2f27b7891d781 Mon Sep 17 00:00:00 2001 From: Bobby <31723128+kris6673@users.noreply.github.com> Date: Sat, 14 Feb 2026 00:34:45 +0100 Subject: [PATCH 400/503] feat: add assignment filter handling in Invoke-AddPolicy - Introduced logic to handle AssignmentFilterName and AssignmentFilterType. - Updated parameters for Set-CIPPIntunePolicy to include assignment filter details if provided. --- .../Endpoint/MEM/Invoke-AddPolicy.ps1 | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-AddPolicy.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-AddPolicy.ps1 index 92dfb409d341..1c633f205e49 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-AddPolicy.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-AddPolicy.ps1 @@ -17,6 +17,15 @@ function Invoke-AddPolicy { $description = $Request.Body.Description $AssignTo = if ($Request.Body.AssignTo -ne 'on') { $Request.Body.AssignTo } $ExcludeGroup = $Request.Body.excludeGroup + $AssignmentFilterSelection = $Request.Body.AssignmentFilterName ?? $Request.Body.assignmentFilter + $AssignmentFilterType = $Request.Body.AssignmentFilterType ?? $Request.Body.assignmentFilterType + $AssignmentFilterName = switch ($AssignmentFilterSelection) { + { $_ -is [string] } { $_; break } + { $_ -and $_.PSObject.Properties['value'] } { $_.value; break } + { $_ -and $_.PSObject.Properties['displayName'] } { $_.displayName; break } + { $_ -and $_.PSObject.Properties['label'] } { $_.label; break } + default { $null } + } $Request.Body.customGroup ? ($AssignTo = $Request.Body.customGroup) : $null $RawJSON = $Request.Body.RAWJson @@ -70,6 +79,12 @@ function Invoke-AddPolicy { Headers = $Headers APIName = $APIName } + + if (-not [string]::IsNullOrWhiteSpace($AssignmentFilterName)) { + $params.AssignmentFilterName = $AssignmentFilterName + $params.AssignmentFilterType = [string]::IsNullOrWhiteSpace($AssignmentFilterType) ? 'include' : $AssignmentFilterType + } + Set-CIPPIntunePolicy @params } catch { "$($_.Exception.Message)" From 0f5efdc4c250f639afe1542405ab585b71373590 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Sat, 14 Feb 2026 17:48:37 +0800 Subject: [PATCH 401/503] Update Invoke-AddUser.ps1 --- .../Identity/Administration/Users/Invoke-AddUser.ps1 | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-AddUser.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-AddUser.ps1 index 5fce41b7da5b..f7637cfa04d8 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-AddUser.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-AddUser.ps1 @@ -75,8 +75,10 @@ function Invoke-AddUser { 'User' = $CreationResults.User } } catch { + $ErrorMessage = $_.TargetObject.Results -join ' ' + $ErrorMessage = [string]::IsNullOrWhiteSpace($ErrorMessage) ? $_.Exception.Message : $ErrorMessage $body = [pscustomobject] @{ - 'Results' = @("$($_.Exception.Message)") + 'Results' = @("$ErrorMessage") } $StatusCode = [HttpStatusCode]::InternalServerError } From 9542e72d1c980adc87c680411846c17d8bdd8bc7 Mon Sep 17 00:00:00 2001 From: Bobby <31723128+kris6673@users.noreply.github.com> Date: Sat, 14 Feb 2026 11:08:35 +0100 Subject: [PATCH 402/503] fix: accidental pipeline output in New-CIPPCAPolicy when creating named locations When creating a new named location, the uncaptured Select-Object on line 198 leaked an id-less object into $LocationLookupTable. This caused duplicate lookup matches where $lookup.id resolved to @($null, "guid"), producing invalid nested-array JSON in excludeLocations/includeLocations. Fixes https://github.com/KelvinTegelaar/CIPP/issues/5368 --- Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 b/Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 index 1f908ff4e0f2..8a713165f482 100644 --- a/Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 +++ b/Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 @@ -195,18 +195,19 @@ function New-CIPPCAPolicy { } } else { if ($location.countriesAndRegions) { $location.countriesAndRegions = @($location.countriesAndRegions) } - $location | Select-Object * -ExcludeProperty id - Remove-ODataProperties -Object $location - $Body = ConvertTo-Json -InputObject $Location + $LocationBody = $location | Select-Object * -ExcludeProperty id + Remove-ODataProperties -Object $LocationBody + $Body = ConvertTo-Json -InputObject $LocationBody $GraphRequest = New-GraphPOSTRequest -uri 'https://graph.microsoft.com/beta/identity/conditionalAccess/namedLocations' -body $body -Type POST -tenantid $tenantfilter -asApp $true $retryCount = 0 + $MaxRetryCount = 10 do { Write-Host "Checking for location $($GraphRequest.id) attempt $retryCount. $TenantFilter" $LocationRequest = New-GraphGETRequest -uri 'https://graph.microsoft.com/beta/identity/conditionalAccess/namedLocations' -tenantid $tenantfilter -asApp $true | Where-Object -Property id -EQ $GraphRequest.id Write-Host "LocationRequest: $($LocationRequest.id)" Start-Sleep -Seconds 2 $retryCount++ - } while ((!$LocationRequest -or !$LocationRequest.id) -and ($retryCount -lt 5)) + } while ((!$LocationRequest -or !$LocationRequest.id) -and ($retryCount -lt $MaxRetryCount)) Write-LogMessage -Tenant $TenantFilter -Headers $Headers -API $APINAME -message "Created new Named Location: $($location.displayName)" -Sev 'Info' [pscustomobject]@{ id = $GraphRequest.id From 2453809ccc23e9e284cd4215e7a8a07ba94af7e8 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Sat, 14 Feb 2026 12:23:26 +0100 Subject: [PATCH 403/503] fixes #5373 --- Modules/CIPPCore/Public/New-CIPPBackup.ps1 | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Modules/CIPPCore/Public/New-CIPPBackup.ps1 b/Modules/CIPPCore/Public/New-CIPPBackup.ps1 index b3f8409ade3e..374dfaf4ee8e 100644 --- a/Modules/CIPPCore/Public/New-CIPPBackup.ps1 +++ b/Modules/CIPPCore/Public/New-CIPPBackup.ps1 @@ -54,6 +54,8 @@ function New-CIPPBackup { 'WebhookRules' 'ScheduledTasks' 'TenantProperties' + 'TenantGroups' + 'TenantGroupMembers' ) $CSVfile = foreach ($CSVTable in $BackupTables) { $Table = Get-CippTable -tablename $CSVTable From a35798a83ccf98537ce54e79f1c812b87786da40 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Sat, 14 Feb 2026 13:24:27 +0100 Subject: [PATCH 404/503] updated domain scores --- Modules/CIPPCore/Public/Alerts/Get-CIPPAlertLowDomainScore.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertLowDomainScore.ps1 b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertLowDomainScore.ps1 index e5284435bcdc..674648585221 100644 --- a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertLowDomainScore.ps1 +++ b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertLowDomainScore.ps1 @@ -13,7 +13,7 @@ function Get-CIPPAlertLowDomainScore { ) $DomainData = Get-CIPPDomainAnalyser -TenantFilter $TenantFilter - $LowScoreDomains = $DomainData | Where-Object { $_.ScorePercentage -lt $InputValue -and $_.ScorePercentage -ne '' } | ForEach-Object { + $LowScoreDomains = $DomainData | Where-Object { $_.ScorePercentage -lt $InputValue -and $_.ScorePercentage -ne '' -and $_.Domain -notlike '*.onmicrosoft.com' -and $_.Domain -notlike '*.mail.onmicrosoft.com' } | ForEach-Object { [PSCustomObject]@{ Message = "$($_.Domain): Domain security score is $($_.ScorePercentage)%, which is below the threshold of $InputValue%. Issues: $($_.ScoreExplanation)" Domain = $_.Domain From 80e9bc19af3c82fbfe2ad834ae950f0f1dfb6559 Mon Sep 17 00:00:00 2001 From: Bobby <31723128+kris6673@users.noreply.github.com> Date: Sat, 14 Feb 2026 13:10:07 +0100 Subject: [PATCH 405/503] fix: logging, appease the great PSScriptAnalyser and casing --- Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 | 76 ++++++++++---------- 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 b/Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 index 8a713165f482..4f61f708abd7 100644 --- a/Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 +++ b/Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 @@ -23,10 +23,10 @@ function New-CIPPCAPolicy { } else { Remove-EmptyArrays $Object[$Key] } } } elseif ($Object -is [PSCustomObject]) { - foreach ($Name in @($Object.psobject.properties.Name)) { + foreach ($Name in @($Object.PSObject.Properties.Name)) { if ($Object.$Name -is [Array] -and $Object.$Name.get_Count() -eq 0) { $Object.PSObject.Properties.Remove($Name) - } elseif ($null -eq $object.$name) { + } elseif ($null -eq $Object.$Name) { $Object.PSObject.Properties.Remove($Name) } else { Remove-EmptyArrays $Object.$Name } } @@ -34,23 +34,23 @@ function New-CIPPCAPolicy { } # Function to check if a string is a GUID function Test-IsGuid($string) { - return [guid]::tryparse($string, [ref][guid]::Empty) + return [guid]::TryParse($string, [ref][guid]::Empty) } # Helper function to replace group display names with GUIDs - function Replace-GroupNameWithId { + function Convert-GroupNameToId { param($TenantFilter, $groupNames, $CreateGroups, $GroupTemplates) $GroupIds = [System.Collections.Generic.List[string]]::new() $groupNames | ForEach-Object { if (Test-IsGuid $_) { - Write-LogMessage -Headers $Headers -API 'Create CA Policy' -message "Already GUID, no need to replace: $_" -Sev 'Debug' + Write-LogMessage -Headers $Headers -API $APIName -message "Already GUID, no need to replace: $_" -Sev 'Debug' $GroupIds.Add($_) # it's a GUID, so we keep it } else { $groupId = ($groups | Where-Object -Property displayName -EQ $_).id # it's a display name, so we get the group ID if ($groupId) { foreach ($gid in $groupId) { Write-Warning "Replaced group name $_ with ID $gid" - $null = Write-LogMessage -Headers $Headers -API 'Create CA Policy' -message "Replaced group name $_ with ID $gid" -Sev 'Debug' + $null = Write-LogMessage -Headers $Headers -API $APIName -message "Replaced group name $_ with ID $gid" -Sev 'Debug' $GroupIds.Add($gid) # add the ID to the list } } elseif ($CreateGroups) { @@ -58,7 +58,7 @@ function New-CIPPCAPolicy { if ($GroupTemplates.displayName -eq $_) { Write-Information "Creating group from template for $_" $GroupTemplate = $GroupTemplates | Where-Object -Property displayName -EQ $_ - $NewGroup = New-CIPPGroup -GroupObject $GroupTemplate -TenantFilter $TenantFilter -APIName 'New-CIPPCAPolicy' + $NewGroup = New-CIPPGroup -GroupObject $GroupTemplate -TenantFilter $TenantFilter -APIName $APIName $GroupIds.Add($NewGroup.GroupId) } else { Write-Information "No template found, creating security group for $_" @@ -72,7 +72,7 @@ function New-CIPPCAPolicy { username = $username securityEnabled = $true } - $NewGroup = New-CIPPGroup -GroupObject $GroupObject -TenantFilter $TenantFilter -APIName 'New-CIPPCAPolicy' + $NewGroup = New-CIPPGroup -GroupObject $GroupObject -TenantFilter $TenantFilter -APIName $APIName $GroupIds.Add($NewGroup.GroupId) } } else { @@ -83,20 +83,20 @@ function New-CIPPCAPolicy { return $GroupIds } - function Replace-UserNameWithId { + function Convert-UserNameToId { param($userNames) $UserIds = [System.Collections.Generic.List[string]]::new() $userNames | ForEach-Object { if (Test-IsGuid $_) { - Write-LogMessage -Headers $Headers -API 'Create CA Policy' -message "Already GUID, no need to replace: $_" -Sev 'Debug' + Write-LogMessage -Headers $Headers -API $APIName -message "Already GUID, no need to replace: $_" -Sev 'Debug' $UserIds.Add($_) # it's a GUID, so we keep it } else { $userId = ($users | Where-Object -Property displayName -EQ $_).id # it's a display name, so we get the user ID if ($userId) { foreach ($uid in $userId) { Write-Warning "Replaced user name $_ with ID $uid" - $null = Write-LogMessage -Headers $Headers -API 'Create CA Policy' -message "Replaced user name $_ with ID $uid" -Sev 'Debug' + $null = Write-LogMessage -Headers $Headers -API $APIName -message "Replaced user name $_ with ID $uid" -Sev 'Debug' $UserIds.Add($uid) # add the ID to the list } } else { @@ -107,7 +107,7 @@ function New-CIPPCAPolicy { return $UserIds } - $displayname = ($RawJSON | ConvertFrom-Json).Displayname + $displayName = ($RawJSON | ConvertFrom-Json).displayName $JSONobj = $RawJSON | ConvertFrom-Json | Select-Object * -ExcludeProperty ID, GUID, *time* Remove-EmptyArrays $JSONobj @@ -125,7 +125,7 @@ function New-CIPPCAPolicy { # no issues here. } - #If Grant Controls contains authenticationstrength, create these and then replace the id + #If Grant Controls contains authenticationStrength, create these and then replace the id if ($JSONobj.GrantControls.authenticationStrength.policyType -eq 'custom' -or $JSONobj.GrantControls.authenticationStrength.policyType -eq 'BuiltIn') { $ExistingStrength = New-GraphGETRequest -uri 'https://graph.microsoft.com/beta/identity/conditionalAccess/authenticationStrength/policies/' -tenantid $TenantFilter -asApp $true | Where-Object -Property displayName -EQ $JSONobj.GrantControls.authenticationStrength.displayName if ($ExistingStrength) { @@ -133,14 +133,13 @@ function New-CIPPCAPolicy { } else { $Body = ConvertTo-Json -InputObject $JSONobj.GrantControls.authenticationStrength - $GraphRequest = New-GraphPOSTRequest -uri 'https://graph.microsoft.com/beta/identity/conditionalAccess/authenticationStrength/policies' -body $body -Type POST -tenantid $tenantfilter -asApp $true + $GraphRequest = New-GraphPOSTRequest -uri 'https://graph.microsoft.com/beta/identity/conditionalAccess/authenticationStrength/policies' -body $body -Type POST -tenantid $TenantFilter -asApp $true $JSONobj.GrantControls.authenticationStrength = @{ id = $ExistingStrength.id } - Write-LogMessage -Headers $Headers -API $APINAME -message "Created new Authentication Strength Policy: $($JSONobj.GrantControls.authenticationStrength.displayName)" -Sev 'Info' + Write-LogMessage -Headers $Headers -API $APIName -message "Created new Authentication Strength Policy: $($JSONobj.GrantControls.authenticationStrength.displayName)" -Sev 'Info' } } #if we have excluded or included applications, we need to remove any appIds that do not have a service principal in the tenant - if (($JSONobj.conditions.applications.includeApplications -and $JSONobj.conditions.applications.includeApplications -notcontains 'All') -or ($JSONobj.conditions.applications.excludeApplications -and $JSONobj.conditions.applications.excludeApplications -notcontains 'All')) { $AllServicePrincipals = New-GraphGETRequest -uri 'https://graph.microsoft.com/v1.0/servicePrincipals?$select=appId' -tenantid $TenantFilter -asApp $true @@ -179,14 +178,14 @@ function New-CIPPCAPolicy { Remove-ODataProperties -Object $LocationUpdate $Body = ConvertTo-Json -InputObject $LocationUpdate -Depth 10 try { - $null = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/identity/conditionalAccess/namedLocations/$($ExistingLocation.id)" -body $body -Type PATCH -tenantid $tenantfilter -asApp $true - Write-LogMessage -Tenant $TenantFilter -Headers $Headers -API $APINAME -message "Updated existing Named Location: $($location.displayName)" -Sev 'Info' + $null = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/identity/conditionalAccess/namedLocations/$($ExistingLocation.id)" -body $body -Type PATCH -tenantid $TenantFilter -asApp $true + Write-LogMessage -Tenant $TenantFilter -Headers $Headers -API $APIName -message "Updated existing Named Location: $($location.displayName)" -Sev 'Info' } catch { Write-Warning "Failed to update location $($location.displayName): $_" - Write-LogMessage -Tenant $TenantFilter -Headers $Headers -API $APINAME -message "Failed to update existing Named Location: $($location.displayName). Error: $_" -Sev 'Error' + Write-LogMessage -Tenant $TenantFilter -Headers $Headers -API $APIName -message "Failed to update existing Named Location: $($location.displayName). Error: $_" -Sev 'Error' } } else { - Write-LogMessage -Tenant $TenantFilter -Headers $Headers -API $APINAME -message "Matched a CA policy with the existing Named Location: $($location.displayName)" -Sev 'Info' + Write-LogMessage -Tenant $TenantFilter -Headers $Headers -API $APIName -message "Matched a CA policy with the existing Named Location: $($location.displayName)" -Sev 'Info' } [pscustomobject]@{ id = $ExistingLocation.id @@ -198,17 +197,17 @@ function New-CIPPCAPolicy { $LocationBody = $location | Select-Object * -ExcludeProperty id Remove-ODataProperties -Object $LocationBody $Body = ConvertTo-Json -InputObject $LocationBody - $GraphRequest = New-GraphPOSTRequest -uri 'https://graph.microsoft.com/beta/identity/conditionalAccess/namedLocations' -body $body -Type POST -tenantid $tenantfilter -asApp $true + $GraphRequest = New-GraphPOSTRequest -uri 'https://graph.microsoft.com/beta/identity/conditionalAccess/namedLocations' -body $body -Type POST -tenantid $TenantFilter -asApp $true $retryCount = 0 $MaxRetryCount = 10 do { Write-Host "Checking for location $($GraphRequest.id) attempt $retryCount. $TenantFilter" - $LocationRequest = New-GraphGETRequest -uri 'https://graph.microsoft.com/beta/identity/conditionalAccess/namedLocations' -tenantid $tenantfilter -asApp $true | Where-Object -Property id -EQ $GraphRequest.id + $LocationRequest = New-GraphGETRequest -uri 'https://graph.microsoft.com/beta/identity/conditionalAccess/namedLocations' -tenantid $TenantFilter -asApp $true | Where-Object -Property id -EQ $GraphRequest.id Write-Host "LocationRequest: $($LocationRequest.id)" Start-Sleep -Seconds 2 $retryCount++ } while ((!$LocationRequest -or !$LocationRequest.id) -and ($retryCount -lt $MaxRetryCount)) - Write-LogMessage -Tenant $TenantFilter -Headers $Headers -API $APINAME -message "Created new Named Location: $($location.displayName)" -Sev 'Info' + Write-LogMessage -Tenant $TenantFilter -Headers $Headers -API $APIName -message "Created new Named Location: $($location.displayName)" -Sev 'Info' [pscustomobject]@{ id = $GraphRequest.id name = $GraphRequest.displayName @@ -281,14 +280,14 @@ function New-CIPPCAPolicy { foreach ($userType in 'includeUsers', 'excludeUsers') { if ($JSONobj.conditions.users.PSObject.Properties.Name -contains $userType -and $JSONobj.conditions.users.$userType -notin 'All', 'None', 'GuestOrExternalUsers') { - $JSONobj.conditions.users.$userType = @(Replace-UserNameWithId -userNames $JSONobj.conditions.users.$userType) + $JSONobj.conditions.users.$userType = @(Convert-UserNameToId -userNames $JSONobj.conditions.users.$userType) } } # Check the included and excluded groups foreach ($groupType in 'includeGroups', 'excludeGroups') { if ($JSONobj.conditions.users.PSObject.Properties.Name -contains $groupType) { - $JSONobj.conditions.users.$groupType = @(Replace-GroupNameWithId -groupNames $JSONobj.conditions.users.$groupType -CreateGroups $CreateGroups -TenantFilter $TenantFilter -GroupTemplates $GroupTemplates) + $JSONobj.conditions.users.$groupType = @(Convert-GroupNameToId -groupNames $JSONobj.conditions.users.$groupType -CreateGroups $CreateGroups -TenantFilter $TenantFilter -GroupTemplates $GroupTemplates) } } } catch { @@ -323,8 +322,8 @@ function New-CIPPCAPolicy { #Send request to disable security defaults. $body = '{ "isEnabled": false }' try { - $null = New-GraphPostRequest -tenantid $TenantFilter -Uri 'https://graph.microsoft.com/beta/policies/identitySecurityDefaultsEnforcementPolicy' -Type patch -Body $body -asApp $true -ContentType 'application/json' - Write-LogMessage -Headers $Headers -API 'Create CA Policy' -tenant $TenantFilter -message "Disabled Security Defaults for tenant $($TenantFilter)" -Sev 'Info' + $null = New-GraphPostRequest -tenantid $TenantFilter -Uri 'https://graph.microsoft.com/beta/policies/identitySecurityDefaultsEnforcementPolicy' -Type patch -Body $body -asApp $true + Write-LogMessage -Headers $Headers -API $APIName -tenant $TenantFilter -message "Disabled Security Defaults for tenant $($TenantFilter)" -Sev 'Info' Start-Sleep 3 } catch { $ErrorMessage = Get-CippException -Exception $_ @@ -335,10 +334,10 @@ function New-CIPPCAPolicy { Write-Information $RawJSON try { Write-Information 'Checking for existing policies' - $CheckExisting = New-GraphGETRequest -uri 'https://graph.microsoft.com/beta/identity/conditionalAccess/policies' -tenantid $TenantFilter -asApp $true | Where-Object -Property displayName -EQ $displayname + $CheckExisting = New-GraphGETRequest -uri 'https://graph.microsoft.com/beta/identity/conditionalAccess/policies' -tenantid $TenantFilter -asApp $true | Where-Object -Property displayName -EQ $displayName if ($CheckExisting) { if ($Overwrite -ne $true) { - throw "Conditional Access Policy with Display Name $($Displayname) Already exists" + throw "Conditional Access Policy with Display Name $($displayName) Already exists" return $false } else { if ($State -eq 'donotchange') { @@ -370,26 +369,27 @@ function New-CIPPCAPolicy { Write-Information "Failed to preserve vacation exclusion group: $($_.Exception.Message)" } Write-Information "overwriting $($CheckExisting.id)" - $null = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/identity/conditionalAccess/policies/$($CheckExisting.id)" -tenantid $tenantfilter -type PATCH -body $RawJSON -asApp $true - Write-LogMessage -Headers $Headers -API 'Create CA Policy' -tenant $($Tenant) -message "Updated Conditional Access Policy $($JSONobj.Displayname) to the template standard." -Sev 'Info' - return "Updated policy $displayname for $tenantfilter" + $null = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/identity/conditionalAccess/policies/$($CheckExisting.id)" -tenantid $TenantFilter -type PATCH -body $RawJSON -asApp $true + Write-LogMessage -Headers $Headers -API $APIName -tenant $TenantFilter -message "Updated Conditional Access Policy $($JSONobj.displayName) to the template standard." -Sev 'Info' + return "Updated policy $($JSONobj.displayName) for $TenantFilter" } } else { Write-Information 'Creating new policy' if ($JSOObj.GrantControls.authenticationStrength.policyType -or $JSONobj.$JSONobj.LocationInfo) { Start-Sleep 3 } - $null = New-GraphPOSTRequest -uri 'https://graph.microsoft.com/beta/identity/conditionalAccess/policies' -tenantid $tenantfilter -type POST -body $RawJSON -asApp $true - Write-LogMessage -Headers $Headers -API 'Create CA Policy' -tenant $($Tenant) -message "Added Conditional Access Policy $($JSONobj.Displayname)" -Sev 'Info' - return "Created policy $displayname for $tenantfilter" + $null = New-GraphPOSTRequest -uri 'https://graph.microsoft.com/beta/identity/conditionalAccess/policies' -tenantid $TenantFilter -type POST -body $RawJSON -asApp $true + Write-LogMessage -Headers $Headers -API $APIName -tenant $TenantFilter -message "Added Conditional Access Policy $($JSONobj.displayName)" -Sev 'Info' + return "Created policy $($JSONobj.displayName) for $TenantFilter" } } catch { $ErrorMessage = Get-CippException -Exception $_ - Write-LogMessage -API 'Standards' -tenant $TenantFilter -message "Failed to create or update conditional access rule $($JSONobj.displayName): $($ErrorMessage.NormalizedError) " -sev 'Error' -LogData $ErrorMessage + $Result = "Failed to create or update conditional access rule $($JSONobj.displayName): $($ErrorMessage.NormalizedError)" + Write-LogMessage -API $APIName -tenant $TenantFilter -message $Result -sev 'Error' -LogData $ErrorMessage - Write-Warning "Failed to create or update conditional access rule $($JSONobj.displayName): $($ErrorMessage.NormalizedError)" + Write-Warning $Result Write-Information $_.InvocationInfo.PositionMessage Write-Information ($JSONobj | ConvertTo-Json -Depth 10) - throw "Failed to create or update conditional access rule $($JSONobj.displayName): $($ErrorMessage.NormalizedError)" + throw $Result } } From 476c0612e6bc33544eafedd2c9a224dc3dae85d3 Mon Sep 17 00:00:00 2001 From: Bobby <31723128+kris6673@users.noreply.github.com> Date: Sat, 14 Feb 2026 13:22:09 +0100 Subject: [PATCH 406/503] fix: sort licensed users and groups by display name Possibly fixes https://github.com/KelvinTegelaar/CIPP/issues/5338 Sort licenses by License name by default ADD WORD --- Modules/CIPPCore/Public/Get-CIPPLAPSPassword.ps1 | 2 +- Modules/CIPPCore/Public/Get-CIPPLicenseOverview.ps1 | 6 +++--- cspell.json | 1 + 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Modules/CIPPCore/Public/Get-CIPPLAPSPassword.ps1 b/Modules/CIPPCore/Public/Get-CIPPLAPSPassword.ps1 index a6718f332a10..b0cd9c6534a0 100644 --- a/Modules/CIPPCore/Public/Get-CIPPLAPSPassword.ps1 +++ b/Modules/CIPPCore/Public/Get-CIPPLAPSPassword.ps1 @@ -9,7 +9,7 @@ function Get-CIPPLapsPassword { ) try { - $GraphRequest = (New-GraphGetRequest -noauthcheck $true -uri "https://graph.microsoft.com/beta/directory/deviceLocalCredentials/$($device)?`$select=credentials" -tenantid $TenantFilter).credentials | Select-Object -First 1 | ForEach-Object { + $GraphRequest = (New-GraphGetRequest -NoAuthCheck $true -uri "https://graph.microsoft.com/beta/directory/deviceLocalCredentials/$($device)?`$select=credentials" -tenantid $TenantFilter).credentials | Select-Object -First 1 | ForEach-Object { $PlainText = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($_.passwordBase64)) $date = $_.BackupDateTime [PSCustomObject]@{ diff --git a/Modules/CIPPCore/Public/Get-CIPPLicenseOverview.ps1 b/Modules/CIPPCore/Public/Get-CIPPLicenseOverview.ps1 index bfa4b519de08..752cf71f8cc7 100644 --- a/Modules/CIPPCore/Public/Get-CIPPLicenseOverview.ps1 +++ b/Modules/CIPPCore/Public/Get-CIPPLicenseOverview.ps1 @@ -63,7 +63,7 @@ function Get-CIPPLicenseOverview { $ExcludedSkuList = Get-CIPPAzDataTableEntity @LicenseTable } - $AllLicensedUsers = @(($Results | Where-Object { $_.id -eq 'licensedUsers' }).body.value) + $AllLicensedUsers = @(($Results | Where-Object { $_.id -eq 'licensedUsers' }).body.value) | Sort-Object -Property displayName $UsersBySku = @{} foreach ($User in $AllLicensedUsers) { if (-not $User.assignedLicenses) { continue } # Skip users with no assigned licenses. Should not happens as the filter is applied, but just in case @@ -84,7 +84,7 @@ function Get-CIPPLicenseOverview { } - $AllLicensedGroups = @(($Results | Where-Object { $_.id -eq 'licensedGroups' }).body.value) + $AllLicensedGroups = @(($Results | Where-Object { $_.id -eq 'licensedGroups' }).body.value) | Sort-Object -Property displayName $GroupsBySku = @{} foreach ($Group in $AllLicensedGroups) { if (-not $Group.assignedLicenses) { continue } @@ -156,5 +156,5 @@ function Get-CIPPLicenseOverview { } } } - return $GraphRequest + return ($GraphRequest | Sort-Object -Property License) } diff --git a/cspell.json b/cspell.json index 28a883c89c5e..597a0ed1277a 100644 --- a/cspell.json +++ b/cspell.json @@ -43,6 +43,7 @@ "SharePoint", "Sherweb", "Signup", + "Skus", "SSPR", "Standardcal", "Terrl", From feb8e7f40803c105983cec5d153d008a6a51f920 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Sat, 14 Feb 2026 14:05:52 +0100 Subject: [PATCH 407/503] Add new alert --- .../Alerts/Get-CIPPAlertNewMFADevice.ps1 | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 Modules/CIPPCore/Public/Alerts/Get-CIPPAlertNewMFADevice.ps1 diff --git a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertNewMFADevice.ps1 b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertNewMFADevice.ps1 new file mode 100644 index 000000000000..22093da183e2 --- /dev/null +++ b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertNewMFADevice.ps1 @@ -0,0 +1,41 @@ +function Get-CIPPAlertNewMFADevice { + <# + .FUNCTIONALITY + Entrypoint + #> + [CmdletBinding()] + param ( + [Parameter(Mandatory = $false)] + [Alias('input')] + $InputValue, + $TenantFilter + ) + + try { + $OneHourAgo = (Get-Date).AddHours(-1).ToString('yyyy-MM-ddTHH:mm:ssZ') + + $AuditLogs = New-GraphGetRequest -uri "https://graph.microsoft.com/v1.0/auditLogs/directoryAudits?`$filter=activityDateTime ge $OneHourAgo and (activityDisplayName eq 'User registered security info' or activityDisplayName eq 'User deleted security info')" -tenantid $TenantFilter + $AlertData = foreach ($Log in $AuditLogs) { + if ($Log.activityDisplayName -eq 'User registered security info') { + $User = $Log.targetResources[0].userPrincipalName + if (-not $User) { $User = $Log.initiatedBy.user.userPrincipalName } + + [PSCustomObject]@{ + Message = "New MFA method registered: $User" + User = $User + DisplayName = $Log.targetResources[0].displayName + Activity = $Log.activityDisplayName + ActivityTime = $Log.activityDateTime + Tenant = $TenantFilter + } + } + } + + if ($AlertData) { + Write-AlertTrace -cmdletName $MyInvocation.MyCommand -tenantFilter $TenantFilter -data $AlertData + } + + } catch { + Write-AlertMessage -tenant $($TenantFilter) -message "Could not check for new MFA devices for $($TenantFilter): $(Get-NormalizedError -message $_.Exception.message)" + } +} From 922c744b91d769594c240d18630960812f97652a Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Sat, 14 Feb 2026 14:05:55 +0100 Subject: [PATCH 408/503] alert add --- .../Public/Alerts/Get-CIPPAlertNewMFADevice.ps1 | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertNewMFADevice.ps1 b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertNewMFADevice.ps1 index 22093da183e2..10254cbb65bc 100644 --- a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertNewMFADevice.ps1 +++ b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertNewMFADevice.ps1 @@ -13,20 +13,20 @@ function Get-CIPPAlertNewMFADevice { try { $OneHourAgo = (Get-Date).AddHours(-1).ToString('yyyy-MM-ddTHH:mm:ssZ') - + $AuditLogs = New-GraphGetRequest -uri "https://graph.microsoft.com/v1.0/auditLogs/directoryAudits?`$filter=activityDateTime ge $OneHourAgo and (activityDisplayName eq 'User registered security info' or activityDisplayName eq 'User deleted security info')" -tenantid $TenantFilter $AlertData = foreach ($Log in $AuditLogs) { if ($Log.activityDisplayName -eq 'User registered security info') { $User = $Log.targetResources[0].userPrincipalName if (-not $User) { $User = $Log.initiatedBy.user.userPrincipalName } - + [PSCustomObject]@{ - Message = "New MFA method registered: $User" - User = $User - DisplayName = $Log.targetResources[0].displayName - Activity = $Log.activityDisplayName - ActivityTime = $Log.activityDateTime - Tenant = $TenantFilter + Message = "New MFA method registered: $User" + User = $User + DisplayName = $Log.targetResources[0].displayName + Activity = $Log.activityDisplayName + ActivityTime = $Log.activityDateTime + Tenant = $TenantFilter } } } From 403588c322675125c16994616ca76aa58d43e954 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Sat, 14 Feb 2026 18:23:33 -0500 Subject: [PATCH 409/503] increase threshold for exchange missing roles --- Modules/CIPPCore/Public/Test-CIPPAccessTenant.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Test-CIPPAccessTenant.ps1 b/Modules/CIPPCore/Public/Test-CIPPAccessTenant.ps1 index 4ca993cbc631..a9f8459aec59 100644 --- a/Modules/CIPPCore/Public/Test-CIPPAccessTenant.ps1 +++ b/Modules/CIPPCore/Public/Test-CIPPAccessTenant.ps1 @@ -120,7 +120,7 @@ function Test-CIPPAccessTenant { $AvailableRoles = $RoleDefinitions | Where-Object -Property displayName -In $AllOrgManagementRoles | Select-Object -Property displayName, id, description Write-Information "Found $($AvailableRoles.Count) available Organization Management roles in Exchange" $MissingOrgMgmtRoles = $AvailableRoles | Where-Object { $OrgManagementRoles.Role -notcontains $_.displayName } - if (($MissingOrgMgmtRoles | Measure-Object).Count -gt 0) { + if (($MissingOrgMgmtRoles | Measure-Object).Count -ge 5) { $Results.OrgManagementRolesMissing = $MissingOrgMgmtRoles Write-Warning "Found $($MissingRoles.Count) missing Organization Management roles in Exchange" $ExchangeStatus = $false From 2b4d5553fcef6270b5c548a521bc7d1b99d94ceb Mon Sep 17 00:00:00 2001 From: John Duprey Date: Sat, 14 Feb 2026 18:28:35 -0500 Subject: [PATCH 410/503] Sort quarantine requests and log errors Order Get-QuarantineMessage results by ReceivedTime and replace Write-AlertMessage with Write-LogMessage (API='Alerts', sev=Error) in the catch block. This makes quarantine release requests deterministic by received time and routes errors to the centralized logging API. --- .../Public/Alerts/Get-CIPPAlertQuarantineReleaseRequests.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertQuarantineReleaseRequests.ps1 b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertQuarantineReleaseRequests.ps1 index f0ca6d528e40..81a1fecada71 100644 --- a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertQuarantineReleaseRequests.ps1 +++ b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertQuarantineReleaseRequests.ps1 @@ -29,7 +29,7 @@ } try { - $RequestedReleases = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-QuarantineMessage' -cmdParams @{ PageSize = 1000; ReleaseStatus = 'Requested'; StartReceivedDate = (Get-Date).AddHours(-6) } -ErrorAction Stop | Select-Object -ExcludeProperty *data.type* + $RequestedReleases = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-QuarantineMessage' -cmdParams @{ PageSize = 1000; ReleaseStatus = 'Requested'; StartReceivedDate = (Get-Date).AddHours(-6) } -ErrorAction Stop | Select-Object -ExcludeProperty *data.type* | Sort-Object -Property ReceivedTime if ($RequestedReleases) { # Get the CIPP URL for the Quarantine link @@ -56,6 +56,6 @@ Write-AlertTrace -cmdletName $MyInvocation.MyCommand -tenantFilter $TenantFilter -data $AlertData } } catch { - Write-AlertMessage -tenant $TenantFilter -message "QuarantineReleaseRequests: $(Get-NormalizedError -message $_.Exception.Message)" + Write-LogMessage -API 'Alerts' -tenant $TenantFilter -message "QuarantineReleaseRequests: $(Get-NormalizedError -message $_.Exception.Message)" -sev Error } } From ccd902366149b652572a6fcdcafe63e630acbf26 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Sat, 14 Feb 2026 18:30:59 -0500 Subject: [PATCH 411/503] Use Write-LogMessage for scripted alert errors Replace Write-AlertMessage calls with Write-LogMessage across multiple Get-CIPPAlert*.ps1 cmdlets. Adds consistent -API 'Alerts' context and appropriate -sev (Error/Warning) values; preserves additional log data where present (e.g. -LogData). This centralizes and standardizes error logging for alert modules and cleans up some ad-hoc Write-Information usage. --- Modules/CIPPCore/Public/Alerts/Get-CIPPAlertAdminPassword.ps1 | 2 +- .../CIPPCore/Public/Alerts/Get-CIPPAlertDefenderMalware.ps1 | 2 +- .../CIPPCore/Public/Alerts/Get-CIPPAlertDefenderStatus.ps1 | 2 +- .../CIPPCore/Public/Alerts/Get-CIPPAlertDepTokenExpiry.ps1 | 2 +- .../CIPPCore/Public/Alerts/Get-CIPPAlertDeviceCompliance.ps1 | 2 +- .../Public/Alerts/Get-CIPPAlertEntraConnectSyncStatus.ps1 | 4 +--- .../Public/Alerts/Get-CIPPAlertGlobalAdminAllowList.ps1 | 2 +- .../Public/Alerts/Get-CIPPAlertInactiveGuestUsers.ps1 | 2 +- .../Public/Alerts/Get-CIPPAlertInactiveLicensedUsers.ps1 | 2 +- Modules/CIPPCore/Public/Alerts/Get-CIPPAlertInactiveUsers.ps1 | 2 +- .../Public/Alerts/Get-CIPPAlertIntunePolicyConflicts.ps1 | 4 ++-- .../Public/Alerts/Get-CIPPAlertLowTenantAlignment.ps1 | 4 ++-- Modules/CIPPCore/Public/Alerts/Get-CIPPAlertNewMFADevice.ps1 | 2 +- Modules/CIPPCore/Public/Alerts/Get-CIPPAlertNewRiskyUsers.ps1 | 4 ++-- Modules/CIPPCore/Public/Alerts/Get-CIPPAlertNewRole.ps1 | 2 +- Modules/CIPPCore/Public/Alerts/Get-CIPPAlertNoCAConfig.ps1 | 2 +- Modules/CIPPCore/Public/Alerts/Get-CIPPAlertOnedriveQuota.ps1 | 2 +- .../CIPPCore/Public/Alerts/Get-CIPPAlertOverusedLicenses.ps1 | 2 +- Modules/CIPPCore/Public/Alerts/Get-CIPPAlertReportOnlyCA.ps1 | 2 +- .../Public/Alerts/Get-CIPPAlertSecDefaultsDisabled.ps1 | 2 +- Modules/CIPPCore/Public/Alerts/Get-CIPPAlertSecureScore.ps1 | 2 +- .../Public/Alerts/Get-CIPPAlertSoftDeletedMailboxes.ps1 | 2 +- .../CIPPCore/Public/Alerts/Get-CIPPAlertStaleEntraDevices.ps1 | 2 +- .../CIPPCore/Public/Alerts/Get-CIPPAlertUnusedLicenses.ps1 | 2 +- 24 files changed, 27 insertions(+), 29 deletions(-) diff --git a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertAdminPassword.ps1 b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertAdminPassword.ps1 index 27e911fe98b0..a0b6888e2046 100644 --- a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertAdminPassword.ps1 +++ b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertAdminPassword.ps1 @@ -21,6 +21,6 @@ function Get-CIPPAlertAdminPassword { } Write-AlertTrace -cmdletName $MyInvocation.MyCommand -tenantFilter $TenantFilter -data $AlertData } catch { - Write-AlertMessage -tenant $($TenantFilter) -message "Could not get admin password changes for $($TenantFilter): $(Get-NormalizedError -message $_.Exception.message)" + Write-LogMessage -API 'Alerts' -tenant $($TenantFilter) -message "Could not get admin password changes for $($TenantFilter): $(Get-NormalizedError -message $_.Exception.message)" -sev Error } } diff --git a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertDefenderMalware.ps1 b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertDefenderMalware.ps1 index 252b5abe1b78..8ac96f34286f 100644 --- a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertDefenderMalware.ps1 +++ b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertDefenderMalware.ps1 @@ -30,6 +30,6 @@ function Get-CIPPAlertDefenderMalware { Write-AlertTrace -cmdletName $MyInvocation.MyCommand -tenantFilter $TenantFilter -data $AlertData } catch { - Write-AlertMessage -tenant $($TenantFilter) -message "Could not get malware data for $($TenantFilter): $(Get-NormalizedError -message $_.Exception.message)" + Write-LogMessage -API 'Alerts' -tenant $($TenantFilter) -message "Could not get malware data for $($TenantFilter): $(Get-NormalizedError -message $_.Exception.message)" -sev Error } } diff --git a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertDefenderStatus.ps1 b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertDefenderStatus.ps1 index defef38677a1..44ae39bfc7f1 100644 --- a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertDefenderStatus.ps1 +++ b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertDefenderStatus.ps1 @@ -29,6 +29,6 @@ function Get-CIPPAlertDefenderStatus { Write-AlertTrace -cmdletName $MyInvocation.MyCommand -tenantFilter $TenantFilter -data $AlertData } catch { - Write-AlertMessage -tenant $($TenantFilter) -message "Could not get defender status for $($TenantFilter): $(Get-NormalizedError -message $_.Exception.message)" + Write-LogMessage -API 'Alerts' -tenant $($TenantFilter) -message "Could not get defender status for $($TenantFilter): $(Get-NormalizedError -message $_.Exception.message)" -sev Error } } diff --git a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertDepTokenExpiry.ps1 b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertDepTokenExpiry.ps1 index 23004aa5c307..afc731eefb89 100644 --- a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertDepTokenExpiry.ps1 +++ b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertDepTokenExpiry.ps1 @@ -26,6 +26,6 @@ function Get-CIPPAlertDepTokenExpiry { } catch { - Write-AlertMessage -tenant $($TenantFilter) -message "Failed to check Apple Device Enrollment Program token expiry for $($TenantFilter): $(Get-NormalizedError -message $_.Exception.message)" + Write-LogMessage -API 'Alerts' -tenant $($TenantFilter) -message "Failed to check Apple Device Enrollment Program token expiry for $($TenantFilter): $(Get-NormalizedError -message $_.Exception.message)" -sev Error } } diff --git a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertDeviceCompliance.ps1 b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertDeviceCompliance.ps1 index ebdf7ee55be8..0ea7bd38fe5c 100644 --- a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertDeviceCompliance.ps1 +++ b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertDeviceCompliance.ps1 @@ -15,6 +15,6 @@ function Get-CIPPAlertDeviceCompliance { $AlertData = New-GraphGETRequest -uri "https://graph.microsoft.com/v1.0/deviceManagement/managedDevices?`$filter=complianceState eq 'noncompliant'&`$select=id,deviceName,managedDeviceOwnerType,complianceState,lastSyncDateTime&`$top=999" -tenantid $TenantFilter Write-AlertTrace -cmdletName $MyInvocation.MyCommand -tenantFilter $TenantFilter -data $AlertData } catch { - Write-AlertMessage -tenant $($TenantFilter) -message "Could not get compliance state for $($TenantFilter): $(Get-NormalizedError -message $_.Exception.message)" + Write-LogMessage -API 'Alerts' -tenant $($TenantFilter) -message "Could not get compliance state for $($TenantFilter): $(Get-NormalizedError -message $_.Exception.message)" -sev Error } } diff --git a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertEntraConnectSyncStatus.ps1 b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertEntraConnectSyncStatus.ps1 index 31ebb125ad83..6dd99ceeee8e 100644 --- a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertEntraConnectSyncStatus.ps1 +++ b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertEntraConnectSyncStatus.ps1 @@ -36,8 +36,6 @@ function Get-CIPPAlertEntraConnectSyncStatus { } } } catch { - Write-AlertMessage -tenant $($TenantFilter) -message "Could not get Entra Connect Sync Status for $($TenantFilter): $(Get-NormalizedError -message $_.Exception.message)" -LogData (Get-CippException -Exception $_) - Write-Information "Could not get Entra Connect Sync Status for $($TenantFilter): $(Get-NormalizedError -message $_.Exception.message)" - Write-Information $_.PositionMessage + Write-LogMessage -API 'Alerts' -tenant $($TenantFilter) -message "Could not get Entra Connect Sync Status for $($TenantFilter): $(Get-NormalizedError -message $_.Exception.message)" -sev Error -LogData (Get-CippException -Exception $_) } } diff --git a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertGlobalAdminAllowList.ps1 b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertGlobalAdminAllowList.ps1 index 82ae1ebc1cfb..31a082b0e714 100644 --- a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertGlobalAdminAllowList.ps1 +++ b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertGlobalAdminAllowList.ps1 @@ -79,6 +79,6 @@ function Get-CIPPAlertGlobalAdminAllowList { Write-AlertTrace -cmdletName $MyInvocation.MyCommand -tenantFilter $TenantFilter -data $AlertData } } catch { - Write-AlertMessage -tenant $TenantFilter -message "Failed to check approved Global Admins: $(Get-NormalizedError -message $_.Exception.Message)" + Write-LogMessage -API 'Alerts' -tenant $TenantFilter -message "Failed to check approved Global Admins: $(Get-NormalizedError -message $_.Exception.Message)" -sev Error } } diff --git a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertInactiveGuestUsers.ps1 b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertInactiveGuestUsers.ps1 index 839a0af97e37..195b5d51c31c 100644 --- a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertInactiveGuestUsers.ps1 +++ b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertInactiveGuestUsers.ps1 @@ -84,6 +84,6 @@ function Get-CIPPAlertInactiveGuestUsers { Write-AlertTrace -cmdletName $MyInvocation.MyCommand -tenantFilter $TenantFilter -data $AlertData } catch {} } catch { - Write-AlertMessage -tenant $($TenantFilter) -message "Failed to check inactive guest users for $($TenantFilter): $(Get-NormalizedError -message $_.Exception.message)" + Write-LogMessage -API 'Alerts' -tenant $($TenantFilter) -message "Failed to check inactive guest users for $($TenantFilter): $(Get-NormalizedError -message $_.Exception.message)" -sev Error } } diff --git a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertInactiveLicensedUsers.ps1 b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertInactiveLicensedUsers.ps1 index 09288d3fee13..d7cdd091a2b2 100644 --- a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertInactiveLicensedUsers.ps1 +++ b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertInactiveLicensedUsers.ps1 @@ -85,6 +85,6 @@ function Get-CIPPAlertInactiveLicensedUsers { Write-AlertTrace -cmdletName $MyInvocation.MyCommand -tenantFilter $TenantFilter -data $AlertData } catch {} } catch { - Write-AlertMessage -tenant $($TenantFilter) -message "Failed to check inactive users with licenses for $($TenantFilter): $(Get-NormalizedError -message $_.Exception.message)" + Write-LogMessage -API 'Alerts' -tenant $($TenantFilter) -message "Failed to check inactive users with licenses for $($TenantFilter): $(Get-NormalizedError -message $_.Exception.message)" -sev Error } } diff --git a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertInactiveUsers.ps1 b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertInactiveUsers.ps1 index 037f37e501d5..0eef10e4991e 100644 --- a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertInactiveUsers.ps1 +++ b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertInactiveUsers.ps1 @@ -80,6 +80,6 @@ function Get-CIPPAlertInactiveUsers { Write-AlertTrace -cmdletName $MyInvocation.MyCommand -tenantFilter $TenantFilter -data $AlertData } catch {} } catch { - Write-AlertMessage -tenant $($TenantFilter) -message "Failed to check inactive users with licenses for $($TenantFilter): $(Get-NormalizedError -message $_.Exception.message)" + Write-LogMessage -API 'Alerts' -tenant $($TenantFilter) -message "Failed to check inactive users with licenses for $($TenantFilter): $(Get-NormalizedError -message $_.Exception.message)" -sev Error } } diff --git a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertIntunePolicyConflicts.ps1 b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertIntunePolicyConflicts.ps1 index 684f6bd0fc87..965170516e28 100644 --- a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertIntunePolicyConflicts.ps1 +++ b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertIntunePolicyConflicts.ps1 @@ -90,7 +90,7 @@ function Get-CIPPAlertIntunePolicyConflicts { } } } catch { - Write-AlertMessage -tenant $TenantFilter -message "Failed to query Intune policy states: $(Get-NormalizedError -message $_.Exception.Message)" + Write-LogMessage -API 'Alerts' -tenant $TenantFilter -message "Failed to query Intune policy states: $(Get-NormalizedError -message $_.Exception.Message)" -sev Error } } @@ -117,7 +117,7 @@ function Get-CIPPAlertIntunePolicyConflicts { } } } catch { - Write-AlertMessage -tenant $TenantFilter -message "Failed to query Intune application states: $(Get-NormalizedError -message $_.Exception.Message)" + Write-LogMessage -API 'Alerts' -tenant $TenantFilter -message "Failed to query Intune application states: $(Get-NormalizedError -message $_.Exception.Message)" -sev Error } } diff --git a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertLowTenantAlignment.ps1 b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertLowTenantAlignment.ps1 index bc8c5a04672d..52de70f3b0a5 100644 --- a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertLowTenantAlignment.ps1 +++ b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertLowTenantAlignment.ps1 @@ -27,7 +27,7 @@ function Get-CIPPAlertLowTenantAlignment { $AlignmentData = Get-CIPPTenantAlignment -TenantFilter $TenantFilter if (-not $AlignmentData) { - Write-AlertMessage -tenant $TenantFilter -message "No alignment data found for tenant $TenantFilter. This may indicate no standards templates are configured or applied to this tenant." + Write-LogMessage -API 'Alerts' -tenant $TenantFilter -message "No alignment data found for tenant $TenantFilter. This may indicate no standards templates are configured or applied to this tenant." -sev Warning return } @@ -47,6 +47,6 @@ function Get-CIPPAlertLowTenantAlignment { } } catch { - Write-AlertMessage -tenant $TenantFilter -message "Could not get tenant alignment data for $TenantFilter`: $(Get-NormalizedError -message $_.Exception.message)" + Write-LogMessage -API 'Alerts' -tenant $TenantFilter -message "Could not get tenant alignment data for $TenantFilter`: $(Get-NormalizedError -message $_.Exception.message)" -sev Error } } diff --git a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertNewMFADevice.ps1 b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertNewMFADevice.ps1 index 10254cbb65bc..9208d700d4dc 100644 --- a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertNewMFADevice.ps1 +++ b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertNewMFADevice.ps1 @@ -36,6 +36,6 @@ function Get-CIPPAlertNewMFADevice { } } catch { - Write-AlertMessage -tenant $($TenantFilter) -message "Could not check for new MFA devices for $($TenantFilter): $(Get-NormalizedError -message $_.Exception.message)" + Write-LogMessage -API 'Alerts' -tenant $($TenantFilter) -message "Could not check for new MFA devices for $($TenantFilter): $(Get-NormalizedError -message $_.Exception.message)" -sev Error } } diff --git a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertNewRiskyUsers.ps1 b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertNewRiskyUsers.ps1 index c0691da7fe0d..9686bd134f87 100644 --- a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertNewRiskyUsers.ps1 +++ b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertNewRiskyUsers.ps1 @@ -14,7 +14,7 @@ function Get-CIPPAlertNewRiskyUsers { # Check if tenant has P2 capabilities $Capabilities = Get-CIPPTenantCapabilities -TenantFilter $TenantFilter if (-not ($Capabilities.AAD_PREMIUM_P2 -eq $true)) { - Write-AlertMessage -tenant $($TenantFilter) -message 'Tenant does not have Azure AD Premium P2 licensing required for risky users detection' + Write-LogMessage -API 'Alerts' -tenant $($TenantFilter) -message 'Tenant does not have Azure AD Premium P2 licensing required for risky users detection' -sev Warning return } @@ -69,6 +69,6 @@ function Get-CIPPAlertNewRiskyUsers { } } } catch { - Write-AlertMessage -tenant $($TenantFilter) -message "Could not get risky users for $($TenantFilter): $(Get-NormalizedError -message $_.Exception.message)" + Write-LogMessage -API 'Alerts' -tenant $($TenantFilter) -message "Could not get risky users for $($TenantFilter): $(Get-NormalizedError -message $_.Exception.message)" -sev Error } } diff --git a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertNewRole.ps1 b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertNewRole.ps1 index 68167632b5b3..c8cae3616484 100644 --- a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertNewRole.ps1 +++ b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertNewRole.ps1 @@ -43,6 +43,6 @@ function Get-CIPPAlertNewRole { Write-AlertTrace -cmdletName $MyInvocation.MyCommand -tenantFilter $TenantFilter -data $AlertData } } catch { - Write-AlertMessage -tenant $($TenantFilter) -message "Could not get get role changes for $($TenantFilter): $(Get-NormalizedError -message $_.Exception.message)" + Write-LogMessage -API 'Alerts' -tenant $($TenantFilter) -message "Could not get get role changes for $($TenantFilter): $(Get-NormalizedError -message $_.Exception.message)" -sev Error } } diff --git a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertNoCAConfig.ps1 b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertNoCAConfig.ps1 index 48ae1dabff57..24055d1b606e 100644 --- a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertNoCAConfig.ps1 +++ b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertNoCAConfig.ps1 @@ -30,7 +30,7 @@ function Get-CIPPAlertNoCAConfig { } } } catch { - Write-AlertMessage -tenant $($TenantFilter) -message "Conditional Access Config Alert: Error occurred: $(Get-NormalizedError -message $_.Exception.message)" + Write-LogMessage -API 'Alerts' -tenant $($TenantFilter) -message "Conditional Access Config Alert: Error occurred: $(Get-NormalizedError -message $_.Exception.message)" -sev Error } } diff --git a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertOnedriveQuota.ps1 b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertOnedriveQuota.ps1 index b39ea94d9a48..b2e786bc9286 100644 --- a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertOnedriveQuota.ps1 +++ b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertOnedriveQuota.ps1 @@ -19,7 +19,7 @@ function Get-CIPPAlertOneDriveQuota { } } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message - Write-AlertMessage -tenant $($TenantFilter) -message "OneDrive quota Alert: Unable to get OneDrive usage: Error occurred: $ErrorMessage" + Write-LogMessage -API 'Alerts' -tenant $($TenantFilter) -message "OneDrive quota Alert: Unable to get OneDrive usage: Error occurred: $ErrorMessage" -sev Error return } diff --git a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertOverusedLicenses.ps1 b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertOverusedLicenses.ps1 index 69e4f0254a84..f0b87d48ac7c 100644 --- a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertOverusedLicenses.ps1 +++ b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertOverusedLicenses.ps1 @@ -39,6 +39,6 @@ function Get-CIPPAlertOverusedLicenses { } } catch { - Write-AlertMessage -tenant $($TenantFilter) -message "Overused Licenses Alert Error occurred: $(Get-NormalizedError -message $_.Exception.message)" + Write-LogMessage -API 'Alerts' -tenant $($TenantFilter) -message "Overused Licenses Alert Error occurred: $(Get-NormalizedError -message $_.Exception.message)" -sev Error } } diff --git a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertReportOnlyCA.ps1 b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertReportOnlyCA.ps1 index 6205c9b6ac40..2ce381def049 100644 --- a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertReportOnlyCA.ps1 +++ b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertReportOnlyCA.ps1 @@ -36,7 +36,7 @@ function Get-CIPPAlertReportOnlyCA { } } } catch { - Write-AlertMessage -tenant $($TenantFilter) -message "Report-Only CA Alert: Error occurred: $(Get-NormalizedError -message $_.Exception.message)" + Write-LogMessage -API 'Alerts' -tenant $($TenantFilter) -message "Report-Only CA Alert: Error occurred: $(Get-NormalizedError -message $_.Exception.message)" -sev Error } } diff --git a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertSecDefaultsDisabled.ps1 b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertSecDefaultsDisabled.ps1 index 104cf17e03fa..a8055e2c6261 100644 --- a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertSecDefaultsDisabled.ps1 +++ b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertSecDefaultsDisabled.ps1 @@ -30,6 +30,6 @@ function Get-CIPPAlertSecDefaultsDisabled { } } } catch { - Write-AlertMessage -tenant $($TenantFilter) -message "Security Defaults Disabled Alert: Error occurred: $(Get-NormalizedError -message $_.Exception.message)" + Write-LogMessage -API 'Alerts' -tenant $($TenantFilter) -message "Security Defaults Disabled Alert: Error occurred: $(Get-NormalizedError -message $_.Exception.message)" -sev Error } } diff --git a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertSecureScore.ps1 b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertSecureScore.ps1 index 4bf49b56bbc7..a48a163d8532 100644 --- a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertSecureScore.ps1 +++ b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertSecureScore.ps1 @@ -41,6 +41,6 @@ function Get-CippAlertSecureScore { } Write-AlertTrace -cmdletName $MyInvocation.MyCommand -tenantFilter $TenantFilter -data $SecureScoreResult -PartitionKey SecureScore } catch { - Write-AlertMessage -tenant $($TenantFilter) -message "Could not get Secure Score for $($TenantFilter): $(Get-NormalizedError -message $_.Exception.message)" + Write-LogMessage -API 'Alerts' -tenant $($TenantFilter) -message "Could not get Secure Score for $($TenantFilter): $(Get-NormalizedError -message $_.Exception.message)" -sev Error } } diff --git a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertSoftDeletedMailboxes.ps1 b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertSoftDeletedMailboxes.ps1 index f446ec2e9161..b8ea70697c90 100644 --- a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertSoftDeletedMailboxes.ps1 +++ b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertSoftDeletedMailboxes.ps1 @@ -24,6 +24,6 @@ function Get-CIPPAlertSoftDeletedMailboxes { Write-AlertTrace -cmdletName $MyInvocation.MyCommand -tenantFilter $TenantFilter -data $AlertData } catch { - Write-AlertMessage -tenant $($TenantFilter) -message "Failed to check for soft deleted mailboxes in $($TenantFilter): $(Get-NormalizedError -message $_.Exception.message)" + Write-LogMessage -API 'Alerts' -tenant $($TenantFilter) -message "Failed to check for soft deleted mailboxes in $($TenantFilter): $(Get-NormalizedError -message $_.Exception.message)" -sev Error } } diff --git a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertStaleEntraDevices.ps1 b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertStaleEntraDevices.ps1 index 29043c3288fd..f44bcd016905 100644 --- a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertStaleEntraDevices.ps1 +++ b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertStaleEntraDevices.ps1 @@ -78,6 +78,6 @@ function Get-CIPPAlertStaleEntraDevices { Write-AlertTrace -cmdletName $MyInvocation.MyCommand -tenantFilter $TenantFilter -data $AlertData } catch {} } catch { - Write-AlertMessage -tenant $($TenantFilter) -message "Failed to check inactive guest users for $($TenantFilter): $(Get-NormalizedError -message $_.Exception.message)" + Write-LogMessage -API 'Alerts' -tenant $($TenantFilter) -message "Failed to check inactive guest users for $($TenantFilter): $(Get-NormalizedError -message $_.Exception.message)" -sev Error } } diff --git a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertUnusedLicenses.ps1 b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertUnusedLicenses.ps1 index 4db4e400eb1e..7f470e07d9a1 100644 --- a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertUnusedLicenses.ps1 +++ b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertUnusedLicenses.ps1 @@ -35,6 +35,6 @@ function Get-CIPPAlertUnusedLicenses { } Write-AlertTrace -cmdletName $MyInvocation.MyCommand -tenantFilter $TenantFilter -data $AlertData } catch { - Write-AlertMessage -tenant $($TenantFilter) -message "Unused Licenses Alert Error occurred: $(Get-NormalizedError -message $_.Exception.message)" + Write-LogMessage -API 'Alerts' -tenant $($TenantFilter) -message "Unused Licenses Alert Error occurred: $(Get-NormalizedError -message $_.Exception.message)" -sev Error } } From fd029ddd53938ab793d22aeefce2351700186891 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Sat, 14 Feb 2026 18:47:22 -0500 Subject: [PATCH 412/503] Use Graph bulk requests for admin password checks Avoid per-assignment GETs (which caused rate limiting) by fetching role assignments without expanding principal, building a bulk GET request for each principalId, and calling New-GraphBulkRequest to retrieve id, UserPrincipalName, and lastPasswordChangeDateTime for users. Filter results for password changes within the last 24 hours, sort by UserPrincipalName to prevent duplicate alerts, and fall back to an empty array when there are no user requests. Trace and error logging behavior is preserved. --- .../Alerts/Get-CIPPAlertAdminPassword.ps1 | 27 ++++++++++++++++--- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertAdminPassword.ps1 b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertAdminPassword.ps1 index a0b6888e2046..b401c18b83b2 100644 --- a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertAdminPassword.ps1 +++ b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertAdminPassword.ps1 @@ -13,12 +13,31 @@ function Get-CIPPAlertAdminPassword { ) try { $TenantId = (Get-Tenants | Where-Object -Property defaultDomainName -EQ $TenantFilter).customerId - $AlertData = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/roleManagement/directory/roleAssignments?`$filter=roleDefinitionId eq '62e90394-69f5-4237-9190-012177145e10'&`$expand=principal" -tenantid $($TenantFilter) | Where-Object { ($_.principalOrganizationId -EQ $TenantId) -and ($_.principal.'@odata.type' -eq '#microsoft.graph.user') } | ForEach-Object { - $LastChanges = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/users/$($_.principalId)?`$select=UserPrincipalName,lastPasswordChangeDateTime" -tenant $($TenantFilter) - if ($LastChanges.LastPasswordChangeDateTime -gt (Get-Date).AddDays(-1)) { - $LastChanges | Select-Object -Property UserPrincipalName, lastPasswordChangeDateTime + + # Get role assignments without expanding principal to avoid rate limiting + $RoleAssignments = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/roleManagement/directory/roleAssignments?`$filter=roleDefinitionId eq '62e90394-69f5-4237-9190-012177145e10'" -tenantid $($TenantFilter) | Where-Object { $_.principalOrganizationId -EQ $TenantId } + + # Build bulk requests for each principalId + $UserRequests = $RoleAssignments | ForEach-Object { + [PSCustomObject]@{ + id = $_.principalId + method = 'GET' + url = "users/$($_.principalId)?`$select=id,UserPrincipalName,lastPasswordChangeDateTime" } } + + # Make bulk call to get user information + if ($UserRequests) { + $BulkResults = New-GraphBulkRequest -Requests @($UserRequests) -tenantid $TenantFilter + + # Filter users with recent password changes and sort to prevent duplicate alerts + $AlertData = $BulkResults | Where-Object { $_.status -eq 200 -and $_.body.lastPasswordChangeDateTime -gt (Get-Date).AddDays(-1) } | ForEach-Object { + $_.body | Select-Object -Property UserPrincipalName, lastPasswordChangeDateTime + } | Sort-Object UserPrincipalName + } else { + $AlertData = @() + } + Write-AlertTrace -cmdletName $MyInvocation.MyCommand -tenantFilter $TenantFilter -data $AlertData } catch { Write-LogMessage -API 'Alerts' -tenant $($TenantFilter) -message "Could not get admin password changes for $($TenantFilter): $(Get-NormalizedError -message $_.Exception.message)" -sev Error From b81ecbb8cd1ba406ef8ce7742c4dc370eaaadcd8 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Sat, 14 Feb 2026 20:42:00 -0500 Subject: [PATCH 413/503] Add permission cache sync and Append support Introduce calendar/mailbox permission cache syncing and related utilities; keep reporting DB in sync when permissions are changed. - Add Sync-CIPPMailboxPermissionCache and Sync-CIPPCalendarPermissionCache to update cached MailboxPermissions/CalendarPermissions entries on Add/Remove. - Add Remove-CIPPCalendarPermissions helper to remove calendar permissions (supports cache-driven bulk removal and per-calendar removal). - Update Remove-CIPPMailboxPermissions to support -UseCache (bulk removal via cached report), and to call Sync-CIPPMailboxPermissionCache after permission changes; improved logging when permissions already absent. - Update Set-CIPPCalendarPermission and Invoke-ExecEditMailboxPermissions to call the cache sync functions after add/remove operations. - Enhance Add-CIPPDbItem with a new -Append switch to add items without clearing existing entries and to optionally increment stored counts when used with -AddCount. - Minor report/log tweaks: include FolderName in Get-CIPPCalendarPermissionReport output and reduce Get-CIPPMailboxPermissionReport startup log severity to Debug. - Simplify offboarding flow to remove mailbox/calendar permissions via the new cache-aware functions. These changes ensure permission changes performed by CIPP are reflected in the cached reporting DB and allow incremental appends for reporting data. --- Modules/CIPPCore/Public/Add-CIPPDbItem.ps1 | 42 ++++- .../Invoke-ExecEditMailboxPermissions.ps1 | 17 +- .../Users/Invoke-CIPPOffboardingJob.ps1 | 28 +-- .../Get-CIPPCalendarPermissionReport.ps1 | 3 + .../Get-CIPPMailboxPermissionReport.ps1 | 2 +- .../Public/Remove-CIPPCalendarPermissions.ps1 | 177 ++++++++++++++++++ .../Public/Remove-CIPPMailboxPermissions.ps1 | 59 +++++- .../Public/Set-CIPPCalendarPermission.ps1 | 6 + .../Sync-CIPPCalendarPermissionCache.ps1 | 173 +++++++++++++++++ .../Sync-CIPPMailboxPermissionCache.ps1 | 157 ++++++++++++++++ 10 files changed, 632 insertions(+), 32 deletions(-) create mode 100644 Modules/CIPPCore/Public/Remove-CIPPCalendarPermissions.ps1 create mode 100644 Modules/CIPPCore/Public/Sync-CIPPCalendarPermissionCache.ps1 create mode 100644 Modules/CIPPCore/Public/Sync-CIPPMailboxPermissionCache.ps1 diff --git a/Modules/CIPPCore/Public/Add-CIPPDbItem.ps1 b/Modules/CIPPCore/Public/Add-CIPPDbItem.ps1 index be862eabe375..9dd5a5af2abf 100644 --- a/Modules/CIPPCore/Public/Add-CIPPDbItem.ps1 +++ b/Modules/CIPPCore/Public/Add-CIPPDbItem.ps1 @@ -22,6 +22,10 @@ function Add-CIPPDbItem { .PARAMETER AddCount If specified, automatically records the total count after processing all items + .PARAMETER Append + If specified, adds items without clearing existing entries for this type/tenant and automatically + increments the count. Useful for accumulating report data over time. By default, existing entries are replaced. + .EXAMPLE Add-CIPPDbItem -TenantFilter 'contoso.onmicrosoft.com' -Type 'Groups' -Data $GroupsData @@ -30,6 +34,9 @@ function Add-CIPPDbItem { .EXAMPLE Add-CIPPDbItem -TenantFilter 'contoso.onmicrosoft.com' -Type 'Groups' -Data $GroupsData -Count + + .EXAMPLE + Add-CIPPDbItem -TenantFilter 'contoso.onmicrosoft.com' -Type 'AlertHistory' -Data $AlertData -Append -AddCount #> [CmdletBinding()] param( @@ -49,7 +56,10 @@ function Add-CIPPDbItem { [switch]$Count, [Parameter(Mandatory = $false)] - [switch]$AddCount + [switch]$AddCount, + + [Parameter(Mandatory = $false)] + [switch]$Append ) begin { @@ -104,7 +114,7 @@ function Add-CIPPDbItem { } } - if (-not $Count.IsPresent) { + if (-not $Count.IsPresent -and -not $Append.IsPresent) { # Delete existing entries for this type $Filter = "PartitionKey eq '{0}' and RowKey ge '{1}-' and RowKey lt '{1}0'" -f $TenantFilter, $Type $ExistingEntities = Get-CIPPAzDataTableEntity @Table -Filter $Filter -Property PartitionKey, RowKey, ETag @@ -168,18 +178,29 @@ function Add-CIPPDbItem { Invoke-FlushBatch -State $State } - if ($Count.IsPresent) { + if ($Count.IsPresent -or $Append.IsPresent) { # Store count record + if ($Append.IsPresent) { + # When appending, always increment the existing count + $Filter = "PartitionKey eq '{0}' and RowKey eq '{1}-Count'" -f $TenantFilter, $Type + $ExistingCount = Get-CIPPAzDataTableEntity @Table -Filter $Filter + $PreviousCount = if ($ExistingCount -and $ExistingCount.DataCount) { [int]$ExistingCount.DataCount } else { 0 } + $NewCount = $PreviousCount + $State.TotalProcessed + } else { + # Normal mode - replace count + $NewCount = $State.TotalProcessed + } + $Entity = @{ PartitionKey = $TenantFilter RowKey = Format-RowKey "$Type-Count" - DataCount = [int]$State.TotalProcessed + DataCount = [int]$NewCount } Add-CIPPAzDataTableEntity @Table -Entity $Entity -Force | Out-Null } Write-LogMessage -API 'CIPPDbItem' -tenant $TenantFilter ` - -message "Added $($State.TotalProcessed) items of type $Type$(if ($Count.IsPresent) { ' (count mode)' })" -sev Debug + -message "Added $($State.TotalProcessed) items of type $Type$(if ($Count.IsPresent) { ' (count mode)' })$(if ($Append.IsPresent) { ' (append mode)' })" -sev Debug } catch { Write-LogMessage -API 'CIPPDbItem' -tenant $TenantFilter ` @@ -191,7 +212,16 @@ function Add-CIPPDbItem { # Record count if AddCount was specified if ($AddCount.IsPresent -and $State.TotalProcessed -gt 0) { try { - Add-CIPPDbItem -TenantFilter $TenantFilter -Type $Type -InputObject $State.TotalProcessed -Count + $countParams = @{ + TenantFilter = $TenantFilter + Type = $Type + InputObject = $State.TotalProcessed + Count = $true + } + if ($Append.IsPresent) { + $countParams.Append = $true + } + Add-CIPPDbItem @countParams } catch { Write-LogMessage -API 'CIPPDbItem' -tenant $TenantFilter ` -message "Failed to record count for $Type : $($_.Exception.Message)" -sev Warning diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecEditMailboxPermissions.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecEditMailboxPermissions.ps1 index ee36d68eb6b2..00d696892b7c 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecEditMailboxPermissions.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecEditMailboxPermissions.ps1 @@ -1,4 +1,4 @@ -Function Invoke-ExecEditMailboxPermissions { +function Invoke-ExecEditMailboxPermissions { <# .FUNCTIONALITY Entrypoint @@ -23,6 +23,9 @@ Function Invoke-ExecEditMailboxPermissions { $MailboxPerms = New-ExoRequest -Anchor $username -tenantid $Tenantfilter -cmdlet 'Remove-mailboxpermission' -cmdParams @{Identity = $userid; user = $RemoveUser; accessRights = @('FullAccess'); } $results.add("Removed $($removeuser) from $($username) Shared Mailbox permissions") Write-LogMessage -headers $Request.Headers -API $APINAME-message "Removed $($RemoveUser) from $($username) Shared Mailbox permission" -Sev 'Info' -tenant $TenantFilter + + # Sync cache + Sync-CIPPMailboxPermissionCache -TenantFilter $TenantFilter -MailboxIdentity $username -User $RemoveUser -PermissionType 'FullAccess' -Action 'Remove' } catch { Write-LogMessage -headers $Request.Headers -API $APINAME-message "Could not remove mailbox permissions for $($removeuser) on $($username)" -Sev 'Error' -tenant $TenantFilter $results.add("Could not remove $($removeuser) shared mailbox permissions for $($username). Error: $($_.Exception.Message)") @@ -36,6 +39,9 @@ Function Invoke-ExecEditMailboxPermissions { $results.add( "Granted $($UserAutomap) access to $($username) Mailbox with automapping") Write-LogMessage -headers $Request.Headers -API $APINAME-message "Granted $($UserAutomap) access to $($username) Mailbox with automapping" -Sev 'Info' -tenant $TenantFilter + # Sync cache + Sync-CIPPMailboxPermissionCache -TenantFilter $TenantFilter -MailboxIdentity $username -User $UserAutomap -PermissionType 'FullAccess' -Action 'Add' + } catch { Write-LogMessage -headers $Request.Headers -API $APINAME-message "Could not add mailbox permissions for $($UserAutomap) on $($username)" -Sev 'Error' -tenant $TenantFilter $results.add( "Could not add $($UserAutomap) shared mailbox permissions for $($username). Error: $($_.Exception.Message)") @@ -48,6 +54,9 @@ Function Invoke-ExecEditMailboxPermissions { $MailboxPerms = New-ExoRequest -Anchor $username -tenantid $Tenantfilter -cmdlet 'Add-MailboxPermission' -cmdParams @{Identity = $userid; user = $UserNoAutomap; accessRights = @('FullAccess'); automapping = $false } $results.add( "Granted $UserNoAutomap access to $($username) Mailbox without automapping") Write-LogMessage -headers $Request.Headers -API $APINAME-message "Granted $UserNoAutomap access to $($username) Mailbox without automapping" -Sev 'Info' -tenant $TenantFilter + + # Sync cache + Sync-CIPPMailboxPermissionCache -TenantFilter $TenantFilter -MailboxIdentity $username -User $UserNoAutomap -PermissionType 'FullAccess' -Action 'Add' } catch { Write-LogMessage -headers $Request.Headers -API $APINAME-message "Could not add mailbox permissions for $($UserNoAutomap) on $($username)" -Sev 'Error' -tenant $TenantFilter $results.add("Could not add $($UserNoAutomap) shared mailbox permissions for $($username). Error: $($_.Exception.Message)") @@ -61,6 +70,9 @@ Function Invoke-ExecEditMailboxPermissions { $MailboxPerms = New-ExoRequest -Anchor $username -tenantid $Tenantfilter -cmdlet 'Add-RecipientPermission' -cmdParams @{Identity = $userid; Trustee = $UserSendAs; accessRights = @('SendAs') } $results.add( "Granted $UserSendAs access to $($username) with Send As permissions") Write-LogMessage -headers $Request.Headers -API $APINAME-message "Granted $UserSendAs access to $($username) with Send As permissions" -Sev 'Info' -tenant $TenantFilter + + # Sync cache + Sync-CIPPMailboxPermissionCache -TenantFilter $TenantFilter -MailboxIdentity $username -User $UserSendAs -PermissionType 'SendAs' -Action 'Add' } catch { Write-LogMessage -headers $Request.Headers -API $APINAME-message "Could not add mailbox permissions for $($UserSendAs) on $($username)" -Sev 'Error' -tenant $TenantFilter $results.add("Could not add $($UserSendAs) send-as permissions for $($username). Error: $($_.Exception.Message)") @@ -74,6 +86,9 @@ Function Invoke-ExecEditMailboxPermissions { $MailboxPerms = New-ExoRequest -Anchor $username -tenantid $Tenantfilter -cmdlet 'Remove-RecipientPermission' -cmdParams @{Identity = $userid; Trustee = $UserSendAs; accessRights = @('SendAs') } $results.add( "Removed $UserSendAs from $($username) with Send As permissions") Write-LogMessage -headers $Request.Headers -API $APINAME-message "Removed $UserSendAs from $($username) with Send As permissions" -Sev 'Info' -tenant $TenantFilter + + # Sync cache + Sync-CIPPMailboxPermissionCache -TenantFilter $TenantFilter -MailboxIdentity $username -User $UserSendAs -PermissionType 'SendAs' -Action 'Remove' } catch { Write-LogMessage -headers $Request.Headers -API $APINAME-message "Could not remove mailbox permissions for $($UserSendAs) on $($username)" -Sev 'Error' -tenant $TenantFilter $results.add("Could not remove $($UserSendAs) send-as permissions for $($username). Error: $($_.Exception.Message)") diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-CIPPOffboardingJob.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-CIPPOffboardingJob.ps1 index 8a9a94c0cbd3..94d7e806ccd8 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-CIPPOffboardingJob.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-CIPPOffboardingJob.ps1 @@ -42,7 +42,7 @@ function Invoke-CIPPOffboardingJob { } { $_.HideFromGAL -eq $true } { try { - Set-CIPPHideFromGAL -tenantFilter $TenantFilter -UserID $username -HideFromGAL $true -Headers $Headers -APIName $APIName + Set-CIPPHideFromGAL -tenantFilter $TenantFilter -UserID $username -hidefromgal $true -Headers $Headers -APIName $APIName } catch { $_.Exception.Message } @@ -151,28 +151,10 @@ function Invoke-CIPPOffboardingJob { } } { $_.removePermissions } { - if ($RunScheduled) { - Remove-CIPPMailboxPermissions -PermissionsLevel @('FullAccess', 'SendAs', 'SendOnBehalf') -userid 'AllUsers' -AccessUser $UserName -TenantFilter $TenantFilter -APIName $APINAME -Headers $Headers - - } else { - $Queue = New-CippQueueEntry -Name "Offboarding - Mailbox Permissions: $Username" -TotalTasks 1 - $InputObject = [PSCustomObject]@{ - Batch = @( - [PSCustomObject]@{ - 'FunctionName' = 'ExecOffboardingMailboxPermissions' - 'TenantFilter' = $TenantFilter - 'User' = $Username - 'Headers' = $Headers - 'APINAME' = $APINAME - 'QueueId' = $Queue.RowKey - } - ) - OrchestratorName = "OffboardingMailboxPermissions_$Username" - SkipLog = $true - } - $null = Start-NewOrchestration -FunctionName CIPPOrchestrator -InputObject ($InputObject | ConvertTo-Json -Depth 10) - "Removal of permissions queued. This task will run in the background and send it's results to the logbook." - } + Remove-CIPPMailboxPermissions -AccessUser $Username -TenantFilter $TenantFilter -UseCache -APIName $APIName -Headers $Headers + } + { $_.removeCalendarPermissions } { + Remove-CIPPCalendarPermissions -UserToRemove $Username -TenantFilter $TenantFilter -UseCache -APIName $APIName -Headers $Headers } { $_.RemoveMFADevices -eq $true } { try { diff --git a/Modules/CIPPCore/Public/Get-CIPPCalendarPermissionReport.ps1 b/Modules/CIPPCore/Public/Get-CIPPCalendarPermissionReport.ps1 index 4f7bd038fea4..e6c66c284a25 100644 --- a/Modules/CIPPCore/Public/Get-CIPPCalendarPermissionReport.ps1 +++ b/Modules/CIPPCore/Public/Get-CIPPCalendarPermissionReport.ps1 @@ -185,6 +185,7 @@ function Get-CIPPCalendarPermissionReport { Calendar = $_.MailboxDisplayName CalendarUPN = $_.MailboxUPN AccessRights = $_.AccessRights + FolderName = $_.FolderName } }) @@ -209,6 +210,7 @@ function Get-CIPPCalendarPermissionReport { [PSCustomObject]@{ User = $_.User AccessRights = $_.AccessRights + FolderName = $_.FolderName } }) @@ -216,6 +218,7 @@ function Get-CIPPCalendarPermissionReport { CalendarUPN = $CalendarUPN CalendarDisplayName = $CalendarInfo.MailboxDisplayName CalendarType = $CalendarInfo.MailboxType + FolderName = $CalendarInfo.FolderName PermissionCount = $_.Count Permissions = $PermissionDetails Tenant = $TenantFilter diff --git a/Modules/CIPPCore/Public/Get-CIPPMailboxPermissionReport.ps1 b/Modules/CIPPCore/Public/Get-CIPPMailboxPermissionReport.ps1 index 389dad4ddd1d..c27b00f532fb 100644 --- a/Modules/CIPPCore/Public/Get-CIPPMailboxPermissionReport.ps1 +++ b/Modules/CIPPCore/Public/Get-CIPPMailboxPermissionReport.ps1 @@ -31,7 +31,7 @@ function Get-CIPPMailboxPermissionReport { ) try { - Write-LogMessage -API 'MailboxPermissionReport' -tenant $TenantFilter -message 'Generating mailbox permission report' -sev Info + Write-LogMessage -API 'MailboxPermissionReport' -tenant $TenantFilter -message 'Generating mailbox permission report' -sev Debug # Handle AllTenants if ($TenantFilter -eq 'AllTenants') { diff --git a/Modules/CIPPCore/Public/Remove-CIPPCalendarPermissions.ps1 b/Modules/CIPPCore/Public/Remove-CIPPCalendarPermissions.ps1 new file mode 100644 index 000000000000..402eca0145b9 --- /dev/null +++ b/Modules/CIPPCore/Public/Remove-CIPPCalendarPermissions.ps1 @@ -0,0 +1,177 @@ +function Remove-CIPPCalendarPermissions { + <# + .SYNOPSIS + Remove calendar permissions for a specific user + + .DESCRIPTION + Removes calendar folder permissions for a user from specified calendars or all calendars they have access to + + .PARAMETER UserToRemove + The user whose calendar access should be removed + + .PARAMETER CalendarIdentity + Optional. Specific calendar identity (e.g., "mailbox@domain.com:\Calendar"). If not provided, will query from cache. + + .PARAMETER FolderName + Optional. Folder name (defaults to "Calendar"). Used with CalendarIdentity or when querying from cache. + + .PARAMETER TenantFilter + The tenant to operate on + + .PARAMETER UseCache + If specified, will query cached calendar permissions to find all calendars the user has access to + + .PARAMETER APIName + API name for logging (defaults to 'Remove Calendar Permissions') + + .PARAMETER Headers + Headers for logging + + .EXAMPLE + Remove-CIPPCalendarPermissions -UserToRemove 'user@domain.com' -CalendarIdentity 'mailbox@domain.com:\Calendar' -TenantFilter 'contoso.com' + + .EXAMPLE + Remove-CIPPCalendarPermissions -UserToRemove 'user@domain.com' -TenantFilter 'contoso.com' -UseCache + #> + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string]$UserToRemove, + + [Parameter(Mandatory = $false)] + [string]$CalendarIdentity, + + [Parameter(Mandatory = $false)] + [string]$FolderName = 'Calendar', + + [Parameter(Mandatory = $true)] + [string]$TenantFilter, + + [Parameter(Mandatory = $false)] + [switch]$UseCache, + + [Parameter(Mandatory = $false)] + [string]$APIName = 'Remove Calendar Permissions', + + [Parameter(Mandatory = $false)] + $Headers + ) + + try { + $Results = [System.Collections.Generic.List[string]]::new() + + if ($UseCache) { + # Get all calendars this user has access to from cache + try { + # Resolve user to display name if a UPN was provided + # Calendar permissions use display names, not UPNs + $UserToMatch = $UserToRemove + if ($UserToRemove -match '@') { + # Try to get display name from mailbox cache + $MailboxItems = Get-CIPPDbItem -TenantFilter $TenantFilter -Type 'Mailboxes' | Where-Object { $_.RowKey -ne 'Mailboxes-Count' } + foreach ($Item in $MailboxItems) { + $Mailbox = $Item.Data | ConvertFrom-Json + if ($Mailbox.UPN -eq $UserToRemove -or $Mailbox.primarySmtpAddress -eq $UserToRemove) { + $UserToMatch = $Mailbox.displayName + Write-Information "Resolved $UserToRemove to display name: $UserToMatch" -InformationAction Continue + break + } + } + } + + $CalendarPermissions = Get-CIPPCalendarPermissionReport -TenantFilter $TenantFilter -ByUser | Where-Object { $_.User -eq $UserToMatch } + + if (-not $CalendarPermissions -or $CalendarPermissions.Permissions.Count -eq 0) { + $Message = "No calendar permissions found for $UserToRemove in cached data" + Write-LogMessage -headers $Headers -API $APIName -message $Message -Sev 'Info' -tenant $TenantFilter + return $Message + } + + # Remove from each calendar + foreach ($CalPermEntry in $CalendarPermissions.Permissions) { + try { + $Folder = if ($CalPermEntry.FolderName) { $CalPermEntry.FolderName } else { 'Calendar' } + $CalIdentity = "$($CalPermEntry.CalendarUPN):\$Folder" + + $RemovalResult = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Remove-MailboxFolderPermission' -cmdParams @{ + Identity = $CalIdentity + User = $UserToMatch + } -UseSystemMailbox $true + + # Sync cache regardless of whether permission existed in Exchange + # Cache sync uses flexible matching so it will find and remove the entry + Sync-CIPPCalendarPermissionCache -TenantFilter $TenantFilter -MailboxIdentity $CalPermEntry.CalendarUPN -FolderName $Folder -User $UserToMatch -Action 'Remove' + + $SuccessMsg = "Removed $UserToRemove from calendar $CalIdentity" + Write-LogMessage -headers $Headers -API $APIName -message $SuccessMsg -Sev 'Info' -tenant $TenantFilter + $Results.Add($SuccessMsg) + } catch { + # Sync cache even on error (permission might not exist) + try { + Sync-CIPPCalendarPermissionCache -TenantFilter $TenantFilter -MailboxIdentity $CalPermEntry.CalendarUPN -FolderName $Folder -User $UserToMatch -Action 'Remove' + } catch { + Write-Verbose "Failed to sync cache: $_" + } + + $ErrorMsg = "Failed to remove $UserToRemove from calendar $($CalPermEntry.CalendarUPN): $($_.Exception.Message)" + Write-LogMessage -headers $Headers -API $APIName -message $ErrorMsg -Sev 'Warning' -tenant $TenantFilter + $Results.Add($ErrorMsg) + } + } + + $SummaryMsg = "Processed $($CalendarPermissions.CalendarCount) calendar(s) - removed $($Results.Count) permission(s)" + Write-LogMessage -headers $Headers -API $APIName -message $SummaryMsg -Sev 'Info' -tenant $TenantFilter + return $Results + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -headers $Headers -API $APIName -message "Failed to query calendar permissions from cache: $($ErrorMessage.NormalizedError)" -Sev 'Error' -tenant $TenantFilter -LogData $ErrorMessage + throw "Failed to query calendar permissions from cache: $($ErrorMessage.NormalizedError)" + } + } else { + # Remove from specific calendar + if ([string]::IsNullOrEmpty($CalendarIdentity)) { + throw 'CalendarIdentity is required when not using cache' + } + + try { + $RemovalResult = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Remove-MailboxFolderPermission' -cmdParams @{ + Identity = $CalendarIdentity + User = $UserToRemove + } -UseSystemMailbox $true + + # Sync cache - extract mailbox UPN from identity + $MailboxUPN = if ($CalendarIdentity -match '^([^:]+):') { $Matches[1] } else { $CalendarIdentity } + $Folder = if ($CalendarIdentity -match ':\\(.+)$') { $Matches[1] } else { $FolderName } + + # Sync cache regardless of whether permission existed in Exchange + # Cache sync uses flexible matching so it will find and remove the entry + Sync-CIPPCalendarPermissionCache -TenantFilter $TenantFilter -MailboxIdentity $MailboxUPN -FolderName $Folder -User $UserToRemove -Action 'Remove' + + $SuccessMsg = "Removed $UserToRemove from calendar $CalendarIdentity" + Write-LogMessage -headers $Headers -API $APIName -message $SuccessMsg -Sev 'Info' -tenant $TenantFilter + return $SuccessMsg + + } catch { + # Sync cache even on error (permission might not exist) + $MailboxUPN = if ($CalendarIdentity -match '^([^:]+):') { $Matches[1] } else { $CalendarIdentity } + $Folder = if ($CalendarIdentity -match ':\\(.+)$') { $Matches[1] } else { $FolderName } + + try { + Sync-CIPPCalendarPermissionCache -TenantFilter $TenantFilter -MailboxIdentity $MailboxUPN -FolderName $Folder -User $UserToRemove -Action 'Remove' + } catch { + Write-Verbose "Failed to sync cache: $_" + } + + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -headers $Headers -API $APIName -message "Failed to remove calendar permission: $($ErrorMessage.NormalizedError)" -Sev 'Error' -tenant $TenantFilter -LogData $ErrorMessage + throw "Failed to remove calendar permission: $($ErrorMessage.NormalizedError)" + } + } + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -headers $Headers -API $APIName -message "Could not remove calendar permissions for $UserToRemove. Error: $($ErrorMessage.NormalizedError)" -Sev 'Error' -tenant $TenantFilter -LogData $ErrorMessage + return "Could not remove calendar permissions for $UserToRemove. Error: $($ErrorMessage.NormalizedError)" + } +} diff --git a/Modules/CIPPCore/Public/Remove-CIPPMailboxPermissions.ps1 b/Modules/CIPPCore/Public/Remove-CIPPMailboxPermissions.ps1 index dc934b29088d..0b8927896be9 100644 --- a/Modules/CIPPCore/Public/Remove-CIPPMailboxPermissions.ps1 +++ b/Modules/CIPPCore/Public/Remove-CIPPMailboxPermissions.ps1 @@ -5,12 +5,54 @@ function Remove-CIPPMailboxPermissions { $AccessUser, $TenantFilter, $PermissionsLevel, + + [Parameter(Mandatory = $false)] + [switch]$UseCache, + $APIName = 'Manage Shared Mailbox Access', $Headers ) try { - if ($userid -eq 'AllUsers') { + if ($UseCache.IsPresent) { + # Use cached permission report to find all mailboxes the user has access to + + Write-Information "Accessing cached mailbox permissions for $AccessUser in tenant $TenantFilter" -InformationAction Continue + Write-LogMessage -headers $Headers -API $APIName -message "Removing mailbox permissions for $AccessUser using cached permission report" -Sev 'Info' -tenant $TenantFilter + + $UserPermissions = Get-CIPPMailboxPermissionReport -TenantFilter $TenantFilter -ByUser | Where-Object { $_.User -eq $AccessUser } + + if (-not $UserPermissions -or $UserPermissions.Permissions.Count -eq 0) { + $Message = "No mailbox permissions found for $AccessUser in cached data" + Write-LogMessage -headers $Headers -API $APIName -message $Message -Sev 'Info' -tenant $TenantFilter + return $Message + } + + $Results = [System.Collections.Generic.List[string]]::new() + + # Loop through each mailbox and remove permissions + foreach ($PermissionEntry in $UserPermissions.Permissions) { + $MailboxUPN = $PermissionEntry.MailboxUPN + $AccessRights = $PermissionEntry.AccessRights -split ', ' + + try { + # Recursively call this function without UseCache + $Result = Remove-CIPPMailboxPermissions -userid $MailboxUPN -AccessUser $AccessUser -TenantFilter $TenantFilter -PermissionsLevel $AccessRights -APIName $APIName -Headers $Headers + if ($Result) { + $Results.Add($Result) + } + } catch { + $ErrorMsg = "Failed to remove permissions from $MailboxUPN for $AccessUser : $($_.Exception.Message)" + Write-LogMessage -headers $Headers -API $APIName -message $ErrorMsg -Sev 'Warning' -tenant $TenantFilter + $Results.Add($ErrorMsg) + } + } + + $SummaryMsg = "Processed $($UserPermissions.MailboxCount) mailbox(es) - removed $($Results.Count) permission(s)" + Write-LogMessage -headers $Headers -API $APIName -message $SummaryMsg -Sev 'Info' -tenant $TenantFilter + return $Results + + } elseif ($userid -eq 'AllUsers') { $Mailboxes = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-Mailbox' -Select UserPrincipalName $Mailboxes | ForEach-Object -Parallel { Import-Module '.\Modules\AzBobbyTables' @@ -20,19 +62,28 @@ function Remove-CIPPMailboxPermissions { } -ThrottleLimit 10 } else { $Results = $PermissionsLevel | ForEach-Object { + Write-Information "Removing $($_) permissions for $AccessUser on mailbox $userid" -InformationAction Continue switch ($_) { 'SendOnBehalf' { $MailboxPerms = New-ExoRequest -Anchor $UserId -tenantid $Tenantfilter -cmdlet 'Set-Mailbox' -cmdParams @{Identity = $userid; GrantSendonBehalfTo = @{'@odata.type' = '#Exchange.GenericHashTable'; remove = $AccessUser }; } if ($MailboxPerms -notlike '*completed successfully but no settings of*') { Write-LogMessage -headers $Headers -API $APIName -message "Removed SendOnBehalf permissions for $($AccessUser) from $($userid)'s mailbox." -Sev 'Info' -tenant $TenantFilter + # Note: SendOnBehalf not cached as separate permission "Removed SendOnBehalf permissions for $($AccessUser) from $($userid)'s mailbox." } } 'SendAS' { $MailboxPerms = New-ExoRequest -Anchor $userId -tenantid $Tenantfilter -cmdlet 'Remove-RecipientPermission' -cmdParams @{Identity = $userid; Trustee = $AccessUser; accessRights = @('SendAs') } + + # Sync cache regardless of whether permission existed + Sync-CIPPMailboxPermissionCache -TenantFilter $TenantFilter -MailboxIdentity $userid -User $AccessUser -PermissionType 'SendAs' -Action 'Remove' + if ($MailboxPerms -notlike "*because the ACE isn't present*") { Write-LogMessage -headers $Headers -API $APIName -message "Removed SendAs permissions for $($AccessUser) from $($userid)'s mailbox." -Sev 'Info' -tenant $TenantFilter "Removed SendAs permissions for $($AccessUser) from $($userid)'s mailbox." + } else { + Write-LogMessage -headers $Headers -API $APIName -message "SendAs permissions for $($AccessUser) on $($userid)'s mailbox were already removed or don't exist." -Sev 'Info' -tenant $TenantFilter + "SendAs permissions for $($AccessUser) on $($userid)'s mailbox were already removed or don't exist." } } 'FullAccess' { @@ -49,9 +100,15 @@ function Remove-CIPPMailboxPermissions { } $permissions = New-ExoRequest @ExoRequest + # Sync cache regardless of whether permission existed + Sync-CIPPMailboxPermissionCache -TenantFilter $TenantFilter -MailboxIdentity $userid -User $AccessUser -PermissionType 'FullAccess' -Action 'Remove' + if ($permissions -notlike "*because the ACE doesn't exist on the object.*") { Write-LogMessage -headers $Headers -API $APIName -message "Removed FullAccess permissions for $($AccessUser) from $($userid)'s mailbox." -Sev 'Info' -tenant $TenantFilter "Removed FullAccess permissions for $($AccessUser) from $($userid)'s mailbox." + } else { + Write-LogMessage -headers $Headers -API $APIName -message "FullAccess permissions for $($AccessUser) on $($userid)'s mailbox were already removed or don't exist." -Sev 'Info' -tenant $TenantFilter + "FullAccess permissions for $($AccessUser) on $($userid)'s mailbox were already removed or don't exist." } } } diff --git a/Modules/CIPPCore/Public/Set-CIPPCalendarPermission.ps1 b/Modules/CIPPCore/Public/Set-CIPPCalendarPermission.ps1 index 57d2c4695680..adba643d3f2b 100644 --- a/Modules/CIPPCore/Public/Set-CIPPCalendarPermission.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPCalendarPermission.ps1 @@ -38,6 +38,9 @@ function Set-CIPPCalendarPermission { $null = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Remove-MailboxFolderPermission' -cmdParams @{Identity = "$($UserID):\$FolderName"; User = $RemoveAccess } $Result = "Successfully removed access for $LoggingName from calendar $($CalParam.Identity)" Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $Result -sev Info + + # Sync cache + Sync-CIPPCalendarPermissionCache -TenantFilter $TenantFilter -MailboxIdentity $UserID -FolderName $FolderName -User $RemoveAccess -Action 'Remove' } } else { if ($PSCmdlet.ShouldProcess("$UserID\$FolderName", "Set permissions for $LoggingName to $Permissions")) { @@ -54,6 +57,9 @@ function Set-CIPPCalendarPermission { $Result += ' A notification has been sent to the user.' } Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $Result -sev Info + + # Sync cache + Sync-CIPPCalendarPermissionCache -TenantFilter $TenantFilter -MailboxIdentity $UserID -FolderName $FolderName -User $UserToGetPermissions -Permissions $Permissions -Action 'Add' } } } catch { diff --git a/Modules/CIPPCore/Public/Sync-CIPPCalendarPermissionCache.ps1 b/Modules/CIPPCore/Public/Sync-CIPPCalendarPermissionCache.ps1 new file mode 100644 index 000000000000..fc59916b8fcc --- /dev/null +++ b/Modules/CIPPCore/Public/Sync-CIPPCalendarPermissionCache.ps1 @@ -0,0 +1,173 @@ +function Sync-CIPPCalendarPermissionCache { + <# + .SYNOPSIS + Synchronize calendar permission changes to the cached reporting database + + .DESCRIPTION + Updates the cached calendar permissions in the reporting database when permissions are + added or removed via CIPP, keeping the cache in sync with actual permissions. + + .PARAMETER TenantFilter + The tenant domain or GUID + + .PARAMETER MailboxIdentity + The mailbox identity (UPN or email) + + .PARAMETER FolderName + The calendar folder name + + .PARAMETER User + The user being granted or removed permissions + + .PARAMETER Permissions + The permission level being granted + + .PARAMETER Action + Whether to 'Add' or 'Remove' the permission + + .EXAMPLE + Sync-CIPPCalendarPermissionCache -TenantFilter 'contoso.com' -MailboxIdentity 'user@contoso.com' -FolderName 'Calendar' -User 'guest@contoso.com' -Permissions 'Editor' -Action 'Add' + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter, + + [Parameter(Mandatory = $true)] + [string]$MailboxIdentity, + + [Parameter(Mandatory = $true)] + [string]$FolderName, + + [Parameter(Mandatory = $true)] + [string]$User, + + [Parameter(Mandatory = $false)] + [string]$Permissions, + + [Parameter(Mandatory = $true)] + [ValidateSet('Add', 'Remove')] + [string]$Action + ) + + try { + $CalendarIdentity = "$MailboxIdentity`:\$FolderName" + + # Resolve user to display name if a UPN was provided + # Calendar permissions use display names, not UPNs + $UserToCache = $User + if ($User -match '@') { + # Try to get display name from mailbox cache + $MailboxItems = Get-CIPPDbItem -TenantFilter $TenantFilter -Type 'Mailboxes' | Where-Object { $_.RowKey -ne 'Mailboxes-Count' } + foreach ($Item in $MailboxItems) { + $Mailbox = $Item.Data | ConvertFrom-Json + if ($Mailbox.UPN -eq $User -or $Mailbox.primarySmtpAddress -eq $User) { + $UserToCache = $Mailbox.displayName + Write-Information "Resolved $User to display name: $UserToCache" -InformationAction Continue + break + } + } + } + + if ($Action -eq 'Add') { + # Create calendar permission object in the same format as cached permissions + $PermissionObject = [PSCustomObject]@{ + id = [guid]::NewGuid().ToString() + Identity = $CalendarIdentity + User = $UserToCache + AccessRights = $Permissions + FolderName = $FolderName + } + + # Add to cache using Append to not clear existing entries + $PermissionObject | Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'CalendarPermissions' -Append + + Write-LogMessage -API 'CalendarPermissionCache' -tenant $TenantFilter ` + -message "Added calendar permission cache entry: $UserToCache on $CalendarIdentity with $Permissions" -sev Debug + + } else { + # Remove from cache - need to find the item by Identity and User combination + try { + $Table = Get-CippTable -tablename 'CippReportingDB' + + # Build mailbox lookup for flexible Identity matching (same as report function) + $MailboxItems = Get-CIPPDbItem -TenantFilter $TenantFilter -Type 'Mailboxes' | Where-Object { $_.RowKey -ne 'Mailboxes-Count' } + $MailboxLookup = @{} + $MailboxByIdLookup = @{} + $MailboxByExternalIdLookup = @{} + + foreach ($Item in $MailboxItems) { + $Mailbox = $Item.Data | ConvertFrom-Json + if ($Mailbox.UPN) { + $MailboxLookup[$Mailbox.UPN.ToLower()] = @{ + UPN = $Mailbox.UPN + Id = $Mailbox.Id + ExternalDirectoryObjectId = $Mailbox.ExternalDirectoryObjectId + } + } + if ($Mailbox.primarySmtpAddress) { + $MailboxLookup[$Mailbox.primarySmtpAddress.ToLower()] = @{ + UPN = if ($Mailbox.UPN) { $Mailbox.UPN } else { $Mailbox.primarySmtpAddress } + Id = $Mailbox.Id + ExternalDirectoryObjectId = $Mailbox.ExternalDirectoryObjectId + } + } + if ($Mailbox.Id) { + $MailboxByIdLookup[$Mailbox.Id] = $Mailbox.UPN + } + if ($Mailbox.ExternalDirectoryObjectId) { + $MailboxByExternalIdLookup[$Mailbox.ExternalDirectoryObjectId] = $Mailbox.UPN + } + } + + # Get all possible identifiers for the target mailbox + $TargetMailboxInfo = $MailboxLookup[$MailboxIdentity.ToLower()] + $PossibleIdentities = @($MailboxIdentity) + if ($TargetMailboxInfo) { + if ($TargetMailboxInfo.Id) { $PossibleIdentities += $TargetMailboxInfo.Id } + if ($TargetMailboxInfo.ExternalDirectoryObjectId) { $PossibleIdentities += $TargetMailboxInfo.ExternalDirectoryObjectId } + if ($TargetMailboxInfo.UPN) { $PossibleIdentities += $TargetMailboxInfo.UPN } + } + + # Build all possible calendar identities (combining each mailbox identifier with folder name) + $PossibleCalendarIdentities = $PossibleIdentities | ForEach-Object { "$_`:\$FolderName" } + + # Query for all CalendarPermissions for this tenant + $Filter = "PartitionKey eq '{0}' and RowKey ge 'CalendarPermissions-' and RowKey lt 'CalendarPermissions0'" -f $TenantFilter + $AllPermissions = Get-CIPPAzDataTableEntity @Table -Filter $Filter | Where-Object { $_.RowKey -ne 'CalendarPermissions-Count' } + + # Find the specific permission entry that matches + foreach ($CachedPerm in $AllPermissions) { + # Skip entries with null or empty Data + if ([string]::IsNullOrEmpty($CachedPerm.Data)) { + continue + } + + $PermData = $CachedPerm.Data | ConvertFrom-Json + + # Match on Identity (flexible) and User + if ($PossibleCalendarIdentities -contains $PermData.Identity -and $PermData.User -eq $User) { + + # Extract ItemId from RowKey (format: "Type-ItemId") + Write-Information "Removing calendar permission cache entry: $User on $CalendarIdentity (matched via $($PermData.Identity))" -InformationAction Continue + $ItemId = $CachedPerm.RowKey -replace '^CalendarPermissions-', '' + Remove-CIPPDbItem -TenantFilter $TenantFilter -Type 'CalendarPermissions' -ItemId $ItemId + + Write-Information "Removed calendar permission cache entry: $User on $CalendarIdentity" -InformationAction Continue + Write-LogMessage -API 'CalendarPermissionCache' -tenant $TenantFilter ` + -message "Removed calendar permission cache entry: $User on $CalendarIdentity" -sev Debug + break + } + } + } catch { + Write-LogMessage -API 'CalendarPermissionCache' -tenant $TenantFilter ` + -message "Failed to remove calendar permission cache entry: $($_.Exception.Message)" -sev Warning + Write-Information "Failed to remove calendar permission cache entry: $($_.Exception.Message)" -InformationAction Continue + } + } + } catch { + Write-LogMessage -API 'CalendarPermissionCache' -tenant $TenantFilter ` + -message "Failed to sync calendar permission cache: $($_.Exception.Message)" -sev Warning + # Don't throw - cache sync failures shouldn't break the main operation + } +} diff --git a/Modules/CIPPCore/Public/Sync-CIPPMailboxPermissionCache.ps1 b/Modules/CIPPCore/Public/Sync-CIPPMailboxPermissionCache.ps1 new file mode 100644 index 000000000000..59e0fa504ee3 --- /dev/null +++ b/Modules/CIPPCore/Public/Sync-CIPPMailboxPermissionCache.ps1 @@ -0,0 +1,157 @@ +function Sync-CIPPMailboxPermissionCache { + <# + .SYNOPSIS + Synchronize mailbox permission changes to the cached reporting database + + .DESCRIPTION + Updates the cached mailbox permissions in the reporting database when permissions are + added or removed via CIPP, keeping the cache in sync with actual permissions. + + .PARAMETER TenantFilter + The tenant domain or GUID + + .PARAMETER MailboxIdentity + The mailbox identity (UPN or email) + + .PARAMETER User + The user/trustee being granted or removed permissions + + .PARAMETER PermissionType + The type of permission: 'FullAccess', 'SendAs', or 'SendOnBehalf' + + .PARAMETER Action + Whether to 'Add' or 'Remove' the permission + + .EXAMPLE + Sync-CIPPMailboxPermissionCache -TenantFilter 'contoso.com' -MailboxIdentity 'mailbox@contoso.com' -User 'user@contoso.com' -PermissionType 'FullAccess' -Action 'Add' + + .EXAMPLE + Sync-CIPPMailboxPermissionCache -TenantFilter 'contoso.com' -MailboxIdentity 'mailbox@contoso.com' -User 'user@contoso.com' -PermissionType 'SendAs' -Action 'Remove' + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter, + + [Parameter(Mandatory = $true)] + [string]$MailboxIdentity, + + [Parameter(Mandatory = $true)] + [string]$User, + + [Parameter(Mandatory = $true)] + [ValidateSet('FullAccess', 'SendAs', 'SendOnBehalf')] + [string]$PermissionType, + + [Parameter(Mandatory = $true)] + [ValidateSet('Add', 'Remove')] + [string]$Action + ) + + try { + if ($Action -eq 'Add') { + # Create permission object in the same format as cached permissions + $PermissionObject = [PSCustomObject]@{ + id = [guid]::NewGuid().ToString() + Identity = $MailboxIdentity + User = $User + AccessRights = @($PermissionType) + IsInherited = $false + Deny = $false + } + + # Determine which type to use based on permission + $Type = if ($PermissionType -eq 'SendAs') { 'MailboxPermissions' } else { 'MailboxPermissions' } + + # Add to cache using Append to not clear existing entries + $PermissionObject | Add-CIPPDbItem -TenantFilter $TenantFilter -Type $Type -Append + + Write-LogMessage -API 'MailboxPermissionCache' -tenant $TenantFilter ` + -message "Added $PermissionType permission cache entry: $User on $MailboxIdentity" -sev Debug + + } else { + # Remove from cache - need to find the item by Identity and User combination + try { + $Table = Get-CippTable -tablename 'CippReportingDB' + + # Build mailbox lookup for flexible Identity matching (same as report function) + $MailboxItems = Get-CIPPDbItem -TenantFilter $TenantFilter -Type 'Mailboxes' | Where-Object { $_.RowKey -ne 'Mailboxes-Count' } + $MailboxLookup = @{} + $MailboxByIdLookup = @{} + $MailboxByExternalIdLookup = @{} + + foreach ($Item in $MailboxItems) { + $Mailbox = $Item.Data | ConvertFrom-Json + if ($Mailbox.UPN) { + $MailboxLookup[$Mailbox.UPN.ToLower()] = @{ + UPN = $Mailbox.UPN + Id = $Mailbox.Id + ExternalDirectoryObjectId = $Mailbox.ExternalDirectoryObjectId + } + } + if ($Mailbox.primarySmtpAddress) { + $MailboxLookup[$Mailbox.primarySmtpAddress.ToLower()] = @{ + UPN = if ($Mailbox.UPN) { $Mailbox.UPN } else { $Mailbox.primarySmtpAddress } + Id = $Mailbox.Id + ExternalDirectoryObjectId = $Mailbox.ExternalDirectoryObjectId + } + } + if ($Mailbox.Id) { + $MailboxByIdLookup[$Mailbox.Id] = $Mailbox.UPN + } + if ($Mailbox.ExternalDirectoryObjectId) { + $MailboxByExternalIdLookup[$Mailbox.ExternalDirectoryObjectId] = $Mailbox.UPN + } + } + + # Get all possible identifiers for the target mailbox + $TargetMailboxInfo = $MailboxLookup[$MailboxIdentity.ToLower()] + $PossibleIdentities = @($MailboxIdentity) + if ($TargetMailboxInfo) { + if ($TargetMailboxInfo.Id) { $PossibleIdentities += $TargetMailboxInfo.Id } + if ($TargetMailboxInfo.ExternalDirectoryObjectId) { $PossibleIdentities += $TargetMailboxInfo.ExternalDirectoryObjectId } + if ($TargetMailboxInfo.UPN) { $PossibleIdentities += $TargetMailboxInfo.UPN } + } + + # Query for all MailboxPermissions for this tenant + $Filter = "PartitionKey eq '{0}' and RowKey ge 'MailboxPermissions-' and RowKey lt 'MailboxPermissions0'" -f $TenantFilter + $AllPermissions = Get-CIPPAzDataTableEntity @Table -Filter $Filter | Where-Object { $_.RowKey -ne 'MailboxPermissions-Count' } + + # Find the specific permission entry that matches + foreach ($CachedPerm in $AllPermissions) { + # Skip entries with null or empty Data + if ([string]::IsNullOrEmpty($CachedPerm.Data)) { + continue + } + + $PermData = $CachedPerm.Data | ConvertFrom-Json + + # Match on Identity (flexible), User, and AccessRights + if ($PossibleIdentities -contains $PermData.Identity -and + $PermData.User -eq $User -and + $PermData.AccessRights -contains $PermissionType) { + + # Extract ItemId from RowKey (format: "Type-ItemId") + Write-Information "Removing $PermissionType permission cache entry: $User on $MailboxIdentity (matched via $($PermData.Identity))" -InformationAction Continue + $ItemId = $CachedPerm.RowKey -replace '^MailboxPermissions-', '' + Remove-CIPPDbItem -TenantFilter $TenantFilter -Type 'MailboxPermissions' -ItemId $ItemId + + Write-Information "Removed $PermissionType permission cache entry: $User on $MailboxIdentity" -InformationAction Continue + Write-LogMessage -API 'MailboxPermissionCache' -tenant $TenantFilter ` + -message "Removed $PermissionType permission cache entry: $User on $MailboxIdentity" -sev Debug + break + } + } + } catch { + Write-LogMessage -API 'MailboxPermissionCache' -tenant $TenantFilter ` + -message "Failed to remove permission cache entry: $($_.Exception.Message)" -sev Warning + Write-Information "Failed to remove permission cache entry: $($_.Exception.Message)" -InformationAction Continue + } + } + } catch { + Write-LogMessage -API 'MailboxPermissionCache' -tenant $TenantFilter ` + -message "Failed to sync permission cache: $($_.Exception.Message)" -sev Warning + # Don't throw - cache sync failures shouldn't break the main operation + Write-Information "Failed to sync permission cache: $($_.Exception.Message)" -InformationAction Continue + } +} From 7dfd70dbeb8b2d73edabfc8eccf56ef10236cac5 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Sat, 14 Feb 2026 23:50:43 -0500 Subject: [PATCH 414/503] Use cached domains and adjust orchestrator schedule Replace live Graph API domain queries with cached DB reads in Push-DomainAnalyserTenant: use Get-Tenants -TenantFilter, fetch domains via New-CIPPDbRequest, log and return when no cached data, and filter/clean domains as before. Also update CIPPTimers.json for Start-DomainOrchestrator to run at 03:30 daily and increase its priority from 10 to 22. These changes reduce Graph API calls, rely on cached data for domain analysis, and shift/or reprioritize the orchestrator run time. --- CIPPTimers.json | 4 ++-- .../Domain Analyser/Push-DomainAnalyserTenant.ps1 | 12 ++++++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/CIPPTimers.json b/CIPPTimers.json index f76dba8941e2..35586b72833e 100644 --- a/CIPPTimers.json +++ b/CIPPTimers.json @@ -135,8 +135,8 @@ "Id": "c2ebde3f-fa35-45aa-8a6b-91c835050b79", "Command": "Start-DomainOrchestrator", "Description": "Orchestrator to process domains", - "Cron": "0 0 0 * * *", - "Priority": 10, + "Cron": "0 30 3 * * *", + "Priority": 22, "RunOnProcessor": true }, { diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Domain Analyser/Push-DomainAnalyserTenant.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Domain Analyser/Push-DomainAnalyserTenant.ps1 index 43444b4a4101..f3ac78cf719a 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Domain Analyser/Push-DomainAnalyserTenant.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Domain Analyser/Push-DomainAnalyserTenant.ps1 @@ -5,7 +5,7 @@ function Push-DomainAnalyserTenant { #> param($Item) - $Tenant = Get-Tenants -IncludeAll | Where-Object { $_.customerId -eq $Item.customerId } | Select-Object -First 1 + $Tenant = Get-Tenants -TenantFilter $Item.customerId $DomainTable = Get-CippTable -tablename 'Domains' if ($Tenant.Excluded -eq $true) { @@ -20,6 +20,14 @@ function Push-DomainAnalyserTenant { return } else { try { + # Get domains from cached database instead of making Graph API calls + $Domains = New-CIPPDbRequest -TenantFilter $Tenant.defaultDomainName -Type 'Domains' + + if (-not $Domains) { + Write-LogMessage -API 'DomainAnalyser' -tenant $Tenant.defaultDomainName -tenantid $Tenant.customerId -message 'No cached domain data found. Domain analysis will be skipped until data collection completes.' -sev Info + return + } + # Remove domains that are not wanted, and used for cloud signature services. Same exclusions also found in Invoke-CIPPStandardAddDKIM $ExclusionDomains = @( '*.microsoftonline.com' @@ -35,7 +43,7 @@ function Push-DomainAnalyserTenant { '*.ucconnect.co.uk' '*.teams-sbc.dk' ) - $Domains = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/domains' -tenantid $Tenant.customerId | Where-Object { $_.isVerified -eq $true } | ForEach-Object { + $Domains = $Domains | Where-Object { $_.isVerified -eq $true } | ForEach-Object { $Domain = $_ foreach ($ExclusionDomain in $ExclusionDomains) { if ($Domain.id -like $ExclusionDomain) { From a3b63a33ac660060b55860bb3213779f63e1af3b Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Sun, 15 Feb 2026 10:29:41 +0100 Subject: [PATCH 415/503] fixed #5275 --- Modules/CIPPCore/Public/New-CIPPRestoreTask.ps1 | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/New-CIPPRestoreTask.ps1 b/Modules/CIPPCore/Public/New-CIPPRestoreTask.ps1 index aec6db68de03..d750ce6cdea9 100644 --- a/Modules/CIPPCore/Public/New-CIPPRestoreTask.ps1 +++ b/Modules/CIPPCore/Public/New-CIPPRestoreTask.ps1 @@ -200,8 +200,10 @@ function New-CIPPRestoreTask { $backupGroups = if ($BackupData.groups -is [string]) { $BackupData.groups | ConvertFrom-Json } else { $BackupData.groups } $Groups = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/groups?$top=999' -tenantid $TenantFilter $BackupGroups | ForEach-Object { + try { - $JSON = $_ | ConvertTo-Json -Depth 100 -Compress + $CleanObj = Clean-GraphObject $_ + $JSON = $CleanObj | ConvertTo-Json -Depth 100 -Compress $DisplayName = $_.displayName if ($overwrite) { if ($_.id -in $Groups.id) { From 63316dd77d1a04010a26ad06608d50f0b49ae56a Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Sun, 15 Feb 2026 11:09:33 +0100 Subject: [PATCH 416/503] Add group membership change alert --- .../Get-CIPPAlertGroupMembershipChange.ps1 | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 Modules/CIPPCore/Public/Alerts/Get-CIPPAlertGroupMembershipChange.ps1 diff --git a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertGroupMembershipChange.ps1 b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertGroupMembershipChange.ps1 new file mode 100644 index 000000000000..79336a3eed63 --- /dev/null +++ b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertGroupMembershipChange.ps1 @@ -0,0 +1,47 @@ +function Get-CIPPAlertGroupMembershipChange { + <# + .FUNCTIONALITY + Entrypoint + #> + [CmdletBinding()] + param ( + [Parameter(Mandatory = $false)] + [Alias('input')] + $InputValue, + $TenantFilter + ) + + try { + $MonitoredGroups = $InputValue -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ } + if (!$MonitoredGroups) { return $true } + + $OneHourAgo = (Get-Date).AddHours(-3).ToString('yyyy-MM-ddTHH:mm:ssZ') + $AuditLogs = New-GraphGetRequest -uri "https://graph.microsoft.com/v1.0/auditLogs/directoryAudits?`$filter=activityDateTime ge $OneHourAgo and (activityDisplayName eq 'Add member to group' or activityDisplayName eq 'Remove member from group')" -tenantid $TenantFilter + + $AlertData = foreach ($Log in $AuditLogs) { + $Member = ($Log.targetResources | Where-Object { $_.type -in @('User', 'ServicePrincipal') })[0] + $GroupProp = ($Member.modifiedProperties | Where-Object { $_.displayName -eq 'Group.DisplayName' }) + $GroupDisplayName = (($GroupProp.newValue ?? $GroupProp.oldValue) -replace '"', '') + if (!$GroupDisplayName -or !($MonitoredGroups | Where-Object { $GroupDisplayName -like $_ })) { continue } + + $InitiatedBy = if ($Log.initiatedBy.user) { $Log.initiatedBy.user.userPrincipalName } else { $Log.initiatedBy.app.displayName } + $Action = if ($Log.activityDisplayName -eq 'Add member to group') { 'added to' } else { 'removed from' } + + [PSCustomObject]@{ + Message = "$($Member.userPrincipalName ?? $Member.displayName) was $Action group '$GroupDisplayName' by $InitiatedBy" + GroupName = $GroupDisplayName + MemberName = $Member.userPrincipalName ?? $Member.displayName + Action = $Log.activityDisplayName + InitiatedBy = $InitiatedBy + ActivityTime = $Log.activityDateTime + Tenant = $TenantFilter + } + } + + if ($AlertData) { + Write-AlertTrace -cmdletName $MyInvocation.MyCommand -tenantFilter $TenantFilter -data $AlertData + } + } catch { + Write-LogMessage -API 'Alerts' -tenant $TenantFilter -message "Could not check group membership changes for $($TenantFilter): $(Get-NormalizedError -message $_.Exception.message)" -sev Error + } +} From 31f17730ddc0c20decb514e18a3843d1ff03eb58 Mon Sep 17 00:00:00 2001 From: rvdwegen Date: Sun, 15 Feb 2026 20:14:53 +0100 Subject: [PATCH 417/503] DetectedApps --- .../Push-CIPPDBCacheData.ps1 | 1 + .../Invoke-ListDetectedAppDevices.ps1 | 40 ++++++++++ .../Entrypoints/Invoke-ListDetectedApps.ps1 | 78 +++++++++++++++++++ .../Public/Set-CIPPDBCacheDetectedApps.ps1 | 75 ++++++++++++++++++ 4 files changed, 194 insertions(+) create mode 100644 Modules/CIPPCore/Public/Entrypoints/Invoke-ListDetectedAppDevices.ps1 create mode 100644 Modules/CIPPCore/Public/Entrypoints/Invoke-ListDetectedApps.ps1 create mode 100644 Modules/CIPPCore/Public/Set-CIPPDBCacheDetectedApps.ps1 diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 index 6bdf8f889ddf..9351980ef740 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 @@ -158,6 +158,7 @@ function Push-CIPPDBCacheData { 'IntunePolicies' 'ManagedDeviceEncryptionStates' 'IntuneAppProtectionPolicies' + 'DetectedApps' ) foreach ($CacheFunction in $IntuneCacheFunctions) { $Batch.Add(@{ diff --git a/Modules/CIPPCore/Public/Entrypoints/Invoke-ListDetectedAppDevices.ps1 b/Modules/CIPPCore/Public/Entrypoints/Invoke-ListDetectedAppDevices.ps1 new file mode 100644 index 000000000000..e394db309281 --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/Invoke-ListDetectedAppDevices.ps1 @@ -0,0 +1,40 @@ +function Invoke-ListDetectedAppDevices { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Identity.Device.Read + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $TenantFilter = $Request.Query.tenantFilter + $AppID = $Request.Query.AppID + + # Get managed devices where a specific detected app is installed + # Uses deviceManagement/detectedApps/{id}/managedDevices endpoint + + try { + if (-not $AppID) { + throw "AppID parameter is required" + } + + $GraphRequest = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/deviceManagement/detectedApps/$AppID/managedDevices" -Tenantid $TenantFilter + + # Ensure we return an array even if null + if ($null -eq $GraphRequest) { + $GraphRequest = @() + } + + $StatusCode = [HttpStatusCode]::OK + } catch { + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + $StatusCode = [HttpStatusCode]::OK + $GraphRequest = $ErrorMessage + } + + return [HttpResponseContext]@{ + StatusCode = $StatusCode + Body = @($GraphRequest) + } +} diff --git a/Modules/CIPPCore/Public/Entrypoints/Invoke-ListDetectedApps.ps1 b/Modules/CIPPCore/Public/Entrypoints/Invoke-ListDetectedApps.ps1 new file mode 100644 index 000000000000..0a8a754c271a --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/Invoke-ListDetectedApps.ps1 @@ -0,0 +1,78 @@ +function Invoke-ListDetectedApps { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Identity.Device.Read + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $TenantFilter = $Request.Query.tenantFilter + $DeviceID = $Request.Query.DeviceID + $IncludeDevices = $Request.Query.includeDevices + + # This is all about the deviceManagement/detectedApps endpoint + # We need to get the detected apps for a given device or the entire tenant + # If no device ID is provided, we need to get the detected apps for the entire tenant + # If a device ID is provided, we need to get the detected apps for the device + # deviceManagement/detectedApps for the entire tenant, or deviceManagement/managedDevices/$DeviceID/detectedApps for the device + # If includeDevices is true, we can use deviceManagement/detectedApps/{id}/managedDevices to get devices where each app is installed + + try { + # If DeviceID is provided, get detected apps for that device + if ($DeviceID) { + $GraphRequest = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/deviceManagement/managedDevices/$DeviceID/detectedApps" -Tenantid $TenantFilter + } + # If no device ID is provided, get detected apps for the entire tenant + else { + $GraphRequest = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/deviceManagement/detectedApps" -Tenantid $TenantFilter + } + + # Ensure we return an array even if null + if ($null -eq $GraphRequest) { + $GraphRequest = @() + } + + # If includeDevices is requested and we have detected apps, fetch devices for each app + if ($IncludeDevices -and $GraphRequest -and ($GraphRequest | Measure-Object).Count -gt 0) { + # Build bulk requests to get devices for each detected app + $BulkRequests = [System.Collections.Generic.List[object]]::new() + foreach ($App in $GraphRequest) { + if ($App.id) { + $BulkRequests.Add(@{ + id = $App.id + method = 'GET' + url = "deviceManagement/detectedApps('$($App.id)')/managedDevices" + }) + } + } + + if ($BulkRequests.Count -gt 0) { + $BulkResults = New-GraphBulkRequest -Requests $BulkRequests -tenantid $TenantFilter + + # Merge device information back into each detected app + $GraphRequest = foreach ($App in $GraphRequest) { + $Devices = Get-GraphBulkResultByID -Results $BulkResults -ID $App.id -Value + if ($Devices) { + $App | Add-Member -NotePropertyName 'managedDevices' -NotePropertyValue $Devices -Force + } else { + $App | Add-Member -NotePropertyName 'managedDevices' -NotePropertyValue @() -Force + } + $App + } + } + } + + $StatusCode = [HttpStatusCode]::OK + } catch { + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + $StatusCode = [HttpStatusCode]::OK + $GraphRequest = $ErrorMessage + } + + return [HttpResponseContext]@{ + StatusCode = $StatusCode + Body = @($GraphRequest) + } +} diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheDetectedApps.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheDetectedApps.ps1 new file mode 100644 index 000000000000..2f209898efed --- /dev/null +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheDetectedApps.ps1 @@ -0,0 +1,75 @@ +function Set-CIPPDBCacheDetectedApps { + <# + .SYNOPSIS + Caches all detected apps for a tenant, including devices that have each app + + .PARAMETER TenantFilter + The tenant to cache detected apps for + + .PARAMETER QueueId + The queue ID to update with total tasks (optional) + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter, + [string]$QueueId + ) + + try { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching detected apps' -sev Debug + + # Fetch all detected apps for the tenant + $DetectedApps = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/deviceManagement/detectedApps' -tenantid $TenantFilter + if (!$DetectedApps) { $DetectedApps = @() } + + if (($DetectedApps | Measure-Object).Count -eq 0) { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'No detected apps found' -sev Debug + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'DetectedApps' -Data @() + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'DetectedApps' -Data @() -Count + return + } + + # Build bulk request for devices that have each detected app + $DeviceRequests = $DetectedApps | ForEach-Object { + if ($_.id) { + [PSCustomObject]@{ + id = $_.id + method = 'GET' + url = "deviceManagement/detectedApps('$($_.id)')/managedDevices" + } + } + } + + if ($DeviceRequests) { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Fetching devices for $($DetectedApps.Count) detected apps" -sev Debug + $DeviceResults = New-GraphBulkRequest -Requests @($DeviceRequests) -tenantid $TenantFilter + + # Add devices to each detected app object + $DetectedAppsWithDevices = foreach ($App in $DetectedApps) { + $Devices = Get-GraphBulkResultByID -Results $DeviceResults -ID $App.id -Value + if ($Devices) { + $App | Add-Member -NotePropertyName 'managedDevices' -NotePropertyValue $Devices -Force + } else { + $App | Add-Member -NotePropertyName 'managedDevices' -NotePropertyValue @() -Force + } + $App + } + + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'DetectedApps' -Data $DetectedAppsWithDevices + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'DetectedApps' -Data $DetectedAppsWithDevices -Count + $DetectedApps = $null + $DetectedAppsWithDevices = $null + } else { + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'DetectedApps' -Data $DetectedApps + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'DetectedApps' -Data $DetectedApps -Count + $DetectedApps = $null + } + + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached detected apps with devices successfully' -sev Debug + + } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter ` + -message "Failed to cache detected apps: $($_.Exception.Message)" -sev Error + } +} From 70a2ebafea8664df169ee870b9591b3d2581a33d Mon Sep 17 00:00:00 2001 From: rvdwegen Date: Sun, 15 Feb 2026 22:27:19 +0100 Subject: [PATCH 418/503] add db cache types --- CIPPDBCacheTypes.json | 332 ++++++++++++++++++ .../Endpoint/MEM/Invoke-ListDefenderState.ps1 | 21 +- 2 files changed, 349 insertions(+), 4 deletions(-) create mode 100644 CIPPDBCacheTypes.json diff --git a/CIPPDBCacheTypes.json b/CIPPDBCacheTypes.json new file mode 100644 index 000000000000..28fac3a6fde8 --- /dev/null +++ b/CIPPDBCacheTypes.json @@ -0,0 +1,332 @@ +[ + { + "type": "Users", + "friendlyName": "Users", + "description": "All Azure AD users with sign-in activity" + }, + { + "type": "Groups", + "friendlyName": "Groups", + "description": "All Azure AD groups with members" + }, + { + "type": "Guests", + "friendlyName": "Guest Users", + "description": "All guest users in the tenant" + }, + { + "type": "ServicePrincipals", + "friendlyName": "Service Principals", + "description": "All service principals (applications)" + }, + { + "type": "Apps", + "friendlyName": "Application Registrations", + "description": "All application registrations with owners" + }, + { + "type": "Devices", + "friendlyName": "Azure AD Devices", + "description": "All Azure AD registered devices" + }, + { + "type": "Organization", + "friendlyName": "Organization", + "description": "Tenant organization information" + }, + { + "type": "Roles", + "friendlyName": "Directory Roles", + "description": "All Azure AD directory roles with members" + }, + { + "type": "AdminConsentRequestPolicy", + "friendlyName": "Admin Consent Request Policy", + "description": "Admin consent request policy settings" + }, + { + "type": "AuthorizationPolicy", + "friendlyName": "Authorization Policy", + "description": "Tenant authorization policy" + }, + { + "type": "AuthenticationMethodsPolicy", + "friendlyName": "Authentication Methods Policy", + "description": "Authentication methods policy configuration" + }, + { + "type": "DeviceSettings", + "friendlyName": "Device Settings", + "description": "Device management settings" + }, + { + "type": "DirectoryRecommendations", + "friendlyName": "Directory Recommendations", + "description": "Azure AD directory recommendations" + }, + { + "type": "CrossTenantAccessPolicy", + "friendlyName": "Cross-Tenant Access Policy", + "description": "Cross-tenant access policy configuration" + }, + { + "type": "DefaultAppManagementPolicy", + "friendlyName": "Default App Management Policy", + "description": "Default application management policy" + }, + { + "type": "Settings", + "friendlyName": "Directory Settings", + "description": "Directory settings configuration" + }, + { + "type": "SecureScore", + "friendlyName": "Secure Score", + "description": "Microsoft Secure Score and control profiles" + }, + { + "type": "PIMSettings", + "friendlyName": "PIM Settings", + "description": "Privileged Identity Management settings and assignments" + }, + { + "type": "Domains", + "friendlyName": "Domains", + "description": "All verified and unverified domains" + }, + { + "type": "RoleEligibilitySchedules", + "friendlyName": "Role Eligibility Schedules", + "description": "PIM role eligibility schedules" + }, + { + "type": "RoleManagementPolicies", + "friendlyName": "Role Management Policies", + "description": "Role management policies" + }, + { + "type": "RoleAssignmentScheduleInstances", + "friendlyName": "Role Assignment Schedule Instances", + "description": "Active role assignment instances" + }, + { + "type": "B2BManagementPolicy", + "friendlyName": "B2B Management Policy", + "description": "B2B collaboration policy settings" + }, + { + "type": "AuthenticationFlowsPolicy", + "friendlyName": "Authentication Flows Policy", + "description": "Authentication flows policy configuration" + }, + { + "type": "DeviceRegistrationPolicy", + "friendlyName": "Device Registration Policy", + "description": "Device registration policy settings" + }, + { + "type": "CredentialUserRegistrationDetails", + "friendlyName": "Credential User Registration Details", + "description": "User credential registration details" + }, + { + "type": "UserRegistrationDetails", + "friendlyName": "User Registration Details", + "description": "MFA registration details for users" + }, + { + "type": "OAuth2PermissionGrants", + "friendlyName": "OAuth2 Permission Grants", + "description": "OAuth2 permission grants" + }, + { + "type": "AppRoleAssignments", + "friendlyName": "App Role Assignments", + "description": "Application role assignments" + }, + { + "type": "LicenseOverview", + "friendlyName": "License Overview", + "description": "License usage overview" + }, + { + "type": "MFAState", + "friendlyName": "MFA State", + "description": "Multi-factor authentication state" + }, + { + "type": "ExoAntiPhishPolicies", + "friendlyName": "Exchange Anti-Phish Policies", + "description": "Exchange Online anti-phishing policies" + }, + { + "type": "ExoMalwareFilterPolicies", + "friendlyName": "Exchange Malware Filter Policies", + "description": "Exchange Online malware filter policies" + }, + { + "type": "ExoSafeLinksPolicies", + "friendlyName": "Exchange Safe Links Policies", + "description": "Exchange Online Safe Links policies" + }, + { + "type": "ExoSafeAttachmentPolicies", + "friendlyName": "Exchange Safe Attachment Policies", + "description": "Exchange Online Safe Attachment policies" + }, + { + "type": "ExoTransportRules", + "friendlyName": "Exchange Transport Rules", + "description": "Exchange Online transport rules" + }, + { + "type": "ExoDkimSigningConfig", + "friendlyName": "Exchange DKIM Signing Config", + "description": "Exchange Online DKIM signing configuration" + }, + { + "type": "ExoOrganizationConfig", + "friendlyName": "Exchange Organization Config", + "description": "Exchange Online organization configuration" + }, + { + "type": "ExoAcceptedDomains", + "friendlyName": "Exchange Accepted Domains", + "description": "Exchange Online accepted domains" + }, + { + "type": "ExoHostedContentFilterPolicy", + "friendlyName": "Exchange Hosted Content Filter Policy", + "description": "Exchange Online hosted content filter policy" + }, + { + "type": "ExoHostedOutboundSpamFilterPolicy", + "friendlyName": "Exchange Hosted Outbound Spam Filter Policy", + "description": "Exchange Online hosted outbound spam filter policy" + }, + { + "type": "ExoAntiPhishPolicy", + "friendlyName": "Exchange Anti-Phish Policy", + "description": "Exchange Online anti-phishing policy" + }, + { + "type": "ExoSafeLinksPolicy", + "friendlyName": "Exchange Safe Links Policy", + "description": "Exchange Online Safe Links policy" + }, + { + "type": "ExoSafeAttachmentPolicy", + "friendlyName": "Exchange Safe Attachment Policy", + "description": "Exchange Online Safe Attachment policy" + }, + { + "type": "ExoMalwareFilterPolicy", + "friendlyName": "Exchange Malware Filter Policy", + "description": "Exchange Online malware filter policy" + }, + { + "type": "ExoAtpPolicyForO365", + "friendlyName": "Exchange ATP Policy for O365", + "description": "Exchange Online Advanced Threat Protection policy" + }, + { + "type": "ExoQuarantinePolicy", + "friendlyName": "Exchange Quarantine Policy", + "description": "Exchange Online quarantine policy" + }, + { + "type": "ExoRemoteDomain", + "friendlyName": "Exchange Remote Domain", + "description": "Exchange Online remote domain configuration" + }, + { + "type": "ExoSharingPolicy", + "friendlyName": "Exchange Sharing Policy", + "description": "Exchange Online sharing policies" + }, + { + "type": "ExoAdminAuditLogConfig", + "friendlyName": "Exchange Admin Audit Log Config", + "description": "Exchange Online admin audit log configuration" + }, + { + "type": "ExoPresetSecurityPolicy", + "friendlyName": "Exchange Preset Security Policy", + "description": "Exchange Online preset security policy" + }, + { + "type": "ExoTenantAllowBlockList", + "friendlyName": "Exchange Tenant Allow/Block List", + "description": "Exchange Online tenant allow/block list" + }, + { + "type": "Mailboxes", + "friendlyName": "Mailboxes", + "description": "All Exchange Online mailboxes" + }, + { + "type": "CASMailboxes", + "friendlyName": "CAS Mailboxes", + "description": "Client Access Server mailbox settings" + }, + { + "type": "MailboxUsage", + "friendlyName": "Mailbox Usage", + "description": "Exchange Online mailbox usage statistics" + }, + { + "type": "OneDriveUsage", + "friendlyName": "OneDrive Usage", + "description": "OneDrive usage statistics" + }, + { + "type": "ConditionalAccessPolicies", + "friendlyName": "Conditional Access Policies", + "description": "Azure AD Conditional Access policies" + }, + { + "type": "RiskyUsers", + "friendlyName": "Risky Users", + "description": "Users flagged as risky by Identity Protection" + }, + { + "type": "RiskyServicePrincipals", + "friendlyName": "Risky Service Principals", + "description": "Service principals flagged as risky by Identity Protection" + }, + { + "type": "ServicePrincipalRiskDetections", + "friendlyName": "Service Principal Risk Detections", + "description": "Risk detections for service principals" + }, + { + "type": "RiskDetections", + "friendlyName": "Risk Detections", + "description": "Identity Protection risk detections" + }, + { + "type": "ManagedDevices", + "friendlyName": "Managed Devices", + "description": "Intune managed devices" + }, + { + "type": "IntunePolicies", + "friendlyName": "Intune Policies", + "description": "All Intune policies including compliance, configuration, and app protection" + }, + { + "type": "ManagedDeviceEncryptionStates", + "friendlyName": "Managed Device Encryption States", + "description": "BitLocker encryption states for managed devices" + }, + { + "type": "IntuneAppProtectionPolicies", + "friendlyName": "Intune App Protection Policies", + "description": "Intune app protection policies for iOS and Android" + }, + { + "type": "DetectedApps", + "friendlyName": "Detected Apps", + "description": "All detected applications with devices where each app is installed" + } +] diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListDefenderState.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListDefenderState.ps1 index e03874e4bac6..4ca3690502b5 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListDefenderState.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListDefenderState.ps1 @@ -9,16 +9,29 @@ Function Invoke-ListDefenderState { param($Request, $TriggerMetadata) $StatusCode = [HttpStatusCode]::OK - - # Interact with query parameters or the body of the request. $TenantFilter = $Request.Query.TenantFilter + $DeviceID = $Request.Query.DeviceID + try { - $GraphRequest = New-GraphGetRequest -tenantid $TenantFilter -uri "https://graph.microsoft.com/beta/deviceManagement/managedDevices?`$expand=windowsProtectionState&`$select=id,deviceName,deviceType,operatingSystem,windowsProtectionState" + # If DeviceID is provided, get Defender state for that specific device + if ($DeviceID) { + $GraphRequest = New-GraphGetRequest -tenantid $TenantFilter -uri "https://graph.microsoft.com/beta/deviceManagement/managedDevices/$($DeviceID)?`$expand=windowsProtectionState&`$select=id,deviceName,deviceType,operatingSystem,windowsProtectionState" + } + # If no DeviceID is provided, get Defender state for all devices + else { + $GraphRequest = New-GraphGetRequest -tenantid $TenantFilter -uri "https://graph.microsoft.com/beta/deviceManagement/managedDevices?`$expand=windowsProtectionState&`$select=id,deviceName,deviceType,operatingSystem,windowsProtectionState" + } + + # Ensure we return an array even if single device + if ($GraphRequest -and -not ($GraphRequest -is [array])) { + $GraphRequest = @($GraphRequest) + } + $StatusCode = [HttpStatusCode]::OK } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message - $StatusCode = [HttpStatusCode]::Forbidden + $StatusCode = [HttpStatusCode]::OK $GraphRequest = "$($ErrorMessage)" } return ([HttpResponseContext]@{ From 12ec2d57c0d1114586e3abe41bfc1be8709a8892 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Mon, 16 Feb 2026 10:12:33 +0100 Subject: [PATCH 419/503] Add retries for CA policies. --- .../Public/GraphHelper/New-GraphPOSTRequest.ps1 | 2 +- Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Modules/CIPPCore/Public/GraphHelper/New-GraphPOSTRequest.ps1 b/Modules/CIPPCore/Public/GraphHelper/New-GraphPOSTRequest.ps1 index 2bfdd9028f56..5d4d1fd3f950 100644 --- a/Modules/CIPPCore/Public/GraphHelper/New-GraphPOSTRequest.ps1 +++ b/Modules/CIPPCore/Public/GraphHelper/New-GraphPOSTRequest.ps1 @@ -96,7 +96,7 @@ function New-GraphPOSTRequest { # Add the scheduled task (hidden = system task) $null = Add-CIPPScheduledTask -Task $TaskObject -Hidden $true - return @{Result = "Scheduled job with id $TaskId as Graph API was too busy to respond" } + return @{Result = "Scheduled job with id $TaskId as Graph API was too busy to respond. Check the job status in the scheduler." } } catch { Write-Warning "Failed to schedule retry task: $($_.Exception.Message)" } diff --git a/Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 b/Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 index 4f61f708abd7..1409a54e962e 100644 --- a/Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 +++ b/Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 @@ -133,7 +133,7 @@ function New-CIPPCAPolicy { } else { $Body = ConvertTo-Json -InputObject $JSONobj.GrantControls.authenticationStrength - $GraphRequest = New-GraphPOSTRequest -uri 'https://graph.microsoft.com/beta/identity/conditionalAccess/authenticationStrength/policies' -body $body -Type POST -tenantid $TenantFilter -asApp $true + $GraphRequest = New-GraphPOSTRequest -uri 'https://graph.microsoft.com/beta/identity/conditionalAccess/authenticationStrength/policies' -body $body -Type POST -tenantid $TenantFilter -asApp $true -ScheduleRetry $true $JSONobj.GrantControls.authenticationStrength = @{ id = $ExistingStrength.id } Write-LogMessage -Headers $Headers -API $APIName -message "Created new Authentication Strength Policy: $($JSONobj.GrantControls.authenticationStrength.displayName)" -Sev 'Info' } @@ -178,7 +178,7 @@ function New-CIPPCAPolicy { Remove-ODataProperties -Object $LocationUpdate $Body = ConvertTo-Json -InputObject $LocationUpdate -Depth 10 try { - $null = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/identity/conditionalAccess/namedLocations/$($ExistingLocation.id)" -body $body -Type PATCH -tenantid $TenantFilter -asApp $true + $null = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/identity/conditionalAccess/namedLocations/$($ExistingLocation.id)" -body $body -Type PATCH -tenantid $TenantFilter -asApp $true -ScheduleRetry $true Write-LogMessage -Tenant $TenantFilter -Headers $Headers -API $APIName -message "Updated existing Named Location: $($location.displayName)" -Sev 'Info' } catch { Write-Warning "Failed to update location $($location.displayName): $_" @@ -347,7 +347,7 @@ function New-CIPPCAPolicy { # Preserve any exclusion groups named "Vacation Exclusion - " 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 @@ -369,7 +369,7 @@ function New-CIPPCAPolicy { Write-Information "Failed to preserve vacation exclusion group: $($_.Exception.Message)" } Write-Information "overwriting $($CheckExisting.id)" - $null = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/identity/conditionalAccess/policies/$($CheckExisting.id)" -tenantid $TenantFilter -type PATCH -body $RawJSON -asApp $true + $null = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/identity/conditionalAccess/policies/$($CheckExisting.id)" -tenantid $TenantFilter -type PATCH -body $RawJSON -asApp $true -ScheduleRetry $true Write-LogMessage -Headers $Headers -API $APIName -tenant $TenantFilter -message "Updated Conditional Access Policy $($JSONobj.displayName) to the template standard." -Sev 'Info' return "Updated policy $($JSONobj.displayName) for $TenantFilter" } @@ -378,7 +378,7 @@ function New-CIPPCAPolicy { if ($JSOObj.GrantControls.authenticationStrength.policyType -or $JSONobj.$JSONobj.LocationInfo) { Start-Sleep 3 } - $null = New-GraphPOSTRequest -uri 'https://graph.microsoft.com/beta/identity/conditionalAccess/policies' -tenantid $TenantFilter -type POST -body $RawJSON -asApp $true + $null = New-GraphPOSTRequest -uri 'https://graph.microsoft.com/beta/identity/conditionalAccess/policies' -tenantid $TenantFilter -type POST -body $RawJSON -asApp $true -ScheduleRetry $true Write-LogMessage -Headers $Headers -API $APIName -tenant $TenantFilter -message "Added Conditional Access Policy $($JSONobj.displayName)" -Sev 'Info' return "Created policy $($JSONobj.displayName) for $TenantFilter" } From 1767fa46d32380347e003f35e82f30dd5cb70cf3 Mon Sep 17 00:00:00 2001 From: rvdwegen Date: Mon, 16 Feb 2026 10:55:37 +0100 Subject: [PATCH 420/503] add groups support for universal search --- .../Invoke-ExecUniversalSearchV2.ps1 | 14 ++++++- Modules/CIPPCore/Public/Search-CIPPDbData.ps1 | 40 ++++++++++++------- 2 files changed, 37 insertions(+), 17 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/Invoke-ExecUniversalSearchV2.ps1 b/Modules/CIPPCore/Public/Entrypoints/Invoke-ExecUniversalSearchV2.ps1 index f110b9a4cab6..df21429f9605 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Invoke-ExecUniversalSearchV2.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Invoke-ExecUniversalSearchV2.ps1 @@ -10,10 +10,20 @@ function Invoke-ExecUniversalSearchV2 { $SearchTerms = $Request.Query.searchTerms $Limit = if ($Request.Query.limit) { [int]$Request.Query.limit } else { 10 } + $Type = if ($Request.Query.type) { $Request.Query.type } else { 'Users' } # Always search all tenants - do not pass TenantFilter parameter - $Results = Search-CIPPDbData -SearchTerms $SearchTerms -Types 'Users' -Limit $Limit -UserProperties 'id', 'userPrincipalName', 'displayName' - + switch ($Type) { + 'Users' { + $Results = Search-CIPPDbData -SearchTerms $SearchTerms -Types 'Users' -Limit $Limit -Properties 'id', 'userPrincipalName', 'displayName' + } + 'Groups' { + $Results = Search-CIPPDbData -SearchTerms $SearchTerms -Types 'Groups' -Limit $Limit -Properties 'id', 'displayName', 'mail', 'mailEnabled', 'securityEnabled', 'groupTypes', 'description' + } + default { + $Results = Search-CIPPDbData -SearchTerms $SearchTerms -Types 'Users' -Limit $Limit -Properties 'id', 'userPrincipalName', 'displayName' + } + } Write-Information "Results: $($Results | ConvertTo-Json -Depth 10)" diff --git a/Modules/CIPPCore/Public/Search-CIPPDbData.ps1 b/Modules/CIPPCore/Public/Search-CIPPDbData.ps1 index b5aca603896f..1f60a59a4f2c 100644 --- a/Modules/CIPPCore/Public/Search-CIPPDbData.ps1 +++ b/Modules/CIPPCore/Public/Search-CIPPDbData.ps1 @@ -30,14 +30,12 @@ function Search-CIPPDbData { .PARAMETER Limit Maximum total number of results to return across all types. Default is unlimited (0) + .PARAMETER Properties + Array of property names to return for the searched types. If not specified, all properties are returned. + Applies to all types in the Types parameter. Only properties that exist in the data will be included. + .PARAMETER UserProperties - Array of property names to return for Users type. If not specified, all properties are returned. - Only applies when Types includes 'Users'. Valid properties include: id, accountEnabled, businessPhones, - city, createdDateTime, companyName, country, department, displayName, faxNumber, givenName, - isResourceAccount, jobTitle, mail, mailNickname, mobilePhone, onPremisesDistinguishedName, - officeLocation, onPremisesLastSyncDateTime, otherMails, postalCode, preferredDataLocation, - preferredLanguage, proxyAddresses, showInAddressList, state, streetAddress, surname, - usageLocation, userPrincipalName, userType, assignedLicenses, onPremisesSyncEnabled, signInActivity + [DEPRECATED] Use Properties parameter instead. Array of property names to return for Users type. .EXAMPLE Search-CIPPDbData -TenantFilter 'contoso.onmicrosoft.com' -SearchTerms 'john.doe' -Types 'Users', 'Groups' @@ -83,6 +81,9 @@ function Search-CIPPDbData { [Parameter(Mandatory = $false)] [int]$Limit = 0, + [Parameter(Mandatory = $false)] + [string[]]$Properties, + [Parameter(Mandatory = $false)] [string[]]$UserProperties ) @@ -159,9 +160,18 @@ function Search-CIPPDbData { try { $Data = $Item.Data | ConvertFrom-Json - # For Users type with UserProperties, verify match is in target properties + # Determine which properties to use (Properties parameter takes precedence, fallback to UserProperties for backward compatibility) + $PropertiesToUse = if ($Properties -and $Properties.Count -gt 0) { + $Properties + } elseif ($Type -eq 'Users' -and $UserProperties -and $UserProperties.Count -gt 0) { + $UserProperties + } else { + $null + } + + # If properties are specified, verify match is in target properties $IsVerifiedMatch = $true - if ($Type -eq 'Users' -and $UserProperties -and $UserProperties.Count -gt 0) { + if ($PropertiesToUse -and $PropertiesToUse.Count -gt 0) { $IsVerifiedMatch = $false if ($MatchAll) { @@ -170,7 +180,7 @@ function Search-CIPPDbData { foreach ($SearchTerm in $SearchTerms) { $SearchPattern = [regex]::Escape($SearchTerm) $TermMatches = $false - foreach ($Property in $UserProperties) { + foreach ($Property in $PropertiesToUse) { if ($Data.PSObject.Properties.Name -contains $Property -and $null -ne $Data.$Property -and $Data.$Property.ToString() -match $SearchPattern) { @@ -187,7 +197,7 @@ function Search-CIPPDbData { # Any search term can match in target properties foreach ($SearchTerm in $SearchTerms) { $SearchPattern = [regex]::Escape($SearchTerm) - foreach ($Property in $UserProperties) { + foreach ($Property in $PropertiesToUse) { if ($Data.PSObject.Properties.Name -contains $Property -and $null -ne $Data.$Property -and $Data.$Property.ToString() -match $SearchPattern) { @@ -200,12 +210,12 @@ function Search-CIPPDbData { } } - # Only add to results if verified (or not Users/UserProperties) + # Only add to results if verified (or no property filtering) if ($IsVerifiedMatch) { - # Filter user properties if specified and type is Users - if ($Type -eq 'Users' -and $UserProperties -and $UserProperties.Count -gt 0) { + # Filter properties if specified + if ($PropertiesToUse -and $PropertiesToUse.Count -gt 0) { $FilteredData = [PSCustomObject]@{} - foreach ($Property in $UserProperties) { + foreach ($Property in $PropertiesToUse) { if ($Data.PSObject.Properties.Name -contains $Property) { $FilteredData | Add-Member -MemberType NoteProperty -Name $Property -Value $Data.$Property -Force } From eab2261f6a89a1bc58ca9d6f386985d450fcc1d6 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Mon, 16 Feb 2026 11:19:04 +0100 Subject: [PATCH 421/503] add top --- Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 b/Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 index 1409a54e962e..635949848451 100644 --- a/Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 +++ b/Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 @@ -141,7 +141,7 @@ function New-CIPPCAPolicy { #if we have excluded or included applications, we need to remove any appIds that do not have a service principal in the tenant if (($JSONobj.conditions.applications.includeApplications -and $JSONobj.conditions.applications.includeApplications -notcontains 'All') -or ($JSONobj.conditions.applications.excludeApplications -and $JSONobj.conditions.applications.excludeApplications -notcontains 'All')) { - $AllServicePrincipals = New-GraphGETRequest -uri 'https://graph.microsoft.com/v1.0/servicePrincipals?$select=appId' -tenantid $TenantFilter -asApp $true + $AllServicePrincipals = New-GraphGETRequest -uri 'https://graph.microsoft.com/v1.0/servicePrincipals?$select=appId&$top=999' -tenantid $TenantFilter -asApp $true $ReservedApplicationNames = @('none', 'All', 'Office365', 'MicrosoftAdminPortals') From 3d43ac19786e6466329df8379f510fc00d5624b5 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Mon, 16 Feb 2026 11:23:56 +0100 Subject: [PATCH 422/503] add too many requests for GET logic. --- Modules/CIPPCore/Public/GraphHelper/New-GraphGetRequest.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/GraphHelper/New-GraphGetRequest.ps1 b/Modules/CIPPCore/Public/GraphHelper/New-GraphGetRequest.ps1 index ec43778f3748..f5dc35876afc 100644 --- a/Modules/CIPPCore/Public/GraphHelper/New-GraphGetRequest.ps1 +++ b/Modules/CIPPCore/Public/GraphHelper/New-GraphGetRequest.ps1 @@ -163,7 +163,7 @@ function New-GraphGetRequest { } } # Check for "Resource temporarily unavailable" - elseif ($Message -like '*Resource temporarily unavailable*') { + elseif ($Message -like '*Resource temporarily unavailable*' -or $Message -like '*Too many requests*') { if ($RetryCount -lt $MaxRetries) { $WaitTime = Get-Random -Minimum 1.1 -Maximum 3.1 # Random sleep between 1-2 seconds Write-Warning "Resource temporarily unavailable. Waiting $WaitTime seconds before retry. Attempt $($RetryCount + 1) of $MaxRetries" From e49d430decde5a9ea83b9c63955b9db2ca9b79bd Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Mon, 16 Feb 2026 11:29:14 +0100 Subject: [PATCH 423/503] remove old measure tasks --- .../Activity Triggers/Standards/Push-CIPPStandard.ps1 | 4 +--- .../Timer Functions/Start-CIPPProcessorQueue.ps1 | 10 +++++----- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Standards/Push-CIPPStandard.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Standards/Push-CIPPStandard.ps1 index e3944c21a4d2..0b70aa838235 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Standards/Push-CIPPStandard.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Standards/Push-CIPPStandard.ps1 @@ -95,9 +95,7 @@ function Push-CIPPStandard { $metadata['CATemplateId'] = $Item.Settings.TemplateList.value } - Measure-CippTask -TaskName $Standard -EventName 'CIPP.StandardCompleted' -Metadata $metadata -Script { - & $FunctionName -Tenant $Item.Tenant -Settings $Settings -ErrorAction Stop - } + & $FunctionName -Tenant $Item.Tenant -Settings $Settings -ErrorAction Stop $result = 'Success' Write-Information "Standard $($Standard) completed for tenant $($Tenant)" diff --git a/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-CIPPProcessorQueue.ps1 b/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-CIPPProcessorQueue.ps1 index 4e34dc09f102..cbc065c4f15c 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-CIPPProcessorQueue.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-CIPPProcessorQueue.ps1 @@ -30,7 +30,7 @@ function Start-CIPPProcessorQueue { TriggerType = 'ProcessorQueue' QueueRowKey = $QueueItem.RowKey } - + # Add parameters info if available if ($Parameters.Count -gt 0) { $metadata['ParameterCount'] = $Parameters.Count @@ -42,11 +42,11 @@ function Start-CIPPProcessorQueue { $metadata['Tenant'] = $Parameters.TenantFilter } } - + # Wrap function execution with telemetry - Measure-CippTask -TaskName $FunctionName -Metadata $metadata -Script { - Invoke-Command -ScriptBlock { & $FunctionName @Parameters } - } + + Invoke-Command -ScriptBlock { & $FunctionName @Parameters } + } catch { Write-Warning "Failed to run function $($FunctionName). Error: $($_.Exception.Message)" } From ab34d8defa24aee2b2deee70e027179e5f258bf1 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Mon, 16 Feb 2026 11:59:20 +0100 Subject: [PATCH 424/503] fixes to CA for timeouts and better handling of standards --- Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 | 146 ++++++++++++++---- ...-CIPPStandardConditionalAccessTemplate.ps1 | 19 +-- 2 files changed, 129 insertions(+), 36 deletions(-) diff --git a/Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 b/Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 index 635949848451..6589daade37f 100644 --- a/Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 +++ b/Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 @@ -10,7 +10,8 @@ function New-CIPPCAPolicy { $DisableSD = $false, $CreateGroups = $false, $APIName = 'Create CA Policy', - $Headers + $Headers, + $PreloadedCAPolicies = $null ) function Remove-EmptyArrays ($Object) { @@ -122,12 +123,77 @@ function New-CIPPCAPolicy { $JSONobj.state = $State } } catch { - # no issues here. + $ErrorMessage = Get-CippException -Exception $_ + Write-Information "Error cleaning JSON properties: $($ErrorMessage | ConvertTo-Json -Depth 10 -Compress)" + } + + # Execute all required GET requests ONCE at the beginning to avoid rate limiting + Write-Information 'Fetching required resources from Graph API...' + + # Get existing CA policies once (or use preloaded ones) + if ($PreloadedCAPolicies) { + Write-Information 'Using preloaded CA policies' + $AllExistingPolicies = $PreloadedCAPolicies + Write-Information "Found $($AllExistingPolicies.Count) preloaded CA policies" + } else { + try { + Write-Information 'Fetching existing CA policies...' + $AllExistingPolicies = New-GraphGETRequest -uri 'https://graph.microsoft.com/beta/identity/conditionalAccess/policies?$top=999' -tenantid $TenantFilter -asApp $true + Write-Information "Found $($AllExistingPolicies.Count) existing CA policies" + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-Information "Error fetching existing policies: $($ErrorMessage | ConvertTo-Json -Depth 10 -Compress)" + throw "Failed to fetch existing CA policies: $($ErrorMessage.NormalizedError)" + } + } + + # Get named locations once if needed + $AllNamedLocations = $null + if ($JSONobj.LocationInfo) { + try { + Write-Information 'Fetching all named locations...' + $AllNamedLocations = New-GraphGETRequest -uri 'https://graph.microsoft.com/beta/identity/conditionalAccess/namedLocations?$top=999' -tenantid $TenantFilter -asApp $true + Write-Information "Found $($AllNamedLocations.Count) existing named locations" + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-Information "Error fetching named locations: $($ErrorMessage | ConvertTo-Json -Depth 10 -Compress)" + throw "Failed to fetch named locations: $($ErrorMessage.NormalizedError)" + } + } + + # Get authentication strength policies once if needed + $AllAuthStrengthPolicies = $null + if ($JSONobj.GrantControls.authenticationStrength.policyType -eq 'custom' -or $JSONobj.GrantControls.authenticationStrength.policyType -eq 'BuiltIn') { + try { + Write-Information 'Fetching authentication strength policies...' + $AllAuthStrengthPolicies = New-GraphGETRequest -uri 'https://graph.microsoft.com/beta/identity/conditionalAccess/authenticationStrength/policies/' -tenantid $TenantFilter -asApp $true + Write-Information "Found $($AllAuthStrengthPolicies.Count) authentication strength policies" + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-Information "Error fetching authentication strength policies: $($ErrorMessage | ConvertTo-Json -Depth 10 -Compress)" + throw "Failed to fetch authentication strength policies: $($ErrorMessage.NormalizedError)" + } + } + + # Get service principals once if needed + $AllServicePrincipals = $null + if (($JSONobj.conditions.applications.includeApplications -and $JSONobj.conditions.applications.includeApplications -notcontains 'All') -or ($JSONobj.conditions.applications.excludeApplications -and $JSONobj.conditions.applications.excludeApplications -notcontains 'All')) { + try { + Write-Information 'Fetching all service principals...' + $AllServicePrincipals = New-GraphGETRequest -uri 'https://graph.microsoft.com/v1.0/servicePrincipals?$select=appId&$top=999' -tenantid $TenantFilter -asApp $true + Write-Information "Found $($AllServicePrincipals.Count) service principals" + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-Information "Error fetching service principals: $($ErrorMessage | ConvertTo-Json -Depth 10 -Compress)" + throw "Failed to fetch service principals: $($ErrorMessage.NormalizedError)" + } } + Write-Information 'All required resources fetched successfully' + #If Grant Controls contains authenticationStrength, create these and then replace the id if ($JSONobj.GrantControls.authenticationStrength.policyType -eq 'custom' -or $JSONobj.GrantControls.authenticationStrength.policyType -eq 'BuiltIn') { - $ExistingStrength = New-GraphGETRequest -uri 'https://graph.microsoft.com/beta/identity/conditionalAccess/authenticationStrength/policies/' -tenantid $TenantFilter -asApp $true | Where-Object -Property displayName -EQ $JSONobj.GrantControls.authenticationStrength.displayName + $ExistingStrength = $AllAuthStrengthPolicies | Where-Object -Property displayName -EQ $JSONobj.GrantControls.authenticationStrength.displayName if ($ExistingStrength) { $JSONobj.GrantControls.authenticationStrength = @{ id = $ExistingStrength.id } @@ -140,9 +206,7 @@ function New-CIPPCAPolicy { } #if we have excluded or included applications, we need to remove any appIds that do not have a service principal in the tenant - if (($JSONobj.conditions.applications.includeApplications -and $JSONobj.conditions.applications.includeApplications -notcontains 'All') -or ($JSONobj.conditions.applications.excludeApplications -and $JSONobj.conditions.applications.excludeApplications -notcontains 'All')) { - $AllServicePrincipals = New-GraphGETRequest -uri 'https://graph.microsoft.com/v1.0/servicePrincipals?$select=appId&$top=999' -tenantid $TenantFilter -asApp $true - + if ($AllServicePrincipals) { $ReservedApplicationNames = @('none', 'All', 'Office365', 'MicrosoftAdminPortals') if ($JSONobj.conditions.applications.excludeApplications -and $JSONobj.conditions.applications.excludeApplications -notcontains 'All') { @@ -170,9 +234,9 @@ function New-CIPPCAPolicy { if (!$locations) { continue } foreach ($location in $locations) { if (!$location.displayName) { continue } - $CheckExisting = New-GraphGETRequest -uri 'https://graph.microsoft.com/beta/identity/conditionalAccess/namedLocations' -tenantid $TenantFilter -asApp $true - if ($Location.displayName -in $CheckExisting.displayName) { - $ExistingLocation = $CheckExisting | Where-Object -Property displayName -EQ $Location.displayName + # Use cached named locations instead of fetching each time + if ($Location.displayName -in $AllNamedLocations.displayName) { + $ExistingLocation = $AllNamedLocations | Where-Object -Property displayName -EQ $Location.displayName if ($Overwrite) { $LocationUpdate = $location | Select-Object * -ExcludeProperty id Remove-ODataProperties -Object $LocationUpdate @@ -181,8 +245,10 @@ function New-CIPPCAPolicy { $null = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/identity/conditionalAccess/namedLocations/$($ExistingLocation.id)" -body $body -Type PATCH -tenantid $TenantFilter -asApp $true -ScheduleRetry $true Write-LogMessage -Tenant $TenantFilter -Headers $Headers -API $APIName -message "Updated existing Named Location: $($location.displayName)" -Sev 'Info' } catch { - Write-Warning "Failed to update location $($location.displayName): $_" - Write-LogMessage -Tenant $TenantFilter -Headers $Headers -API $APIName -message "Failed to update existing Named Location: $($location.displayName). Error: $_" -Sev 'Error' + $ErrorMessage = Get-CippException -Exception $_ + Write-Information "Error updating named location: $($ErrorMessage | ConvertTo-Json -Depth 10 -Compress)" + Write-Warning "Failed to update location $($location.displayName): $($ErrorMessage.NormalizedError)" + Write-LogMessage -Tenant $TenantFilter -Headers $Headers -API $APIName -message "Failed to update existing Named Location: $($location.displayName). Error: $($ErrorMessage.NormalizedError)" -Sev 'Error' -LogData $ErrorMessage } } else { Write-LogMessage -Tenant $TenantFilter -Headers $Headers -API $APIName -message "Matched a CA policy with the existing Named Location: $($location.displayName)" -Sev 'Info' @@ -197,17 +263,35 @@ function New-CIPPCAPolicy { $LocationBody = $location | Select-Object * -ExcludeProperty id Remove-ODataProperties -Object $LocationBody $Body = ConvertTo-Json -InputObject $LocationBody - $GraphRequest = New-GraphPOSTRequest -uri 'https://graph.microsoft.com/beta/identity/conditionalAccess/namedLocations' -body $body -Type POST -tenantid $TenantFilter -asApp $true - $retryCount = 0 - $MaxRetryCount = 10 - do { - Write-Host "Checking for location $($GraphRequest.id) attempt $retryCount. $TenantFilter" - $LocationRequest = New-GraphGETRequest -uri 'https://graph.microsoft.com/beta/identity/conditionalAccess/namedLocations' -tenantid $TenantFilter -asApp $true | Where-Object -Property id -EQ $GraphRequest.id - Write-Host "LocationRequest: $($LocationRequest.id)" - Start-Sleep -Seconds 2 - $retryCount++ - } while ((!$LocationRequest -or !$LocationRequest.id) -and ($retryCount -lt $MaxRetryCount)) - Write-LogMessage -Tenant $TenantFilter -Headers $Headers -API $APIName -message "Created new Named Location: $($location.displayName)" -Sev 'Info' + try { + $GraphRequest = New-GraphPOSTRequest -uri 'https://graph.microsoft.com/beta/identity/conditionalAccess/namedLocations' -body $body -Type POST -tenantid $TenantFilter -asApp $true + Write-Information "Created named location with ID: $($GraphRequest.id)" + # Wait for location to be available - reduced retry count and increased delay + $retryCount = 0 + $MaxRetryCount = 5 + $LocationRequest = $null + do { + Write-Information "Verifying location $($GraphRequest.id) exists, attempt $($retryCount + 1)/$MaxRetryCount" + Start-Sleep -Seconds 3 + try { + # Get specific location by ID instead of all locations + $LocationRequest = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/identity/conditionalAccess/namedLocations/$($GraphRequest.id)" -tenantid $TenantFilter -asApp $true -ErrorAction Stop + Write-Information "Location verified: $($LocationRequest.id)" + } catch { + Write-Information 'Location not yet available, will retry...' + } + $retryCount++ + } while ((!$LocationRequest -or !$LocationRequest.id) -and ($retryCount -lt $MaxRetryCount)) + + if (!$LocationRequest -or !$LocationRequest.id) { + Write-Warning "Location created but could not verify availability after $MaxRetryCount attempts. Proceeding anyway." + } + Write-LogMessage -Tenant $TenantFilter -Headers $Headers -API $APIName -message "Created new Named Location: $($location.displayName)" -Sev 'Info' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-Information "Error creating named location: $($ErrorMessage | ConvertTo-Json -Depth 10 -Compress)" + throw "Failed to create named location $($location.displayName): $($ErrorMessage.NormalizedError)" + } [pscustomobject]@{ id = $GraphRequest.id name = $GraphRequest.displayName @@ -292,6 +376,7 @@ function New-CIPPCAPolicy { } } catch { $ErrorMessage = Get-CippException -Exception $_ + Write-Information "Error replacing displayNames: $($ErrorMessage | ConvertTo-Json -Depth 10 -Compress)" Write-LogMessage -API 'Standards' -tenant $TenantFilter -message "Failed to replace displayNames for conditional access rule $($JSONobj.displayName). Error: $($ErrorMessage.NormalizedError)" -sev 'Error' -LogData $ErrorMessage throw "Failed to replace displayNames for conditional access rule $($JSONobj.displayName): $($ErrorMessage.NormalizedError)" } @@ -327,6 +412,7 @@ function New-CIPPCAPolicy { Start-Sleep 3 } catch { $ErrorMessage = Get-CippException -Exception $_ + Write-Information "Error disabling security defaults: $($ErrorMessage | ConvertTo-Json -Depth 10 -Compress)" Write-Information "Failed to disable security defaults for tenant $($TenantFilter): $($ErrorMessage.NormalizedError)" } } @@ -334,7 +420,8 @@ function New-CIPPCAPolicy { Write-Information $RawJSON try { Write-Information 'Checking for existing policies' - $CheckExisting = New-GraphGETRequest -uri 'https://graph.microsoft.com/beta/identity/conditionalAccess/policies' -tenantid $TenantFilter -asApp $true | Where-Object -Property displayName -EQ $displayName + # Use cached policies instead of fetching again + $CheckExisting = $AllExistingPolicies | Where-Object -Property displayName -EQ $displayName if ($CheckExisting) { if ($Overwrite -ne $true) { throw "Conditional Access Policy with Display Name $($displayName) Already exists" @@ -366,7 +453,9 @@ function New-CIPPCAPolicy { $RawJSON = ConvertTo-Json -InputObject $JSONobj -Depth 10 -Compress } } catch { - Write-Information "Failed to preserve vacation exclusion group: $($_.Exception.Message)" + $ErrorMessage = Get-CippException -Exception $_ + Write-Information "Error preserving vacation exclusion group: $($ErrorMessage | ConvertTo-Json -Depth 10 -Compress)" + Write-Information "Failed to preserve vacation exclusion group: $($ErrorMessage.NormalizedError)" } Write-Information "overwriting $($CheckExisting.id)" $null = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/identity/conditionalAccess/policies/$($CheckExisting.id)" -tenantid $TenantFilter -type PATCH -body $RawJSON -asApp $true -ScheduleRetry $true @@ -385,11 +474,14 @@ function New-CIPPCAPolicy { } catch { $ErrorMessage = Get-CippException -Exception $_ $Result = "Failed to create or update conditional access rule $($JSONobj.displayName): $($ErrorMessage.NormalizedError)" - Write-LogMessage -API $APIName -tenant $TenantFilter -message $Result -sev 'Error' -LogData $ErrorMessage + # Full error details for debugging + Write-Information "Full error details: $($ErrorMessage | ConvertTo-Json -Depth 10 -Compress)" + Write-Information "Position: $($_.InvocationInfo.PositionMessage)" + Write-Information "Policy JSON: $($JSONobj | ConvertTo-Json -Depth 10 -Compress)" + + Write-LogMessage -API $APIName -tenant $TenantFilter -message $Result -sev 'Error' -LogData $ErrorMessage Write-Warning $Result - Write-Information $_.InvocationInfo.PositionMessage - Write-Information ($JSONobj | ConvertTo-Json -Depth 10) throw $Result } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardConditionalAccessTemplate.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardConditionalAccessTemplate.ps1 index 07da79a1fc70..a41018355017 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardConditionalAccessTemplate.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardConditionalAccessTemplate.ps1 @@ -67,15 +67,16 @@ function Invoke-CIPPStandardConditionalAccessTemplate { } } $NewCAPolicy = @{ - replacePattern = 'displayName' - TenantFilter = $Tenant - state = $Setting.state - RawJSON = $JSONObj - Overwrite = $true - APIName = 'Standards' - Headers = $Request.Headers - DisableSD = $Setting.DisableSD - CreateGroups = $Setting.CreateGroups ?? $false + replacePattern = 'displayName' + TenantFilter = $Tenant + state = $Setting.state + RawJSON = $JSONObj + Overwrite = $true + APIName = 'Standards' + Headers = $Request.Headers + DisableSD = $Setting.DisableSD + CreateGroups = $Setting.CreateGroups ?? $false + PreloadedCAPolicies = $AllCAPolicies } $null = New-CIPPCAPolicy @NewCAPolicy From 14ece32d5a9707f423fa922fa43615e1850a438c Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Mon, 16 Feb 2026 12:09:37 +0100 Subject: [PATCH 425/503] locationdependancy --- Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 | 36 +++++++++++--------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 b/Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 index 6589daade37f..306814ce68af 100644 --- a/Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 +++ b/Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 @@ -302,25 +302,27 @@ function New-CIPPCAPolicy { Write-Information 'Location Lookup Table:' Write-Information ($LocationLookupTable | ConvertTo-Json -Depth 10) - foreach ($location in $JSONobj.conditions.locations.includeLocations) { - if ($null -eq $location) { continue } - $lookup = $LocationLookupTable | Where-Object { $_.name -eq $location -or $_.displayName -eq $location -or $_.templateId -eq $location } - if (!$lookup) { continue } - Write-Information "Replacing named location - $location" - $index = [array]::IndexOf($JSONobj.conditions.locations.includeLocations, $location) - if ($lookup.id) { - $JSONobj.conditions.locations.includeLocations[$index] = $lookup.id + if ($LocationLookupTable -and $JSONobj.conditions.locations) { + foreach ($location in $JSONobj.conditions.locations.includeLocations) { + if ($null -eq $location) { continue } + $lookup = $LocationLookupTable | Where-Object { $_.name -eq $location -or $_.displayName -eq $location -or $_.templateId -eq $location } + if (!$lookup) { continue } + Write-Information "Replacing named location - $location" + $index = [array]::IndexOf($JSONobj.conditions.locations.includeLocations, $location) + if ($lookup.id) { + $JSONobj.conditions.locations.includeLocations[$index] = $lookup.id + } } - } - foreach ($location in $JSONobj.conditions.locations.excludeLocations) { - if ($null -eq $location) { continue } - $lookup = $LocationLookupTable | Where-Object { $_.name -eq $location -or $_.displayName -eq $location -or $_.templateId -eq $location } - if (!$lookup) { continue } - Write-Information "Replacing named location - $location" - $index = [array]::IndexOf($JSONobj.conditions.locations.excludeLocations, $location) - if ($lookup.id) { - $JSONobj.conditions.locations.excludeLocations[$index] = $lookup.id + foreach ($location in $JSONobj.conditions.locations.excludeLocations) { + if ($null -eq $location) { continue } + $lookup = $LocationLookupTable | Where-Object { $_.name -eq $location -or $_.displayName -eq $location -or $_.templateId -eq $location } + if (!$lookup) { continue } + Write-Information "Replacing named location - $location" + $index = [array]::IndexOf($JSONobj.conditions.locations.excludeLocations, $location) + if ($lookup.id) { + $JSONobj.conditions.locations.excludeLocations[$index] = $lookup.id + } } } switch ($ReplacePattern) { From f2367f9e6df4e7faca43ac05fb50e65b851e598e Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Mon, 16 Feb 2026 12:48:18 +0100 Subject: [PATCH 426/503] removes troubleshooting lines --- Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 | 23 +++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 b/Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 index 306814ce68af..5abdc29c44a6 100644 --- a/Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 +++ b/Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 @@ -134,12 +134,10 @@ function New-CIPPCAPolicy { if ($PreloadedCAPolicies) { Write-Information 'Using preloaded CA policies' $AllExistingPolicies = $PreloadedCAPolicies - Write-Information "Found $($AllExistingPolicies.Count) preloaded CA policies" } else { try { Write-Information 'Fetching existing CA policies...' $AllExistingPolicies = New-GraphGETRequest -uri 'https://graph.microsoft.com/beta/identity/conditionalAccess/policies?$top=999' -tenantid $TenantFilter -asApp $true - Write-Information "Found $($AllExistingPolicies.Count) existing CA policies" } catch { $ErrorMessage = Get-CippException -Exception $_ Write-Information "Error fetching existing policies: $($ErrorMessage | ConvertTo-Json -Depth 10 -Compress)" @@ -153,7 +151,6 @@ function New-CIPPCAPolicy { try { Write-Information 'Fetching all named locations...' $AllNamedLocations = New-GraphGETRequest -uri 'https://graph.microsoft.com/beta/identity/conditionalAccess/namedLocations?$top=999' -tenantid $TenantFilter -asApp $true - Write-Information "Found $($AllNamedLocations.Count) existing named locations" } catch { $ErrorMessage = Get-CippException -Exception $_ Write-Information "Error fetching named locations: $($ErrorMessage | ConvertTo-Json -Depth 10 -Compress)" @@ -167,7 +164,6 @@ function New-CIPPCAPolicy { try { Write-Information 'Fetching authentication strength policies...' $AllAuthStrengthPolicies = New-GraphGETRequest -uri 'https://graph.microsoft.com/beta/identity/conditionalAccess/authenticationStrength/policies/' -tenantid $TenantFilter -asApp $true - Write-Information "Found $($AllAuthStrengthPolicies.Count) authentication strength policies" } catch { $ErrorMessage = Get-CippException -Exception $_ Write-Information "Error fetching authentication strength policies: $($ErrorMessage | ConvertTo-Json -Depth 10 -Compress)" @@ -181,7 +177,6 @@ function New-CIPPCAPolicy { try { Write-Information 'Fetching all service principals...' $AllServicePrincipals = New-GraphGETRequest -uri 'https://graph.microsoft.com/v1.0/servicePrincipals?$select=appId&$top=999' -tenantid $TenantFilter -asApp $true - Write-Information "Found $($AllServicePrincipals.Count) service principals" } catch { $ErrorMessage = Get-CippException -Exception $_ Write-Information "Error fetching service principals: $($ErrorMessage | ConvertTo-Json -Depth 10 -Compress)" @@ -189,8 +184,6 @@ function New-CIPPCAPolicy { } } - Write-Information 'All required resources fetched successfully' - #If Grant Controls contains authenticationStrength, create these and then replace the id if ($JSONobj.GrantControls.authenticationStrength.policyType -eq 'custom' -or $JSONobj.GrantControls.authenticationStrength.policyType -eq 'BuiltIn') { $ExistingStrength = $AllAuthStrengthPolicies | Where-Object -Property displayName -EQ $JSONobj.GrantControls.authenticationStrength.displayName @@ -422,9 +415,23 @@ function New-CIPPCAPolicy { Write-Information $RawJSON try { Write-Information 'Checking for existing policies' - # Use cached policies instead of fetching again + # Use cached policies from the beginning $CheckExisting = $AllExistingPolicies | Where-Object -Property displayName -EQ $displayName + + # Handle multiple policies with the same name (should not happen but does) + if ($CheckExisting -is [Array] -and $CheckExisting.Count -gt 1) { + Write-Warning "Found $($CheckExisting.Count) policies with display name '$displayName'. IDs: $($CheckExisting.id -join ', '). Using the first one." + $CheckExisting = $CheckExisting[0] + } + if ($CheckExisting) { + Write-Information "Found existing policy: displayName=$($CheckExisting.displayName), id=$($CheckExisting.id)" + + # Validate the ID before proceeding + if ([string]::IsNullOrWhiteSpace($CheckExisting.id)) { + Write-Information "ERROR: Policy found but ID is null/empty. Full object: $($CheckExisting | ConvertTo-Json -Depth 5 -Compress)" + throw "Found existing policy '$displayName' but ID is null or empty. This may indicate an API issue." + } if ($Overwrite -ne $true) { throw "Conditional Access Policy with Display Name $($displayName) Already exists" return $false From 24ac8f883ec511e8de11df9ec4e946b7af6475e4 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Mon, 16 Feb 2026 12:57:23 +0100 Subject: [PATCH 427/503] POSt request retry logic improvements --- .../GraphHelper/New-GraphPOSTRequest.ps1 | 46 +++++++++++++++---- 1 file changed, 36 insertions(+), 10 deletions(-) diff --git a/Modules/CIPPCore/Public/GraphHelper/New-GraphPOSTRequest.ps1 b/Modules/CIPPCore/Public/GraphHelper/New-GraphPOSTRequest.ps1 index 5d4d1fd3f950..d2ac97fb8839 100644 --- a/Modules/CIPPCore/Public/GraphHelper/New-GraphPOSTRequest.ps1 +++ b/Modules/CIPPCore/Public/GraphHelper/New-GraphPOSTRequest.ps1 @@ -39,25 +39,51 @@ function New-GraphPOSTRequest { $body = Get-CIPPTextReplacement -TenantFilter $tenantid -Text $body -EscapeForJson - $x = 0 + $RetryCount = 0 + $RequestSuccessful = $false do { try { - Write-Information "$($type.ToUpper()) [ $uri ] | tenant: $tenantid | attempt: $($x + 1) of $maxRetries" - $success = $false + Write-Information "$($type.ToUpper()) [ $uri ] | tenant: $tenantid | attempt: $($RetryCount + 1) of $maxRetries" $ReturnedData = (Invoke-RestMethod -Uri $($uri) -Method $TYPE -Body $body -Headers $headers -ContentType $contentType -SkipHttpErrorCheck:$IgnoreErrors -ResponseHeadersVariable responseHeaders) - $success = $true + $RequestSuccessful = $true } catch { - + $ShouldRetry = $false + $WaitTime = 0 $Message = if ($_.ErrorDetails.Message) { Get-NormalizedError -Message $_.ErrorDetails.Message } else { $_.Exception.message } - $x++ - Start-Sleep -Seconds (2 * $x) + + # Check for 429 Too Many Requests + if ($_.Exception.Response.StatusCode -eq 429) { + $RetryAfterHeader = $_.Exception.Response.Headers['Retry-After'] + if ($RetryAfterHeader) { + $WaitTime = [int]$RetryAfterHeader + Write-Warning "Rate limited (429). Waiting $WaitTime seconds before retry. Attempt $($RetryCount + 1) of $maxRetries" + $ShouldRetry = $true + } + } + # Check for "Resource temporarily unavailable" + elseif ($Message -like '*Resource temporarily unavailable*' -or $Message -like '*Too many requests*') { + $WaitTime = Get-Random -Minimum 1.1 -Maximum 3.1 + Write-Warning "Resource temporarily unavailable. Waiting $WaitTime seconds before retry. Attempt $($RetryCount + 1) of $maxRetries" + $ShouldRetry = $true + } + + if ($ShouldRetry) { + $RetryCount++ + if ($RetryCount -lt $maxRetries) { + Start-Sleep -Seconds $WaitTime + } + } else { + # Not a retryable error, exit immediately + break + } } - } while (($x -lt $maxRetries) -and ($success -eq $false)) - if (($maxRetries -and $success -eq $false) -and $ScheduleRetry -eq $true) { + } while (-not $RequestSuccessful -and $RetryCount -lt $maxRetries) + + if (($RequestSuccessful -eq $false) -and $ScheduleRetry -eq $true -and $ShouldRetry -eq $true) { #Create a scheduled task to retry the task later, when there is less pressure on the system, but only if ScheduledRetry is true. try { $TaskId = (New-Guid).Guid.ToString() @@ -102,7 +128,7 @@ function New-GraphPOSTRequest { } } - if ($success -eq $false) { + if ($RequestSuccessful -eq $false) { throw $Message } From c2448ffc1a5999025afac6174f841d2701e98b06 Mon Sep 17 00:00:00 2001 From: rvdwegen Date: Mon, 16 Feb 2026 13:00:27 +0100 Subject: [PATCH 428/503] add log retention logic --- CIPPTimers.json | 13 ++- .../Invoke-ExecLogRetentionConfig.ps1 | 61 ++++++++++++ .../Start-LogRetentionCleanup.ps1 | 96 +++++++++++++++++++ 3 files changed, 168 insertions(+), 2 deletions(-) create mode 100644 Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecLogRetentionConfig.ps1 create mode 100644 Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-LogRetentionCleanup.ps1 diff --git a/CIPPTimers.json b/CIPPTimers.json index 35586b72833e..ed7cd4c31819 100644 --- a/CIPPTimers.json +++ b/CIPPTimers.json @@ -223,12 +223,21 @@ "RunOnProcessor": true, "IsSystem": true }, + { + "Id": "a9e8d7c6-b5a4-3f2e-1d0c-9b8a7f6e5d4c", + "Command": "Start-LogRetentionCleanup", + "Description": "Timer to cleanup old logs based on retention policy", + "Cron": "0 30 2 * * *", + "Priority": 22, + "RunOnProcessor": true, + "IsSystem": true + }, { "Id": "9a7f8e6d-5c4b-3a2d-1e0f-9b8c7d6e5f4a", "Command": "Start-CIPPDBCacheOrchestrator", "Description": "Timer to collect and cache Microsoft Graph data for all tenants", "Cron": "0 0 3 * * *", - "Priority": 22, + "Priority": 23, "RunOnProcessor": true, "IsSystem": true }, @@ -237,7 +246,7 @@ "Command": "Start-TestsOrchestrator", "Description": "Timer to run security and compliance tests against cached data", "Cron": "0 0 4 * * *", - "Priority": 23, + "Priority": 24, "RunOnProcessor": true, "IsSystem": true } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecLogRetentionConfig.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecLogRetentionConfig.ps1 new file mode 100644 index 000000000000..2f8cb0168ec7 --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecLogRetentionConfig.ps1 @@ -0,0 +1,61 @@ +function Invoke-ExecLogRetentionConfig { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + CIPP.AppSettings.ReadWrite + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + $Table = Get-CIPPTable -TableName Config + $Filter = "PartitionKey eq 'LogRetention' and RowKey eq 'Settings'" + + $results = try { + if ($Request.Query.List) { + $RetentionSettings = Get-CIPPAzDataTableEntity @Table -Filter $Filter + if (!$RetentionSettings) { + # Return default values if not set + @{ + RetentionDays = 90 + } + } else { + @{ + RetentionDays = [int]$RetentionSettings.RetentionDays + } + } + } else { + $RetentionDays = [int]$Request.Body.RetentionDays + + # Validate minimum value + if ($RetentionDays -lt 7) { + throw 'Retention days must be at least 7 days' + } + + # Validate maximum value + if ($RetentionDays -gt 365) { + throw 'Retention days must be at most 365 days' + } + + $RetentionConfig = @{ + 'RetentionDays' = $RetentionDays + 'PartitionKey' = 'LogRetention' + 'RowKey' = 'Settings' + } + + Add-CIPPAzDataTableEntity @Table -Entity $RetentionConfig -Force | Out-Null + Write-LogMessage -headers $Request.Headers -API $Request.Params.CIPPEndpoint -message "Set log retention to $RetentionDays days" -Sev 'Info' + "Successfully set log retention to $RetentionDays days" + } + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -headers $Request.Headers -API $Request.Params.CIPPEndpoint -message "Failed to set log retention configuration: $($ErrorMessage.NormalizedError)" -Sev 'Error' -LogData $ErrorMessage + "Failed to set configuration: $($ErrorMessage.NormalizedError)" + } + + $body = [pscustomobject]@{'Results' = $Results } + + return ([HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::OK + Body = $body + }) +} diff --git a/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-LogRetentionCleanup.ps1 b/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-LogRetentionCleanup.ps1 new file mode 100644 index 000000000000..d1e32b85fcfa --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-LogRetentionCleanup.ps1 @@ -0,0 +1,96 @@ +function Start-LogRetentionCleanup { + <# + .SYNOPSIS + Start the Log Retention Cleanup Timer + .DESCRIPTION + This function cleans up old CIPP logs based on the retention policy + #> + [CmdletBinding(SupportsShouldProcess = $true)] + param() + + try { + # Get retention settings + $ConfigTable = Get-CippTable -tablename Config + $Filter = "PartitionKey eq 'LogRetention' and RowKey eq 'Settings'" + $RetentionSettings = Get-CIPPAzDataTableEntity @ConfigTable -Filter $Filter + + # Default to 90 days if not set + $RetentionDays = if ($RetentionSettings.RetentionDays) { + [int]$RetentionSettings.RetentionDays + } else { + 90 + } + + # Ensure minimum retention of 7 days + if ($RetentionDays -lt 7) { + $RetentionDays = 7 + } + + # Ensure maximum retention of 365 days + if ($RetentionDays -gt 365) { + $RetentionDays = 365 + } + + Write-Host "Starting log cleanup with retention of $RetentionDays days" + + # Calculate cutoff date + $CutoffDate = (Get-Date).AddDays(-$RetentionDays).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ') + + $DeletedCount = 0 + + # Clean up CIPP Logs + if ($PSCmdlet.ShouldProcess('CippLogs', 'Cleaning up old logs')) { + $CippLogsTable = Get-CippTable -tablename 'CippLogs' + $CutoffFilter = "Timestamp lt datetime'$CutoffDate'" + + # Fetch all old log entries + $OldLogs = Get-AzDataTableEntity @CippLogsTable -Filter $CutoffFilter -Property @('PartitionKey', 'RowKey', 'ETag') + if ($OldLogs) { + # Delete logs in batches to avoid overwhelming the table service + $BatchSize = 100 + $LogBatches = @() + $CurrentBatch = @() + + foreach ($Log in $OldLogs) { + $CurrentBatch += $Log + if ($CurrentBatch.Count -ge $BatchSize) { + $LogBatches += , @($CurrentBatch) + $CurrentBatch = @() + } + } + + # Add remaining logs as final batch + if ($CurrentBatch.Count -gt 0) { + $LogBatches += , @($CurrentBatch) + } + + # Delete logs in batches + foreach ($Batch in $LogBatches) { + try { + Remove-AzDataTableEntity @CippLogsTable -Entity $Batch -Force + $DeletedCount += $Batch.Count + Write-Host "Deleted batch of $($Batch.Count) log entries" + } catch { + Write-LogMessage -API 'LogRetentionCleanup' -message "Failed to delete log batch: $($_.Exception.Message)" -Sev 'Warning' + } + } + + if ($DeletedCount -gt 0) { + Write-LogMessage -API 'LogRetentionCleanup' -message "Deleted $DeletedCount old log entries (retention: $RetentionDays days)" -Sev 'Info' + Write-Host "Deleted $DeletedCount old log entries" + } else { + Write-Host 'No old logs found' + } + } else { + Write-Host 'No old logs found' + } + } + + Write-LogMessage -API 'LogRetentionCleanup' -message "Log cleanup completed. Total logs deleted: $DeletedCount (retention: $RetentionDays days)" -Sev 'Info' + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'LogRetentionCleanup' -message "Failed to run log cleanup: $($ErrorMessage.NormalizedError)" -Sev 'Error' -LogData $ErrorMessage + throw + } +} From 82558d2c75c78e8e72e8bb8863176295e21d49dd Mon Sep 17 00:00:00 2001 From: rvdwegen Date: Mon, 16 Feb 2026 13:02:49 +0100 Subject: [PATCH 429/503] No batching on old log cleanup --- .../Start-LogRetentionCleanup.ps1 | 40 +++---------------- 1 file changed, 5 insertions(+), 35 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-LogRetentionCleanup.ps1 b/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-LogRetentionCleanup.ps1 index d1e32b85fcfa..19ad42a6c5ad 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-LogRetentionCleanup.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-LogRetentionCleanup.ps1 @@ -45,42 +45,12 @@ function Start-LogRetentionCleanup { # Fetch all old log entries $OldLogs = Get-AzDataTableEntity @CippLogsTable -Filter $CutoffFilter -Property @('PartitionKey', 'RowKey', 'ETag') - if ($OldLogs) { - # Delete logs in batches to avoid overwhelming the table service - $BatchSize = 100 - $LogBatches = @() - $CurrentBatch = @() - - foreach ($Log in $OldLogs) { - $CurrentBatch += $Log - if ($CurrentBatch.Count -ge $BatchSize) { - $LogBatches += , @($CurrentBatch) - $CurrentBatch = @() - } - } - - # Add remaining logs as final batch - if ($CurrentBatch.Count -gt 0) { - $LogBatches += , @($CurrentBatch) - } - # Delete logs in batches - foreach ($Batch in $LogBatches) { - try { - Remove-AzDataTableEntity @CippLogsTable -Entity $Batch -Force - $DeletedCount += $Batch.Count - Write-Host "Deleted batch of $($Batch.Count) log entries" - } catch { - Write-LogMessage -API 'LogRetentionCleanup' -message "Failed to delete log batch: $($_.Exception.Message)" -Sev 'Warning' - } - } - - if ($DeletedCount -gt 0) { - Write-LogMessage -API 'LogRetentionCleanup' -message "Deleted $DeletedCount old log entries (retention: $RetentionDays days)" -Sev 'Info' - Write-Host "Deleted $DeletedCount old log entries" - } else { - Write-Host 'No old logs found' - } + if ($OldLogs) { + Remove-AzDataTableEntity @CippLogsTable -Entity $OldLogs -Force + $DeletedCount = ($OldLogs | Measure-Object).Count + Write-LogMessage -API 'LogRetentionCleanup' -message "Deleted $DeletedCount old log entries (retention: $RetentionDays days)" -Sev 'Info' + Write-Host "Deleted $DeletedCount old log entries" } else { Write-Host 'No old logs found' } From 4252f26a5ce6346808292d3ff3753357543e2782 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Mon, 16 Feb 2026 13:12:17 +0100 Subject: [PATCH 430/503] add member for template --- Modules/CIPPCore/Public/New-CIPPCATemplate.ps1 | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Modules/CIPPCore/Public/New-CIPPCATemplate.ps1 b/Modules/CIPPCore/Public/New-CIPPCATemplate.ps1 index 060b91863208..2a3f4c43b3cc 100644 --- a/Modules/CIPPCore/Public/New-CIPPCATemplate.ps1 +++ b/Modules/CIPPCore/Public/New-CIPPCATemplate.ps1 @@ -46,7 +46,9 @@ function New-CIPPCATemplate { $null = if ($locationinfo) { $includelocations.add($locationinfo.displayName) } else { $includelocations.add($location) } $locationinfo } - if ($includelocations) { $JSON.conditions.locations.includeLocations = $includelocations } + if ($includelocations) { + $JSON.conditions.locations | Add-Member -NotePropertyName 'includeLocations' -NotePropertyValue $includelocations -Force + } $excludelocations = [system.collections.generic.list[object]]::new() $ExcludeJSON = foreach ($Location in $JSON.conditions.locations.excludeLocations) { @@ -55,7 +57,9 @@ function New-CIPPCATemplate { $locationinfo } - if ($excludelocations) { $JSON.conditions.locations.excludeLocations = $excludelocations } + if ($excludelocations) { + $JSON.conditions.locations | Add-Member -NotePropertyName 'excludeLocations' -NotePropertyValue $excludelocations -Force + } # Check if conditions.users exists and is a PSCustomObject (not an array) before accessing properties $hasConditionsUsers = $null -ne $JSON.conditions.users # Explicitly exclude array types - arrays have properties but we can't set custom properties on them From d1aa064bad0f4bc1f72bcb7760729940eb8e8f66 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Mon, 16 Feb 2026 13:27:50 +0100 Subject: [PATCH 431/503] use cache to preload locations --- Modules/CIPPCore/Public/New-CIPPCATemplate.ps1 | 15 ++++++++++----- ...voke-CIPPStandardConditionalAccessTemplate.ps1 | 4 +++- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/Modules/CIPPCore/Public/New-CIPPCATemplate.ps1 b/Modules/CIPPCore/Public/New-CIPPCATemplate.ps1 index 2a3f4c43b3cc..d3b4fd97ad35 100644 --- a/Modules/CIPPCore/Public/New-CIPPCATemplate.ps1 +++ b/Modules/CIPPCore/Public/New-CIPPCATemplate.ps1 @@ -6,7 +6,8 @@ function New-CIPPCATemplate { $APIName = 'Add CIPP CA Template', $Headers, $preloadedUsers, - $preloadedGroups + $preloadedGroups, + $preloadedLocations ) $JSON = ([pscustomobject]$JSON) | ForEach-Object { @@ -34,8 +35,12 @@ function New-CIPPCATemplate { } $namedLocations = $null - if ($JSON.conditions.locations.includeLocations -or $JSON.conditions.locations.excludeLocations) { - $namedLocations = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/identity/conditionalAccess/namedLocations' -tenantid $TenantFilter + if ($preloadedLocations) { + $namedLocations = $preloadedLocations + } else { + if ($JSON.conditions.locations.includeLocations -or $JSON.conditions.locations.excludeLocations) { + $namedLocations = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/identity/conditionalAccess/namedLocations?$top=999' -tenantid $TenantFilter + } } $AllLocations = [system.collections.generic.list[object]]::new() @@ -46,7 +51,7 @@ function New-CIPPCATemplate { $null = if ($locationinfo) { $includelocations.add($locationinfo.displayName) } else { $includelocations.add($location) } $locationinfo } - if ($includelocations) { + if ($includelocations) { $JSON.conditions.locations | Add-Member -NotePropertyName 'includeLocations' -NotePropertyValue $includelocations -Force } @@ -57,7 +62,7 @@ function New-CIPPCATemplate { $locationinfo } - if ($excludelocations) { + if ($excludelocations) { $JSON.conditions.locations | Add-Member -NotePropertyName 'excludeLocations' -NotePropertyValue $excludelocations -Force } # Check if conditions.users exists and is a PSCustomObject (not an array) before accessing properties diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardConditionalAccessTemplate.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardConditionalAccessTemplate.ps1 index a41018355017..e9f81a0323a5 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardConditionalAccessTemplate.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardConditionalAccessTemplate.ps1 @@ -90,6 +90,7 @@ function Invoke-CIPPStandardConditionalAccessTemplate { $Filter = "PartitionKey eq 'CATemplate'" $Policies = (Get-CippAzDataTableEntity @Table -Filter $Filter | Where-Object RowKey -In $Settings.TemplateList.value).JSON | ConvertFrom-Json -Depth 10 $AllCAPolicies = New-GraphGetRequest -Uri 'https://graph.microsoft.com/beta/identity/conditionalAccess/policies?$top=999' -tenantid $Tenant -asApp $true + #check if all groups.displayName are in the existingGroups, if not $fieldvalue should contain all missing groups, else it should be true. $MissingPolicies = foreach ($Setting in $Settings.TemplateList) { $policy = $Policies | Where-Object { $_.displayName -eq $Setting.label } @@ -105,7 +106,8 @@ function Invoke-CIPPStandardConditionalAccessTemplate { Set-CIPPStandardsCompareField -FieldName "standards.ConditionalAccessTemplate.$($Setting.value)" -FieldValue "Policy $($Setting.label) is missing from this tenant." -Tenant $Tenant } } else { - $templateResult = New-CIPPCATemplate -TenantFilter $tenant -JSON $CheckExististing + $preloadedLocations = New-CIPPDbRequest -TenantFilter $tenant -Type 'NamedLocations' + $templateResult = New-CIPPCATemplate -TenantFilter $tenant -JSON $CheckExististing -preloadedLocations $preloadedLocations $CompareObj = ConvertFrom-Json -ErrorAction SilentlyContinue -InputObject $templateResult try { $Compare = Compare-CIPPIntuneObject -ReferenceObject $policy -DifferenceObject $CompareObj -CompareType 'ca' From b4b9d6432d53bfa41bc6c34a2a24bca61c2f06ed Mon Sep 17 00:00:00 2001 From: rvdwegen Date: Mon, 16 Feb 2026 14:01:12 +0100 Subject: [PATCH 432/503] Minor standards optimization by moving license checks --- .../Invoke-CIPPStandardConditionalAccessTemplate.ps1 | 6 ++++-- .../Standards/Invoke-CIPPStandardGroupTemplate.ps1 | 12 +++++++----- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardConditionalAccessTemplate.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardConditionalAccessTemplate.ps1 index a41018355017..9c4497aa5752 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardConditionalAccessTemplate.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardConditionalAccessTemplate.ps1 @@ -34,9 +34,7 @@ function Invoke-CIPPStandardConditionalAccessTemplate { #> param($Tenant, $Settings) ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'ConditionalAccess' - $Table = Get-CippTable -tablename 'templates' $TestResult = Test-CIPPStandardLicense -StandardName 'ConditionalAccessTemplate_general' -TenantFilter $Tenant -RequiredCapabilities @('AAD_PREMIUM', 'AAD_PREMIUM_P2') - $TestP2 = Test-CIPPStandardLicense -StandardName 'ConditionalAccessTemplate_p2' -TenantFilter $Tenant -RequiredCapabilities @('AAD_PREMIUM_P2') -SkipLog if ($TestResult -eq $false) { #writing to each item that the license is not present. foreach ($Template in $settings.TemplateList) { @@ -45,6 +43,8 @@ function Invoke-CIPPStandardConditionalAccessTemplate { return $true } #we're done. + $Table = Get-CippTable -tablename 'templates' + try { $AllCAPolicies = New-GraphGetRequest -Uri 'https://graph.microsoft.com/beta/identity/conditionalAccess/policies?$top=999' -tenantid $Tenant -asApp $true } catch { @@ -60,6 +60,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 if (!$TestP2) { Write-Information "Skipping policy $($Policy.displayName) as it requires AAD Premium P2 license." Set-CIPPStandardsCompareField -FieldName "standards.ConditionalAccessTemplate.$($Setting.value)" -FieldValue "Policy $($Policy.displayName) requires AAD Premium P2 license." -Tenant $Tenant @@ -96,6 +97,7 @@ function Invoke-CIPPStandardConditionalAccessTemplate { $CheckExististing = $AllCAPolicies | Where-Object -Property displayName -EQ $Setting.label if (!$CheckExististing) { if ($Setting.conditions.userRiskLevels.Count -gt 0 -or $Setting.conditions.signInRiskLevels.Count -gt 0) { + $TestP2 = Test-CIPPStandardLicense -StandardName 'ConditionalAccessTemplate_p2' -TenantFilter $Tenant -RequiredCapabilities @('AAD_PREMIUM_P2') -SkipLog if (!$TestP2) { Set-CIPPStandardsCompareField -FieldName "standards.ConditionalAccessTemplate.$($Setting.value)" -FieldValue "Policy $($Setting.label) requires AAD Premium P2 license." -Tenant $Tenant } else { diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardGroupTemplate.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardGroupTemplate.ps1 index ed52d13b0b4b..9eddfc36d59b 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardGroupTemplate.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardGroupTemplate.ps1 @@ -33,8 +33,6 @@ function Invoke-CIPPStandardGroupTemplate { $existingGroups = New-GraphGETRequest -uri 'https://graph.microsoft.com/beta/groups?$top=999&$select=id,displayName,description,membershipRule' -tenantid $tenant - $TestResult = Test-CIPPStandardLicense -StandardName 'GroupTemplate' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') -SkipLog - $Settings.groupTemplate ? ($Settings | Add-Member -NotePropertyName 'TemplateList' -NotePropertyValue $Settings.groupTemplate) : $null $Table = Get-CippTable -tablename 'templates' @@ -64,9 +62,12 @@ function Invoke-CIPPStandardGroupTemplate { $ActionType = 'create' # Check if Exchange license is required for distribution groups - if ($groupobj.groupType -in @('distribution', 'dynamicdistribution') -and !$TestResult) { - Write-LogMessage -API 'Standards' -tenant $tenant -message "Cannot create group $($groupobj.displayname) as the tenant is not licensed for Exchange." -Sev 'Error' - continue + if ($groupobj.groupType -in @('distribution', 'dynamicdistribution')) { + $TestResult = Test-CIPPStandardLicense -StandardName 'GroupTemplate' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_LITE') -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 + } } # Use the centralized New-CIPPGroup function @@ -127,6 +128,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 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 From df29e37dde90b529b31acf312d1bf4cb03b41f99 Mon Sep 17 00:00:00 2001 From: rvdwegen Date: Mon, 16 Feb 2026 14:19:36 +0100 Subject: [PATCH 433/503] Duplicate API call in CIPPStandardSafeAttachmentPolicy --- .../Invoke-CIPPStandardSafeAttachmentPolicy.ps1 | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeAttachmentPolicy.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeAttachmentPolicy.ps1 index 79c6d309ee85..4a9d73a371d4 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeAttachmentPolicy.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeAttachmentPolicy.ps1 @@ -57,6 +57,14 @@ function Invoke-CIPPStandardSafeAttachmentPolicy { Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the SafeAttachmentPolicy state for $Tenant. Error: $ErrorMessage" -Sev Error return } + # Cache all Safe Attachment Rules to avoid duplicate API calls + try { + $AllSafeAttachmentRule = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-SafeAttachmentRule' + } catch { + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the SafeAttachmentRule state for $Tenant. Error: $ErrorMessage" -Sev Error + return + } # Use custom name if provided, otherwise use default for backward compatibility $PolicyName = if ($Settings.name) { $Settings.name } else { 'CIPP Default Safe Attachment Policy' } @@ -72,7 +80,7 @@ function Invoke-CIPPStandardSafeAttachmentPolicy { # Derive rule name from policy name, but check for old names for backward compatibility $DesiredRuleName = "$PolicyName Rule" $RuleList = @($DesiredRuleName, 'CIPP Default Safe Attachment Rule', 'CIPP Default Safe Attachment Policy') - $ExistingRule = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-SafeAttachmentRule' | Where-Object -Property Name -In $RuleList | Select-Object -First 1 + $ExistingRule = $AllSafeAttachmentRule | Where-Object -Property Name -In $RuleList | Select-Object -First 1 if ($null -eq $ExistingRule.Name) { # No existing rule - use the derived name $RuleName = $DesiredRuleName @@ -94,7 +102,7 @@ function Invoke-CIPPStandardSafeAttachmentPolicy { $AcceptedDomains = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-AcceptedDomain' - $RuleState = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-SafeAttachmentRule' | + $RuleState = $AllSafeAttachmentRule | Where-Object -Property Name -EQ $RuleName | Select-Object Name, SafeAttachmentPolicy, Priority, RecipientDomainIs From 900aa1de0d6da035813c2053d1214263d767f200 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Mon, 16 Feb 2026 14:20:35 +0100 Subject: [PATCH 434/503] added rate limit capture for environments without retry header --- Modules/CIPPCore/Public/GraphHelper/New-GraphGetRequest.ps1 | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Modules/CIPPCore/Public/GraphHelper/New-GraphGetRequest.ps1 b/Modules/CIPPCore/Public/GraphHelper/New-GraphGetRequest.ps1 index f5dc35876afc..23b0e59d3dc7 100644 --- a/Modules/CIPPCore/Public/GraphHelper/New-GraphGetRequest.ps1 +++ b/Modules/CIPPCore/Public/GraphHelper/New-GraphGetRequest.ps1 @@ -160,6 +160,11 @@ function New-GraphGetRequest { $WaitTime = [int]$RetryAfterHeader Write-Warning "Rate limited (429). Waiting $WaitTime seconds before retry. Attempt $($RetryCount + 1) of $MaxRetries" $ShouldRetry = $true + } else { + # If no Retry-After header, use exponential backoff with jitter + $WaitTime = Get-Random -Minimum 1.1 -Maximum 4.1 # Random sleep between 1-4 seconds + Write-Warning "Rate limited (429) with no Retry-After header. Waiting $WaitTime seconds before retry. Attempt $($RetryCount + 1) of $MaxRetries. Headers: $(($HttpResponseDetails.Headers | ConvertTo-Json -Compress))" + $ShouldRetry = $true } } # Check for "Resource temporarily unavailable" From 4afa2748d4f13b8da1f6075d6492be6955176145 Mon Sep 17 00:00:00 2001 From: rvdwegen Date: Mon, 16 Feb 2026 14:38:48 +0100 Subject: [PATCH 435/503] Move NamedLocations CIPPDbRequest outside of the loop --- .../Invoke-CIPPStandardConditionalAccessTemplate.ps1 | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardConditionalAccessTemplate.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardConditionalAccessTemplate.ps1 index 9f8a8559c2a7..c1455d3ba37b 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardConditionalAccessTemplate.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardConditionalAccessTemplate.ps1 @@ -92,6 +92,9 @@ function Invoke-CIPPStandardConditionalAccessTemplate { $Policies = (Get-CippAzDataTableEntity @Table -Filter $Filter | Where-Object RowKey -In $Settings.TemplateList.value).JSON | ConvertFrom-Json -Depth 10 $AllCAPolicies = New-GraphGetRequest -Uri 'https://graph.microsoft.com/beta/identity/conditionalAccess/policies?$top=999' -tenantid $Tenant -asApp $true + # Preload named locations once outside the loop to avoid duplicate database queries + $preloadedLocations = New-CIPPDbRequest -TenantFilter $tenant -Type 'NamedLocations' + #check if all groups.displayName are in the existingGroups, if not $fieldvalue should contain all missing groups, else it should be true. $MissingPolicies = foreach ($Setting in $Settings.TemplateList) { $policy = $Policies | Where-Object { $_.displayName -eq $Setting.label } @@ -108,7 +111,6 @@ function Invoke-CIPPStandardConditionalAccessTemplate { Set-CIPPStandardsCompareField -FieldName "standards.ConditionalAccessTemplate.$($Setting.value)" -FieldValue "Policy $($Setting.label) is missing from this tenant." -Tenant $Tenant } } else { - $preloadedLocations = New-CIPPDbRequest -TenantFilter $tenant -Type 'NamedLocations' $templateResult = New-CIPPCATemplate -TenantFilter $tenant -JSON $CheckExististing -preloadedLocations $preloadedLocations $CompareObj = ConvertFrom-Json -ErrorAction SilentlyContinue -InputObject $templateResult try { From 970454c9ec6797705441f9513acc7b6444a3f7d8 Mon Sep 17 00:00:00 2001 From: rvdwegen Date: Mon, 16 Feb 2026 14:45:20 +0100 Subject: [PATCH 436/503] Move helper functions outside of New-CIPPCAPolicy --- .../Public/Functions/Remove-EmptyArrays.ps1 | 50 +++++++++++++++++++ .../CIPPCore/Public/Functions/Test-IsGuid.ps1 | 23 +++++++++ Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 | 29 ++--------- .../CIPPCore/Public/New-CIPPCATemplate.ps1 | 9 +--- 4 files changed, 78 insertions(+), 33 deletions(-) create mode 100644 Modules/CIPPCore/Public/Functions/Remove-EmptyArrays.ps1 create mode 100644 Modules/CIPPCore/Public/Functions/Test-IsGuid.ps1 diff --git a/Modules/CIPPCore/Public/Functions/Remove-EmptyArrays.ps1 b/Modules/CIPPCore/Public/Functions/Remove-EmptyArrays.ps1 new file mode 100644 index 000000000000..85726be4607a --- /dev/null +++ b/Modules/CIPPCore/Public/Functions/Remove-EmptyArrays.ps1 @@ -0,0 +1,50 @@ +function Remove-EmptyArrays { + <# + .SYNOPSIS + Recursively removes empty arrays and null properties from objects + .DESCRIPTION + This function recursively traverses an object (Array, Hashtable, or PSCustomObject) and removes: + - Empty arrays + - Null properties + The function modifies the object in place. + .PARAMETER Object + The object to process (can be Array, Hashtable, or PSCustomObject) + .FUNCTIONALITY + Internal + .EXAMPLE + $obj = @{ items = @(); name = "test"; value = $null } + Remove-EmptyArrays -Object $obj + .EXAMPLE + $obj = [PSCustomObject]@{ items = @(); name = "test" } + Remove-EmptyArrays -Object $obj + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [object]$Object + ) + + if ($Object -is [Array]) { + foreach ($Item in $Object) { + Remove-EmptyArrays -Object $Item + } + } elseif ($Object -is [HashTable]) { + foreach ($Key in @($Object.get_Keys())) { + if ($Object[$Key] -is [Array] -and $Object[$Key].get_Count() -eq 0) { + $Object.Remove($Key) + } else { + Remove-EmptyArrays -Object $Object[$Key] + } + } + } elseif ($Object -is [PSCustomObject]) { + foreach ($Name in @($Object.PSObject.Properties.Name)) { + if ($Object.$Name -is [Array] -and $Object.$Name.get_Count() -eq 0) { + $Object.PSObject.Properties.Remove($Name) + } elseif ($null -eq $Object.$Name) { + $Object.PSObject.Properties.Remove($Name) + } else { + Remove-EmptyArrays -Object $Object.$Name + } + } + } +} diff --git a/Modules/CIPPCore/Public/Functions/Test-IsGuid.ps1 b/Modules/CIPPCore/Public/Functions/Test-IsGuid.ps1 new file mode 100644 index 000000000000..3beda62ec9cf --- /dev/null +++ b/Modules/CIPPCore/Public/Functions/Test-IsGuid.ps1 @@ -0,0 +1,23 @@ +function Test-IsGuid { + <# + .SYNOPSIS + Tests if a string is a valid GUID + .DESCRIPTION + This function checks if a string can be parsed as a valid GUID using .NET's Guid.TryParse method. + .PARAMETER String + The string to test for GUID format + .FUNCTIONALITY + Internal + .EXAMPLE + Test-IsGuid -String "123e4567-e89b-12d3-a456-426614174000" + .EXAMPLE + Test-IsGuid -String "not-a-guid" + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$String + ) + + return [guid]::TryParse($String, [ref][guid]::Empty) +} diff --git a/Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 b/Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 index 5abdc29c44a6..e97eb74500d3 100644 --- a/Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 +++ b/Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 @@ -14,36 +14,13 @@ function New-CIPPCAPolicy { $PreloadedCAPolicies = $null ) - function Remove-EmptyArrays ($Object) { - if ($Object -is [Array]) { - foreach ($Item in $Object) { Remove-EmptyArrays $Item } - } elseif ($Object -is [HashTable]) { - foreach ($Key in @($Object.get_Keys())) { - if ($Object[$Key] -is [Array] -and $Object[$Key].get_Count() -eq 0) { - $Object.Remove($Key) - } else { Remove-EmptyArrays $Object[$Key] } - } - } elseif ($Object -is [PSCustomObject]) { - foreach ($Name in @($Object.PSObject.Properties.Name)) { - if ($Object.$Name -is [Array] -and $Object.$Name.get_Count() -eq 0) { - $Object.PSObject.Properties.Remove($Name) - } elseif ($null -eq $Object.$Name) { - $Object.PSObject.Properties.Remove($Name) - } else { Remove-EmptyArrays $Object.$Name } - } - } - } - # Function to check if a string is a GUID - function Test-IsGuid($string) { - return [guid]::TryParse($string, [ref][guid]::Empty) - } # Helper function to replace group display names with GUIDs function Convert-GroupNameToId { param($TenantFilter, $groupNames, $CreateGroups, $GroupTemplates) $GroupIds = [System.Collections.Generic.List[string]]::new() $groupNames | ForEach-Object { - if (Test-IsGuid $_) { + if (Test-IsGuid -String $_) { Write-LogMessage -Headers $Headers -API $APIName -message "Already GUID, no need to replace: $_" -Sev 'Debug' $GroupIds.Add($_) # it's a GUID, so we keep it } else { @@ -89,7 +66,7 @@ function New-CIPPCAPolicy { $UserIds = [System.Collections.Generic.List[string]]::new() $userNames | ForEach-Object { - if (Test-IsGuid $_) { + if (Test-IsGuid -String $_) { Write-LogMessage -Headers $Headers -API $APIName -message "Already GUID, no need to replace: $_" -Sev 'Debug' $UserIds.Add($_) # it's a GUID, so we keep it } else { @@ -111,7 +88,7 @@ function New-CIPPCAPolicy { $displayName = ($RawJSON | ConvertFrom-Json).displayName $JSONobj = $RawJSON | ConvertFrom-Json | Select-Object * -ExcludeProperty ID, GUID, *time* - Remove-EmptyArrays $JSONobj + Remove-EmptyArrays -Object $JSONobj #Remove context as it does not belong in the payload. try { $JSONobj.grantControls.PSObject.Properties.Remove('authenticationStrength@odata.context') diff --git a/Modules/CIPPCore/Public/New-CIPPCATemplate.ps1 b/Modules/CIPPCore/Public/New-CIPPCATemplate.ps1 index d3b4fd97ad35..6103d5ad945c 100644 --- a/Modules/CIPPCore/Public/New-CIPPCATemplate.ps1 +++ b/Modules/CIPPCore/Public/New-CIPPCATemplate.ps1 @@ -18,11 +18,6 @@ function New-CIPPCATemplate { Write-Information "Processing CA Template for tenant $TenantFilter" Write-Information ($JSON | ConvertTo-Json -Depth 10) - # Function to check if a string is a GUID - function Test-IsGuid($string) { - return [guid]::tryparse($string, [ref][guid]::Empty) - } - if ($preloadedUsers) { $users = $preloadedUsers } else { @@ -94,7 +89,7 @@ function New-CIPPCATemplate { if ($isPSCustomObject -and $null -ne $JSON.conditions.users.includeGroups) { $JSON.conditions.users.includeGroups = @($JSON.conditions.users.includeGroups | ForEach-Object { $originalID = $_ - if ($_ -in 'All', 'None', 'GuestOrExternalUsers' -or -not (Test-IsGuid $_)) { return $_ } + if ($_ -in 'All', 'None', 'GuestOrExternalUsers' -or -not (Test-IsGuid -String $_)) { return $_ } $match = $groups | Where-Object { $_.id -eq $originalID } if ($match) { $match.displayName } else { $originalID } }) @@ -102,7 +97,7 @@ function New-CIPPCATemplate { if ($isPSCustomObject -and $null -ne $JSON.conditions.users.excludeGroups) { $JSON.conditions.users.excludeGroups = @($JSON.conditions.users.excludeGroups | ForEach-Object { $originalID = $_ - if ($_ -in 'All', 'None', 'GuestOrExternalUsers' -or -not (Test-IsGuid $_)) { return $_ } + if ($_ -in 'All', 'None', 'GuestOrExternalUsers' -or -not (Test-IsGuid -String $_)) { return $_ } $match = $groups | Where-Object { $_.id -eq $originalID } if ($match) { $match.displayName } else { $originalID } }) From 42608852ca931135b8268ab8a4810b2bec409ff5 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Mon, 16 Feb 2026 16:50:36 +0100 Subject: [PATCH 437/503] updated CATemplates --- .../Standards/Push-CIPPStandardsList.ps1 | 32 ++++ Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 | 22 ++- ...-CIPPStandardConditionalAccessTemplate.ps1 | 142 +++++++++--------- 3 files changed, 117 insertions(+), 79 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Standards/Push-CIPPStandardsList.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Standards/Push-CIPPStandardsList.ps1 index 01acec8da950..c0af2f9c5467 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Standards/Push-CIPPStandardsList.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Standards/Push-CIPPStandardsList.ps1 @@ -180,6 +180,38 @@ 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') + if (-not $TestResult) { + $CAKeys = @($ComputedStandards.Keys | Where-Object { $_ -like '*ConditionalAccessTemplate*' }) + $BulkFields = [System.Collections.Generic.List[object]]::new() + foreach ($Key in $CAKeys) { + $TemplateKey = ($Key -split '\|', 2)[1] + if ($TemplateKey) { + $BulkFields.Add([PSCustomObject]@{ + FieldName = "standards.ConditionalAccessTemplate.$TemplateKey" + FieldValue = 'This tenant does not have the required license for this standard.' + }) + } + [void]$ComputedStandards.Remove($Key) + } + if ($BulkFields.Count -gt 0) { + Set-CIPPStandardsCompareField -TenantFilter $TenantFilter -BulkFields $BulkFields + } + + Write-Information "Removed ConditionalAccessTemplate standards for $TenantFilter - missing required license" + } else { + # License valid - update CIPPDB cache with latest CA information before we run so that standards have the most up to date info + try { + Write-Information "Updating CIPPDB cache for Conditional Access policies for $TenantFilter" + Set-CIPPDBCacheConditionalAccessPolicies -TenantFilter $TenantFilter + } catch { + Write-Warning "Failed to update CA cache for $TenantFilter : $($_.Exception.Message)" + } + } + } + Write-Host "Returning $($ComputedStandards.Count) standards for tenant $TenantFilter after filtering." # Return filtered standards $FilteredStandards = $ComputedStandards.Values | ForEach-Object { diff --git a/Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 b/Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 index 5abdc29c44a6..bcea50fb716b 100644 --- a/Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 +++ b/Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 @@ -11,7 +11,8 @@ function New-CIPPCAPolicy { $CreateGroups = $false, $APIName = 'Create CA Policy', $Headers, - $PreloadedCAPolicies = $null + $PreloadedCAPolicies = $null, + $PreloadedLocations = $null ) function Remove-EmptyArrays ($Object) { @@ -148,13 +149,18 @@ function New-CIPPCAPolicy { # Get named locations once if needed $AllNamedLocations = $null if ($JSONobj.LocationInfo) { - try { - Write-Information 'Fetching all named locations...' - $AllNamedLocations = New-GraphGETRequest -uri 'https://graph.microsoft.com/beta/identity/conditionalAccess/namedLocations?$top=999' -tenantid $TenantFilter -asApp $true - } catch { - $ErrorMessage = Get-CippException -Exception $_ - Write-Information "Error fetching named locations: $($ErrorMessage | ConvertTo-Json -Depth 10 -Compress)" - throw "Failed to fetch named locations: $($ErrorMessage.NormalizedError)" + if ($PreloadedLocations) { + Write-Information 'Using preloaded named locations' + $AllNamedLocations = $PreloadedLocations + } else { + try { + Write-Information 'Fetching all named locations...' + $AllNamedLocations = New-GraphGETRequest -uri 'https://graph.microsoft.com/beta/identity/conditionalAccess/namedLocations?$top=999' -tenantid $TenantFilter -asApp $true + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-Information "Error fetching named locations: $($ErrorMessage | ConvertTo-Json -Depth 10 -Compress)" + throw "Failed to fetch named locations: $($ErrorMessage.NormalizedError)" + } } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardConditionalAccessTemplate.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardConditionalAccessTemplate.ps1 index c1455d3ba37b..85261df84091 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardConditionalAccessTemplate.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardConditionalAccessTemplate.ps1 @@ -33,20 +33,29 @@ function Invoke-CIPPStandardConditionalAccessTemplate { https://docs.cipp.app/user-documentation/tenant/standards/list-standards #> param($Tenant, $Settings) - ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'ConditionalAccess' + + #Checking if the DB has been updated in the last 3h, if not, run an update before we run the standard, as CA policies are critical and we want to make sure we have the latest state before making changes or comparisons. + $LastDBUpdate = Get-CIPPDbItem -TenantFilter $Tenant -Type 'ConditionalAccessPolicies' -CountsOnly + if ($LastDBUpdate -eq $null -or ($LastDBUpdate.Timestamp -lt (Get-Date).AddHours(-3) -or $LastDBUpdate.DataCount -eq 0)) { + Write-Information "DB last updated at $($LastDBUpdate.Timestamp). Updating DB before running standard, this is probably a manual run." + Set-CIPPDBCacheConditionalAccessPolicies -TenantFilter $Tenant + } else { + Write-Information "DB last updated at $($LastDBUpdate.Timestamp). No need to update before running standard." + } + + $TestResult = Test-CIPPStandardLicense -StandardName 'ConditionalAccessTemplate_general' -TenantFilter $Tenant -RequiredCapabilities @('AAD_PREMIUM', 'AAD_PREMIUM_P2') if ($TestResult -eq $false) { - #writing to each item that the license is not present. - foreach ($Template in $settings.TemplateList) { - Set-CIPPStandardsCompareField -FieldName "standards.ConditionalAccessTemplate.$($Template.value)" -FieldValue 'This tenant does not have the required license for this standard.' -Tenant $Tenant - } + Set-CIPPStandardsCompareField -FieldName "standards.ConditionalAccessTemplate.$($Settings.TemplateList.value)" -FieldValue 'This tenant does not have the required license for this standard.' -Tenant $Tenant return $true } #we're done. $Table = Get-CippTable -tablename 'templates' try { - $AllCAPolicies = New-GraphGetRequest -Uri 'https://graph.microsoft.com/beta/identity/conditionalAccess/policies?$top=999' -tenantid $Tenant -asApp $true + #Get from DB, as we just downloaded the latest before the standard runs. + $AllCAPolicies = New-CIPPDbRequest -TenantFilter $tenant -Type 'ConditionalAccessPolicies' + $PreloadedLocations = New-CIPPDbRequest -TenantFilter $tenant -Type 'NamedLocations' } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the ConditionalAccessTemplate state for $Tenant. Error: $ErrorMessage" -Sev Error @@ -54,80 +63,71 @@ function Invoke-CIPPStandardConditionalAccessTemplate { } if ($Settings.remediate -eq $true) { - foreach ($Setting in $Settings) { - try { - $Filter = "PartitionKey eq 'CATemplate' and RowKey eq '$($Setting.TemplateList.value)'" - $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 - if (!$TestP2) { - Write-Information "Skipping policy $($Policy.displayName) as it requires AAD Premium P2 license." - Set-CIPPStandardsCompareField -FieldName "standards.ConditionalAccessTemplate.$($Setting.value)" -FieldValue "Policy $($Policy.displayName) requires AAD Premium P2 license." -Tenant $Tenant - continue - } - } - $NewCAPolicy = @{ - replacePattern = 'displayName' - TenantFilter = $Tenant - state = $Setting.state - RawJSON = $JSONObj - Overwrite = $true - APIName = 'Standards' - Headers = $Request.Headers - DisableSD = $Setting.DisableSD - CreateGroups = $Setting.CreateGroups ?? $false - PreloadedCAPolicies = $AllCAPolicies + try { + $Filter = "PartitionKey eq 'CATemplate' and RowKey eq '$($Settings.TemplateList.value)'" + $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 + 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 + return $true } - - $null = New-CIPPCAPolicy @NewCAPolicy - } catch { - $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message - Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to create or update conditional access rule $($JSONObj.displayName). Error: $ErrorMessage" -sev 'Error' } + $NewCAPolicy = @{ + replacePattern = 'displayName' + TenantFilter = $Tenant + state = $Settings.state + RawJSON = $JSONObj + Overwrite = $true + APIName = 'Standards' + Headers = $Request.Headers + DisableSD = $Settings.DisableSD + CreateGroups = $Settings.CreateGroups ?? $false + PreloadedCAPolicies = $AllCAPolicies + PreloadedLocations = $PreloadedLocations + } + + $null = New-CIPPCAPolicy @NewCAPolicy + } catch { + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to create or update conditional access rule $($JSONObj.displayName). Error: $ErrorMessage" -sev 'Error' } } if ($Settings.report -eq $true -or $Settings.remediate -eq $true) { - $Filter = "PartitionKey eq 'CATemplate'" - $Policies = (Get-CippAzDataTableEntity @Table -Filter $Filter | Where-Object RowKey -In $Settings.TemplateList.value).JSON | ConvertFrom-Json -Depth 10 - $AllCAPolicies = New-GraphGetRequest -Uri 'https://graph.microsoft.com/beta/identity/conditionalAccess/policies?$top=999' -tenantid $Tenant -asApp $true - - # Preload named locations once outside the loop to avoid duplicate database queries - $preloadedLocations = New-CIPPDbRequest -TenantFilter $tenant -Type 'NamedLocations' + $Filter = "PartitionKey eq 'CATemplate' and RowKey eq '$($Settings.TemplateList.value)'" + $Policy = (Get-CippAzDataTableEntity @Table -Filter $Filter).JSON | ConvertFrom-Json -Depth 10 - #check if all groups.displayName are in the existingGroups, if not $fieldvalue should contain all missing groups, else it should be true. - $MissingPolicies = foreach ($Setting in $Settings.TemplateList) { - $policy = $Policies | Where-Object { $_.displayName -eq $Setting.label } - $CheckExististing = $AllCAPolicies | Where-Object -Property displayName -EQ $Setting.label - if (!$CheckExististing) { - if ($Setting.conditions.userRiskLevels.Count -gt 0 -or $Setting.conditions.signInRiskLevels.Count -gt 0) { - $TestP2 = Test-CIPPStandardLicense -StandardName 'ConditionalAccessTemplate_p2' -TenantFilter $Tenant -RequiredCapabilities @('AAD_PREMIUM_P2') -SkipLog - if (!$TestP2) { - Set-CIPPStandardsCompareField -FieldName "standards.ConditionalAccessTemplate.$($Setting.value)" -FieldValue "Policy $($Setting.label) requires AAD Premium P2 license." -Tenant $Tenant - } else { - Set-CIPPStandardsCompareField -FieldName "standards.ConditionalAccessTemplate.$($Setting.value)" -FieldValue "Policy $($Setting.label) is missing from this tenant." -Tenant $Tenant - } + $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 + if (!$TestP2) { + Set-CIPPStandardsCompareField -FieldName "standards.ConditionalAccessTemplate.$($Settings.TemplateList.value)" -FieldValue "Policy $($Settings.TemplateList.label) requires AAD Premium P2 license." -Tenant $Tenant } else { - Set-CIPPStandardsCompareField -FieldName "standards.ConditionalAccessTemplate.$($Setting.value)" -FieldValue "Policy $($Setting.label) is missing from this tenant." -Tenant $Tenant + Set-CIPPStandardsCompareField -FieldName "standards.ConditionalAccessTemplate.$($Settings.TemplateList.value)" -FieldValue "Policy $($Settings.TemplateList.label) is missing from this tenant." -Tenant $Tenant } } else { - $templateResult = New-CIPPCATemplate -TenantFilter $tenant -JSON $CheckExististing -preloadedLocations $preloadedLocations - $CompareObj = ConvertFrom-Json -ErrorAction SilentlyContinue -InputObject $templateResult - try { - $Compare = Compare-CIPPIntuneObject -ReferenceObject $policy -DifferenceObject $CompareObj -CompareType 'ca' - } catch { - Write-LogMessage -API 'Standards' -tenant $Tenant -message "Error comparing CA policy: $($_.Exception.Message)" -sev Error - Set-CIPPStandardsCompareField -FieldName "standards.ConditionalAccessTemplate.$($Setting.value)" -FieldValue "Error comparing policy: $($_.Exception.Message)" -Tenant $Tenant - continue - } - if (!$Compare) { - Set-CIPPStandardsCompareField -FieldName "standards.ConditionalAccessTemplate.$($Setting.value)" -FieldValue $true -Tenant $Tenant - } else { - #this can still be prettified but is for later. - $ExpectedValue = @{ 'Differences' = @() } - $CurrentValue = @{ 'Differences' = $Compare } - Set-CIPPStandardsCompareField -FieldName "standards.ConditionalAccessTemplate.$($Setting.value)" -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -Tenant $Tenant - } + Set-CIPPStandardsCompareField -FieldName "standards.ConditionalAccessTemplate.$($Settings.TemplateList.value)" -FieldValue "Policy $($Settings.TemplateList.label) is missing from this tenant." -Tenant $Tenant + } + } else { + $templateResult = New-CIPPCATemplate -TenantFilter $tenant -JSON $CheckExististing -preloadedLocations $preloadedLocations + $CompareObj = ConvertFrom-Json -ErrorAction SilentlyContinue -InputObject $templateResult + try { + $Compare = Compare-CIPPIntuneObject -ReferenceObject $Policy -DifferenceObject $CompareObj -CompareType 'ca' + } catch { + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Error comparing CA policy: $($_.Exception.Message)" -sev Error + Set-CIPPStandardsCompareField -FieldName "standards.ConditionalAccessTemplate.$($Settings.TemplateList.value)" -FieldValue "Error comparing policy: $($_.Exception.Message)" -Tenant $Tenant + return + } + if (!$Compare) { + Set-CIPPStandardsCompareField -FieldName "standards.ConditionalAccessTemplate.$($Settings.TemplateList.value)" -FieldValue $true -Tenant $Tenant + } else { + #this can still be prettified but is for later. + $ExpectedValue = @{ 'Differences' = @() } + $CurrentValue = @{ 'Differences' = $Compare } + Set-CIPPStandardsCompareField -FieldName "standards.ConditionalAccessTemplate.$($Settings.TemplateList.value)" -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -Tenant $Tenant } } } From 44d6eb8f9db4246703138a60062cc19bbf2cf6b1 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Mon, 16 Feb 2026 14:52:05 -0500 Subject: [PATCH 438/503] Refine mailbox matching and contact filter Remove displayName from the local usernames array comparison to avoid unintended matches. When building the Graph contacts filter, keep displayName as-is but strip spaces from mailNickname and properly escape single quotes so mailNickname queries match Graph-stored values. This adjusts matching behavior for more accurate mailbox/contact lookups. --- .../Administration/Users/Invoke-ListUserMailboxDetails.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserMailboxDetails.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserMailboxDetails.ps1 index 636639f32296..a0336ea3f1f9 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserMailboxDetails.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserMailboxDetails.ps1 @@ -155,7 +155,6 @@ function Invoke-ListUserMailboxDetails { # First try users array $matchedUser = $usernames | Where-Object { $_.id -eq $rawAddress -or - $_.displayName -eq $rawAddress -or $_.mailNickname -eq $rawAddress } @@ -166,7 +165,8 @@ function Invoke-ListUserMailboxDetails { try { # Escape single quotes in the filter value $escapedAddress = $rawAddress -replace "'", "''" - $filterQuery = "displayName eq '$escapedAddress' or mailNickname eq '$escapedAddress'" + $escapedNickname = $rawAddress -replace "'", "''" -replace ' ', '' + $filterQuery = "displayName eq '$escapedAddress' or mailNickname eq '$escapedNickname'" $contactUri = "https://graph.microsoft.com/beta/contacts?`$filter=$filterQuery&`$select=displayName,mail,mailNickname" $matchedContacts = New-GraphGetRequest -tenantid $TenantFilter -uri $contactUri From 9aa02eabb3e364737840328f280b552e188cb88c Mon Sep 17 00:00:00 2001 From: John Duprey Date: Mon, 16 Feb 2026 16:21:08 -0500 Subject: [PATCH 439/503] Guard against null grantControls before removal Add a null-check for $JSONobj.grantControls before removing the 'authenticationStrength@odata.context' property to prevent errors when grantControls is absent. Also adjust indentation/formatting of a Where-Object call for readability; other payload-cleanup logic is unchanged. --- Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 b/Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 index 8c9f26a57097..1196ee1f7488 100644 --- a/Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 +++ b/Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 @@ -92,7 +92,9 @@ function New-CIPPCAPolicy { Remove-EmptyArrays -Object $JSONobj #Remove context as it does not belong in the payload. try { - $JSONobj.grantControls.PSObject.Properties.Remove('authenticationStrength@odata.context') + if ($JSONobj.grantControls) { + $JSONobj.grantControls.PSObject.Properties.Remove('authenticationStrength@odata.context') + } $JSONobj.templateId ? $JSONobj.PSObject.Properties.Remove('templateId') : $null if ($JSONobj.conditions.users.excludeGuestsOrExternalUsers.externalTenants.Members) { $JSONobj.conditions.users.excludeGuestsOrExternalUsers.externalTenants.PSObject.Properties.Remove('@odata.context') @@ -426,7 +428,7 @@ function New-CIPPCAPolicy { # Preserve any exclusion groups named "Vacation Exclusion - " 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 From 8e7cccb4847b4879d2d5103e8f6592c9fa876e17 Mon Sep 17 00:00:00 2001 From: James Tarran Date: Tue, 17 Feb 2026 09:49:35 +0000 Subject: [PATCH 440/503] Update Invoke-CIPPStandardPasswordExpireDisabled.ps1 Filter out subdomains from password expiration policy checks --- ...voke-CIPPStandardPasswordExpireDisabled.ps1 | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPasswordExpireDisabled.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPasswordExpireDisabled.ps1 index 2caaa9e00f7f..fee656c81a6a 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPasswordExpireDisabled.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPasswordExpireDisabled.ps1 @@ -43,8 +43,24 @@ function Invoke-CIPPStandardPasswordExpireDisabled { return } + $DomainIdSet = [System.Collections.Generic.HashSet[string]]::new([string[]]$GraphRequest.id) + $SubDomains = [System.Collections.Generic.HashSet[string]]::new() + foreach ($id in $DomainIdSet) { + $dot = $id.IndexOf('.') + while ($dot -gt 0) { + if ($DomainIdSet.Contains($id.Substring($dot + 1))) { + [void]$SubDomains.Add($id) + break + } + $dot = $id.IndexOf('.', $dot + 1) + } + } $DomainsWithoutPassExpire = $GraphRequest | - Where-Object { $_.isVerified -eq $true -and $_.passwordValidityPeriodInDays -ne 2147483647 } + Where-Object { + $_.isVerified -eq $true -and + $_.passwordValidityPeriodInDays -ne 2147483647 -and + -not $SubDomains.Contains($_.id) + } if ($Settings.remediate -eq $true) { From 11e8a489c1aed78875ab71ce184a8b1b7bb98558 Mon Sep 17 00:00:00 2001 From: James Tarran Date: Tue, 17 Feb 2026 11:04:32 +0000 Subject: [PATCH 441/503] Update Invoke-CIPPStandardPasswordExpireDisabled.ps1 Remove subdomains from password expiry check --- ...oke-CIPPStandardPasswordExpireDisabled.ps1 | 25 +++++++------------ 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPasswordExpireDisabled.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPasswordExpireDisabled.ps1 index fee656c81a6a..56a55dad0a4d 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPasswordExpireDisabled.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPasswordExpireDisabled.ps1 @@ -43,23 +43,16 @@ function Invoke-CIPPStandardPasswordExpireDisabled { return } - $DomainIdSet = [System.Collections.Generic.HashSet[string]]::new([string[]]$GraphRequest.id) - $SubDomains = [System.Collections.Generic.HashSet[string]]::new() - foreach ($id in $DomainIdSet) { - $dot = $id.IndexOf('.') - while ($dot -gt 0) { - if ($DomainIdSet.Contains($id.Substring($dot + 1))) { - [void]$SubDomains.Add($id) - break - } - $dot = $id.IndexOf('.', $dot + 1) - } - } + $DomainIds = @($GraphRequest.id) $DomainsWithoutPassExpire = $GraphRequest | - Where-Object { - $_.isVerified -eq $true -and - $_.passwordValidityPeriodInDays -ne 2147483647 -and - -not $SubDomains.Contains($_.id) + Where-Object { + $id = $_.id + $_.isVerified -eq $true ` + -and $_.passwordValidityPeriodInDays -ne 2147483647 ` + -and -not ($DomainIds | Where-Object { + $id -ne $_ ` + -and $id.EndsWith(".$_") + }) } if ($Settings.remediate -eq $true) { From 4f3c6101d2b71f9b8f2a98461e03fbfc73c09c36 Mon Sep 17 00:00:00 2001 From: James Tarran Date: Tue, 17 Feb 2026 11:15:38 +0000 Subject: [PATCH 442/503] Refactor domain ID handling for password expiration check Exclude subdomains from password expiry check --- ...nvoke-CIPPStandardPasswordExpireDisabled.ps1 | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPasswordExpireDisabled.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPasswordExpireDisabled.ps1 index 56a55dad0a4d..a84376bb6265 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPasswordExpireDisabled.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPasswordExpireDisabled.ps1 @@ -43,16 +43,19 @@ function Invoke-CIPPStandardPasswordExpireDisabled { return } - $DomainIds = @($GraphRequest.id) + $DomainIds = [System.Collections.Generic.HashSet[string]]::new([string[]]$GraphRequest.id) + $SubDomains = foreach ($id in $DomainIds) { + foreach ($parent in $DomainIds) { + if ($id -ne $parent -and $id.EndsWith(".$parent")) { + $id; break + } + } + } $DomainsWithoutPassExpire = $GraphRequest | Where-Object { - $id = $_.id $_.isVerified -eq $true ` - -and $_.passwordValidityPeriodInDays -ne 2147483647 ` - -and -not ($DomainIds | Where-Object { - $id -ne $_ ` - -and $id.EndsWith(".$_") - }) + -and $_.passwordValidityPeriodInDays -ne 2147483647 ` + -and $_.id -notin $SubDomains } if ($Settings.remediate -eq $true) { From 79e9eb64132a2ce72500e32d4821f963f40012ec Mon Sep 17 00:00:00 2001 From: James Tarran Date: Tue, 17 Feb 2026 11:18:22 +0000 Subject: [PATCH 443/503] Change DomainIds initialization to array format --- .../Standards/Invoke-CIPPStandardPasswordExpireDisabled.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPasswordExpireDisabled.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPasswordExpireDisabled.ps1 index a84376bb6265..f4fcbd90c718 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPasswordExpireDisabled.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPasswordExpireDisabled.ps1 @@ -43,7 +43,7 @@ function Invoke-CIPPStandardPasswordExpireDisabled { return } - $DomainIds = [System.Collections.Generic.HashSet[string]]::new([string[]]$GraphRequest.id) + $DomainIds = @($GraphRequest.id) $SubDomains = foreach ($id in $DomainIds) { foreach ($parent in $DomainIds) { if ($id -ne $parent -and $id.EndsWith(".$parent")) { From c77e8b8f8630b9ffd5725e676ddd636f594b6a35 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Tue, 17 Feb 2026 13:38:34 +0100 Subject: [PATCH 444/503] remove backtics --- .../Invoke-CIPPStandardPasswordExpireDisabled.ps1 | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPasswordExpireDisabled.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPasswordExpireDisabled.ps1 index f4fcbd90c718..fcf52e7bf4b8 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPasswordExpireDisabled.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPasswordExpireDisabled.ps1 @@ -46,17 +46,12 @@ function Invoke-CIPPStandardPasswordExpireDisabled { $DomainIds = @($GraphRequest.id) $SubDomains = foreach ($id in $DomainIds) { foreach ($parent in $DomainIds) { - if ($id -ne $parent -and $id.EndsWith(".$parent")) { - $id; break + if ($id -ne $parent -and $id.EndsWith(".$parent")) { + $id; break } } } - $DomainsWithoutPassExpire = $GraphRequest | - Where-Object { - $_.isVerified -eq $true ` - -and $_.passwordValidityPeriodInDays -ne 2147483647 ` - -and $_.id -notin $SubDomains - } + $DomainsWithoutPassExpire = $GraphRequest | Where-Object { $_.isVerified -eq $true -and $_.passwordValidityPeriodInDays -ne 2147483647 -and $_.id -notin $SubDomains } if ($Settings.remediate -eq $true) { From 0b9ea4c48e4c64be326cef667ca09689f090ee0f Mon Sep 17 00:00:00 2001 From: John Duprey Date: Tue, 17 Feb 2026 10:06:04 -0500 Subject: [PATCH 445/503] add OFFICE_BUSINESS license sku for standards targeting branding --- .../CIPPCore/Public/Standards/Invoke-CIPPStandardBranding.ps1 | 2 +- .../Public/Standards/Invoke-CIPPStandardPhishProtection.ps1 | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardBranding.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardBranding.ps1 index a0efe5f4ea5b..d68230294b11 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardBranding.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardBranding.ps1 @@ -37,7 +37,7 @@ function Invoke-CIPPStandardBranding { param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'Branding' -TenantFilter $Tenant -RequiredCapabilities @('AAD_PREMIUM', 'AAD_PREMIUM_P2') + $TestResult = Test-CIPPStandardLicense -StandardName 'Branding' -TenantFilter $Tenant -RequiredCapabilities @('AAD_PREMIUM', 'AAD_PREMIUM_P2', 'OFFICE_BUSINESS') if ($TestResult -eq $false) { return $true diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPhishProtection.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPhishProtection.ps1 index 8f79c8d5a850..0f3ca5cc94e4 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPhishProtection.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPhishProtection.ps1 @@ -34,13 +34,13 @@ function Invoke-CIPPStandardPhishProtection { param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'PhishProtection' -TenantFilter $Tenant -RequiredCapabilities @('AAD_PREMIUM', 'AAD_PREMIUM_P2') + $TestResult = Test-CIPPStandardLicense -StandardName 'PhishProtection' -TenantFilter $Tenant -RequiredCapabilities @('AAD_PREMIUM', 'AAD_PREMIUM_P2', 'OFFICE_BUSINESS') if ($TestResult -eq $false) { return $true } #we're done. - $TenantId = Get-Tenants | Where-Object -Property defaultDomainName -EQ $tenant + $TenantId = Get-Tenants | Where-Object -Property defaultDomainName -EQ $Tenant $Table = Get-CIPPTable -TableName Config $CippConfig = (Get-CIPPAzDataTableEntity @Table) From 2ff0251ac99ff912e31a477a073215789a384ee1 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Tue, 17 Feb 2026 13:10:32 -0500 Subject: [PATCH 446/503] Improve GUID extraction for template lookups Update Get-CIPPDrift.ps1 to more robustly extract GUIDs from StandardName for Intune and Conditional Access templates by splitting the string and selecting the element that matches a GUID regex. Use the found GUID to match templates (using -match), add verbose logs when no GUID is present, and warnings when a template isn't found. This makes template resolution more reliable for names like standards.IntuneTemplate.{GUID}.IntuneTemplate.json and similar CA template names. --- Modules/CIPPCore/Public/Get-CIPPDrift.ps1 | 48 +++++++++++++++-------- 1 file changed, 32 insertions(+), 16 deletions(-) diff --git a/Modules/CIPPCore/Public/Get-CIPPDrift.ps1 b/Modules/CIPPCore/Public/Get-CIPPDrift.ps1 index f6cb38f81a04..2396a749d20e 100644 --- a/Modules/CIPPCore/Public/Get-CIPPDrift.ps1 +++ b/Modules/CIPPCore/Public/Get-CIPPDrift.ps1 @@ -104,28 +104,44 @@ function Get-CIPPDrift { $standardDescription = $null #if the $ComparisonItem.StandardName contains *IntuneTemplate*, then it's an Intune policy deviation, and we need to grab the correct displayname from the template table if ($ComparisonItem.StandardName -like '*IntuneTemplate*') { - $CompareGuid = $ComparisonItem.StandardName.Split('.') | Select-Object -Last 1 - Write-Verbose "Extracted Intune GUID: $CompareGuid from $($ComparisonItem.StandardName)" - $Template = $AllIntuneTemplates | Where-Object { $_.GUID -eq "$CompareGuid" } - if ($Template) { - $displayName = $Template.displayName - $standardDescription = $Template.description - Write-Verbose "Found Intune template: $displayName" + # Extract GUID from format like: standards.IntuneTemplate.{GUID}.IntuneTemplate.json + # Split by '.' and find the element that looks like a GUID (contains hyphens and is 36 chars) + $Parts = $ComparisonItem.StandardName.Split('.') + $CompareGuid = $Parts | Where-Object { $_ -match '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$' } | Select-Object -First 1 + + if ($CompareGuid) { + Write-Verbose "Extracted Intune GUID: $CompareGuid from $($ComparisonItem.StandardName)" + $Template = $AllIntuneTemplates | Where-Object { $_.GUID -match "$CompareGuid" } + if ($Template) { + $displayName = $Template.displayName + $standardDescription = $Template.description + Write-Verbose "Found Intune template: $displayName" + } else { + Write-Warning "Intune template not found for GUID: $CompareGuid" + } } else { - Write-Warning "Intune template not found for GUID: $CompareGuid" + Write-Verbose "No valid GUID found in: $($ComparisonItem.StandardName)" } } # Handle Conditional Access templates if ($ComparisonItem.StandardName -like '*ConditionalAccessTemplate*') { - $CompareGuid = $ComparisonItem.StandardName.Split('.') | Select-Object -Last 1 - Write-Verbose "Extracted CA GUID: $CompareGuid from $($ComparisonItem.StandardName)" - $Template = $AllCATemplates | Where-Object { $_.GUID -eq "$CompareGuid" } - if ($Template) { - $displayName = $Template.displayName - $standardDescription = $Template.description - Write-Verbose "Found CA template: $displayName" + # Extract GUID from format like: standards.ConditionalAccessTemplate.{GUID}.CATemplate.json + # Split by '.' and find the element that looks like a GUID (contains hyphens and is 36 chars) + $Parts = $ComparisonItem.StandardName.Split('.') + $CompareGuid = $Parts | Where-Object { $_ -match '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$' } | Select-Object -First 1 + + if ($CompareGuid) { + Write-Verbose "Extracted CA GUID: $CompareGuid from $($ComparisonItem.StandardName)" + $Template = $AllCATemplates | Where-Object { $_.GUID -match "$CompareGuid" } + if ($Template) { + $displayName = $Template.displayName + $standardDescription = $Template.description + Write-Verbose "Found CA template: $displayName" + } else { + Write-Warning "CA template not found for GUID: $CompareGuid" + } } else { - Write-Warning "CA template not found for GUID: $CompareGuid" + Write-Verbose "No valid GUID found in: $($ComparisonItem.StandardName)" } } $reason = if ($ExistingDriftStates.ContainsKey($ComparisonItem.StandardName)) { $ExistingDriftStates[$ComparisonItem.StandardName].Reason } From fda343efc92299fdc0d18a7620e77786bd5540ba Mon Sep 17 00:00:00 2001 From: John Duprey Date: Tue, 17 Feb 2026 13:46:38 -0500 Subject: [PATCH 447/503] Handle optional GDAP roles and batch group adds Mark several GDAP roles as optional (Billing Administrator, Global Reader, Domain Name Administrator) and update role-check logic to fail only when required roles are missing. Improve logging to list missing roles when failing and to include missing roles when continuing. Refactor Microsoft Graph calls to use bulk requests: fetch /me and transitiveMemberOf in one bulk call and batch group membership additions via New-GraphBulkRequest, with per-request success/error logging and better error messages. Changes applied to Push-ExecOnboardTenantQueue.ps1 and Invoke-ExecAddGDAPRole.ps1. --- .../Push-ExecOnboardTenantQueue.ps1 | 72 +++++++++++++++---- .../Tenant/GDAP/Invoke-ExecAddGDAPRole.ps1 | 5 +- 2 files changed, 62 insertions(+), 15 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecOnboardTenantQueue.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecOnboardTenantQueue.ps1 index 23ca76c1adeb..2d997231efa6 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecOnboardTenantQueue.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecOnboardTenantQueue.ps1 @@ -47,7 +47,10 @@ function Push-ExecOnboardTenantQueue { @{ Name = 'SharePoint Administrator'; Id = 'f28a1f50-f6e7-4571-818b-6a12f2af6b6c' }, @{ Name = 'Authentication Policy Administrator'; Id = '0526716b-113d-4c15-b2c8-68e3c22b9f80' }, @{ Name = 'Privileged Role Administrator'; Id = 'e8611ab8-c189-46e8-94e1-60213ab1f814' }, - @{ Name = 'Privileged Authentication Administrator'; Id = '7be44c8a-adaf-4e2a-84d6-ab2649e08a13' } + @{ Name = 'Privileged Authentication Administrator'; Id = '7be44c8a-adaf-4e2a-84d6-ab2649e08a13' }, + @{ Name = 'Billing Administrator'; Id = 'b0f54661-2d74-4c50-afa3-1ec803f12efe'; Optional = $true }, + @{ Name = 'Global Reader'; Id = 'f2ef992c-3afb-46b9-b7cf-a126ee74c451'; Optional = $true }, + @{ Name = 'Domain Name Administrator'; Id = '8329153b-31d0-4727-b945-745eb3bc5f31'; Optional = $true } ) if ($OnboardingSteps.Step1.Status -ne 'succeeded') { @@ -99,14 +102,16 @@ function Push-ExecOnboardTenantQueue { } if (($MissingRoles | Measure-Object).Count -gt 0) { $Logs.Add([PSCustomObject]@{ Date = (Get-Date).ToUniversalTime(); Log = 'Missing roles for relationship' }) - if ($Item.IgnoreMissingRoles -ne $true) { + $RequiredMissingRoles = $ExpectedRoles | Where-Object { $_.Optional -ne $true -and $MissingRoles -contains $_.Name } + if ($Item.IgnoreMissingRoles -ne $true -and ($RequiredMissingRoles | Measure-Object).Count -gt 0) { + $Logs.Add([PSCustomObject]@{ Date = (Get-Date).ToUniversalTime(); Log = "Missing the following required roles: $($MissingRoles -join ', ')" }) $TenantOnboarding.Status = 'failed' $OnboardingSteps.Step2.Status = 'failed' $OnboardingSteps.Step2.Message = "Your GDAP relationship is missing the following roles: $($MissingRoles -join ', ')" } else { $Logs.Add([PSCustomObject]@{ Date = (Get-Date).ToUniversalTime(); Log = 'Ignoring missing roles' }) $OnboardingSteps.Step2.Status = 'succeeded' - $OnboardingSteps.Step2.Message = 'Your GDAP relationship is missing some roles, but the onboarding will continue' + $OnboardingSteps.Step2.Message = "Your GDAP relationship is missing some roles, but the onboarding will continue. Missing roles: $($MissingRoles -join ', ')" } } else { $Logs.Add([PSCustomObject]@{ Date = (Get-Date).ToUniversalTime(); Log = 'Required roles found' }) @@ -231,19 +236,58 @@ function Push-ExecOnboardTenantQueue { $OnboardingSteps.Step3.Status = 'succeeded' $Logs.Add([PSCustomObject]@{ Date = (Get-Date).ToUniversalTime(); Log = 'Checking for missing groups for SAM user' }) - $SamUserId = (New-GraphGetRequest -uri "https://graph.microsoft.com/beta/me?`$select=id" -NoAuthCheck $true).id - $CurrentMemberships = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/me/transitiveMemberOf?`$select=id,displayName" -NoAuthCheck $true - $ExpectedCippRoles = $Item.Roles | Where-Object { $_.roleDefinitionId -in $ExpectedRoles.roleDefinitionId } + $BulkRequests = @( + @{ + id = 'samUserId' + method = 'GET' + url = "/me?`$select=id" + }, + @{ + id = 'currentMemberships' + method = 'GET' + url = "/me/transitiveMemberOf?`$select=id,displayName" + } + ) + $BulkResults = New-GraphBulkRequest -Requests $BulkRequests -NoAuthCheck $true + $SamUserId = ($BulkResults | Where-Object { $_.id -eq 'samUserId' }).body.id + $CurrentMemberships = ($BulkResults | Where-Object { $_.id -eq 'currentMemberships' }).body.value + $ExpectedCippRoles = $Item.Roles | Where-Object { $_.roleDefinitionId -in $ExpectedRoles.Id } + + # Build bulk requests for missing group memberships + $GroupMembershipRequests = [System.Collections.Generic.List[object]]::new() + $GroupMembershipLogs = [System.Collections.Generic.List[object]]::new() + foreach ($Role in $ExpectedCippRoles) { if ($CurrentMemberships.id -notcontains $Role.GroupId) { - $PostBody = @{ - '@odata.id' = 'https://graph.microsoft.com/v1.0/directoryObjects/{0}' -f $SamUserId - } | ConvertTo-Json -Compress - try { - New-GraphPostRequest -uri "https://graph.microsoft.com/beta/groups/$($Role.GroupId)/members/`$ref" -body $PostBody -AsApp $true -NoAuthCheck $true - $Logs.Add([PSCustomObject]@{ Date = (Get-Date).ToUniversalTime(); Log = "Added SAM user to $($Role.GroupName)" }) - } catch { - $Logs.Add([PSCustomObject]@{ Date = (Get-Date).ToUniversalTime(); Log = "Failed to add SAM user to $($Role.GroupName) - $($_.Exception.Message)" }) + $GroupMembershipRequests.Add(@{ + id = "addSamUser-$($Role.GroupId)" + method = 'POST' + url = "groups/$($Role.GroupId)/members/`$ref" + body = @{ + '@odata.id' = 'https://graph.microsoft.com/v1.0/directoryObjects/{0}' -f $SamUserId + } + headers = @{ + 'Content-Type' = 'application/json' + } + }) + $GroupMembershipLogs.Add(@{ + id = "addSamUser-$($Role.GroupId)" + GroupName = $Role.GroupName + }) + } + } + + # Execute bulk group membership additions if any are needed + if ($GroupMembershipRequests.Count -gt 0) { + $GroupMembershipResults = New-GraphBulkRequest -Requests $GroupMembershipRequests -AsApp $true -NoAuthCheck $true + + foreach ($LogEntry in $GroupMembershipLogs) { + $Result = $GroupMembershipResults | Where-Object { $_.id -eq $LogEntry.id } + if ($Result.status -match '^2[0-9]+') { + $Logs.Add([PSCustomObject]@{ Date = (Get-Date).ToUniversalTime(); Log = "Added SAM user to $($LogEntry.GroupName)" }) + } else { + $ErrorMessage = if ($Result.body.error.message) { $Result.body.error.message } else { 'Unknown error' } + $Logs.Add([PSCustomObject]@{ Date = (Get-Date).ToUniversalTime(); Log = "Failed to add SAM user to $($LogEntry.GroupName) - $ErrorMessage" }) } } } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/GDAP/Invoke-ExecAddGDAPRole.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/GDAP/Invoke-ExecAddGDAPRole.ps1 index 65380b19933c..dcf555008f41 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/GDAP/Invoke-ExecAddGDAPRole.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/GDAP/Invoke-ExecAddGDAPRole.ps1 @@ -71,7 +71,10 @@ function Invoke-ExecAddGDAPRole { @{ label = 'SharePoint Administrator'; value = 'f28a1f50-f6e7-4571-818b-6a12f2af6b6c' }, @{ label = 'Authentication Policy Administrator'; value = '0526716b-113d-4c15-b2c8-68e3c22b9f80' }, @{ label = 'Privileged Role Administrator'; value = 'e8611ab8-c189-46e8-94e1-60213ab1f814' }, - @{ label = 'Privileged Authentication Administrator'; value = '7be44c8a-adaf-4e2a-84d6-ab2649e08a13' } + @{ label = 'Privileged Authentication Administrator'; value = '7be44c8a-adaf-4e2a-84d6-ab2649e08a13' }, + @{ label = 'Billing Administrator'; value = 'b0f54661-2d74-4c50-afa3-1ec803f12efe' }, + @{ label = 'Global Reader'; value = 'f2ef992c-3afb-46b9-b7cf-a126ee74c451' }, + @{ label = 'Domain Name Administrator'; value = '8329153b-31d0-4727-b945-745eb3bc5f31' } ) $Groups = $Request.Body.gdapRoles ?? $CippDefaults From f34126f571a0951c46d206f712af4b45445d1a18 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Tue, 17 Feb 2026 14:03:31 -0500 Subject: [PATCH 448/503] Ignore deleted accessAssignments & add optional roles Filter out accessAssignments with status 'deleted' or 'deleting' in Push-ExecOnboardTenantQueue.ps1 to avoid treating removed entries as active during mapping and polling. In Test-CIPPAccessTenant.ps1, add Billing Administrator, Global Reader, and Domain Name Administrator as optional GDAP roles, store an Optional flag on missing role objects, and update the status message to distinguish missing required vs optional GDAP roles. Also apply minor formatting adjustments. --- .../Push-ExecOnboardTenantQueue.ps1 | 2 ++ .../CIPPCore/Public/Test-CIPPAccessTenant.ps1 | 18 ++++++++++++++---- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecOnboardTenantQueue.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecOnboardTenantQueue.ps1 index 2d997231efa6..b3d6805ff988 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecOnboardTenantQueue.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecOnboardTenantQueue.ps1 @@ -126,6 +126,7 @@ function Push-ExecOnboardTenantQueue { if ($OnboardingSteps.Step2.Status -eq 'succeeded') { $Logs.Add([PSCustomObject]@{ Date = (Get-Date).ToUniversalTime(); Log = 'Checking group mapping' }) $AccessAssignments = New-GraphGetRequest -Uri "https://graph.microsoft.com/beta/tenantRelationships/delegatedAdminRelationships/$Id/accessAssignments" + $AccessAssignments = $AccessAssignments | Where-Object { $_.status -notin @('deleted', 'deleting') } if ($AccessAssignments.id -and $Item.AutoMapRoles -ne $true) { $Logs.Add([PSCustomObject]@{ Date = (Get-Date).ToUniversalTime(); Log = 'Groups mapped' }) $OnboardingSteps.Step3.Status = 'succeeded' @@ -228,6 +229,7 @@ function Push-ExecOnboardTenantQueue { do { $AccessAssignments = New-GraphGetRequest -Uri "https://graph.microsoft.com/beta/tenantRelationships/delegatedAdminRelationships/$Id/accessAssignments" + $AccessAssignments = $AccessAssignments | Where-Object { $_.status -notin @('deleted', 'deleting') } Start-Sleep -Seconds 15 } while ($AccessAssignments.status -contains 'pending' -and (Get-Date) -lt $Start.AddMinutes(8)) diff --git a/Modules/CIPPCore/Public/Test-CIPPAccessTenant.ps1 b/Modules/CIPPCore/Public/Test-CIPPAccessTenant.ps1 index a9f8459aec59..51dda90f7ff4 100644 --- a/Modules/CIPPCore/Public/Test-CIPPAccessTenant.ps1 +++ b/Modules/CIPPCore/Public/Test-CIPPAccessTenant.ps1 @@ -17,7 +17,10 @@ function Test-CIPPAccessTenant { @{ Name = 'SharePoint Administrator'; Id = 'f28a1f50-f6e7-4571-818b-6a12f2af6b6c' }, @{ Name = 'Authentication Policy Administrator'; Id = '0526716b-113d-4c15-b2c8-68e3c22b9f80' }, @{ Name = 'Privileged Role Administrator'; Id = 'e8611ab8-c189-46e8-94e1-60213ab1f814' }, - @{ Name = 'Privileged Authentication Administrator'; Id = '7be44c8a-adaf-4e2a-84d6-ab2649e08a13' } + @{ Name = 'Privileged Authentication Administrator'; Id = '7be44c8a-adaf-4e2a-84d6-ab2649e08a13' }, + @{ Name = 'Billing Administrator'; Id = 'b0f54661-2d74-4c50-afa3-1ec803f12efe'; Optional = $true }, + @{ Name = 'Global Reader'; Id = 'f2ef992c-3afb-46b9-b7cf-a126ee74c451'; Optional = $true }, + @{ Name = 'Domain Name Administrator'; Id = '8329153b-31d0-4727-b945-745eb3bc5f31'; Optional = $true } ) $TenantParams = @{ @@ -82,11 +85,11 @@ function Test-CIPPAccessTenant { if (!$Role) { $MissingRoles.Add( [PSCustomObject]@{ - Name = $RoleId.Name - Type = 'Tenant' + Name = $RoleId.Name + Type = 'Tenant' + Optional = $RoleId.Optional } ) - $AddedText = 'but missing GDAP roles' } else { $GDAPRoles.Add([PSCustomObject]@{ Role = $RoleId.Name @@ -95,6 +98,13 @@ function Test-CIPPAccessTenant { } } + $RequiredMissingRoles = $MissingRoles | Where-Object { $_.Optional -ne $true } + if (($RequiredMissingRoles | Measure-Object).Count -gt 0) { + $AddedText = 'but missing required GDAP roles' + } elseif (($MissingRoles | Measure-Object).Count -gt 0) { + $AddedText = 'but missing optional GDAP roles' + } + $GraphTest = "Successfully connected to Graph $($AddedText)" $GraphStatus = $true } catch { From d95fa06ce6a7ac539bd6a9c665b1fa394bf924d1 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Tue, 17 Feb 2026 14:30:51 -0500 Subject: [PATCH 449/503] Use Graph bulk requests for JIT admin listing Replace per-user Graph queries with Graph Bulk requests when listing JIT admin states and role memberships. Both entrypoints now build a bulk GET for users (with $count, $select, $filter and $top=999), parse the bulk response to get users, then construct and submit bulk membership requests. Added explicit initialization/clearing of the BulkRequests list and a guard to ensure non-empty requests before sending. Updated metadata to indicate Method='BulkRequest'. This reduces the number of individual Graph calls and improves performance and reliability when enumerating users and their directory roles. --- .../Push-ExecJITAdminListAllTenants.ps1 | 33 +++++++++--------- .../Users/Invoke-ListJITAdmin.ps1 | 34 +++++++++---------- 2 files changed, 33 insertions(+), 34 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecJITAdminListAllTenants.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecJITAdminListAllTenants.ps1 index 0f14c76645ab..2a4ec142805d 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecJITAdminListAllTenants.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecJITAdminListAllTenants.ps1 @@ -13,27 +13,26 @@ function Push-ExecJITAdminListAllTenants { # Get schema extensions $Schema = Get-CIPPSchemaExtensions | Where-Object { $_.id -match '_cippUser' } | Select-Object -First 1 - # Query users with JIT Admin enabled - $Query = @{ - TenantFilter = $DomainName # Use $DomainName for the current tenant - Endpoint = 'users' - Parameters = @{ - '$count' = 'true' - '$select' = "id,accountEnabled,displayName,userPrincipalName,$($Schema.id)" - '$filter' = "$($Schema.id)/jitAdminEnabled eq true or $($Schema.id)/jitAdminEnabled eq false" # Fetches both states to cache current status - } - } - $Users = Get-GraphRequestList @Query | Where-Object { $_.id } + # Query users with JIT Admin enabled using bulk request + $BulkRequests = [System.Collections.Generic.List[object]]::new() + $BulkRequests.Add(@{ + id = 'users' + method = 'GET' + url = "users?`$count=true&`$select=id,accountEnabled,displayName,userPrincipalName,$($Schema.id)&`$filter=$($Schema.id)/jitAdminEnabled eq true or $($Schema.id)/jitAdminEnabled eq false&`$top=999" + }) + + $BulkResults = New-GraphBulkRequest -tenantid $DomainName -Requests $BulkRequests + $Users = ($BulkResults | Where-Object { $_.id -eq 'users' }).body.value | Where-Object { $_.id } if ($Users) { # Get role memberships - $BulkRequests = $Users | ForEach-Object { @( - @{ - id = $_.id + $BulkRequests.Clear() + foreach ($User in $Users) { + $BulkRequests.Add(@{ + id = $User.id method = 'GET' - url = "users/$($_.id)/memberOf/microsoft.graph.directoryRole/?`$select=id,displayName" - } - ) + url = "users/$($User.id)/memberOf/microsoft.graph.directoryRole/?`$select=id,displayName" + }) } # Ensure $BulkRequests is not empty or null before making the bulk request if ($BulkRequests -and $BulkRequests.Count -gt 0) { diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListJITAdmin.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListJITAdmin.ps1 index 705e4b205258..954760a7a464 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListJITAdmin.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListJITAdmin.ps1 @@ -17,23 +17,23 @@ if ($TenantFilter -ne 'AllTenants') { # Single tenant logic - $Query = @{ - TenantFilter = $TenantFilter - Endpoint = 'users' - Parameters = @{ - '$count' = 'true' - '$select' = "id,accountEnabled,displayName,userPrincipalName,$($Schema.id)" - '$filter' = "$($Schema.id)/jitAdminEnabled eq true or $($Schema.id)/jitAdminEnabled eq false" - } - } - $Users = Get-GraphRequestList @Query | Where-Object { $_.id } - $BulkRequests = $Users | ForEach-Object { @( - @{ - id = $_.id + $BulkRequests = [System.Collections.Generic.List[object]]::new() + $BulkRequests.Add(@{ + id = 'users' + method = 'GET' + url = "users?`$count=true&`$select=id,accountEnabled,displayName,userPrincipalName,$($Schema.id)&`$filter=$($Schema.id)/jitAdminEnabled eq true or $($Schema.id)/jitAdminEnabled eq false&`$top=999" + }) + + $BulkResults = New-GraphBulkRequest -tenantid $TenantFilter -Requests $BulkRequests + $Users = ($BulkResults | Where-Object { $_.id -eq 'users' }).body.value | Where-Object { $_.id } + + $BulkRequests.Clear() + foreach ($User in $Users) { + $BulkRequests.Add(@{ + id = $User.id method = 'GET' - url = "users/$($_.id)/memberOf/microsoft.graph.directoryRole/?`$select=id,displayName" - } - ) + url = "users/$($User.id)/memberOf/microsoft.graph.directoryRole/?`$select=id,displayName" + }) } $RoleResults = New-GraphBulkRequest -tenantid $TenantFilter -Requests @($BulkRequests) # Write-Information ($RoleResults | ConvertTo-Json -Depth 10 ) @@ -54,7 +54,7 @@ } # Write-Information ($Results | ConvertTo-Json -Depth 10) - $Metadata = [PSCustomObject]@{Parameters = $Query.Parameters } + $Metadata = [PSCustomObject]@{Method = 'BulkRequest' } } else { # AllTenants logic $Results = [System.Collections.Generic.List[object]]::new() From 226cb241c9dc9823741c0d8272f8daf590132a4a Mon Sep 17 00:00:00 2001 From: John Duprey Date: Tue, 17 Feb 2026 15:23:57 -0500 Subject: [PATCH 450/503] Make user cache query dynamic based on licenses Build a BaseSelect property list of user fields (identity, contact, org, licenses, on-prem sync, etc.) and detect if the tenant supports signInActivity via Test-CIPPStandardLicense. If signInActivity is available, include signInActivity in the $select and use $top=500; otherwise use the full BaseSelect and $top=999. Update the Graph request to use the dynamic $select and $top parameters and include $count, streaming results into Add-CIPPDbItem. This ensures required fields for tests, UI and integrations are cached while handling the signInActivity limitation. --- .../CIPPCore/Public/Set-CIPPDBCacheUsers.ps1 | 70 ++++++++++++++++++- 1 file changed, 68 insertions(+), 2 deletions(-) diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheUsers.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheUsers.ps1 index d500fcbe16f2..e987260ab09b 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheUsers.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheUsers.ps1 @@ -19,9 +19,75 @@ 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 + + # Base properties needed by tests, standards, reports, UI, and integrations (Hudu, NinjaOne) + $BaseSelect = @( + # Core identity + 'id' + 'displayName' + 'userPrincipalName' + 'givenName' + 'surname' + 'mailNickname' + + # Account status + 'accountEnabled' + 'userType' + 'isResourceAccount' + 'createdDateTime' + + # Security & policies + 'passwordPolicies' + 'perUserMfaState' + + # Contact information + 'mail' + 'otherMails' + 'mobilePhone' + 'businessPhones' + 'faxNumber' + 'proxyAddresses' + + # Location & organization + 'jobTitle' + 'department' + 'companyName' + 'officeLocation' + 'city' + 'state' + 'country' + 'postalCode' + 'streetAddress' + + # Settings + 'preferredLanguage' + 'usageLocation' + 'preferredDataLocation' + 'showInAddressList' + + # Licenses + 'assignedLicenses' + 'assignedPlans' + 'licenseAssignmentStates' + + # On-premises sync + 'onPremisesSyncEnabled' + 'onPremisesImmutableId' + 'onPremisesLastSyncDateTime' + 'onPremisesDistinguishedName' + ) + + if ($SignInLogsCapable) { + $Select = ($BaseSelect + 'signInActivity') -join ',' + $Top = 500 + } else { + $Select = $BaseSelect -join ',' + $Top = 999 + } + # Stream users directly from Graph API to batch processor - # Using $top=500 due to signInActivity limitation - New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/users?$top=500&$select=signInActivity' -tenantid $TenantFilter | + New-GraphGetRequest -uri "https://graph.microsoft.com/beta/users?`$top=$Top&`$select=$Select&`$count=true" -ComplexFilter -tenantid $TenantFilter | Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Users' -AddCount Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached users successfully' -sev Debug From 2c3bc5be4fda65f89746e603985e68f710c2fd9b Mon Sep 17 00:00:00 2001 From: John Duprey Date: Tue, 17 Feb 2026 16:04:34 -0500 Subject: [PATCH 451/503] Fetch full managedDevices; improve NinjaOne sync Remove the $select projection from the Graph managedDevices request so the full device objects are cached. In the NinjaOne tenant sync, avoid re-evaluating the device pipeline by introducing $DevicesToProcess, normalize serial numbers (strip spaces) for more reliable serial matching, fall back to deviceName for name matching, and wrap the PATCH update in a try/catch that logs error details. Also remove/comment noisy Write-Information lines and the debug Ninja body log to reduce log spam. --- .../Public/Set-CIPPDBCacheManagedDevices.ps1 | 2 +- .../NinjaOne/Invoke-NinjaOneTenantSync.ps1 | 31 ++++++++++--------- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheManagedDevices.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheManagedDevices.ps1 index 509a136a4fec..5190fd72c24f 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheManagedDevices.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheManagedDevices.ps1 @@ -18,7 +18,7 @@ function Set-CIPPDBCacheManagedDevices { try { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching managed devices' -sev Debug - $ManagedDevices = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/deviceManagement/managedDevices?$top=999&$select=id,deviceName,operatingSystem,osVersion,complianceState,managedDeviceOwnerType,enrolledDateTime,lastSyncDateTime' -tenantid $TenantFilter + $ManagedDevices = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/deviceManagement/managedDevices?$top=999' -tenantid $TenantFilter if (!$ManagedDevices) { $ManagedDevices = @() } Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ManagedDevices' -Data $ManagedDevices Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'ManagedDevices' -Data $ManagedDevices -Count diff --git a/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneTenantSync.ps1 b/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneTenantSync.ps1 index 7d2b1a4b9aab..bca90aa87eba 100644 --- a/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneTenantSync.ps1 +++ b/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneTenantSync.ps1 @@ -461,15 +461,21 @@ function Invoke-NinjaOneTenantSync { [System.Collections.Generic.List[PSCustomObject]]$DeviceMap = @() } + $DevicesToProcess = $Devices | Where-Object { $_.id -notin $ParsedDevices.id } + # Parse Devices - foreach ($Device in $Devices | Where-Object { $_.id -notin $ParsedDevices.id }) { + foreach ($Device in $DevicesToProcess) { - # First lets match on serial - $MatchedNinjaDevice = $NinjaDevices | Where-Object { $_.system.biosSerialNumber -eq $Device.SerialNumber -or $_.system.serialNumber -eq $Device.SerialNumber } + # First lets match on serial (normalize by removing spaces for comparison) + $NormalizedDeviceSerial = $Device.SerialNumber -replace '\s', '' + $MatchedNinjaDevice = $NinjaDevices | Where-Object { + ($_.system.biosSerialNumber -replace '\s', '') -eq $NormalizedDeviceSerial -or + ($_.system.serialNumber -replace '\s', '') -eq $NormalizedDeviceSerial + } # See if we found just one device, if not match on name if (($MatchedNinjaDevice | Measure-Object).count -ne 1) { - $MatchedNinjaDevice = $NinjaDevices | Where-Object { $_.systemName -eq $Device.Name -or $_.dnsName -eq $Device.Name } + $MatchedNinjaDevice = $NinjaDevices | Where-Object { $_.systemName -eq $Device.deviceName -or $_.dnsName -eq $Device.deviceName } } # Check on a match again and set name @@ -710,7 +716,12 @@ function Invoke-NinjaOneTenantSync { # Update Device if ($MappedFields.DeviceSummary -or $MappedFields.DeviceLinks -or $MappedFields.DeviceCompliance) { - $Result = Invoke-WebRequest -Uri "https://$($Configuration.Instance)/api/v2/device/$($MatchedNinjaDevice.id)/custom-fields" -Method PATCH -Headers @{Authorization = "Bearer $($token.access_token)" } -ContentType 'application/json; charset=utf-8' -Body ($NinjaDeviceUpdate | ConvertTo-Json -Depth 100) + try { + $UpdateBody = $NinjaDeviceUpdate | ConvertTo-Json -Depth 100 + $Result = Invoke-WebRequest -Uri "https://$($Configuration.Instance)/api/v2/device/$($MatchedNinjaDevice.id)/custom-fields" -Method PATCH -Headers @{Authorization = "Bearer $($token.access_token)" } -ContentType 'application/json; charset=utf-8' -Body $UpdateBody + } catch { + Write-Verbose "Error details: $($_ | ConvertTo-Json -Depth 5)" + } } } @@ -1543,8 +1554,6 @@ function Invoke-NinjaOneTenantSync { ### M365 Links Section if ($MappedFields.TenantLinks) { - Write-Information 'Tenant Links' - $ManagementLinksData = @( @{ Name = 'M365 Admin Portal' @@ -1650,8 +1659,6 @@ function Invoke-NinjaOneTenantSync { if ($MappedFields.TenantSummary) { - Write-Information 'Tenant Summary' - ### Tenant Overview Card $ParsedAdmins = [PSCustomObject]@{} @@ -1672,7 +1679,6 @@ function Invoke-NinjaOneTenantSync { $TenantSummaryCard = Get-NinjaOneInfoCard -Title 'Tenant Details' -Data $TenantDetailsItems -Icon 'fas fa-building' ### Users details card - Write-Information 'User Details' $TotalUsersCount = ($Users | Measure-Object).count $GuestUsersCount = ($Users | Where-Object { $_.UserType -eq 'Guest' } | Measure-Object).count $LicensedUsersCount = ($licensedUsers | Measure-Object).count @@ -1730,7 +1736,6 @@ function Invoke-NinjaOneTenantSync { ### Device Details Card - Write-Information 'Device Details' $TotalDeviceswCount = ($Devices | Measure-Object).count $ComplianceDevicesCount = ($Devices | Where-Object { $_.complianceState -eq 'compliant' } | Measure-Object).count $WindowsCount = ($Devices | Where-Object { $_.operatingSystem -eq 'Windows' } | Measure-Object).count @@ -1810,7 +1815,6 @@ function Invoke-NinjaOneTenantSync { $DeviceSummaryCardHTML = Get-NinjaOneCard -Title 'Device Details' -Body $DeviceCardBodyHTML -Icon 'fas fa-network-wired' -TitleLink $TitleLink #### Secure Score Card - Write-Information 'Secure Score Details' $Top5Actions = ($SecureScoreParsed | Where-Object { $_.scoreInPercentage -ne 100 } | Sort-Object 'Score Impact', adjustedRank -Descending) | Select-Object -First 5 # Score Chart @@ -1845,7 +1849,6 @@ function Invoke-NinjaOneTenantSync { ### CIPP Applied Standards Cards - Write-Information 'Applied Standards' $ModuleBase = Get-Module CIPPExtensions | Select-Object -ExpandProperty ModuleBase $CIPPRoot = (Get-Item $ModuleBase).Parent.Parent.FullName Set-Location $CIPPRoot @@ -2195,7 +2198,7 @@ function Invoke-NinjaOneTenantSync { $Token = Get-NinjaOneToken -configuration $Configuration - Write-Information "Ninja Body: $($NinjaOrgUpdate | ConvertTo-Json -Depth 100)" + #Write-Information "Ninja Body: $($NinjaOrgUpdate | ConvertTo-Json -Depth 100)" $Result = Invoke-WebRequest -Uri "https://$($Configuration.Instance)/api/v2/organization/$($MappedTenant.IntegrationId)/custom-fields" -Method PATCH -Headers @{Authorization = "Bearer $($token.access_token)" } -ContentType 'application/json; charset=utf-8' -Body ($NinjaOrgUpdate | ConvertTo-Json -Depth 100) From bc49356ef6557a8a2decd66f1e5ffecf97e17d8e Mon Sep 17 00:00:00 2001 From: Integrated Solutions <159874617+isgq-github01@users.noreply.github.com> Date: Wed, 18 Feb 2026 15:16:21 +1000 Subject: [PATCH 452/503] Update Invoke-SetAuthMethod.ps1 - added functionality for deploying to groups --- .../Administration/Invoke-SetAuthMethod.ps1 | 31 ++----------------- 1 file changed, 2 insertions(+), 29 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-SetAuthMethod.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-SetAuthMethod.ps1 index 5c696ca8e199..71b5adf5984d 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-SetAuthMethod.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-SetAuthMethod.ps1 @@ -15,34 +15,7 @@ function Invoke-SetAuthMethod { $State = if ($Request.Body.state -eq 'enabled') { $true } else { $false } $TenantFilter = $Request.Body.tenantFilter $AuthenticationMethodId = $Request.Body.Id - $GroupIdsRaw = $Request.Body.GroupIds - - function Get-StandardizedList { - param($InputObject) - - if ($null -eq $InputObject) { return @() } - - if ($InputObject -is [string]) { - return @( - $InputObject -split ',' | - ForEach-Object { $_.Trim() } | - Where-Object { -not [string]::IsNullOrWhiteSpace($_) } - ) - } - - if ($InputObject -is [array] -or $InputObject -is [System.Collections.IEnumerable]) { - return @( - $InputObject | - ForEach-Object { "$_".Trim() } | - Where-Object { -not [string]::IsNullOrWhiteSpace($_) } - ) - } - - return @("$InputObject".Trim()) | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } - } - - $GroupIds = Get-StandardizedList -InputObject $GroupIdsRaw - + $GroupIds = $Request.Body.GroupIds try { $Params = @{ @@ -52,7 +25,7 @@ function Invoke-SetAuthMethod { Enabled = $State Headers = $Headers } - if (@($GroupIds).Count -gt 0) { + if ($GroupIds) { $Params.GroupIds = @($GroupIds) } $Result = Set-CIPPAuthenticationPolicy @Params From 442a3db1289e97ebe801b7c76eff9f44bc085ebf Mon Sep 17 00:00:00 2001 From: Integrated Solutions <159874617+isgq-github01@users.noreply.github.com> Date: Wed, 18 Feb 2026 15:17:30 +1000 Subject: [PATCH 453/503] Update New-CIPPGroup.ps1 - allow blank usernames, generate GUID --- Modules/CIPPCore/Public/New-CIPPGroup.ps1 | 29 ++++------------------- 1 file changed, 4 insertions(+), 25 deletions(-) diff --git a/Modules/CIPPCore/Public/New-CIPPGroup.ps1 b/Modules/CIPPCore/Public/New-CIPPGroup.ps1 index 72dc499d5748..ea08927acaa8 100644 --- a/Modules/CIPPCore/Public/New-CIPPGroup.ps1 +++ b/Modules/CIPPCore/Public/New-CIPPGroup.ps1 @@ -77,32 +77,11 @@ function New-CIPPGroup { $null } - # Determine if we should generate a random mailNickname: - # For Security/Generic groups WITHOUT a username filled in - $ShouldGenerateRandomMailNickname = ($NormalizedGroupType -in @('Generic', 'Security')) -and [string]::IsNullOrWhiteSpace($GroupObject.username) - - # Extract local part of username if exists and remove special characters for mailNickname - if ($ShouldGenerateRandomMailNickname) { - # Generate a random alphanumeric mailNickname for security groups without a username - # Format: 8 hex characters + hyphen + 1 hex character (e.g., "450662e4-3") - $RandomPart1 = -join ((0..7) | ForEach-Object { (0..15 | ForEach-Object { '0123456789abcdef'[$_] } | Get-Random) }) - $RandomPart2 = (0..15 | ForEach-Object { '0123456789abcdef'[$_] } | Get-Random) - $MailNickname = "$RandomPart1-$RandomPart2" + # Determine if we should generate a mailNickname with a GUID, or use the username field + if (-not $GroupObject.Username) { + $MailNickname = (New-Guid).guid.substring(0, 10) } else { - if ($GroupObject.username) { - $MailNickname = ($GroupObject.username -split '@')[0] - } else { - $MailNickname = $GroupObject.username - } - - # Remove forbidden characters per Microsoft 365 mailNickname requirements: - # ASCII 0-127 only, excluding: @ () / [] ' ; : <> , SPACE and any non-ASCII - $MailNickname = $MailNickname -replace "[@()\[\]/'`;:<>,\s]|[^\x00-\x7F]", '' - - # Ensure max length of 64 characters - if ($MailNickname.Length -gt 64) { - $MailNickname = $MailNickname.Substring(0, 64) - } + $MailNickname = $GroupObject.Username } Write-LogMessage -API $APIName -tenant $TenantFilter -message "Creating group $($GroupObject.displayName) of type $NormalizedGroupType$(if ($NeedsEmail) { " with email $Email" })" -Sev Info From 6a1367ee057c2e12c3a086550d9f6196bd3321f9 Mon Sep 17 00:00:00 2001 From: Integrated Solutions <159874617+isgq-github01@users.noreply.github.com> Date: Wed, 18 Feb 2026 15:18:14 +1000 Subject: [PATCH 454/503] Update Set-CIPPAuthenticationPolicy.ps1 - allow deploying to groups --- .../Public/Set-CIPPAuthenticationPolicy.ps1 | 40 +++++-------------- 1 file changed, 10 insertions(+), 30 deletions(-) diff --git a/Modules/CIPPCore/Public/Set-CIPPAuthenticationPolicy.ps1 b/Modules/CIPPCore/Public/Set-CIPPAuthenticationPolicy.ps1 index f4b1b0f9793b..a7202202bef0 100644 --- a/Modules/CIPPCore/Public/Set-CIPPAuthenticationPolicy.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPAuthenticationPolicy.ps1 @@ -120,39 +120,19 @@ function Set-CIPPAuthenticationPolicy { } } - if ($PSBoundParameters.ContainsKey('GroupIds') -and @($GroupIds).Count -gt 0) { - $ResolvedGroupIds = @( - @($GroupIds) | - ForEach-Object { "$_".Trim() } | - Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | - Select-Object -Unique - ) - - if ($ResolvedGroupIds.Count -gt 0) { - $TargetTemplate = $null - if ($CurrentInfo.includeTargets -and @($CurrentInfo.includeTargets).Count -gt 0) { - $TargetTemplate = $CurrentInfo.includeTargets | Select-Object -First 1 - } - - $CurrentInfo.includeTargets = @( - foreach ($GroupId in $ResolvedGroupIds) { - $TargetProperties = [ordered]@{} - if ($TargetTemplate) { - foreach ($Property in $TargetTemplate.PSObject.Properties) { - if ($Property.Name -ne 'id' -and $Property.Name -ne 'targetType') { - $TargetProperties[$Property.Name] = $Property.Value - } - } - } - $TargetProperties.targetType = 'group' - $TargetProperties.id = $GroupId - [pscustomobject]$TargetProperties + if ($PSBoundParameters.ContainsKey('GroupIds') -and $GroupIds) { + $CurrentInfo.includeTargets = @( + foreach ($id in $GroupIds ) { + [pscustomobject]@{ + targetType = 'group' + id = $id } - ) - $OptionalLogMessage = "$OptionalLogMessage and targeted groups set to $($ResolvedGroupIds -join ', ')" - } + } + ) + $OptionalLogMessage += " and targeted groups set to $($CurrentInfo.includeTargets.id -join ', ')" } + # Set state of the authentication method try { if ($PSCmdlet.ShouldProcess($AuthenticationMethodId, "Set state to $State $OptionalLogMessage")) { From c9afb7ec9cb072d3c6a5255c88c13183bbf3916f Mon Sep 17 00:00:00 2001 From: John Duprey Date: Wed, 18 Feb 2026 12:05:46 -0500 Subject: [PATCH 455/503] Orchestrator offboarding, task alerts, and fixes Introduce orchestration-driven offboarding and improve scheduled task handling. Added Push-CIPPOffboardingTask and Push-CIPPOffboardingComplete entrypoints and refactored Invoke-CIPPOffboardingJob to build a task batch and start a durable orchestration. Updated Push-ExecScheduledCommand to recognize orchestrator-based commands (skip post-exec alerts/state updates and attach TaskInfo for offboarding). Enhanced Clear-CIPPImmutableId to schedule immutable ID clears when users are synced from on-premises and to log/restore as needed. Added Send-CIPPScheduledTaskAlert utility and wired it into task flows. Made Set-CIPPMailboxAccess and Set-CIPPSharePointPerms handle arrays and return per-user results; ensure scheduled tasks avoid duplicate names in Remove-CIPPLicense. Minor fix in CippEntrypoints to capture invoked function output. --- .../CIPPCore/Public/Clear-CIPPImmutableId.ps1 | 63 ++- .../Push-CIPPOffboardingComplete.ps1 | 120 +++++ .../Push-CIPPOffboardingTask.ps1 | 38 ++ .../Push-ExecScheduledCommand.ps1 | 56 +-- .../Users/Invoke-CIPPOffboardingJob.ps1 | 450 +++++++++++------- .../CIPPCore/Public/Remove-CIPPLicense.ps1 | 2 +- .../Public/Send-CIPPScheduledTaskAlert.ps1 | 100 ++++ .../CIPPCore/Public/Set-CIPPMailboxAccess.ps1 | 39 +- .../Public/Set-CIPPSharePointPerms.ps1 | 57 ++- Modules/CippEntrypoints/CippEntrypoints.psm1 | 2 +- 10 files changed, 686 insertions(+), 241 deletions(-) create mode 100644 Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPOffboardingComplete.ps1 create mode 100644 Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPOffboardingTask.ps1 create mode 100644 Modules/CIPPCore/Public/Send-CIPPScheduledTaskAlert.ps1 diff --git a/Modules/CIPPCore/Public/Clear-CIPPImmutableId.ps1 b/Modules/CIPPCore/Public/Clear-CIPPImmutableId.ps1 index 60255008687e..4f4f11e1ad2e 100644 --- a/Modules/CIPPCore/Public/Clear-CIPPImmutableId.ps1 +++ b/Modules/CIPPCore/Public/Clear-CIPPImmutableId.ps1 @@ -3,14 +3,69 @@ function Clear-CIPPImmutableId { param ( $TenantFilter, $UserID, + $Username, # Optional - used for better logging and scheduling messages + $User, # Optional - if provided, will check sync status and schedule if needed $Headers, $APIName = 'Clear Immutable ID' ) try { + # If User object is provided, check if we need to schedule instead of clearing immediately + if ($User) { + # User has ImmutableID but is not synced from on-premises - safe to clear immediately + if ($User.onPremisesSyncEnabled -ne $true -and ![string]::IsNullOrEmpty($User.onPremisesImmutableId)) { + $DisplayName = $Username ?? $UserID + Write-LogMessage -Message "User $DisplayName has an ImmutableID set but is not synced from on-premises. Proceeding to clear the ImmutableID." -TenantFilter $TenantFilter -Severity 'Warning' -APIName $APIName -headers $Headers + # Continue to clear below + } + # User is synced from on-premises - must schedule for after deletion + elseif ($User.onPremisesSyncEnabled -eq $true -and ![string]::IsNullOrEmpty($User.onPremisesImmutableId)) { + $DisplayName = $Username ?? $UserID + Write-LogMessage -Message "User $DisplayName is synced from on-premises. Scheduling an Immutable ID clear for when the user account has been soft deleted." -TenantFilter $TenantFilter -Severity 'Warning' -APIName $APIName -headers $Headers + + $ScheduledTask = @{ + TenantFilter = $TenantFilter + Name = "Clear Immutable ID: $DisplayName" + Command = @{ value = 'Clear-CIPPImmutableID' } + Parameters = [pscustomobject]@{ + UserID = $UserID + TenantFilter = $TenantFilter + APIName = $APIName + } + Trigger = @{ + Type = 'DeltaQuery' + DeltaResource = 'users' + ResourceFilter = @($UserID) + EventType = 'deleted' + UseConditions = $false + ExecutePerResource = $true + ExecutionMode = 'once' + } + ScheduledTime = [int64](([datetime]::UtcNow).AddMinutes(5) - (Get-Date '1/1/1970')).TotalSeconds + Recurrence = '15m' + PostExecution = @{ + Webhook = $false + Email = $false + PSA = $false + } + } + Add-CIPPScheduledTask -Task $ScheduledTask -hidden $false -DisallowDuplicateName $true + return 'Scheduled Immutable ID clear task for when the user account is no longer synced in the on-premises directory.' + } + # User has no ImmutableID or is already clear + else { + $DisplayName = $Username ?? $UserID + $Result = "User $DisplayName does not have an ImmutableID set or it is already cleared." + Write-LogMessage -headers $Headers -API $APIName -message $Result -sev Info -tenant $TenantFilter + return $Result + } + } + + # Perform the actual clear operation try { - $User = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/users/$UserID" -tenantid $TenantFilter -ErrorAction SilentlyContinue + $UserObj = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/users/$UserID" -tenantid $TenantFilter -ErrorAction SilentlyContinue } catch { + # User might be deleted, try to restore it $DeletedUser = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/directory/deletedItems/$UserID" -tenantid $TenantFilter if ($DeletedUser.id) { # Restore deleted user object @@ -22,12 +77,14 @@ function Clear-CIPPImmutableId { $Body = [pscustomobject]@{ onPremisesImmutableId = $null } $Body = ConvertTo-Json -InputObject $Body -Depth 5 -Compress $null = New-GraphPostRequest -uri "https://graph.microsoft.com/beta/users/$UserID" -tenantid $TenantFilter -type PATCH -body $Body - $Result = "Successfully cleared immutable ID for user $UserID" + $DisplayName = $Username ?? $UserID + $Result = "Successfully cleared immutable ID for user $DisplayName" Write-LogMessage -headers $Headers -API $APIName -message $Result -sev Info -tenant $TenantFilter return $Result } catch { $ErrorMessage = Get-CippException -Exception $_ - $Result = "Failed to clear immutable ID for $($UserID). Error: $($ErrorMessage.NormalizedError)" + $DisplayName = $Username ?? $UserID + $Result = "Failed to clear immutable ID for $DisplayName. Error: $($ErrorMessage.NormalizedError)" Write-LogMessage -headers $Headers -API $APIName -message $Result -sev Error -tenant $TenantFilter -LogData $ErrorMessage throw $Result } diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPOffboardingComplete.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPOffboardingComplete.ps1 new file mode 100644 index 000000000000..8d86e31a27a3 --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPOffboardingComplete.ps1 @@ -0,0 +1,120 @@ +function Push-CIPPOffboardingComplete { + <# + .SYNOPSIS + Post-execution handler for offboarding orchestration completion + + .DESCRIPTION + Updates the scheduled task state when offboarding completes + + .FUNCTIONALITY + Entrypoint + #> + [CmdletBinding()] + param($Item) + + $TaskInfo = $Item.Parameters.TaskInfo + $TenantFilter = $Item.Parameters.TenantFilter + $Username = $Item.Parameters.Username + $Results = $Item.Results # Results come from orchestrator, not Parameters + + try { + Write-Information "Completing offboarding orchestration for $Username in tenant $TenantFilter" + Write-Information "Raw results from orchestrator: $($Results | ConvertTo-Json -Depth 10)" + + # Flatten nested arrays from orchestrator results + # Activity functions may return arrays like [result, "status message"] + $FlattenedResults = @( + foreach ($BatchResult in $Results) { + if ($BatchResult -is [array] -and $BatchResult.Count -gt 0) { + Write-Information "Result is array with $($BatchResult.Count) elements, extracting elements" + # Output all elements from the array + foreach ($element in $BatchResult) { + if ($null -ne $element -and $element -ne '') { + $element + } + } + } elseif ($null -ne $BatchResult -and $BatchResult -ne '') { + # Single item - output it + $BatchResult + } + } + ) + + # Process results in the same way as Push-ExecScheduledCommand + if ($FlattenedResults.Count -eq 0) { + $ProcessedResults = "Offboarding completed successfully for $Username" + } else { + Write-Information "Processing $($FlattenedResults.Count) flattened results: $($FlattenedResults | ConvertTo-Json -Depth 10)" + + # Normalize results format + if ($FlattenedResults -is [string]) { + $ProcessedResults = @{ Results = $FlattenedResults } + } elseif ($FlattenedResults -is [array]) { + # Filter and process string or resultText items + $StringResults = $FlattenedResults | Where-Object { $_ -is [string] -or $_.resultText -is [string] } + if ($StringResults) { + $ProcessedResults = $StringResults | ForEach-Object { + $Message = if ($_ -is [string]) { $_ } else { $_.resultText } + @{ Results = $Message } + } + } else { + # Keep structured results as-is + $ProcessedResults = $FlattenedResults + } + } else { + $ProcessedResults = $FlattenedResults + } + } + + Write-Information "Results after processing: $($ProcessedResults | ConvertTo-Json -Depth 10)" + + # Prepare results for storage + if ($ProcessedResults -is [string]) { + $StoredResults = $ProcessedResults + } else { + $ProcessedResults = $ProcessedResults | Select-Object * -ExcludeProperty RowKey, PartitionKey + $StoredResults = $ProcessedResults | ConvertTo-Json -Compress -Depth 20 | Out-String + } + + if ($TaskInfo) { + # Update scheduled task to completed state + $Table = Get-CippTable -tablename 'ScheduledTasks' + $currentUnixTime = [int64](([datetime]::UtcNow) - (Get-Date '1/1/1970')).TotalSeconds + + # Check if results are too large and need separate storage + if ($StoredResults.Length -gt 64000) { + Write-Information 'Results exceed 64KB limit. Storing in ScheduledTaskResults table.' + $TaskResultsTable = Get-CippTable -tablename 'ScheduledTaskResults' + $TaskResults = @{ + PartitionKey = $TaskInfo.RowKey + RowKey = $TenantFilter + Results = [string](ConvertTo-Json -Compress -Depth 20 $ProcessedResults) + } + $null = Add-CIPPAzDataTableEntity @TaskResultsTable -Entity $TaskResults -Force + $StoredResults = @{ Results = 'Offboarding completed, details are available in the More Info pane' } | ConvertTo-Json -Compress + } + + $null = Update-AzDataTableEntity -Force @Table -Entity @{ + PartitionKey = $TaskInfo.PartitionKey + RowKey = $TaskInfo.RowKey + Results = "$StoredResults" + ExecutedTime = "$currentUnixTime" + TaskState = 'Completed' + } + + Write-LogMessage -API 'Offboarding' -tenant $TenantFilter -message "Offboarding completed successfully for $Username" -sev Info + + # Send post-execution alerts if configured + if ($TaskInfo.PostExecution -and $ProcessedResults) { + Send-CIPPScheduledTaskAlert -Results $ProcessedResults -TaskInfo $TaskInfo -TenantFilter $TenantFilter + } + } + + return "Offboarding completed for $Username" + + } catch { + $ErrorMsg = "Failed to complete offboarding for $Username : $($_.Exception.Message)" + Write-LogMessage -API 'Offboarding' -tenant $TenantFilter -message $ErrorMsg -sev Error + throw $ErrorMsg + } +} diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPOffboardingTask.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPOffboardingTask.ps1 new file mode 100644 index 000000000000..7f0243f4d9c9 --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPOffboardingTask.ps1 @@ -0,0 +1,38 @@ +function Push-CIPPOffboardingTask { + <# + .SYNOPSIS + Generic wrapper to execute individual offboarding task cmdlets + + .DESCRIPTION + Executes the specified cmdlet with the provided parameters as part of user offboarding + + .FUNCTIONALITY + Entrypoint + #> + [CmdletBinding()] + param($Item) + + $Cmdlet = $Item.Cmdlet + $Parameters = $Item.Parameters | ConvertTo-Json -Depth 5 | ConvertFrom-Json -AsHashtable + + try { + Write-Information "Executing offboarding cmdlet: $Cmdlet" + + # Check if cmdlet exists + $CmdletInfo = Get-Command -Name $Cmdlet -ErrorAction SilentlyContinue + if (-not $CmdletInfo) { + throw "Cmdlet $Cmdlet does not exist" + } + + # Execute the cmdlet with splatting + $Result = & $Cmdlet @Parameters + + Write-Information "Completed $Cmdlet successfully" + return $Result + + } catch { + $ErrorMsg = "Failed to execute $Cmdlet : $($_.Exception.Message)" + Write-Information $ErrorMsg + return $ErrorMsg + } +} diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecScheduledCommand.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecScheduledCommand.ps1 index ccc7249ed798..22607cb0841b 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecScheduledCommand.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecScheduledCommand.ps1 @@ -7,6 +7,9 @@ function Push-ExecScheduledCommand { $item = $Item | ConvertTo-Json -Depth 100 | ConvertFrom-Json Write-Information "We are going to be running a scheduled task: $($Item.TaskInfo | ConvertTo-Json -Depth 10)" + # Define orchestrator-based commands that handle their own post-execution and state updates + $OrchestratorBasedCommands = @('Invoke-CIPPOffboardingJob') + # Initialize AsyncLocal storage for thread-safe per-invocation context if (-not $script:CippScheduledTaskIdStorage) { $script:CippScheduledTaskIdStorage = [System.Threading.AsyncLocal[string]]::new() @@ -225,6 +228,12 @@ function Push-ExecScheduledCommand { try { if (-not $Trigger.ExecutePerResource) { try { + # For orchestrator-based commands, add TaskInfo to enable post-execution updates + if ($Item.Command -eq 'Invoke-CIPPOffboardingJob') { + Write-Information 'Adding TaskInfo to command parameters for orchestrator-based offboarding' + $commandParameters['TaskInfo'] = $task + } + Write-Information "Starting task: $($Item.Command) for tenant: $Tenant with parameters: $($commandParameters | ConvertTo-Json)" $results = & $Item.Command @commandParameters } catch { @@ -310,43 +319,24 @@ function Push-ExecScheduledCommand { } Write-Information 'Sending task results to target. Updating the task state.' - if ($Results) { - $TableDesign = '' - $FinalResults = if ($results -is [array] -and $results[0] -is [string]) { $Results | ConvertTo-Html -Fragment -Property @{ l = 'Text'; e = { $_ } } } else { $Results | ConvertTo-Html -Fragment } - $HTML = $FinalResults -replace '
', "This alert is for tenant $Tenant.

$TableDesign
" | Out-String - - # Add alert comment if available - if ($task.AlertComment) { - if ($task.AlertComment -match '%resultcount%') { - $resultCount = if ($Results -is [array]) { $Results.Count } else { 1 } - $task.AlertComment = $task.AlertComment -replace '%resultcount%', "$resultCount" - } - $task.AlertComment = Get-CIPPTextReplacement -Text $task.AlertComment -TenantFilter $Tenant - $HTML += "

Alert Information

$($task.AlertComment)

" - } - - $title = "$TaskType - $Tenant - $($task.Name)$(if ($task.Reference) { " - Reference: $($task.Reference)" })" - Write-Information 'Scheduler: Sending the results to the target.' - Write-Information "The content of results is: $Results" - switch -wildcard ($task.PostExecution) { - '*psa*' { Send-CIPPAlert -Type 'psa' -Title $title -HTMLContent $HTML -TenantFilter $Tenant } - '*email*' { Send-CIPPAlert -Type 'email' -Title $title -HTMLContent $HTML -TenantFilter $Tenant } - '*webhook*' { - $Webhook = [PSCustomObject]@{ - 'tenantId' = $TenantInfo.customerId - 'Tenant' = $Tenant - 'TaskInfo' = $Item.TaskInfo - 'Results' = $Results - 'AlertComment' = $task.AlertComment - } - Send-CIPPAlert -Type 'webhook' -Title $title -TenantFilter $Tenant -JSONContent $($Webhook | ConvertTo-Json -Depth 20) - } - } + # For orchestrator-based commands, skip post-execution alerts as they will be handled by the orchestrator's post-execution function + if ($Results -and $Item.Command -notin $OrchestratorBasedCommands) { + Send-CIPPScheduledTaskAlert -Results $Results -TaskInfo $task -TenantFilter $Tenant -TaskType $TaskType } Write-Information 'Sent the results to the target. Updating the task state.' try { - if ($task.Recurrence -eq '0' -or [string]::IsNullOrEmpty($task.Recurrence) -or $Trigger.ExecutionMode.value -eq 'once' -or $Trigger.ExecutionMode -eq 'once') { + # For orchestrator-based commands, skip task state update as it will be handled by post-execution + if ($Item.Command -in $OrchestratorBasedCommands) { + Write-Information "Command $($Item.Command) is orchestrator-based. Skipping task state update - will be handled by post-execution." + # Update task state to 'Running' to indicate orchestration is in progress + Update-AzDataTableEntity -Force @Table -Entity @{ + PartitionKey = $task.PartitionKey + RowKey = $task.RowKey + Results = 'Orchestration in progress' + TaskState = 'Processing' + } + } elseif ($task.Recurrence -eq '0' -or [string]::IsNullOrEmpty($task.Recurrence) -or $Trigger.ExecutionMode.value -eq 'once' -or $Trigger.ExecutionMode -eq 'once') { Write-Information 'Recurrence empty or 0. Task is not recurring. Setting task state to completed.' Update-AzDataTableEntity -Force @Table -Entity @{ PartitionKey = $task.PartitionKey diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-CIPPOffboardingJob.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-CIPPOffboardingJob.ps1 index 94d7e806ccd8..6da71f0a98bf 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-CIPPOffboardingJob.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-CIPPOffboardingJob.ps1 @@ -6,206 +6,308 @@ function Invoke-CIPPOffboardingJob { [switch]$RunScheduled, $Options, $APIName = 'Offboard user', - $Headers + $Headers, + $TaskInfo ) - if ($Options -is [string]) { - $Options = $Options | ConvertFrom-Json - } - $User = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/users/$($Username)?`$select=id,displayName,onPremisesSyncEnabled,onPremisesImmutableId" -tenantid $TenantFilter - $UserID = $User.id - $DisplayName = $User.displayName - Write-Host "Running offboarding job for $Username with options: $($Options | ConvertTo-Json -Depth 10)" - $Return = switch ($Options) { - { $_.ConvertToShared -eq $true } { - try { - Set-CIPPMailboxType -Headers $Headers -tenantFilter $TenantFilter -userid $UserID -username $Username -MailboxType 'Shared' -APIName $APIName - } catch { - $_.Exception.Message - } + + try { + if ($Options -is [string]) { + $Options = $Options | ConvertFrom-Json } - { $_.RevokeSessions -eq $true } { - try { - Revoke-CIPPSessions -tenantFilter $TenantFilter -username $Username -userid $UserID -Headers $Headers -APIName $APIName - } catch { - $_.Exception.Message + + Write-Information "Starting offboarding job for $Username in tenant $TenantFilter" + Write-LogMessage -API 'Offboarding' -tenant $TenantFilter -message "Starting offboarding orchestration for user $Username" -sev Info + + # Get user information needed for various tasks + $User = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/users/$($Username)?`$select=id,displayName,onPremisesSyncEnabled,onPremisesImmutableId" -tenantid $TenantFilter + $UserID = $User.id + $DisplayName = $User.displayName + + # Build dynamic batch of offboarding tasks based on selected options + $Batch = [System.Collections.Generic.List[object]]::new() + + # Build list of tasks in execution order with their cmdlets + $TaskOrder = @( + @{ + Condition = { $Options.RevokeSessions -eq $true } + Cmdlet = 'Revoke-CIPPSessions' + Parameters = @{ + tenantFilter = $TenantFilter + username = $Username + userid = $UserID + APIName = $APIName + } } - } - { $_.ResetPass -eq $true } { - try { - Set-CIPPResetPassword -tenantFilter $TenantFilter -DisplayName $DisplayName -UserID $username -Headers $Headers -APIName $APIName - } catch { - $_.Exception.Message + @{ + Condition = { $Options.ResetPass -eq $true } + Cmdlet = 'Set-CIPPResetPassword' + Parameters = @{ + tenantFilter = $TenantFilter + DisplayName = $DisplayName + UserID = $Username + APIName = $APIName + } } - } - { $_.RemoveGroups -eq $true } { - Remove-CIPPGroups -userid $UserID -tenantFilter $TenantFilter -Headers $Headers -APIName $APIName -Username $Username - } - { $_.HideFromGAL -eq $true } { - try { - Set-CIPPHideFromGAL -tenantFilter $TenantFilter -UserID $username -hidefromgal $true -Headers $Headers -APIName $APIName - } catch { - $_.Exception.Message + @{ + Condition = { $Options.DisableSignIn -eq $true } + Cmdlet = 'Set-CIPPSignInState' + Parameters = @{ + TenantFilter = $TenantFilter + userid = $Username + AccountEnabled = $false + APIName = $APIName + } } - } - { $_.DisableSignIn -eq $true } { - try { - Set-CIPPSignInState -TenantFilter $TenantFilter -userid $username -AccountEnabled $false -Headers $Headers -APIName $APIName - } catch { - $_.Exception.Message + @{ + Condition = { $Options.HideFromGAL -eq $true } + Cmdlet = 'Set-CIPPHideFromGAL' + Parameters = @{ + tenantFilter = $TenantFilter + UserID = $Username + hidefromgal = $true + APIName = $APIName + } } - } - { $_.OnedriveAccess } { - $Options.OnedriveAccess | ForEach-Object { - try { - Set-CIPPSharePointPerms -tenantFilter $TenantFilter -userid $username -OnedriveAccessUser $_.value -Headers $Headers -APIName $APIName - } catch { - $_.Exception.Message + @{ + Condition = { $Options.RemoveGroups -eq $true } + Cmdlet = 'Remove-CIPPGroups' + Parameters = @{ + userid = $UserID + tenantFilter = $TenantFilter + APIName = $APIName + Username = $Username } } - } - { $_.AccessNoAutomap } { - $Options.AccessNoAutomap | ForEach-Object { - try { - Set-CIPPMailboxAccess -tenantFilter $TenantFilter -userid $username -AccessUser $_.value -Automap $false -AccessRights @('FullAccess') -Headers $Headers -APIName $APIName - } catch { - $_.Exception.Message + @{ + Condition = { $Options.RemoveRules -eq $true } + Cmdlet = 'Remove-CIPPMailboxRule' + Parameters = @{ + userid = $UserID + username = $Username + tenantFilter = $TenantFilter + APIName = $APIName + RemoveAllRules = $true } } - } - { $_.AccessAutomap } { - $Options.AccessAutomap | ForEach-Object { - try { - Set-CIPPMailboxAccess -tenantFilter $TenantFilter -userid $username -AccessUser $_.value -Automap $true -AccessRights @('FullAccess') -Headers $Headers -APIName $APIName - } catch { - $_.Exception.Message + @{ + Condition = { $Options.RemoveMobile -eq $true } + Cmdlet = 'Remove-CIPPMobileDevice' + Parameters = @{ + userid = $UserID + username = $Username + tenantFilter = $TenantFilter + APIName = $APIName } } - } - { $_.OOO } { - try { - Set-CIPPOutOfOffice -tenantFilter $TenantFilter -UserID $username -InternalMessage $Options.OOO -ExternalMessage $Options.OOO -Headers $Headers -APIName $APIName -state 'Enabled' - } catch { - $_.Exception.Message + @{ + Condition = { $Options.removeCalendarInvites -eq $true } + Cmdlet = 'Remove-CIPPCalendarInvites' + Parameters = @{ + UserID = $UserID + Username = $Username + TenantFilter = $TenantFilter + APIName = $APIName + } } - } - { $_.forward } { - if (!$Options.KeepCopy) { - try { - Set-CIPPForwarding -userid $userid -username $username -tenantFilter $TenantFilter -Forward $Options.forward.value -Headers $Headers -APIName $APIName - } catch { - $_.Exception.Message + @{ + Condition = { ![string]::IsNullOrEmpty($Options.OOO) } + Cmdlet = 'Set-CIPPOutOfOffice' + Parameters = @{ + tenantFilter = $TenantFilter + UserID = $Username + InternalMessage = $Options.OOO + ExternalMessage = $Options.OOO + APIName = $APIName + state = 'Enabled' } - } else { - $KeepCopy = [boolean]$Options.KeepCopy - try { - Set-CIPPForwarding -userid $userid -username $username -tenantFilter $TenantFilter -Forward $Options.forward.value -KeepCopy $KeepCopy -Headers $Headers -APIName $APIName - } catch { - $_.Exception.Message + } + @{ + Condition = { ![string]::IsNullOrEmpty($Options.forward) } + Cmdlet = 'Set-CIPPForwarding' + Parameters = @{ + userid = $UserID + username = $Username + tenantFilter = $TenantFilter + Forward = $Options.forward.value + KeepCopy = [bool]$Options.KeepCopy + APIName = $APIName } } - } - { $_.disableForwarding } { - try { - Set-CIPPForwarding -userid $userid -username $username -tenantFilter $TenantFilter -Disable $true -Headers $Headers -APIName $APIName - } catch { - $_.Exception.Message + @{ + Condition = { $Options.disableForwarding -eq $true } + Cmdlet = 'Set-CIPPForwarding' + Parameters = @{ + userid = $UserID + username = $Username + tenantFilter = $TenantFilter + Disable = $true + APIName = $APIName + } } - } - { $_.RemoveTeamsPhoneDID } { - try { - Remove-CIPPUserTeamsPhoneDIDs -userid $userid -username $username -tenantFilter $TenantFilter -Headers $Headers -APIName $APIName - } catch { - $_.Exception.Message + @{ + Condition = { ![string]::IsNullOrEmpty($Options.OnedriveAccess) } + Cmdlet = 'Set-CIPPSharePointPerms' + Parameters = @{ + tenantFilter = $TenantFilter + userid = $Username + OnedriveAccessUser = $Options.OnedriveAccess + APIName = $APIName + } } - } - { $_.RemoveLicenses -eq $true } { - Remove-CIPPLicense -userid $userid -username $Username -tenantFilter $TenantFilter -Headers $Headers -APIName $APIName -Schedule - } - { $_.DeleteUser -eq $true } { - try { - Remove-CIPPUser -UserID $userid -Username $Username -TenantFilter $TenantFilter -Headers $Headers -APIName $APIName - } catch { - $_.Exception.Message + @{ + Condition = { ![string]::IsNullOrEmpty($Options.AccessNoAutomap) } + Cmdlet = 'Set-CIPPMailboxAccess' + Parameters = @{ + tenantFilter = $TenantFilter + userid = $Username + AccessUser = $Options.AccessNoAutomap + Automap = $false + AccessRights = @('FullAccess') + APIName = $APIName + } } - } - { $_.RemoveRules -eq $true } { - Write-Host "Removing rules for $username" - try { - Remove-CIPPMailboxRule -userid $userid -username $Username -tenantFilter $TenantFilter -Headers $Headers -APIName $APIName -RemoveAllRules - } catch { - $_.Exception.Message + @{ + Condition = { ![string]::IsNullOrEmpty($Options.AccessAutomap) } + Cmdlet = 'Set-CIPPMailboxAccess' + Parameters = @{ + tenantFilter = $TenantFilter + userid = $Username + AccessUser = $Options.AccessAutomap + Automap = $true + AccessRights = @('FullAccess') + APIName = $APIName + } } - } - { $_.RemoveMobile -eq $true } { - try { - Remove-CIPPMobileDevice -userid $userid -username $Username -tenantFilter $TenantFilter -Headers $Headers -APIName $APIName - } catch { - $_.Exception.Message + @{ + Condition = { $Options.removePermissions -eq $true } + Cmdlet = 'Remove-CIPPMailboxPermissions' + Parameters = @{ + AccessUser = $Username + TenantFilter = $TenantFilter + UseCache = $true + APIName = $APIName + } } - } - { $_.removeCalendarInvites -eq $true } { - try { - Remove-CIPPCalendarInvites -UserID $userid -Username $Username -TenantFilter $TenantFilter -Headers $Headers -APIName $APIName - } catch { - $_.Exception.Message + @{ + Condition = { $Options.removeCalendarPermissions -eq $true } + Cmdlet = 'Remove-CIPPCalendarPermissions' + Parameters = @{ + UserToRemove = $Username + TenantFilter = $TenantFilter + UseCache = $true + APIName = $APIName + } + } + @{ + Condition = { $Options.ConvertToShared -eq $true } + Cmdlet = 'Set-CIPPMailboxType' + Parameters = @{ + tenantFilter = $TenantFilter + userid = $UserID + username = $Username + MailboxType = 'Shared' + APIName = $APIName + } + } + @{ + Condition = { $Options.RemoveMFADevices -eq $true } + Cmdlet = 'Remove-CIPPUserMFA' + Parameters = @{ + UserPrincipalName = $Username + TenantFilter = $TenantFilter + } + } + @{ + Condition = { $Options.RemoveTeamsPhoneDID -eq $true } + Cmdlet = 'Remove-CIPPUserTeamsPhoneDIDs' + Parameters = @{ + userid = $UserID + username = $Username + tenantFilter = $TenantFilter + APIName = $APIName + } + } + @{ + Condition = { $Options.RemoveLicenses -eq $true } + Cmdlet = 'Remove-CIPPLicense' + Parameters = @{ + userid = $UserID + username = $Username + tenantFilter = $TenantFilter + APIName = $APIName + Schedule = $true + } + } + @{ + Condition = { $Options.ClearImmutableId -eq $true } + Cmdlet = 'Clear-CIPPImmutableID' + Parameters = @{ + UserID = $UserID + Username = $Username + TenantFilter = $TenantFilter + User = $User + APIName = $APIName + } + } + @{ + Condition = { $Options.DeleteUser -eq $true } + Cmdlet = 'Remove-CIPPUser' + Parameters = @{ + UserID = $UserID + Username = $Username + TenantFilter = $TenantFilter + APIName = $APIName + } + } + ) + + # Build batch from selected tasks + foreach ($Task in $TaskOrder) { + if (& $Task.Condition) { + $Batch.Add(@{ + FunctionName = 'CIPPOffboardingTask' + Cmdlet = $Task.Cmdlet + Parameters = $Task.Parameters + }) } } - { $_.removePermissions } { - Remove-CIPPMailboxPermissions -AccessUser $Username -TenantFilter $TenantFilter -UseCache -APIName $APIName -Headers $Headers - } - { $_.removeCalendarPermissions } { - Remove-CIPPCalendarPermissions -UserToRemove $Username -TenantFilter $TenantFilter -UseCache -APIName $APIName -Headers $Headers + + if ($Batch.Count -eq 0) { + Write-LogMessage -API 'Offboarding' -tenant $TenantFilter -message "No offboarding tasks selected for user $Username" -sev Warning + return "No offboarding tasks were selected for $Username" } - { $_.RemoveMFADevices -eq $true } { - try { - Remove-CIPPUserMFA -UserPrincipalName $Username -TenantFilter $TenantFilter -Headers $Headers - } catch { - $_.Exception.Message - } + + Write-Information "Built batch of $($Batch.Count) offboarding tasks for $Username" + + # Start orchestration + $InputObject = [PSCustomObject]@{ + OrchestratorName = "OffboardingUser_$($Username)_$TenantFilter" + Batch = @($Batch) + SkipLog = $true + DurableMode = 'Sequence' } - { $_.ClearImmutableId -eq $true } { - if ($User.onPremisesSyncEnabled -ne $true -and ![string]::IsNullOrEmpty($User.onPremisesImmutableId)) { - Write-LogMessage -Message "User $Username has an ImmutableID set but is not synced from on-premises. Proceeding to clear the ImmutableID." -TenantFilter $TenantFilter -Severity 'Warning' -APIName $APIName -Headers $Headers - try { - Clear-CIPPImmutableID -UserID $userid -TenantFilter $TenantFilter -Headers $Headers -APIName $APIName - } catch { - $_.Exception.Message - } - } elseif ($User.onPremisesSyncEnabled -eq $true -and ![string]::IsNullOrEmpty($User.onPremisesImmutableId)) { - Write-LogMessage -Message "User $Username is synced from on-premises. Scheduling an Immutable ID clear for when the user account has been soft deleted." -TenantFilter $TenantFilter -Severity 'Error' -APIName $APIName -Headers $Headers - 'Scheduling Immutable ID clear task for when the user account is no longer synced in the on-premises directory.' - $ScheduledTask = @{ - TenantFilter = $TenantFilter - Name = "Clear Immutable ID: $Username" - Command = @{ - value = 'Clear-CIPPImmutableID' - } - Parameters = [pscustomobject]@{ - userid = $userid - APIName = $APIName - Headers = $Headers - } - Trigger = @{ - Type = 'DeltaQuery' - DeltaResource = 'users' - ResourceFilter = @($UserID) - EventType = 'deleted' - UseConditions = $false - ExecutePerResource = $true - ExecutionMode = 'once' - } - ScheduledTime = [int64](([datetime]::UtcNow).AddMinutes(5) - (Get-Date '1/1/1970')).TotalSeconds - Recurrence = '15m' - PostExecution = @{ - Webhook = $false - Email = $false - PSA = $false - } - } - Add-CIPPScheduledTask -Task $ScheduledTask -hidden $false + + # Add post-execution handler if TaskInfo is provided (from scheduled task) + if ($TaskInfo) { + $InputObject | Add-Member -NotePropertyName PostExecution -NotePropertyValue @{ + FunctionName = 'CIPPOffboardingComplete' + Parameters = @{ + TaskInfo = $TaskInfo + TenantFilter = $TenantFilter + Username = $Username + } } } - } - return $Return + $InstanceId = Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Depth 10 -Compress) + Write-Information "Started offboarding job for $Username with ID = '$InstanceId'" + Write-LogMessage -API 'Offboarding' -tenant $TenantFilter -message "Started offboarding job for $Username with $($Batch.Count) tasks. Instance ID: $InstanceId" -sev Info + + return "Offboarding job started for $Username with $($Batch.Count) tasks" + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Offboarding' -tenant $TenantFilter -message "Failed to start offboarding job for $Username : $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + throw $ErrorMessage + } } diff --git a/Modules/CIPPCore/Public/Remove-CIPPLicense.ps1 b/Modules/CIPPCore/Public/Remove-CIPPLicense.ps1 index 3b18b54b0678..be55ac1b73c1 100644 --- a/Modules/CIPPCore/Public/Remove-CIPPLicense.ps1 +++ b/Modules/CIPPCore/Public/Remove-CIPPLicense.ps1 @@ -29,7 +29,7 @@ function Remove-CIPPLicense { PSA = $false } } - Add-CIPPScheduledTask -Task $ScheduledTask -hidden $false + Add-CIPPScheduledTask -Task $ScheduledTask -hidden $false -DisallowDuplicateName $true return "Scheduled license removal for $username" } else { try { diff --git a/Modules/CIPPCore/Public/Send-CIPPScheduledTaskAlert.ps1 b/Modules/CIPPCore/Public/Send-CIPPScheduledTaskAlert.ps1 new file mode 100644 index 000000000000..1f8b163fe714 --- /dev/null +++ b/Modules/CIPPCore/Public/Send-CIPPScheduledTaskAlert.ps1 @@ -0,0 +1,100 @@ +function Send-CIPPScheduledTaskAlert { + <# + .SYNOPSIS + Send post-execution alerts for scheduled tasks + + .DESCRIPTION + Handles sending alerts (PSA, Email, Webhook) for scheduled task completion + + .PARAMETER Results + The results to send in the alert + + .PARAMETER TaskInfo + The task information from the ScheduledTasks table + + .PARAMETER TenantFilter + The tenant filter for the task + + .PARAMETER TaskType + The type of task (default: 'Scheduled Task') + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + $Results, + + [Parameter(Mandatory = $true)] + $TaskInfo, + + [Parameter(Mandatory = $true)] + [string]$TenantFilter, + + [Parameter(Mandatory = $false)] + [string]$TaskType = 'Scheduled Task' + ) + + try { + Write-Information "Sending post-execution alerts for task $($TaskInfo.Name)" + + # Get tenant information + $TenantInfo = Get-Tenants -TenantFilter $TenantFilter + + # Build HTML with adaptive table styling + $TableDesign = '' + $FinalResults = if ($Results -is [array] -and $Results[0] -is [string]) { + $Results | ConvertTo-Html -Fragment -Property @{ l = 'Text'; e = { $_ } } + } else { + $Results | ConvertTo-Html -Fragment + } + $HTML = $FinalResults -replace '
', "This alert is for tenant $TenantFilter.

$TableDesign
" | Out-String + + # Add alert comment if available + if ($TaskInfo.AlertComment) { + $AlertComment = $TaskInfo.AlertComment + + # Replace %resultcount% variable + if ($AlertComment -match '%resultcount%') { + $resultCount = if ($Results -is [array]) { $Results.Count } else { 1 } + $AlertComment = $AlertComment -replace '%resultcount%', "$resultCount" + } + + # Replace other variables + $AlertComment = Get-CIPPTextReplacement -Text $AlertComment -TenantFilter $TenantFilter + $HTML += "

Alert Information

$AlertComment

" + } + + # Build title + $title = "$TaskType - $TenantFilter - $($TaskInfo.Name)" + if ($TaskInfo.Reference) { + $title += " - Reference: $($TaskInfo.Reference)" + } + + Write-Information 'Scheduler: Sending the results to configured targets.' + + # Send to configured alert targets + switch -wildcard ($TaskInfo.PostExecution) { + '*psa*' { + Send-CIPPAlert -Type 'psa' -Title $title -HTMLContent $HTML -TenantFilter $TenantFilter + } + '*email*' { + Send-CIPPAlert -Type 'email' -Title $title -HTMLContent $HTML -TenantFilter $TenantFilter + } + '*webhook*' { + $Webhook = [PSCustomObject]@{ + 'tenantId' = $TenantInfo.customerId + 'Tenant' = $TenantFilter + 'TaskInfo' = $TaskInfo + 'Results' = $Results + 'AlertComment' = $TaskInfo.AlertComment + } + Send-CIPPAlert -Type 'webhook' -Title $title -TenantFilter $TenantFilter -JSONContent $($Webhook | ConvertTo-Json -Depth 20) + } + } + + Write-Information "Successfully sent alerts for task $($TaskInfo.Name)" + + } catch { + Write-Warning "Failed to send scheduled task alerts: $($_.Exception.Message)" + Write-LogMessage -API 'Scheduler_Alerts' -tenant $TenantFilter -message "Failed to send alerts for task $($TaskInfo.Name): $($_.Exception.Message)" -sev Error + } +} diff --git a/Modules/CIPPCore/Public/Set-CIPPMailboxAccess.ps1 b/Modules/CIPPCore/Public/Set-CIPPMailboxAccess.ps1 index c4ab09866086..b16d867d6f41 100644 --- a/Modules/CIPPCore/Public/Set-CIPPMailboxAccess.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPMailboxAccess.ps1 @@ -2,7 +2,7 @@ function Set-CIPPMailboxAccess { [CmdletBinding()] param ( $userid, - $AccessUser, + [array]$AccessUser, # Can be single value or array of users [bool]$Automap, $TenantFilter, $APIName = 'Manage Shared Mailbox Access', @@ -10,16 +10,33 @@ function Set-CIPPMailboxAccess { [array]$AccessRights ) - try { - $null = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Add-MailboxPermission' -cmdParams @{Identity = $userid; user = $AccessUser; AutoMapping = $Automap; accessRights = $AccessRights; InheritanceType = 'all' } -Anchor $userid + # Ensure AccessUser is always an array + if ($AccessUser -isnot [array]) { + $AccessUser = @($AccessUser) + } + + # Extract values if objects with .value property (from frontend) + $AccessUser = $AccessUser | ForEach-Object { + if ($_ -is [PSCustomObject] -and $_.value) { $_.value } else { $_ } + } - $Message = "Successfully added $($AccessUser) to $($userid) Shared Mailbox $($Automap ? 'with' : 'without') AutoMapping, with the following permissions: $AccessRights" - Write-LogMessage -headers $Headers -API $APIName -message $Message -Sev 'Info' -tenant $TenantFilter - return $Message - } catch { - $ErrorMessage = Get-CippException -Exception $_ - $Message = "Failed to add mailbox permissions for $($AccessUser) on $($userid). Error: $($ErrorMessage.NormalizedError)" - Write-LogMessage -headers $Headers -API $APIName -message $Message -Sev 'Error' -tenant $TenantFilter -LogData $ErrorMessage - throw $Message + $Results = [system.collections.generic.list[string]]::new() + + # Process each access user + foreach ($User in $AccessUser) { + try { + $null = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Add-MailboxPermission' -cmdParams @{Identity = $userid; user = $User; AutoMapping = $Automap; accessRights = $AccessRights; InheritanceType = 'all' } -Anchor $userid + + $Message = "Successfully added $($User) to $($userid) Shared Mailbox $($Automap ? 'with' : 'without') AutoMapping, with the following permissions: $AccessRights" + Write-LogMessage -headers $Headers -API $APIName -message $Message -Sev 'Info' -tenant $TenantFilter + $Results.Add($Message) + } catch { + $ErrorMessage = Get-CippException -Exception $_ + $Message = "Failed to add mailbox permissions for $($User) on $($userid). Error: $($ErrorMessage.NormalizedError)" + Write-LogMessage -headers $Headers -API $APIName -message $Message -Sev 'Error' -tenant $TenantFilter -LogData $ErrorMessage + $Results.Add($Message) + } } + + return $Results } diff --git a/Modules/CIPPCore/Public/Set-CIPPSharePointPerms.ps1 b/Modules/CIPPCore/Public/Set-CIPPSharePointPerms.ps1 index ffc4dbd72fd6..525ae41f1f39 100644 --- a/Modules/CIPPCore/Public/Set-CIPPSharePointPerms.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPSharePointPerms.ps1 @@ -2,19 +2,27 @@ function Set-CIPPSharePointPerms { [CmdletBinding()] param ( $UserId, # The UPN or ID of the users OneDrive we are changing permissions on - $OnedriveAccessUser, # The UPN of the user we are adding or removing permissions for + [array]$OnedriveAccessUser, # The UPN(s) of the user(s) we are adding or removing permissions for - can be single value or array $TenantFilter, $APIName = 'Manage SharePoint Owner', $RemovePermission, $Headers, $URL ) - if ($RemovePermission -eq $true) { - $SiteAdmin = 'false' - } else { - $SiteAdmin = 'true' + + # Ensure OnedriveAccessUser is always an array + if ($OnedriveAccessUser -isnot [array]) { + $OnedriveAccessUser = @($OnedriveAccessUser) + } + + # Extract values if objects with .value property (from frontend) + $OnedriveAccessUser = $OnedriveAccessUser | ForEach-Object { + if ($_ -is [PSCustomObject] -and $_.value) { $_.value } else { $_ } } + $SiteAdmin = if ($RemovePermission -eq $true) { 'false' } else { 'true' } + $Results = [system.collections.generic.list[string]]::new() + try { if (!$URL) { Write-Information 'No URL provided, getting URL from Graph' @@ -22,7 +30,11 @@ function Set-CIPPSharePointPerms { } $SharePointInfo = Get-SharePointAdminLink -Public $false -tenantFilter $TenantFilter - $XML = @" + + # Process each access user + foreach ($AccessUser in $OnedriveAccessUser) { + try { + $XML = @" @@ -31,7 +43,7 @@ function Set-CIPPSharePointPerms { $URL - $OnedriveAccessUser + $AccessUser $SiteAdmin @@ -39,20 +51,29 @@ function Set-CIPPSharePointPerms { "@ - $request = New-GraphPostRequest -scope "$($SharePointInfo.AdminUrl)/.default" -tenantid $TenantFilter -Uri "$($SharePointInfo.AdminUrl)/_vti_bin/client.svc/ProcessQuery" -Type POST -Body $XML -ContentType 'text/xml' - # Write-Host $($request) - if (!$request.ErrorInfo.ErrorMessage) { - $Message = "Successfully $($RemovePermission ? 'removed' : 'added') $($OnedriveAccessUser) as an owner of $URL" - Write-LogMessage -headers $Headers -API $APIName -message $Message -Sev Info -tenant $TenantFilter - return $Message - } else { - $Message = "Failed to change access: $($request.ErrorInfo.ErrorMessage)" - Write-LogMessage -headers $Headers -API $APIName -message $Message -Sev Error -tenant $TenantFilter - throw $Message + $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 $($RemovePermission ? 'removed' : 'added') $($AccessUser) as an owner of $URL" + Write-LogMessage -headers $Headers -API $APIName -message $Message -Sev Info -tenant $TenantFilter + $Results.Add($Message) + } else { + $Message = "Failed to change access for $($AccessUser): $($request.ErrorInfo.ErrorMessage)" + Write-LogMessage -headers $Headers -API $APIName -message $Message -Sev Error -tenant $TenantFilter + $Results.Add($Message) + } + } catch { + $ErrorMessage = Get-CippException -Exception $_ + $Message = "Failed to change access for $($AccessUser) on $URL. Error: $($ErrorMessage.NormalizedError)" + Write-LogMessage -headers $Headers -API $APIName -message $Message -Sev Error -tenant $TenantFilter -LogData $ErrorMessage + $Results.Add($Message) + } } + + return $Results } catch { $ErrorMessage = Get-CippException -Exception $_ - $Message = "Failed to set SharePoint permissions for $($OnedriveAccessUser) on $URL. Error: $($ErrorMessage.NormalizedError)" + $Message = "Failed to process SharePoint permissions. Error: $($ErrorMessage.NormalizedError)" Write-LogMessage -headers $Headers -API $APIName -message $Message -Sev Error -tenant $TenantFilter -LogData $ErrorMessage throw $Message } diff --git a/Modules/CippEntrypoints/CippEntrypoints.psm1 b/Modules/CippEntrypoints/CippEntrypoints.psm1 index bc94fb638e28..08614116fba9 100644 --- a/Modules/CippEntrypoints/CippEntrypoints.psm1 +++ b/Modules/CippEntrypoints/CippEntrypoints.psm1 @@ -360,7 +360,7 @@ function Receive-CippActivityTrigger { try { Write-Verbose "Activity starting Function: $FunctionName." - Invoke-Command -ScriptBlock { & $FunctionName -Item $Item } + $Output = Invoke-Command -ScriptBlock { & $FunctionName -Item $Item } $Status = 'Completed' Write-Verbose "Activity completed Function: $FunctionName." From 646ed28b48dfdc53494529f32a5fe1d92d048c93 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Wed, 18 Feb 2026 12:23:24 -0500 Subject: [PATCH 456/503] Enforce tenant access in application entrypoints Filter selected tenants using Test-CIPPAccess and restrict processing to allowed tenants; add AnyTenant to functionality tags. This change updates Invoke-AddChocoApp, Invoke-AddMSPApp, Invoke-AddOfficeApp and Invoke-AddStoreApp to call Test-CIPPAccess -TenantList, compute $AllowedTenants, and only iterate over tenants present in that list (or 'AllTenants'). Minor doc updates mark these entrypoints as AnyTenant and ensure AllTenants handling remains supported. --- .../Endpoint/Applications/Invoke-AddChocoApp.ps1 | 8 +++++--- .../Endpoint/Applications/Invoke-AddMSPApp.ps1 | 5 +++-- .../Endpoint/Applications/Invoke-AddOfficeApp.ps1 | 6 +++--- .../Endpoint/Applications/Invoke-AddStoreApp.ps1 | 6 +++--- 4 files changed, 14 insertions(+), 11 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-AddChocoApp.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-AddChocoApp.ps1 index 246ee7071f45..de8bdb8a5172 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-AddChocoApp.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-AddChocoApp.ps1 @@ -1,7 +1,7 @@ -Function Invoke-AddChocoApp { +function Invoke-AddChocoApp { <# .FUNCTIONALITY - Entrypoint + Entrypoint,AnyTenant .ROLE Endpoint.Application.ReadWrite #> @@ -30,7 +30,9 @@ Function Invoke-AddChocoApp { $intuneBody.detectionRules[0].path = "$($ENV:SystemDrive)\programdata\chocolatey\lib" $intuneBody.detectionRules[0].fileOrFolderName = "$($ChocoApp.PackageName)" - $Tenants = $Request.Body.selectedTenants.defaultDomainName + $AllowedTenants = Test-CIPPAccess -Request $Request -TenantList + $Tenants = ($Request.Body.selectedTenants | Where-Object { $AllowedTenants -contains $_.customerId -or $AllowedTenants -contains 'AllTenants' }).defaultDomainName + $Results = foreach ($Tenant in $Tenants) { try { # Apply CIPP text replacement for tenant-specific variables diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-AddMSPApp.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-AddMSPApp.ps1 index bdbc7f7dba1f..fe750f11e6fb 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-AddMSPApp.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-AddMSPApp.ps1 @@ -1,7 +1,7 @@ function Invoke-AddMSPApp { <# .FUNCTIONALITY - Entrypoint + Entrypoint,AnyTenant .ROLE Endpoint.Application.ReadWrite #> @@ -17,7 +17,8 @@ function Invoke-AddMSPApp { $intuneBody = Get-Content "AddMSPApp\$($RMMApp.RMMName.value).app.json" | ConvertFrom-Json $intuneBody.displayName = $RMMApp.DisplayName - $Tenants = $Request.Body.selectedTenants + $AllowedTenants = Test-CIPPAccess -Request $Request -TenantList + $Tenants = $Request.Body.selectedTenants | Where-Object { $AllowedTenants -contains $_.customerId -or $AllowedTenants -contains 'AllTenants' } $Results = foreach ($Tenant in $Tenants) { $InstallParams = [PSCustomObject]$RMMApp.params switch ($RMMApp.RMMName.value) { diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-AddOfficeApp.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-AddOfficeApp.ps1 index 97d65b678542..ff880318819f 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-AddOfficeApp.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-AddOfficeApp.ps1 @@ -1,15 +1,15 @@ function Invoke-AddOfficeApp { <# .FUNCTIONALITY - Entrypoint + Entrypoint,AnyTenant .ROLE Endpoint.Application.ReadWrite #> [CmdletBinding()] param($Request, $TriggerMetadata) - + $AllowedTenants = Test-CIPPAccess -Request $Request -TenantList + $Tenants = ($Request.Body.selectedTenants | Where-Object { $AllowedTenants -contains $_.customerId -or $AllowedTenants -contains 'AllTenants' }).defaultDomainName # Input bindings are passed in via param block. - $Tenants = $Request.Body.selectedTenants.defaultDomainName $Headers = $Request.Headers $APIName = $Request.Params.CIPPEndpoint if ('AllTenants' -in $Tenants) { $Tenants = (Get-Tenants).defaultDomainName } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-AddStoreApp.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-AddStoreApp.ps1 index af6eb44c1be7..90d4c94285ea 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-AddStoreApp.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-AddStoreApp.ps1 @@ -1,7 +1,7 @@ function Invoke-AddStoreApp { <# .FUNCTIONALITY - Entrypoint + Entrypoint,AnyTenant .ROLE Endpoint.Application.ReadWrite #> @@ -26,8 +26,8 @@ function Invoke-AddStoreApp { 'runAsAccount' = 'system' } } - - $Tenants = $Request.body.selectedTenants.defaultDomainName + $AllowedTenants = Test-CIPPAccess -Request $Request -TenantList + $Tenants = ($Request.Body.selectedTenants | Where-Object { $AllowedTenants -contains $_.customerId -or $AllowedTenants -contains 'AllTenants' }).defaultDomainName $Results = foreach ($Tenant in $Tenants) { try { $CompleteObject = [PSCustomObject]@{ From 5ca0443148b2c94f2b7d54995f595f644e611868 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Wed, 18 Feb 2026 15:41:22 -0500 Subject: [PATCH 457/503] Enable servicePrincipalLockConfiguration in SAM Add a servicePrincipalLockConfiguration entry to Modules/CIPPCore/lib/data/SAMManifest.json with isEnabled: true and allProperties: true. This updates the SAM manifest to include service principal lock settings so the service principal's properties are locked according to the manifest configuration. --- Modules/CIPPCore/lib/data/SAMManifest.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Modules/CIPPCore/lib/data/SAMManifest.json b/Modules/CIPPCore/lib/data/SAMManifest.json index 534b0a29e5de..bbdeaa675acf 100644 --- a/Modules/CIPPCore/lib/data/SAMManifest.json +++ b/Modules/CIPPCore/lib/data/SAMManifest.json @@ -10,6 +10,10 @@ "http://localhost:8400" ] }, + "servicePrincipalLockConfiguration": { + "isEnabled": true, + "allProperties": true + }, "requiredResourceAccess": [ { "resourceAppId": "c5393580-f805-4401-95e8-94b7a6ef2fc2", From 68a5d082a553138e11bde1a847ef526cfd944703 Mon Sep 17 00:00:00 2001 From: Bobby <31723128+kris6673@users.noreply.github.com> Date: Thu, 19 Feb 2026 15:27:36 +0100 Subject: [PATCH 458/503] feat: enhance SendFromAlias standard to be able to disable too --- .../Invoke-CIPPStandardSendFromAlias.ps1 | 47 ++++++++++--------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSendFromAlias.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSendFromAlias.ps1 index 8dc6a4af027b..94b377657fdc 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSendFromAlias.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSendFromAlias.ps1 @@ -5,9 +5,9 @@ function Invoke-CIPPStandardSendFromAlias { .COMPONENT (APIName) SendFromAlias .SYNOPSIS - (Label) Allow users to send from their alias addresses + (Label) Set Send from alias state .DESCRIPTION - (Helptext) Enables the ability for users to send from their alias addresses. + (Helptext) Enables or disables the ability for users to send from their alias addresses. (DocsDescription) Allows users to change the 'from' address to any set in their Azure AD Profile. .NOTES CAT @@ -16,6 +16,7 @@ function Invoke-CIPPStandardSendFromAlias { EXECUTIVETEXT Allows employees to send emails from their alternative email addresses (aliases) rather than just their primary address. This is useful for employees who manage multiple roles or departments, enabling them to send emails from the most appropriate address for the context. ADDEDCOMPONENT + {"type":"autoComplete","multiple":false,"creatable":false,"label":"Select value","name":"standards.SendFromAlias.state","options":[{"label":"Enabled","value":"true"},{"label":"Disabled","value":"false"}]} IMPACT Medium Impact ADDEDDATE @@ -40,43 +41,43 @@ function Invoke-CIPPStandardSendFromAlias { try { $CurrentInfo = (New-ExoRequest -tenantid $Tenant -cmdlet 'Get-OrganizationConfig').SendFromAliasEnabled } catch { - $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message - Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the SendFromAlias state for $Tenant. Error: $ErrorMessage" -Sev Error + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the SendFromAlias state for $Tenant. Error: $($ErrorMessage.NormalizedError)" -Sev Error -LogData $ErrorMessage return } + # Backwards compat: existing configs have no state (null) → default to 'true' (original behavior). For pre v10.1 + $state = $Settings.state.value ?? $Settings.state ?? 'true' + $WantedState = [System.Convert]::ToBoolean($state) + if ($Settings.remediate -eq $true) { - if ($CurrentInfo -ne $true) { + if ($CurrentInfo -ne $WantedState) { try { - New-ExoRequest -tenantid $Tenant -cmdlet 'Set-OrganizationConfig' -cmdParams @{ SendFromAliasEnabled = $true } - Write-LogMessage -API 'Standards' -tenant $tenant -message 'Send from alias enabled.' -sev Info - $CurrentInfo = $true + New-ExoRequest -tenantid $Tenant -cmdlet 'Set-OrganizationConfig' -cmdParams @{ SendFromAliasEnabled = $WantedState } + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Send from alias set to $state." -sev Info + $CurrentInfo = $WantedState } catch { - $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message - Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to enable send from alias. Error: $ErrorMessage" -sev Error + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to set send from alias to $state. Error: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage } } else { - Write-LogMessage -API 'Standards' -tenant $tenant -message 'Send from alias is already enabled.' -sev Info + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Send from alias is already set to $state." -sev Info } } if ($Settings.alert -eq $true) { - if ($CurrentInfo -eq $true) { - Write-LogMessage -API 'Standards' -tenant $tenant -message 'Send from alias is enabled.' -sev Info + if ($CurrentInfo -eq $WantedState) { + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Send from alias is set to $state." -sev Info } else { - Write-StandardsAlert -message 'Send from alias is not enabled' -object $CurrentInfo -tenant $tenant -standardName 'SendFromAlias' -standardId $Settings.standardId - Write-LogMessage -API 'Standards' -tenant $tenant -message 'Send from alias is not enabled.' -sev Info + Write-StandardsAlert -message "Send from alias is not set to $state" -object $CurrentInfo -tenant $Tenant -standardName 'SendFromAlias' -standardId $Settings.standardId + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Send from alias is not set to $state." -sev Info } } if ($Settings.report -eq $true) { - Add-CIPPBPAField -FieldName 'SendFromAlias' -FieldValue $CurrentInfo -StoreAs bool -Tenant $tenant - $CurrentValue = @{ - SendFromAliasEnabled = $CurrentInfo - } - $ExpectedValue = @{ - SendFromAliasEnabled = $true - } - Set-CIPPStandardsCompareField -FieldName 'standards.SendFromAlias' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -Tenant $tenant + Add-CIPPBPAField -FieldName 'SendFromAlias' -FieldValue $CurrentInfo -StoreAs bool -Tenant $Tenant + $CurrentValue = @{ SendFromAliasEnabled = $CurrentInfo } + $ExpectedValue = @{ SendFromAliasEnabled = $WantedState } + Set-CIPPStandardsCompareField -FieldName 'standards.SendFromAlias' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -Tenant $Tenant } } From 0452567e662303da8c11e03b91f102a2d4b8ed16 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 19 Feb 2026 09:55:29 -0500 Subject: [PATCH 459/503] Add app lock config Update Start-UpdateTokensTimer.ps1 to include servicePrincipalLockConfiguration in the Graph GET response, rename variables for clarity. Check servicePrincipalLockConfiguration; if it's not enabled, enable it via a PATCH request and write an informational log entry. --- .../Start-UpdateTokensTimer.ps1 | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-UpdateTokensTimer.ps1 b/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-UpdateTokensTimer.ps1 index 416318bb4438..8b9695149aab 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-UpdateTokensTimer.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-UpdateTokensTimer.ps1 @@ -39,12 +39,12 @@ function Start-UpdateTokensTimer { # Check application secret expiration for $env:ApplicationId and generate a new application secret if expiration is within 30 days. try { $AppId = $env:ApplicationID - $PasswordCredentials = New-GraphGetRequest -uri "https://graph.microsoft.com/v1.0/applications(appId='$AppId')?`$select=id,passwordCredentials" -NoAuthCheck $true -AsApp $true -ErrorAction Stop + $AppRegistration = New-GraphGetRequest -uri "https://graph.microsoft.com/v1.0/applications(appId='$AppId')?`$select=id,passwordCredentials,servicePrincipalLockConfiguration" -NoAuthCheck $true -AsApp $true -ErrorAction Stop # sort by latest expiration date and get the first one - $LastPasswordCredential = $PasswordCredentials.passwordCredentials | Sort-Object -Property endDateTime -Descending | Select-Object -First 1 + $LastPasswordCredential = $AppRegistration.passwordCredentials | Sort-Object -Property endDateTime -Descending | Select-Object -First 1 if ($LastPasswordCredential.endDateTime -lt (Get-Date).AddDays(30).ToUniversalTime()) { Write-Information "Application secret for $AppId is expiring soon. Generating a new application secret." - $AppSecret = New-GraphPostRequest -uri "https://graph.microsoft.com/v1.0/applications/$($PasswordCredentials.id)/addPassword" -Body '{"passwordCredential":{"displayName":"UpdateTokens"}}' -NoAuthCheck $true -AsApp $true -ErrorAction Stop + $AppSecret = New-GraphPostRequest -uri "https://graph.microsoft.com/v1.0/applications/$($AppRegistration.id)/addPassword" -Body '{"passwordCredential":{"displayName":"UpdateTokens"}}' -NoAuthCheck $true -AsApp $true -ErrorAction Stop Write-Information "New application secret generated for $AppId. Expiration date: $($AppSecret.endDateTime)." } else { Write-Information "Application secret for $AppId is valid until $($LastPasswordCredential.endDateTime). No need to generate a new application secret." @@ -77,6 +77,20 @@ function Start-UpdateTokensTimer { } else { Write-Information "No expired application secrets found for $AppId." } + + if (!$AppRegistration.servicePrincipalLockConfiguration.isEnabled) { + Write-Warning "Service principal lock configuration is not enabled for $AppId" + $Body = @{ + servicePrincipalLockConfiguration = @{ + isEnabled = $true + allProperties = $true + } + } | ConvertTo-Json + New-GraphPOSTRequest -type PATCH -uri "https://graph.microsoft.com/v1.0/applications/$($AppRegistration.id)" -Body $Body -NoAuthCheck $true -AsApp $true -ErrorAction Stop + Write-Information "Service principal lock configuration has been enabled for application $AppId." + Write-LogMessage -API 'Update Tokens' -message "Service principal lock configuration has been enabled for application $AppId." -sev 'Info' + } + } catch { Write-Warning "Error updating application secret $($_.Exception.Message)." Write-Information ($_.InvocationInfo.PositionMessage) From 4447d609c7628527436ed23a7225d268f177b5ee Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 19 Feb 2026 10:54:04 -0500 Subject: [PATCH 460/503] Add app management policy helper Introduce Update-AppManagementPolicy.ps1 which queries tenant default and app management policies via bulk Graph requests, detects credential creation restrictions, and creates/updates/assigns a "CIPP-SAM Exemption Policy" to allow the CIPP-SAM app to manage credentials. The function returns a PSCustomObject with policy state and a PolicyAction message and handles errors gracefully. Also update Invoke-ExecCreateSAMApp.ps1 and Start-UpdateTokensTimer.ps1 to call Update-AppManagementPolicy and log the resulting PolicyAction before proceeding with password/key operations. --- .../CIPP/Setup/Invoke-ExecCreateSAMApp.ps1 | 4 + .../Start-UpdateTokensTimer.ps1 | 4 + .../Update-AppManagementPolicy.ps1 | 237 ++++++++++++++++++ 3 files changed, 245 insertions(+) create mode 100644 Modules/CIPPCore/Public/GraphHelper/Update-AppManagementPolicy.ps1 diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecCreateSAMApp.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecCreateSAMApp.ps1 index 3e00555ce247..75569685738a 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecCreateSAMApp.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecCreateSAMApp.ps1 @@ -68,6 +68,10 @@ function Invoke-ExecCreateSAMApp { } } until ($attempt -gt 3) } + + $AppPolicyStatus = Update-AppManagementPolicy + Write-Information $AppPolicyStatus.PolicyAction + $AppPassword = (Invoke-RestMethod "https://graph.microsoft.com/v1.0/applications/$($AppId.id)/addPassword" -Headers @{ authorization = "Bearer $($Token.access_token)" } -Method POST -Body '{"passwordCredential":{"displayName":"CIPPInstall"}}' -ContentType 'application/json').secretText if ($env:AzureWebJobsStorage -eq 'UseDevelopmentStorage=true' -or $env:NonLocalHostAzurite -eq 'true') { diff --git a/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-UpdateTokensTimer.ps1 b/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-UpdateTokensTimer.ps1 index 8b9695149aab..ecfff8c18feb 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-UpdateTokensTimer.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-UpdateTokensTimer.ps1 @@ -42,6 +42,10 @@ function Start-UpdateTokensTimer { $AppRegistration = New-GraphGetRequest -uri "https://graph.microsoft.com/v1.0/applications(appId='$AppId')?`$select=id,passwordCredentials,servicePrincipalLockConfiguration" -NoAuthCheck $true -AsApp $true -ErrorAction Stop # sort by latest expiration date and get the first one $LastPasswordCredential = $AppRegistration.passwordCredentials | Sort-Object -Property endDateTime -Descending | Select-Object -First 1 + + $AppPolicyStatus = Update-AppManagementPolicy + Write-Information $AppPolicyStatus.PolicyAction + if ($LastPasswordCredential.endDateTime -lt (Get-Date).AddDays(30).ToUniversalTime()) { Write-Information "Application secret for $AppId is expiring soon. Generating a new application secret." $AppSecret = New-GraphPostRequest -uri "https://graph.microsoft.com/v1.0/applications/$($AppRegistration.id)/addPassword" -Body '{"passwordCredential":{"displayName":"UpdateTokens"}}' -NoAuthCheck $true -AsApp $true -ErrorAction Stop diff --git a/Modules/CIPPCore/Public/GraphHelper/Update-AppManagementPolicy.ps1 b/Modules/CIPPCore/Public/GraphHelper/Update-AppManagementPolicy.ps1 new file mode 100644 index 000000000000..3655919169c5 --- /dev/null +++ b/Modules/CIPPCore/Public/GraphHelper/Update-AppManagementPolicy.ps1 @@ -0,0 +1,237 @@ +function Update-AppManagementPolicy { + <# + .SYNOPSIS + Check and update app management policies for credential restrictions + + .DESCRIPTION + Retrieves tenant default app management policy and app management policies to check if + passwordCredential or keyCredential creation is restricted. If the default policy blocks + credential addition and CIPP-SAM app doesn't have an exemption, creates or updates a policy + to allow CIPP-SAM to manage credentials. + + .FUNCTIONALITY + Internal + #> + [CmdletBinding()] + param() + + try { + # Create bulk request to fetch both policies at once + $Requests = @( + @{ + id = 'defaultPolicy' + method = 'GET' + url = '/policies/defaultAppManagementPolicy' + } + @{ + id = 'appPolicies' + method = 'GET' + url = '/policies/appManagementPolicies' + } + @{ + id = 'appRegistration' + method = 'GET' + url = "applications(appId='$env:ApplicationID')?`$select=id,appId,displayName" + } + ) + + # Execute bulk request + $Results = New-GraphBulkRequest -Requests $Requests -NoAuthCheck $true -asapp $true + + # Parse results + $DefaultPolicy = ($Results | Where-Object { $_.id -eq 'defaultPolicy' }).body + $AppPolicies = ($Results | Where-Object { $_.id -eq 'appPolicies' }).body.value + $CIPPApp = ($Results | Where-Object { $_.id -eq 'appRegistration' }).body + + # Check if CIPP-SAM app is targeted by any policies + $CIPPAppTargeted = $false + $CIPPAppPolicyId = $null + if ($AppPolicies -and $env:ApplicationID) { + # Build bulk requests to get appliesTo for each policy + $AppliesToRequests = @($AppPolicies | ForEach-Object { + @{ + id = $_.id + method = 'GET' + url = "/policies/appManagementPolicies/$($_.id)/appliesTo" + } + }) + + if ($AppliesToRequests.Count -gt 0) { + $AppliesToResults = New-GraphBulkRequest -Requests $AppliesToRequests -NoAuthCheck $true -asapp $true + + # Find which policy (if any) targets CIPP Ap + $CIPPPolicyResult = $AppliesToResults | Where-Object { $_.body.value.appId -contains $env:ApplicationID } | Select-Object -First 1 + if ($CIPPPolicyResult) { + $CIPPAppTargeted = $true + $CIPPAppPolicyId = $CIPPPolicyResult.id + } + } + } + + # Check for credential restrictions across all policies + $PasswordAdditionBlocked = $false + $SymmetricKeyAdditionBlocked = $false + $AsymmetricKeyAdditionBlocked = $false + $PasswordLifetimeRestricted = $false + $KeyLifetimeRestricted = $false + + # Helper function to check restrictions in a policy + function Test-PolicyRestrictions { + param($Policy, [switch]$IsDefaultPolicy) + + # Default policy has applicationRestrictions, app-specific policies have restrictions + $pwdCreds = if ($IsDefaultPolicy) { $Policy.applicationRestrictions.passwordCredentials } else { $Policy.restrictions.passwordCredentials } + $keyCreds = if ($IsDefaultPolicy) { $Policy.applicationRestrictions.keyCredentials } else { $Policy.restrictions.keyCredentials } + + if ($pwdCreds) { + foreach ($restriction in $pwdCreds | Where-Object { $_.state -eq 'enabled' }) { + switch ($restriction.restrictionType) { + 'passwordAddition' { $PasswordAdditionBlocked = $true } + 'symmetricKeyAddition' { $SymmetricKeyAdditionBlocked = $true } + 'passwordLifetime' { $PasswordLifetimeRestricted = $true } + 'symmetricKeyLifetime' { $PasswordLifetimeRestricted = $true } + } + } + } + + if ($keyCreds) { + foreach ($restriction in $keyCreds | Where-Object { $_.state -eq 'enabled' }) { + switch ($restriction.restrictionType) { + 'asymmetricKeyLifetime' { $KeyLifetimeRestricted = $true } + 'trustedCertificateAuthority' { $AsymmetricKeyAdditionBlocked = $true } + } + } + } + } + + # Check default policy (uses applicationRestrictions structure) + if ($DefaultPolicy) { + Test-PolicyRestrictions -Policy $DefaultPolicy -IsDefaultPolicy + } + + # Check app-specific policies (use restrictions structure) + if ($AppPolicies) { + foreach ($AppPolicy in $AppPolicies | Where-Object { $_.isEnabled -eq $true }) { + Test-PolicyRestrictions -Policy $AppPolicy + } + } + + # Determine if default policy blocks credential addition + $DefaultPolicyBlocksCredentials = $false + if ($DefaultPolicy.applicationRestrictions.passwordCredentials) { + $DefaultPolicyBlocksCredentials = ($DefaultPolicy.applicationRestrictions.passwordCredentials | Where-Object { $_.restrictionType -in @('passwordAddition', 'symmetricKeyAddition') -and $_.state -eq 'enabled' }).Count -gt 0 + } + + # If default policy blocks credentials and CIPP app doesn't have an exemption, create/update policy + $PolicyAction = $null + if ($DefaultPolicyBlocksCredentials -and $CIPPApp) { + # Check if a CIPP-SAM Exemption Policy already exists + $ExistingExemptionPolicy = $AppPolicies | Where-Object { $_.displayName -eq 'CIPP-SAM Exemption Policy' } | Select-Object -First 1 + + # Check if CIPP app has a policy that allows credentials + $CIPPHasExemption = $false + if ($CIPPAppPolicyId) { + $CIPPPolicy = $AppPolicies | Where-Object { $_.id -eq $CIPPAppPolicyId } + # Check if the policy explicitly allows credentials (no enabled passwordAddition/symmetricKeyAddition restriction) + if ($CIPPPolicy.restrictions.passwordCredentials) { + $CIPPHasExemption = -not ($CIPPPolicy.restrictions.passwordCredentials | Where-Object { $_.restrictionType -in @('passwordAddition', 'symmetricKeyAddition') -and $_.state -eq 'enabled' }) + } else { + # No password restrictions means it allows credentials + $CIPPHasExemption = $true + } + } + + if (-not $CIPPHasExemption) { + # Need to create or update a policy for CIPP-SAM + try { + # Define policy structure with disabled restrictions + $PolicyBody = @{ + displayName = 'CIPP-SAM Exemption Policy' + description = 'Allows CIPP-SAM app to manage credentials' + isEnabled = $true + restrictions = @{ + passwordCredentials = @( + @{ + restrictionType = 'passwordAddition' + state = 'disabled' + restrictForAppsCreatedAfterDateTime = '0001-01-01T00:00:00Z' + } + @{ + restrictionType = 'symmetricKeyAddition' + state = 'disabled' + restrictForAppsCreatedAfterDateTime = '0001-01-01T00:00:00Z' + } + ) + keyCredentials = @() + } + } + + if ($CIPPAppPolicyId) { + # Update existing policy that's already assigned to the app + $null = New-GraphPostRequest -uri "https://graph.microsoft.com/v1.0/policies/appManagementPolicies/$CIPPAppPolicyId" -type PATCH -body ($PolicyBody | ConvertTo-Json -Depth 10) -asapp $true -NoAuthCheck $true + $PolicyAction = "Updated existing policy $CIPPAppPolicyId to allow credentials" + } elseif ($ExistingExemptionPolicy) { + # Exemption policy exists but not assigned to app - update and assign it + $null = New-GraphPostRequest -uri "https://graph.microsoft.com/v1.0/policies/appManagementPolicies/$($ExistingExemptionPolicy.id)" -type PATCH -body ($PolicyBody | ConvertTo-Json -Depth 10) -asapp $true -NoAuthCheck $true + + if ($CIPPApp.id) { + # Assign existing policy to CIPP-SAM application + $AssignBody = @{ + '@odata.id' = "https://graph.microsoft.com/beta/policies/appManagementPolicies/$($ExistingExemptionPolicy.id)" + } + $null = New-GraphPostRequest -uri "https://graph.microsoft.com/beta/applications/$($CIPPApp.id)/appManagementPolicies/`$ref" -type POST -body ($AssignBody | ConvertTo-Json) -asapp $true -NoAuthCheck $true + $PolicyAction = "Updated and assigned existing policy $($ExistingExemptionPolicy.id) to CIPP-SAM" + $CIPPAppPolicyId = $ExistingExemptionPolicy.id + $CIPPAppTargeted = $true + } else { + $PolicyAction = "Updated policy $($ExistingExemptionPolicy.id) but failed to assign: CIPP application not found" + } + } else { + # Create new policy and assign to CIPP-SAM app + $CreatedPolicy = New-GraphPostRequest -uri 'https://graph.microsoft.com/v1.0/policies/appManagementPolicies' -type POST -body ($PolicyBody | ConvertTo-Json -Depth 10) -asapp $true -NoAuthCheck $true + + if ($CIPPApp.id) { + # Assign policy to CIPP-SAM application using beta endpoint + $AssignBody = @{ + '@odata.id' = "https://graph.microsoft.com/beta/policies/appManagementPolicies/$($CreatedPolicy.id)" + } + $null = New-GraphPostRequest -uri "https://graph.microsoft.com/beta/applications/$($CIPPApp.id)/appManagementPolicies/`$ref" -type POST -body ($AssignBody | ConvertTo-Json) -asapp $true -NoAuthCheck $true + $PolicyAction = "Created new policy $($CreatedPolicy.id) and assigned to CIPP-SAM" + $CIPPAppPolicyId = $CreatedPolicy.id + $CIPPAppTargeted = $true + } else { + $PolicyAction = "Created new policy $($CreatedPolicy.id) but failed to assign: CIPP application not found" + } + } + } catch { + $PolicyAction = "Failed to update policy: $($_.Exception.Message)" + } + } else { + $PolicyAction = 'CIPP-SAM app is already exempt from credential restrictions. No action needed.' + } + } + + # Build result object + $PolicyInfo = [PSCustomObject]@{ + DefaultPolicy = $DefaultPolicy + AppPolicies = $AppPolicies + CIPPAppTargeted = $CIPPAppTargeted + CIPPAppPolicyId = $CIPPAppPolicyId + CIPPHasExemption = $CIPPHasExemption + PolicyAction = $PolicyAction + PasswordAdditionBlocked = $PasswordAdditionBlocked + SymmetricKeyAdditionBlocked = $SymmetricKeyAdditionBlocked + PasswordLifetimeRestricted = $PasswordLifetimeRestricted + KeyLifetimeRestricted = $KeyLifetimeRestricted + AnyCredentialCreationRestricted = $PasswordAdditionBlocked -or $SymmetricKeyAdditionBlocked + PolicyCount = if ($AppPolicies) { $AppPolicies.Count } else { 0 } + EnabledPolicyCount = if ($AppPolicies) { ($AppPolicies | Where-Object { $_.isEnabled -eq $true }).Count } else { 0 } + } + + return $PolicyInfo + + } catch { + Write-Warning "Failed to retrieve app management policies: $($_.Exception.Message)" + return $null + } +} From 49d4bcff72c26870cf09c790feac9701aff264f6 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 19 Feb 2026 10:55:18 -0500 Subject: [PATCH 461/503] Handle errors from Update-AppManagementPolicy Wrap calls to Update-AppManagementPolicy in try/catch in two entrypoints to avoid unhandled exceptions and improve diagnostics. Files changed: Invoke-ExecCreateSAMApp.ps1 and Start-UpdateTokensTimer.ps1. On success the original PolicyAction is still written; on failure a warning with the exception message is logged and the invocation position info is emitted to aid troubleshooting. --- .../CIPP/Setup/Invoke-ExecCreateSAMApp.ps1 | 9 +++++++-- .../Timer Functions/Start-UpdateTokensTimer.ps1 | 9 +++++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecCreateSAMApp.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecCreateSAMApp.ps1 index 75569685738a..202d6e4dc182 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecCreateSAMApp.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecCreateSAMApp.ps1 @@ -69,8 +69,13 @@ function Invoke-ExecCreateSAMApp { } until ($attempt -gt 3) } - $AppPolicyStatus = Update-AppManagementPolicy - Write-Information $AppPolicyStatus.PolicyAction + try { + $AppPolicyStatus = Update-AppManagementPolicy + Write-Information $AppPolicyStatus.PolicyAction + } catch { + Write-Warning "Error updating app management policy $($_.Exception.Message)." + Write-Information ($_.InvocationInfo.PositionMessage) + } $AppPassword = (Invoke-RestMethod "https://graph.microsoft.com/v1.0/applications/$($AppId.id)/addPassword" -Headers @{ authorization = "Bearer $($Token.access_token)" } -Method POST -Body '{"passwordCredential":{"displayName":"CIPPInstall"}}' -ContentType 'application/json').secretText diff --git a/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-UpdateTokensTimer.ps1 b/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-UpdateTokensTimer.ps1 index ecfff8c18feb..037203bb9952 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-UpdateTokensTimer.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-UpdateTokensTimer.ps1 @@ -43,8 +43,13 @@ function Start-UpdateTokensTimer { # sort by latest expiration date and get the first one $LastPasswordCredential = $AppRegistration.passwordCredentials | Sort-Object -Property endDateTime -Descending | Select-Object -First 1 - $AppPolicyStatus = Update-AppManagementPolicy - Write-Information $AppPolicyStatus.PolicyAction + try { + $AppPolicyStatus = Update-AppManagementPolicy + Write-Information $AppPolicyStatus.PolicyAction + } catch { + Write-Warning "Error updating app management policy $($_.Exception.Message)." + Write-Information ($_.InvocationInfo.PositionMessage) + } if ($LastPasswordCredential.endDateTime -lt (Get-Date).AddDays(30).ToUniversalTime()) { Write-Information "Application secret for $AppId is expiring soon. Generating a new application secret." From 55ec43f81a60e74b3c42a72124a6c3e7129e947b Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 19 Feb 2026 11:08:25 -0500 Subject: [PATCH 462/503] Update app management policy handling Call Update-AppManagementPolicy after creating apps/SPs and make the policy helper tenant- and app-aware. - New-CIPPAPIConfig.ps1 & Invoke-ExecSendPush.ps1: add try/catch calls to Update-AppManagementPolicy immediately after creating the application/service principal and log the result or failure. - Update-AppManagementPolicy.ps1: add parameters (TenantFilter, ApplicationId) instead of relying on environment variables; pass tenantid into Graph requests; check the provided ApplicationId when evaluating policy targets; rename exemption policy displayName/description from "CIPP-SAM Exemption Policy" to "CIPP Exemption Policy" and adjust related logic; ensure updates/assignments use the tenant scope. These changes ensure newly created apps get an exemption when tenant defaults block credential creation and allow the helper to operate across explicit tenants and application IDs. --- .../Authentication/New-CIPPAPIConfig.ps1 | 8 +++++ .../Users/Invoke-ExecSendPush.ps1 | 6 ++++ .../Update-AppManagementPolicy.ps1 | 33 ++++++++++--------- 3 files changed, 32 insertions(+), 15 deletions(-) diff --git a/Modules/CIPPCore/Public/Authentication/New-CIPPAPIConfig.ps1 b/Modules/CIPPCore/Public/Authentication/New-CIPPAPIConfig.ps1 index 948d2a17f1fa..0ddd57a18fd2 100644 --- a/Modules/CIPPCore/Public/Authentication/New-CIPPAPIConfig.ps1 +++ b/Modules/CIPPCore/Public/Authentication/New-CIPPAPIConfig.ps1 @@ -65,6 +65,14 @@ function New-CIPPAPIConfig { Write-Information $CreateBody $Step = 'Creating Application' $APIApp = New-GraphPOSTRequest -uri 'https://graph.microsoft.com/v1.0/applications' -AsApp $true -NoAuthCheck $true -type POST -body $CreateBody + + try { + $PolicyUpdate = Update-AppManagementPolicy -ApplicationId $APIApp.appId + Write-Information $PolicyUpdate.PolicyAction + } catch { + Write-Information "Failed to update app management policy: $($_.Exception.Message)" + } + Write-Information 'Creating password' $Step = 'Creating Application Password' $APIPassword = New-GraphPOSTRequest -uri "https://graph.microsoft.com/v1.0/applications/$($APIApp.id)/addPassword" -AsApp $true -NoAuthCheck $true -type POST -body "{`"passwordCredential`":{`"displayName`":`"Generated by API Setup`"}}" -maxRetries 3 diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecSendPush.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecSendPush.ps1 index 93d534a2a37e..f04ecb0608fc 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecSendPush.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecSendPush.ps1 @@ -51,6 +51,12 @@ function Invoke-ExecSendPush { $SPID = (New-GraphPostRequest -uri 'https://graph.microsoft.com/v1.0/servicePrincipals' -tenantid $TenantFilter -type POST -body $SPBody -AsApp $true).id } + try { + $PolicyUpdate = Update-AppManagementPolicy -TenantFilter $TenantFilter -ApplicationId $MFAAppID + Write-Information $PolicyUpdate.PolicyAction + } catch { + Write-Information "Failed to update app management policy: $($_.Exception.Message)" + } $PassReqBody = @{ 'passwordCredential' = @{ diff --git a/Modules/CIPPCore/Public/GraphHelper/Update-AppManagementPolicy.ps1 b/Modules/CIPPCore/Public/GraphHelper/Update-AppManagementPolicy.ps1 index 3655919169c5..1c5e20ae81df 100644 --- a/Modules/CIPPCore/Public/GraphHelper/Update-AppManagementPolicy.ps1 +++ b/Modules/CIPPCore/Public/GraphHelper/Update-AppManagementPolicy.ps1 @@ -6,14 +6,17 @@ function Update-AppManagementPolicy { .DESCRIPTION Retrieves tenant default app management policy and app management policies to check if passwordCredential or keyCredential creation is restricted. If the default policy blocks - credential addition and CIPP-SAM app doesn't have an exemption, creates or updates a policy - to allow CIPP-SAM to manage credentials. + credential addition and the targeted app doesn't have an exemption, creates or updates a policy + to allow the app to manage credentials. .FUNCTIONALITY Internal #> [CmdletBinding()] - param() + param( + $TenantFilter = $env:TenantID, + $ApplicationId = $env:ApplicationID + ) try { # Create bulk request to fetch both policies at once @@ -31,12 +34,12 @@ function Update-AppManagementPolicy { @{ id = 'appRegistration' method = 'GET' - url = "applications(appId='$env:ApplicationID')?`$select=id,appId,displayName" + url = "applications(appId='$ApplicationId')?`$select=id,appId,displayName" } ) # Execute bulk request - $Results = New-GraphBulkRequest -Requests $Requests -NoAuthCheck $true -asapp $true + $Results = New-GraphBulkRequest -Requests $Requests -NoAuthCheck $true -asapp $true -tenantid $TenantFilter # Parse results $DefaultPolicy = ($Results | Where-Object { $_.id -eq 'defaultPolicy' }).body @@ -46,7 +49,7 @@ function Update-AppManagementPolicy { # Check if CIPP-SAM app is targeted by any policies $CIPPAppTargeted = $false $CIPPAppPolicyId = $null - if ($AppPolicies -and $env:ApplicationID) { + if ($AppPolicies -and $ApplicationId) { # Build bulk requests to get appliesTo for each policy $AppliesToRequests = @($AppPolicies | ForEach-Object { @{ @@ -57,10 +60,10 @@ function Update-AppManagementPolicy { }) if ($AppliesToRequests.Count -gt 0) { - $AppliesToResults = New-GraphBulkRequest -Requests $AppliesToRequests -NoAuthCheck $true -asapp $true + $AppliesToResults = New-GraphBulkRequest -Requests $AppliesToRequests -NoAuthCheck $true -asapp $true -tenantid $TenantFilter - # Find which policy (if any) targets CIPP Ap - $CIPPPolicyResult = $AppliesToResults | Where-Object { $_.body.value.appId -contains $env:ApplicationID } | Select-Object -First 1 + # Find which policy (if any) targets the app + $CIPPPolicyResult = $AppliesToResults | Where-Object { $_.body.value.appId -contains $ApplicationId } | Select-Object -First 1 if ($CIPPPolicyResult) { $CIPPAppTargeted = $true $CIPPAppPolicyId = $CIPPPolicyResult.id @@ -126,7 +129,7 @@ function Update-AppManagementPolicy { $PolicyAction = $null if ($DefaultPolicyBlocksCredentials -and $CIPPApp) { # Check if a CIPP-SAM Exemption Policy already exists - $ExistingExemptionPolicy = $AppPolicies | Where-Object { $_.displayName -eq 'CIPP-SAM Exemption Policy' } | Select-Object -First 1 + $ExistingExemptionPolicy = $AppPolicies | Where-Object { $_.displayName -eq 'CIPP Exemption Policy' } | Select-Object -First 1 # Check if CIPP app has a policy that allows credentials $CIPPHasExemption = $false @@ -142,12 +145,12 @@ function Update-AppManagementPolicy { } if (-not $CIPPHasExemption) { - # Need to create or update a policy for CIPP-SAM + # Need to create or update a policy for CIPP try { # Define policy structure with disabled restrictions $PolicyBody = @{ - displayName = 'CIPP-SAM Exemption Policy' - description = 'Allows CIPP-SAM app to manage credentials' + displayName = 'CIPP Exemption Policy' + description = 'Allows CIPP app to manage credentials' isEnabled = $true restrictions = @{ passwordCredentials = @( @@ -168,7 +171,7 @@ function Update-AppManagementPolicy { if ($CIPPAppPolicyId) { # Update existing policy that's already assigned to the app - $null = New-GraphPostRequest -uri "https://graph.microsoft.com/v1.0/policies/appManagementPolicies/$CIPPAppPolicyId" -type PATCH -body ($PolicyBody | ConvertTo-Json -Depth 10) -asapp $true -NoAuthCheck $true + $null = New-GraphPostRequest -uri "https://graph.microsoft.com/v1.0/policies/appManagementPolicies/$CIPPAppPolicyId" -type PATCH -body ($PolicyBody | ConvertTo-Json -Depth 10) -asapp $true -NoAuthCheck $true -tenantid $TenantFilter $PolicyAction = "Updated existing policy $CIPPAppPolicyId to allow credentials" } elseif ($ExistingExemptionPolicy) { # Exemption policy exists but not assigned to app - update and assign it @@ -179,7 +182,7 @@ function Update-AppManagementPolicy { $AssignBody = @{ '@odata.id' = "https://graph.microsoft.com/beta/policies/appManagementPolicies/$($ExistingExemptionPolicy.id)" } - $null = New-GraphPostRequest -uri "https://graph.microsoft.com/beta/applications/$($CIPPApp.id)/appManagementPolicies/`$ref" -type POST -body ($AssignBody | ConvertTo-Json) -asapp $true -NoAuthCheck $true + $null = New-GraphPostRequest -uri "https://graph.microsoft.com/beta/applications/$($CIPPApp.id)/appManagementPolicies/`$ref" -type POST -body ($AssignBody | ConvertTo-Json) -asapp $true -NoAuthCheck $true -tenantid $TenantFilter $PolicyAction = "Updated and assigned existing policy $($ExistingExemptionPolicy.id) to CIPP-SAM" $CIPPAppPolicyId = $ExistingExemptionPolicy.id $CIPPAppTargeted = $true From 2c39eae4ad8667e16fdcc07679ec462d7368edb2 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 19 Feb 2026 12:00:07 -0500 Subject: [PATCH 463/503] Add try/catch and logging for Autopilot assignment Wraps the Autopilot profile assignment in a try/catch to handle errors, moves the success info log into the try block, and logs failures with Get-CippException details. Also tightens message interpolation for AssignTo and TenantFilter to produce clearer logs and a consistent success string. --- .../Set-CIPPDefaultAPDeploymentProfile.ps1 | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/Modules/CIPPCore/Public/Set-CIPPDefaultAPDeploymentProfile.ps1 b/Modules/CIPPCore/Public/Set-CIPPDefaultAPDeploymentProfile.ps1 index 818c2f97dfea..4a46e344d5e9 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDefaultAPDeploymentProfile.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDefaultAPDeploymentProfile.ps1 @@ -70,17 +70,22 @@ function Set-CIPPDefaultAPDeploymentProfile { } if ($AssignTo -eq $true) { - $AssignBody = '{"target":{"@odata.type":"#microsoft.graph.allDevicesAssignmentTarget"}}' - if ($PSCmdlet.ShouldProcess($AssignTo, "Assign Autopilot profile $DisplayName")) { - #Get assignments - $Assignments = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/deviceManagement/windowsAutopilotDeploymentProfiles/$($GraphRequest.id)/assignments" -tenantid $TenantFilter - if (!$Assignments) { - $null = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/deviceManagement/windowsAutopilotDeploymentProfiles/$($GraphRequest.id)/assignments" -tenantid $TenantFilter -type POST -body $AssignBody + try { + $AssignBody = '{"target":{"@odata.type":"#microsoft.graph.allDevicesAssignmentTarget"}}' + if ($PSCmdlet.ShouldProcess($AssignTo, "Assign Autopilot profile $DisplayName")) { + #Get assignments + $Assignments = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/deviceManagement/windowsAutopilotDeploymentProfiles/$($GraphRequest.id)/assignments" -tenantid $TenantFilter + if (!$Assignments) { + $null = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/deviceManagement/windowsAutopilotDeploymentProfiles/$($GraphRequest.id)/assignments" -tenantid $TenantFilter -type POST -body $AssignBody + } + Write-LogMessage -Headers $User -API $APIName -tenant $TenantFilter -message "Assigned autopilot profile $($DisplayName) to $($AssignTo)" -Sev 'Info' } - Write-LogMessage -Headers $User -API $APIName -tenant $TenantFilter -message "Assigned autopilot profile $($DisplayName) to $AssignTo" -Sev 'Info' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -Headers $User -API $APIName -tenant $TenantFilter -message "Failed to assign Autopilot profile $($DisplayName) to $($AssignTo): $($ErrorMessage.NormalizedError)" -Sev 'Error' -LogData $ErrorMessage } } - "Successfully $($Type)ed profile for $TenantFilter" + "Successfully $($Type)ed profile for $($TenantFilter)" } catch { $ErrorMessage = Get-CippException -Exception $_ $Result = "Failed $($Type)ing Autopilot Profile $($DisplayName). Error: $($ErrorMessage.NormalizedError)" From 9fd4fc7b306416a2f25612f35588eb8ec9604adb Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 19 Feb 2026 12:24:28 -0500 Subject: [PATCH 464/503] fix autopilot standard comparisons --- .../Invoke-CIPPStandardAutopilotProfile.ps1 | 2 +- .../Invoke-CIPPStandardAutopilotStatusPage.ps1 | 13 ++++++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAutopilotProfile.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAutopilotProfile.ps1 index 508e56635b2e..d077b530922e 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAutopilotProfile.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAutopilotProfile.ps1 @@ -52,7 +52,7 @@ function Invoke-CIPPStandardAutopilotProfile { Where-Object { $_.displayName -eq $Settings.DisplayName } | Select-Object -Property displayName, description, deviceNameTemplate, locale, preprovisioningAllowed, hardwareHashExtractionEnabled, outOfBoxExperienceSetting - if ($Settings.NotLocalAdmin -eq $true) { $userType = 'Standard' } else { $userType = 'Administrator' } + if ($Settings.NotLocalAdmin -eq $true) { $userType = 'standard' } else { $userType = 'administrator' } if ($Settings.SelfDeployingMode -eq $true) { $DeploymentMode = 'shared' $Settings.AllowWhiteGlove = $false diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAutopilotStatusPage.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAutopilotStatusPage.ps1 index 1fcf83165723..6631b4809b5d 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAutopilotStatusPage.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAutopilotStatusPage.ps1 @@ -67,7 +67,18 @@ function Invoke-CIPPStandardAutopilotStatusPage { $StateIsCorrect = $false } - $CurrentValue = $CurrentConfig | Select-Object -Property id, displayName, priority, showInstallationProgress, blockDeviceSetupRetryByUser, allowDeviceResetOnInstallFailure, allowLogCollectionOnInstallFailure, customErrorMessage, installProgressTimeoutInMinutes, allowDeviceUseOnInstallFailure, trackInstallProgressForAutopilotOnly, installQualityUpdates + $CurrentValue = [PSCustomObject]@{ + installProgressTimeoutInMinutes = $CurrentConfig.installProgressTimeoutInMinutes + customErrorMessage = $CurrentConfig.customErrorMessage + showInstallationProgress = $CurrentConfig.showInstallationProgress + allowLogCollectionOnInstallFailure = $CurrentConfig.allowLogCollectionOnInstallFailure + trackInstallProgressForAutopilotOnly = $CurrentConfig.trackInstallProgressForAutopilotOnly + blockDeviceSetupRetryByUser = $CurrentConfig.blockDeviceSetupRetryByUser + installQualityUpdates = $CurrentConfig.installQualityUpdates + allowDeviceResetOnInstallFailure = $CurrentConfig.allowDeviceResetOnInstallFailure + allowDeviceUseOnInstallFailure = $CurrentConfig.allowDeviceUseOnInstallFailure + } + $ExpectedValue = [PSCustomObject]@{ installProgressTimeoutInMinutes = $Settings.TimeOutInMinutes customErrorMessage = $Settings.ErrorMessage From ac075b253f56587d39e20e3cad93abdddc6f2e14 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 19 Feb 2026 12:34:49 -0500 Subject: [PATCH 465/503] add default empty strings for better comparison --- .../Invoke-CIPPStandardintuneBrandingProfile.ps1 | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardintuneBrandingProfile.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardintuneBrandingProfile.ps1 index 66ed49f285ec..ecac361e88e6 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardintuneBrandingProfile.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardintuneBrandingProfile.ps1 @@ -114,13 +114,13 @@ function Invoke-CIPPStandardintuneBrandingProfile { displayName = $CurrentState.displayName showLogo = $CurrentState.showLogo showDisplayNameNextToLogo = $CurrentState.showDisplayNameNextToLogo - contactITName = $CurrentState.contactITName - contactITPhoneNumber = $CurrentState.contactITPhoneNumber - contactITEmailAddress = $CurrentState.contactITEmailAddress - contactITNotes = $CurrentState.contactITNotes - onlineSupportSiteName = $CurrentState.onlineSupportSiteName - onlineSupportSiteUrl = $CurrentState.onlineSupportSiteUrl - privacyUrl = $CurrentState.privacyUrl + contactITName = $CurrentState.contactITName ?? '' + contactITPhoneNumber = $CurrentState.contactITPhoneNumber ?? '' + contactITEmailAddress = $CurrentState.contactITEmailAddress ?? '' + contactITNotes = $CurrentState.contactITNotes ?? '' + onlineSupportSiteName = $CurrentState.onlineSupportSiteName ?? '' + onlineSupportSiteUrl = $CurrentState.onlineSupportSiteUrl ?? '' + privacyUrl = $CurrentState.privacyUrl ?? '' } $ExpectedValue = @{ displayName = $Settings.displayName From d38f8af30d9aedadf6eca544818ec46a2e0ecdd7 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 19 Feb 2026 12:48:20 -0500 Subject: [PATCH 466/503] return error if blob upload fails --- Modules/CIPPCore/Public/New-CIPPBackup.ps1 | 1 + 1 file changed, 1 insertion(+) diff --git a/Modules/CIPPCore/Public/New-CIPPBackup.ps1 b/Modules/CIPPCore/Public/New-CIPPBackup.ps1 index 374dfaf4ee8e..f5760c802330 100644 --- a/Modules/CIPPCore/Public/New-CIPPBackup.ps1 +++ b/Modules/CIPPCore/Public/New-CIPPBackup.ps1 @@ -147,6 +147,7 @@ function New-CIPPBackup { } catch { $ErrorMessage = Get-CippException -Exception $_ Write-LogMessage -headers $Headers -API $APINAME -message "Blob upload failed: $($ErrorMessage.NormalizedError)" -Sev 'Error' -LogData $ErrorMessage + return [pscustomobject]@{'Results' = "Blob Upload failed: $($ErrorMessage.NormalizedError)" } } # Write table entity pointing to blob resource From 3aebafb112b5a8e5de102cc2638d46c13d65d2d1 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 19 Feb 2026 15:56:27 -0500 Subject: [PATCH 467/503] Prefer latest Intune policy when filtering by name When multiple policies share the same displayName, choose the most recently modified one. Added Sort-Object -Property lastModifiedDateTime -Descending | Select-Object -First 1 to displayName/Name lookups across Get-CIPPIntunePolicy.ps1 (including Android/iOS bulk results and various template branches) so the function returns the latest matching policy instead of an arbitrary/older one or duplicates. --- .../CIPPCore/Public/Get-CIPPIntunePolicy.ps1 | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Modules/CIPPCore/Public/Get-CIPPIntunePolicy.ps1 b/Modules/CIPPCore/Public/Get-CIPPIntunePolicy.ps1 index ba0cbda8cfaa..9d9143b63151 100644 --- a/Modules/CIPPCore/Public/Get-CIPPIntunePolicy.ps1 +++ b/Modules/CIPPCore/Public/Get-CIPPIntunePolicy.ps1 @@ -35,8 +35,8 @@ function Get-CIPPIntunePolicy { $iOSPolicies = ($BulkResults | Where-Object { $_.id -eq 'iOSPolicies' }).body.value if ($DisplayName) { - $androidPolicy = $androidPolicies | Where-Object -Property displayName -EQ $DisplayName - $iOSPolicy = $iOSPolicies | Where-Object -Property displayName -EQ $DisplayName + $androidPolicy = $androidPolicies | Where-Object -Property displayName -EQ $DisplayName | Sort-Object -Property lastModifiedDateTime -Descending | Select-Object -First 1 + $iOSPolicy = $iOSPolicies | Where-Object -Property displayName -EQ $DisplayName | Sort-Object -Property lastModifiedDateTime -Descending | Select-Object -First 1 # Return the matching policy (Android or iOS) - using full data from bulk request if ($androidPolicy) { @@ -92,7 +92,7 @@ function Get-CIPPIntunePolicy { if ($DisplayName) { $policies = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/$PlatformType/$TemplateTypeURL" -tenantid $tenantFilter - $policy = $policies | Where-Object -Property displayName -EQ $DisplayName + $policy = $policies | Where-Object -Property displayName -EQ $DisplayName | Sort-Object -Property lastModifiedDateTime -Descending | Select-Object -First 1 if ($policy) { $policyDetails = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/$PlatformType/$TemplateTypeURL('$($policy.id)')?`$expand=scheduledActionsForRule(`$expand=scheduledActionConfigurations)" -tenantid $tenantFilter $policyJson = ConvertTo-Json -InputObject $policyDetails -Depth 100 -Compress @@ -122,7 +122,7 @@ function Get-CIPPIntunePolicy { if ($DisplayName) { $policies = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/$PlatformType/$TemplateTypeURL" -tenantid $tenantFilter - $policy = $policies | Where-Object -Property displayName -EQ $DisplayName + $policy = $policies | Where-Object -Property displayName -EQ $DisplayName | Sort-Object -Property lastModifiedDateTime -Descending | Select-Object -First 1 if ($policy) { $definitionValues = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/$PlatformType/$TemplateTypeURL('$($policy.id)')/definitionValues" -tenantid $tenantFilter $policy | Add-Member -MemberType NoteProperty -Name 'definitionValues' -Value $definitionValues -Force @@ -237,7 +237,7 @@ function Get-CIPPIntunePolicy { if ($DisplayName) { $policies = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/$PlatformType/$TemplateTypeURL" -tenantid $tenantFilter - $policy = $policies | Where-Object -Property displayName -EQ $DisplayName + $policy = $policies | Where-Object -Property displayName -EQ $DisplayName | Sort-Object -Property lastModifiedDateTime -Descending | Select-Object -First 1 if ($policy) { $policyDetails = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/$PlatformType/$TemplateTypeURL('$($policy.id)')" -tenantid $tenantFilter $policyDetails = $policyDetails | Select-Object * -ExcludeProperty id, lastModifiedDateTime, '@odata.context', 'ScopeTagIds', 'supportsScopeTags', 'createdDateTime' @@ -270,7 +270,7 @@ function Get-CIPPIntunePolicy { if ($DisplayName) { $policies = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/$PlatformType/$TemplateTypeURL" -tenantid $tenantFilter - $policy = $policies | Where-Object -Property Name -EQ $DisplayName + $policy = $policies | Where-Object -Property Name -EQ $DisplayName | Sort-Object -Property lastModifiedDateTime -Descending | Select-Object -First 1 if ($policy) { $policyDetails = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/$PlatformType/$TemplateTypeURL('$($policy.id)')?`$expand=settings" -tenantid $tenantFilter $policyDetails = $policyDetails | Select-Object name, description, settings, platforms, technologies, templateReference @@ -303,7 +303,7 @@ function Get-CIPPIntunePolicy { if ($DisplayName) { $policies = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/$PlatformType/$TemplateTypeURL" -tenantid $tenantFilter - $policy = $policies | Where-Object -Property displayName -EQ $DisplayName + $policy = $policies | Where-Object -Property displayName -EQ $DisplayName | Sort-Object -Property lastModifiedDateTime -Descending | Select-Object -First 1 if ($policy) { $policyDetails = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/$PlatformType/$TemplateTypeURL('$($policy.id)')" -tenantid $tenantFilter $policyDetails = $policyDetails | Select-Object * -ExcludeProperty id, lastModifiedDateTime, '@odata.context', 'ScopeTagIds', 'supportsScopeTags', 'createdDateTime' @@ -336,7 +336,7 @@ function Get-CIPPIntunePolicy { if ($DisplayName) { $policies = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/$PlatformType/$TemplateTypeURL" -tenantid $tenantFilter - $policy = $policies | Where-Object -Property displayName -EQ $DisplayName + $policy = $policies | Where-Object -Property displayName -EQ $DisplayName | Sort-Object -Property lastModifiedDateTime -Descending | Select-Object -First 1 if ($policy) { $policyDetails = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/$PlatformType/$TemplateTypeURL('$($policy.id)')" -tenantid $tenantFilter $policyDetails = $policyDetails | Select-Object * -ExcludeProperty id, lastModifiedDateTime, '@odata.context', 'ScopeTagIds', 'supportsScopeTags', 'createdDateTime' @@ -369,7 +369,7 @@ function Get-CIPPIntunePolicy { if ($DisplayName) { $policies = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/$PlatformType/$TemplateTypeURL" -tenantid $tenantFilter - $policy = $policies | Where-Object -Property displayName -EQ $DisplayName + $policy = $policies | Where-Object -Property displayName -EQ $DisplayName | Sort-Object -Property lastModifiedDateTime -Descending | Select-Object -First 1 if ($policy) { $policyDetails = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/$PlatformType/$TemplateTypeURL('$($policy.id)')" -tenantid $tenantFilter $policyDetails = $policyDetails | Select-Object * -ExcludeProperty id, lastModifiedDateTime, '@odata.context', 'ScopeTagIds', 'supportsScopeTags', 'createdDateTime' @@ -402,7 +402,7 @@ function Get-CIPPIntunePolicy { if ($DisplayName) { $policies = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/$PlatformType/$TemplateTypeURL" -tenantid $tenantFilter - $policy = $policies | Where-Object -Property displayName -EQ $DisplayName + $policy = $policies | Where-Object -Property displayName -EQ $DisplayName | Sort-Object -Property lastModifiedDateTime -Descending | Select-Object -First 1 if ($policy) { $policyDetails = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/$PlatformType/$TemplateTypeURL('$($policy.id)')" -tenantid $tenantFilter $policyDetails = $policyDetails | Select-Object * -ExcludeProperty id, lastModifiedDateTime, '@odata.context', 'ScopeTagIds', 'supportsScopeTags', 'createdDateTime' From 46ec3ec56c6559509ce0eaf8f11a701d422f9352 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 19 Feb 2026 16:32:42 -0500 Subject: [PATCH 468/503] Validate LitigationHoldDuration input Only assign $Settings.days to the LitigationHoldDuration parameter if it is a positive integer or the string 'Unlimited'. Adds a TryParse check and conditional logic to avoid passing invalid/non-numeric values to the cmdlet, preventing erroneous requests. --- .../Standards/Invoke-CIPPStandardEnableLitigationHold.ps1 | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableLitigationHold.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableLitigationHold.ps1 index 35f68f3350d9..54a45b6c08c5 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableLitigationHold.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableLitigationHold.ps1 @@ -59,12 +59,14 @@ function Invoke-CIPPStandardEnableLitigationHold { } } if ($null -ne $Settings.days) { - $params.CmdletInput.Parameters['LitigationHoldDuration'] = $Settings.days + $Days = [int]::TryParse($Settings.days, [ref]$null) ? $Settings.days : $null + if ($Days -gt 0 -or $Settings.days -eq 'Unlimited') { + $params.CmdletInput.Parameters['LitigationHoldDuration'] = $Settings.days + } } $params } - $BatchResults = New-ExoBulkRequest -tenantid $Tenant -cmdletArray @($Request) foreach ($Result in $BatchResults) { if ($Result.error) { From dc0de25601b616f10695c14f246d0100916e3ce2 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 19 Feb 2026 22:08:55 -0500 Subject: [PATCH 469/503] fix casing for json comparison --- .../Public/Standards/Invoke-CIPPStandardOauthConsent.ps1 | 6 +++--- .../Standards/Invoke-CIPPStandardOauthConsentLowSec.ps1 | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardOauthConsent.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardOauthConsent.ps1 index bf86be5f502e..18ab63baafb8 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardOauthConsent.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardOauthConsent.ps1 @@ -47,7 +47,7 @@ function Invoke-CIPPStandardOauthConsent { Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the OauthConsent state for $Tenant. Error: $ErrorMessage" -Sev Error return } - $StateIsCorrect = if ($State.permissionGrantPolicyIdsAssignedToDefaultUserRole -eq 'managePermissionGrantsForSelf.cipp-consent-policy') { $true } else { $false } + $StateIsCorrect = if ($State.permissionGrantPolicyIdsAssignedToDefaultUserRole -eq 'ManagePermissionGrantsForSelf.cipp-consent-policy') { $true } else { $false } if ($Settings.remediate -eq $true) { $AllowedAppIdsForTenant = $settings.AllowedApps -split ',' | ForEach-Object { $_.Trim() } @@ -77,8 +77,8 @@ function Invoke-CIPPStandardOauthConsent { "Could not add exclusions, probably already exist: $($_)" } - if ($State.permissionGrantPolicyIdsAssignedToDefaultUserRole -notin @('managePermissionGrantsForSelf.cipp-consent-policy')) { - New-GraphPostRequest -tenantid $tenant -Uri 'https://graph.microsoft.com/beta/policies/authorizationPolicy/authorizationPolicy' -Type PATCH -Body '{"permissionGrantPolicyIdsAssignedToDefaultUserRole":["managePermissionGrantsForSelf.cipp-consent-policy"]}' -ContentType 'application/json' + if ($State.permissionGrantPolicyIdsAssignedToDefaultUserRole -notin @('ManagePermissionGrantsForSelf.cipp-consent-policy')) { + New-GraphPostRequest -tenantid $tenant -Uri 'https://graph.microsoft.com/beta/policies/authorizationPolicy/authorizationPolicy' -Type PATCH -Body '{"permissionGrantPolicyIdsAssignedToDefaultUserRole":["ManagePermissionGrantsForSelf.cipp-consent-policy"]}' -ContentType 'application/json' } Write-LogMessage -API 'Standards' -tenant $tenant -message 'Application Consent Mode has been enabled.' -sev Info diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardOauthConsentLowSec.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardOauthConsentLowSec.ps1 index e28c87dc27e9..34d47a81ffee 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardOauthConsentLowSec.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardOauthConsentLowSec.ps1 @@ -49,7 +49,7 @@ function Invoke-CIPPStandardOauthConsentLowSec { $ConflictingStandard = $Standards | Where-Object -Property Standard -EQ 'OauthConsent' if ($Settings.remediate -eq $true) { - if ($State.permissionGrantPolicyIdsAssignedToDefaultUserRole -in @('managePermissionGrantsForSelf.microsoft-user-default-low')) { + if ($State.permissionGrantPolicyIdsAssignedToDefaultUserRole -in @('ManagePermissionGrantsForSelf.microsoft-user-default-low')) { Write-LogMessage -API 'Standards' -tenant $tenant -message 'Application Consent Mode(microsoft-user-default-low) is already enabled.' -sev Info } elseif ($ConflictingStandard -and $State.permissionGrantPolicyIdsAssignedToDefaultUserRole -contains 'ManagePermissionGrantsForSelf.cipp-consent-policy') { Write-LogMessage -API 'Standards' -tenant $tenant -message 'There is a conflicting OAuth Consent policy standard enabled for this tenant. Remove the Require admin consent for applications (Prevent OAuth phishing) standard from this tenant to apply the low security standard.' -sev Error @@ -60,7 +60,7 @@ function Invoke-CIPPStandardOauthConsentLowSec { Uri = 'https://graph.microsoft.com/beta/policies/authorizationPolicy/authorizationPolicy' Type = 'PATCH' Body = @{ - permissionGrantPolicyIdsAssignedToDefaultUserRole = @('managePermissionGrantsForSelf.microsoft-user-default-low') + permissionGrantPolicyIdsAssignedToDefaultUserRole = @('ManagePermissionGrantsForSelf.microsoft-user-default-low') } | ConvertTo-Json ContentType = 'application/json' } @@ -98,7 +98,7 @@ function Invoke-CIPPStandardOauthConsentLowSec { } if ($Settings.alert -eq $true) { - if ($State.permissionGrantPolicyIdsAssignedToDefaultUserRole -notin @('managePermissionGrantsForSelf.microsoft-user-default-low')) { + if ($State.permissionGrantPolicyIdsAssignedToDefaultUserRole -notin @('ManagePermissionGrantsForSelf.microsoft-user-default-low')) { Write-StandardsAlert -message 'Application Consent Mode(microsoft-user-default-low) is not enabled' -object $State -tenant $tenant -standardName 'OauthConsentLowSec' -standardId $Settings.standardId Write-LogMessage -API 'Standards' -tenant $tenant -message 'Application Consent Mode(microsoft-user-default-low) is not enabled.' -sev Info } else { From 115ab34eeb4102eff26565cb3217d23081c6401f Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 19 Feb 2026 22:38:43 -0500 Subject: [PATCH 470/503] Group PIM cache items under P2 section Move PIM-related cache entries into the Azure AD Premium P2 cache list and update the section heading. Removed RoleEligibilitySchedules, RoleManagementPolicies and RoleAssignmentScheduleInstances from the earlier list and added RoleEligibilitySchedules, RoleAssignmentSchedules and RoleManagementPolicies to the P2 cache functions. Also updated the region comment to "Identity Protection/PIM features" to reflect the grouping. --- .../Activity Triggers/Push-CIPPDBCacheData.ps1 | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 index 9351980ef740..907f5ad3de9d 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 @@ -50,9 +50,6 @@ function Push-CIPPDBCacheData { 'SecureScore' 'PIMSettings' 'Domains' - 'RoleEligibilitySchedules' - 'RoleManagementPolicies' - 'RoleAssignmentScheduleInstances' 'B2BManagementPolicy' 'AuthenticationFlowsPolicy' 'DeviceRegistrationPolicy' @@ -130,13 +127,16 @@ function Push-CIPPDBCacheData { } #endregion Conditional Access Licensed - #region Azure AD Premium P2 - Identity Protection features + #region Azure AD Premium P2 - Identity Protection/PIM features if ($AzureADPremiumP2Capable) { $P2CacheFunctions = @( 'RiskyUsers' 'RiskyServicePrincipals' 'ServicePrincipalRiskDetections' 'RiskDetections' + 'RoleEligibilitySchedules' + 'RoleAssignmentSchedules' + 'RoleManagementPolicies' ) foreach ($CacheFunction in $P2CacheFunctions) { $Batch.Add(@{ From 7a33197177fd701ce5ee54166cbc3280d1a49222 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 19 Feb 2026 22:53:33 -0500 Subject: [PATCH 471/503] fix json body for webhooks --- Modules/CIPPCore/Public/Send-CIPPAlert.ps1 | 24 ++++++++++++++-------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/Modules/CIPPCore/Public/Send-CIPPAlert.ps1 b/Modules/CIPPCore/Public/Send-CIPPAlert.ps1 index 3451827a67d3..c26f0f2254e8 100644 --- a/Modules/CIPPCore/Public/Send-CIPPAlert.ps1 +++ b/Modules/CIPPCore/Public/Send-CIPPAlert.ps1 @@ -112,35 +112,41 @@ function Send-CIPPAlert { $Headers = $null } - $JSONBody = Get-CIPPTextReplacement -TenantFilter $TenantFilter -Text $JSONContent -EscapeForJson + $ReplacedContent = Get-CIPPTextReplacement -TenantFilter $TenantFilter -Text $JSONContent -EscapeForJson try { if (![string]::IsNullOrWhiteSpace($Config.webhook) -or ![string]::IsNullOrWhiteSpace($AltWebhook)) { if ($PSCmdlet.ShouldProcess($Config.webhook, 'Sending webhook')) { $webhook = if ($AltWebhook) { $AltWebhook } else { $Config.webhook } switch -wildcard ($webhook) { '*webhook.office.com*' { - $JSONBody = "{`"text`": `"You've setup your alert policies to be alerted whenever specific events happen. We've found some of these events in the log.

$JSONContent`"}" - Invoke-RestMethod -Uri $webhook -Method POST -ContentType 'Application/json' -Body $JSONBody + $TeamsBody = [PSCustomObject]@{ + text = "You've setup your alert policies to be alerted whenever specific events happen. We've found some of these events in the log.

$ReplacedContent" + } | ConvertTo-Json -Compress + Invoke-RestMethod -Uri $webhook -Method POST -ContentType 'Application/json' -Body $TeamsBody } '*discord.com*' { - $JSONBody = "{`"content`": `"You've setup your alert policies to be alerted whenever specific events happen. We've found some of these events in the log. $JSONContent`"}" - Invoke-RestMethod -Uri $webhook -Method POST -ContentType 'Application/json' -Body $JSONBody + $DiscordBody = [PSCustomObject]@{ + content = "You've setup your alert policies to be alerted whenever specific events happen. We've found some of these events in the log. ``````$ReplacedContent``````" + } | ConvertTo-Json -Compress + Invoke-RestMethod -Uri $webhook -Method POST -ContentType 'Application/json' -Body $DiscordBody } '*slack.com*' { $SlackBlocks = Get-SlackAlertBlocks -JSONBody $JSONContent if ($SlackBlocks.blocks) { - $JSONBody = $SlackBlocks | ConvertTo-Json -Depth 10 -Compress + $SlackBody = $SlackBlocks | ConvertTo-Json -Depth 10 -Compress } else { - $JSONBody = "{`"text`": `"You've setup your alert policies to be alerted whenever specific events happen. We've found some of these events in the log. $JSONContent`"}" + $SlackBody = [PSCustomObject]@{ + text = "You've setup your alert policies to be alerted whenever specific events happen. We've found some of these events in the log. ``````$ReplacedContent``````" + } | ConvertTo-Json -Compress } - Invoke-RestMethod -Uri $webhook -Method POST -ContentType 'Application/json' -Body $JSONBody + Invoke-RestMethod -Uri $webhook -Method POST -ContentType 'Application/json' -Body $SlackBody } default { $RestMethod = @{ Uri = $webhook Method = 'POST' ContentType = 'application/json' - Body = $JSONContent + Body = $ReplacedContent } if ($Headers) { $RestMethod['Headers'] = $Headers From 4fef64712f02368bd1aee83ce669fb599a7af73a Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 20 Feb 2026 00:37:32 -0500 Subject: [PATCH 472/503] remove logging --- Modules/CIPPCore/Public/Get-CIPPCalendarPermissionReport.ps1 | 2 -- 1 file changed, 2 deletions(-) diff --git a/Modules/CIPPCore/Public/Get-CIPPCalendarPermissionReport.ps1 b/Modules/CIPPCore/Public/Get-CIPPCalendarPermissionReport.ps1 index e6c66c284a25..b8a014d50656 100644 --- a/Modules/CIPPCore/Public/Get-CIPPCalendarPermissionReport.ps1 +++ b/Modules/CIPPCore/Public/Get-CIPPCalendarPermissionReport.ps1 @@ -31,8 +31,6 @@ function Get-CIPPCalendarPermissionReport { ) try { - Write-LogMessage -API 'CalendarPermissionReport' -tenant $TenantFilter -message 'Generating calendar permission report' -sev Info - # Handle AllTenants if ($TenantFilter -eq 'AllTenants') { # Get all tenants that have calendar data From 56f7e9b3136e6a0038c9c6831fa191742d555851 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Fri, 20 Feb 2026 10:29:29 +0100 Subject: [PATCH 473/503] endREceivedDate --- .../Alerts/Get-CIPPAlertQuarantineReleaseRequests.ps1 | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertQuarantineReleaseRequests.ps1 b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertQuarantineReleaseRequests.ps1 index 81a1fecada71..58ed3b035407 100644 --- a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertQuarantineReleaseRequests.ps1 +++ b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertQuarantineReleaseRequests.ps1 @@ -29,7 +29,13 @@ } try { - $RequestedReleases = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-QuarantineMessage' -cmdParams @{ PageSize = 1000; ReleaseStatus = 'Requested'; StartReceivedDate = (Get-Date).AddHours(-6) } -ErrorAction Stop | Select-Object -ExcludeProperty *data.type* | Sort-Object -Property ReceivedTime + $cmdParams = @{ + PageSize = 1000 + ReleaseStatus = 'Requested' + StartReceivedDate = (Get-Date).AddHours(-6) + EndReceivedDate = (Get-Date).AddHours(0) + } + $RequestedReleases = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-QuarantineMessage' -cmdParams $cmdParams -ErrorAction Stop | Select-Object -ExcludeProperty *data.type* | Sort-Object -Property ReceivedTime if ($RequestedReleases) { # Get the CIPP URL for the Quarantine link From 3cfb562051c3b105410ccd31b3fd1dcc9c2fed47 Mon Sep 17 00:00:00 2001 From: rvdwegen Date: Fri, 20 Feb 2026 10:43:26 +0100 Subject: [PATCH 474/503] concept gdap trace --- .../CIPP/Settings/Invoke-ExecGDAPTrace.ps1 | 782 ++++++++++++++++++ 1 file changed, 782 insertions(+) create mode 100644 Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecGDAPTrace.ps1 diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecGDAPTrace.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecGDAPTrace.ps1 new file mode 100644 index 000000000000..d138555690a5 --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecGDAPTrace.ps1 @@ -0,0 +1,782 @@ +function Invoke-ExecAccessTest { + <# + .SYNOPSIS + Tests the complete GDAP (Granular Delegated Admin Privileges) access path for a user. + + This function traces the access path from customer tenant → GDAP relationships → mapped security groups → user, + checking all 15 standard GDAP roles. It verifies whether a SAM user in the partner tenant has access to each + role through direct or nested group memberships across all active GDAP relationships for a customer tenant. + + The function returns a role-centric view showing: + - For each of the 15 GDAP roles: whether it's assigned, whether the user has access, and the complete path + - Complete traceability: Role → Relationship → Group → User (including nested group paths) + - Broken path detection: identifies roles assigned but user not a member of the required groups + + The output is structured as JSON suitable for diagram visualization, showing the complete access chain + regardless of which relationship provides each role. + + Very boilerplate AI code. Needs some simplification and cleanup. + Ridiculous amount of comments to explain the logic so I don't have to explain it to Claude on the frontend. - rvd + + .DESCRIPTION + GDAP Access Path Testing: + 1. Validates input parameters (TenantFilter and UPN) + 2. Retrieves customer tenant information + 3. Gets all active GDAP relationships for the customer tenant + 4. Locates the UPN in the partner tenant + 5. Gets user's transitive group memberships (handles nested groups automatically) + 6. For each GDAP relationship: + - Retrieves all access assignments (mapped security groups) + - For each group: checks user membership (direct or nested) and traces the path + - Maps roles to relationships and groups + 7. For each of the 15 GDAP roles: + - Finds all relationships/groups that have this role assigned + - Checks if user is a member of any group with this role + - Builds complete access path showing how user gets the role (if they do) + 8. Returns comprehensive JSON with role-centric view and complete path traces + + .FUNCTIONALITY + Entrypoint + .ROLE + CIPP.AppSettings.Read + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + # Initialize API logging + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + Write-LogMessage -Headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug' + + # Extract query parameters + # TenantFilter: The customer tenant ID or domain name to test access for + # UPN: The User Principal Name of the SAM user in the partner tenant whose access we're testing + $TenantFilter = $Request.Query.TenantFilter + $UPN = $Request.Query.UPN + + # Validate required input parameters + if ([string]::IsNullOrWhiteSpace($TenantFilter)) { + return [HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::BadRequest + Body = @{ Error = 'TenantFilter is required' } + } + } + + if ([string]::IsNullOrWhiteSpace($UPN)) { + return [HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::BadRequest + Body = @{ Error = 'UPN is required' } + } + } + + try { + # ============================================================================ + # STEP 1: Define all 15 standard GDAP roles + # ============================================================================ + # These are the roles that should be available through GDAP relationships. + # Each role has a unique roleDefinitionId (GUID) that Microsoft Graph uses + # to identify the role. We'll check if the user has access to each of these + # roles through any GDAP relationship, regardless of which relationship provides it. + # + # Note: The roleDefinitionId is the template ID used in Azure AD role definitions. + # These IDs are consistent across all tenants and are used in GDAP access assignments. + # ============================================================================ + + # Get these from the repo in future -rvd + $AllGDAPRoles = @( + @{ Name = 'Application Administrator'; Id = '9b895d92-2cd3-44c7-9d02-a6ac2d5ea5c3'; Description = 'Can create and manage all applications, service principals, app registration, enterprise apps, consent requests. Cannot manage directory roles, security groups.' }, + @{ Name = 'Authentication Policy Administrator'; Id = '0526716b-113d-4c15-b2c8-68e3c22b9f80'; Description = 'Configures authentication methods policy, MFA settings, manages Password Protection settings, creates/manages verifiable credentials, Azure support tickets. Restrictions on updating sensitive properties, deleting/restoring users, legacy MFA settings.' }, + @{ Name = 'Billing Administrator'; Id = 'b0f54661-2d74-4c50-afa3-1ec803f12efe'; Description = 'Can perform common billing related tasks like updating payment information.' }, + @{ Name = 'Cloud App Security Administrator'; Id = '892c5842-a9a6-463a-8041-72aa08ca3cf6'; Description = 'Manages all aspects of the Defender for Cloud App Security in Azure AD, including policies, alerts, and related configurations.' }, + @{ Name = 'Cloud Device Administrator'; Id = '7698a772-787b-4ac8-901f-60d6b08affd2'; Description = 'Enables, disables, deletes devices in Azure AD, reads Windows 10 BitLocker keys. Does not grant permissions to manage other properties on the device.' }, + @{ Name = 'Domain Name Administrator'; Id = '8329153a-20ed-4bf8-aa37-81242c6e8e01'; Description = 'Can manage domain names in cloud and on-premises.' }, + @{ Name = 'Exchange Administrator'; Id = '29232cdf-9323-42fd-ade2-1d097af3e4de'; Description = 'Manages all aspects of Exchange Online, including mailboxes, permissions, connectivity, and related settings. Limited access to related Exchange settings in Azure AD.' }, + @{ Name = 'Global Reader'; Id = 'f2ef992c-3afb-46b9-b7cf-a126ee74c451'; Description = 'Can read everything that a Global Administrator can but not update anything.' }, + @{ Name = 'Intune Administrator'; Id = '3a2c62db-5318-420d-8d74-23affee5d9d5'; Description = 'Manages all aspects of Intune, including all related resources, policies, configurations, and tasks.' }, + @{ Name = 'Privileged Authentication Administrator'; Id = '7be44c8a-adaf-4e2a-84d6-ab2649e08a13'; Description = 'Sets/resets authentication methods for all users (admin or non-admin), deletes/restores any users. Manages support tickets in Azure and Microsoft 365. Restrictions on managing per-user MFA in legacy MFA portal.' }, + @{ Name = 'Privileged Role Administrator'; Id = 'e8611ab8-c189-46e8-94e1-60213ab1f814'; Description = 'Manages role assignments in Azure AD, Azure AD Privileged Identity Management, creates/manages groups, manages all aspects of Privileged Identity Management, administrative units. Allows managing assignments for all Azure AD roles including Global Administrator.' }, + @{ Name = 'Security Administrator'; Id = '194ae4cb-b126-40b2-bd5b-6091b380977d'; Description = 'Can read security information and reports, and manages security-related features, including identity protection, security policies, device management, and threat management in Azure AD and Office 365.' }, + @{ Name = 'SharePoint Administrator'; Id = 'f28a1f50-f6e7-4571-818b-6a12f2af6b6c'; Description = 'Manages all aspects of SharePoint Online, Microsoft 365 groups, support tickets, service health. Scoped permissions for Microsoft Intune, SharePoint, and OneDrive resources.' }, + @{ Name = 'Teams Administrator'; Id = '69091246-20e8-4a56-aa4d-066075b2a7a8'; Description = 'Manages all aspects of Microsoft Teams, including telephony, messaging, meetings, teams, Microsoft 365 groups, support tickets, and service health.' }, + @{ Name = 'User Administrator'; Id = 'fe930be7-5e62-47db-91af-98c3a49a38b1'; Description = 'Manages all aspects of users, groups, registration, and resets passwords for limited admins. Cannot manage security-related policies or other configuration objects.' } + ) + + # ============================================================================ + # STEP 2: Get customer tenant information + # ============================================================================ + # The TenantFilter can be either a tenant ID (GUID) or a domain name. + # Get-Tenants will resolve it and return the tenant object with customerId and displayName. + # ============================================================================ + $Tenant = Get-Tenants -TenantFilter $TenantFilter + if (-not $Tenant) { + return [HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::NotFound + Body = @{ Error = "Tenant not found: $TenantFilter" } + } + } + + $CustomerTenantId = $Tenant.customerId + $CustomerTenantName = $Tenant.displayName + + # ============================================================================ + # STEP 3: Get all active GDAP relationships for the customer tenant + # ============================================================================ + # GDAP relationships are created in the partner tenant and link to customer tenants. + # We query from the partner tenant perspective ($env:TenantID) and filter for: + # - status eq 'active': Only relationships that are currently active + # - customer/tenantId eq '$CustomerTenantId': Only relationships for this specific customer + # + # A tenant can have multiple GDAP relationships, each potentially with different roles. + # We need to check all of them to see which roles are available through which relationships. + # ============================================================================ + $BaseUri = 'https://graph.microsoft.com/beta/tenantRelationships/delegatedAdminRelationships' + $FilterValue = "status eq 'active' and customer/tenantId eq '$CustomerTenantId'" + $RelationshipsUri = "$($BaseUri)?`$filter=$($FilterValue)" + $Relationships = New-GraphGetRequest -uri $RelationshipsUri -tenantid $env:TenantID -NoAuthCheck $true + + # If no active relationships exist, return early with an informative message + if (-not $Relationships -or $Relationships.Count -eq 0) { + return [HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::OK + Body = @{ + tenantId = $CustomerTenantId + tenantName = $CustomerTenantName + relationships = @() + error = "No active GDAP relationships found for tenant $CustomerTenantName" + } + } + } + + # ============================================================================ + # STEP 4: Get the SAM user in the partner tenant + # ============================================================================ + # The UPN provided is for a user in the PARTNER tenant (not the customer tenant). + # This is the SAM (Service Account Manager) user whose access we're testing. + # The user must be in the partner tenant because GDAP groups are in the partner tenant. + # + # We try two methods: + # 1. Filter query: More efficient if it works + # 2. Direct lookup: Fallback if filter query doesn't return results + # ============================================================================ + $User = $null + try { + # Filter didn't work, try direct lookup by UPN (works if UPN is unique identifier) + $User = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/users/$UPN" -tenantid $env:TenantID -NoAuthCheck $true + } catch { + Write-LogMessage -Headers $Headers -API $APIName -message "Could not find user $UPN in partner tenant: $($_.Exception.Message)" -Sev 'Warning' + } + + # If user not found, return error + if (-not $User) { + return [HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::NotFound + Body = @{ + tenantId = $CustomerTenantId + tenantName = $CustomerTenantName + relationships = @() + error = "User $UPN not found in partner tenant" + } + } + } + + $UserId = $User.id + $UserDisplayName = $User.displayName + + # ============================================================================ + # STEP 5: Get user's transitive group memberships + # ============================================================================ + # This is a critical step. We use transitiveMemberOf which automatically handles + # nested groups at any depth. This means: + # - If user is directly in Group A, they're included + # - If user is in Group B, and Group B is in Group A, they're included + # - If user is in Group C, Group C is in Group B, Group B is in Group A, they're included + # - And so on for any depth of nesting + # + # We build a hashtable (dictionary) for O(1) lookup performance when checking + # if the user is a member of a specific group later. + # + # We filter for only groups (@odata.type = '#microsoft.graph.group') because + # transitiveMemberOf can also return role assignments, which we don't need here. + # ============================================================================ + $UserGroupMemberships = @{} + try { + # Use AsApp=true to get all memberships regardless of current user context + $PartnerUserMemberships = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/users/$UserId/transitiveMemberOf?`$select=id,displayName" -tenantid $env:TenantID -NoAuthCheck $true -AsApp $true -ErrorAction SilentlyContinue + if ($PartnerUserMemberships) { + foreach ($Membership in $PartnerUserMemberships) { + # Only include groups, not role assignments + if ($Membership.'@odata.type' -eq '#microsoft.graph.group') { + # Store in hashtable for fast lookup: key = groupId, value = membership object + $UserGroupMemberships[$Membership.id] = $Membership + } + } + } + } catch { + Write-LogMessage -Headers $Headers -API $APIName -message "Could not get user group memberships: $($_.Exception.Message)" -Sev 'Warning' + } + + # ============================================================================ + # STEP 6: Collect all relationships, groups, and build role mapping + # ============================================================================ + # We need to: + # 1. For each relationship, get all access assignments (mapped groups) + # 2. Collect all unique group IDs from all assignments + # 3. Batch fetch all groups at once (more efficient than individual calls) + # 4. For each group, check if user is a member and trace the path + # 5. Build a map from roleId -> list of relationships/groups that have that role + # + # This allows us to later check each of the 15 roles and see: + # - Which relationships have this role + # - Which groups in those relationships have this role + # - Whether the user is a member of any of those groups + # ============================================================================ + $AllRelationshipData = [System.Collections.Generic.List[object]]::new() + # This map will store: roleId -> list of {relationship, group} objects that have this role assigned + $RoleToRelationshipsMap = @{} + # This map will store: roleId -> list of relationships that have this role available (but may not be assigned) + $RoleToAvailableRelationshipsMap = @{} + + # ======================================================================== + # PHASE 1: Collect all access assignments and extract unique group IDs + # ======================================================================== + # First, we'll collect all access assignments from all relationships + # and extract the unique group IDs. Then we'll fetch all groups in batch. + # Also track which roles are available in each relationship. + # ======================================================================== + $AllAccessAssignments = [System.Collections.Generic.List[object]]::new() + $RelationshipAssignmentMap = @{} # Maps relationshipId -> list of assignments + + foreach ($Relationship in $Relationships) { + $RelationshipId = $Relationship.id + $RelationshipName = $Relationship.displayName + $RelationshipStatus = $Relationship.status + + # Track roles available in this relationship (from accessDetails.unifiedRoles) + if ($Relationship.accessDetails -and $Relationship.accessDetails.unifiedRoles) { + foreach ($Role in $Relationship.accessDetails.unifiedRoles) { + $RoleId = $Role.roleDefinitionId + if ($RoleId) { + if (-not $RoleToAvailableRelationshipsMap.ContainsKey($RoleId)) { + $RoleToAvailableRelationshipsMap[$RoleId] = [System.Collections.Generic.List[object]]::new() + } + $RoleToAvailableRelationshipsMap[$RoleId].Add([PSCustomObject]@{ + relationshipId = $RelationshipId + relationshipName = $RelationshipName + relationshipStatus = $RelationshipStatus + }) + } + } + } + + # Get access assignments (mapped security groups) for this relationship + $AccessAssignments = @() + try { + $AccessAssignments = New-GraphGetRequest -Uri "https://graph.microsoft.com/beta/tenantRelationships/delegatedAdminRelationships/$RelationshipId/accessAssignments" -tenantid $env:TenantID -NoAuthCheck $true + + # Handle case where response might be a single object instead of array + if ($AccessAssignments -and -not ($AccessAssignments -is [System.Array])) { + $AccessAssignments = @($AccessAssignments) + } + + Write-LogMessage -Headers $Headers -API $APIName -message "Retrieved $($AccessAssignments.Count) access assignments for relationship ${RelationshipName}" -Sev 'Debug' + + # Store assignments for this relationship + $RelationshipAssignmentMap[$RelationshipId] = @{ + Relationship = $Relationship + Assignments = $AccessAssignments + } + + # Add to master list + foreach ($Assignment in $AccessAssignments) { + $AllAccessAssignments.Add(@{ + RelationshipId = $RelationshipId + RelationshipName = $RelationshipName + RelationshipStatus = $RelationshipStatus + Assignment = $Assignment + }) + } + } catch { + Write-LogMessage -Headers $Headers -API $APIName -message "Could not get access assignments for relationship ${RelationshipName}: $($_.Exception.Message)" -Sev 'Warning' + } + } + + # Extract all unique group IDs from all assignments + $AllGroupIds = [System.Collections.Generic.HashSet[string]]::new() + foreach ($AssignmentData in $AllAccessAssignments) { + $Assignment = $AssignmentData.Assignment + $GroupId = $null + + # Extract group ID from assignment + if ($Assignment.accessContainer) { + $GroupId = $Assignment.accessContainer.accessContainerId + } elseif ($Assignment.value -and $Assignment.value.accessContainer) { + $GroupId = $Assignment.value.accessContainer.accessContainerId + } + + if ($GroupId -and -not [string]::IsNullOrWhiteSpace($GroupId)) { + [void]$AllGroupIds.Add($GroupId) + } + } + + Write-LogMessage -Headers $Headers -API $APIName -message "Found $($AllGroupIds.Count) unique groups across all relationships" -Sev 'Debug' + + # ======================================================================== + # PHASE 2: Fetch all groups at once and filter in memory + # ======================================================================== + # Fetch all groups in a single request, then create a lookup dictionary + # for fast in-memory filtering when processing assignments + # ======================================================================== + $GroupLookup = @{} # Maps groupId -> group object + + try { + # Fetch all groups at once (similar to Set-CIPPDBCacheGroups) + $AllGroups = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/groups?$top=999&$select=id,displayName' -tenantid $env:TenantID -NoAuthCheck $true -AsApp $true + + # Handle case where response might be a single object instead of array + if ($AllGroups -and -not ($AllGroups -is [System.Array])) { + $AllGroups = @($AllGroups) + } + + # Build lookup dictionary for O(1) access + foreach ($Group in $AllGroups) { + if ($Group.id) { + $GroupLookup[$Group.id] = $Group + } + } + + Write-LogMessage -Headers $Headers -API $APIName -message "Fetched $($AllGroups.Count) total groups, $($GroupLookup.Count) in lookup" -Sev 'Debug' + } catch { + Write-LogMessage -Headers $Headers -API $APIName -message "Could not fetch all groups: $($_.Exception.Message). Will use fallback for missing groups." -Sev 'Warning' + } + + # ======================================================================== + # PHASE 3: Process all assignments using the group lookup + # ======================================================================== + # Now that we have all groups, process each relationship's assignments + # ======================================================================== + foreach ($Relationship in $Relationships) { + $RelationshipId = $Relationship.id + $RelationshipName = $Relationship.displayName + $RelationshipStatus = $Relationship.status + + # Get assignments for this relationship + if (-not $RelationshipAssignmentMap.ContainsKey($RelationshipId)) { + # No assignments for this relationship, create empty groups list + $AllRelationshipData.Add([PSCustomObject]@{ + relationshipId = $RelationshipId + relationshipName = $RelationshipName + relationshipStatus = $RelationshipStatus + customerTenantId = $Relationship.customer.tenantId + customerTenantName = $Relationship.customer.displayName + groups = @() + }) + continue + } + + $AccessAssignments = $RelationshipAssignmentMap[$RelationshipId].Assignments + $RelationshipGroups = [System.Collections.Generic.List[object]]::new() + + Write-LogMessage -Headers $Headers -API $APIName -message "Processing $($AccessAssignments.Count) access assignments for relationship ${RelationshipName}" -Sev 'Debug' + + foreach ($Assignment in $AccessAssignments) { + # Extract the security group ID and roles from the assignment + $GroupId = $null + if ($Assignment.accessContainer) { + $GroupId = $Assignment.accessContainer.accessContainerId + } elseif ($Assignment.value -and $Assignment.value.accessContainer) { + $GroupId = $Assignment.value.accessContainer.accessContainerId + $Assignment = $Assignment.value + } else { + Write-LogMessage -Headers $Headers -API $APIName -message "Access assignment missing accessContainer: $($Assignment | ConvertTo-Json -Compress)" -Sev 'Warning' + continue + } + + if ([string]::IsNullOrWhiteSpace($GroupId)) { + Write-LogMessage -Headers $Headers -API $APIName -message "Access assignment has empty accessContainerId: $($Assignment | ConvertTo-Json -Compress)" -Sev 'Warning' + continue + } + + # Extract roles - handle both direct and nested structures + $Roles = $null + if ($Assignment.accessDetails -and $Assignment.accessDetails.unifiedRoles) { + $Roles = $Assignment.accessDetails.unifiedRoles + } elseif ($Assignment.unifiedRoles) { + $Roles = $Assignment.unifiedRoles + } + + if (-not $Roles -or $Roles.Count -eq 0) { + Write-LogMessage -Headers $Headers -API $APIName -message "Access assignment for group $GroupId has no roles assigned" -Sev 'Warning' + $Roles = @() + } + + # Get group from lookup (already fetched all groups at once) + $Group = $null + if ($GroupLookup.ContainsKey($GroupId)) { + $Group = $GroupLookup[$GroupId] + } else { + # Fallback: create minimal group object if not in lookup + # This can happen if the group was deleted or doesn't exist + $Group = [PSCustomObject]@{ + id = $GroupId + displayName = "Unknown Group ($GroupId)" + } + Write-LogMessage -Headers $Headers -API $APIName -message "Group $GroupId not found in lookup, using fallback" -Sev 'Warning' + } + + # Process the assignment even if group lookup failed - we still have the group ID and roles + if ($Group) { + # ================================================================ + # Check if user is a member of this group (direct or nested) + # ================================================================ + # We already have the user's transitive memberships, so we can + # quickly check if they're a member using our hashtable lookup. + # This is O(1) performance. + # ================================================================ + $IsMember = $UserGroupMemberships.ContainsKey($GroupId) + $MembershipPath = @() + $IsPathComplete = $false + + if ($IsMember) { + # ============================================================ + # User IS a member (either direct or nested) + # ============================================================ + # We know from transitiveMemberOf that the user is a member, + # but we need to determine if it's direct or nested, and if + # nested, try to find the path through intermediate groups. + # ============================================================ + $IsPathComplete = $true + # Start with assumption of direct membership + $MembershipPath = @( + @{ + groupId = $GroupId + groupName = $Group.displayName + membershipType = 'direct' + } + ) + + # ============================================================ + # Determine if membership is direct or nested + # ============================================================ + # We check the direct members of the group to see if the user + # is directly in it. If not, they must be nested (through + # another group that's a member of this group). + # ============================================================ + try { + # Get direct members of the target group + $DirectMembers = New-GraphGetRequest -Uri "https://graph.microsoft.com/beta/groups/$GroupId/members?`$select=id,displayName,userPrincipalName" -tenantid $env:TenantID -NoAuthCheck $true -AsApp $true + $IsDirectMember = $DirectMembers.value | Where-Object { $_.id -eq $UserId } + + if (-not $IsDirectMember) { + # ==================================================== + # User is nested - find the path through nested groups + # ==================================================== + # The user is not directly in this group, so they must + # be in a group that's a member of this group. + # We try to find which of the user's direct groups + # are members of this target group. + # ==================================================== + $MembershipPath[0].membershipType = 'nested' + + # Get groups the user is directly in (not nested) + $UserDirectGroups = New-GraphGetRequest -Uri "https://graph.microsoft.com/beta/users/$UserId/memberOf?`$select=id,displayName" -tenantid $env:TenantID -NoAuthCheck $true -AsApp $true -ErrorAction SilentlyContinue + if ($UserDirectGroups) { + $NestedGroups = @() + # Check each of the user's direct groups + foreach ($UserGroup in $UserDirectGroups) { + if ($UserGroup.'@odata.type' -eq '#microsoft.graph.group') { + try { + # Check if this user's direct group is a member of the target group + $GroupMembers = New-GraphGetRequest -Uri "https://graph.microsoft.com/beta/groups/$GroupId/members?`$select=id" -tenantid $env:TenantID -NoAuthCheck $true -AsApp $true -ErrorAction SilentlyContinue + if ($GroupMembers.value | Where-Object { $_.id -eq $UserGroup.id }) { + # Found it! This is the intermediate group + $NestedGroups += @{ + groupId = $UserGroup.id + groupName = $UserGroup.displayName + membershipType = 'direct' # User is direct member of this intermediate group + } + } + } catch { + # Skip if we can't check (permissions issue, etc.) + } + } + } + if ($NestedGroups.Count -gt 0) { + # Build the complete path: User → Intermediate Group → Target Group + # Add the target group to complete the path + $NestedGroups += @{ + groupId = $GroupId + groupName = $Group.displayName + membershipType = 'nested' # Intermediate group is nested in target group + } + $MembershipPath = $NestedGroups + } + } + } + # If IsDirectMember is true, membershipPath already shows 'direct' - we're done + } catch { + # If we can't check direct members (permissions, API error), assume nested + # This is a safe assumption - we know they're a member somehow + $MembershipPath[0].membershipType = 'nested' + } + } else { + # ============================================================ + # User is NOT a member of this group + # ============================================================ + # The group exists and has roles assigned, but the user isn't + # a member. This represents a broken path - the role is assigned + # but the user can't access it. + # ============================================================ + # Check if the group has any members at all (for diagnostic purposes) + $GroupHasMembers = $false + try { + $GroupMembers = New-GraphGetRequest -Uri "https://graph.microsoft.com/beta/groups/$GroupId/members?`$top=1" -tenantid $env:TenantID -NoAuthCheck $true -AsApp $true -ErrorAction SilentlyContinue + $GroupHasMembers = $GroupMembers.value.Count -gt 0 + } catch { + $GroupHasMembers = $false + } + + # Record the broken path + $MembershipPath = @( + @{ + groupId = $GroupId + groupName = $Group.displayName + membershipType = 'not_member' + groupHasMembers = $GroupHasMembers # Helps diagnose if group is empty + } + ) + } + + # ================================================================ + # Store group data for this relationship + # ================================================================ + # We store all the information about this group including: + # - Whether user is a member + # - The membership path (direct/nested/not_member) + # - All roles assigned to this group + # ================================================================ + $GroupData = [PSCustomObject]@{ + groupId = $GroupId + groupName = $Group.displayName + roles = $Roles # Array of role objects with roleDefinitionId + isMember = $IsMember + isPathComplete = $IsPathComplete # True if user can access this group + membershipPath = $MembershipPath # The path showing how user gets access (or why they don't) + assignmentStatus = $Assignment.status # Status of the access assignment + } + + $RelationshipGroups.Add($GroupData) + Write-LogMessage -Headers $Headers -API $APIName -message "Processed group $GroupDisplayName ($GroupId) with $($Roles.Count) roles for relationship ${RelationshipName}" -Sev 'Debug' + + # ================================================================ + # Map each role to this relationship/group combination + # ================================================================ + # This builds our role-to-relationships map that we'll use later + # to check each of the 15 GDAP roles. For each role, we'll know: + # - Which relationships have it + # - Which groups in those relationships have it + # - Whether the user is a member of those groups + # ================================================================ + if ($Roles -and $Roles.Count -gt 0) { + foreach ($Role in $Roles) { + # Handle both direct role objects and role objects with roleDefinitionId property + $RoleId = $null + if ($Role.roleDefinitionId) { + $RoleId = $Role.roleDefinitionId + } elseif ($Role -is [string]) { + $RoleId = $Role + } else { + Write-LogMessage -Headers $Headers -API $APIName -message "Role object missing roleDefinitionId: $($Role | ConvertTo-Json -Compress)" -Sev 'Warning' + continue + } + + if ([string]::IsNullOrWhiteSpace($RoleId)) { + Write-LogMessage -Headers $Headers -API $APIName -message "Role has empty roleDefinitionId for group $GroupId" -Sev 'Warning' + continue + } + + # Initialize list for this role if we haven't seen it before + if (-not $RoleToRelationshipsMap.ContainsKey($RoleId)) { + $RoleToRelationshipsMap[$RoleId] = [System.Collections.Generic.List[object]]::new() + } + # Add this relationship/group combination to the role's list + $RoleToRelationshipsMap[$RoleId].Add([PSCustomObject]@{ + relationshipId = $RelationshipId + relationshipName = $RelationshipName + relationshipStatus = $RelationshipStatus + groupId = $GroupId + groupName = $Group.displayName + groupData = $GroupData # Full group data including membership info + }) + } + } + } + } + + # Store relationship data for reference + $AllRelationshipData.Add([PSCustomObject]@{ + relationshipId = $RelationshipId + relationshipName = $RelationshipName + relationshipStatus = $RelationshipStatus + customerTenantId = $Relationship.customer.tenantId + customerTenantName = $Relationship.customer.displayName + groups = $RelationshipGroups + }) + } + + # ============================================================================ + # STEP 7: Trace each of the 15 GDAP roles to the user + # ============================================================================ + # This is the core logic - for each of the 15 standard GDAP roles, we: + # 1. Find all relationships/groups that have this role assigned + # 2. Check if the user is a member of any of those groups + # 3. Build the complete access path showing how the user gets the role (if they do) + # 4. Identify broken paths (role assigned but user not a member) + # + # The result is a role-centric view where each role shows: + # - Whether it's assigned in any relationship + # - Whether the user has access to it + # - All relationships/groups that have it + # - The complete path from role to user (if access exists) + # ============================================================================ + $RoleTraces = [System.Collections.Generic.List[object]]::new() + + # Check each of the 15 standard GDAP roles + foreach ($GDAPRole in $AllGDAPRoles) { + $RoleId = $GDAPRole.Id + $RoleName = $GDAPRole.Name + $RoleDescription = $GDAPRole.Description + + # ======================================================================== + # Find all relationships/groups that have this role assigned + # ======================================================================== + # We use the RoleToRelationshipsMap we built earlier. For each role, + # this map contains all relationship/group combinations that have + # this role assigned. + # ======================================================================== + $RelationshipsWithRole = @() + $UserHasAccess = $false + $AccessPaths = [System.Collections.Generic.List[object]]::new() + + if ($RoleToRelationshipsMap.ContainsKey($RoleId)) { + # This role exists in at least one relationship + foreach ($RoleRelationship in $RoleToRelationshipsMap[$RoleId]) { + $GroupData = $RoleRelationship.groupData + + # Record all relationships/groups that have this role (for reference) + $RelationshipsWithRole += [PSCustomObject]@{ + relationshipId = $RoleRelationship.relationshipId + relationshipName = $RoleRelationship.relationshipName + relationshipStatus = $RoleRelationship.relationshipStatus + groupId = $RoleRelationship.groupId + groupName = $RoleRelationship.groupName + isUserMember = $GroupData.isMember # Whether user is in this group + membershipPath = $GroupData.membershipPath # How user gets access (or why they don't) + } + + # ================================================================ + # Check if user has access through this group + # ================================================================ + # If the user is a member of this group (direct or nested), + # they have access to this role. We only need ONE path where + # the user is a member - if they're in any group with this role, + # they have access. + # ================================================================ + if ($GroupData.isMember) { + $UserHasAccess = $true + # Record the access path for this role + $AccessPaths.Add([PSCustomObject]@{ + relationshipId = $RoleRelationship.relationshipId + relationshipName = $RoleRelationship.relationshipName + groupId = $RoleRelationship.groupId + groupName = $RoleRelationship.groupName + membershipPath = $GroupData.membershipPath # Shows: User → Group (or User → Intermediate → Group) + }) + } + } + } + + # ======================================================================== + # Build the role trace object + # ======================================================================== + # This contains all information about this role: + # - roleExistsInRelationship: Role is available in at least one relationship (may not be assigned to any group) + # - isAssigned: Role is assigned to at least one group (must exist in relationship first) + # - isUserHasAccess: User is a member of at least one group with this role + # - relationshipsWithRole: All relationships/groups that have this role assigned + # - relationshipsWithRoleAvailable: All relationships where this role is available (but may not be assigned) + # - accessPaths: Only the paths where user actually has access (if any) + # ======================================================================== + $RoleExistsInRelationship = $RoleToAvailableRelationshipsMap.ContainsKey($RoleId) + $IsAssigned = $RelationshipsWithRole.Count -gt 0 + + # Get relationships where role is available but may not be assigned + $RelationshipsWithRoleAvailable = @() + if ($RoleToAvailableRelationshipsMap.ContainsKey($RoleId)) { + $RelationshipsWithRoleAvailable = $RoleToAvailableRelationshipsMap[$RoleId] + } + + $RoleTraces.Add([PSCustomObject]@{ + roleName = $RoleName + roleId = $RoleId + roleDescription = $RoleDescription + roleExistsInRelationship = $RoleExistsInRelationship # Role exists in at least one relationship + isAssigned = $IsAssigned # Role is assigned to at least one group + isUserHasAccess = $UserHasAccess + relationshipsWithRole = $RelationshipsWithRole # All places this role is assigned to groups + relationshipsWithRoleAvailable = $RelationshipsWithRoleAvailable # All relationships where role is available + accessPaths = $AccessPaths # Only paths where user has access + }) + } + + # ============================================================================ + # STEP 8: Build final result structure - role-centric view + # ============================================================================ + # The output is structured to be role-centric, making it easy to: + # - See which of the 15 roles the user has access to + # - See which roles are missing + # - See the complete path for each role (if access exists) + # - Identify broken paths (roles assigned but user not a member) + # + # The JSON structure is designed for diagram visualization, showing the + # complete chain: Role → Relationship → Group → User (with nested groups) + # ============================================================================ + + # Calculate summary statistics + $RolesWithAccess = ($RoleTraces | Where-Object { $_.isUserHasAccess -eq $true }).Count + $RolesAssignedButNoAccess = ($RoleTraces | Where-Object { ($_.isAssigned -eq $true) -and ($_.isUserHasAccess -eq $false) }).Count + $RolesInRelationshipButNotAssigned = ($RoleTraces | Where-Object { ($_.roleExistsInRelationship -eq $true) -and ($_.isAssigned -eq $false) }).Count + $RolesNotInAnyRelationship = ($RoleTraces | Where-Object { $_.roleExistsInRelationship -eq $false }).Count + + # Build the results object with role-centric view + $Results = [PSCustomObject]@{ + tenantId = $CustomerTenantId + tenantName = $CustomerTenantName + userUPN = $UPN + userId = $UserId + userDisplayName = $UserDisplayName + roles = $RoleTraces + relationships = $AllRelationshipData + summary = [PSCustomObject]@{ + totalRelationships = $Relationships.Count + totalRoles = $AllGDAPRoles.Count + rolesWithAccess = $RolesWithAccess + rolesAssignedButNoAccess = $RolesAssignedButNoAccess + rolesInRelationshipButNotAssigned = $RolesInRelationshipButNotAssigned + rolesNotInAnyRelationship = $RolesNotInAnyRelationship + } + } + + return [HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::OK + Body = $Results + } + + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -Headers $Headers -API $APIName -message "Failed to test GDAP access path: $($ErrorMessage.NormalizedError)" -Sev 'Error' -LogData $ErrorMessage + return [HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::InternalServerError + Body = @{ Error = $ErrorMessage.NormalizedError } + } + } +} From d2aecb2dad9490f614473d7afe4182a7b8f31fd1 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Fri, 20 Feb 2026 11:07:56 +0100 Subject: [PATCH 475/503] minor update to fix grantControls --- Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 b/Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 index 1196ee1f7488..76fba584da6d 100644 --- a/Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 +++ b/Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 @@ -93,7 +93,11 @@ function New-CIPPCAPolicy { #Remove context as it does not belong in the payload. try { if ($JSONobj.grantControls) { - $JSONobj.grantControls.PSObject.Properties.Remove('authenticationStrength@odata.context') + try { + $JSONobj.grantControls.PSObject.Properties.Remove('authenticationStrength@odata.context') + } catch { + #did not need to remove because didn't exist. + } } $JSONobj.templateId ? $JSONobj.PSObject.Properties.Remove('templateId') : $null if ($JSONobj.conditions.users.excludeGuestsOrExternalUsers.externalTenants.Members) { @@ -428,7 +432,7 @@ function New-CIPPCAPolicy { # Preserve any exclusion groups named "Vacation Exclusion - " 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 From 25d325459ec9843412d99d361dff3742ee9e2c79 Mon Sep 17 00:00:00 2001 From: rvdwegen Date: Fri, 20 Feb 2026 13:22:32 +0100 Subject: [PATCH 476/503] UploadApplication changes --- .../Public/Add-CIPPPackagedApplication.ps1 | 77 ++++++ .../Public/Add-CIPPW32ScriptApplication.ps1 | 186 +++++++++++++++ .../Public/Add-CIPPWin32LobAppContent.ps1 | 152 ++++++++++++ Modules/CIPPCore/Public/Add-CIPPWinGetApp.ps1 | 24 ++ .../Applications/Push-UploadApplication.ps1 | 225 +++++++++++------- 5 files changed, 575 insertions(+), 89 deletions(-) create mode 100644 Modules/CIPPCore/Public/Add-CIPPPackagedApplication.ps1 create mode 100644 Modules/CIPPCore/Public/Add-CIPPW32ScriptApplication.ps1 create mode 100644 Modules/CIPPCore/Public/Add-CIPPWin32LobAppContent.ps1 create mode 100644 Modules/CIPPCore/Public/Add-CIPPWinGetApp.ps1 diff --git a/Modules/CIPPCore/Public/Add-CIPPPackagedApplication.ps1 b/Modules/CIPPCore/Public/Add-CIPPPackagedApplication.ps1 new file mode 100644 index 000000000000..0730a11e16a1 --- /dev/null +++ b/Modules/CIPPCore/Public/Add-CIPPPackagedApplication.ps1 @@ -0,0 +1,77 @@ +function Add-CIPPPackagedApplication { + <# + .SYNOPSIS + Adds a packaged Win32Lob application to Intune. + + .DESCRIPTION + Handles creation of Win32Lob apps with intunewin files and uploads the content. + + .PARAMETER AppBody + Hashtable or PSCustomObject containing the app configuration. + + .PARAMETER TenantFilter + Tenant ID or domain name for the Graph API call. + + .PARAMETER AppType + Type of app: 'Choco' or 'MSPApp'. + + .PARAMETER FilePath + Path to the intunewin file. + + .PARAMETER FileName + Name of the file from XML metadata. + + .PARAMETER UnencryptedSize + Unencrypted size of the file from XML metadata. + + .PARAMETER EncryptionInfo + Hashtable containing encryption information from XML. + + .PARAMETER DisplayName + Display name of the app for logging. + + .PARAMETER APIName + API name for logging (optional). + + .PARAMETER Headers + Request headers for logging (optional). + + .EXAMPLE + $AppBody = @{ '@odata.type' = '#microsoft.graph.win32LobApp'; displayName = 'My App' } + $EncryptionInfo = @{ EncryptionKey = '...'; MacKey = '...'; ... } + Add-CIPPPackagedApplication -AppBody $AppBody -TenantFilter 'contoso.com' -AppType 'Choco' -FilePath 'app.intunewin' -FileName 'app.intunewin' -UnencryptedSize 1024000 -EncryptionInfo $EncryptionInfo + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [object]$AppBody, + + [Parameter(Mandatory = $true)] + [string]$TenantFilter, + + [Parameter(Mandatory = $true)] + [string]$FilePath, + + [Parameter(Mandatory = $true)] + [string]$FileName, + + [Parameter(Mandatory = $true)] + [int64]$UnencryptedSize, + + [Parameter(Mandatory = $true)] + [hashtable]$EncryptionInfo, + + [Parameter(Mandatory = $false)] + [string]$DisplayName + ) + + $BaseUri = 'https://graph.microsoft.com/beta/deviceAppManagement/mobileApps' + + # Create the Win32Lob app + $NewApp = New-GraphPostRequest -Uri $BaseUri -Body ($AppBody | ConvertTo-Json) -Type POST -tenantid $TenantFilter + + # Upload intunewin content + Add-CIPPWin32LobAppContent -AppId $NewApp.id -FilePath $FilePath -FileName $FileName -UnencryptedSize $UnencryptedSize -EncryptionInfo $EncryptionInfo -TenantFilter $TenantFilter -APIName $APIName -Headers $Headers + + return $NewApp +} diff --git a/Modules/CIPPCore/Public/Add-CIPPW32ScriptApplication.ps1 b/Modules/CIPPCore/Public/Add-CIPPW32ScriptApplication.ps1 new file mode 100644 index 000000000000..add8ade366d2 --- /dev/null +++ b/Modules/CIPPCore/Public/Add-CIPPW32ScriptApplication.ps1 @@ -0,0 +1,186 @@ +function Add-CIPPW32ScriptApplication { + <# + .SYNOPSIS + Adds a Win32 app with PowerShell script installer to Intune. + + .DESCRIPTION + Creates a Win32 app using the PowerShell script installer feature. + Uploads an intunewin file and PowerShell scripts via the scripts endpoint. + + .PARAMETER TenantFilter + Tenant ID or domain name for the Graph API call. + + .PARAMETER Properties + PSCustomObject containing all Win32 app properties: + - displayName (required): Display name of the app + - description: Description of the app + - publisher: Publisher name + - installScript (required): PowerShell install script content (plaintext) + - uninstallScript: PowerShell uninstall script content (plaintext) + - detectionScript: PowerShell detection script content (plaintext) + - runAsAccount: 'system' or 'user' (default: 'system') + - deviceRestartBehavior: 'allow', 'suppress', or 'force' (default: 'suppress') + - runAs32Bit: Boolean, run scripts as 32-bit on 64-bit clients (default: false) + - enforceSignatureCheck: Boolean, enforce script signature validation (default: false) + + .PARAMETER FilePath + Path to the intunewin file. + + .PARAMETER FileName + Name of the file from XML metadata. + + .PARAMETER UnencryptedSize + Unencrypted size of the file from XML metadata. + + .PARAMETER EncryptionInfo + Hashtable containing encryption information from XML. + + .EXAMPLE + $Properties = @{ + displayName = 'My Script App' + installScript = 'Write-Host "Installing..."' + } + $EncryptionInfo = @{ EncryptionKey = '...'; MacKey = '...'; ... } + Add-CIPPW32ScriptApplication -TenantFilter 'contoso.com' -Properties $Properties -FilePath 'app.intunewin' -FileName 'app.intunewin' -UnencryptedSize 1024000 -EncryptionInfo $EncryptionInfo + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter, + + [Parameter(Mandatory = $true)] + [PSCustomObject]$Properties, + + [Parameter(Mandatory = $true)] + [string]$FilePath, + + [Parameter(Mandatory = $true)] + [string]$FileName, + + [Parameter(Mandatory = $true)] + [int64]$UnencryptedSize, + + [Parameter(Mandatory = $true)] + [hashtable]$EncryptionInfo + ) + + # Build Win32 app body + $intuneBody = @{ + '@odata.type' = '#microsoft.graph.win32LobApp' + displayName = $Properties.displayName + description = $Properties.description + publisher = $Properties.publisher + fileName = $FileName + setupFilePath = 'N/A' + minimumSupportedWindowsRelease = '1607' + returnCodes = @( + @{ returnCode = 0; type = 'success' } + @{ returnCode = 1707; type = 'success' } + @{ returnCode = 3010; type = 'softReboot' } + @{ returnCode = 1641; type = 'hardReboot' } + @{ returnCode = 1618; type = 'retry' } + ) + } + + # Add install experience + $intuneBody.installExperience = @{ + '@odata.type' = 'microsoft.graph.win32LobAppInstallExperience' + runAsAccount = if ($Properties.runAsAccount) { $Properties.runAsAccount } else { 'system' } + deviceRestartBehavior = if ($Properties.deviceRestartBehavior) { $Properties.deviceRestartBehavior } else { 'suppress' } + maxRunTimeInMinutes = 60 + } + + # Create the app + $Baseuri = 'https://graph.microsoft.com/beta/deviceAppManagement/mobileApps' + $NewApp = New-GraphPostRequest -Uri $Baseuri -Body ($intuneBody | ConvertTo-Json -Depth 10) -Type POST -tenantid $TenantFilter + Start-Sleep -Milliseconds 200 + + # Upload intunewin file using shared helper + Add-CIPPWin32LobAppContent -AppId $NewApp.id -FilePath $FilePath -FileName $FileName -UnencryptedSize $UnencryptedSize -EncryptionInfo $EncryptionInfo -TenantFilter $TenantFilter + + # Upload PowerShell scripts via the scripts endpoint + $RunAs32Bit = if ($null -ne $Properties.runAs32Bit) { [bool]$Properties.runAs32Bit } else { $false } + $EnforceSignatureCheck = if ($null -ne $Properties.enforceSignatureCheck) { [bool]$Properties.enforceSignatureCheck } else { $false } + + $InstallScriptId = $null + $UninstallScriptId = $null + + if ($Properties.installScript) { + $InstallScriptContent = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($Properties.installScript)) + $InstallScriptBody = @{ + '@odata.type' = '#microsoft.graph.win32LobAppInstallPowerShellScript' + displayName = 'install.ps1' + enforceSignatureCheck = $EnforceSignatureCheck + runAs32Bit = $RunAs32Bit + content = $InstallScriptContent + } | ConvertTo-Json + + $InstallScriptResponse = New-GraphPostRequest -Uri "$Baseuri/$($NewApp.id)/microsoft.graph.win32LobApp/contentVersions/1/scripts" -Body $InstallScriptBody -Type POST -tenantid $TenantFilter + $InstallScriptId = $InstallScriptResponse.id + + # Wait for script to be committed + do { + $ScriptState = New-GraphGetRequest -Uri "$Baseuri/$($NewApp.id)/microsoft.graph.win32LobApp/contentVersions/1/scripts/$InstallScriptId" -tenantid $TenantFilter + if ($ScriptState.state -like '*fail*') { + throw "Failed to commit install script: $($ScriptState.state)" + } + Start-Sleep -Milliseconds 300 + } while ($ScriptState.state -eq 'commitPending') + } + + if ($Properties.uninstallScript) { + $UninstallScriptContent = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($Properties.uninstallScript)) + $UninstallScriptBody = @{ + '@odata.type' = '#microsoft.graph.win32LobAppUninstallPowerShellScript' + displayName = 'uninstall.ps1' + enforceSignatureCheck = $EnforceSignatureCheck + runAs32Bit = $RunAs32Bit + content = $UninstallScriptContent + } | ConvertTo-Json + + $UninstallScriptResponse = New-GraphPostRequest -Uri "$Baseuri/$($NewApp.id)/microsoft.graph.win32LobApp/contentVersions/1/scripts" -Body $UninstallScriptBody -Type POST -tenantid $TenantFilter + $UninstallScriptId = $UninstallScriptResponse.id + + # Wait for script to be committed + do { + $ScriptState = New-GraphGetRequest -Uri "$Baseuri/$($NewApp.id)/microsoft.graph.win32LobApp/contentVersions/1/scripts/$UninstallScriptId" -tenantid $TenantFilter + if ($ScriptState.state -like '*fail*') { + throw "Failed to commit uninstall script: $($ScriptState.state)" + } + Start-Sleep -Milliseconds 300 + } while ($ScriptState.state -eq 'commitPending') + } + + # Build final commit body with active script references + $CommitBody = @{ + '@odata.type' = '#microsoft.graph.win32LobApp' + committedContentVersion = '1' + } + + if ($InstallScriptId) { + $CommitBody['activeInstallScript'] = @{ targetId = $InstallScriptId } + } + + if ($UninstallScriptId) { + $CommitBody['activeUninstallScript'] = @{ targetId = $UninstallScriptId } + } + + # Add detection rules if provided + if ($Properties.detectionScript) { + $DetectionScriptContent = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($Properties.detectionScript)) + $CommitBody['detectionRules'] = @( + @{ + '@odata.type' = '#microsoft.graph.win32LobAppPowerShellScriptDetection' + scriptContent = $DetectionScriptContent + enforceSignatureCheck = $EnforceSignatureCheck + runAs32Bit = $RunAs32Bit + } + ) + } + + # Commit the app with script references + $null = New-GraphPostRequest -Uri "$Baseuri/$($NewApp.id)" -tenantid $TenantFilter -Body ($CommitBody | ConvertTo-Json -Depth 10) -Type PATCH + + return $NewApp + +} diff --git a/Modules/CIPPCore/Public/Add-CIPPWin32LobAppContent.ps1 b/Modules/CIPPCore/Public/Add-CIPPWin32LobAppContent.ps1 new file mode 100644 index 000000000000..1b238870e622 --- /dev/null +++ b/Modules/CIPPCore/Public/Add-CIPPWin32LobAppContent.ps1 @@ -0,0 +1,152 @@ +function Add-CIPPWin32LobAppContent { + <# + .SYNOPSIS + Uploads intunewin file content to a Win32Lob app in Intune. + + .DESCRIPTION + This function handles the complete process of uploading an intunewin file to a Win32Lob app: + 1. Creates a content version file entry + 2. Waits for Azure Storage URI + 3. Uploads the file to Azure Storage in chunks + 4. Commits the file with encryption info + 5. Finalizes the content version + + .PARAMETER AppId + The ID of the Win32Lob app to upload content to. + + .PARAMETER FilePath + Path to the intunewin file to upload. + + .PARAMETER FileName + Name of the file (from XML metadata). + + .PARAMETER UnencryptedSize + Unencrypted size of the file (from XML metadata). + + .PARAMETER EncryptionInfo + Hashtable containing encryption information from XML: + - EncryptionKey + - MacKey + - InitializationVector + - Mac + - ProfileIdentifier + - FileDigest + - FileDigestAlgorithm + + .PARAMETER TenantFilter + Tenant ID or domain name for the Graph API call. + + .PARAMETER APIName + API name for logging (optional). + + .PARAMETER Headers + Request headers for logging (optional). + + .EXAMPLE + $EncryptionInfo = @{ + EncryptionKey = '...' + MacKey = '...' + InitializationVector = '...' + Mac = '...' + ProfileIdentifier = 'ProfileVersion1' + FileDigest = '...' + FileDigestAlgorithm = 'SHA256' + } + Add-CIPPWin32LobAppContent -AppId '12345' -FilePath 'C:\app.intunewin' -FileName 'app.intunewin' -UnencryptedSize 1024000 -EncryptionInfo $EncryptionInfo -TenantFilter 'contoso.com' + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$AppId, + + [Parameter(Mandatory = $true)] + [string]$FilePath, + + [Parameter(Mandatory = $true)] + [string]$FileName, + + [Parameter(Mandatory = $true)] + [int64]$UnencryptedSize, + + [Parameter(Mandatory = $true)] + [hashtable]$EncryptionInfo, + + [Parameter(Mandatory = $true)] + [string]$TenantFilter, + + [Parameter(Mandatory = $false)] + [string]$APIName = 'AppUpload', + + [Parameter(Mandatory = $false)] + [hashtable]$Headers + ) + + $BaseUri = 'https://graph.microsoft.com/beta/deviceAppManagement/mobileApps' + $FileInfo = Get-Item $FilePath + + # Create content version file entry + $ContentBody = ConvertTo-Json @{ + name = $FileName + size = $UnencryptedSize + sizeEncrypted = [int64]$FileInfo.Length + } + + $ContentReq = New-GraphPostRequest -Uri "$BaseUri/$AppId/microsoft.graph.win32lobapp/contentVersions/1/files/" -Body $ContentBody -Type POST -tenantid $TenantFilter + + # Wait for Azure Storage URI + do { + $AzFileUri = New-GraphGetRequest -Uri "$BaseUri/$AppId/microsoft.graph.win32lobapp/contentVersions/1/files/$($ContentReq.id)" -tenantid $TenantFilter + if ($AzFileUri.uploadState -like '*fail*') { + throw "Failed to get Azure Storage URI. Upload state: $($AzFileUri.uploadState)" + } + Start-Sleep -Milliseconds 300 + } while ($null -eq $AzFileUri.AzureStorageUri) + + if ($Headers) { + Write-LogMessage -Headers $Headers -API $APIName -message "Uploading file to $($AzFileUri.azureStorageUri)" -Sev 'Info' -tenant $TenantFilter + } else { + Write-Host "Uploading file to $($AzFileUri.azureStorageUri)" + } + + # Upload file to Azure Storage in chunks + $chunkSizeInBytes = 4mb + [byte[]]$bytes = [System.IO.File]::ReadAllBytes($FileInfo.FullName) + $chunks = [Math]::Ceiling($bytes.Length / $chunkSizeInBytes) + $id = [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes($chunks.ToString('0000'))) + # For anyone that reads this, The maximum chunk size is 100MB for blob storage, so we can upload it as one part and just give it the single ID. Easy :) + $null = Invoke-RestMethod -Uri "$($AzFileUri.azureStorageUri)&comp=block&blockid=$id" -Method Put -Headers @{'x-ms-blob-type' = 'BlockBlob' } -InFile $FilePath -ContentType 'application/octet-stream' + $null = Invoke-RestMethod -Uri "$($AzFileUri.azureStorageUri)&comp=blocklist" -Method Put -Body "$id" -ContentType 'application/xml' + + # Commit the file with encryption info + $EncBody = @{ + fileEncryptionInfo = @{ + encryptionKey = $EncryptionInfo.EncryptionKey + macKey = $EncryptionInfo.MacKey + initializationVector = $EncryptionInfo.InitializationVector + mac = $EncryptionInfo.Mac + profileIdentifier = $EncryptionInfo.ProfileIdentifier + fileDigest = $EncryptionInfo.FileDigest + fileDigestAlgorithm = $EncryptionInfo.FileDigestAlgorithm + } + } | ConvertTo-Json + + $null = New-GraphPostRequest -Uri "$BaseUri/$AppId/microsoft.graph.win32lobapp/contentVersions/1/files/$($ContentReq.id)/commit" -Body $EncBody -Type POST -tenantid $TenantFilter + + # Wait for commit to complete + do { + $CommitStateReq = New-GraphGetRequest -Uri "$BaseUri/$AppId/microsoft.graph.win32lobapp/contentVersions/1/files/$($ContentReq.id)" -tenantid $TenantFilter + if ($CommitStateReq.uploadState -like '*fail*') { + $errorMsg = "Commit failed. Upload state: $($CommitStateReq.uploadState)" + if ($Headers) { + Write-LogMessage -Headers $Headers -API $APIName -message $errorMsg -Sev 'Warning' -tenant $TenantFilter + } + throw $errorMsg + } + Start-Sleep -Milliseconds 300 + } while ($CommitStateReq.uploadState -eq 'commitFilePending') + + # Finalize content version + $null = New-GraphPostRequest -Uri "$BaseUri/$AppId" -tenantid $TenantFilter -Body '{"@odata.type":"#microsoft.graph.win32lobapp","committedContentVersion":"1"}' -type PATCH + + return $true +} diff --git a/Modules/CIPPCore/Public/Add-CIPPWinGetApp.ps1 b/Modules/CIPPCore/Public/Add-CIPPWinGetApp.ps1 new file mode 100644 index 000000000000..df30b2db096c --- /dev/null +++ b/Modules/CIPPCore/Public/Add-CIPPWinGetApp.ps1 @@ -0,0 +1,24 @@ +function Add-CIPPWinGetApp { + <# + .SYNOPSIS + Creates a WinGet app in Intune. + + .DESCRIPTION + Creates a new WinGet app using the provided app body. + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [object]$AppBody, + + [Parameter(Mandatory = $true)] + [string]$TenantFilter + ) + + $BaseUri = 'https://graph.microsoft.com/beta/deviceAppManagement/mobileApps' + + # Create the WinGet app + $NewApp = New-GraphPostRequest -Uri $BaseUri -Body ($AppBody | ConvertTo-Json -Compress) -Type POST -tenantid $TenantFilter + + return $NewApp +} diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Applications/Push-UploadApplication.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Applications/Push-UploadApplication.ps1 index 5aae136f1ba4..e4c19265966b 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Applications/Push-UploadApplication.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Applications/Push-UploadApplication.ps1 @@ -14,118 +14,165 @@ function Push-UploadApplication { $CippRoot = (Get-Item $ModuleRoot).Parent.Parent Set-Location $CippRoot - $ChocoApp = (Get-CIPPAzDataTableEntity @Table -filter $Filter).JSON | ConvertFrom-Json - $intuneBody = $ChocoApp.IntuneBody - $tenants = if ($ChocoApp.tenant -eq 'AllTenants') { + $AppConfig = (Get-CIPPAzDataTableEntity @Table -filter $Filter).JSON | ConvertFrom-Json + $intuneBody = $AppConfig.IntuneBody + $tenants = if ($AppConfig.tenant -eq 'AllTenants') { (Get-Tenants -IncludeErrors).defaultDomainName } else { - $ChocoApp.tenant - } - if ($ChocoApp.type -eq 'MSPApp') { - [xml]$Intunexml = Get-Content "AddMSPApp\$($ChocoApp.MSPAppName).app.xml" - $intunewinFilesize = (Get-Item "AddMSPApp\$($ChocoApp.MSPAppName).intunewin") - $Infile = "AddMSPApp\$($ChocoApp.MSPAppName).intunewin" - } else { - [xml]$Intunexml = Get-Content 'AddChocoApp\Choco.App.xml' - $intunewinFilesize = (Get-Item 'AddChocoApp\IntunePackage.intunewin') - $Infile = "AddChocoApp\$($intunexml.ApplicationInfo.FileName)" - } - $assignTo = $ChocoApp.assignTo - $AssignToIntent = $ChocoApp.InstallationIntent - $Baseuri = 'https://graph.microsoft.com/beta/deviceAppManagement/mobileApps' - $ContentBody = ConvertTo-Json @{ - name = $intunexml.ApplicationInfo.FileName - size = [int64]$intunexml.ApplicationInfo.UnencryptedContentSize - sizeEncrypted = [int64]($intunewinFilesize).length + $AppConfig.tenant } + $assignTo = $AppConfig.assignTo + $AssignToIntent = $AppConfig.InstallationIntent $ClearRow = Get-CIPPAzDataTableEntity @Table -Filter $Filter - $RemoveCacheFile = if ($ChocoApp.tenant -ne 'AllTenants') { - Remove-AzDataTableEntity -Force @Table -Entity $clearRow + if ($AppConfig.tenant -ne 'AllTenants') { + $null = Remove-AzDataTableEntity -Force @Table -Entity $clearRow } else { $Table.Force = $true - Add-CIPPAzDataTableEntity @Table -Entity @{ - JSON = "$($ChocoApp | ConvertTo-Json)" + $null = Add-CIPPAzDataTableEntity @Table -Entity @{ + JSON = "$($AppConfig | ConvertTo-Json)" RowKey = "$($ClearRow.RowKey)" PartitionKey = 'apps' status = 'Deployed' } } - $EncBody = @{ - fileEncryptionInfo = @{ - encryptionKey = $intunexml.ApplicationInfo.EncryptionInfo.EncryptionKey - macKey = $intunexml.ApplicationInfo.EncryptionInfo.MacKey - initializationVector = $intunexml.ApplicationInfo.EncryptionInfo.InitializationVector - mac = $intunexml.ApplicationInfo.EncryptionInfo.Mac - profileIdentifier = $intunexml.ApplicationInfo.EncryptionInfo.ProfileIdentifier - fileDigest = $intunexml.ApplicationInfo.EncryptionInfo.FileDigest - fileDigestAlgorithm = $intunexml.ApplicationInfo.EncryptionInfo.FileDigestAlgorithm - } - } | ConvertTo-Json + # Determine app type (default to 'Choco' if not specified) + $AppType = if ($AppConfig.type) { $AppConfig.type } else { 'Choco' } + + # Load files based on app type (only for types that need them) + $Intunexml = $null + $Infile = $null + if ($AppType -eq 'MSPApp') { + [xml]$Intunexml = Get-Content "AddMSPApp\$($AppConfig.MSPAppName).app.xml" + $Infile = "AddMSPApp\$($AppConfig.MSPAppName).intunewin" + } elseif ($AppType -in @('Choco', 'Win32ScriptApp')) { + [xml]$Intunexml = Get-Content 'AddChocoApp\Choco.App.xml' + $Infile = "AddChocoApp\$($Intunexml.ApplicationInfo.FileName)" + } + + + $baseuri = 'https://graph.microsoft.com/beta/deviceAppManagement/mobileApps' foreach ($tenant in $tenants) { try { - $ApplicationList = New-GraphGetRequest -Uri $baseuri -tenantid $tenant | Where-Object { $_.DisplayName -eq $ChocoApp.Applicationname -and ($_.'@odata.type' -eq '#microsoft.graph.win32LobApp' -or $_.'@odata.type' -eq '#microsoft.graph.winGetApp') } + # Check if app already exists + $ApplicationList = New-GraphGetRequest -Uri $baseuri -tenantid $tenant | Where-Object { $_.DisplayName -eq $AppConfig.Applicationname -and ($_.'@odata.type' -eq '#microsoft.graph.win32LobApp' -or $_.'@odata.type' -eq '#microsoft.graph.winGetApp') } if ($ApplicationList.displayname.count -ge 1) { - Write-LogMessage -api 'AppUpload' -tenant $tenant -message "$($ChocoApp.Applicationname) exists. Skipping this application" -Sev 'Info' + Write-LogMessage -api 'AppUpload' -tenant $tenant -message "$($AppConfig.Applicationname) exists. Skipping this application" -Sev 'Info' continue } - if ($ChocoApp.type -eq 'WinGet') { - Write-Host 'Winget!' - Write-Host ($intuneBody | ConvertTo-Json -Compress) - $NewApp = New-GraphPostRequest -Uri $baseuri -Body ($intuneBody | ConvertTo-Json -Compress) -Type POST -tenantid $tenant - Start-Sleep -Milliseconds 200 - Write-LogMessage -api 'AppUpload' -tenant $tenant -message "$($ChocoApp.Applicationname) uploaded as WinGet app." -Sev 'Info' - if ($AssignTo -ne 'On') { - $intent = if ($AssignToIntent) { 'Uninstall' } else { 'Required' } - Set-CIPPAssignedApplication -ApplicationId $NewApp.Id -Intent $intent -TenantFilter $tenant -groupName "$AssignTo" -AppType 'WinGet' + + # Route to appropriate handler based on app type + $NewApp = $null + switch ($AppType) { + 'WinGet' { + $NewApp = Add-CIPPWinGetApp -AppBody $intuneBody -TenantFilter $tenant } - Write-LogMessage -api 'AppUpload' -tenant $tenant -message "$($ChocoApp.Applicationname) Successfully created" -Sev 'Info' - continue - } else { - $NewApp = New-GraphPostRequest -Uri $baseuri -Body ($intuneBody | ConvertTo-Json) -Type POST -tenantid $tenant + 'Choco' { + # Prepare encryption info from XML + $EncryptionInfo = @{ + EncryptionKey = $Intunexml.ApplicationInfo.EncryptionInfo.EncryptionKey + MacKey = $Intunexml.ApplicationInfo.EncryptionInfo.MacKey + InitializationVector = $Intunexml.ApplicationInfo.EncryptionInfo.InitializationVector + Mac = $Intunexml.ApplicationInfo.EncryptionInfo.Mac + ProfileIdentifier = $Intunexml.ApplicationInfo.EncryptionInfo.ProfileIdentifier + FileDigest = $Intunexml.ApplicationInfo.EncryptionInfo.FileDigest + FileDigestAlgorithm = $Intunexml.ApplicationInfo.EncryptionInfo.FileDigestAlgorithm + } - } - $ContentReq = New-GraphPostRequest -Uri "$($BaseURI)/$($NewApp.id)/microsoft.graph.win32lobapp/contentVersions/1/files/" -Body $ContentBody -Type POST -tenantid $tenant - do { - $AzFileUri = New-graphGetRequest -Uri "$($BaseURI)/$($NewApp.id)/microsoft.graph.win32lobapp/contentVersions/1/files/$($ContentReq.id)" -tenantid $tenant - if ($AZfileuri.uploadState -like '*fail*') { break } - Start-Sleep -Milliseconds 300 - } while ($AzFileUri.AzureStorageUri -eq $null) - Write-Host "Uploading file to $($AzFileUri.azureStorageUri)" - Write-Host "Complete AZ file uri data: $($AzFileUri | ConvertTo-Json -Depth 10)" - $chunkSizeInBytes = 4mb - [byte[]]$bytes = [System.IO.File]::ReadAllBytes($($intunewinFilesize.fullname)) - $chunks = [Math]::Ceiling($bytes.Length / $chunkSizeInBytes) - $id = [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes($chunks.ToString('0000'))) - #For anyone that reads this, The maximum chunk size is 100MB for blob storage, so we can upload it as one part and just give it the single ID. Easy :) - $Upload = Invoke-RestMethod -Uri "$($AzFileUri.azureStorageUri)&comp=block&blockid=$id" -Method Put -Headers @{'x-ms-blob-type' = 'BlockBlob' } -InFile $inFile -ContentType 'application/octet-stream' - Write-Host "Upload data: $($Upload | ConvertTo-Json -Depth 10)" - $ConfirmUpload = Invoke-RestMethod -Uri "$($AzFileUri.azureStorageUri)&comp=blocklist" -Method Put -Body "$id" -ContentType 'application/xml' - Write-Host "Confirm Upload data: $($ConfirmUpload | ConvertTo-Json -Depth 10)" - $CommitReq = New-graphPostRequest -Uri "$($BaseURI)/$($NewApp.id)/microsoft.graph.win32lobapp/contentVersions/1/files/$($ContentReq.id)/commit" -Body $EncBody -Type POST -tenantid $tenant - Write-Host "Commit Request: $($CommitReq | ConvertTo-Json -Depth 10)" - - do { - $CommitStateReq = New-graphGetRequest -Uri "$($BaseURI)/$($NewApp.id)/microsoft.graph.win32lobapp/contentVersions/1/files/$($ContentReq.id)" -tenantid $tenant - Write-Host "Commit State Request: $($CommitStateReq | ConvertTo-Json -Depth 10)" - if ($CommitStateReq.uploadState -like '*fail*') { - Write-LogMessage -api 'AppUpload' -tenant $tenant -message "$($ChocoApp.Applicationname) Commit failed. Please check if app uploaded succesful" -Sev 'Warning' - break + # Build parameters dynamically + $Params = @{ + AppBody = $intuneBody + TenantFilter = $tenant + FilePath = $Infile + FileName = $Intunexml.ApplicationInfo.FileName + UnencryptedSize = [int64]$Intunexml.ApplicationInfo.UnencryptedContentSize + EncryptionInfo = $EncryptionInfo + } + if ($AppConfig.Applicationname) { $Params.DisplayName = $AppConfig.Applicationname } + + $NewApp = Add-CIPPPackagedApplication @Params + } + 'MSPApp' { + # Prepare encryption info from XML + $EncryptionInfo = @{ + EncryptionKey = $Intunexml.ApplicationInfo.EncryptionInfo.EncryptionKey + MacKey = $Intunexml.ApplicationInfo.EncryptionInfo.MacKey + InitializationVector = $Intunexml.ApplicationInfo.EncryptionInfo.InitializationVector + Mac = $Intunexml.ApplicationInfo.EncryptionInfo.Mac + ProfileIdentifier = $Intunexml.ApplicationInfo.EncryptionInfo.ProfileIdentifier + FileDigest = $Intunexml.ApplicationInfo.EncryptionInfo.FileDigest + FileDigestAlgorithm = $Intunexml.ApplicationInfo.EncryptionInfo.FileDigestAlgorithm + } + + # Build parameters dynamically + $Params = @{ + AppBody = $intuneBody + TenantFilter = $tenant + FilePath = $Infile + FileName = $Intunexml.ApplicationInfo.FileName + UnencryptedSize = [int64]$Intunexml.ApplicationInfo.UnencryptedContentSize + EncryptionInfo = $EncryptionInfo + } + if ($AppConfig.Applicationname) { $Params.DisplayName = $AppConfig.Applicationname } + + $NewApp = Add-CIPPPackagedApplication @Params + } + 'Win32ScriptApp' { + # Prepare encryption info from XML + $EncryptionInfo = @{ + EncryptionKey = $Intunexml.ApplicationInfo.EncryptionInfo.EncryptionKey + MacKey = $Intunexml.ApplicationInfo.EncryptionInfo.MacKey + InitializationVector = $Intunexml.ApplicationInfo.EncryptionInfo.InitializationVector + Mac = $Intunexml.ApplicationInfo.EncryptionInfo.Mac + ProfileIdentifier = $Intunexml.ApplicationInfo.EncryptionInfo.ProfileIdentifier + FileDigest = $Intunexml.ApplicationInfo.EncryptionInfo.FileDigest + FileDigestAlgorithm = $Intunexml.ApplicationInfo.EncryptionInfo.FileDigestAlgorithm + } + + # Build properties dynamically + $Properties = @{ + displayName = $AppConfig.Applicationname + installScript = $AppConfig.installScript + } + + # A few of these are probably mandatory + if ($AppConfig.description) { $Properties['description'] = $AppConfig.description } + if ($AppConfig.publisher) { $Properties['publisher'] = $AppConfig.publisher } + if ($AppConfig.uninstallScript) { $Properties['uninstallScript'] = $AppConfig.uninstallScript } + if ($AppConfig.detectionScript) { $Properties['detectionScript'] = $AppConfig.detectionScript } + if ($AppConfig.runAsAccount) { $Properties['runAsAccount'] = $AppConfig.runAsAccount } + if ($AppConfig.deviceRestartBehavior) { $Properties['deviceRestartBehavior'] = $AppConfig.deviceRestartBehavior } + if ($null -ne $AppConfig.runAs32Bit) { $Properties['runAs32Bit'] = $AppConfig.runAs32Bit } + if ($null -ne $AppConfig.enforceSignatureCheck) { $Properties['enforceSignatureCheck'] = $AppConfig.enforceSignatureCheck } + + $NewApp = Add-CIPPW32ScriptApplication -TenantFilter $tenant -Properties ([PSCustomObject]$Properties) -FilePath $Infile -FileName $Intunexml.ApplicationInfo.FileName -UnencryptedSize ([int64]$Intunexml.ApplicationInfo.UnencryptedContentSize) -EncryptionInfo $EncryptionInfo + } + 'WinGetNew' { + # I think we don't need a separate WinGetNew type, just use WinGet? } - Start-Sleep -Milliseconds 300 - } while ($CommitStateReq.uploadState -eq 'commitFilePending') - $CommitFinalizeReq = New-graphPostRequest -Uri "$($BaseURI)/$($NewApp.id)" -tenantid $tenant -Body '{"@odata.type":"#microsoft.graph.win32lobapp","committedContentVersion":"1"}' -type PATCH - Write-Host "Commit Finalize Request: $($CommitFinalizeReq | ConvertTo-Json -Depth 10)" - Write-LogMessage -api 'AppUpload' -tenant $tenant -message "Added Application $($ChocoApp.Applicationname)" -Sev 'Info' - if ($AssignTo -ne 'On') { - $intent = if ($AssignToIntent) { 'Uninstall' } else { 'Required' } - Set-CIPPAssignedApplication -ApplicationId $NewApp.Id -Intent $intent -TenantFilter $tenant -groupName "$AssignTo" -AppType 'Win32Lob' + default { + throw "Unsupported app type: $($AppConfig.type)" + } + } + # Log success and assign app if requested + if ($NewApp) { + Write-LogMessage -api 'AppUpload' -tenant $tenant -message "$($AppConfig.Applicationname) Successfully created" -Sev 'Info' + + if ($assignTo -and $assignTo -ne 'On') { + $intent = if ($AssignToIntent) { 'Uninstall' } else { 'Required' } + $AppTypeForAssignment = switch ($AppType) { + 'WinGet' { 'WinGet' } + 'WinGetNew' { 'WinGet' } + default { 'Win32Lob' } + } + Start-Sleep -Milliseconds 200 + Set-CIPPAssignedApplication -ApplicationId $NewApp.Id -TenantFilter $tenant -groupName $assignTo -Intent $intent -AppType $AppTypeForAssignment -APIName 'AppUpload' + } } - Write-LogMessage -api 'AppUpload' -tenant $tenant -message 'Successfully added Application' -Sev 'Info' } catch { - "Failed to add Application for $($Tenant): $($_.Exception.Message)" - Write-LogMessage -api 'AppUpload' -tenant $tenant -message "Failed adding Application $($ChocoApp.Applicationname). Error: $($_.Exception.Message)" -LogData (Get-CippException -Exception $_) -Sev 'Error' + "Failed to add Application for $tenant : $($_.Exception.Message)" + Write-LogMessage -api 'AppUpload' -tenant $tenant -message "Failed adding Application $($AppConfig.Applicationname). Error: $($_.Exception.Message)" -LogData (Get-CippException -Exception $_) -Sev 'Error' continue } } From bf9fbc5354f31d910bfe66587a59a2659910b400 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Fri, 20 Feb 2026 13:22:59 +0100 Subject: [PATCH 477/503] remove text identitfier in case its multiple errors --- .../Identity/Administration/Users/Invoke-AddUser.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-AddUser.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-AddUser.ps1 index f7637cfa04d8..d00439f6d36c 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-AddUser.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-AddUser.ps1 @@ -78,7 +78,7 @@ function Invoke-AddUser { $ErrorMessage = $_.TargetObject.Results -join ' ' $ErrorMessage = [string]::IsNullOrWhiteSpace($ErrorMessage) ? $_.Exception.Message : $ErrorMessage $body = [pscustomobject] @{ - 'Results' = @("$ErrorMessage") + 'Results' = @($ErrorMessage) } $StatusCode = [HttpStatusCode]::InternalServerError } From b0c42a2326dccbe0aa0d5045b4fdc58e0b81aad3 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Fri, 20 Feb 2026 14:36:44 +0100 Subject: [PATCH 478/503] small changes to allow CIPPW32ScriptApplications. --- .../Public/Add-CIPPW32ScriptApplication.ps1 | 181 ++++++++++-------- 1 file changed, 105 insertions(+), 76 deletions(-) diff --git a/Modules/CIPPCore/Public/Add-CIPPW32ScriptApplication.ps1 b/Modules/CIPPCore/Public/Add-CIPPW32ScriptApplication.ps1 index add8ade366d2..370a70d5f762 100644 --- a/Modules/CIPPCore/Public/Add-CIPPW32ScriptApplication.ps1 +++ b/Modules/CIPPCore/Public/Add-CIPPW32ScriptApplication.ps1 @@ -1,11 +1,11 @@ function Add-CIPPW32ScriptApplication { <# .SYNOPSIS - Adds a Win32 app with PowerShell script installer to Intune. + Adds a Win32 app with PowerShell script installer to Intune using the standard Chocolatey package. .DESCRIPTION - Creates a Win32 app using the PowerShell script installer feature. - Uploads an intunewin file and PowerShell scripts via the scripts endpoint. + Creates a Win32 app that uses the standard Chocolatey intunewin package but with custom PowerShell scripts. + Always uploads the same Choco package, but uses user-provided scripts for install/uninstall commands. .PARAMETER TenantFilter Tenant ID or domain name for the Graph API call. @@ -17,31 +17,21 @@ function Add-CIPPW32ScriptApplication { - publisher: Publisher name - installScript (required): PowerShell install script content (plaintext) - uninstallScript: PowerShell uninstall script content (plaintext) - - detectionScript: PowerShell detection script content (plaintext) + - detectionPath (required): Full path to the file or folder to detect (e.g., 'C:\\Program Files\\MyApp') + - detectionFile: File name to detect (optional, for folder path detection) + - detectionType: 'exists', 'modifiedDate', 'createdDate', 'version', 'sizeInMB' (default: 'exists') + - check32BitOn64System: Boolean, check 32-bit registry/paths on 64-bit systems (default: false) - runAsAccount: 'system' or 'user' (default: 'system') - deviceRestartBehavior: 'allow', 'suppress', or 'force' (default: 'suppress') - - runAs32Bit: Boolean, run scripts as 32-bit on 64-bit clients (default: false) - - enforceSignatureCheck: Boolean, enforce script signature validation (default: false) - - .PARAMETER FilePath - Path to the intunewin file. - - .PARAMETER FileName - Name of the file from XML metadata. - - .PARAMETER UnencryptedSize - Unencrypted size of the file from XML metadata. - - .PARAMETER EncryptionInfo - Hashtable containing encryption information from XML. .EXAMPLE $Properties = @{ displayName = 'My Script App' installScript = 'Write-Host "Installing..."' + detectionPath = 'C:\\Program Files\\MyApp' + detectionFile = 'app.exe' } - $EncryptionInfo = @{ EncryptionKey = '...'; MacKey = '...'; ... } - Add-CIPPW32ScriptApplication -TenantFilter 'contoso.com' -Properties $Properties -FilePath 'app.intunewin' -FileName 'app.intunewin' -UnencryptedSize 1024000 -EncryptionInfo $EncryptionInfo + Add-CIPPW32ScriptApplication -TenantFilter 'contoso.com' -Properties $Properties #> [CmdletBinding()] param( @@ -49,59 +39,112 @@ function Add-CIPPW32ScriptApplication { [string]$TenantFilter, [Parameter(Mandatory = $true)] - [PSCustomObject]$Properties, + [PSCustomObject]$Properties + ) - [Parameter(Mandatory = $true)] - [string]$FilePath, + # Get the standard Chocolatey package location (relative to function app root) + $IntuneWinFile = 'AddChocoApp\IntunePackage.intunewin' + $ChocoXmlFile = 'AddChocoApp\Choco.App.xml' - [Parameter(Mandatory = $true)] - [string]$FileName, + if (-not (Test-Path $IntuneWinFile)) { + throw "Chocolatey IntunePackage.intunewin not found at: $IntuneWinFile (Current directory: $PWD)" + } - [Parameter(Mandatory = $true)] - [int64]$UnencryptedSize, + if (-not (Test-Path $ChocoXmlFile)) { + throw "Choco.App.xml not found at: $ChocoXmlFile (Current directory: $PWD)" + } - [Parameter(Mandatory = $true)] - [hashtable]$EncryptionInfo - ) + # Parse the Choco XML to get encryption info + [xml]$ChocoXml = Get-Content $ChocoXmlFile + $EncryptionInfo = @{ + EncryptionKey = $ChocoXml.ApplicationInfo.EncryptionInfo.EncryptionKey + MacKey = $ChocoXml.ApplicationInfo.EncryptionInfo.MacKey + InitializationVector = $ChocoXml.ApplicationInfo.EncryptionInfo.InitializationVector + Mac = $ChocoXml.ApplicationInfo.EncryptionInfo.Mac + ProfileIdentifier = $ChocoXml.ApplicationInfo.EncryptionInfo.ProfileIdentifier + FileDigest = $ChocoXml.ApplicationInfo.EncryptionInfo.FileDigest + FileDigestAlgorithm = $ChocoXml.ApplicationInfo.EncryptionInfo.FileDigestAlgorithm + } + + $FileName = $ChocoXml.ApplicationInfo.FileName + $UnencryptedSize = [int64]$ChocoXml.ApplicationInfo.UnencryptedContentSize + + # Build detection rules + if ($Properties.detectionPath) { + # Determine if this is a file or folder detection + $DetectionRule = @{ + '@odata.type' = '#microsoft.graph.win32LobAppFileSystemDetection' + check32BitOn64System = if ($null -ne $Properties.check32BitOn64System) { [bool]$Properties.check32BitOn64System } else { $false } + detectionType = if ($Properties.detectionType) { $Properties.detectionType } else { 'exists' } + } + + if ($Properties.detectionFile) { + # File detection (path + file) + $DetectionRule['path'] = $Properties.detectionPath + $DetectionRule['fileOrFolderName'] = $Properties.detectionFile + } else { + # Folder/File detection (full path) + # Split the path into directory and file/folder name + $PathItem = Split-Path $Properties.detectionPath -Leaf + $ParentPath = Split-Path $Properties.detectionPath -Parent + + if ([string]::IsNullOrEmpty($ParentPath)) { + throw "Invalid detection path: $($Properties.detectionPath). Must be a full path." + } - # Build Win32 app body - $intuneBody = @{ - '@odata.type' = '#microsoft.graph.win32LobApp' - displayName = $Properties.displayName - description = $Properties.description - publisher = $Properties.publisher - fileName = $FileName - setupFilePath = 'N/A' - minimumSupportedWindowsRelease = '1607' - returnCodes = @( + $DetectionRule['path'] = $ParentPath + $DetectionRule['fileOrFolderName'] = $PathItem + } + + $DetectionRules = @($DetectionRule) + } else { + # Default detection: Check for a marker file in ProgramData + $DetectionRules = @( + @{ + '@odata.type' = '#microsoft.graph.win32LobAppFileSystemDetection' + path = '%ProgramData%\CIPPApps' + fileOrFolderName = "$($Properties.displayName -replace '[^a-zA-Z0-9]', '_').txt" + check32BitOn64System = $false + detectionType = 'exists' + } + ) + } + + # Build the Win32 app body + $AppBody = @{ + '@odata.type' = '#microsoft.graph.win32LobApp' + displayName = $Properties.displayName + description = $Properties.description + publisher = if ($Properties.publisher) { $Properties.publisher } else { 'CIPP' } + fileName = $FileName + setupFilePath = 'N/A' + installCommandLine = 'powershell.exe -ExecutionPolicy Bypass -File install.ps1' + uninstallCommandLine = 'powershell.exe -ExecutionPolicy Bypass -File uninstall.ps1' + minimumSupportedWindowsRelease = '1607' + detectionRules = $DetectionRules + returnCodes = @( @{ returnCode = 0; type = 'success' } @{ returnCode = 1707; type = 'success' } @{ returnCode = 3010; type = 'softReboot' } @{ returnCode = 1641; type = 'hardReboot' } @{ returnCode = 1618; type = 'retry' } ) + installExperience = @{ + '@odata.type' = 'microsoft.graph.win32LobAppInstallExperience' + runAsAccount = if ($Properties.runAsAccount) { $Properties.runAsAccount } else { 'system' } + deviceRestartBehavior = if ($Properties.deviceRestartBehavior) { $Properties.deviceRestartBehavior } else { 'suppress' } + } } - # Add install experience - $intuneBody.installExperience = @{ - '@odata.type' = 'microsoft.graph.win32LobAppInstallExperience' - runAsAccount = if ($Properties.runAsAccount) { $Properties.runAsAccount } else { 'system' } - deviceRestartBehavior = if ($Properties.deviceRestartBehavior) { $Properties.deviceRestartBehavior } else { 'suppress' } - maxRunTimeInMinutes = 60 - } - - # Create the app + # Create the app first $Baseuri = 'https://graph.microsoft.com/beta/deviceAppManagement/mobileApps' - $NewApp = New-GraphPostRequest -Uri $Baseuri -Body ($intuneBody | ConvertTo-Json -Depth 10) -Type POST -tenantid $TenantFilter + $NewApp = New-GraphPostRequest -Uri $Baseuri -Body ($AppBody | ConvertTo-Json -Depth 10) -Type POST -tenantid $TenantFilter Start-Sleep -Milliseconds 200 - # Upload intunewin file using shared helper - Add-CIPPWin32LobAppContent -AppId $NewApp.id -FilePath $FilePath -FileName $FileName -UnencryptedSize $UnencryptedSize -EncryptionInfo $EncryptionInfo -TenantFilter $TenantFilter - - # Upload PowerShell scripts via the scripts endpoint - $RunAs32Bit = if ($null -ne $Properties.runAs32Bit) { [bool]$Properties.runAs32Bit } else { $false } - $EnforceSignatureCheck = if ($null -ne $Properties.enforceSignatureCheck) { [bool]$Properties.enforceSignatureCheck } else { $false } + # Upload the Chocolatey intunewin content + Add-CIPPWin32LobAppContent -AppId $NewApp.id -FilePath $IntuneWinFile -FileName $FileName -UnencryptedSize $UnencryptedSize -EncryptionInfo $EncryptionInfo -TenantFilter $TenantFilter + # Upload PowerShell scripts via the scripts endpoint (newer method) $InstallScriptId = $null $UninstallScriptId = $null @@ -110,8 +153,8 @@ function Add-CIPPW32ScriptApplication { $InstallScriptBody = @{ '@odata.type' = '#microsoft.graph.win32LobAppInstallPowerShellScript' displayName = 'install.ps1' - enforceSignatureCheck = $EnforceSignatureCheck - runAs32Bit = $RunAs32Bit + enforceSignatureCheck = $false + runAs32Bit = $false content = $InstallScriptContent } | ConvertTo-Json @@ -133,8 +176,8 @@ function Add-CIPPW32ScriptApplication { $UninstallScriptBody = @{ '@odata.type' = '#microsoft.graph.win32LobAppUninstallPowerShellScript' displayName = 'uninstall.ps1' - enforceSignatureCheck = $EnforceSignatureCheck - runAs32Bit = $RunAs32Bit + enforceSignatureCheck = $false + runAs32Bit = $false content = $UninstallScriptContent } | ConvertTo-Json @@ -153,8 +196,8 @@ function Add-CIPPW32ScriptApplication { # Build final commit body with active script references $CommitBody = @{ - '@odata.type' = '#microsoft.graph.win32LobApp' - committedContentVersion = '1' + '@odata.type' = '#microsoft.graph.win32LobApp' + committedContentVersion = '1' } if ($InstallScriptId) { @@ -165,22 +208,8 @@ function Add-CIPPW32ScriptApplication { $CommitBody['activeUninstallScript'] = @{ targetId = $UninstallScriptId } } - # Add detection rules if provided - if ($Properties.detectionScript) { - $DetectionScriptContent = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($Properties.detectionScript)) - $CommitBody['detectionRules'] = @( - @{ - '@odata.type' = '#microsoft.graph.win32LobAppPowerShellScriptDetection' - scriptContent = $DetectionScriptContent - enforceSignatureCheck = $EnforceSignatureCheck - runAs32Bit = $RunAs32Bit - } - ) - } - # Commit the app with script references $null = New-GraphPostRequest -Uri "$Baseuri/$($NewApp.id)" -tenantid $TenantFilter -Body ($CommitBody | ConvertTo-Json -Depth 10) -Type PATCH return $NewApp - } From ccc337b251e147aa0074a507ee146e5e14d8500c Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 20 Feb 2026 10:14:26 -0500 Subject: [PATCH 479/503] add default empty strings --- .../Standards/Invoke-CIPPStandardDeployMailContact.ps1 | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployMailContact.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployMailContact.ps1 index 0bc606b7adbf..e264f723eb36 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployMailContact.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployMailContact.ps1 @@ -105,15 +105,15 @@ function Invoke-CIPPStandardDeployMailContact { $ContactData = @{ DisplayName = $Settings.DisplayName ExternalEmailAddress = $Settings.ExternalEmailAddress - FirstName = $Settings.FirstName - LastName = $Settings.LastName + FirstName = $Settings.FirstName ?? '' + LastName = $Settings.LastName ?? '' } $CurrentValue = $ExistingContact | Select-Object DisplayName, ExternalEmailAddress, FirstName, LastName $currentValue = @{ DisplayName = $ExistingContact.displayName ExternalEmailAddress = ($ExistingContact.ExternalEmailAddress -replace 'SMTP:', '') - FirstName = $ExistingContact.firstName - LastName = $ExistingContact.lastName + FirstName = $ExistingContact.firstName ?? '' + LastName = $ExistingContact.lastName ?? '' } Add-CIPPBPAField -FieldName 'DeployMailContact' -FieldValue $ReportData -StoreAs json -Tenant $Tenant Set-CIPPStandardsCompareField -FieldName 'standards.DeployMailContact' -CurrentValue $CurrentValue -ExpectedValue $ReportData -Tenant $Tenant From ac2d462d2eb8d7c40c8031e696696e5fa9470255 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Fri, 20 Feb 2026 16:44:51 +0100 Subject: [PATCH 480/503] custom apps --- .../Public/Add-CIPPW32ScriptApplication.ps1 | 2 +- .../Applications/Push-UploadApplication.ps1 | 25 +- .../Applications/Invoke-AddWin32ScriptApp.ps1 | 80 ++ openapi.json | 940 ++++++------------ 4 files changed, 394 insertions(+), 653 deletions(-) create mode 100644 Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-AddWin32ScriptApp.ps1 diff --git a/Modules/CIPPCore/Public/Add-CIPPW32ScriptApplication.ps1 b/Modules/CIPPCore/Public/Add-CIPPW32ScriptApplication.ps1 index 370a70d5f762..5ef92c5a997a 100644 --- a/Modules/CIPPCore/Public/Add-CIPPW32ScriptApplication.ps1 +++ b/Modules/CIPPCore/Public/Add-CIPPW32ScriptApplication.ps1 @@ -54,7 +54,7 @@ function Add-CIPPW32ScriptApplication { throw "Choco.App.xml not found at: $ChocoXmlFile (Current directory: $PWD)" } - # Parse the Choco XML to get encryption info + # Parse the Choco XML to get encryption info. We need a wrapper around the application and this is a tiny intune file, perfect for our purpose. [xml]$ChocoXml = Get-Content $ChocoXmlFile $EncryptionInfo = @{ EncryptionKey = $ChocoXml.ApplicationInfo.EncryptionInfo.EncryptionKey diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Applications/Push-UploadApplication.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Applications/Push-UploadApplication.ps1 index e4c19265966b..b643716861a1 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Applications/Push-UploadApplication.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Applications/Push-UploadApplication.ps1 @@ -81,12 +81,12 @@ function Push-UploadApplication { # Build parameters dynamically $Params = @{ - AppBody = $intuneBody - TenantFilter = $tenant - FilePath = $Infile - FileName = $Intunexml.ApplicationInfo.FileName + AppBody = $intuneBody + TenantFilter = $tenant + FilePath = $Infile + FileName = $Intunexml.ApplicationInfo.FileName UnencryptedSize = [int64]$Intunexml.ApplicationInfo.UnencryptedContentSize - EncryptionInfo = $EncryptionInfo + EncryptionInfo = $EncryptionInfo } if ($AppConfig.Applicationname) { $Params.DisplayName = $AppConfig.Applicationname } @@ -106,12 +106,12 @@ function Push-UploadApplication { # Build parameters dynamically $Params = @{ - AppBody = $intuneBody - TenantFilter = $tenant - FilePath = $Infile - FileName = $Intunexml.ApplicationInfo.FileName + AppBody = $intuneBody + TenantFilter = $tenant + FilePath = $Infile + FileName = $Intunexml.ApplicationInfo.FileName UnencryptedSize = [int64]$Intunexml.ApplicationInfo.UnencryptedContentSize - EncryptionInfo = $EncryptionInfo + EncryptionInfo = $EncryptionInfo } if ($AppConfig.Applicationname) { $Params.DisplayName = $AppConfig.Applicationname } @@ -139,13 +139,14 @@ function Push-UploadApplication { if ($AppConfig.description) { $Properties['description'] = $AppConfig.description } if ($AppConfig.publisher) { $Properties['publisher'] = $AppConfig.publisher } if ($AppConfig.uninstallScript) { $Properties['uninstallScript'] = $AppConfig.uninstallScript } - if ($AppConfig.detectionScript) { $Properties['detectionScript'] = $AppConfig.detectionScript } + if ($AppConfig.detectionPath) { $Properties['detectionPath'] = $AppConfig.detectionPath } + if ($AppConfig.detectionFile) { $Properties['detectionFile'] = $AppConfig.detectionFile } if ($AppConfig.runAsAccount) { $Properties['runAsAccount'] = $AppConfig.runAsAccount } if ($AppConfig.deviceRestartBehavior) { $Properties['deviceRestartBehavior'] = $AppConfig.deviceRestartBehavior } if ($null -ne $AppConfig.runAs32Bit) { $Properties['runAs32Bit'] = $AppConfig.runAs32Bit } if ($null -ne $AppConfig.enforceSignatureCheck) { $Properties['enforceSignatureCheck'] = $AppConfig.enforceSignatureCheck } - $NewApp = Add-CIPPW32ScriptApplication -TenantFilter $tenant -Properties ([PSCustomObject]$Properties) -FilePath $Infile -FileName $Intunexml.ApplicationInfo.FileName -UnencryptedSize ([int64]$Intunexml.ApplicationInfo.UnencryptedContentSize) -EncryptionInfo $EncryptionInfo + $NewApp = Add-CIPPW32ScriptApplication -TenantFilter $tenant -Properties ([PSCustomObject]$Properties) } 'WinGetNew' { # I think we don't need a separate WinGetNew type, just use WinGet? diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-AddWin32ScriptApp.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-AddWin32ScriptApp.ps1 new file mode 100644 index 000000000000..bdb797cbdd88 --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-AddWin32ScriptApp.ps1 @@ -0,0 +1,80 @@ +function Invoke-AddWin32ScriptApp { + <# + .FUNCTIONALITY + Entrypoint,AnyTenant + .ROLE + Endpoint.Application.ReadWrite + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + + $Win32ScriptApp = $Request.Body + $AssignTo = $Win32ScriptApp.AssignTo -eq 'customGroup' ? $Win32ScriptApp.CustomGroup : $Win32ScriptApp.AssignTo + + # Validate required fields + if ([string]::IsNullOrEmpty($Win32ScriptApp.ApplicationName) -and [string]::IsNullOrEmpty($Win32ScriptApp.applicationName)) { + return ([HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::BadRequest + Body = @{ Results = @('Application name is required') } + }) + } + + if ([string]::IsNullOrEmpty($Win32ScriptApp.installScript)) { + return ([HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::BadRequest + Body = @{ Results = @('Install script is required') } + }) + } + + # Use whichever case was provided + $AppName = if ($Win32ScriptApp.ApplicationName) { $Win32ScriptApp.ApplicationName } else { $Win32ScriptApp.applicationName } + + $AllowedTenants = Test-CIPPAccess -Request $Request -TenantList + $Tenants = ($Request.Body.selectedTenants | Where-Object { $AllowedTenants -contains $_.customerId -or $AllowedTenants -contains 'AllTenants' }).defaultDomainName + + $Results = foreach ($Tenant in $Tenants) { + try { + $CompleteObject = [PSCustomObject]@{ + tenant = $Tenant + Applicationname = $AppName + assignTo = $AssignTo + InstallationIntent = $Win32ScriptApp.InstallationIntent + type = 'Win32ScriptApp' + description = $Win32ScriptApp.description + publisher = $Win32ScriptApp.publisher + installScript = $Win32ScriptApp.installScript + uninstallScript = $Win32ScriptApp.uninstallScript + detectionPath = $Win32ScriptApp.detectionPath + detectionFile = $Win32ScriptApp.detectionFile + runAsAccount = if ($Win32ScriptApp.InstallAsSystem) { 'system' } else { 'user' } + deviceRestartBehavior = if ($Win32ScriptApp.DisableRestart) { 'suppress' } else { 'allow' } + runAs32Bit = [bool]$Win32ScriptApp.runAs32Bit + enforceSignatureCheck = [bool]$Win32ScriptApp.enforceSignatureCheck + } | ConvertTo-Json -Depth 15 + + $Table = Get-CippTable -tablename 'apps' + $Table.Force = $true + Add-CIPPAzDataTableEntity @Table -Entity @{ + JSON = "$CompleteObject" + RowKey = "$((New-Guid).GUID)" + PartitionKey = 'apps' + status = 'Not Deployed yet' + } + "Successfully added Win32 Script App for $($Tenant) to queue." + Write-LogMessage -headers $Headers -API $APIName -tenant $Tenant -message "Successfully added Win32 Script App $AppName to queue" -Sev 'Info' + } catch { + Write-LogMessage -headers $Headers -API $APIName -tenant $Tenant -message "Failed to add Win32 Script App $AppName to queue. Error: $($_.Exception.Message)" -Sev 'Error' + "Failed to add Win32 Script App for $($Tenant) to queue: $($_.Exception.Message)" + } + } + + $body = [PSCustomObject]@{ 'Results' = $Results } + + return ([HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::OK + Body = $body + }) +} diff --git a/openapi.json b/openapi.json index 013d331ba238..194c1d67d499 100644 --- a/openapi.json +++ b/openapi.json @@ -13,9 +13,7 @@ "post": { "description": "AddGroup", "summary": "AddGroup", - "tags": [ - "POST" - ], + "tags": ["POST"], "parameters": [ { "required": true, @@ -53,9 +51,7 @@ "post": { "description": "AddChocoApp", "summary": "AddChocoApp", - "tags": [ - "POST" - ], + "tags": ["POST"], "parameters": [ { "required": true, @@ -89,13 +85,65 @@ } } }, + "/AddWin32ScriptApp": { + "post": { + "description": "AddWin32ScriptApp", + "summary": "AddWin32ScriptApp", + "tags": ["POST"], + "parameters": [ + { + "required": true, + "schema": { + "type": "string" + }, + "name": "ApplicationName", + "in": "body" + }, + { + "required": true, + "schema": { + "type": "string" + }, + "name": "installScript", + "in": "body" + }, + { + "required": true, + "schema": { + "type": "string" + }, + "name": "AssignTo", + "in": "body" + }, + { + "required": false, + "schema": { + "type": "string" + }, + "name": "InstallationIntent", + "in": "body" + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": {}, + "type": "object" + } + } + }, + "description": "Successful operation" + } + } + } + }, "/RemoveUser": { "get": { "description": "RemoveUser", "summary": "RemoveUser", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -133,9 +181,7 @@ "get": { "description": "ListTeams", "summary": "ListTeams", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -181,9 +227,7 @@ "get": { "description": "ExecGroupsDelete", "summary": "ExecGroupsDelete", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -237,9 +281,7 @@ "get": { "description": "ListRoles", "summary": "ListRoles", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -269,9 +311,7 @@ "get": { "description": "ListUserMailboxRules", "summary": "ListUserMailboxRules", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -317,9 +357,7 @@ "get": { "description": "ExecBECCheck", "summary": "ExecBECCheck", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -365,9 +403,7 @@ "get": { "description": "ListCalendarPermissions", "summary": "ListCalendarPermissions", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -405,9 +441,7 @@ "get": { "description": "ExecAddSPN", "summary": "ExecAddSPN", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -437,9 +471,7 @@ "get": { "description": "ListLicenses", "summary": "ListLicenses", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -469,9 +501,7 @@ "post": { "description": "AddCATemplate", "summary": "AddCATemplate", - "tags": [ - "POST" - ], + "tags": ["POST"], "parameters": [ { "required": true, @@ -509,9 +539,7 @@ "get": { "description": "ExecIncidentsList", "summary": "ExecIncidentsList", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -541,9 +569,7 @@ "post": { "description": "AddSharedMailbox", "summary": "AddSharedMailbox", - "tags": [ - "POST" - ], + "tags": ["POST"], "parameters": [ { "required": true, @@ -573,9 +599,7 @@ "get": { "description": "ListApps", "summary": "ListApps", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -605,9 +629,7 @@ "get": { "description": "ListSharepointSettings", "summary": "ListSharepointSettings", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -653,9 +675,7 @@ "get": { "description": "ExecSendOrgMessage", "summary": "ExecSendOrgMessage", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -717,9 +737,7 @@ "post": { "description": "AddSpamFilterTemplate", "summary": "AddSpamFilterTemplate", - "tags": [ - "POST" - ], + "tags": ["POST"], "parameters": [ { "required": true, @@ -757,9 +775,7 @@ "get": { "description": "ListGroupTemplates", "summary": "ListGroupTemplates", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -789,9 +805,7 @@ "get": { "description": "ListAutopilotconfig", "summary": "ListAutopilotconfig", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -837,9 +851,7 @@ "get": { "description": "ExecSetSecurityIncident", "summary": "ExecSetSecurityIncident", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -917,9 +929,7 @@ "get": { "description": "EditExConnector", "summary": "EditExConnector", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -973,9 +983,7 @@ "post": { "description": "AddUser", "summary": "AddUser", - "tags": [ - "POST" - ], + "tags": ["POST"], "parameters": [ { "required": true, @@ -1005,9 +1013,7 @@ "get": { "description": "ListUserPhoto", "summary": "ListUserPhoto", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -1045,9 +1051,7 @@ "get": { "description": "ListConditionalAccessPolicies", "summary": "ListConditionalAccessPolicies", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -1077,9 +1081,7 @@ "get": { "description": "ListBasicAuth", "summary": "ListBasicAuth", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -1109,9 +1111,7 @@ "get": { "description": "RemoveSpamfilterTemplate", "summary": "RemoveSpamfilterTemplate", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -1141,9 +1141,7 @@ "post": { "description": "ExecGDAPInvite", "summary": "ExecGDAPInvite", - "tags": [ - "POST" - ], + "tags": ["POST"], "parameters": [ { "required": true, @@ -1173,9 +1171,7 @@ "get": { "description": "ListUserSigninLogs", "summary": "ListUserSigninLogs", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -1213,9 +1209,7 @@ "get": { "description": "EditCAPolicy", "summary": "EditCAPolicy", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -1261,9 +1255,7 @@ "get": { "description": "ListDomainHealth", "summary": "ListDomainHealth", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -1333,9 +1325,7 @@ "get": { "description": "ExecUniversalSearch", "summary": "ExecUniversalSearch", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -1365,9 +1355,7 @@ "get": { "description": "ListMailboxCAS", "summary": "ListMailboxCAS", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -1397,9 +1385,7 @@ "get": { "description": "RemoveExConnectorTemplate", "summary": "RemoveExConnectorTemplate", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -1429,9 +1415,7 @@ "get": { "description": "ListUserCounts", "summary": "ListUserCounts", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -1461,9 +1445,7 @@ "get": { "description": "ExecGetLocalAdminPassword", "summary": "ExecGetLocalAdminPassword", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -1501,9 +1483,7 @@ "get": { "description": "ListOrg", "summary": "ListOrg", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -1533,9 +1513,7 @@ "post": { "description": "ExecExcludeLicenses", "summary": "ExecExcludeLicenses", - "tags": [ - "POST" - ], + "tags": ["POST"], "parameters": [ { "required": true, @@ -1603,9 +1581,7 @@ "get": { "description": "ExecExcludeLicenses", "summary": "ExecExcludeLicenses", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -1675,9 +1651,7 @@ "get": { "description": "ExecDeleteGDAPRelationship", "summary": "ExecDeleteGDAPRelationship", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -1707,9 +1681,7 @@ "post": { "description": "AddMSPApp", "summary": "AddMSPApp", - "tags": [ - "POST" - ], + "tags": ["POST"], "parameters": [ { "required": true, @@ -1747,9 +1719,7 @@ "post": { "description": "EditPolicy", "summary": "EditPolicy", - "tags": [ - "POST" - ], + "tags": ["POST"], "parameters": [ { "required": true, @@ -1811,9 +1781,7 @@ "post": { "description": "ExecDeviceAction", "summary": "ExecDeviceAction", - "tags": [ - "POST" - ], + "tags": ["POST"], "parameters": [ { "required": true, @@ -1865,9 +1833,7 @@ "get": { "description": "ExecDeviceAction", "summary": "ExecDeviceAction", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -1921,9 +1887,7 @@ "post": { "description": "AddWinGetApp", "summary": "AddWinGetApp", - "tags": [ - "POST" - ], + "tags": ["POST"], "parameters": [ { "required": true, @@ -1961,9 +1925,7 @@ "get": { "description": "RemovePolicy", "summary": "RemovePolicy", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -2009,9 +1971,7 @@ "post": { "description": "AddExConnector", "summary": "AddExConnector", - "tags": [ - "POST" - ], + "tags": ["POST"], "parameters": [ { "required": true, @@ -2041,9 +2001,7 @@ "get": { "description": "ListTeamsVoice", "summary": "ListTeamsVoice", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -2073,9 +2031,7 @@ "get": { "description": "ListTeamsActivity", "summary": "ListTeamsActivity", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -2113,9 +2069,7 @@ "get": { "description": "ListSpamFilterTemplates", "summary": "ListSpamFilterTemplates", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -2145,9 +2099,7 @@ "get": { "description": "ExecCopyForSent", "summary": "ExecCopyForSent", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -2193,9 +2145,7 @@ "get": { "description": "ListAzureADConnectStatus", "summary": "ListAzureADConnectStatus", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -2233,9 +2183,7 @@ "get": { "description": "ExecEnableArchive", "summary": "ExecEnableArchive", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -2273,9 +2221,7 @@ "get": { "description": "ExecGetRecoveryKey", "summary": "ExecGetRecoveryKey", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -2313,9 +2259,7 @@ "get": { "description": "ListSharedMailboxStatistics", "summary": "ListSharedMailboxStatistics", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -2345,9 +2289,7 @@ "post": { "description": "ListPotentialApps", "summary": "ListPotentialApps", - "tags": [ - "POST" - ], + "tags": ["POST"], "parameters": [ { "required": true, @@ -2385,9 +2327,7 @@ "get": { "description": "ExecCPVPermissions", "summary": "ExecCPVPermissions", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -2417,9 +2357,7 @@ "get": { "description": "ListSharepointQuota", "summary": "ListSharepointQuota", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -2449,9 +2387,7 @@ "get": { "description": "ListDefenderTVM", "summary": "ListDefenderTVM", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -2481,9 +2417,7 @@ "post": { "description": "ExecEditMailboxPermissions", "summary": "ExecEditMailboxPermissions", - "tags": [ - "POST" - ], + "tags": ["POST"], "parameters": [ { "required": true, @@ -2569,9 +2503,7 @@ "post": { "description": "AddOfficeApp", "summary": "AddOfficeApp", - "tags": [ - "POST" - ], + "tags": ["POST"], "parameters": [ { "required": true, @@ -2657,9 +2589,7 @@ "get": { "description": "EditSpamFilter", "summary": "EditSpamFilter", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -2705,9 +2635,7 @@ "get": { "description": "ListSignIns", "summary": "ListSignIns", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -2753,9 +2681,7 @@ "get": { "description": "ExecDnsConfig", "summary": "ExecDnsConfig", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -2801,9 +2727,7 @@ "post": { "description": "ExecEmailForward", "summary": "ExecEmailForward", - "tags": [ - "POST" - ], + "tags": ["POST"], "parameters": [ { "required": true, @@ -2873,9 +2797,7 @@ "post": { "description": "ExecBECRemediate", "summary": "ExecBECRemediate", - "tags": [ - "POST" - ], + "tags": ["POST"], "parameters": [ { "required": true, @@ -2913,9 +2835,7 @@ "get": { "description": "ListPartnerRelationships", "summary": "ListPartnerRelationships", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -2945,9 +2865,7 @@ "post": { "description": "ListAppsRepository", "summary": "ListAppsRepository", - "tags": [ - "POST" - ], + "tags": ["POST"], "parameters": [ { "required": true, @@ -2985,9 +2903,7 @@ "get": { "description": "ExecClrImmId", "summary": "ExecClrImmId", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -3025,9 +2941,7 @@ "post": { "description": "AddGroupTemplate", "summary": "AddGroupTemplate", - "tags": [ - "POST" - ], + "tags": ["POST"], "parameters": [ { "required": true, @@ -3097,9 +3011,7 @@ "post": { "description": "Standards_IntuneTemplate", "summary": "Standards_IntuneTemplate", - "tags": [ - "POST" - ], + "tags": ["POST"], "parameters": [ { "required": true, @@ -3161,9 +3073,7 @@ "get": { "description": "ListIntuneTemplates", "summary": "ListIntuneTemplates", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -3193,9 +3103,7 @@ "get": { "description": "ExecResetPass", "summary": "ExecResetPass", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -3249,9 +3157,7 @@ "get": { "description": "ExecAlertsList", "summary": "ExecAlertsList", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -3281,9 +3187,7 @@ "get": { "description": "ExecQuarantineManagement", "summary": "ExecQuarantineManagement", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -3337,9 +3241,7 @@ "get": { "description": "ExecRestoreDeleted", "summary": "ExecRestoreDeleted", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -3377,9 +3279,7 @@ "get": { "description": "ListUserGroups", "summary": "ListUserGroups", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -3417,9 +3317,7 @@ "get": { "description": "RemoveStandard", "summary": "RemoveStandard", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -3449,9 +3347,7 @@ "get": { "description": "ListUserConditionalAccessPolicies", "summary": "ListUserConditionalAccessPolicies", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -3489,9 +3385,7 @@ "get": { "description": "ListCAtemplates", "summary": "ListCAtemplates", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -3521,9 +3415,7 @@ "get": { "description": "ListContacts", "summary": "ListContacts", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -3644,9 +3536,7 @@ "get": { "description": "ListContactPermissions - Retrieves contact folder permissions for a specified user", "summary": "ListContactPermissions", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -3729,20 +3619,14 @@ "post": { "description": "ExecModifyContactPerms - Modifies contact folder permissions for a specified user", "summary": "ExecModifyContactPerms", - "tags": [ - "POST" - ], + "tags": ["POST"], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", - "required": [ - "userID", - "tenantFilter", - "permissions" - ], + "required": ["userID", "tenantFilter", "permissions"], "properties": { "userID": { "type": "string", @@ -3801,11 +3685,7 @@ "default": false } }, - "required": [ - "PermissionLevel", - "Modification", - "UserID" - ] + "required": ["PermissionLevel", "Modification", "UserID"] } } } @@ -3897,9 +3777,7 @@ "get": { "description": "ListMailboxRules", "summary": "ListMailboxRules", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -3929,9 +3807,7 @@ "get": { "description": "ListSites", "summary": "ListSites", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -3977,9 +3853,7 @@ "post": { "description": "ExecSetOoO", "summary": "ExecSetOoO", - "tags": [ - "POST" - ], + "tags": ["POST"], "parameters": [ { "required": true, @@ -4033,9 +3907,7 @@ "post": { "description": "AddExConnectorTemplate", "summary": "AddExConnectorTemplate", - "tags": [ - "POST" - ], + "tags": ["POST"], "parameters": [ { "required": true, @@ -4073,9 +3945,7 @@ "post": { "description": "ExecOffboardUser", "summary": "ExecOffboardUser", - "tags": [ - "POST" - ], + "tags": ["POST"], "parameters": [ { "required": true, @@ -4153,9 +4023,7 @@ "get": { "description": "ListTenantDetails", "summary": "ListTenantDetails", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -4185,9 +4053,7 @@ "get": { "description": "ExecConverttoSharedMailbox", "summary": "ExecConverttoSharedMailbox", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -4233,9 +4099,7 @@ "get": { "description": "ExecGraphRequest", "summary": "ExecGraphRequest", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -4281,9 +4145,7 @@ "get": { "description": "ListDefenderState", "summary": "ListDefenderState", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -4313,9 +4175,7 @@ "get": { "description": "ListMailQuarantine", "summary": "ListMailQuarantine", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -4345,9 +4205,7 @@ "get": { "description": "ListIntunePolicy", "summary": "ListIntunePolicy", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -4393,9 +4251,7 @@ "get": { "description": "ExecRevokeSessions", "summary": "ExecRevokeSessions", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -4433,9 +4289,7 @@ "get": { "description": "ListmailboxPermissions", "summary": "ListmailboxPermissions", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -4473,9 +4327,7 @@ "post": { "description": "ExecExtensionsConfig", "summary": "ExecExtensionsConfig", - "tags": [ - "POST" - ], + "tags": ["POST"], "parameters": [ { "required": true, @@ -4505,9 +4357,7 @@ "get": { "description": "ListDevices", "summary": "ListDevices", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -4537,9 +4387,7 @@ "get": { "description": "RemoveCATemplate", "summary": "RemoveCATemplate", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -4569,9 +4417,7 @@ "get": { "description": "RemoveQueuedApp", "summary": "RemoveQueuedApp", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -4601,9 +4447,7 @@ "get": { "description": "DomainAnalyser_List", "summary": "DomainAnalyser_List", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -4633,9 +4477,7 @@ "post": { "description": "AddNamedLocation", "summary": "AddNamedLocation", - "tags": [ - "POST" - ], + "tags": ["POST"], "parameters": [ { "required": true, @@ -4713,9 +4555,7 @@ "get": { "description": "ListTransportRulesTemplates", "summary": "ListTransportRulesTemplates", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -4745,9 +4585,7 @@ "get": { "description": "ExecEditCalendarPermissions", "summary": "ExecEditCalendarPermissions", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -4817,9 +4655,7 @@ "get": { "description": "ExecDisableUser", "summary": "ExecDisableUser", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -4865,9 +4701,7 @@ "post": { "description": "AddTransportRule", "summary": "AddTransportRule", - "tags": [ - "POST" - ], + "tags": ["POST"], "parameters": [ { "required": true, @@ -4897,9 +4731,7 @@ "get": { "description": "ListTenants", "summary": "ListTenants", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -4953,9 +4785,7 @@ "get": { "description": "ExecResetMFA", "summary": "ExecResetMFA", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -4993,9 +4823,7 @@ "get": { "description": "ListIntuneIntents", "summary": "ListIntuneIntents", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -5025,9 +4853,7 @@ "get": { "description": "ListStandards", "summary": "ListStandards", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -5057,9 +4883,7 @@ "get": { "description": "ListDeletedItems", "summary": "ListDeletedItems", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -5089,9 +4913,7 @@ "get": { "description": "ExecGroupsHideFromGAL", "summary": "ExecGroupsHideFromGAL", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -5145,9 +4967,7 @@ "get": { "description": "ExecSendPush", "summary": "ExecSendPush", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -5185,9 +5005,7 @@ "post": { "description": "ExecExtensionMapping", "summary": "ExecExtensionMapping", - "tags": [ - "POST" - ], + "tags": ["POST"], "parameters": [ { "required": true, @@ -5231,9 +5049,7 @@ "get": { "description": "ExecExtensionMapping", "summary": "ExecExtensionMapping", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -5279,9 +5095,7 @@ "get": { "description": "ListAppStatus", "summary": "ListAppStatus", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -5319,9 +5133,7 @@ "get": { "description": "GetVersion", "summary": "GetVersion", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -5351,9 +5163,7 @@ "get": { "description": "ExecMailboxMobileDevices", "summary": "ExecMailboxMobileDevices", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -5415,9 +5225,7 @@ "get": { "description": "RemoveApp", "summary": "RemoveApp", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -5455,9 +5263,7 @@ "post": { "description": "ExecPasswordConfig", "summary": "ExecPasswordConfig", - "tags": [ - "POST" - ], + "tags": ["POST"], "parameters": [ { "required": true, @@ -5493,9 +5299,7 @@ "get": { "description": "ExecPasswordConfig", "summary": "ExecPasswordConfig", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -5533,9 +5337,7 @@ "get": { "description": "ExecHideFromGAL", "summary": "ExecHideFromGAL", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -5581,9 +5383,7 @@ "post": { "description": "EditUser", "summary": "EditUser", - "tags": [ - "POST" - ], + "tags": ["POST"], "parameters": [ { "required": true, @@ -5613,9 +5413,7 @@ "get": { "description": "ListOAuthApps", "summary": "ListOAuthApps", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -5645,9 +5443,7 @@ "get": { "description": "ListDeviceDetails", "summary": "ListDeviceDetails", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -5701,9 +5497,7 @@ "get": { "description": "ListLogs", "summary": "ListLogs", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -5765,9 +5559,7 @@ "get": { "description": "ListAllTenantDeviceCompliance", "summary": "ListAllTenantDeviceCompliance", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -5797,9 +5589,7 @@ "get": { "description": "ListUsers", "summary": "ListUsers", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -5853,9 +5643,7 @@ "get": { "description": "ListMessageTrace", "summary": "ListMessageTrace", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -5925,9 +5713,7 @@ "get": { "description": "ListPhishPolicies", "summary": "ListPhishPolicies", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -5957,9 +5743,7 @@ "get": { "description": "RemoveSpamfilter", "summary": "RemoveSpamfilter", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -5997,9 +5781,7 @@ "post": { "description": "ExecNotificationConfig", "summary": "ExecNotificationConfig", - "tags": [ - "POST" - ], + "tags": ["POST"], "parameters": [ { "required": true, @@ -6069,9 +5851,7 @@ "post": { "description": "ExecSAMSetup", "summary": "ExecSAMSetup", - "tags": [ - "POST" - ], + "tags": ["POST"], "parameters": [ { "required": true, @@ -6195,9 +5975,7 @@ "get": { "description": "ExecSAMSetup", "summary": "ExecSAMSetup", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -6323,9 +6101,7 @@ "get": { "description": "ListUserMailboxDetails", "summary": "ListUserMailboxDetails", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -6363,9 +6139,7 @@ "get": { "description": "ListExConnectorTemplates", "summary": "ListExConnectorTemplates", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -6395,9 +6169,7 @@ "post": { "description": "AddDefenderDeployment", "summary": "AddDefenderDeployment", - "tags": [ - "POST" - ], + "tags": ["POST"], "parameters": [ { "required": true, @@ -6459,9 +6231,7 @@ "get": { "description": "ListGDAPInvite", "summary": "ListGDAPInvite", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -6491,9 +6261,7 @@ "post": { "description": "AddIntuneTemplate", "summary": "AddIntuneTemplate", - "tags": [ - "POST" - ], + "tags": ["POST"], "parameters": [ { "required": true, @@ -6569,9 +6337,7 @@ "get": { "description": "AddIntuneTemplate", "summary": "AddIntuneTemplate", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -6649,9 +6415,7 @@ "post": { "description": "AddAPDevice", "summary": "AddAPDevice", - "tags": [ - "POST" - ], + "tags": ["POST"], "parameters": [ { "required": true, @@ -6697,9 +6461,7 @@ "get": { "description": "RemoveTransportRuleTemplate", "summary": "RemoveTransportRuleTemplate", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -6729,9 +6491,7 @@ "get": { "description": "ListMailboxMobileDevices", "summary": "ListMailboxMobileDevices", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -6769,9 +6529,7 @@ "post": { "description": "ExecExcludeTenant", "summary": "ExecExcludeTenant", - "tags": [ - "POST" - ], + "tags": ["POST"], "parameters": [ { "required": true, @@ -6839,9 +6597,7 @@ "get": { "description": "ExecExcludeTenant", "summary": "ExecExcludeTenant", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -6911,9 +6667,7 @@ "get": { "description": "ExecSetSecurityAlert", "summary": "ExecSetSecurityAlert", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -6975,9 +6729,7 @@ "post": { "description": "ExecAddGDAPRole", "summary": "ExecAddGDAPRole", - "tags": [ - "POST" - ], + "tags": ["POST"], "parameters": [ { "required": true, @@ -7015,9 +6767,7 @@ "get": { "description": "ListExchangeConnectors", "summary": "ListExchangeConnectors", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -7047,9 +6797,7 @@ "post": { "description": "AddTransportTemplate", "summary": "AddTransportTemplate", - "tags": [ - "POST" - ], + "tags": ["POST"], "parameters": [ { "required": true, @@ -7087,9 +6835,7 @@ "get": { "description": "ExecCreateTAP", "summary": "ExecCreateTAP", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -7127,9 +6873,7 @@ "get": { "description": "RemoveCAPolicy", "summary": "RemoveCAPolicy", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -7167,9 +6911,7 @@ "get": { "description": "RemoveTransportRule", "summary": "RemoveTransportRule", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -7207,9 +6949,7 @@ "get": { "description": "RemoveAPDevice", "summary": "RemoveAPDevice", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -7247,9 +6987,7 @@ "post": { "description": "AddPolicy", "summary": "AddPolicy", - "tags": [ - "POST" - ], + "tags": ["POST"], "parameters": [ { "required": true, @@ -7319,9 +7057,7 @@ "post": { "description": "AddContact", "summary": "AddContact", - "tags": [ - "POST" - ], + "tags": ["POST"], "parameters": [ { "required": true, @@ -7351,9 +7087,7 @@ "get": { "description": "PublicScripts", "summary": "PublicScripts", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -7383,9 +7117,7 @@ "get": { "description": "RemoveContact", "summary": "RemoveContact", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -7423,9 +7155,7 @@ "get": { "description": "EditTransportRule", "summary": "EditTransportRule", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -7471,9 +7201,7 @@ "post": { "description": "AddSpamFilter", "summary": "AddSpamFilter", - "tags": [ - "POST" - ], + "tags": ["POST"], "parameters": [ { "required": true, @@ -7503,9 +7231,7 @@ "get": { "description": "RemoveIntuneTemplate", "summary": "RemoveIntuneTemplate", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -7535,9 +7261,7 @@ "get": { "description": "ExecGroupsDeliveryManagement", "summary": "ExecGroupsDeliveryManagement", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -7591,9 +7315,7 @@ "get": { "description": "ListUserDevices", "summary": "ListUserDevices", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -7631,9 +7353,7 @@ "post": { "description": "EditTenant", "summary": "EditTenant", - "tags": [ - "POST" - ], + "tags": ["POST"], "parameters": [ { "required": true, @@ -7687,9 +7407,7 @@ "get": { "description": "ExecAppApproval", "summary": "ExecAppApproval", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -7727,9 +7445,7 @@ "get": { "description": "ListInactiveAccounts", "summary": "ListInactiveAccounts", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -7759,9 +7475,7 @@ "post": { "description": "AddAlert", "summary": "AddAlert", - "tags": [ - "POST" - ], + "tags": ["POST"], "parameters": [ { "required": true, @@ -7927,9 +7641,7 @@ "get": { "description": "ListMailboxes", "summary": "ListMailboxes", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -7959,9 +7671,7 @@ "get": { "description": "ListMFAUsers", "summary": "ListMFAUsers", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -7991,9 +7701,7 @@ "get": { "description": "RemoveGroupTemplate", "summary": "RemoveGroupTemplate", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -8023,9 +7731,7 @@ "get": { "description": "ExecRunBackup", "summary": "ExecRunBackup", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -8055,9 +7761,7 @@ "get": { "description": "ListTransportRules", "summary": "ListTransportRules", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -8087,9 +7791,7 @@ "get": { "description": "ExecMaintenanceScripts", "summary": "ExecMaintenanceScripts", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -8127,9 +7829,7 @@ "get": { "description": "GetCippAlerts", "summary": "GetCippAlerts", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -8159,9 +7859,7 @@ "post": { "description": "ExecGDAPMigration", "summary": "ExecGDAPMigration", - "tags": [ - "POST" - ], + "tags": ["POST"], "parameters": [ { "required": true, @@ -8199,9 +7897,7 @@ "get": { "description": "ListGroups", "summary": "ListGroups", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -8255,9 +7951,7 @@ "get": { "description": "ListDomains", "summary": "ListDomains", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -8287,9 +7981,7 @@ "get": { "description": "ListExternalTenantInfo", "summary": "ListExternalTenantInfo", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -8319,9 +8011,7 @@ "get": { "description": "ListAPDevices", "summary": "ListAPDevices", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -8359,9 +8049,7 @@ "post": { "description": "AddAutopilotConfig", "summary": "AddAutopilotConfig", - "tags": [ - "POST" - ], + "tags": ["POST"], "parameters": [ { "required": true, @@ -8407,9 +8095,7 @@ "post": { "description": "AddCAPolicy", "summary": "AddCAPolicy", - "tags": [ - "POST" - ], + "tags": ["POST"], "parameters": [ { "required": true, @@ -8439,9 +8125,7 @@ "get": { "description": "ListMailboxStatistics", "summary": "ListMailboxStatistics", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -8471,9 +8155,7 @@ "get": { "description": "ExecAssignApp", "summary": "ExecAssignApp", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -8519,9 +8201,7 @@ "get": { "description": "ExecExtensionTest", "summary": "ExecExtensionTest", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -8551,9 +8231,7 @@ "post": { "description": "ExecSetMailboxQuota", "summary": "ExecSetMailboxQuota", - "tags": [ - "POST" - ], + "tags": ["POST"], "parameters": [ { "required": true, @@ -8623,9 +8301,7 @@ "get": { "description": "ListNamedLocations", "summary": "ListNamedLocations", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -8655,9 +8331,7 @@ "get": { "description": "ListMFAUsersAllTenants", "summary": "ListMFAUsersAllTenants", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -8687,9 +8361,7 @@ "get": { "description": "RemoveQueuedAlert", "summary": "RemoveQueuedAlert", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -8719,9 +8391,7 @@ "post": { "description": "ExecAccessChecks", "summary": "ExecAccessChecks", - "tags": [ - "POST" - ], + "tags": ["POST"], "parameters": [ { "required": true, @@ -8765,9 +8435,7 @@ "get": { "description": "ExecAccessChecks", "summary": "ExecAccessChecks", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -8813,9 +8481,7 @@ "get": { "description": "ListSharedMailboxAccountEnabled", "summary": "ListSharedMailboxAccountEnabled", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -8845,9 +8511,7 @@ "get": { "description": "ListSpamfilter", "summary": "ListSpamfilter", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -8877,9 +8541,7 @@ "get": { "description": "ListQuarantinePolicy", "summary": "ListQuarantinePolicy", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -8917,9 +8579,7 @@ "post": { "description": "AddQuarantinePolicy", "summary": "AddQuarantinePolicy", - "tags": [ - "POST" - ], + "tags": ["POST"], "parameters": [ { "required": true, @@ -9013,9 +8673,7 @@ "post": { "description": "EditQuarantinePolicy", "summary": "EditQuarantinePolicy", - "tags": [ - "POST" - ], + "tags": ["POST"], "parameters": [ { "required": true, @@ -9141,9 +8799,7 @@ "get": { "description": "RemoveExConnector", "summary": "RemoveExConnector", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -9189,9 +8845,7 @@ "get": { "description": "RemoveQuarantinePolicy", "summary": "RemoveQuarantinePolicy", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -9238,9 +8892,7 @@ "get": { "description": "ExecDeleteSafeLinksPolicy", "summary": "ExecDeleteSafeLinksPolicy", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -9286,9 +8938,7 @@ "post": { "description": "EditSafeLinksPolicy", "summary": "EditSafeLinksPolicy", - "tags": [ - "POST" - ], + "tags": ["POST"], "parameters": [ { "required": true, @@ -9434,9 +9084,7 @@ "get": { "description": "ListSafeLinksPolicyDetails", "summary": "ListSafeLinksPolicyDetails", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -9594,9 +9242,7 @@ "post": { "description": "Create a new SafeLinks policy and associated rule", "summary": "Create SafeLinks Policy and Rule Configuration", - "tags": [ - "POST" - ], + "tags": ["POST"], "parameters": [ { "required": true, @@ -9791,9 +9437,7 @@ "get": { "description": "List SafeLinks Policy Templates", "summary": "List SafeLinks Policy Templates", - "tags": [ - "GET" - ], + "tags": ["GET"], "responses": { "200": { "content": { @@ -9816,9 +9460,7 @@ "get": { "description": "Remove SafeLinks Policy Template", "summary": "Remove SafeLinks Policy Template", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -9848,9 +9490,7 @@ "post": { "description": "Add SafeLinks Policy Template", "summary": "Add SafeLinks Policy Template", - "tags": [ - "POST" - ], + "tags": ["POST"], "requestBody": { "content": { "application/json": { @@ -9880,9 +9520,7 @@ "post": { "description": "Deploy SafeLinks Policy From Template", "summary": "Deploy SafeLinks Policy From Template", - "tags": [ - "POST" - ], + "tags": ["POST"], "requestBody": { "content": { "application/json": { @@ -9912,9 +9550,7 @@ "get": { "description": "List retention policies or get a specific retention policy by name", "summary": "Get Retention Policies", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -9996,9 +9632,7 @@ "post": { "description": "Create, modify, or delete retention policies", "summary": "Manage Retention Policies", - "tags": [ - "POST" - ], + "tags": ["POST"], "parameters": [ { "required": true, @@ -10134,9 +9768,7 @@ "get": { "description": "List retention tags or get a specific retention tag by name", "summary": "Get Retention Tags", - "tags": [ - "GET" - ], + "tags": ["GET"], "parameters": [ { "required": true, @@ -10224,9 +9856,7 @@ "post": { "description": "Create, modify, or delete retention tags", "summary": "Manage Retention Tags", - "tags": [ - "POST" - ], + "tags": ["POST"], "parameters": [ { "required": true, @@ -10254,12 +9884,39 @@ }, "Type": { "type": "string", - "enum": ["All", "Inbox", "SentItems", "DeletedItems", "Drafts", "Outbox", "JunkEmail", "Journal", "SyncIssues", "ConversationHistory", "Personal", "RecoverableItems", "NonIpmRoot", "LegacyArchiveJournals", "Clutter", "Calendar", "Notes", "Tasks", "Contacts", "RssSubscriptions", "ManagedCustomFolder"], + "enum": [ + "All", + "Inbox", + "SentItems", + "DeletedItems", + "Drafts", + "Outbox", + "JunkEmail", + "Journal", + "SyncIssues", + "ConversationHistory", + "Personal", + "RecoverableItems", + "NonIpmRoot", + "LegacyArchiveJournals", + "Clutter", + "Calendar", + "Notes", + "Tasks", + "Contacts", + "RssSubscriptions", + "ManagedCustomFolder" + ], "description": "Type of the retention tag" }, "RetentionAction": { "type": "string", - "enum": ["DeleteAndAllowRecovery", "PermanentlyDelete", "MoveToArchive", "MarkAsPastRetentionLimit"], + "enum": [ + "DeleteAndAllowRecovery", + "PermanentlyDelete", + "MoveToArchive", + "MarkAsPastRetentionLimit" + ], "description": "Action to take when retention period expires" }, "AgeLimitForRetention": { @@ -10304,7 +9961,12 @@ }, "RetentionAction": { "type": "string", - "enum": ["DeleteAndAllowRecovery", "PermanentlyDelete", "MoveToArchive", "MarkAsPastRetentionLimit"], + "enum": [ + "DeleteAndAllowRecovery", + "PermanentlyDelete", + "MoveToArchive", + "MarkAsPastRetentionLimit" + ], "description": "Action to take when retention period expires" }, "AgeLimitForRetention": { @@ -10393,9 +10055,7 @@ "post": { "description": "Apply a retention policy to one or more mailboxes", "summary": "Set Mailbox Retention Policies", - "tags": [ - "POST" - ], + "tags": ["POST"], "parameters": [ { "required": true, From fb7a409cf30eee39397e5a7bcdb9c312f33c8ea3 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Fri, 20 Feb 2026 17:44:15 +0100 Subject: [PATCH 481/503] version up --- version_latest.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version_latest.txt b/version_latest.txt index 82f3d338cfb6..4149c39eec6f 100644 --- a/version_latest.txt +++ b/version_latest.txt @@ -1 +1 @@ -10.0.9 +10.1.0 From d37f630ea0c9c525a0fb2794f7a490ec9e4a03c5 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 20 Feb 2026 15:01:33 -0500 Subject: [PATCH 482/503] Support missing CIDR prefix and dedupe maxBits calc If the supplied range omits a CIDR prefix (e.g. "10.0.0.0"), default the prefix to the address-family max bits (32 for IPv4, 128 for IPv6). Move the $maxBits calculation before prefix parsing so the default can be applied, and remove the duplicate $maxBits assignment later in the function. This also ensures consistent mask computation for both IPv4 and IPv6. --- Modules/CIPPCore/Public/Authentication/Test-IpInRange.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Modules/CIPPCore/Public/Authentication/Test-IpInRange.ps1 b/Modules/CIPPCore/Public/Authentication/Test-IpInRange.ps1 index 2279b20ce110..f7103fbca239 100644 --- a/Modules/CIPPCore/Public/Authentication/Test-IpInRange.ps1 +++ b/Modules/CIPPCore/Public/Authentication/Test-IpInRange.ps1 @@ -31,7 +31,8 @@ function Test-IpInRange { $IP = [System.Net.IPAddress]::Parse($IPAddress) $rangeParts = $Range -split '/' $networkAddr = [System.Net.IPAddress]::Parse($rangeParts[0]) - $prefix = [int]$rangeParts[1] + $maxBits = if ($networkAddr.AddressFamily -eq 'InterNetworkV6') { 128 } else { 32 } + $prefix = if ($rangeParts.Count -gt 1) { [int]$rangeParts[1] } else { $maxBits } if ($networkAddr.AddressFamily -ne $IP.AddressFamily) { return $false @@ -39,7 +40,6 @@ function Test-IpInRange { $ipBig = ConvertIpToBigInteger $IP $netBig = ConvertIpToBigInteger $networkAddr - $maxBits = if ($networkAddr.AddressFamily -eq 'InterNetworkV6') { 128 } else { 32 } $shift = $maxBits - $prefix $mask = [System.Numerics.BigInteger]::Pow(2, $shift) - [System.Numerics.BigInteger]::One $invertedMask = [System.Numerics.BigInteger]::MinusOne -bxor $mask From f674c9225bf10cfb910c854af226cd797641b8ac Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 20 Feb 2026 17:32:25 -0500 Subject: [PATCH 483/503] Normalize default tenant groups JSON Update Invoke-ExecCreateDefaultGroups.ps1 to adjust the $DefaultGroups JSON payload. The Business Premium group's DynamicRules were consolidated into a single object with a value array (now including GUIDs for license entries) and several redundant @type fields were simplified for more consistent JSON parsing when creating default tenant groups. --- .../CIPP/Settings/Invoke-ExecCreateDefaultGroups.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecCreateDefaultGroups.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecCreateDefaultGroups.ps1 index bd2a682429ac..4fc1b2f5575c 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecCreateDefaultGroups.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecCreateDefaultGroups.ps1 @@ -16,7 +16,7 @@ function Invoke-ExecCreateDefaultGroups { $Table = Get-CippTable -tablename 'TenantGroups' $Results = [System.Collections.Generic.List[object]]::new() $ExistingGroups = Get-CIPPAzDataTableEntity @Table -Filter "PartitionKey eq 'TenantGroup' and Type eq 'dynamic'" - $DefaultGroups = '[{"PartitionKey":"TenantGroup","RowKey":"369d985e-0fba-48f9-844f-9f793b10a12c","Description":"This group does not have a license for intune, nor a license for Entra ID Premium","Description@type":null,"DynamicRules":"[{\"property\":\"availableServicePlan\",\"operator\":\"notIn\",\"value\":[{\"label\":\"Microsoft Intune\",\"value\":\"INTUNE_A\",\"id\":\"c1ec4a95-1f05-45b3-a911-aa3fa01094f5\"}]},{\"property\":\"availableServicePlan\",\"operator\":\"notIn\",\"value\":[{\"label\":\"Microsoft Entra ID P1\",\"value\":\"AAD_PREMIUM\",\"id\":\"41781fb2-bc02-4b7c-bd55-b576c07bb09d\"}]}]","DynamicRules@type":null,"GroupType":"dynamic","GroupType@type":null,"RuleLogic":"and","RuleLogic@type":null,"Name":"Not Intune and Entra Premium Capable","Name@type":null},{"PartitionKey":"TenantGroup","RowKey":"4dbca08b-7dc5-4e0f-bc25-14a90c8e0941","Description":"This group has atleast one Business Premium License available","Description@type":null,"DynamicRules":"[{\"property\":\"availableLicense\",\"operator\":\"in\",\"value\":[{\"label\":\"Microsoft 365 Business Premium\",\"value\":\"SPB\"}]},{\"property\":\"availableLicense\",\"operator\":\"in\",\"value\":[{\"label\":\"Microsoft 365 Business Premium (no Teams)\",\"value\":\"Microsoft_365_ Business_ Premium_(no Teams)\"}]},{\"property\":\"availableLicense\",\"operator\":\"in\",\"value\":[{\"label\":\"Microsoft 365 Business Premium Donation\",\"value\":\"Microsoft_365_Business_Premium_Donation_(Non_Profit_Pricing)\"}]},{\"property\":\"availableLicense\",\"operator\":\"in\",\"value\":[{\"label\":\"Microsoft 365 Business Premium EEA (no Teams)\",\"value\":\"Office_365_w\/o_Teams_Bundle_Business_Premium\"}]}]","DynamicRules@type":null,"GroupType":"dynamic","GroupType@type":null,"RuleLogic":"or","RuleLogic@type":null,"Name":"Business Premium License available","Name@type":null},{"PartitionKey":"TenantGroup","RowKey":"703c0e69-84a8-4dcf-a1c2-4986d2ccc850","Description":"This group does have a license for Entra Premium but does not have a license for Intune","Description@type":null,"DynamicRules":"[{\"property\":\"availableServicePlan\",\"operator\":\"in\",\"value\":[{\"label\":\"Microsoft Entra ID P1\",\"value\":\"AAD_PREMIUM\",\"id\":\"41781fb2-bc02-4b7c-bd55-b576c07bb09d\"}]},{\"property\":\"availableServicePlan\",\"operator\":\"notIn\",\"value\":[{\"label\":\"Microsoft Intune\",\"value\":\"INTUNE_A\",\"id\":\"c1ec4a95-1f05-45b3-a911-aa3fa01094f5\"}]}]","DynamicRules@type":null,"GroupType":"dynamic","GroupType@type":null,"RuleLogic":"and","RuleLogic@type":null,"Name":"Entra Premium Capable, Not Intune Capable","Name@type":null},{"PartitionKey":"TenantGroup","RowKey":"c1dadbc0-f0b4-448c-a2e6-e1938ba102e0","Description":"This group has Intune and Entra ID Premium available","Description@type":null,"DynamicRules":"{\"property\":\"availableServicePlan\",\"operator\":\"in\",\"value\":[{\"label\":\"Microsoft Intune\",\"value\":\"INTUNE_A\"},{\"label\":\"Microsoft Entra ID P1\",\"value\":\"AAD_PREMIUM\"}]}","DynamicRules@type":null,"GroupType":"dynamic","GroupType@type":null,"RuleLogic":"and","RuleLogic@type":null,"Name":"Entra ID Premium and Intune Capable","Name@type":null}]' | ConvertFrom-Json + $DefaultGroups = '[{"PartitionKey":"TenantGroup","RowKey":"369d985e-0fba-48f9-844f-9f793b10a12c","Description":"This group does not have a license for intune, nor a license for Entra ID Premium","Description@type":null,"DynamicRules":"[{\"property\":\"availableServicePlan\",\"operator\":\"notIn\",\"value\":[{\"label\":\"Microsoft Intune\",\"value\":\"INTUNE_A\",\"id\":\"c1ec4a95-1f05-45b3-a911-aa3fa01094f5\"}]},{\"property\":\"availableServicePlan\",\"operator\":\"notIn\",\"value\":[{\"label\":\"Microsoft Entra ID P1\",\"value\":\"AAD_PREMIUM\",\"id\":\"41781fb2-bc02-4b7c-bd55-b576c07bb09d\"}]}]","DynamicRules@type":null,"GroupType":"dynamic","GroupType@type":null,"RuleLogic":"and","RuleLogic@type":null,"Name":"Not Intune and Entra Premium Capable","Name@type":null},{"PartitionKey":"TenantGroup","RowKey":"4dbca08b-7dc5-4e0f-bc25-14a90c8e0941","Description":"This group has atleast one Business Premium License available","DynamicRules":"{\"property\":\"availableLicense\",\"operator\":\"in\",\"value\":[{\"label\":\"Microsoft 365 Business Premium\",\"value\":\"SPB\",\"guid\":\"cbdc14ab-d96c-4c30-b9f4-6ada7cdc1d46\"},{\"label\":\"Microsoft 365 Business Premium (no Teams)\",\"value\":\"Microsoft_365_ Business_ Premium_(no Teams)\",\"guid\":\"00e1ec7b-e4a3-40d1-9441-b69b597ab222\"},{\"label\":\"Microsoft 365 Business Premium Donation\",\"value\":\"Microsoft_365_Business_Premium_Donation_(Non_Profit_Pricing)\",\"guid\":\"24c35284-d768-4e53-84d9-b7ae73dddf69\"},{\"label\":\"Microsoft 365 Business Premium EEA (no Teams)\",\"value\":\"Office_365_w/o_Teams_Bundle_Business_Premium\",\"guid\":\"a3f586b6-8cce-4d9b-99d6-55238397f77a\"}]}","GroupType":"dynamic","Name":"Business Premium License available","RuleLogic":"or"},{"PartitionKey":"TenantGroup","RowKey":"703c0e69-84a8-4dcf-a1c2-4986d2ccc850","Description":"This group does have a license for Entra Premium but does not have a license for Intune","Description@type":null,"DynamicRules":"[{\"property\":\"availableServicePlan\",\"operator\":\"in\",\"value\":[{\"label\":\"Microsoft Entra ID P1\",\"value\":\"AAD_PREMIUM\",\"id\":\"41781fb2-bc02-4b7c-bd55-b576c07bb09d\"}]},{\"property\":\"availableServicePlan\",\"operator\":\"notIn\",\"value\":[{\"label\":\"Microsoft Intune\",\"value\":\"INTUNE_A\",\"id\":\"c1ec4a95-1f05-45b3-a911-aa3fa01094f5\"}]}]","DynamicRules@type":null,"GroupType":"dynamic","GroupType@type":null,"RuleLogic":"and","RuleLogic@type":null,"Name":"Entra Premium Capable, Not Intune Capable","Name@type":null},{"PartitionKey":"TenantGroup","RowKey":"c1dadbc0-f0b4-448c-a2e6-e1938ba102e0","Description":"This group has Intune and Entra ID Premium available","Description@type":null,"DynamicRules":"{\"property\":\"availableServicePlan\",\"operator\":\"in\",\"value\":[{\"label\":\"Microsoft Intune\",\"value\":\"INTUNE_A\"},{\"label\":\"Microsoft Entra ID P1\",\"value\":\"AAD_PREMIUM\"}]}","DynamicRules@type":null,"GroupType":"dynamic","GroupType@type":null,"RuleLogic":"and","RuleLogic@type":null,"Name":"Entra ID Premium and Intune Capable","Name@type":null}]' | ConvertFrom-Json foreach ($Group in $DefaultGroups) { From b6e89c2a551895c2f6d2463204db54487c37ef47 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 20 Feb 2026 18:14:01 -0500 Subject: [PATCH 484/503] Add log entry to Invoke-AddAlert Add a Write-LogMessage call to Invoke-AddAlert to record alert additions (API='AddAlert') with message, severity Info, LogData and request headers for telemetry/troubleshooting. Also normalize the function keyword casing from 'Function' to 'function' for consistency. --- .../Tenant/Administration/Alerts/Invoke-AddAlert.ps1 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-AddAlert.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-AddAlert.ps1 index a683b153c508..df6818654545 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-AddAlert.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-AddAlert.ps1 @@ -1,4 +1,4 @@ -Function Invoke-AddAlert { +function Invoke-AddAlert { <# .FUNCTIONALITY Entrypoint @@ -27,6 +27,7 @@ Function Invoke-AddAlert { $WebhookTable = Get-CippTable -TableName 'WebhookRules' Add-CIPPAzDataTableEntity @WebhookTable -Entity $CompleteObject -Force $Results = "Added Audit Log Alert for $($Tenants.count) tenants. It may take up to four hours before Microsoft starts delivering these alerts." + Write-LogMessage -API 'AddAlert' -message $Results -sev Info -LogData $CompleteObject -headers $Request.Headers return ([HttpResponseContext]@{ StatusCode = [HttpStatusCode]::OK From 5f240c15f99694725f84223fb895bf9ba9f43479 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 20 Feb 2026 18:23:21 -0500 Subject: [PATCH 485/503] fix quarantine return --- .../Public/Alerts/Get-CIPPAlertQuarantineReleaseRequests.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertQuarantineReleaseRequests.ps1 b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertQuarantineReleaseRequests.ps1 index 58ed3b035407..b20096d59020 100644 --- a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertQuarantineReleaseRequests.ps1 +++ b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertQuarantineReleaseRequests.ps1 @@ -14,7 +14,7 @@ #Add rerun protection: This Monitor can only run once every hour. $Rerun = Test-CIPPRerun -TenantFilter $TenantFilter -Type 'ExchangeMonitor' -API 'Get-CIPPAlertQuarantineReleaseRequests' if ($Rerun) { - return $true + return } $HasLicense = Test-CIPPStandardLicense -StandardName 'QuarantineReleaseRequests' -TenantFilter $TenantFilter -RequiredCapabilities @( 'EXCHANGE_S_STANDARD', @@ -25,7 +25,7 @@ ) if (-not $HasLicense) { - return $true + return } try { From e9a01a9c4ecd0e8b57f276b2d19292069a763f21 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 20 Feb 2026 19:31:11 -0500 Subject: [PATCH 486/503] Add cleanup rule and use OData timestamp filters Invoke server-side OData timestamp filtering and add a table cleanup rule for quarantine messages. - Invoke-ListMailQuarantine.ps1: replace client-side Where-Object Timestamp check with an OData filter that uses a UTC datetime string (yyyy-MM-ddTHH:mm:ssZ). This constructs the timestamp ($30MinutesAgo) via ToUniversalTime and embeds it in the Table query to avoid fetching then filtering locally. - Start-TableCleanup.ps1: add a CleanupRule entry for the cacheQuarantineMessages table to delete QuarantineMessage rows older than 1 day (uses an OData lt datetime filter). The rule requests up to 10000 rows and returns PartitionKey/RowKey/ETag for deletion. These changes move time-based filtering into the Azure Table query to reduce data transfer and add automated cleanup for quarantine messages. --- .../Spamfilter/Invoke-ListMailQuarantine.ps1 | 5 +++-- .../Entrypoints/Timer Functions/Start-TableCleanup.ps1 | 10 ++++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Spamfilter/Invoke-ListMailQuarantine.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Spamfilter/Invoke-ListMailQuarantine.ps1 index d785b015f940..7780f90588eb 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Spamfilter/Invoke-ListMailQuarantine.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Spamfilter/Invoke-ListMailQuarantine.ps1 @@ -16,8 +16,9 @@ function Invoke-ListMailQuarantine { } else { $Table = Get-CIPPTable -TableName cacheQuarantineMessages $PartitionKey = 'QuarantineMessage' - $Filter = "PartitionKey eq '$PartitionKey'" - $Rows = Get-CIPPAzDataTableEntity @Table -filter $Filter | Where-Object -Property Timestamp -GT (Get-Date).AddMinutes(-30) + $30MinutesAgo = (Get-Date).AddMinutes(-30).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ') + $Filter = "PartitionKey eq '$PartitionKey' and Timestamp gt datetime'$30MinutesAgo'" + $Rows = Get-CIPPAzDataTableEntity @Table -filter $Filter $QueueReference = '{0}-{1}' -f $TenantFilter, $PartitionKey $RunningQueue = Invoke-ListCippQueue -Reference $QueueReference | Where-Object { $_.Status -notmatch 'Completed' -and $_.Status -notmatch 'Failed' } # If a queue is running, we will not start a new one diff --git a/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-TableCleanup.ps1 b/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-TableCleanup.ps1 index 74f620e133e1..771e9d66363c 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-TableCleanup.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-TableCleanup.ps1 @@ -75,6 +75,16 @@ function Start-TableCleanup { Property = @('PartitionKey', 'RowKey', 'ETag') } } + @{ + FunctionName = 'TableCleanupTask' + Type = 'CleanupRule' + TableName = 'cacheQuarantineMessages' + DataTableProps = @{ + Filter = "PartitionKey eq 'QuarantineMessage' and Timestamp lt datetime'$((Get-Date).AddDays(-1).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ'))'" + First = 10000 + Property = @('PartitionKey', 'RowKey', 'ETag') + } + } @{ FunctionName = 'TableCleanupTask' Type = 'DeleteTable' From 80c4477632674cff5a02259bf3d82343dc1dac26 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 20 Feb 2026 22:12:41 -0500 Subject: [PATCH 487/503] cleanup logging --- .../Activity Triggers/Push-ExecScheduledCommand.ps1 | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecScheduledCommand.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecScheduledCommand.ps1 index 22607cb0841b..a7744b0a1af0 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecScheduledCommand.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecScheduledCommand.ps1 @@ -317,13 +317,12 @@ function Push-ExecScheduledCommand { } Write-LogMessage -API 'Scheduler_UserTasks' -tenant $Tenant -tenantid $TenantInfo.customerId -message "Failed to execute task $($task.Name): $errorMessage" -sev Error -LogData (Get-CippExceptionData -Exception $_.Exception) } - Write-Information 'Sending task results to target. Updating the task state.' # For orchestrator-based commands, skip post-execution alerts as they will be handled by the orchestrator's post-execution function if ($Results -and $Item.Command -notin $OrchestratorBasedCommands) { + Write-Information "Sending task results to post execution target(s): $($Task.PostExecution -join ', ')." Send-CIPPScheduledTaskAlert -Results $Results -TaskInfo $task -TenantFilter $Tenant -TaskType $TaskType } - Write-Information 'Sent the results to the target. Updating the task state.' try { # For orchestrator-based commands, skip task state update as it will be handled by post-execution From 68e8562bd0e045e7e597df17dc2dd699a9bc5f16 Mon Sep 17 00:00:00 2001 From: Chris Dewey <142454021+chris-dewey-1991@users.noreply.github.com> Date: Sun, 22 Feb 2026 21:36:56 +0000 Subject: [PATCH 488/503] Fix drift comparison issue for NotifyOutboundSpamRecipients Convert NotifyOutboundSpamRecipients array to comma-separated string to fix drift comparison and reporting issues --- .../Public/Standards/Invoke-CIPPStandardOutBoundSpamAlert.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardOutBoundSpamAlert.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardOutBoundSpamAlert.ps1 index 0ae5d1e92eab..d703bf2c5569 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardOutBoundSpamAlert.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardOutBoundSpamAlert.ps1 @@ -76,7 +76,7 @@ function Invoke-CIPPStandardOutBoundSpamAlert { Add-CIPPBPAField -FieldName 'OutboundSpamAlert' -FieldValue $CurrentInfo.NotifyOutboundSpam -StoreAs bool -Tenant $tenant $CurrentValue = @{ NotifyOutboundSpam = $CurrentInfo.NotifyOutboundSpam - NotifyOutboundSpamRecipients = $CurrentInfo.NotifyOutboundSpamRecipients + NotifyOutboundSpamRecipients = ($CurrentInfo.NotifyOutboundSpamRecipients -join ', ') } $ExpectedValue = @{ NotifyOutboundSpam = $true From d06acf015b36eb0f9860df6517e75d5298a1df61 Mon Sep 17 00:00:00 2001 From: Chris Dewey <142454021+chris-dewey-1991@users.noreply.github.com> Date: Sun, 22 Feb 2026 21:43:56 +0000 Subject: [PATCH 489/503] Fix self-service license handling and logging Refactor logic to avoid mutating original objects and improve logging. --- ...CIPPStandardDisableSelfServiceLicenses.ps1 | 40 +++++++++++++------ 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSelfServiceLicenses.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSelfServiceLicenses.ps1 index 03845d7a4d03..1a5bad508ffb 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSelfServiceLicenses.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSelfServiceLicenses.ps1 @@ -45,41 +45,54 @@ function Invoke-CIPPStandardDisableSelfServiceLicenses { throw $Message } - if ($settings.exclusions -like '*;*') { $exclusions = $settings.Exclusions -split (';') } else { $exclusions = $settings.Exclusions -split (',') } + $CurrentValues = $selfServiceItems | Select-Object -Property productName, productId, policyValue + $ExpectedValues = [System.Collections.Generic.List[PSCustomObject]]::new() + foreach ($Item in $selfServiceItems) { + if ($Item.productId -in $exclusions) { - $Item.policyValue = "Enabled" - $ExpectedValues.add(($Item | Select-Object -Property productName, productId, policyValue)) - Write-LogMessage -API 'Standards' -tenant $Tenant -message "Exclusion present for self-service license '$($Item.productName) - $($Item.productId)'" + $desiredPolicyValue = "Enabled" + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Exclusion present for self-service license '$($Item.productName) - $($Item.productId)'" } else { - $Item.policyValue = "Disabled" - $ExpectedValues.add(($Item | Select-Object -Property productName, productId, policyValue)) + $desiredPolicyValue = "Disabled" } - } - $CurrentValues = $selfServiceItems | Select-Object -Property productName, productId, policyValue + $ExpectedValues.Add([PSCustomObject]@{ + productName = $Item.productName + productId = $Item.productId + policyValue = $desiredPolicyValue + }) + } if ($settings.remediate) { + $Compare = Compare-Object -ReferenceObject $ExpectedValues -DifferenceObject $CurrentValues -Property productName, productId, policyValue if (!$Compare) { Write-LogMessage -API 'Standards' -tenant $Tenant -message 'self service licenses are already set correctly.' -sev Info } else { - $NeedsUpdate = $Compare | Where-Object {$_.SideIndicator -eq "<="} + + $NeedsUpdate = $Compare | Where-Object { $_.SideIndicator -eq "<=" } + foreach ($Item in $NeedsUpdate) { try { - $body = @{policyValue=$Item.policyValue} | ConvertTo-Json -Compress + + $currentItem = $CurrentValues | Where-Object { $_.productId -eq $Item.productId } | Select-Object -First 1 + $currentValue = if ($currentItem) { $currentItem.policyValue } else { "" } + + $body = @{ policyValue = $Item.policyValue } | ConvertTo-Json -Compress New-GraphPOSTRequest -scope 'aeb86249-8ea3-49e2-900b-54cc8e308f85/.default' -uri "https://licensing.m365.microsoft.com/v1.0/policies/AllowSelfServicePurchase/products/$($Item.productId)" -tenantid $Tenant -body $body -type PUT - Write-LogMessage -API 'Standards' -tenant $tenant -message "Changed Self Service status for product '$($Item.productName) - $($Item.productId)' to '$($Item.policyValue)'" + + Write-LogMessage -API 'Standards' -tenant $tenant -message "Changed Self Service status for product '$($Item.productName) - $($Item.productId)' from '$currentValue' to '$($Item.policyValue)'" -sev Info } catch { Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to set product status for '$($Item.productName) - $($Item.productId)' with body $($body) for reason: $($_.Exception.Message)" -sev Error } @@ -100,12 +113,13 @@ function Invoke-CIPPStandardDisableSelfServiceLicenses { } if ($Settings.report -eq $true) { + $StateIsCorrect = !(Compare-Object -ReferenceObject $ExpectedValues -DifferenceObject $CurrentValues -Property productName, productId, policyValue) $ExpectedValuesHash = @{} foreach ($Item in $ExpectedValues) { $ExpectedValuesHash[$Item.productName] = [PSCustomObject]@{ - Id = $Item.productId + Id = $Item.productId Value = $Item.policyValue } } @@ -114,7 +128,7 @@ function Invoke-CIPPStandardDisableSelfServiceLicenses { $CurrentValuesHash = @{} foreach ($Item in $CurrentValues) { $CurrentValuesHash[$Item.productName] = [PSCustomObject]@{ - Id = $Item.productId + Id = $Item.productId Value = $Item.policyValue } } From 518d970fa3e31726ea2b30425c6fd62254668566 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Sun, 22 Feb 2026 21:42:15 -0500 Subject: [PATCH 490/503] Use Graph bulk API and improve group handling Replace multiple per-call Graph requests with a single New-GraphBulkRequest to improve performance and reduce API calls. Adds a BulkInfoRequests list to fetch user ID (when missing), all groups, and the user's memberOf groups in one bulk call. Also switches from a GetMemberGroups POST to the memberOf/microsoft.graph.group GET result and adjusts downstream logic for mail-enabled, M365, licensed, and dynamic groups. This avoids returning transitive group memberships. --- Modules/CIPPCore/Public/Remove-CIPPGroups.ps1 | 44 ++++++++++++++----- 1 file changed, 34 insertions(+), 10 deletions(-) diff --git a/Modules/CIPPCore/Public/Remove-CIPPGroups.ps1 b/Modules/CIPPCore/Public/Remove-CIPPGroups.ps1 index 527de771990e..f901ac82953e 100644 --- a/Modules/CIPPCore/Public/Remove-CIPPGroups.ps1 +++ b/Modules/CIPPCore/Public/Remove-CIPPGroups.ps1 @@ -8,14 +8,38 @@ function Remove-CIPPGroups { $UserID ) - if (-not $userid) { - $UserID = (New-GraphGetRequest -uri "https://graph.microsoft.com/beta/users/$($Username)" -tenantid $TenantFilter).id + $BulkInfoRequests = [System.Collections.Generic.List[object]]::new() + + if (-not $UserID) { + $BulkInfoRequests.Add(@{ + id = 'getUserID' + method = 'GET' + url = "users/$($Username)?`$select=id" + }) } - $AllGroups = (New-GraphGetRequest -uri "https://graph.microsoft.com/beta/groups/?`$select=displayName,mailEnabled,id,groupTypes,assignedLicenses,onPremisesSyncEnabled,membershipRule&`$top=999" -tenantid $TenantFilter) - # Get user's groups - $UserGroups = (New-GraphPostRequest -uri "https://graph.microsoft.com/beta/users/$($UserID)/GetMemberGroups" -tenantid $TenantFilter -type POST -body '{"securityEnabledOnly": false}').value + $BulkInfoRequests.Add( + @{ + id = 'getAllGroups' + method = 'GET' + url = "groups/?`$select=displayName,mailEnabled,id,groupTypes,assignedLicenses,onPremisesSyncEnabled,membershipRule&`$top=999" + }) + $BulkInfoRequests.Add(@{ + id = 'getUserGroups' + method = 'GET' + url = "users/$($UserID ?? $Username)/memberOf/microsoft.graph.group?`$select=id" + }) + + $BulkGetResults = New-GraphBulkRequest -tenantid $TenantFilter -Requests @($BulkInfoRequests) + + $UserInfo = ($BulkGetResults | Where-Object { $_.id -eq 'getUserID' }).body + if ($UserInfo) { + $UserID = $UserInfo.id + } + $AllGroups = ($BulkGetResults | Where-Object { $_.id -eq 'getAllGroups' }).body.value + $UserGroups = ($BulkGetResults | Where-Object { $_.id -eq 'getUserGroups' }).body.value + #users/$($User.id)/memberOf/microsoft.graph.directoryRole if (-not $UserGroups) { $Returnval = "$($Username) is not a member of any groups." Write-LogMessage -headers $Headers -API $APIName -message "$($Username) is not a member of any groups" -Sev 'Info' -tenant $TenantFilter @@ -31,10 +55,10 @@ function Remove-CIPPGroups { # Process each group and prepare bulk requests foreach ($Group in $UserGroups) { - $GroupInfo = $AllGroups | Where-Object -Property id -EQ $Group + $GroupInfo = $AllGroups | Where-Object -Property id -EQ $Group.id $GroupName = $GroupInfo.displayName $IsMailEnabled = $GroupInfo.mailEnabled - $IsM365Group = $null -ne ($AllGroups | Where-Object { $_.id -eq $Group -and $_.groupTypes -contains 'Unified' }) + $IsM365Group = $GroupInfo.groupTypes -and $GroupInfo.groupTypes -contains 'Unified' $IsLicensed = $GroupInfo.assignedLicenses.Count -gt 0 $IsDynamic = -not [string]::IsNullOrWhiteSpace($GroupInfo.membershipRule) @@ -51,13 +75,13 @@ function Remove-CIPPGroups { if ($IsM365Group -or (-not $IsMailEnabled)) { # Use Graph API for M365 Groups and Security Groups $BulkRequests.Add(@{ - id = "removeFromGroup-$Group" + id = "removeFromGroup-$($Group.id)" method = 'DELETE' - url = "groups/$Group/members/$UserID/`$ref" + url = "groups/$($Group.id)/members/$UserID/`$ref" }) $GraphLogs.Add(@{ message = "Removed $Username from $GroupName" - id = "removeFromGroup-$Group" + id = "removeFromGroup-$($Group.id)" groupName = $GroupName }) } elseif ($IsMailEnabled) { From aa885f179f6c665ea0eedc27a9ef65e613b79cd5 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Sun, 22 Feb 2026 23:46:48 -0500 Subject: [PATCH 491/503] Standardize log severity and propagate headers Normalize logging across the codebase by replacing Write-LogMessage calls using -Sev 'Warning' with -sev 'Warn' and standardizing the severity parameter name. Propagate Headers through the offboarding orchestration (Invoke-CIPPOffboardingJob, Push-CIPPOffboardingComplete and related task parameter objects), include headers in log calls and post-execution parameters, and attach error log data where applicable. Also restrict Get-Command lookup to the CIPPCore module in Push-CIPPOffboardingTask and apply minor whitespace/formatting fixes. --- .../Public/Add-CIPPWin32LobAppContent.ps1 | 2 +- .../Push-CIPPOffboardingComplete.ps1 | 7 +- .../Push-CIPPOffboardingTask.ps1 | 2 +- .../Invoke-ListScheduledItemDetails.ps1 | 4 +- .../CIPP/Settings/Invoke-ExecApiClient.ps1 | 2 +- .../CIPP/Settings/Invoke-ExecGDAPTrace.ps1 | 20 +- .../Settings/Invoke-ListCustomVariables.ps1 | 2 +- .../Invoke-DeployContactTemplates.ps1 | 6 +- .../Contacts/Invoke-ListContactTemplates.ps1 | 2 +- .../Administration/Invoke-ExecHVEUser.ps1 | 4 +- .../Invoke-ExecModifyCalPerms.ps1 | 2 +- .../Invoke-ExecModifyContactPerms.ps1 | 2 +- .../Invoke-ExecModifyMBPerms.ps1 | 4 +- .../Users/Invoke-AddJITAdminTemplate.ps1 | 4 +- .../Users/Invoke-CIPPOffboardingJob.ps1 | 43 ++-- .../Users/Invoke-EditJITAdminTemplate.ps1 | 4 +- .../Users/Invoke-ListJITAdminTemplates.ps1 | 2 +- .../Users/Invoke-RemoveJITAdminTemplate.ps1 | 4 +- .../Invoke-RemoveUserDefaultTemplate.ps1 | 2 +- .../Invoke-AddSafeLinksPolicyFromTemplate.ps1 | 4 +- .../Invoke-ExecDeleteSafeLinksPolicy.ps1 | 4 +- .../Invoke-ExecNewSafeLinksPolicy.ps1 | 4 +- .../Invoke-ListSafeLinksPolicy.ps1 | 2 +- .../Invoke-ListSafeLinksPolicyDetails.ps1 | 4 +- .../Invoke-ExecCreateAppTemplate.ps1 | 12 +- .../Tenant/Invoke-ListTenants.ps1 | 2 +- .../GDAP/Invoke-ExecGDAPRoleTemplate.ps1 | 8 +- .../Invoke-ExecUpdateDriftDeviation.ps1 | 2 +- .../Entrypoints/Invoke-ExecListAppId.ps1 | 4 +- .../Public/Entrypoints/Invoke-ListLogs.ps1 | 4 +- .../Start-BackupRetentionCleanup.ps1 | 4 +- .../CIPPCore/Public/New-CIPPRestoreTask.ps1 | 2 +- .../Public/Remove-CIPPCalendarPermissions.ps1 | 6 +- Modules/CIPPCore/Public/Remove-CIPPGroups.ps1 | 186 +++++++++--------- .../Public/Remove-CIPPMailboxPermissions.ps1 | 2 +- .../Public/Remove-CIPPMailboxRule.ps1 | 2 +- .../Public/Set-CIPPAssignedPolicy.ps1 | 4 +- .../CIPPCore/Public/Set-CIPPMailboxRule.ps1 | 2 +- .../CIPPCore/Public/Set-CIPPResetPassword.ps1 | 2 +- .../CIPPCore/Public/Test-CIPPAccessTenant.ps1 | 2 +- .../Invoke-CIPPGraphWebhookRenewal.ps1 | 2 +- .../Public/PwPush/New-PwPushLink.ps1 | 2 +- 42 files changed, 207 insertions(+), 177 deletions(-) diff --git a/Modules/CIPPCore/Public/Add-CIPPWin32LobAppContent.ps1 b/Modules/CIPPCore/Public/Add-CIPPWin32LobAppContent.ps1 index 1b238870e622..2b9b7393d9c4 100644 --- a/Modules/CIPPCore/Public/Add-CIPPWin32LobAppContent.ps1 +++ b/Modules/CIPPCore/Public/Add-CIPPWin32LobAppContent.ps1 @@ -138,7 +138,7 @@ function Add-CIPPWin32LobAppContent { if ($CommitStateReq.uploadState -like '*fail*') { $errorMsg = "Commit failed. Upload state: $($CommitStateReq.uploadState)" if ($Headers) { - Write-LogMessage -Headers $Headers -API $APIName -message $errorMsg -Sev 'Warning' -tenant $TenantFilter + Write-LogMessage -Headers $Headers -API $APIName -message $errorMsg -sev 'Warn' -tenant $TenantFilter } throw $errorMsg } diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPOffboardingComplete.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPOffboardingComplete.ps1 index 8d86e31a27a3..faca0853ea6f 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPOffboardingComplete.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPOffboardingComplete.ps1 @@ -15,6 +15,7 @@ function Push-CIPPOffboardingComplete { $TaskInfo = $Item.Parameters.TaskInfo $TenantFilter = $Item.Parameters.TenantFilter $Username = $Item.Parameters.Username + $Headers = $Item.Parameters.Headers $Results = $Item.Results # Results come from orchestrator, not Parameters try { @@ -102,19 +103,19 @@ function Push-CIPPOffboardingComplete { TaskState = 'Completed' } - Write-LogMessage -API 'Offboarding' -tenant $TenantFilter -message "Offboarding completed successfully for $Username" -sev Info + Write-LogMessage -API 'Offboarding' -tenant $TenantFilter -message "Offboarding completed successfully for $Username" -sev Info -headers $Headers # Send post-execution alerts if configured if ($TaskInfo.PostExecution -and $ProcessedResults) { Send-CIPPScheduledTaskAlert -Results $ProcessedResults -TaskInfo $TaskInfo -TenantFilter $TenantFilter } } - + Write-LogMessage -API 'Offboarding' -tenant $TenantFilter -message "Offboarding completed for $Username" -sev Info -headers $Headers return "Offboarding completed for $Username" } catch { $ErrorMsg = "Failed to complete offboarding for $Username : $($_.Exception.Message)" - Write-LogMessage -API 'Offboarding' -tenant $TenantFilter -message $ErrorMsg -sev Error + Write-LogMessage -API 'Offboarding' -tenant $TenantFilter -message $ErrorMsg -sev Error -headers $Headers -LogData (Get-CippException -Exception $_) throw $ErrorMsg } } diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPOffboardingTask.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPOffboardingTask.ps1 index 7f0243f4d9c9..eacb976bf70e 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPOffboardingTask.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPOffboardingTask.ps1 @@ -19,7 +19,7 @@ function Push-CIPPOffboardingTask { Write-Information "Executing offboarding cmdlet: $Cmdlet" # Check if cmdlet exists - $CmdletInfo = Get-Command -Name $Cmdlet -ErrorAction SilentlyContinue + $CmdletInfo = Get-Command -Name $Cmdlet -Module CIPPCore -ErrorAction SilentlyContinue if (-not $CmdletInfo) { throw "Cmdlet $Cmdlet does not exist" } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Scheduler/Invoke-ListScheduledItemDetails.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Scheduler/Invoke-ListScheduledItemDetails.ps1 index be60e04ec9b2..b5d04c9de337 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Scheduler/Invoke-ListScheduledItemDetails.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Scheduler/Invoke-ListScheduledItemDetails.ps1 @@ -119,7 +119,7 @@ function Invoke-ListScheduledItemDetails { } } catch { # If JSON parsing fails, use raw value - Write-LogMessage -API $APIName -message "Error parsing Task.Results as JSON: $_" -Sev 'Warning' + Write-LogMessage -API $APIName -message "Error parsing Task.Results as JSON: $_" -sev 'Warn' $ResultData = $Task.Results } } else { @@ -155,7 +155,7 @@ function Invoke-ListScheduledItemDetails { try { $ParsedResults = $Result.Results | ConvertFrom-Json -ErrorAction Stop } catch { - Write-LogMessage -API $APIName -message "Failed to parse result as JSON: $_" -Sev 'Warning' + Write-LogMessage -API $APIName -message "Failed to parse result as JSON: $_" -sev 'Warn' # On failure, keep as string $ParsedResults = $Result.Results } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecApiClient.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecApiClient.ps1 index c38ece6e144c..eb7dc7273680 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecApiClient.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecApiClient.ps1 @@ -192,7 +192,7 @@ function Invoke-ExecApiClient { $Body = @{ Results = "API client $ClientId not found or not a valid CIPP-API application" } } } catch { - Write-LogMessage -headers $Request.Headers -API 'ExecApiClient' -message "Failed to remove app registration for $ClientId" -Sev 'Warning' + Write-LogMessage -headers $Request.Headers -API 'ExecApiClient' -message "Failed to remove app registration for $ClientId" -sev 'Warn' } } default { diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecGDAPTrace.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecGDAPTrace.ps1 index d138555690a5..930c54b297d9 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecGDAPTrace.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecGDAPTrace.ps1 @@ -163,7 +163,7 @@ function Invoke-ExecAccessTest { # Filter didn't work, try direct lookup by UPN (works if UPN is unique identifier) $User = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/users/$UPN" -tenantid $env:TenantID -NoAuthCheck $true } catch { - Write-LogMessage -Headers $Headers -API $APIName -message "Could not find user $UPN in partner tenant: $($_.Exception.Message)" -Sev 'Warning' + Write-LogMessage -Headers $Headers -API $APIName -message "Could not find user $UPN in partner tenant: $($_.Exception.Message)" -sev 'Warn' } # If user not found, return error @@ -212,7 +212,7 @@ function Invoke-ExecAccessTest { } } } catch { - Write-LogMessage -Headers $Headers -API $APIName -message "Could not get user group memberships: $($_.Exception.Message)" -Sev 'Warning' + Write-LogMessage -Headers $Headers -API $APIName -message "Could not get user group memberships: $($_.Exception.Message)" -sev 'Warn' } # ============================================================================ @@ -296,7 +296,7 @@ function Invoke-ExecAccessTest { }) } } catch { - Write-LogMessage -Headers $Headers -API $APIName -message "Could not get access assignments for relationship ${RelationshipName}: $($_.Exception.Message)" -Sev 'Warning' + Write-LogMessage -Headers $Headers -API $APIName -message "Could not get access assignments for relationship ${RelationshipName}: $($_.Exception.Message)" -sev 'Warn' } } @@ -346,7 +346,7 @@ function Invoke-ExecAccessTest { Write-LogMessage -Headers $Headers -API $APIName -message "Fetched $($AllGroups.Count) total groups, $($GroupLookup.Count) in lookup" -Sev 'Debug' } catch { - Write-LogMessage -Headers $Headers -API $APIName -message "Could not fetch all groups: $($_.Exception.Message). Will use fallback for missing groups." -Sev 'Warning' + Write-LogMessage -Headers $Headers -API $APIName -message "Could not fetch all groups: $($_.Exception.Message). Will use fallback for missing groups." -sev 'Warn' } # ======================================================================== @@ -387,12 +387,12 @@ function Invoke-ExecAccessTest { $GroupId = $Assignment.value.accessContainer.accessContainerId $Assignment = $Assignment.value } else { - Write-LogMessage -Headers $Headers -API $APIName -message "Access assignment missing accessContainer: $($Assignment | ConvertTo-Json -Compress)" -Sev 'Warning' + Write-LogMessage -Headers $Headers -API $APIName -message "Access assignment missing accessContainer: $($Assignment | ConvertTo-Json -Compress)" -sev 'Warn' continue } if ([string]::IsNullOrWhiteSpace($GroupId)) { - Write-LogMessage -Headers $Headers -API $APIName -message "Access assignment has empty accessContainerId: $($Assignment | ConvertTo-Json -Compress)" -Sev 'Warning' + Write-LogMessage -Headers $Headers -API $APIName -message "Access assignment has empty accessContainerId: $($Assignment | ConvertTo-Json -Compress)" -sev 'Warn' continue } @@ -405,7 +405,7 @@ function Invoke-ExecAccessTest { } if (-not $Roles -or $Roles.Count -eq 0) { - Write-LogMessage -Headers $Headers -API $APIName -message "Access assignment for group $GroupId has no roles assigned" -Sev 'Warning' + Write-LogMessage -Headers $Headers -API $APIName -message "Access assignment for group $GroupId has no roles assigned" -sev 'Warn' $Roles = @() } @@ -420,7 +420,7 @@ function Invoke-ExecAccessTest { id = $GroupId displayName = "Unknown Group ($GroupId)" } - Write-LogMessage -Headers $Headers -API $APIName -message "Group $GroupId not found in lookup, using fallback" -Sev 'Warning' + Write-LogMessage -Headers $Headers -API $APIName -message "Group $GroupId not found in lookup, using fallback" -sev 'Warn' } # Process the assignment even if group lookup failed - we still have the group ID and roles @@ -585,12 +585,12 @@ function Invoke-ExecAccessTest { } elseif ($Role -is [string]) { $RoleId = $Role } else { - Write-LogMessage -Headers $Headers -API $APIName -message "Role object missing roleDefinitionId: $($Role | ConvertTo-Json -Compress)" -Sev 'Warning' + Write-LogMessage -Headers $Headers -API $APIName -message "Role object missing roleDefinitionId: $($Role | ConvertTo-Json -Compress)" -sev 'Warn' continue } if ([string]::IsNullOrWhiteSpace($RoleId)) { - Write-LogMessage -Headers $Headers -API $APIName -message "Role has empty roleDefinitionId for group $GroupId" -Sev 'Warning' + Write-LogMessage -Headers $Headers -API $APIName -message "Role has empty roleDefinitionId for group $GroupId" -sev 'Warn' continue } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ListCustomVariables.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ListCustomVariables.ps1 index d512c090da28..e77018c09ffc 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ListCustomVariables.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ListCustomVariables.ps1 @@ -242,7 +242,7 @@ function Invoke-ListCustomVariables { } } } catch { - Write-LogMessage -API $APIName -message "Could not retrieve tenant-specific variables for $TenantFilter : $($_.Exception.Message)" -Sev 'Warning' + Write-LogMessage -API $APIName -message "Could not retrieve tenant-specific variables for $TenantFilter : $($_.Exception.Message)" -sev 'Warn' } } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-DeployContactTemplates.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-DeployContactTemplates.ps1 index 84b79cff13be..d3a188c2211c 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-DeployContactTemplates.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-DeployContactTemplates.ps1 @@ -24,7 +24,7 @@ Function Invoke-DeployContactTemplates { if ($TenantItem.value) { $SelectedTenants.Add($TenantItem.value) } else { - Write-LogMessage -headers $Headers -API $APIName -message "Tenant item missing value property: $($TenantItem | ConvertTo-Json -Compress)" -Sev 'Warning' + Write-LogMessage -headers $Headers -API $APIName -message "Tenant item missing value property: $($TenantItem | ConvertTo-Json -Compress)" -sev 'Warn' } } @@ -46,7 +46,7 @@ Function Invoke-DeployContactTemplates { if ($TemplateItem.value) { $ContactTemplates.Add($TemplateItem.value) } else { - Write-LogMessage -headers $Headers -API $APIName -message "Template item missing value property: $($TemplateItem | ConvertTo-Json -Compress)" -Sev 'Warning' + Write-LogMessage -headers $Headers -API $APIName -message "Template item missing value property: $($TemplateItem | ConvertTo-Json -Compress)" -sev 'Warn' } } } else { @@ -74,7 +74,7 @@ Function Invoke-DeployContactTemplates { $ContactExists = $ExistingContacts | Where-Object { $_.ExternalEmailAddress -eq $ContactTemplate.email } if ($ContactExists) { - Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Contact with email '$($ContactTemplate.email)' already exists in tenant $TenantFilter" -Sev 'Warning' + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Contact with email '$($ContactTemplate.email)' already exists in tenant $TenantFilter" -sev 'Warn' "Contact '$($ContactTemplate.displayName)' with email '$($ContactTemplate.email)' already exists in tenant $TenantFilter" continue } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-ListContactTemplates.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-ListContactTemplates.ps1 index 9abe8a741a25..05ae42c1ed52 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-ListContactTemplates.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-ListContactTemplates.ps1 @@ -37,7 +37,7 @@ Function Invoke-ListContactTemplates { } if (-not $Templates) { - Write-LogMessage -headers $Headers -API $APIName -message "Template with ID $RequestedID not found" -Sev 'Warning' + Write-LogMessage -headers $Headers -API $APIName -message "Template with ID $RequestedID not found" -sev 'Warn' return ([HttpResponseContext]@{ StatusCode = [HttpStatusCode]::NotFound Body = @{ Error = "Template with ID $RequestedID not found" } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecHVEUser.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecHVEUser.ps1 index bdb783995d9b..5c1b8e2e52d0 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecHVEUser.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecHVEUser.ps1 @@ -75,7 +75,7 @@ function Invoke-ExecHVEUser { } catch { $ErrorMessage = Get-CippException -Exception $_ $Message = "Failed to exclude from CA policy '$($Policy.displayName)': $($ErrorMessage.NormalizedError)" - Write-LogMessage -Headers $Headers -API $APIName -tenant $Tenant -message $Message -Sev 'Warning' -LogData $ErrorMessage + Write-LogMessage -Headers $Headers -API $APIName -tenant $Tenant -message $Message -sev 'Warn' -LogData $ErrorMessage $Results.Add($Message) } } @@ -85,7 +85,7 @@ function Invoke-ExecHVEUser { } catch { $ErrorMessage = Get-CippException -Exception $_ $Message = "Failed to check/update Conditional Access policies: $($ErrorMessage.NormalizedError)" - Write-LogMessage -Headers $Headers -API $APIName -tenant $Tenant -message $Message -Sev 'Warning' -LogData $ErrorMessage + Write-LogMessage -Headers $Headers -API $APIName -tenant $Tenant -message $Message -sev 'Warn' -LogData $ErrorMessage $Results.Add($Message) } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecModifyCalPerms.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecModifyCalPerms.ps1 index 562fb1dd2f92..24f88d8cd971 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecModifyCalPerms.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecModifyCalPerms.ps1 @@ -105,7 +105,7 @@ function Invoke-ExecModifyCalPerms { } if ($Results.Count -eq 0) { - Write-LogMessage -headers $Headers -API $APIName -message 'No results were generated from the operation' -Sev 'Warning' + Write-LogMessage -headers $Headers -API $APIName -message 'No results were generated from the operation' -sev 'Warn' $Results.Add('No results were generated from the operation. Please check the logs for more details.') $HasErrors = $true } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecModifyContactPerms.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecModifyContactPerms.ps1 index 75877cbbdb9c..ad659857cc75 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecModifyContactPerms.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecModifyContactPerms.ps1 @@ -104,7 +104,7 @@ function Invoke-ExecModifyContactPerms { } if ($Results.Count -eq 0) { - Write-LogMessage -headers $Headers -API $APIName -message 'No results were generated from the operation' -Sev 'Warning' + Write-LogMessage -headers $Headers -API $APIName -message 'No results were generated from the operation' -sev 'Warn' $Results.Add('No results were generated from the operation. Please check the logs for more details.') $HasErrors = $true } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecModifyMBPerms.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecModifyMBPerms.ps1 index 4d768e96cc1a..5e051053f90b 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecModifyMBPerms.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecModifyMBPerms.ps1 @@ -293,7 +293,7 @@ Function Invoke-ExecModifyMBPerms { } if ($CmdletArray.Count -eq 0) { - Write-LogMessage -headers $Request.Headers -API $APINAME -message 'No valid cmdlets to process' -Sev 'Warning' -tenant $TenantFilter + Write-LogMessage -headers $Request.Headers -API $APINAME -message 'No valid cmdlets to process' -sev 'Warn' -tenant $TenantFilter $body = [pscustomobject]@{'Results' = @("No valid permission changes to process") } return ([HttpResponseContext]@{ StatusCode = [HttpStatusCode]::OK @@ -327,7 +327,7 @@ Function Invoke-ExecModifyMBPerms { Write-LogMessage -headers $Request.Headers -API $APINAME -message "Success for operation $operationGuid`: $($metadata.ExpectedResult)" -Sev 'Info' -tenant $TenantFilter } } else { - Write-LogMessage -headers $Request.Headers -API $APINAME -message "Could not map result to operation. GUID: $operationGuid, Available GUIDs: $($GuidToMetadataMap.Keys -join ', ')" -Sev 'Warning' -tenant $TenantFilter + Write-LogMessage -headers $Request.Headers -API $APINAME -message "Could not map result to operation. GUID: $operationGuid, Available GUIDs: $($GuidToMetadataMap.Keys -join ', ')" -sev 'Warn' -tenant $TenantFilter # Fallback for unmapped results if ($result.error) { diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-AddJITAdminTemplate.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-AddJITAdminTemplate.ps1 index dd2de9d523e0..7f8777f94899 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-AddJITAdminTemplate.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-AddJITAdminTemplate.ps1 @@ -61,7 +61,7 @@ function Invoke-AddJITAdminTemplate { Write-LogMessage -headers $Headers -API $APIName -message "Unset default flag for existing template: $($data.templateName)" -Sev 'Info' } } catch { - Write-LogMessage -headers $Headers -API $APIName -message "Failed to update existing template: $($_.Exception.Message)" -Sev 'Warning' + Write-LogMessage -headers $Headers -API $APIName -message "Failed to update existing template: $($_.Exception.Message)" -sev 'Warn' } } } @@ -104,7 +104,7 @@ function Invoke-AddJITAdminTemplate { if (![string]::IsNullOrWhiteSpace($Request.Body.defaultUserName)) { $TemplateObject.defaultUserName = $Request.Body.defaultUserName } - + # defaultDomain is only saved for specific tenant templates (not AllTenants) if ($TenantFilter -ne 'AllTenants' -and $Request.Body.defaultDomain) { if ($Request.Body.defaultDomain -is [string]) { diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-CIPPOffboardingJob.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-CIPPOffboardingJob.ps1 index 6da71f0a98bf..b36cf8ea3987 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-CIPPOffboardingJob.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-CIPPOffboardingJob.ps1 @@ -16,7 +16,6 @@ function Invoke-CIPPOffboardingJob { } Write-Information "Starting offboarding job for $Username in tenant $TenantFilter" - Write-LogMessage -API 'Offboarding' -tenant $TenantFilter -message "Starting offboarding orchestration for user $Username" -sev Info # Get user information needed for various tasks $User = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/users/$($Username)?`$select=id,displayName,onPremisesSyncEnabled,onPremisesImmutableId" -tenantid $TenantFilter @@ -36,6 +35,7 @@ function Invoke-CIPPOffboardingJob { username = $Username userid = $UserID APIName = $APIName + Headers = $Headers } } @{ @@ -46,6 +46,7 @@ function Invoke-CIPPOffboardingJob { DisplayName = $DisplayName UserID = $Username APIName = $APIName + Headers = $Headers } } @{ @@ -56,6 +57,7 @@ function Invoke-CIPPOffboardingJob { userid = $Username AccountEnabled = $false APIName = $APIName + Headers = $Headers } } @{ @@ -66,6 +68,7 @@ function Invoke-CIPPOffboardingJob { UserID = $Username hidefromgal = $true APIName = $APIName + Headers = $Headers } } @{ @@ -76,6 +79,7 @@ function Invoke-CIPPOffboardingJob { tenantFilter = $TenantFilter APIName = $APIName Username = $Username + Headers = $Headers } } @{ @@ -87,6 +91,7 @@ function Invoke-CIPPOffboardingJob { tenantFilter = $TenantFilter APIName = $APIName RemoveAllRules = $true + Headers = $Headers } } @{ @@ -97,6 +102,7 @@ function Invoke-CIPPOffboardingJob { username = $Username tenantFilter = $TenantFilter APIName = $APIName + Headers = $Headers } } @{ @@ -107,6 +113,7 @@ function Invoke-CIPPOffboardingJob { Username = $Username TenantFilter = $TenantFilter APIName = $APIName + Headers = $Headers } } @{ @@ -119,6 +126,7 @@ function Invoke-CIPPOffboardingJob { ExternalMessage = $Options.OOO APIName = $APIName state = 'Enabled' + Headers = $Headers } } @{ @@ -131,6 +139,7 @@ function Invoke-CIPPOffboardingJob { Forward = $Options.forward.value KeepCopy = [bool]$Options.KeepCopy APIName = $APIName + Headers = $Headers } } @{ @@ -142,6 +151,7 @@ function Invoke-CIPPOffboardingJob { tenantFilter = $TenantFilter Disable = $true APIName = $APIName + Headers = $Headers } } @{ @@ -152,6 +162,7 @@ function Invoke-CIPPOffboardingJob { userid = $Username OnedriveAccessUser = $Options.OnedriveAccess APIName = $APIName + Headers = $Headers } } @{ @@ -164,6 +175,7 @@ function Invoke-CIPPOffboardingJob { Automap = $false AccessRights = @('FullAccess') APIName = $APIName + Headers = $Headers } } @{ @@ -176,6 +188,7 @@ function Invoke-CIPPOffboardingJob { Automap = $true AccessRights = @('FullAccess') APIName = $APIName + Headers = $Headers } } @{ @@ -186,6 +199,7 @@ function Invoke-CIPPOffboardingJob { TenantFilter = $TenantFilter UseCache = $true APIName = $APIName + Headers = $Headers } } @{ @@ -196,6 +210,7 @@ function Invoke-CIPPOffboardingJob { TenantFilter = $TenantFilter UseCache = $true APIName = $APIName + Headers = $Headers } } @{ @@ -207,6 +222,7 @@ function Invoke-CIPPOffboardingJob { username = $Username MailboxType = 'Shared' APIName = $APIName + Headers = $Headers } } @{ @@ -215,6 +231,8 @@ function Invoke-CIPPOffboardingJob { Parameters = @{ UserPrincipalName = $Username TenantFilter = $TenantFilter + APIName = $APIName + Headers = $Headers } } @{ @@ -225,6 +243,7 @@ function Invoke-CIPPOffboardingJob { username = $Username tenantFilter = $TenantFilter APIName = $APIName + Headers = $Headers } } @{ @@ -236,6 +255,7 @@ function Invoke-CIPPOffboardingJob { tenantFilter = $TenantFilter APIName = $APIName Schedule = $true + Headers = $Headers } } @{ @@ -247,6 +267,7 @@ function Invoke-CIPPOffboardingJob { TenantFilter = $TenantFilter User = $User APIName = $APIName + Headers = $Headers } } @{ @@ -257,6 +278,7 @@ function Invoke-CIPPOffboardingJob { Username = $Username TenantFilter = $TenantFilter APIName = $APIName + Headers = $Headers } } ) @@ -273,7 +295,7 @@ function Invoke-CIPPOffboardingJob { } if ($Batch.Count -eq 0) { - Write-LogMessage -API 'Offboarding' -tenant $TenantFilter -message "No offboarding tasks selected for user $Username" -sev Warning + Write-LogMessage -API $APIName -tenant $TenantFilter -message "No offboarding tasks selected for user $Username" -sev Warning return "No offboarding tasks were selected for $Username" } @@ -288,20 +310,19 @@ function Invoke-CIPPOffboardingJob { } # Add post-execution handler if TaskInfo is provided (from scheduled task) - if ($TaskInfo) { - $InputObject | Add-Member -NotePropertyName PostExecution -NotePropertyValue @{ - FunctionName = 'CIPPOffboardingComplete' - Parameters = @{ - TaskInfo = $TaskInfo - TenantFilter = $TenantFilter - Username = $Username - } + $InputObject | Add-Member -NotePropertyName PostExecution -NotePropertyValue @{ + FunctionName = 'CIPPOffboardingComplete' + Parameters = @{ + TaskInfo = $TaskInfo ?? $null + TenantFilter = $TenantFilter + Username = $Username + Headers = $Headers } } $InstanceId = Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Depth 10 -Compress) Write-Information "Started offboarding job for $Username with ID = '$InstanceId'" - Write-LogMessage -API 'Offboarding' -tenant $TenantFilter -message "Started offboarding job for $Username with $($Batch.Count) tasks. Instance ID: $InstanceId" -sev Info + Write-LogMessage -API $APIName -tenant $TenantFilter -message "Started offboarding job for $Username with $($Batch.Count) tasks. Instance ID: $InstanceId" -sev Info return "Offboarding job started for $Username with $($Batch.Count) tasks" diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-EditJITAdminTemplate.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-EditJITAdminTemplate.ps1 index 35bbd95139ca..868208b51e87 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-EditJITAdminTemplate.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-EditJITAdminTemplate.ps1 @@ -76,7 +76,7 @@ function Invoke-EditJITAdminTemplate { Write-LogMessage -headers $Headers -API $APIName -message "Unset default flag for existing template: $($data.templateName)" -Sev 'Info' } } catch { - Write-LogMessage -headers $Headers -API $APIName -message "Failed to update existing template: $($_.Exception.Message)" -Sev 'Warning' + Write-LogMessage -headers $Headers -API $APIName -message "Failed to update existing template: $($_.Exception.Message)" -sev 'Warn' } } } @@ -121,7 +121,7 @@ function Invoke-EditJITAdminTemplate { if (![string]::IsNullOrWhiteSpace($Request.Body.defaultUserName)) { $TemplateObject.defaultUserName = $Request.Body.defaultUserName } - + # defaultDomain is only saved for specific tenant templates (not AllTenants) if ($TenantFilter -ne 'AllTenants' -and $Request.Body.defaultDomain) { if ($Request.Body.defaultDomain -is [string]) { diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListJITAdminTemplates.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListJITAdminTemplates.ps1 index aa5ad886758e..3a99bf6d7337 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListJITAdminTemplates.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListJITAdminTemplates.ps1 @@ -34,7 +34,7 @@ function Invoke-ListJITAdminTemplates { $data | Add-Member -NotePropertyName 'RowKey' -NotePropertyValue $row.RowKey -Force $data } catch { - Write-LogMessage -headers $Headers -API $APIName -message "Failed to process JIT Admin template: $($row.RowKey) - $($_.Exception.Message)" -Sev 'Warning' + Write-LogMessage -headers $Headers -API $APIName -message "Failed to process JIT Admin template: $($row.RowKey) - $($_.Exception.Message)" -sev 'Warn' } } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-RemoveJITAdminTemplate.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-RemoveJITAdminTemplate.ps1 index e0ac56a8f294..ae25c9b97c18 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-RemoveJITAdminTemplate.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-RemoveJITAdminTemplate.ps1 @@ -13,7 +13,7 @@ function Invoke-RemoveJITAdminTemplate { try { $ID = $Request.Query.ID ?? $Request.Body.ID - + if ([string]::IsNullOrWhiteSpace($ID)) { throw 'ID is required' } @@ -29,7 +29,7 @@ function Invoke-RemoveJITAdminTemplate { $StatusCode = [HttpStatusCode]::OK } else { $Result = "JIT Admin Template with ID $ID not found" - Write-LogMessage -headers $Headers -API $APIName -message $Result -Sev 'Warning' + Write-LogMessage -headers $Headers -API $APIName -message $Result -sev 'Warn' $StatusCode = [HttpStatusCode]::NotFound } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-RemoveUserDefaultTemplate.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-RemoveUserDefaultTemplate.ps1 index c97f52617d00..3efb129e21eb 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-RemoveUserDefaultTemplate.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-RemoveUserDefaultTemplate.ps1 @@ -24,7 +24,7 @@ function Invoke-RemoveUserDefaultTemplate { $StatusCode = [HttpStatusCode]::OK } else { $Result = "User Default Template with ID $ID not found" - Write-LogMessage -headers $Headers -API $APIName -message $Result -Sev 'Warning' + Write-LogMessage -headers $Headers -API $APIName -message $Result -sev 'Warn' $StatusCode = [HttpStatusCode]::NotFound } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-AddSafeLinksPolicyFromTemplate.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-AddSafeLinksPolicyFromTemplate.ps1 index a48db0eaccaf..2c02b5b70942 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-AddSafeLinksPolicyFromTemplate.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-AddSafeLinksPolicyFromTemplate.ps1 @@ -82,13 +82,13 @@ Function Invoke-AddSafeLinksPolicyFromTemplate { # Check if policy already exists if (Test-PolicyExists -TenantFilter $TenantFilter -PolicyName $PolicyName) { - Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Policy '$PolicyName' already exists" -Sev 'Warning' + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Policy '$PolicyName' already exists" -sev 'Warn' return "Policy '$PolicyName' already exists in tenant $TenantFilter" } # Check if rule already exists if (Test-RuleExists -TenantFilter $TenantFilter -RuleName $RuleName) { - Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Rule '$RuleName' already exists" -Sev 'Warning' + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Rule '$RuleName' already exists" -sev 'Warn' return "Rule '$RuleName' already exists in tenant $TenantFilter" } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ExecDeleteSafeLinksPolicy.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ExecDeleteSafeLinksPolicy.ps1 index f3fa33bcaa46..4f124a557e9a 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ExecDeleteSafeLinksPolicy.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ExecDeleteSafeLinksPolicy.ps1 @@ -39,7 +39,7 @@ function Invoke-ExecDeleteSafeLinksPolicy { catch { $ErrorMessage = Get-CippException -Exception $_ $ResultMessages.Add("Failed to delete SafeLinks rule '$RuleName'. Error: $($ErrorMessage.NormalizedError)") | Out-Null - Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Failed to delete SafeLinks rule '$RuleName'. Error: $($ErrorMessage.NormalizedError)" -Sev 'Warning' + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Failed to delete SafeLinks rule '$RuleName'. Error: $($ErrorMessage.NormalizedError)" -sev 'Warn' } } else { @@ -66,7 +66,7 @@ function Invoke-ExecDeleteSafeLinksPolicy { catch { $ErrorMessage = Get-CippException -Exception $_ $ResultMessages.Add("Failed to delete SafeLinks policy '$PolicyName'. Error: $($ErrorMessage.NormalizedError)") | Out-Null - Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Failed to delete SafeLinks policy '$PolicyName'. Error: $($ErrorMessage.NormalizedError)" -Sev 'Warning' + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Failed to delete SafeLinks policy '$PolicyName'. Error: $($ErrorMessage.NormalizedError)" -sev 'Warn' } } else { diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ExecNewSafeLinksPolicy.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ExecNewSafeLinksPolicy.ps1 index dbf3790b3090..dafc18eb2dcc 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ExecNewSafeLinksPolicy.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ExecNewSafeLinksPolicy.ps1 @@ -124,13 +124,13 @@ function Invoke-ExecNewSafeLinksPolicy { try { # Check if policy already exists if (Test-PolicyExists -TenantFilter $TenantFilter -PolicyName $PolicyName) { - Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Policy '$PolicyName' already exists" -Sev 'Warning' + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Policy '$PolicyName' already exists" -sev 'Warn' return "Policy '$PolicyName' already exists in tenant $TenantFilter" } # Check if rule already exists if (Test-RuleExists -TenantFilter $TenantFilter -RuleName $RuleName) { - Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Rule '$RuleName' already exists" -Sev 'Warning' + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Rule '$RuleName' already exists" -sev 'Warn' return "Rule '$RuleName' already exists in tenant $TenantFilter" } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ListSafeLinksPolicy.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ListSafeLinksPolicy.ps1 index 8c4ec6595652..e679b579b329 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ListSafeLinksPolicy.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ListSafeLinksPolicy.ps1 @@ -179,7 +179,7 @@ Function Invoke-ListSafeLinksPolicy { $BuiltInOnlyConfigs = ($SortedOutput | Where-Object { $_.ConfigurationStatus -like "*Built-In Rule Only*" }).Count if ($PolicyOnlyConfigs -gt 0 -or $RuleOnlyConfigs -gt 0) { - Write-LogMessage -headers $Headers -API $APIName -message "Found $($PolicyOnlyConfigs + $RuleOnlyConfigs) orphaned SafeLinks configurations that may need attention" -Sev 'Warning' + Write-LogMessage -headers $Headers -API $APIName -message "Found $($PolicyOnlyConfigs + $RuleOnlyConfigs) orphaned SafeLinks configurations that may need attention" -sev 'Warn' } $StatusCode = [HttpStatusCode]::OK diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ListSafeLinksPolicyDetails.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ListSafeLinksPolicyDetails.ps1 index 89840a6a07ed..5f8167531d3e 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ListSafeLinksPolicyDetails.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ListSafeLinksPolicyDetails.ps1 @@ -43,7 +43,7 @@ function Invoke-ListSafeLinksPolicyDetails { catch { $ErrorMessage = Get-CippException -Exception $_ $LogMessages.Add("Failed to retrieve details for SafeLinks policy '$PolicyName'. Error: $($ErrorMessage.NormalizedError)") | Out-Null - Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Failed to retrieve details for SafeLinks policy '$PolicyName'. Error: $($ErrorMessage.NormalizedError)" -Sev 'Warning' + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Failed to retrieve details for SafeLinks policy '$PolicyName'. Error: $($ErrorMessage.NormalizedError)" -sev 'Warn' $Result.PolicyError = "Failed to retrieve: $($ErrorMessage.NormalizedError)" } } @@ -72,7 +72,7 @@ function Invoke-ListSafeLinksPolicyDetails { catch { $ErrorMessage = Get-CippException -Exception $_ $LogMessages.Add("Failed to retrieve details for SafeLinks rule '$RuleName'. Error: $($ErrorMessage.NormalizedError)") | Out-Null - Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Failed to retrieve details for SafeLinks rule '$RuleName'. Error: $($ErrorMessage.NormalizedError)" -Sev 'Warning' + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Failed to retrieve details for SafeLinks rule '$RuleName'. Error: $($ErrorMessage.NormalizedError)" -sev 'Warn' $Result.RuleError = "Failed to retrieve: $($ErrorMessage.NormalizedError)" } } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Application Approval/Invoke-ExecCreateAppTemplate.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Application Approval/Invoke-ExecCreateAppTemplate.ps1 index 23315d7eb700..520536881638 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Application Approval/Invoke-ExecCreateAppTemplate.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Application Approval/Invoke-ExecCreateAppTemplate.ps1 @@ -123,7 +123,7 @@ function Invoke-ExecCreateAppTemplate { $Permissions = @($DelegateResourceAccess) + @($ApplicationResourceAccess) | Where-Object { $_ -ne $null } if ($Permissions.Count -eq 0) { - Write-LogMessage -headers $Request.headers -API $APINAME -message "No permissions found for $AppId via any method" -Sev 'Warning' + Write-LogMessage -headers $Request.headers -API $APINAME -message "No permissions found for $AppId via any method" -sev 'Warn' } else { Write-LogMessage -headers $Request.headers -API $APINAME -message "Extracted $($Permissions.Count) resource permission(s) from service principal grants" -Sev 'Info' } @@ -245,7 +245,7 @@ function Invoke-ExecCreateAppTemplate { }) $RequestIndex++ } else { - Write-LogMessage -headers $Request.headers -API $APINAME -message "Service principal not found in tenant for appId: $ResourceAppId" -Sev 'Warning' + Write-LogMessage -headers $Request.headers -API $APINAME -message "Service principal not found in tenant for appId: $ResourceAppId" -sev 'Warn' } } @@ -274,7 +274,7 @@ function Invoke-ExecCreateAppTemplate { $ResourceSP = $SPLookup[$ResourceAppId] if (!$ResourceSP) { - Write-LogMessage -headers $Request.headers -API $APINAME -message "Service principal not found for appId: $ResourceAppId - skipping permission translation" -Sev 'Warning' + Write-LogMessage -headers $Request.headers -API $APINAME -message "Service principal not found for appId: $ResourceAppId - skipping permission translation" -sev 'Warn' continue } @@ -291,7 +291,7 @@ function Invoke-ExecCreateAppTemplate { } [void]$AppPerms.Add($PermObj) } else { - Write-LogMessage -headers $Request.headers -API $APINAME -message "Application permission $($Access.id) not found in $ResourceAppId appRoles" -Sev 'Warning' + Write-LogMessage -headers $Request.headers -API $APINAME -message "Application permission $($Access.id) not found in $ResourceAppId appRoles" -sev 'Warn' } } elseif ($Access.type -eq 'Scope') { Write-Information "Processing delegated permission with id $($Access.id) for resource appId $ResourceAppId" @@ -364,14 +364,14 @@ function Invoke-ExecCreateAppTemplate { $PermissionSetId = $TemplateData.PermissionSetId Write-LogMessage -headers $Request.headers -API $APINAME -message "Found existing permission set ID: $PermissionSetId in template" -Sev 'Info' } else { - Write-LogMessage -headers $Request.headers -API $APINAME -message 'Existing template found but has no PermissionSetId' -Sev 'Warning' + Write-LogMessage -headers $Request.headers -API $APINAME -message 'Existing template found but has no PermissionSetId' -sev 'Warn' } break } } } catch { # Ignore lookup errors - Write-LogMessage -headers $Request.headers -API $APINAME -message "Error during template lookup: $($_.Exception.Message)" -Sev 'Warning' + Write-LogMessage -headers $Request.headers -API $APINAME -message "Error during template lookup: $($_.Exception.Message)" -sev 'Warn' } } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Tenant/Invoke-ListTenants.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Tenant/Invoke-ListTenants.ps1 index c0fa299b5466..e114d1a32977 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Tenant/Invoke-ListTenants.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Tenant/Invoke-ListTenants.ps1 @@ -94,7 +94,7 @@ function Invoke-ListTenants { try { $Tenant | Add-Member -MemberType NoteProperty -Name 'offboardingDefaults' -Value ($TenantDefaults.Value | ConvertFrom-Json) -Force } catch { - Write-LogMessage -headers $Headers -API $APIName -message "Failed to parse offboarding defaults for tenant $($Tenant.customerId): $($_.Exception.Message)" -Sev 'Warning' + Write-LogMessage -headers $Headers -API $APIName -message "Failed to parse offboarding defaults for tenant $($Tenant.customerId): $($_.Exception.Message)" -sev 'Warn' $Tenant | Add-Member -MemberType NoteProperty -Name 'offboardingDefaults' -Value $null -Force } } else { diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/GDAP/Invoke-ExecGDAPRoleTemplate.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/GDAP/Invoke-ExecGDAPRoleTemplate.ps1 index 07018acad177..17f648740620 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/GDAP/Invoke-ExecGDAPRoleTemplate.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/GDAP/Invoke-ExecGDAPRoleTemplate.ps1 @@ -18,7 +18,7 @@ function Invoke-ExecGDAPRoleTemplate { if ($Request.Query.TemplateId) { $Template = $Templates | Where-Object -Property RowKey -EQ $Request.Query.TemplateId if (!$Template) { - Write-LogMessage -headers $Headers -API $APIName -message "GDAP role template '$($Request.Query.TemplateId)' not found" -Sev 'Warning' + Write-LogMessage -headers $Headers -API $APIName -message "GDAP role template '$($Request.Query.TemplateId)' not found" -sev 'Warn' $Body = @{} } else { Write-LogMessage -headers $Headers -API $APIName -message "Retrieved GDAP role template '$($Request.Query.TemplateId)'" -Sev 'Info' @@ -50,7 +50,7 @@ function Invoke-ExecGDAPRoleTemplate { $Template = $Templates | Where-Object -Property RowKey -EQ $OriginalRowKey if ($Template) { $RoleMappings = $Request.Body.RoleMappings - + # If the template ID is being changed, delete the old one and create a new one if ($OriginalRowKey -ne $NewRowKey) { Remove-AzDataTableEntity -Force @Table -Entity $Template @@ -68,7 +68,7 @@ function Invoke-ExecGDAPRoleTemplate { } } } else { - Write-LogMessage -headers $Headers -API $APIName -message "GDAP role template '$OriginalRowKey' not found for editing" -Sev 'Warning' + Write-LogMessage -headers $Headers -API $APIName -message "GDAP role template '$OriginalRowKey' not found for editing" -sev 'Warn' $Body = @{ Results = "Template $OriginalRowKey not found" } @@ -84,7 +84,7 @@ function Invoke-ExecGDAPRoleTemplate { Results = "Deleted template $RowKey" } } else { - Write-LogMessage -headers $Headers -API $APIName -message "GDAP role template '$RowKey' not found for deletion" -Sev 'Warning' + Write-LogMessage -headers $Headers -API $APIName -message "GDAP role template '$RowKey' not found for deletion" -sev 'Warn' $Body = @{ Results = "Template $RowKey not found" } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ExecUpdateDriftDeviation.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ExecUpdateDriftDeviation.ps1 index 63b639e3769e..90dcfc65b713 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ExecUpdateDriftDeviation.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ExecUpdateDriftDeviation.ps1 @@ -100,7 +100,7 @@ function Invoke-ExecUpdateDriftDeviation { Write-LogMessage -tenant $TenantFilter -user $request.headers.'x-ms-client-principal' -API $APINAME -message "Deleted Policy with ID $($ID)" -Sev 'Info' } else { "could not find policy with ID $($ID)" - Write-LogMessage -tenant $TenantFilter -user $request.headers.'x-ms-client-principal' -API $APINAME -message "Could not find Policy with ID $($ID) to delete for remediation" -Sev 'Warning' + Write-LogMessage -tenant $TenantFilter -user $request.headers.'x-ms-client-principal' -API $APINAME -message "Could not find Policy with ID $($ID) to delete for remediation" -sev 'Warn' } diff --git a/Modules/CIPPCore/Public/Entrypoints/Invoke-ExecListAppId.ps1 b/Modules/CIPPCore/Public/Entrypoints/Invoke-ExecListAppId.ps1 index 6c0581ad0c8e..27a369092a08 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Invoke-ExecListAppId.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Invoke-ExecListAppId.ps1 @@ -83,13 +83,13 @@ function Invoke-ExecListAppId { Invoke-GraphRequest -Method PATCH -Url "https://graph.microsoft.com/v1.0/applications/$($AppResponse.body.id)" -Body $AppUpdateBody -tenantid $env:TenantID -NoAuthCheck $true Write-LogMessage -message "Updated redirect URIs for application $($env:ApplicationID) to include $NewRedirectUri" -Sev 'Info' } catch { - Write-LogMessage -message "Failed to update redirect URIs for application $($env:ApplicationID)" -LogData (Get-CippException -Exception $_) -Sev 'Warning' + Write-LogMessage -message "Failed to update redirect URIs for application $($env:ApplicationID)" -LogData (Get-CippException -Exception $_) -sev 'Warn' } } } } } catch { - Write-LogMessage -message 'Failed to retrieve organization info and authenticated user' -LogData (Get-CippException -Exception $_) -Sev 'Warning' + Write-LogMessage -message 'Failed to retrieve organization info and authenticated user' -LogData (Get-CippException -Exception $_) -sev 'Warn' } $Results = @{ diff --git a/Modules/CIPPCore/Public/Entrypoints/Invoke-ListLogs.ps1 b/Modules/CIPPCore/Public/Entrypoints/Invoke-ListLogs.ps1 index 8c4d4953bdf0..bd708947c66f 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Invoke-ListLogs.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Invoke-ListLogs.ps1 @@ -82,7 +82,7 @@ function Invoke-ListLogs { } } else { if ($request.Query.Filter -eq 'True') { - $LogLevel = if ($Request.Query.Severity) { ($Request.query.Severity).split(',') } else { 'Info', 'Warn', 'Error', 'Critical', 'Alert' } + $LogLevel = if ($Request.Query.Severity) { ($Request.query.Severity).split(',') } else { 'Info', 'Warn', 'Warning', 'Error', 'Critical', 'Alert' } $PartitionKey = $Request.Query.DateFilter $username = $Request.Query.User ?? '*' $TenantFilter = $Request.Query.Tenant @@ -102,7 +102,7 @@ function Invoke-ListLogs { $Filter = "PartitionKey eq '{0}'" -f (Get-Date -UFormat '%Y%m%d') } } else { - $LogLevel = 'Info', 'Warn', 'Error', 'Critical', 'Alert' + $LogLevel = 'Info', 'Warn', 'Warning', 'Error', 'Critical', 'Alert' $PartitionKey = Get-Date -UFormat '%Y%m%d' $username = '*' $TenantFilter = $null diff --git a/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-BackupRetentionCleanup.ps1 b/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-BackupRetentionCleanup.ps1 index ed00a0292aa1..b8edf3aedd94 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-BackupRetentionCleanup.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-BackupRetentionCleanup.ps1 @@ -64,7 +64,7 @@ function Start-BackupRetentionCleanup { $BlobDeletedCount++ Write-Host "Deleted blob: $BlobPath" } catch { - Write-LogMessage -API 'BackupRetentionCleanup' -message "Failed to delete blob $($Backup.Backup): $($_.Exception.Message)" -Sev 'Warning' + Write-LogMessage -API 'BackupRetentionCleanup' -message "Failed to delete blob $($Backup.Backup): $($_.Exception.Message)" -sev 'Warn' } } } @@ -124,7 +124,7 @@ function Start-BackupRetentionCleanup { $BlobDeletedCount++ Write-Host "Deleted blob: $BlobPath" } catch { - Write-LogMessage -API 'BackupRetentionCleanup' -message "Failed to delete blob $($Backup.Backup): $($_.Exception.Message)" -Sev 'Warning' + Write-LogMessage -API 'BackupRetentionCleanup' -message "Failed to delete blob $($Backup.Backup): $($_.Exception.Message)" -sev 'Warn' } } } diff --git a/Modules/CIPPCore/Public/New-CIPPRestoreTask.ps1 b/Modules/CIPPCore/Public/New-CIPPRestoreTask.ps1 index d750ce6cdea9..4ef75831b703 100644 --- a/Modules/CIPPCore/Public/New-CIPPRestoreTask.ps1 +++ b/Modules/CIPPCore/Public/New-CIPPRestoreTask.ps1 @@ -103,7 +103,7 @@ function New-CIPPRestoreTask { } } catch { $restorationStats['CustomVariables'].failed++ - Write-LogMessage -message "Failed to restore custom variable $($variable.RowKey): $($_.Exception.Message)" -Sev 'Warning' + Write-LogMessage -message "Failed to restore custom variable $($variable.RowKey): $($_.Exception.Message)" -sev 'Warn' $RestoreData.Add("Failed to restore custom variable $($variable.RowKey)") } } diff --git a/Modules/CIPPCore/Public/Remove-CIPPCalendarPermissions.ps1 b/Modules/CIPPCore/Public/Remove-CIPPCalendarPermissions.ps1 index 402eca0145b9..9a547a29f1ec 100644 --- a/Modules/CIPPCore/Public/Remove-CIPPCalendarPermissions.ps1 +++ b/Modules/CIPPCore/Public/Remove-CIPPCalendarPermissions.ps1 @@ -112,9 +112,9 @@ function Remove-CIPPCalendarPermissions { } catch { Write-Verbose "Failed to sync cache: $_" } - + $ErrorMsg = "Failed to remove $UserToRemove from calendar $($CalPermEntry.CalendarUPN): $($_.Exception.Message)" - Write-LogMessage -headers $Headers -API $APIName -message $ErrorMsg -Sev 'Warning' -tenant $TenantFilter + Write-LogMessage -headers $Headers -API $APIName -message $ErrorMsg -sev 'Warn' -tenant $TenantFilter $Results.Add($ErrorMsg) } } @@ -156,7 +156,7 @@ function Remove-CIPPCalendarPermissions { # Sync cache even on error (permission might not exist) $MailboxUPN = if ($CalendarIdentity -match '^([^:]+):') { $Matches[1] } else { $CalendarIdentity } $Folder = if ($CalendarIdentity -match ':\\(.+)$') { $Matches[1] } else { $FolderName } - + try { Sync-CIPPCalendarPermissionCache -TenantFilter $TenantFilter -MailboxIdentity $MailboxUPN -FolderName $Folder -User $UserToRemove -Action 'Remove' } catch { diff --git a/Modules/CIPPCore/Public/Remove-CIPPGroups.ps1 b/Modules/CIPPCore/Public/Remove-CIPPGroups.ps1 index f901ac82953e..bec2daa302b3 100644 --- a/Modules/CIPPCore/Public/Remove-CIPPGroups.ps1 +++ b/Modules/CIPPCore/Public/Remove-CIPPGroups.ps1 @@ -8,102 +8,111 @@ function Remove-CIPPGroups { $UserID ) - $BulkInfoRequests = [System.Collections.Generic.List[object]]::new() + try { - if (-not $UserID) { + $BulkInfoRequests = [System.Collections.Generic.List[object]]::new() + + if (-not $UserID) { + $BulkInfoRequests.Add(@{ + id = 'getUserID' + method = 'GET' + url = "users/$($Username)?`$select=id" + }) + } + + $BulkInfoRequests.Add( + @{ + id = 'getAllGroups' + method = 'GET' + url = "groups/?`$select=displayName,mailEnabled,id,groupTypes,assignedLicenses,onPremisesSyncEnabled,membershipRule&`$top=999" + }) $BulkInfoRequests.Add(@{ - id = 'getUserID' + id = 'getUserGroups' method = 'GET' - url = "users/$($Username)?`$select=id" + url = "users/$($UserID ?? $Username)/memberOf/microsoft.graph.group?`$select=id" }) - } - $BulkInfoRequests.Add( - @{ - id = 'getAllGroups' - method = 'GET' - url = "groups/?`$select=displayName,mailEnabled,id,groupTypes,assignedLicenses,onPremisesSyncEnabled,membershipRule&`$top=999" - }) - $BulkInfoRequests.Add(@{ - id = 'getUserGroups' - method = 'GET' - url = "users/$($UserID ?? $Username)/memberOf/microsoft.graph.group?`$select=id" - }) - - $BulkGetResults = New-GraphBulkRequest -tenantid $TenantFilter -Requests @($BulkInfoRequests) - - $UserInfo = ($BulkGetResults | Where-Object { $_.id -eq 'getUserID' }).body - if ($UserInfo) { - $UserID = $UserInfo.id - } - $AllGroups = ($BulkGetResults | Where-Object { $_.id -eq 'getAllGroups' }).body.value - $UserGroups = ($BulkGetResults | Where-Object { $_.id -eq 'getUserGroups' }).body.value - - #users/$($User.id)/memberOf/microsoft.graph.directoryRole - if (-not $UserGroups) { - $Returnval = "$($Username) is not a member of any groups." - Write-LogMessage -headers $Headers -API $APIName -message "$($Username) is not a member of any groups" -Sev 'Info' -tenant $TenantFilter - return $Returnval - } + $BulkGetResults = New-GraphBulkRequest -tenantid $TenantFilter -Requests @($BulkInfoRequests) + + $UserInfo = ($BulkGetResults | Where-Object { $_.id -eq 'getUserID' }).body + if ($UserInfo) { + $UserID = $UserInfo.id + } + $AllGroups = ($BulkGetResults | Where-Object { $_.id -eq 'getAllGroups' }).body.value + $UserGroups = ($BulkGetResults | Where-Object { $_.id -eq 'getUserGroups' }).body.value + + #users/$($User.id)/memberOf/microsoft.graph.directoryRole + if (-not $UserGroups) { + $Returnval = "$($Username) is not a member of any groups." + Write-LogMessage -headers $Headers -API $APIName -message "$($Username) is not a member of any groups" -Sev 'Info' -tenant $TenantFilter + return $Returnval + } + + Write-Information "Initiating group membership removal for user: $Username in tenant: $TenantFilter" + + # Initialize bulk request arrays and results + $BulkRequests = [System.Collections.Generic.List[object]]::new() + $ExoBulkRequests = [System.Collections.Generic.List[object]]::new() + $GraphLogs = [System.Collections.Generic.List[object]]::new() + $ExoLogs = [System.Collections.Generic.List[object]]::new() + $Results = [System.Collections.Generic.List[string]]::new() + + # Process each group and prepare bulk requests + foreach ($Group in $UserGroups) { + $GroupInfo = $AllGroups | Where-Object -Property id -EQ $Group.id + $GroupName = $GroupInfo.displayName + $IsMailEnabled = $GroupInfo.mailEnabled + $IsM365Group = $GroupInfo.groupTypes -and $GroupInfo.groupTypes -contains 'Unified' + $IsLicensed = $GroupInfo.assignedLicenses.Count -gt 0 + $IsDynamic = -not [string]::IsNullOrWhiteSpace($GroupInfo.membershipRule) - # Initialize bulk request arrays and results - $BulkRequests = [System.Collections.Generic.List[object]]::new() - $ExoBulkRequests = [System.Collections.Generic.List[object]]::new() - $GraphLogs = [System.Collections.Generic.List[object]]::new() - $ExoLogs = [System.Collections.Generic.List[object]]::new() - $Results = [System.Collections.Generic.List[string]]::new() - - # Process each group and prepare bulk requests - foreach ($Group in $UserGroups) { - $GroupInfo = $AllGroups | Where-Object -Property id -EQ $Group.id - $GroupName = $GroupInfo.displayName - $IsMailEnabled = $GroupInfo.mailEnabled - $IsM365Group = $GroupInfo.groupTypes -and $GroupInfo.groupTypes -contains 'Unified' - $IsLicensed = $GroupInfo.assignedLicenses.Count -gt 0 - $IsDynamic = -not [string]::IsNullOrWhiteSpace($GroupInfo.membershipRule) - - if ($IsLicensed) { - $Results.Add("Could not remove $Username from group '$GroupName' because it has assigned licenses. These groups are removed during the license removal step.") - Write-LogMessage -headers $Headers -API $APIName -message "Could not remove $Username from group '$GroupName' because it has assigned licenses. These groups are removed during the license removal step." -Sev 'Warning' -tenant $TenantFilter - } elseif ($IsDynamic) { - $Results.Add("Error: Could not remove $Username from group '$GroupName' because it is a Dynamic Group.") - Write-LogMessage -headers $Headers -API $APIName -message "Could not remove $Username from group '$GroupName' because it is a Dynamic Group." -Sev 'Warning' -tenant $TenantFilter - } elseif ($GroupInfo.onPremisesSyncEnabled) { - $Results.Add("Error: Could not remove $Username from group '$GroupName' because it is synced with Active Directory.") - Write-LogMessage -headers $Headers -API $APIName -message "Could not remove $Username from group '$GroupName' because it is synced with Active Directory." -Sev 'Warning' -tenant $TenantFilter - } else { - if ($IsM365Group -or (-not $IsMailEnabled)) { - # Use Graph API for M365 Groups and Security Groups - $BulkRequests.Add(@{ - id = "removeFromGroup-$($Group.id)" - method = 'DELETE' - url = "groups/$($Group.id)/members/$UserID/`$ref" - }) - $GraphLogs.Add(@{ - message = "Removed $Username from $GroupName" - id = "removeFromGroup-$($Group.id)" - groupName = $GroupName - }) - } elseif ($IsMailEnabled) { - # Use Exchange Online for Distribution Lists - $Params = @{ - Identity = $GroupName - Member = $UserID - BypassSecurityGroupManagerCheck = $true + if ($IsLicensed) { + $Results.Add("Could not remove $Username from group '$GroupName' because it has assigned licenses. These groups are removed during the license removal step.") + Write-LogMessage -headers $Headers -API $APIName -message "Could not remove $Username from group '$GroupName' because it has assigned licenses. These groups are removed during the license removal step." -sev 'Warn' -tenant $TenantFilter + } elseif ($IsDynamic) { + $Results.Add("Error: Could not remove $Username from group '$GroupName' because it is a Dynamic Group.") + Write-LogMessage -headers $Headers -API $APIName -message "Could not remove $Username from group '$GroupName' because it is a Dynamic Group." -sev 'Warn' -tenant $TenantFilter + } elseif ($GroupInfo.onPremisesSyncEnabled) { + $Results.Add("Error: Could not remove $Username from group '$GroupName' because it is synced with Active Directory.") + Write-LogMessage -headers $Headers -API $APIName -message "Could not remove $Username from group '$GroupName' because it is synced with Active Directory." -sev 'Warn' -tenant $TenantFilter + } else { + if ($IsM365Group -or (-not $IsMailEnabled)) { + # Use Graph API for M365 Groups and Security Groups + $BulkRequests.Add(@{ + id = "removeFromGroup-$($Group.id)" + method = 'DELETE' + url = "groups/$($Group.id)/members/$UserID/`$ref" + }) + $GraphLogs.Add(@{ + message = "Removed $Username from $GroupName" + id = "removeFromGroup-$($Group.id)" + groupName = $GroupName + }) + } elseif ($IsMailEnabled) { + # Use Exchange Online for Distribution Lists + $Params = @{ + Identity = $GroupName + Member = $UserID + BypassSecurityGroupManagerCheck = $true + } + $ExoBulkRequests.Add(@{ + CmdletInput = @{ + CmdletName = 'Remove-DistributionGroupMember' + Parameters = $Params + } + }) + $ExoLogs.Add(@{ + message = "Removed $Username from $GroupName" + target = $UserID + groupName = $GroupName + }) } - $ExoBulkRequests.Add(@{ - CmdletInput = @{ - CmdletName = 'Remove-DistributionGroupMember' - Parameters = $Params - } - }) - $ExoLogs.Add(@{ - message = "Removed $Username from $GroupName" - target = $UserID - groupName = $GroupName - }) } } + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -headers $Headers -API $APIName -message "Error preparing bulk group removal requests: $($ErrorMessage.NormalizedError)" -Sev 'Error' -tenant $TenantFilter -LogData $ErrorMessage + return "Error preparing bulk group removal requests: $($ErrorMessage.NormalizedError)" } # Execute Graph bulk requests @@ -124,8 +133,7 @@ function Remove-CIPPGroups { } } catch { $ErrorMessage = Get-CippException -Exception $_ - Write-LogMessage -headers $Headers -API $APIName -message "Error executing Graph bulk requests: $($ErrorMessage.NormalizedError)" -Sev 'Error' -tenant $TenantFilter -LogData $ErrorMessage - $Results.Add("Error executing bulk removal requests: $($ErrorMessage.NormalizedError)") + Write-Information "Error executing bulk Graph requests: $($ErrorMessage | ConvertTo-Json -Depth 5)" } } diff --git a/Modules/CIPPCore/Public/Remove-CIPPMailboxPermissions.ps1 b/Modules/CIPPCore/Public/Remove-CIPPMailboxPermissions.ps1 index 0b8927896be9..b3bbee435102 100644 --- a/Modules/CIPPCore/Public/Remove-CIPPMailboxPermissions.ps1 +++ b/Modules/CIPPCore/Public/Remove-CIPPMailboxPermissions.ps1 @@ -43,7 +43,7 @@ function Remove-CIPPMailboxPermissions { } } catch { $ErrorMsg = "Failed to remove permissions from $MailboxUPN for $AccessUser : $($_.Exception.Message)" - Write-LogMessage -headers $Headers -API $APIName -message $ErrorMsg -Sev 'Warning' -tenant $TenantFilter + Write-LogMessage -headers $Headers -API $APIName -message $ErrorMsg -sev 'Warn' -tenant $TenantFilter $Results.Add($ErrorMsg) } } diff --git a/Modules/CIPPCore/Public/Remove-CIPPMailboxRule.ps1 b/Modules/CIPPCore/Public/Remove-CIPPMailboxRule.ps1 index 1f470021a92b..9559f97eb0a7 100644 --- a/Modules/CIPPCore/Public/Remove-CIPPMailboxRule.ps1 +++ b/Modules/CIPPCore/Public/Remove-CIPPMailboxRule.ps1 @@ -46,7 +46,7 @@ function Remove-CIPPMailboxRule { try { Remove-CIPPDbItem -TenantFilter $TenantFilter -Type 'MailboxRules' -ItemId $RuleId } catch { - Write-LogMessage -headers $Headers -API $APIName -message "Rule deleted but failed to remove from cache: $($_.Exception.Message)" -Sev 'Warning' -tenant $TenantFilter + Write-LogMessage -headers $Headers -API $APIName -message "Rule deleted but failed to remove from cache: $($_.Exception.Message)" -sev 'Warn' -tenant $TenantFilter } return $Message diff --git a/Modules/CIPPCore/Public/Set-CIPPAssignedPolicy.ps1 b/Modules/CIPPCore/Public/Set-CIPPAssignedPolicy.ps1 index eadfeef81f39..680ac28c9adf 100644 --- a/Modules/CIPPCore/Public/Set-CIPPAssignedPolicy.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPAssignedPolicy.ps1 @@ -31,7 +31,7 @@ function Set-CIPPAssignedPolicy { Write-Host "Found assignment filter: $($MatchingFilter.displayName) with ID: $ResolvedFilterId" } else { $ErrorMessage = "No assignment filter found matching the name: $AssignmentFilterName. Policy assigned without filter." - Write-LogMessage -headers $Headers -API $APIName -message $ErrorMessage -Sev 'Warning' -tenant $TenantFilter + Write-LogMessage -headers $Headers -API $APIName -message $ErrorMessage -sev 'Warn' -tenant $TenantFilter Write-Host $ErrorMessage } } @@ -95,7 +95,7 @@ function Set-CIPPAssignedPolicy { if (-not $resolvedGroupIds -or $resolvedGroupIds.Count -eq 0) { $ErrorMessage = "No groups found matching the specified name(s): $GroupName. Policy not assigned." - Write-LogMessage -headers $Headers -API $APIName -message $ErrorMessage -Sev 'Warning' -tenant $TenantFilter + Write-LogMessage -headers $Headers -API $APIName -message $ErrorMessage -sev 'Warn' -tenant $TenantFilter throw $ErrorMessage } diff --git a/Modules/CIPPCore/Public/Set-CIPPMailboxRule.ps1 b/Modules/CIPPCore/Public/Set-CIPPMailboxRule.ps1 index e99ead09a19d..06dc1d126e59 100644 --- a/Modules/CIPPCore/Public/Set-CIPPMailboxRule.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPMailboxRule.ps1 @@ -32,7 +32,7 @@ Enabled = $EnabledValue } } catch { - Write-LogMessage -headers $Headers -API $APIName -message "Rule updated but failed to update cache: $($_.Exception.Message)" -Sev 'Warning' -tenant $TenantFilter + Write-LogMessage -headers $Headers -API $APIName -message "Rule updated but failed to update cache: $($_.Exception.Message)" -sev 'Warn' -tenant $TenantFilter } return "Successfully set mailbox rule $($RuleName) for $($Username) to $($State)d" diff --git a/Modules/CIPPCore/Public/Set-CIPPResetPassword.ps1 b/Modules/CIPPCore/Public/Set-CIPPResetPassword.ps1 index 1285bbf1f402..0513553aaa3c 100644 --- a/Modules/CIPPCore/Public/Set-CIPPResetPassword.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPResetPassword.ps1 @@ -30,7 +30,7 @@ function Set-CIPPResetPassword { } } catch { - Write-LogMessage -headers $Headers -API $APIName -message "Failed to create PwPush link, using plain password. Error: $($_.Exception.Message)" -Sev 'Warning' -tenant $TenantFilter + Write-LogMessage -headers $Headers -API $APIName -message "Failed to create PwPush link, using plain password. Error: $($_.Exception.Message)" -sev 'Warn' -tenant $TenantFilter } Write-LogMessage -headers $Headers -API $APIName -message "Successfully reset the password for $DisplayName, $($UserID). User must change password is set to $forceChangePasswordNextSignIn" -Sev 'Info' -tenant $TenantFilter diff --git a/Modules/CIPPCore/Public/Test-CIPPAccessTenant.ps1 b/Modules/CIPPCore/Public/Test-CIPPAccessTenant.ps1 index 51dda90f7ff4..271ce8a186b0 100644 --- a/Modules/CIPPCore/Public/Test-CIPPAccessTenant.ps1 +++ b/Modules/CIPPCore/Public/Test-CIPPAccessTenant.ps1 @@ -135,7 +135,7 @@ function Test-CIPPAccessTenant { Write-Warning "Found $($MissingRoles.Count) missing Organization Management roles in Exchange" $ExchangeStatus = $false $ExchangeTest = 'Connected to Exchange but missing permissions in Organization Management. This may impact the ability to manage Exchange features' - Write-LogMessage -headers $Headers -API $APINAME -tenant $tenant.defaultDomainName -message 'Tenant access check for Exchange failed: Missing Organization Management roles' -Sev 'Warning' -LogData $MissingOrgMgmtRoles + Write-LogMessage -headers $Headers -API $APINAME -tenant $tenant.defaultDomainName -message 'Tenant access check for Exchange failed: Missing Organization Management roles' -sev 'Warn' -LogData $MissingOrgMgmtRoles } else { Write-Warning 'All available Organization Management roles are present in Exchange' $ExchangeStatus = $true diff --git a/Modules/CIPPCore/Public/Webhooks/Invoke-CIPPGraphWebhookRenewal.ps1 b/Modules/CIPPCore/Public/Webhooks/Invoke-CIPPGraphWebhookRenewal.ps1 index 3bc4693fe6ed..9dd938033660 100644 --- a/Modules/CIPPCore/Public/Webhooks/Invoke-CIPPGraphWebhookRenewal.ps1 +++ b/Modules/CIPPCore/Public/Webhooks/Invoke-CIPPGraphWebhookRenewal.ps1 @@ -19,7 +19,7 @@ function Invoke-CippGraphWebhookRenewal { try { $TenantFilter = $UpdateSub.PartitionKey if ($Tenants.defaultDomainName -notcontains $TenantFilter -and $Tenants.customerId -notcontains $TenantFilter) { - Write-LogMessage -API 'Renew_Graph_Subscriptions' -message "Removing Subscription Renewal for $($UpdateSub.SubscriptionID) as tenant $TenantFilter is not in the tenant list." -Sev 'Warning' -tenant $TenantFilter + Write-LogMessage -API 'Renew_Graph_Subscriptions' -message "Removing Subscription Renewal for $($UpdateSub.SubscriptionID) as tenant $TenantFilter is not in the tenant list." -sev 'Warn' -tenant $TenantFilter Remove-AzDataTableEntity -Force @WebhookTable -Entity $UpdateSub continue } diff --git a/Modules/CippExtensions/Public/PwPush/New-PwPushLink.ps1 b/Modules/CippExtensions/Public/PwPush/New-PwPushLink.ps1 index ad58593c9676..1991b90598ae 100644 --- a/Modules/CippExtensions/Public/PwPush/New-PwPushLink.ps1 +++ b/Modules/CippExtensions/Public/PwPush/New-PwPushLink.ps1 @@ -55,7 +55,7 @@ function New-PwPushLink { 'Exception' = Get-CippException -Exception $_ } Write-LogMessage -API PwPush -Message "Failed to create a new PwPush link: $($_.Exception.Message)" -Sev 'Error' -LogData $LogData - Write-LogMessage -API PwPush -Message "Continuing without PwPush link due to error" -Sev 'Warning' + Write-LogMessage -API PwPush -Message "Continuing without PwPush link due to error" -sev 'Warn' return $false } } catch { From fdc5462252f492620db6abe3686261f0c513abf2 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Mon, 23 Feb 2026 00:11:46 -0500 Subject: [PATCH 492/503] Add Invoke-ListDBCache HTTP entrypoint Introduce Invoke-ListDBCache PowerShell function as an HTTP entrypoint for listing DB cache entries. Validates required query params (tenantFilter and type), returns 400 BadRequest when tenantFilter is missing, and when type is missing returns 400 with an AvailableTypes list derived from Get-CIPPDbItem -CountsOnly. When tenant exists, issues a New-CIPPDbRequest for the given tenantFilter and type and returns results in a 200 OK HttpResponseContext. --- .../HTTP Functions/Invoke-ListDBCache.ps1 | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ListDBCache.ps1 diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ListDBCache.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ListDBCache.ps1 new file mode 100644 index 000000000000..76268bd14dcb --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ListDBCache.ps1 @@ -0,0 +1,46 @@ +function Invoke-ListDBCache { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + CIPP.Core.Read + #> + [CmdletBinding()] + param ( + $Request, + $TriggerMetadata + ) + + $TenantFilter = $Request.Query.tenantFilter + $Type = $Request.Query.type + + if (-not $TenantFilter) { + return ([HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::BadRequest + Body = @{ Results = 'Error: tenantFilter query parameter is required' } + }) + } + + if (-not $Type) { + $Types = Get-CIPPDbItem -CountsOnly -TenantFilter $TenantFilter | Select-Object -ExpandProperty RowKey + $Types = $Types | ForEach-Object { $_ -replace '-Count$', '' } | Sort-Object + + return ([HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::BadRequest + Body = @{ + Results = 'Error: type query parameter is required' + AvailableTypes = $Types + } + }) + } + + $Tenant = Get-Tenants -TenantFilter $TenantFilter + if ($Tenant) { + $Results = New-CIPPDbRequest -TenantFilter $TenantFilter -Type $Type + } + + return ([HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::OK + Body = @{ Results = $Results } + }) +} From 641a37ad00345d96121dd4689d514d8da421ea9e Mon Sep 17 00:00:00 2001 From: James Tarran Date: Mon, 23 Feb 2026 12:26:24 +0000 Subject: [PATCH 493/503] Update Invoke-CIPPStandardUserSubmissions.ps1 Previously, $StateIsCorrect only checked a subset of policy fields, while drift detection compared all fields. This caused perpetual drift reports for states that remediation considered correct and would never act on. Changes: - Expanded $StateIsCorrect checks to include address array counts (.Count -eq 0) and rule state, fully matching drift detection fields - Use Remove-ReportSubmissionRule to clear the rule - Wrap Exchange address collection properties with @() in $CurrentValue to normalise the Exchange MultiValuedProperty type for consistent JSON serialisation - Fix $ExpectedValue address fields: inline `if { @() }` returns $null (empty arrays write nothing to the pipeline); use @(if { ... }) outer wrapping instead to correctly produce an empty array - Fix $ExpectedValue.RuleState condition to include $state -eq 'disable', preventing an incorrect State='Enabled' expectation when email is configured but the standard is set to disable - Normalise RuleState.State to 'Disabled' and SentTo to $null in $CurrentValue when no submission rule exists --- .../Invoke-CIPPStandardUserSubmissions.ps1 | 43 +++++++++++++------ 1 file changed, 30 insertions(+), 13 deletions(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardUserSubmissions.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardUserSubmissions.ps1 index 265959e7ce6a..6a949fad057f 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardUserSubmissions.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardUserSubmissions.ps1 @@ -70,8 +70,11 @@ function Invoke-CIPPStandardUserSubmissions { $PolicyIsCorrect = ($PolicyState.EnableReportToMicrosoft -eq $true) -and ($PolicyState.ReportJunkToCustomizedAddress -eq $false) -and ($PolicyState.ReportNotJunkToCustomizedAddress -eq $false) -and - ($PolicyState.ReportPhishToCustomizedAddress -eq $false) - $RuleIsCorrect = $true + ($PolicyState.ReportPhishToCustomizedAddress -eq $false) -and + ($PolicyState.ReportJunkAddresses.Count -eq 0) -and + ($PolicyState.ReportNotJunkAddresses.Count -eq 0) -and + ($PolicyState.ReportPhishAddresses.Count -eq 0) + $RuleIsCorrect = ($RuleState.length -eq 0) -or ($RuleState.State -ne 'Enabled') } else { $PolicyIsCorrect = ($PolicyState.EnableReportToMicrosoft -eq $true) -and ($PolicyState.ReportJunkToCustomizedAddress -eq $true) -and @@ -91,8 +94,11 @@ function Invoke-CIPPStandardUserSubmissions { $PolicyIsCorrect = ($PolicyState.EnableReportToMicrosoft -eq $false) -and ($PolicyState.ReportJunkToCustomizedAddress -eq $false) -and ($PolicyState.ReportNotJunkToCustomizedAddress -eq $false) -and - ($PolicyState.ReportPhishToCustomizedAddress -eq $false) - $RuleIsCorrect = $true + ($PolicyState.ReportPhishToCustomizedAddress -eq $false) -and + ($PolicyState.ReportJunkAddresses.Count -eq 0) -and + ($PolicyState.ReportNotJunkAddresses.Count -eq 0) -and + ($PolicyState.ReportPhishAddresses.Count -eq 0) + $RuleIsCorrect = ($RuleState.length -eq 0) -or ($RuleState.State -ne 'Enabled') } } @@ -132,8 +138,11 @@ function Invoke-CIPPStandardUserSubmissions { $PolicyParams = @{ EnableReportToMicrosoft = $false ReportJunkToCustomizedAddress = $false + ReportJunkAddresses = $null ReportNotJunkToCustomizedAddress = $false + ReportNotJunkAddresses = $null ReportPhishToCustomizedAddress = $false + ReportPhishAddresses = $null } } @@ -177,6 +186,14 @@ function Invoke-CIPPStandardUserSubmissions { Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to enable User Submission rule. Error: $($ErrorMessage.NormalizedError)" -sev Error } } + } elseif ($RuleState.length -gt 0 -and $RuleState.State -eq 'Enabled') { + try { + $null = New-ExoRequest -tenantid $Tenant -cmdlet 'Remove-ReportSubmissionRule' -cmdParams @{ Identity = 'DefaultReportSubmissionRule' } -UseSystemMailbox $true + Write-LogMessage -API 'Standards' -tenant $Tenant -message 'User Submission rule removed.' -sev Info + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to remove User Submission rule. Error: $($ErrorMessage.NormalizedError)" -sev Error + } } } } @@ -211,12 +228,12 @@ function Invoke-CIPPStandardUserSubmissions { ReportJunkToCustomizedAddress = $PolicyState.ReportJunkToCustomizedAddress ReportNotJunkToCustomizedAddress = $PolicyState.ReportNotJunkToCustomizedAddress ReportPhishToCustomizedAddress = $PolicyState.ReportPhishToCustomizedAddress - ReportJunkAddresses = $PolicyState.ReportJunkAddresses - ReportNotJunkAddresses = $PolicyState.ReportNotJunkAddresses - ReportPhishAddresses = $PolicyState.ReportPhishAddresses + ReportJunkAddresses = @($PolicyState.ReportJunkAddresses) + ReportNotJunkAddresses = @($PolicyState.ReportNotJunkAddresses) + ReportPhishAddresses = @($PolicyState.ReportPhishAddresses) RuleState = @{ - State = $RuleState.State - SentTo = $RuleState.SentTo + State = if ($RuleState.length -eq 0) { 'Disabled' } else { $RuleState.State } + SentTo = if ($RuleState.length -eq 0) { $null } else { @($RuleState.SentTo) } } } $ExpectedValue = @{ @@ -224,10 +241,10 @@ function Invoke-CIPPStandardUserSubmissions { ReportJunkToCustomizedAddress = if ([string]::IsNullOrWhiteSpace($Email)) { $false } else { $true } ReportNotJunkToCustomizedAddress = if ([string]::IsNullOrWhiteSpace($Email)) { $false } else { $true } ReportPhishToCustomizedAddress = if ([string]::IsNullOrWhiteSpace($Email)) { $false } else { $true } - ReportJunkAddresses = if ([string]::IsNullOrWhiteSpace($Email)) { $null } else { @($Email) } - ReportNotJunkAddresses = if ([string]::IsNullOrWhiteSpace($Email)) { $null } else { @($Email) } - ReportPhishAddresses = if ([string]::IsNullOrWhiteSpace($Email)) { $null } else { @($Email) } - RuleState = if ([string]::IsNullOrWhiteSpace($Email)) { + ReportJunkAddresses = @(if (-not [string]::IsNullOrWhiteSpace($Email)) { $Email }) + ReportNotJunkAddresses = @(if (-not [string]::IsNullOrWhiteSpace($Email)) { $Email }) + ReportPhishAddresses = @(if (-not [string]::IsNullOrWhiteSpace($Email)) { $Email }) + RuleState = if ([string]::IsNullOrWhiteSpace($Email) -or $state -eq 'disable') { @{ State = 'Disabled' SentTo = $null From fa426208d5d045fa4462d0969cf6af2464783430 Mon Sep 17 00:00:00 2001 From: rvdwegen Date: Mon, 23 Feb 2026 15:29:48 +0100 Subject: [PATCH 494/503] Better tenant lookup --- .../Entrypoints/Invoke-ListExternalTenantInfo.ps1 | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Modules/CIPPCore/Public/Entrypoints/Invoke-ListExternalTenantInfo.ps1 b/Modules/CIPPCore/Public/Entrypoints/Invoke-ListExternalTenantInfo.ps1 index 1bf2c4bfaabc..e1fe3dc11a16 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Invoke-ListExternalTenantInfo.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Invoke-ListExternalTenantInfo.ps1 @@ -22,10 +22,24 @@ function Invoke-ListExternalTenantInfo { if ($TenantId) { $GraphRequest = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/tenantRelationships/findTenantInformationByTenantId(tenantId='$TenantId')" -NoAuthCheck $true -tenantid $env:TenantID + + + # New API call to retrieve branding details + $brandingBody = @{ + username = "completelymadeupdoesnthavetobevalid@$($GraphRequest.defaultDomainName)" + } | ConvertTo-Json + + $brandingHeaders = @{ + "Content-Type" = "application/json" + } + + $brandingResponse = Invoke-RestMethod -Method Post -Uri "https://login.microsoftonline.com/common/GetCredentialType" -Body $brandingBody -Headers $brandingHeaders + $StatusCode = [HttpStatusCode]::OK $HttpResponse.Body = [PSCustomObject]@{ GraphRequest = $GraphRequest OpenIdConfig = $OpenIdConfig + UserTenantBranding = $brandingResponse.EstsProperties.UserTenantBranding } } else { $HttpResponse.StatusCode = [HttpStatusCode]::BadRequest From 362cdacc7c3c25d94e6872407a130781d39bf938 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Mon, 23 Feb 2026 18:23:56 +0100 Subject: [PATCH 495/503] removed for prettiness sake --- Modules/CIPPCore/Public/Functions/Get-CIPPTenantAlignment.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Modules/CIPPCore/Public/Functions/Get-CIPPTenantAlignment.ps1 b/Modules/CIPPCore/Public/Functions/Get-CIPPTenantAlignment.ps1 index a2a6553fb250..b0eac304e54c 100644 --- a/Modules/CIPPCore/Public/Functions/Get-CIPPTenantAlignment.ps1 +++ b/Modules/CIPPCore/Public/Functions/Get-CIPPTenantAlignment.ps1 @@ -128,8 +128,8 @@ function Get-CIPPTenantAlignment { $TenantValues.Add($filterItem.value) } } -` - if ($TenantValues -contains 'AllTenants') { + + if ($TenantValues -contains 'AllTenants') { $AppliestoAllTenants = $true } elseif ($TenantValues.Count -gt 0) { $TemplateAssignedTenants = @($TenantValues) From 6c38c16154e42a96533cfc0c2897512dee67d838 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Tue, 24 Feb 2026 17:19:37 +0800 Subject: [PATCH 496/503] Add device local admin standard function Add Invoke-CIPPStandardintuneDeviceRegLocalAdmins. Control whether users who register/enroll devices are granted local admin rights and whether Global Administrators are added as local admins. --- ...CIPPStandardintuneDeviceRegLocalAdmins.ps1 | 101 ++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardintuneDeviceRegLocalAdmins.ps1 diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardintuneDeviceRegLocalAdmins.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardintuneDeviceRegLocalAdmins.ps1 new file mode 100644 index 000000000000..178db7c71abf --- /dev/null +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardintuneDeviceRegLocalAdmins.ps1 @@ -0,0 +1,101 @@ +function Invoke-CIPPStandardintuneDeviceRegLocalAdmins { + <# + .FUNCTIONALITY + Internal + .COMPONENT + (APIName) intuneDeviceRegLocalAdmins + .SYNOPSIS + (Label) Configure local administrator rights for users joining devices + .DESCRIPTION + (Helptext) Controls whether users who register Microsoft Entra joined devices are granted local administrator rights on those devices and if Global Administrators are added as local admins. + (DocsDescription) Configures the Device Registration Policy local administrator behavior for registering users. When enabled, users who register devices are not granted local administrator rights, you can also configure if Global Administrators are added as local admins. + .NOTES + CAT + Entra (AAD) Standards + TAG + EXECUTIVETEXT + Controls whether employees who enroll devices automatically receive local administrator access. Disabling registering-user admin rights follows least-privilege principles and reduces security risk from over-privileged endpoints. + ADDEDCOMPONENT + {"type":"switch","name":"standards.intuneDeviceRegLocalAdmins.disableRegisteringUsers","label":"Disable registering users as local administrators","defaultValue":true} + {"type":"switch","name":"standards.intuneDeviceRegLocalAdmins.enableGlobalAdmins","label":"Allow Global Administrators to be local administrators","defaultValue":true} + IMPACT + Medium Impact + ADDEDDATE + 2026-02-23 + POWERSHELLEQUIVALENT + Update-MgBetaPolicyDeviceRegistrationPolicy + RECOMMENDEDBY + UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block + .LINK + https://docs.cipp.app/user-documentation/tenant/standards/list-standards + #> + + param($Tenant, $Settings) + + try { + $PreviousSetting = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/policies/deviceRegistrationPolicy' -tenantid $Tenant + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the intuneDeviceRegLocalAdmins state for $Tenant. Error: $($ErrorMessage.NormalizedError)" -Sev Error -LogData $ErrorMessage + return + } + # Current M365 Config + $CurrentOdataType = $PreviousSetting.azureADJoin.localAdmins.registeringUsers.'@odata.type' + $CurrentEnableGlobalAdmins = [bool]$PreviousSetting.azureADJoin.localAdmins.enableGlobalAdmins + + # Standards Config + $DisableRegisteringUsers = [bool]$Settings.disableRegisteringUsers + $EnableGlobalAdmins = [bool]$Settings.enableGlobalAdmins + + # State comparison + $DesiredOdataType = if ($DisableRegisteringUsers) { '#microsoft.graph.noDeviceRegistrationMembership' } else { '#microsoft.graph.allDeviceRegistrationMembership' } + $StateIsCorrect = ($CurrentOdataType -eq $DesiredOdataType) -and ($CurrentEnableGlobalAdmins -eq $EnableGlobalAdmins) + $DesiredStateText = if ($DisableRegisteringUsers) { 'disabled' } else { 'enabled' } + $DesiredGlobalAdminsText = if ($EnableGlobalAdmins) { 'enabled' } else { 'disabled' } + + if ($Settings.remediate -eq $true) { + if ($StateIsCorrect -eq $true) { + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Local administrator settings are already configured (registering users: $DesiredStateText, global admins: $DesiredGlobalAdminsText)." -sev Info + } else { + try { + $PreviousSetting.azureADJoin.localAdmins.registeringUsers = @{ '@odata.type' = $DesiredOdataType } + $PreviousSetting.azureADJoin.localAdmins.enableGlobalAdmins = $EnableGlobalAdmins + $NewBody = ConvertTo-Json -Compress -InputObject $PreviousSetting -Depth 10 + New-GraphPostRequest -tenantid $Tenant -Uri 'https://graph.microsoft.com/beta/policies/deviceRegistrationPolicy' -Type PUT -Body $NewBody -ContentType 'application/json' + $CurrentOdataType = $DesiredOdataType + $CurrentEnableGlobalAdmins = $EnableGlobalAdmins + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Set local administrator settings (registering users: $DesiredStateText, global admins: $DesiredGlobalAdminsText)." -sev Info + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to set local administrator settings (registering users: $DesiredStateText, global admins: $DesiredGlobalAdminsText). Error: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + } + } + } + + if ($Settings.alert -eq $true) { + if ($StateIsCorrect -eq $true) { + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Local administrator settings are configured as expected (registering users: $DesiredStateText, global admins: $DesiredGlobalAdminsText)." -sev Info + } else { + Write-StandardsAlert -message "Local administrator settings are not configured as expected (registering users: $DesiredStateText, global admins: $DesiredGlobalAdminsText)" -object @{ current = @{ registeringUsers = $CurrentOdataType; enableGlobalAdmins = $CurrentEnableGlobalAdmins }; desired = @{ registeringUsers = $DesiredOdataType; enableGlobalAdmins = $EnableGlobalAdmins } } -tenant $Tenant -standardName 'intuneDeviceRegLocalAdmins' -standardId $Settings.standardId + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Local administrator settings are not configured as expected (registering users: $DesiredStateText, global admins: $DesiredGlobalAdminsText)." -sev Info + } + } + + if ($Settings.report -eq $true) { + $CurrentValue = @{ + registeringUsers = @{ + '@odata.type' = $CurrentOdataType + } + enableGlobalAdmins = $CurrentEnableGlobalAdmins + } + $ExpectedValue = @{ + registeringUsers = @{ + '@odata.type' = $DesiredOdataType + } + enableGlobalAdmins = $EnableGlobalAdmins + } + Set-CIPPStandardsCompareField -FieldName 'standards.intuneDeviceRegLocalAdmins' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant + Add-CIPPBPAField -FieldName 'intuneDeviceRegLocalAdmins' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $Tenant + } +} From 7baeb92e0cac454287ff75f9e6efc0f66d036845 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Tue, 24 Feb 2026 11:21:58 +0100 Subject: [PATCH 497/503] fixes setup wizard to allow temproary headers. --- .../CIPP/Setup/Invoke-ExecCreateSAMApp.ps1 | 3 ++- .../GraphHelper/New-GraphBulkRequest.ps1 | 13 +++++++++---- .../GraphHelper/New-GraphGetRequest.ps1 | 14 +++++++++----- .../GraphHelper/New-GraphPOSTRequest.ps1 | 13 +++++++++---- .../Update-AppManagementPolicy.ps1 | 19 +++++++++---------- 5 files changed, 38 insertions(+), 24 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecCreateSAMApp.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecCreateSAMApp.ps1 index 202d6e4dc182..29cc2abb5221 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecCreateSAMApp.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecCreateSAMApp.ps1 @@ -70,7 +70,8 @@ function Invoke-ExecCreateSAMApp { } try { - $AppPolicyStatus = Update-AppManagementPolicy + + $AppPolicyStatus = Update-AppManagementPolicy -Headers @{ authorization = "Bearer $($Token.access_token)" } -ApplicationId $appId.appId Write-Information $AppPolicyStatus.PolicyAction } catch { Write-Warning "Error updating app management policy $($_.Exception.Message)." diff --git a/Modules/CIPPCore/Public/GraphHelper/New-GraphBulkRequest.ps1 b/Modules/CIPPCore/Public/GraphHelper/New-GraphBulkRequest.ps1 index 0b961c534385..2cfabcdc42f2 100644 --- a/Modules/CIPPCore/Public/GraphHelper/New-GraphBulkRequest.ps1 +++ b/Modules/CIPPCore/Public/GraphHelper/New-GraphBulkRequest.ps1 @@ -12,11 +12,16 @@ function New-GraphBulkRequest { $Requests, $NoPaginateIds = @(), [ValidateSet('v1.0', 'beta')] - $Version = 'beta' + $Version = 'beta', + $Headers ) if ($NoAuthCheck -or (Get-AuthorisedRequest -Uri $uri -TenantID $tenantid)) { - $headers = Get-GraphToken -tenantid $tenantid -scope $scope -AsApp $asapp + if ($Headers) { + $Headers = $Headers + } else { + $Headers = Get-GraphToken -tenantid $tenantid -scope $scope -AsApp $asapp + } if ($script:XMsThrottlePriority) { $headers['x-ms-throttle-priority'] = $script:XMsThrottlePriority @@ -56,13 +61,14 @@ function New-GraphBulkRequest { } Write-Host 'Getting more' Write-Host $MoreData.body.'@odata.nextLink' - $AdditionalValues = New-GraphGetRequest -ComplexFilter -uri $MoreData.body.'@odata.nextLink' -tenantid $tenantid -NoAuthCheck $NoAuthCheck -scope $scope -AsApp $asapp + $AdditionalValues = New-GraphGetRequest -ComplexFilter -uri $MoreData.body.'@odata.nextLink' -tenantid $tenantid -NoAuthCheck $NoAuthCheck -scope $scope -AsApp $asapp -headers $Headers $NewValues = [System.Collections.Generic.List[PSCustomObject]]$MoreData.body.value $AdditionalValues | ForEach-Object { $NewValues.add($_) } $MoreData.body.value = $NewValues } } catch { + Write-Host 'updating graph table because something failed.' # Try to parse ErrorDetails.Message as JSON if ($_.ErrorDetails.Message) { try { @@ -91,7 +97,6 @@ function New-GraphBulkRequest { $Tenant.LastGraphError = '' } Update-AzDataTableEntity -Force @TenantsTable -Entity $Tenant - return $ReturnedData.responses } else { Write-Error 'Not allowed. You cannot manage your own tenant or tenants not under your scope' diff --git a/Modules/CIPPCore/Public/GraphHelper/New-GraphGetRequest.ps1 b/Modules/CIPPCore/Public/GraphHelper/New-GraphGetRequest.ps1 index 23b0e59d3dc7..be328974d1e2 100644 --- a/Modules/CIPPCore/Public/GraphHelper/New-GraphGetRequest.ps1 +++ b/Modules/CIPPCore/Public/GraphHelper/New-GraphGetRequest.ps1 @@ -17,7 +17,8 @@ function New-GraphGetRequest { [switch]$CountOnly, [switch]$IncludeResponseHeaders, [hashtable]$extraHeaders, - [switch]$ReturnRawResponse + [switch]$ReturnRawResponse, + $Headers ) if ($NoAuthCheck -eq $false) { @@ -27,12 +28,15 @@ function New-GraphGetRequest { } if ($NoAuthCheck -eq $true -or $IsAuthorised) { - if ($scope -eq 'ExchangeOnline') { - $headers = Get-GraphToken -tenantid $tenantid -scope 'https://outlook.office365.com/.default' -AsApp $asapp -SkipCache $skipTokenCache + if ($headers) { + $headers = $Headers } else { - $headers = Get-GraphToken -tenantid $tenantid -scope $scope -AsApp $asapp -SkipCache $skipTokenCache + if ($scope -eq 'ExchangeOnline') { + $headers = Get-GraphToken -tenantid $tenantid -scope 'https://outlook.office365.com/.default' -AsApp $asapp -SkipCache $skipTokenCache + } else { + $headers = Get-GraphToken -tenantid $tenantid -scope $scope -AsApp $asapp -SkipCache $skipTokenCache + } } - if ($ComplexFilter) { $headers['ConsistencyLevel'] = 'eventual' } diff --git a/Modules/CIPPCore/Public/GraphHelper/New-GraphPOSTRequest.ps1 b/Modules/CIPPCore/Public/GraphHelper/New-GraphPOSTRequest.ps1 index d2ac97fb8839..d637e0d7c6c8 100644 --- a/Modules/CIPPCore/Public/GraphHelper/New-GraphPOSTRequest.ps1 +++ b/Modules/CIPPCore/Public/GraphHelper/New-GraphPOSTRequest.ps1 @@ -18,11 +18,16 @@ function New-GraphPOSTRequest { $IgnoreErrors = $false, $returnHeaders = $false, $maxRetries = 3, - $ScheduleRetry = $false + $ScheduleRetry = $false, + $headers ) if ($NoAuthCheck -or (Get-AuthorisedRequest -Uri $uri -TenantID $tenantid)) { - $headers = Get-GraphToken -tenantid $tenantid -scope $scope -AsApp $asapp -SkipCache $skipTokenCache + if ($Headers) { + $Headers = $Headers + } else { + $Headers = Get-GraphToken -tenantid $tenantid -scope $scope -AsApp $asapp -SkipCache $skipTokenCache + } if ($AddedHeaders) { foreach ($header in $AddedHeaders.GetEnumerator()) { $headers.Add($header.Key, $header.Value) @@ -36,8 +41,8 @@ function New-GraphPOSTRequest { if (!$contentType) { $contentType = 'application/json; charset=utf-8' } - - $body = Get-CIPPTextReplacement -TenantFilter $tenantid -Text $body -EscapeForJson + #Only do text replacement if no headers are set. + if (!$headers) { $body = Get-CIPPTextReplacement -TenantFilter $tenantid -Text $body -EscapeForJson } $RetryCount = 0 $RequestSuccessful = $false diff --git a/Modules/CIPPCore/Public/GraphHelper/Update-AppManagementPolicy.ps1 b/Modules/CIPPCore/Public/GraphHelper/Update-AppManagementPolicy.ps1 index 1c5e20ae81df..5a44d65fddd5 100644 --- a/Modules/CIPPCore/Public/GraphHelper/Update-AppManagementPolicy.ps1 +++ b/Modules/CIPPCore/Public/GraphHelper/Update-AppManagementPolicy.ps1 @@ -15,7 +15,8 @@ function Update-AppManagementPolicy { [CmdletBinding()] param( $TenantFilter = $env:TenantID, - $ApplicationId = $env:ApplicationID + $ApplicationId = $env:ApplicationID, + $headers ) try { @@ -39,8 +40,7 @@ function Update-AppManagementPolicy { ) # Execute bulk request - $Results = New-GraphBulkRequest -Requests $Requests -NoAuthCheck $true -asapp $true -tenantid $TenantFilter - + $Results = New-GraphBulkRequest -Requests $Requests -NoAuthCheck $true -asapp $true -tenantid $TenantFilter -headers $headers # Parse results $DefaultPolicy = ($Results | Where-Object { $_.id -eq 'defaultPolicy' }).body $AppPolicies = ($Results | Where-Object { $_.id -eq 'appPolicies' }).body.value @@ -60,8 +60,7 @@ function Update-AppManagementPolicy { }) if ($AppliesToRequests.Count -gt 0) { - $AppliesToResults = New-GraphBulkRequest -Requests $AppliesToRequests -NoAuthCheck $true -asapp $true -tenantid $TenantFilter - + $AppliesToResults = New-GraphBulkRequest -Requests $AppliesToRequests -NoAuthCheck $true -asapp $true -tenantid $TenantFilter -headers $headers # Find which policy (if any) targets the app $CIPPPolicyResult = $AppliesToResults | Where-Object { $_.body.value.appId -contains $ApplicationId } | Select-Object -First 1 if ($CIPPPolicyResult) { @@ -171,18 +170,18 @@ function Update-AppManagementPolicy { if ($CIPPAppPolicyId) { # Update existing policy that's already assigned to the app - $null = New-GraphPostRequest -uri "https://graph.microsoft.com/v1.0/policies/appManagementPolicies/$CIPPAppPolicyId" -type PATCH -body ($PolicyBody | ConvertTo-Json -Depth 10) -asapp $true -NoAuthCheck $true -tenantid $TenantFilter + $null = New-GraphPostRequest -uri "https://graph.microsoft.com/v1.0/policies/appManagementPolicies/$CIPPAppPolicyId" -type PATCH -body ($PolicyBody | ConvertTo-Json -Depth 10) -asapp $true -NoAuthCheck $true -tenantid $TenantFilter -headers $headers $PolicyAction = "Updated existing policy $CIPPAppPolicyId to allow credentials" } elseif ($ExistingExemptionPolicy) { # Exemption policy exists but not assigned to app - update and assign it - $null = New-GraphPostRequest -uri "https://graph.microsoft.com/v1.0/policies/appManagementPolicies/$($ExistingExemptionPolicy.id)" -type PATCH -body ($PolicyBody | ConvertTo-Json -Depth 10) -asapp $true -NoAuthCheck $true + $null = New-GraphPostRequest -uri "https://graph.microsoft.com/v1.0/policies/appManagementPolicies/$($ExistingExemptionPolicy.id)" -type PATCH -body ($PolicyBody | ConvertTo-Json -Depth 10) -asapp $true -NoAuthCheck $true -headers $headers if ($CIPPApp.id) { # Assign existing policy to CIPP-SAM application $AssignBody = @{ '@odata.id' = "https://graph.microsoft.com/beta/policies/appManagementPolicies/$($ExistingExemptionPolicy.id)" } - $null = New-GraphPostRequest -uri "https://graph.microsoft.com/beta/applications/$($CIPPApp.id)/appManagementPolicies/`$ref" -type POST -body ($AssignBody | ConvertTo-Json) -asapp $true -NoAuthCheck $true -tenantid $TenantFilter + $null = New-GraphPostRequest -uri "https://graph.microsoft.com/beta/applications/$($CIPPApp.id)/appManagementPolicies/`$ref" -type POST -body ($AssignBody | ConvertTo-Json) -asapp $true -NoAuthCheck $true -tenantid $TenantFilter -headers $headers $PolicyAction = "Updated and assigned existing policy $($ExistingExemptionPolicy.id) to CIPP-SAM" $CIPPAppPolicyId = $ExistingExemptionPolicy.id $CIPPAppTargeted = $true @@ -191,14 +190,14 @@ function Update-AppManagementPolicy { } } else { # Create new policy and assign to CIPP-SAM app - $CreatedPolicy = New-GraphPostRequest -uri 'https://graph.microsoft.com/v1.0/policies/appManagementPolicies' -type POST -body ($PolicyBody | ConvertTo-Json -Depth 10) -asapp $true -NoAuthCheck $true + $CreatedPolicy = New-GraphPostRequest -uri 'https://graph.microsoft.com/v1.0/policies/appManagementPolicies' -type POST -body ($PolicyBody | ConvertTo-Json -Depth 10) -asapp $true -NoAuthCheck $true -headers $headers if ($CIPPApp.id) { # Assign policy to CIPP-SAM application using beta endpoint $AssignBody = @{ '@odata.id' = "https://graph.microsoft.com/beta/policies/appManagementPolicies/$($CreatedPolicy.id)" } - $null = New-GraphPostRequest -uri "https://graph.microsoft.com/beta/applications/$($CIPPApp.id)/appManagementPolicies/`$ref" -type POST -body ($AssignBody | ConvertTo-Json) -asapp $true -NoAuthCheck $true + $null = New-GraphPostRequest -uri "https://graph.microsoft.com/beta/applications/$($CIPPApp.id)/appManagementPolicies/`$ref" -type POST -body ($AssignBody | ConvertTo-Json) -asapp $true -NoAuthCheck $true -headers $headers $PolicyAction = "Created new policy $($CreatedPolicy.id) and assigned to CIPP-SAM" $CIPPAppPolicyId = $CreatedPolicy.id $CIPPAppTargeted = $true From c6d1a49a44c3de2ade590b6f27396e5465c67b1a Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Tue, 24 Feb 2026 11:47:11 +0100 Subject: [PATCH 498/503] remove secret logging line for local dev --- .../HTTP Functions/CIPP/Setup/Invoke-ExecCreateSAMApp.ps1 | 2 -- 1 file changed, 2 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecCreateSAMApp.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecCreateSAMApp.ps1 index 29cc2abb5221..f7ca33f56c56 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecCreateSAMApp.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecCreateSAMApp.ps1 @@ -89,10 +89,8 @@ function Invoke-ExecCreateSAMApp { $Secret | Add-Member -MemberType NoteProperty -Name 'tenantid' -Value $TenantId -Force $Secret | Add-Member -MemberType NoteProperty -Name 'applicationid' -Value $AppId.appId -Force $Secret | Add-Member -MemberType NoteProperty -Name 'applicationsecret' -Value $AppPassword -Force - Write-Information ($Secret | ConvertTo-Json -Depth 5) Add-CIPPAzDataTableEntity @DevSecretsTable -Entity $Secret -Force } else { - Set-CippKeyVaultSecret -VaultName $kv -Name 'tenantid' -SecretValue (ConvertTo-SecureString -String $TenantId -AsPlainText -Force) Set-CippKeyVaultSecret -VaultName $kv -Name 'applicationid' -SecretValue (ConvertTo-SecureString -String $Appid.appId -AsPlainText -Force) Set-CippKeyVaultSecret -VaultName $kv -Name 'applicationsecret' -SecretValue (ConvertTo-SecureString -String $AppPassword -AsPlainText -Force) From 68bbf04a33ecc69540cff0ea6812a0de183494e8 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Tue, 24 Feb 2026 12:06:46 +0100 Subject: [PATCH 499/503] fixed #5444 --- .../Endpoint/MEM/Invoke-AddIntuneTemplate.ps1 | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-AddIntuneTemplate.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-AddIntuneTemplate.ps1 index ad2c5d7c5466..74d66cea6d1a 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-AddIntuneTemplate.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-AddIntuneTemplate.ps1 @@ -19,21 +19,21 @@ function Invoke-AddIntuneTemplate { $reusableTemplateRefs = @() $object = [PSCustomObject]@{ - Displayname = $Request.Body.displayName - Description = $Request.Body.description - RAWJson = $Request.Body.RawJSON - Type = $Request.Body.TemplateType - GUID = $GUID - ReusableSettings = $reusableTemplateRefs + Displayname = $Request.Body.displayName + Description = $Request.Body.description + RAWJson = $Request.Body.RawJSON + Type = $Request.Body.TemplateType + GUID = $GUID + ReusableSettings = $reusableTemplateRefs } | ConvertTo-Json $Table = Get-CippTable -tablename 'templates' $Table.Force = $true Add-CIPPAzDataTableEntity @Table -Entity @{ - JSON = "$object" + JSON = "$object" ReusableSettingsCount = $reusableTemplateRefs.Count - RowKey = "$GUID" - PartitionKey = 'IntuneTemplate' - GUID = "$GUID" + RowKey = "$GUID" + PartitionKey = 'IntuneTemplate' + GUID = "$GUID" } Write-LogMessage -headers $Headers -API $APIName -message "Created intune policy template named $($Request.Body.displayName) with GUID $GUID" -Sev 'Debug' @@ -56,8 +56,7 @@ function Invoke-AddIntuneTemplate { Type = $Template.Type GUID = $GUID ReusableSettings = $reusableTemplateRefs - } - + } | ConvertTo-Json -Compress $Table = Get-CippTable -tablename 'templates' $Table.Force = $true Add-CIPPAzDataTableEntity @Table -Entity @{ From 7f1ed6009f3df1e50de0d61ddfe40e0b42905758 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Tue, 24 Feb 2026 12:13:00 +0100 Subject: [PATCH 500/503] contact emails --- .../Public/Standards/Invoke-CIPPStandardMailContacts.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMailContacts.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMailContacts.ps1 index e804e605ce61..b06b0567df97 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMailContacts.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMailContacts.ps1 @@ -106,12 +106,12 @@ function Invoke-CIPPStandardMailContacts { } if ($Settings.report -eq $true) { $CurrentValue = @{ - marketingNotificationEmails = $CurrentInfo.marketingNotificationEmails + marketingNotificationEmails = @($CurrentInfo.marketingNotificationEmails) technicalNotificationMails = @($CurrentInfo.technicalNotificationMails) contactEmail = $CurrentInfo.privacyProfile.contactEmail } $ExpectedValue = @{ - marketingNotificationEmails = $Contacts.MarketingContact + marketingNotificationEmails = @($Contacts.MarketingContact) technicalNotificationMails = @($Contacts.SecurityContact, $Contacts.TechContact) | Where-Object { $_ -ne $null } contactEmail = $Contacts.GeneralContact } From 4ff873286647b21f6b7656e84c1026f794c32464 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Tue, 24 Feb 2026 18:27:51 +0100 Subject: [PATCH 501/503] maximum kilobytes of internet(fixes remove empty array and returns it to KelvinCode --- .../Public/Functions/Remove-EmptyArrays.ps1 | 39 ++----------------- Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 | 2 +- 2 files changed, 5 insertions(+), 36 deletions(-) diff --git a/Modules/CIPPCore/Public/Functions/Remove-EmptyArrays.ps1 b/Modules/CIPPCore/Public/Functions/Remove-EmptyArrays.ps1 index 85726be4607a..fd46b76e72b5 100644 --- a/Modules/CIPPCore/Public/Functions/Remove-EmptyArrays.ps1 +++ b/Modules/CIPPCore/Public/Functions/Remove-EmptyArrays.ps1 @@ -1,40 +1,11 @@ -function Remove-EmptyArrays { - <# - .SYNOPSIS - Recursively removes empty arrays and null properties from objects - .DESCRIPTION - This function recursively traverses an object (Array, Hashtable, or PSCustomObject) and removes: - - Empty arrays - - Null properties - The function modifies the object in place. - .PARAMETER Object - The object to process (can be Array, Hashtable, or PSCustomObject) - .FUNCTIONALITY - Internal - .EXAMPLE - $obj = @{ items = @(); name = "test"; value = $null } - Remove-EmptyArrays -Object $obj - .EXAMPLE - $obj = [PSCustomObject]@{ items = @(); name = "test" } - Remove-EmptyArrays -Object $obj - #> - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [object]$Object - ) - +function Remove-EmptyArrays ($Object) { if ($Object -is [Array]) { - foreach ($Item in $Object) { - Remove-EmptyArrays -Object $Item - } + foreach ($Item in $Object) { Remove-EmptyArrays $Item } } elseif ($Object -is [HashTable]) { foreach ($Key in @($Object.get_Keys())) { if ($Object[$Key] -is [Array] -and $Object[$Key].get_Count() -eq 0) { $Object.Remove($Key) - } else { - Remove-EmptyArrays -Object $Object[$Key] - } + } else { Remove-EmptyArrays $Object[$Key] } } } elseif ($Object -is [PSCustomObject]) { foreach ($Name in @($Object.PSObject.Properties.Name)) { @@ -42,9 +13,7 @@ function Remove-EmptyArrays { $Object.PSObject.Properties.Remove($Name) } elseif ($null -eq $Object.$Name) { $Object.PSObject.Properties.Remove($Name) - } else { - Remove-EmptyArrays -Object $Object.$Name - } + } else { Remove-EmptyArrays $Object.$Name } } } } diff --git a/Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 b/Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 index 76fba584da6d..d637c614e88d 100644 --- a/Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 +++ b/Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 @@ -89,7 +89,7 @@ function New-CIPPCAPolicy { $displayName = ($RawJSON | ConvertFrom-Json).displayName $JSONobj = $RawJSON | ConvertFrom-Json | Select-Object * -ExcludeProperty ID, GUID, *time* - Remove-EmptyArrays -Object $JSONobj + Remove-EmptyArrays $JSONobj #Remove context as it does not belong in the payload. try { if ($JSONobj.grantControls) { From 733ca03dc70d7302848faee4fd5afb87f4b09dd6 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Tue, 24 Feb 2026 13:14:59 -0500 Subject: [PATCH 502/503] Add remediate/report to proper StandardTemplate object Fix in Invoke-ExecUpdateDriftDeviation.ps1: previously the 'remediate' and 'report' NoteProperties were being added to $StandardTemplate.standards.$Setting and $Settings was set to that nested object. This change adds the properties directly to $StandardTemplate and sets $Settings to $StandardTemplate, ensuring the correct object receives the flags and that downstream code receives the expected settings structure. --- .../Tenant/Standards/Invoke-ExecUpdateDriftDeviation.ps1 | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ExecUpdateDriftDeviation.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ExecUpdateDriftDeviation.ps1 index 90dcfc65b713..0d6b2b52c592 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ExecUpdateDriftDeviation.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ExecUpdateDriftDeviation.ps1 @@ -58,9 +58,9 @@ function Invoke-ExecUpdateDriftDeviation { $Settings = $StandardTemplate } else { $StandardTemplate = $StandardTemplate.standardSettings.$Setting - $StandardTemplate.standards.$Setting | Add-Member -MemberType NoteProperty -Name 'remediate' -Value $true -Force - $StandardTemplate.standards.$Setting | Add-Member -MemberType NoteProperty -Name 'report' -Value $true -Force - $Settings = $StandardTemplate.standards.$Setting + $StandardTemplate | Add-Member -MemberType NoteProperty -Name 'remediate' -Value $true -Force + $StandardTemplate | Add-Member -MemberType NoteProperty -Name 'report' -Value $true -Force + $Settings = $StandardTemplate } $TaskBody = @{ TenantFilter = $TenantFilter From 7ec3326547393a39eca84373c31bf25c2ec1e1b6 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Tue, 24 Feb 2026 13:16:57 -0500 Subject: [PATCH 503/503] bump version --- host.json | 2 +- version_latest.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/host.json b/host.json index 2af179e475df..e215e52d2205 100644 --- a/host.json +++ b/host.json @@ -16,7 +16,7 @@ "distributedTracingEnabled": false, "version": "None" }, - "defaultVersion": "10.0.9", + "defaultVersion": "10.1.1", "versionMatchStrategy": "Strict", "versionFailureStrategy": "Fail" } diff --git a/version_latest.txt b/version_latest.txt index 4149c39eec6f..23127993ac05 100644 --- a/version_latest.txt +++ b/version_latest.txt @@ -1 +1 @@ -10.1.0 +10.1.1