diff --git a/.github/instructions/cippdb.instructions.md b/.github/instructions/cippdb.instructions.md index d78f037bd026c..19d31e78dcdd5 100644 --- a/.github/instructions/cippdb.instructions.md +++ b/.github/instructions/cippdb.instructions.md @@ -119,7 +119,7 @@ Search-CIPPDbData -TenantFilter $Tenant -SearchTerms @('john', 'admin') -Types @ ## Cache types -Available types are defined in `CIPPDBCacheTypes.json`. Each type maps to a `Set-CIPPDBCache*` writer function. Check that file for the current type list — it covers identity, Exchange, security, Intune, compliance, and usage data. +Available types are defined in `Config\CIPPDBCacheTypes.json`. Each type maps to a `Set-CIPPDBCache*` writer function. Check that file for the current type list — it covers identity, Exchange, security, Intune, compliance, and usage data. ## Writing a new Set-CIPPDBCache* function diff --git a/.github/workflows/upload_dev.yml b/.github/workflows/upload_dev.yml index 145ca2b8fcda5..82d3238166ecc 100644 --- a/.github/workflows/upload_dev.yml +++ b/.github/workflows/upload_dev.yml @@ -21,7 +21,7 @@ jobs: # Create version.json with version and commit hash - name: Create version.json run: | - VERSION=$(cat version_latest.txt | tr -d '[:space:]') + VERSION=$(cat ./version_latest.txt | tr -d '[:space:]') SHORT_SHA="${GITHUB_SHA::7}" echo "{\"version\": \"${VERSION}\", \"commit\": \"${SHORT_SHA}\"}" > version.json diff --git a/CIPPDBCacheTypes.json b/Config/CIPPDBCacheTypes.json similarity index 95% rename from CIPPDBCacheTypes.json rename to Config/CIPPDBCacheTypes.json index e3386e1b9a4a2..f42fcf180bda3 100644 --- a/CIPPDBCacheTypes.json +++ b/Config/CIPPDBCacheTypes.json @@ -254,11 +254,26 @@ "friendlyName": "Mailbox Usage", "description": "Exchange Online mailbox usage statistics" }, + { + "type": "OneDriveSiteListing", + "friendlyName": "OneDrive Site Listing", + "description": "OneDrive personal site listing details used for usage reporting" + }, { "type": "OneDriveUsage", "friendlyName": "OneDrive Usage", "description": "OneDrive usage statistics" }, + { + "type": "SharePointSiteListing", + "friendlyName": "SharePoint Site Listing", + "description": "SharePoint site listing details used for usage reporting" + }, + { + "type": "SharePointSiteUsage", + "friendlyName": "SharePoint Site Usage", + "description": "SharePoint site usage statistics" + }, { "type": "OfficeActivations", "friendlyName": "Office Activations", diff --git a/CIPPTimers.json b/Config/CIPPTimers.json similarity index 100% rename from CIPPTimers.json rename to Config/CIPPTimers.json diff --git a/CommunityRepos.json b/Config/CommunityRepos.json similarity index 100% rename from CommunityRepos.json rename to Config/CommunityRepos.json diff --git a/TemplateEmail.html b/Config/TemplateEmail.html similarity index 100% rename from TemplateEmail.html rename to Config/TemplateEmail.html diff --git a/Config/cipp-roles.json b/Config/cipp-roles.json index f95e32fa18c62..ac3c389f65a23 100644 --- a/Config/cipp-roles.json +++ b/Config/cipp-roles.json @@ -1,10 +1,19 @@ { "readonly": { - "include": ["*.Read"], - "exclude": ["CIPP.SuperAdmin.*"] + "include": [ + "*.Read" + ], + "exclude": [ + "CIPP.SuperAdmin.*", + "CIPP.Admin.*", + "CIPP.AppSettings.*" + ] }, "editor": { - "include": ["*.Read", "*.ReadWrite"], + "include": [ + "*.Read", + "*.ReadWrite" + ], "exclude": [ "CIPP.SuperAdmin.*", "CIPP.Admin.*", @@ -13,11 +22,17 @@ ] }, "admin": { - "include": ["*"], - "exclude": ["CIPP.SuperAdmin.*"] + "include": [ + "*" + ], + "exclude": [ + "CIPP.SuperAdmin.*" + ] }, "superadmin": { - "include": ["*"], + "include": [ + "*" + ], "exclude": [] } -} +} \ No newline at end of file diff --git a/intuneCollection.json b/Config/intuneCollection.json similarity index 100% rename from intuneCollection.json rename to Config/intuneCollection.json diff --git a/Config/version_latest.txt b/Config/version_latest.txt new file mode 100644 index 0000000000000..bb13e7c9bc64d --- /dev/null +++ b/Config/version_latest.txt @@ -0,0 +1 @@ +10.4.2 diff --git a/words.txt b/Config/words.txt similarity index 100% rename from words.txt rename to Config/words.txt diff --git a/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/CIPPDBCache/Push-ExecCIPPDBCache.ps1 b/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/CIPPDBCache/Push-ExecCIPPDBCache.ps1 index 3addac873be07..c9a16df871ff5 100644 --- a/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/CIPPDBCache/Push-ExecCIPPDBCache.ps1 +++ b/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/CIPPDBCache/Push-ExecCIPPDBCache.ps1 @@ -38,7 +38,7 @@ function Push-ExecCIPPDBCache { # Single-type mode (legacy) — used by HTTP endpoint for on-demand cache refresh $Name = $Item.Name - $Types = $Item.Types + $Types = @($Item.Types | Where-Object { -not [string]::IsNullOrWhiteSpace($_) -and $_ -ne 'None' }) Write-Information "Collecting $Name for tenant $TenantFilter" @@ -62,7 +62,8 @@ function Push-ExecCIPPDBCache { } # Add Types if provided (for Mailboxes function) - if ($Types) { + $FunctionSupportsTypes = $Function.Parameters.ContainsKey('Types') + if ($Types.Count -gt 0 -and $FunctionSupportsTypes) { $CacheFunctionParams.Types = $Types } diff --git a/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Push-ExecScheduledCommand.ps1 b/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Push-ExecScheduledCommand.ps1 index 1b9c4c169eaca..403da6b88d0f3 100644 --- a/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Push-ExecScheduledCommand.ps1 +++ b/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Push-ExecScheduledCommand.ps1 @@ -11,10 +11,10 @@ function Push-ExecScheduledCommand { $OrchestratorBasedCommands = @('Invoke-CIPPOffboardingJob') # Initialize AsyncLocal storage for thread-safe per-invocation context - if (-not $script:CippScheduledTaskIdStorage) { - $script:CippScheduledTaskIdStorage = [System.Threading.AsyncLocal[string]]::new() + if (-not $global:CippScheduledTaskIdStorage) { + $global:CippScheduledTaskIdStorage = [System.Threading.AsyncLocal[string]]::new() } - $script:CippScheduledTaskIdStorage.Value = $Item.TaskInfo.RowKey + $global:CippScheduledTaskIdStorage.Value = $Item.TaskInfo.RowKey $Table = Get-CippTable -tablename 'ScheduledTasks' $task = $Item.TaskInfo diff --git a/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Standards/Push-CIPPStandard.ps1 b/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Standards/Push-CIPPStandard.ps1 index 0b70aa8382356..e7df2cc7f7237 100644 --- a/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Standards/Push-CIPPStandard.ps1 +++ b/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Standards/Push-CIPPStandard.ps1 @@ -41,10 +41,11 @@ function Push-CIPPStandard { } # Initialize AsyncLocal storage for thread-safe per-invocation context - if (-not $script:CippStandardInfoStorage) { - $script:CippStandardInfoStorage = [System.Threading.AsyncLocal[object]]::new() + # Uses $global: so Write-LogMessage (CIPPCore module) can read it across module boundaries + if (-not $global:CippStandardInfoStorage) { + $global:CippStandardInfoStorage = [System.Threading.AsyncLocal[object]]::new() } - $script:CippStandardInfoStorage.Value = $StandardInfo + $global:CippStandardInfoStorage.Value = $StandardInfo # ---- Standard execution telemetry ---- $runId = [guid]::NewGuid().ToString() @@ -124,8 +125,8 @@ function Push-CIPPStandard { Error = $err } | ConvertTo-Json -Compress) - if ($script:CippStandardInfoStorage) { - $script:CippStandardInfoStorage.Value = $null + if ($global:CippStandardInfoStorage) { + $global:CippStandardInfoStorage.Value = $null } } } diff --git a/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Webhooks/Push-AuditLogSearchCreation.ps1 b/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Webhooks/Push-AuditLogSearchCreation.ps1 index a3517b3c3d994..3dc790ffdcc28 100644 --- a/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Webhooks/Push-AuditLogSearchCreation.ps1 +++ b/Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Webhooks/Push-AuditLogSearchCreation.ps1 @@ -41,7 +41,7 @@ function Push-AuditLogSearchCreation { } catch { Write-Information "Error creating audit log search $($Tenant.defaultDomainName) - $($_.Exception.Message)" Write-Information $_.InvocationInfo.PositionMessage - Write-LogMessage -API 'Audit Logs' -tenant $Tenant.defaultDomainName -Message "Error creating audit log search for tenant $($Tenant.defaultDomainName): $($_.Exception.Message)" -Sev Error -LogData (Get-CippException -Exception $_) + #Write-LogMessage -API 'Audit Logs' -tenant $Tenant.defaultDomainName -Message "Error creating audit log search for tenant $($Tenant.defaultDomainName): $($_.Exception.Message)" -Sev Error -LogData (Get-CippException -Exception $_) } return $true } diff --git a/Modules/CIPPCore/Public/Add-CIPPApplicationPermission.ps1 b/Modules/CIPPCore/Public/Add-CIPPApplicationPermission.ps1 index b882c8cc52b77..25481b363bccc 100644 --- a/Modules/CIPPCore/Public/Add-CIPPApplicationPermission.ps1 +++ b/Modules/CIPPCore/Public/Add-CIPPApplicationPermission.ps1 @@ -58,13 +58,8 @@ function Add-CIPPApplicationPermission { Write-Information "Adding application permissions to application $ApplicationId in tenant $TenantFilter" $ServicePrincipalList = [System.Collections.Generic.List[object]]::new() - $CachedSPs = New-CIPPDbRequest -TenantFilter $TenantFilter -Type 'ServicePrincipals' - if ($CachedSPs) { - foreach ($SP in $CachedSPs) { $ServicePrincipalList.Add($SP) } - } else { - $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) } - } + $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. diff --git a/Modules/CIPPCore/Public/Add-CIPPDelegatedPermission.ps1 b/Modules/CIPPCore/Public/Add-CIPPDelegatedPermission.ps1 index 5bfc926276401..821a1fe115dfb 100644 --- a/Modules/CIPPCore/Public/Add-CIPPDelegatedPermission.ps1 +++ b/Modules/CIPPCore/Public/Add-CIPPDelegatedPermission.ps1 @@ -71,12 +71,7 @@ function Add-CIPPDelegatedPermission { } $Translator = Get-Content (Join-Path $env:CIPPRootPath 'Config\PermissionsTranslator.json') | ConvertFrom-Json - $CachedSPs = New-CIPPDbRequest -TenantFilter $TenantFilter -Type 'ServicePrincipals' - $ServicePrincipalList = if ($CachedSPs) { - $CachedSPs - } else { - New-GraphGETRequest -uri "https://graph.microsoft.com/beta/servicePrincipals?`$select=appId,id,displayName&`$top=999" -tenantid $TenantFilter -skipTokenCache $true -NoAuthCheck $true - } + $ServicePrincipalList = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/servicePrincipals?`$select=appId,id,displayName&`$top=999" -tenantid $TenantFilter -skipTokenCache $true -NoAuthCheck $true $Results = [System.Collections.Generic.List[string]]::new() $ourSVCPrincipal = $ServicePrincipalList | Where-Object -Property AppId -EQ $ApplicationId | Select-Object -First 1 diff --git a/Modules/CIPPCore/Public/Add-CIPPScheduledTask.ps1 b/Modules/CIPPCore/Public/Add-CIPPScheduledTask.ps1 index 51ad97a763846..f9a5c01599239 100644 --- a/Modules/CIPPCore/Public/Add-CIPPScheduledTask.ps1 +++ b/Modules/CIPPCore/Public/Add-CIPPScheduledTask.ps1 @@ -204,6 +204,10 @@ function Add-CIPPScheduledTask { } + if ($task.Tag) { + $entity['Tag'] = [string]$task.Tag + } + # Always store DesiredStartTime if provided if ($DesiredStartTime) { $entity['DesiredStartTime'] = [string]$DesiredStartTime diff --git a/Modules/CIPPCore/Public/AuditLogs/New-CippAuditLogSearch.ps1 b/Modules/CIPPCore/Public/AuditLogs/New-CippAuditLogSearch.ps1 index c6a0c6b93c9e7..3b49c88182020 100644 --- a/Modules/CIPPCore/Public/AuditLogs/New-CippAuditLogSearch.ps1 +++ b/Modules/CIPPCore/Public/AuditLogs/New-CippAuditLogSearch.ps1 @@ -169,10 +169,10 @@ function New-CippAuditLogSearch { try { $AuditDisabledTable = Get-CIPPTable -TableName 'AuditLogDisabledTenants' $DisabledEntity = [PSCustomObject]@{ - PartitionKey = [string]'AuditDisabledTenant' - RowKey = [string]$TenantFilter - TenantFilter = [string]$TenantFilter - Status = [string]'AuditingDisabledTenant' + PartitionKey = [string]'AuditDisabledTenant' + RowKey = [string]$TenantFilter + TenantFilter = [string]$TenantFilter + Status = [string]'AuditingDisabledTenant' ExpiresAtUnix = [int64]([datetimeoffset]::UtcNow.AddHours(24).ToUnixTimeSeconds()) } Add-CIPPAzDataTableEntity @AuditDisabledTable -Entity $DisabledEntity -Force | Out-Null @@ -189,6 +189,33 @@ function New-CippAuditLogSearch { message = [string]'Unified auditing is disabled for this tenant.' } } + + # Handle HTML error pages (e.g. Azure Front Door 502/504 gateway timeouts) + if ($TrimmedAuditLogErrorMessage -match '([^<]+)') { + $HtmlTitle = $Matches[1].Trim() + Write-LogMessage -API 'Audit Logs' -tenant $TenantFilter -message "Audit log search creation failed with gateway error for tenant $TenantFilter ($HtmlTitle) - will retry next cycle" -sev Warning + return [PSCustomObject]@{ + id = $null + displayName = [string]$DisplayName + status = [string]'GatewayError' + cippStatus = [string]'TransientError' + message = [string]"Microsoft returned gateway error ($HtmlTitle) - search will be retried next cycle." + } + } + + # Handle Microsoft-side timeouts / transient errors (e.g. UnknownError with empty message) + $ErrorCode = $AuditLogError.error.code ?? $AuditLogError.code + if ($ErrorCode -in @('UnknownError', 'ServiceUnavailable', 'RequestTimeout', 'GatewayTimeout', 'TooManyRequests')) { + Write-LogMessage -API 'Audit Logs' -tenant $TenantFilter -message "Audit log search creation failed with transient error for tenant $TenantFilter ($ErrorCode) - will retry next cycle" -sev Warning + return [PSCustomObject]@{ + id = $null + displayName = [string]$DisplayName + status = [string]$ErrorCode + cippStatus = [string]'TransientError' + message = [string]"Microsoft returned $ErrorCode - search will be retried next cycle." + } + } + throw } diff --git a/Modules/CIPPCore/Public/Authentication/Get-CippAllowedPermissions.ps1 b/Modules/CIPPCore/Public/Authentication/Get-CippAllowedPermissions.ps1 index 5a16b3c70a4e0..0d0130689cd9a 100644 --- a/Modules/CIPPCore/Public/Authentication/Get-CippAllowedPermissions.ps1 +++ b/Modules/CIPPCore/Public/Authentication/Get-CippAllowedPermissions.ps1 @@ -23,7 +23,7 @@ function Get-CippAllowedPermissions { # Get all available permissions and base roles configuration - $Version = (Get-Content -Path (Join-Path $env:CIPPRootPath 'version_latest.txt')).trim() + $Version = (Get-Content -Path (Join-Path $env:CIPPRootPath 'Config\version_latest.txt')).trim() $BaseRoles = Get-Content -Path (Join-Path $env:CIPPRootPath 'Config\cipp-roles.json') | ConvertFrom-Json $DefaultRoles = @('superadmin', 'admin', 'editor', 'readonly', 'anonymous', 'authenticated') diff --git a/Modules/CIPPCore/Public/Authentication/New-CIPPAPIConfig.ps1 b/Modules/CIPPCore/Public/Authentication/New-CIPPAPIConfig.ps1 index 1629df34d70cc..53cac2a2feffd 100644 --- a/Modules/CIPPCore/Public/Authentication/New-CIPPAPIConfig.ps1 +++ b/Modules/CIPPCore/Public/Authentication/New-CIPPAPIConfig.ps1 @@ -76,8 +76,8 @@ function New-CIPPAPIConfig { Write-Information 'Creating password' $Step = 'Creating Application Password' - $AppManagementPolicy = New-GraphGetRequest -uri "https://graph.microsoft.com/v1.0/policies/defaultAppManagementPolicy" -AsApp $true -NoAuthCheck $true - $PasswordExpirationPolicy = $AppManagementPolicy.applicationRestrictions.passwordcredentials | + $AppManagementPolicy = New-GraphGetRequest -uri 'https://graph.microsoft.com/v1.0/policies/defaultAppManagementPolicy' -AsApp $true -NoAuthCheck $true + $PasswordExpirationPolicy = $AppManagementPolicy.applicationRestrictions.passwordcredentials | Where-Object { $_.restrictionType -eq 'passwordLifetime' } $PasswordBody = $null if (-not ($PasswordExpirationPolicy.state -eq 'disabled' -or $null -eq $PasswordExpirationPolicy.state)) { @@ -93,7 +93,9 @@ function New-CIPPAPIConfig { $APIPassword = New-GraphPOSTRequest -uri "https://graph.microsoft.com/v1.0/applications/$($APIApp.id)/addPassword" -AsApp $true -NoAuthCheck $true -type POST -body $PasswordBody -maxRetries 3 break } catch { + $ExceptionMessage = $_.Exception.Message $IsNotReplicatedYet = $_.Exception.Message -match "Resource '.*' does not exist or one of its queried reference-property objects are not present" + $IsCredentialPolicyBlocked = $ExceptionMessage -match 'Credential type not allowed as per assigned policy' if ($IsNotReplicatedYet -and $Attempt -lt 6) { $DelaySeconds = 3 Write-Information "Application object not yet replicated for addPassword (attempt $Attempt of 6). Retrying in $DelaySeconds second(s)." @@ -105,6 +107,14 @@ function New-CIPPAPIConfig { } continue } + + if ($IsCredentialPolicyBlocked -and $Attempt -lt 6) { + $DelaySeconds = [Math]::Min(30, 5 * $Attempt) + Write-Information "Credential policy still blocks addPassword (attempt $Attempt of 6). Waiting for policy propagation and retrying in $DelaySeconds second(s)." + Start-Sleep -Seconds $DelaySeconds + continue + } + throw } } @@ -167,7 +177,7 @@ function New-CIPPAPIConfig { } catch { if ($Attempt -lt 6) { Start-Sleep -Seconds 3 - Write-Information "Retrying service principal creation for AppId $($APIApp.appId) (attempt $Attempt of 6) after failure: $($_.Exception.Message)" + Write-Information "Retrying service principal creation for AppId $($APIApp.appId) (attempt $Attempt of 6) after failure: $($_.Exception.Message)" continue } throw diff --git a/Modules/CIPPCore/Public/Compare-CIPPIntuneObject.ps1 b/Modules/CIPPCore/Public/Compare-CIPPIntuneObject.ps1 index b20124b9372a3..d4a19075ac9e5 100644 --- a/Modules/CIPPCore/Public/Compare-CIPPIntuneObject.ps1 +++ b/Modules/CIPPCore/Public/Compare-CIPPIntuneObject.ps1 @@ -362,7 +362,7 @@ function Compare-CIPPIntuneObject { return $null } } else { - $intuneCollection = Get-Content .\intuneCollection.json | ConvertFrom-Json -ErrorAction SilentlyContinue + $intuneCollection = Get-Content "$env:CIPPRootPath\Config\intuneCollection.json" | ConvertFrom-Json -ErrorAction SilentlyContinue # Build a hashtable index for O(1) lookups instead of O(n) Where-Object scans $intuneCollectionIndex = @{} foreach ($item in $intuneCollection) { diff --git a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-CIPPOrchestrator.ps1 b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-CIPPOrchestrator.ps1 index ac493937c2f88..4ee6c5c2ab172 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-CIPPOrchestrator.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-CIPPOrchestrator.ps1 @@ -37,6 +37,24 @@ function Start-CIPPOrchestrator { # ─── CIPPNG runtime: push batch directly to OrchestratorService ─── if ($env:CIPPNG -eq 'true' -and $InputObject) { $OrchestratorName = $InputObject.OrchestratorName ?? 'UnnamedOrchestrator' + + # QueueFunction pattern: call the function first to generate batch items + if (-not $InputObject.Batch -and $InputObject.QueueFunction) { + $QueueFuncName = "Push-$($InputObject.QueueFunction.FunctionName)" + Write-Information "CIPP-NG: Calling QueueFunction '$QueueFuncName' to build batch for '$OrchestratorName'" + $QueueItem = [PSCustomObject]@{} + if ($InputObject.QueueFunction.Parameters) { + $QueueItem = [PSCustomObject]$InputObject.QueueFunction.Parameters + } + $BatchResult = & $QueueFuncName -Item $QueueItem + $QueueBatch = @($BatchResult | Where-Object { $null -ne $_ }) + if ($QueueBatch.Count -eq 0) { + Write-Information "CIPP-NG: QueueFunction '$QueueFuncName' returned 0 tasks for '$OrchestratorName' - skipping" + return "CIPPNG-$OrchestratorName-NoTasks" + } + $InputObject | Add-Member -MemberType NoteProperty -Name 'Batch' -Value $QueueBatch -Force + } + $BatchJson = ConvertTo-Json -InputObject @($InputObject.Batch) -Depth 10 -Compress $PostExecFunctionName = $null diff --git a/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-CIPPStatsTimer.ps1 b/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-CIPPStatsTimer.ps1 index 7df1c6fa0b46a..8bb757c4fbcf1 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-CIPPStatsTimer.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-CIPPStatsTimer.ps1 @@ -15,7 +15,7 @@ function Start-CIPPStatsTimer { $TenantCount = (Get-Tenants -IncludeAll).count - $APIVersion = Get-Content (Join-Path $env:CIPPRootPath 'version_latest.txt') | Out-String + $APIVersion = Get-Content (Join-Path $env:CIPPRootPath 'Config\version_latest.txt') | Out-String $Table = Get-CIPPTable -TableName Extensionsconfig try { $RawExt = (Get-CIPPAzDataTableEntity @Table).config | ConvertFrom-Json -Depth 10 -ErrorAction Stop diff --git a/Modules/CIPPCore/Public/Get-CIPPLicenseOverview.ps1 b/Modules/CIPPCore/Public/Get-CIPPLicenseOverview.ps1 index f6f955e247817..1161fc6646eb6 100644 --- a/Modules/CIPPCore/Public/Get-CIPPLicenseOverview.ps1 +++ b/Modules/CIPPCore/Public/Get-CIPPLicenseOverview.ps1 @@ -119,11 +119,11 @@ function Get-CIPPLicenseOverview { $SubInfo = $SkuIDs | Where-Object { $_.id -eq $Subscription } $diff = $SubInfo.nextLifecycleDateTime - $SubInfo.createdDateTime $Term = 'Term unknown or non-NCE license' - if ($diff.Days -ge 360 -and $diff.Days -le 1089) { + if ($diff.Days -ge 32 -and $diff.Days -le 1089) { $Term = 'Yearly' } elseif ($diff.Days -ge 1090 -and $diff.Days -le 1100) { $Term = '3 Year' - } elseif ($diff.Days -ge 25 -and $diff.Days -le 35) { + } elseif ($diff.Days -ge 25 -and $diff.Days -le 31) { $Term = 'Monthly' } $TimeUntilRenew = ($subinfo.nextLifecycleDateTime - (Get-Date)).days @@ -133,6 +133,7 @@ function Get-CIPPLicenseOverview { TotalLicenses = $SubInfo.totalLicenses DaysUntilRenew = $TimeUntilRenew NextLifecycle = $SubInfo.nextLifecycleDateTime + CreatedDateTime = $SubInfo.createdDateTime IsTrial = $SubInfo.isTrial SubscriptionId = $subinfo.id CSPSubscriptionId = $SubInfo.commerceSubscriptionId diff --git a/Modules/CIPPCore/Public/Get-CIPPOneDriveUsageReport.ps1 b/Modules/CIPPCore/Public/Get-CIPPOneDriveUsageReport.ps1 new file mode 100644 index 0000000000000..79f3b3c7bb29a --- /dev/null +++ b/Modules/CIPPCore/Public/Get-CIPPOneDriveUsageReport.ps1 @@ -0,0 +1,113 @@ +function Get-CIPPOneDriveUsageReport { + <# + .SYNOPSIS + Generates a OneDrive usage report from the CIPP Reporting database + + .DESCRIPTION + Retrieves cached OneDrive site listing and usage data and combines them to match + the payload shape of Invoke-ListSites for Type=OneDriveUsageAccount. + + .PARAMETER TenantFilter + The tenant to generate the report for + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter + ) + + try { + if ($TenantFilter -eq 'AllTenants') { + $AllSiteItems = Get-CIPPDbItem -TenantFilter 'allTenants' -Type 'OneDriveSiteListing' + $Tenants = @($AllSiteItems | Where-Object { $_.RowKey -ne 'OneDriveSiteListing-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-CIPPOneDriveUsageReport -TenantFilter $Tenant + foreach ($Result in $TenantResults) { + $Result | Add-Member -NotePropertyName 'Tenant' -NotePropertyValue $Tenant -Force + $AllResults.Add($Result) + } + } catch { + Write-LogMessage -API 'OneDriveUsageReport' -tenant $Tenant -message "Failed to get report for tenant: $($_.Exception.Message)" -sev Warning + } + } + return $AllResults + } + + $SiteItems = @(Get-CIPPDbItem -TenantFilter $TenantFilter -Type 'OneDriveSiteListing' | Where-Object { $_.RowKey -ne 'OneDriveSiteListing-Count' }) + if (-not $SiteItems) { + throw 'No OneDrive site listing data found in reporting database. Sync OneDriveUsage cache first.' + } + + $UsageItems = @(Get-CIPPDbItem -TenantFilter $TenantFilter -Type 'OneDriveUsage' | Where-Object { $_.RowKey -ne 'OneDriveUsage-Count' }) + if (-not $UsageItems) { + throw 'No OneDrive usage data found in reporting database. Sync OneDriveUsage cache first.' + } + + $LatestSiteTimestamp = ($SiteItems | Where-Object { $_.Timestamp } | Sort-Object Timestamp -Descending | Select-Object -First 1).Timestamp + $LatestUsageTimestamp = ($UsageItems | Where-Object { $_.Timestamp } | Sort-Object Timestamp -Descending | Select-Object -First 1).Timestamp + $CacheTimestamp = if ($LatestSiteTimestamp -and $LatestUsageTimestamp) { + if ($LatestSiteTimestamp -gt $LatestUsageTimestamp) { $LatestSiteTimestamp } else { $LatestUsageTimestamp } + } else { + $LatestSiteTimestamp ?? $LatestUsageTimestamp + } + + $UsageBySiteId = [System.Collections.Generic.Dictionary[string, object]]::new([System.StringComparer]::OrdinalIgnoreCase) + foreach ($UsageItem in $UsageItems) { + $UsageRow = $UsageItem.Data | ConvertFrom-Json -Depth 10 + if (-not [string]::IsNullOrWhiteSpace($UsageRow.siteId)) { + $UsageBySiteId[[string]$UsageRow.siteId] = $UsageRow + } + } + + $Report = [System.Collections.Generic.List[PSCustomObject]]::new() + foreach ($SiteItem in $SiteItems) { + $Site = $SiteItem.Data | ConvertFrom-Json -Depth 10 + if ($Site.isPersonalSite -ne $true) { + continue + } + + $SiteUsage = $null + [void]$UsageBySiteId.TryGetValue([string]$Site.sharepointIds.siteId, [ref]$SiteUsage) + + $StorageUsedInBytes = [double]($SiteUsage.storageUsedInBytes ?? 0) + $StorageAllocatedInBytes = [double]($SiteUsage.storageAllocatedInBytes ?? 0) + + $ReportItem = [PSCustomObject]@{ + siteId = $Site.sharepointIds.siteId + webId = $Site.sharepointIds.webId + createdDateTime = $Site.createdDateTime + displayName = $Site.displayName + webUrl = $Site.webUrl + ownerDisplayName = $SiteUsage.ownerDisplayName + ownerPrincipalName = $SiteUsage.ownerPrincipalName + lastActivityDate = $SiteUsage.lastActivityDate + fileCount = $SiteUsage.fileCount + storageUsedInGigabytes = [math]::round($StorageUsedInBytes / 1GB, 2) + storageAllocatedInGigabytes = [math]::round($StorageAllocatedInBytes / 1GB, 2) + storageUsedInBytes = $SiteUsage.storageUsedInBytes + storageAllocatedInBytes = $SiteUsage.storageAllocatedInBytes + rootWebTemplate = $SiteUsage.rootWebTemplate + reportRefreshDate = $SiteUsage.reportRefreshDate + AutoMapUrl = $Site.AutoMapUrl + } + + if ($CacheTimestamp) { + $ReportItem | Add-Member -NotePropertyName 'CacheTimestamp' -NotePropertyValue $CacheTimestamp -Force + } + + $Report.Add($ReportItem) + } + + return $Report | Sort-Object -Property displayName + + } catch { + Write-LogMessage -API 'OneDriveUsageReport' -tenant $TenantFilter -message "Failed to generate OneDrive usage report: $($_.Exception.Message)" -sev Error -LogData (Get-CippException -Exception $_) + throw + } +} diff --git a/Modules/CIPPCore/Public/Get-CIPPSharePointSiteUsageReport.ps1 b/Modules/CIPPCore/Public/Get-CIPPSharePointSiteUsageReport.ps1 new file mode 100644 index 0000000000000..c4f76eaaa2726 --- /dev/null +++ b/Modules/CIPPCore/Public/Get-CIPPSharePointSiteUsageReport.ps1 @@ -0,0 +1,113 @@ +function Get-CIPPSharePointSiteUsageReport { + <# + .SYNOPSIS + Generates a SharePoint site usage report from the CIPP Reporting database + + .DESCRIPTION + Retrieves cached SharePoint site listing and usage data and combines them to match + the payload shape of Invoke-ListSites for Type=SharePointSiteUsage. + + .PARAMETER TenantFilter + The tenant to generate the report for + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter + ) + + try { + if ($TenantFilter -eq 'AllTenants') { + $AllSiteItems = Get-CIPPDbItem -TenantFilter 'allTenants' -Type 'SharePointSiteListing' + $Tenants = @($AllSiteItems | Where-Object { $_.RowKey -ne 'SharePointSiteListing-Count' } | Select-Object -ExpandProperty PartitionKey -Unique) + + $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-CIPPSharePointSiteUsageReport -TenantFilter $Tenant + foreach ($Result in $TenantResults) { + $Result | Add-Member -NotePropertyName 'Tenant' -NotePropertyValue $Tenant -Force + $AllResults.Add($Result) + } + } catch { + Write-LogMessage -API 'SharePointSiteUsageReport' -tenant $Tenant -message "Failed to get report for tenant: $($_.Exception.Message)" -sev Warning + } + } + return $AllResults + } + + $SiteItems = @(Get-CIPPDbItem -TenantFilter $TenantFilter -Type 'SharePointSiteListing' | Where-Object { $_.RowKey -ne 'SharePointSiteListing-Count' }) + if (-not $SiteItems) { + throw 'No SharePoint site listing data found in reporting database. Sync SharePointSiteUsage cache first.' + } + + $UsageItems = @(Get-CIPPDbItem -TenantFilter $TenantFilter -Type 'SharePointSiteUsage' | Where-Object { $_.RowKey -ne 'SharePointSiteUsage-Count' }) + if (-not $UsageItems) { + throw 'No SharePoint site usage data found in reporting database. Sync SharePointSiteUsage cache first.' + } + + $LatestSiteTimestamp = ($SiteItems | Where-Object { $_.Timestamp } | Sort-Object Timestamp -Descending | Select-Object -First 1).Timestamp + $LatestUsageTimestamp = ($UsageItems | Where-Object { $_.Timestamp } | Sort-Object Timestamp -Descending | Select-Object -First 1).Timestamp + $CacheTimestamp = if ($LatestSiteTimestamp -and $LatestUsageTimestamp) { + if ($LatestSiteTimestamp -gt $LatestUsageTimestamp) { $LatestSiteTimestamp } else { $LatestUsageTimestamp } + } else { + $LatestSiteTimestamp ?? $LatestUsageTimestamp + } + + $UsageBySiteId = [System.Collections.Generic.Dictionary[string, object]]::new([System.StringComparer]::OrdinalIgnoreCase) + foreach ($UsageItem in $UsageItems) { + $UsageRow = $UsageItem.Data | ConvertFrom-Json -Depth 10 + if (-not [string]::IsNullOrWhiteSpace($UsageRow.siteId)) { + $UsageBySiteId[[string]$UsageRow.siteId] = $UsageRow + } + } + + $Report = [System.Collections.Generic.List[PSCustomObject]]::new() + foreach ($SiteItem in $SiteItems) { + $Site = $SiteItem.Data | ConvertFrom-Json -Depth 10 + if ($Site.isPersonalSite -eq $true) { + continue + } + + $SiteUsage = $null + [void]$UsageBySiteId.TryGetValue([string]$Site.sharepointIds.siteId, [ref]$SiteUsage) + + $StorageUsedInBytes = [double]($SiteUsage.storageUsedInBytes ?? 0) + $StorageAllocatedInBytes = [double]($SiteUsage.storageAllocatedInBytes ?? 0) + + $ReportItem = [PSCustomObject]@{ + siteId = $Site.sharepointIds.siteId + webId = $Site.sharepointIds.webId + createdDateTime = $Site.createdDateTime + displayName = $Site.displayName + webUrl = $Site.webUrl + ownerDisplayName = $SiteUsage.ownerDisplayName + ownerPrincipalName = $SiteUsage.ownerPrincipalName + lastActivityDate = $SiteUsage.lastActivityDate + fileCount = $SiteUsage.fileCount + storageUsedInGigabytes = [math]::round($StorageUsedInBytes / 1GB, 2) + storageAllocatedInGigabytes = [math]::round($StorageAllocatedInBytes / 1GB, 2) + storageUsedInBytes = $SiteUsage.storageUsedInBytes + storageAllocatedInBytes = $SiteUsage.storageAllocatedInBytes + rootWebTemplate = $SiteUsage.rootWebTemplate + reportRefreshDate = $SiteUsage.reportRefreshDate + AutoMapUrl = $Site.AutoMapUrl + } + + if ($CacheTimestamp) { + $ReportItem | Add-Member -NotePropertyName 'CacheTimestamp' -NotePropertyValue $CacheTimestamp -Force + } + + $Report.Add($ReportItem) + } + + return $Report | Sort-Object -Property displayName + + } catch { + Write-LogMessage -API 'SharePointSiteUsageReport' -tenant $TenantFilter -message "Failed to generate SharePoint site usage report: $($_.Exception.Message)" -sev Error -LogData (Get-CippException -Exception $_) + throw + } +} \ No newline at end of file diff --git a/Modules/CIPPCore/Public/Get-CIPPTimerFunctions.ps1 b/Modules/CIPPCore/Public/Get-CIPPTimerFunctions.ps1 index 0ab553c0f479e..306bce8d75c7d 100644 --- a/Modules/CIPPCore/Public/Get-CIPPTimerFunctions.ps1 +++ b/Modules/CIPPCore/Public/Get-CIPPTimerFunctions.ps1 @@ -44,7 +44,7 @@ function Get-CIPPTimerFunctions { } } - $CippTimers = Get-Content -Path (Join-Path $env:CIPPRootPath 'CIPPTimers.json') + $CippTimers = Get-Content -Path (Join-Path $env:CIPPRootPath 'Config\CIPPTimers.json') if (!('Cronos.CronExpression' -as [type])) { try { diff --git a/Modules/CIPPCore/Public/GraphHelper/Get-GraphToken.ps1 b/Modules/CIPPCore/Public/GraphHelper/Get-GraphToken.ps1 index 87a829fe795f9..257b7b22aad05 100644 --- a/Modules/CIPPCore/Public/GraphHelper/Get-GraphToken.ps1 +++ b/Modules/CIPPCore/Public/GraphHelper/Get-GraphToken.ps1 @@ -26,6 +26,28 @@ function Get-GraphToken($tenantid, $scope, $AsApp, $AppID, $AppSecret, $refreshT } # ── Slow path: need a new token — do table lookups + token acquisition ── + # Acquire per-key lock to prevent thundering herd (multiple runspaces + # all missing cache and independently fetching the same token). + $LockAcquired = $false + if ($UseSharedTokenCache -and $SharedTokenCacheKey) { + $LockAcquired = [CIPP.CIPPTokenCache]::AcquireLock($SharedTokenCacheKey, 30000) + if ($LockAcquired) { + # Double-check: another thread may have stored the token while we waited + $SharedCacheEntry = [CIPP.CIPPTokenCache]::Lookup($SharedTokenCacheKey, 120) + if ($SharedCacheEntry.Found -and -not [string]::IsNullOrWhiteSpace($SharedCacheEntry.TokenPayloadJson)) { + try { + $AccessToken = $SharedCacheEntry.TokenPayloadJson | ConvertFrom-Json -ErrorAction Stop + [CIPP.CIPPTokenCache]::ReleaseLock($SharedTokenCacheKey) + $LockAcquired = $false + if ($ReturnRefresh) { return $AccessToken } + return @{ Authorization = "Bearer $($AccessToken.access_token)" } + } catch { + [CIPP.CIPPTokenCache]::Remove($SharedTokenCacheKey) + } + } + } + } + try { if (!$env:SetFromProfile) { $CIPPAuth = Get-CIPPAuthentication; Write-Host 'Could not get Refreshtoken from environment variable. Reloading token.' } $ConfigTable = Get-CippTable -tablename 'Config' $Filter = "PartitionKey eq 'AppCache' and RowKey eq 'AppCache'" @@ -164,4 +186,10 @@ function Get-GraphToken($tenantid, $scope, $AsApp, $AppID, $AppSecret, $refreshT if (!$donotset) { Update-AzDataTableEntity -Force @TenantsTable -Entity $Tenant } throw "Could not get token: $($Tenant.LastGraphError)" } + } finally { + # Always release the per-key lock if we acquired it + if ($LockAcquired -and $SharedTokenCacheKey) { + [CIPP.CIPPTokenCache]::ReleaseLock($SharedTokenCacheKey) + } + } } diff --git a/Modules/CIPPCore/Public/GraphHelper/New-passwordString.ps1 b/Modules/CIPPCore/Public/GraphHelper/New-passwordString.ps1 index 158fe01302ce2..0ecc138fa68b7 100644 --- a/Modules/CIPPCore/Public/GraphHelper/New-passwordString.ps1 +++ b/Modules/CIPPCore/Public/GraphHelper/New-passwordString.ps1 @@ -119,7 +119,7 @@ function New-passwordString { throw "Word count must be between 2 and 10 for passphrase generation" } - $Words = @(Get-Content (Join-Path $env:CIPPRootPath 'words.txt') -Encoding UTF8 | Where-Object { $_.Length -gt 0 -and $_ -match '^[a-zA-Z]+$' }) + $Words = @(Get-Content (Join-Path $env:CIPPRootPath 'Config\words.txt') -Encoding UTF8 | Where-Object { $_.Length -gt 0 -and $_ -match '^[a-zA-Z]+$' }) $wordPool = [System.Collections.Generic.List[string]]::new() $Words | ForEach-Object { $wordPool.Add($_) } $SelectedWords = @(1..$WordCount | ForEach-Object { diff --git a/Modules/CIPPCore/Public/GraphHelper/Write-LogMessage.ps1 b/Modules/CIPPCore/Public/GraphHelper/Write-LogMessage.ps1 index 4b6e85e740436..904e25454d493 100644 --- a/Modules/CIPPCore/Public/GraphHelper/Write-LogMessage.ps1 +++ b/Modules/CIPPCore/Public/GraphHelper/Write-LogMessage.ps1 @@ -68,18 +68,18 @@ function Write-LogMessage { if ($tenantId) { $TableRow.Add('TenantID', [string]$tenantId) } - if ($script:CippStandardInfoStorage -and $script:CippStandardInfoStorage.Value) { - $TableRow.Standard = [string]$script:CippStandardInfoStorage.Value.Standard - $TableRow.StandardTemplateId = [string]$script:CippStandardInfoStorage.Value.StandardTemplateId - if ($script:CippStandardInfoStorage.Value.IntuneTemplateId) { - $TableRow.IntuneTemplateId = [string]$script:CippStandardInfoStorage.Value.IntuneTemplateId + if ($global:CippStandardInfoStorage -and $global:CippStandardInfoStorage.Value) { + $TableRow.Standard = [string]$global:CippStandardInfoStorage.Value.Standard + $TableRow.StandardTemplateId = [string]$global:CippStandardInfoStorage.Value.StandardTemplateId + if ($global:CippStandardInfoStorage.Value.IntuneTemplateId) { + $TableRow.IntuneTemplateId = [string]$global:CippStandardInfoStorage.Value.IntuneTemplateId } - if ($script:CippStandardInfoStorage.Value.ConditionalAccessTemplateId) { - $TableRow.ConditionalAccessTemplateId = [string]$script:CippStandardInfoStorage.Value.ConditionalAccessTemplateId + if ($global:CippStandardInfoStorage.Value.ConditionalAccessTemplateId) { + $TableRow.ConditionalAccessTemplateId = [string]$global:CippStandardInfoStorage.Value.ConditionalAccessTemplateId } } - if ($script:CippScheduledTaskIdStorage -and $script:CippScheduledTaskIdStorage.Value) { - $TableRow.ScheduledTaskId = [string]$script:CippScheduledTaskIdStorage.Value + if ($global:CippScheduledTaskIdStorage -and $global:CippScheduledTaskIdStorage.Value) { + $TableRow.ScheduledTaskId = [string]$global:CippScheduledTaskIdStorage.Value } $Table.Entity = $TableRow diff --git a/Modules/CIPPCore/Public/Invoke-CIPPDBCacheCollection.ps1 b/Modules/CIPPCore/Public/Invoke-CIPPDBCacheCollection.ps1 index fa132ab38132c..5a4a570ee044a 100644 --- a/Modules/CIPPCore/Public/Invoke-CIPPDBCacheCollection.ps1 +++ b/Modules/CIPPCore/Public/Invoke-CIPPDBCacheCollection.ps1 @@ -91,7 +91,10 @@ function Invoke-CIPPDBCacheCollection { ExchangeData = @( 'CASMailboxes' 'MailboxUsage' + 'OneDriveSiteListing' 'OneDriveUsage' + 'SharePointSiteListing' + 'SharePointSiteUsage' 'OfficeActivations' ) ConditionalAccess = @( diff --git a/Modules/CIPPCore/Public/Invoke-CIPPRestMethod.ps1 b/Modules/CIPPCore/Public/Invoke-CIPPRestMethod.ps1 index 95d6c92054571..07e5301fdfac4 100644 --- a/Modules/CIPPCore/Public/Invoke-CIPPRestMethod.ps1 +++ b/Modules/CIPPCore/Public/Invoke-CIPPRestMethod.ps1 @@ -89,9 +89,11 @@ function Invoke-CIPPRestMethod { ) # ------------------------------------------------------------------ - # Escape hatch — env var kill switch or per-call legacy switch + # Escape hatch — env var kill switch, missing pooled client type, + # or per-call legacy switch # ------------------------------------------------------------------ - if ($UseLegacyInvokeRestMethod -or $env:DisableCIPPRestMethod -eq 'true') { + $HasCippRestClient = $null -ne ('CIPP.CIPPRestClient' -as [type]) + if ($UseLegacyInvokeRestMethod -or $env:DisableCIPPRestMethod -eq 'true' -or -not $HasCippRestClient) { $LegacyParams = @{ Uri = $Uri Method = $Method @@ -172,26 +174,45 @@ function Invoke-CIPPRestMethod { $TimeoutSec, $MaximumRedirection ) - } catch [System.Net.Http.HttpRequestException] { + } catch { + # PowerShell wraps .NET static method exceptions in MethodInvocationException. + # The actual HttpRequestException / OperationCanceledException is the InnerException. + $InnerEx = $_.Exception.InnerException ?? $_.Exception + + if ($InnerEx -is [System.OperationCanceledException]) { + $PSCmdlet.ThrowTerminatingError( + [System.Management.Automation.ErrorRecord]::new( + [System.TimeoutException]::new("The request to '$Uri' timed out after ${TimeoutSec}s.", $InnerEx), + 'RequestTimeout', + [System.Management.Automation.ErrorCategory]::OperationTimeout, + $Uri + ) + ) + return + } + + if ($InnerEx -is [System.Net.Http.HttpRequestException]) { + $PSCmdlet.ThrowTerminatingError( + [System.Management.Automation.ErrorRecord]::new( + $InnerEx, + 'HttpRequestFailed', + [System.Management.Automation.ErrorCategory]::ConnectionError, + $Uri + ) + ) + return + } + + # Unknown exception type — re-throw with the inner exception for a cleaner message $PSCmdlet.ThrowTerminatingError( [System.Management.Automation.ErrorRecord]::new( - $_.Exception, + $InnerEx, 'HttpRequestFailed', [System.Management.Automation.ErrorCategory]::ConnectionError, $Uri ) ) return - } catch [System.OperationCanceledException] { - $PSCmdlet.ThrowTerminatingError( - [System.Management.Automation.ErrorRecord]::new( - [System.TimeoutException]::new("The request to '$Uri' timed out after ${TimeoutSec}s."), - 'RequestTimeout', - [System.Management.Automation.ErrorCategory]::OperationTimeout, - $Uri - ) - ) - return } # ------------------------------------------------------------------ diff --git a/Modules/CIPPCore/Public/New-CIPPAlertTemplate.ps1 b/Modules/CIPPCore/Public/New-CIPPAlertTemplate.ps1 index cfb5743cf14bf..3cbe28edcd9e8 100644 --- a/Modules/CIPPCore/Public/New-CIPPAlertTemplate.ps1 +++ b/Modules/CIPPCore/Public/New-CIPPAlertTemplate.ps1 @@ -14,7 +14,7 @@ function New-CIPPAlertTemplate { $CustomSubject ) $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 - $TemplatePath = Join-Path $env:CIPPRootPath 'TemplateEmail.html' + $TemplatePath = Join-Path $env:CIPPRootPath 'Config\TemplateEmail.html' $HTMLTemplate = Get-Content $TemplatePath -Raw | Out-String $Title = '' $IntroText = '' @@ -32,7 +32,7 @@ function New-CIPPAlertTemplate { } if ($InputObject -eq 'driftStandard') { $Title = "CIPP Alert - Standard Drift Detected for $($Tenant)" - $DataHTML = ($Data | ConvertTo-Html | Out-String).Replace('', '
') + $DataHTML = ($Data | ConvertTo-Html -Fragment | Out-String).Replace('
', '
') $IntroText = "

You've setup your instance to receive alerts when a tenant is drifting away from your standard. This seems to have happened! We've found the following deviations.

$dataHTML" $ButtonUrl = "$CIPPURL/tenant/manage/drift?tenantFilter=$($Tenant)&templateId=$($AuditLogLink)" $ButtonText = 'Investigate and remediate deviations' @@ -40,24 +40,24 @@ function New-CIPPAlertTemplate { } if ($InputObject -eq 'sherwebmig') { - $DataHTML = ($Data | ConvertTo-Html | Out-String).Replace('
', '
') + $DataHTML = ($Data | ConvertTo-Html -Fragment | Out-String).Replace('
', '
') $IntroText = "

The following licenses have not yet been found at Sherweb, and are expiring within 7 days:

$dataHTML" if ($data.SherwebMig -like '*buy*') { $introText = "

The following licenses have not yet been found at Sherweb, and are expiring within 7 days. We have started the process to automatically buy these licenses:

$dataHTML" } } if ($InputObject -eq 'sherwebmigfailcancel') { - $DataHTML = ($Data | ConvertTo-Html | Out-String).Replace('
', '
') + $DataHTML = ($Data | ConvertTo-Html -Fragment | Out-String).Replace('
', '
') $IntroText = "

The following licenses have not been cancelled due to an API error at the old provider:

$dataHTML" } if ($InputObject -eq 'sherwebmigBuyFail') { - $DataHTML = ($Data | ConvertTo-Html | Out-String).Replace('
', '
') + $DataHTML = ($Data | ConvertTo-Html -Fragment | Out-String).Replace('
', '
') $IntroText = "

The following licenses have not been bought as we could not find a correctly matching license. Please login and buy the license:

$dataHTML" } if ($InputObject -eq 'table') { #data can be a array of strings or a string, if it is, we need to convert it to an object so it shows up nicely, that object will have one header: message. - $DataHTML = ($Data | Select-Object * -ExcludeProperty Etag, PartitionKey, TimeStamp | ConvertTo-Html | Out-String).Replace('
', '
') + $DataHTML = ($Data | Select-Object * -ExcludeProperty Etag, PartitionKey, TimeStamp | ConvertTo-Html -Fragment | Out-String).Replace('
', '
') $IntroText = "

You've configured CIPP to send you alerts based on the logbook. The following alerts match your configured rules

$dataHTML" # Add alert comment if provided diff --git a/Modules/CIPPCore/Public/Set-CIPPSPOTenant.ps1 b/Modules/CIPPCore/Public/Set-CIPPSPOTenant.ps1 index 6f7251b04034a..ea3e9ded38515 100644 --- a/Modules/CIPPCore/Public/Set-CIPPSPOTenant.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPSPOTenant.ps1 @@ -88,6 +88,14 @@ function Set-CIPPSPOTenant { if ($PSCmdlet.ShouldProcess(($Properties.Keys -join ', '), 'Set Tenant Properties')) { New-GraphPostRequest -scope "$AdminURL/.default" -tenantid $TenantFilter -Uri "$AdminURL/_vti_bin/client.svc/ProcessQuery" -Type POST -Body $XML -ContentType 'text/xml' -AddedHeaders $AdditionalHeaders + + # Invalidate cached tenant data so subsequent reads reflect the change + $Table = Get-CIPPTable -tablename 'cachespotenant' + $SafeTenantFilter = ConvertTo-CIPPODataFilterValue -Value $TenantFilter -Type String + $CacheEntity = Get-CIPPAzDataTableEntity @Table -Filter "PartitionKey eq 'Tenant' and RowKey eq '$SafeTenantFilter'" + if ($CacheEntity) { + Remove-AzDataTableEntity @Table -Entity $CacheEntity + } } } } diff --git a/Modules/CIPPCore/Public/Set-CIPPStandardsCompareField.ps1 b/Modules/CIPPCore/Public/Set-CIPPStandardsCompareField.ps1 index ab01757ac4e6f..f22252fc4165c 100644 --- a/Modules/CIPPCore/Public/Set-CIPPStandardsCompareField.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPStandardsCompareField.ps1 @@ -70,7 +70,7 @@ function Set-CIPPStandardsCompareField { 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 TemplateId -NotePropertyValue ([string]$global: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 @@ -79,7 +79,7 @@ function Set-CIPPStandardsCompareField { PartitionKey = [string]$TenantName.defaultDomainName RowKey = [string]$Field.FieldName Value = $NormalizedValue - TemplateId = [string]$script:CippStandardInfoStorage.Value.StandardTemplateId + TemplateId = [string]$global:CippStandardInfoStorage.Value.StandardTemplateId LicenseAvailable = [bool]$Field.LicenseAvailable CurrentValue = [string]$Field.CurrentValue ExpectedValue = [string]$Field.ExpectedValue @@ -106,7 +106,7 @@ function Set-CIPPStandardsCompareField { try { if ($Existing) { $Existing.Value = $NormalizedValue - $Existing | Add-Member -NotePropertyName TemplateId -NotePropertyValue ([string]$script:CippStandardInfoStorage.Value.StandardTemplateId) -Force + $Existing | Add-Member -NotePropertyName TemplateId -NotePropertyValue ([string]$global: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 @@ -116,7 +116,7 @@ function Set-CIPPStandardsCompareField { PartitionKey = [string]$TenantName.defaultDomainName RowKey = [string]$FieldName Value = $NormalizedValue - TemplateId = [string]$script:CippStandardInfoStorage.Value.StandardTemplateId + TemplateId = [string]$global:CippStandardInfoStorage.Value.StandardTemplateId LicenseAvailable = [bool]$LicenseAvailable CurrentValue = [string]$CurrentValue ExpectedValue = [string]$ExpectedValue diff --git a/Modules/CIPPCore/Public/Test-CIPPAccessPermissions.ps1 b/Modules/CIPPCore/Public/Test-CIPPAccessPermissions.ps1 index 9843f4c7de098..fe23662fcb033 100644 --- a/Modules/CIPPCore/Public/Test-CIPPAccessPermissions.ps1 +++ b/Modules/CIPPCore/Public/Test-CIPPAccessPermissions.ps1 @@ -115,11 +115,6 @@ function Test-CIPPAccessPermissions { } } $Success = $false - $Links.Add([PSCustomObject]@{ - Text = 'Permissions' - Href = 'https://docs.cipp.app/setup/installation/permissions' - } - ) | Out-Null } else { $Messages.Add('You have all the required permissions.') | Out-Null } diff --git a/Modules/CIPPCore/Public/Test-CIPPGDAPRelationships.ps1 b/Modules/CIPPCore/Public/Test-CIPPGDAPRelationships.ps1 index b078eead29325..590fef5f8ddf8 100644 --- a/Modules/CIPPCore/Public/Test-CIPPGDAPRelationships.ps1 +++ b/Modules/CIPPCore/Public/Test-CIPPGDAPRelationships.ps1 @@ -20,7 +20,7 @@ function Test-CIPPGDAPRelationships { Issue = 'This tenant only has a MLT(Microsoft Led Transition) relationship. This is a read-only relationship. You must migrate this tenant to GDAP.' Tenant = [string]$Tenant.Group.customer.displayName Relationship = [string]$Tenant.Group.displayName - Link = 'https://docs.cipp.app/setup/gdap/index' + Link = 'https://docs.cipp.app/setup/installation/gdap-invite-wizard' }) | Out-Null } foreach ($Group in $Tenant.Group) { diff --git a/Modules/CIPPCore/Public/Test-CustomScriptSecurity.ps1 b/Modules/CIPPCore/Public/Test-CustomScriptSecurity.ps1 index 0eb7f4b522b04..323ef953cb5f2 100644 --- a/Modules/CIPPCore/Public/Test-CustomScriptSecurity.ps1 +++ b/Modules/CIPPCore/Public/Test-CustomScriptSecurity.ps1 @@ -59,7 +59,10 @@ function Test-CustomScriptSecurity { 'ConvertTo-Json', 'ConvertFrom-Json', 'Write-Output', 'Write-Host', # CIPP data access (read-only) - 'New-CIPPDbRequest', 'Get-CIPPDbItem', 'Get-CIPPTestData' + 'New-CIPPDbRequest', 'Get-CIPPDbItem', 'Get-CIPPTestData', + + # Test specific methods + 'Add-CIPPTestResult' ) # Find all command invocations (exclude hashtable key assignments and property access) diff --git a/Modules/CIPPCore/Public/Tools/Get-CIPPSchedulerBlockedCommands.ps1 b/Modules/CIPPCore/Public/Tools/Get-CIPPSchedulerBlockedCommands.ps1 index 5887377fd3c07..5f5e997a0eb01 100644 --- a/Modules/CIPPCore/Public/Tools/Get-CIPPSchedulerBlockedCommands.ps1 +++ b/Modules/CIPPCore/Public/Tools/Get-CIPPSchedulerBlockedCommands.ps1 @@ -19,6 +19,10 @@ function Get-CIPPSchedulerBlockedCommands { 'Get-CIPPAuthentication' 'New-CIPPAzServiceSAS' + # Az Functions cmdlet + 'Get-CIPPAzFunctionAppSetting' + 'Update-CIPPAzFunctionAppSetting' + # Extension authentication tokens 'Get-GradientToken' 'Get-HaloToken' diff --git a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheOneDriveUsage.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheOneDriveUsage.ps1 index 152bd22aa1d5e..4dedf324a0c20 100644 --- a/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheOneDriveUsage.ps1 +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheOneDriveUsage.ps1 @@ -17,14 +17,52 @@ function Set-CIPPDBCacheOneDriveUsage { ) try { - 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 - $OneDriveUsage | ForEach-Object { $_ | Add-Member -NotePropertyName 'userPrincipalName' -NotePropertyValue $_.ownerPrincipalName -Force } - 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 Debug + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching OneDrive site listing and usage' -sev Debug + + $BulkRequests = @( + @{ + id = 'listAllSites' + method = 'GET' + url = "sites/getAllSites?`$filter=isPersonalSite eq true&`$select=id,createdDateTime,description,name,displayName,isPersonalSite,lastModifiedDateTime,webUrl,siteCollection,sharepointIds&`$top=999" + } + @{ + id = 'usage' + method = 'GET' + url = "reports/getOneDriveUsageAccountDetail(period='D7')?`$format=application/json&`$top=999" + } + ) + + $Result = New-GraphBulkRequest -tenantid $TenantFilter -Requests @($BulkRequests) -asapp $true + $Sites = @(($Result | Where-Object { $_.id -eq 'listAllSites' }).body.value) + $UsageBase64 = ($Result | Where-Object { $_.id -eq 'usage' }).body + $UsageJson = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($UsageBase64)) + $OneDriveUsage = @(($UsageJson | ConvertFrom-Json).value) + + foreach ($UsageRow in $OneDriveUsage) { + $UsageRow | Add-Member -NotePropertyName 'id' -NotePropertyValue $UsageRow.siteId -Force + $UsageRow | Add-Member -NotePropertyName 'userPrincipalName' -NotePropertyValue $UsageRow.ownerPrincipalName -Force + } + + $OneDriveListing = [System.Collections.Generic.List[object]]::new() + foreach ($Site in $Sites) { + $OneDriveListing.Add([PSCustomObject]@{ + id = $Site.id + sharepointIds = $Site.sharepointIds + createdDateTime = $Site.createdDateTime + displayName = $Site.displayName + webUrl = $Site.webUrl + isPersonalSite = $Site.isPersonalSite + AutoMapUrl = '' + }) + } + + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'OneDriveSiteListing' -Data @($OneDriveListing) + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'OneDriveSiteListing' -Data @($OneDriveListing) -Count + + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'OneDriveUsage' -Data @($OneDriveUsage) + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'OneDriveUsage' -Data @($OneDriveUsage) -Count + + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached OneDrive site listing and 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/CIPPDB/Public/DBCache/Set-CIPPDBCacheSharePointSiteUsage.ps1 b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheSharePointSiteUsage.ps1 new file mode 100644 index 0000000000000..90ab81351b7af --- /dev/null +++ b/Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheSharePointSiteUsage.ps1 @@ -0,0 +1,96 @@ +function Set-CIPPDBCacheSharePointSiteUsage { + <# + .SYNOPSIS + Caches SharePoint site listing and site usage details for a tenant + + .PARAMETER TenantFilter + The tenant to cache SharePoint site usage 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 SharePoint site listing and usage' -sev Debug + + $Tenant = Get-Tenants -TenantFilter $TenantFilter + $TenantId = $Tenant.customerId + + $BulkRequests = @( + @{ + id = 'listAllSites' + method = 'GET' + url = "sites/getAllSites?`$filter=isPersonalSite eq false&`$select=id,createdDateTime,description,name,displayName,isPersonalSite,lastModifiedDateTime,webUrl,siteCollection,sharepointIds&`$top=999" + } + @{ + id = 'usage' + method = 'GET' + url = "reports/getSharePointSiteUsageDetail(period='D7')?`$format=application/json&`$top=999" + } + ) + + $Result = New-GraphBulkRequest -tenantid $TenantFilter -Requests @($BulkRequests) -asapp $true + $Sites = @(($Result | Where-Object { $_.id -eq 'listAllSites' }).body.value) + $UsageBase64 = ($Result | Where-Object { $_.id -eq 'usage' }).body + $UsageJson = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($UsageBase64)) + $UsageRows = @(($UsageJson | ConvertFrom-Json).value) + + # Ensure a stable row key for usage rows. + foreach ($UsageRow in $UsageRows) { + $UsageRow | Add-Member -NotePropertyName 'id' -NotePropertyValue $UsageRow.siteId -Force + } + + $SiteListing = [System.Collections.Generic.List[object]]::new() + foreach ($Site in $Sites) { + $SiteListing.Add([PSCustomObject]@{ + id = $Site.id + sharepointIds = $Site.sharepointIds + createdDateTime = $Site.createdDateTime + displayName = $Site.displayName + webUrl = $Site.webUrl + isPersonalSite = $Site.isPersonalSite + AutoMapUrl = '' + }) + } + + $RequestId = 0 + $ListRequests = foreach ($Site in $SiteListing) { + @{ + id = $RequestId++ + method = 'GET' + url = "sites/$($Site.sharepointIds.siteId)/lists?`$select=id,name,list,parentReference" + } + } + + $LibraryLists = @() + if ($ListRequests.Count -gt 0) { + try { + $LibraryLists = @((New-GraphBulkRequest -tenantid $TenantFilter -scope 'https://graph.microsoft.com/.default' -Requests @($ListRequests) -asapp $true).body.value | Where-Object { $_.list.template -eq 'DocumentLibrary' }) + } catch { + Write-LogMessage -Message "Error getting auto map urls for SharePoint cache: $($_.Exception.Message)" -Sev 'Error' -tenant $TenantFilter -API 'CIPPDBCache' -LogData (Get-CippException -Exception $_) + } + } + + foreach ($Site in $SiteListing) { + $ListId = ($LibraryLists | Where-Object { $_.parentReference.siteId -like "*$($Site.sharepointIds.siteId)*" } | Select-Object -First 1 -ExpandProperty id) + $Site.AutoMapUrl = "tenantId=$($TenantId)&webId={$($Site.sharepointIds.webId)}&siteid={$($Site.sharepointIds.siteId)}&webUrl=$($Site.webUrl)&listId={$($ListId)}" + } + + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'SharePointSiteListing' -Data @($SiteListing) + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'SharePointSiteListing' -Data @($SiteListing) -Count + + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'SharePointSiteUsage' -Data @($UsageRows) + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'SharePointSiteUsage' -Data @($UsageRows) -Count + + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached SharePoint site listing and usage successfully' -sev Debug + + } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache SharePoint site usage: $($_.Exception.Message)" -sev Error -LogData (Get-CippException -Exception $_) + } +} \ No newline at end of file diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ExecCIPPDBCache.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ExecCIPPDBCache.ps1 index 51a56d7ff61a0..0a4d9b5e1fe2b 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ExecCIPPDBCache.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ExecCIPPDBCache.ps1 @@ -13,6 +13,11 @@ function Invoke-ExecCIPPDBCache { $Name = $Request.Query.Name $Types = $Request.Query.Types + $ParsedTypes = @() + if (-not [string]::IsNullOrWhiteSpace($Types)) { + $ParsedTypes = @($Types -split ',' | ForEach-Object { $_.Trim() } | Where-Object { -not [string]::IsNullOrWhiteSpace($_) -and $_ -ne 'None' }) + } + Write-Information "ExecCIPPDBCache called with Name: '$Name', TenantFilter: '$TenantFilter', Types: '$Types'" try { @@ -66,8 +71,8 @@ function Invoke-ExecCIPPDBCache { QueueId = $Queue.RowKey } # Add Types parameter if provided - if ($Types) { - $BatchItem | Add-Member -NotePropertyName 'Types' -NotePropertyValue @($Types -split ',') -Force + if ($ParsedTypes.Count -gt 0) { + $BatchItem | Add-Member -NotePropertyName 'Types' -NotePropertyValue $ParsedTypes -Force } $BatchItem } @@ -91,8 +96,8 @@ function Invoke-ExecCIPPDBCache { QueueId = $Queue.RowKey } # Add Types parameter if provided - if ($Types) { - $BatchItem | Add-Member -NotePropertyName 'Types' -NotePropertyValue @($Types -split ',') -Force + if ($ParsedTypes.Count -gt 0) { + $BatchItem | Add-Member -NotePropertyName 'Types' -NotePropertyValue $ParsedTypes -Force } $InputObject = [PSCustomObject]@{ diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-GetVersion.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-GetVersion.ps1 index 61a8266714f32..8f98217f1ed2b 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-GetVersion.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-GetVersion.ps1 @@ -1,9 +1,9 @@ -Function Invoke-GetVersion { +function Invoke-GetVersion { <# .FUNCTIONALITY Entrypoint,AnyTenant .ROLE - CIPP.AppSettings.Read + CIPP.Core.Read #> [CmdletBinding()] param($Request, $TriggerMetadata) @@ -15,5 +15,4 @@ Function Invoke-GetVersion { StatusCode = [HttpStatusCode]::OK Body = $Version }) - } diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecApiClient.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecApiClient.ps1 index 4452b4023795e..8315d00c1ca67 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecApiClient.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecApiClient.ps1 @@ -29,6 +29,7 @@ function Invoke-ExecApiClient { } } 'AddUpdate' { + $Results = [System.Collections.Generic.List[object]]::new() if ($Request.Body.ClientId -or $Request.Body.AppName) { $ClientId = $Request.Body.ClientId.value ?? $Request.Body.ClientId $AddUpdateSuccess = $false @@ -75,12 +76,16 @@ function Invoke-ExecApiClient { } } + $IPValidationErrors = [System.Collections.Generic.List[string]]::new() if ($Request.Body.IpRange.value) { $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.value)) { + $IP = $IP.Trim() if ($IP -match $regexPattern) { $IpRange.Add($IP) + } else { + $IPValidationErrors.Add("'$IP' is not a valid IP address or CIDR range.") } } } else { @@ -88,8 +93,8 @@ function Invoke-ExecApiClient { } if (!$AddUpdateSuccess) { - $Body = @{ - Results = @($AddedText) + if ($AddedText) { + $Results.Add($AddedText) } } else { $ExistingClient = Get-CIPPAzDataTableEntity @Table -Filter "RowKey eq '$($ClientId)'" @@ -100,13 +105,13 @@ function Invoke-ExecApiClient { $Client.Enabled = $Request.Body.Enabled ?? $false Write-LogMessage -headers $Request.Headers -API 'ExecApiClient' -message "Updated API client $($Request.Body.ClientId)" -Sev 'Info' if ($APIConfig.ApplicationSecret) { - $Results = @{ - resultText = "API client updated and application secret reset for '$($Client.AppName)'. Use the Copy to Clipboard button to retrieve the new secret." - copyField = $APIConfig.ApplicationSecret - state = 'success' - } + $Results.Add(@{ + resultText = "API client updated and application secret reset for '$($Client.AppName)'. Use the Copy to Clipboard button to retrieve the new secret." + copyField = $APIConfig.ApplicationSecret + state = 'success' + }) } else { - $Results = 'API client updated' + $Results.Add('API client updated') } } else { $Client = @{ @@ -117,14 +122,30 @@ function Invoke-ExecApiClient { 'IPRange' = "$(@($IpRange) | ConvertTo-Json -Compress)" 'Enabled' = $Request.Body.Enabled ?? $false } - $Results = @{ - resultText = "API Client created with the name '$($Client.AppName)'. Use the Copy to Clipboard button to retrieve the secret." - copyField = $APIConfig.ApplicationSecret - state = 'success' - } + $Results.Add(@{ + resultText = "API Client created with the name '$($Client.AppName)'. Use the Copy to Clipboard button to retrieve the secret." + copyField = $APIConfig.ApplicationSecret + state = 'success' + }) } Add-CIPPAzDataTableEntity @Table -Entity $Client -Force | Out-Null + } + + if ($IPValidationErrors.Count -gt 0) { + foreach ($ValidationError in $IPValidationErrors) { + $Results.Add(@{ + resultText = $ValidationError + state = 'warning' + }) + } + } + + if (!$AddUpdateSuccess) { + $Body = @{ + Results = @($Results) + } + } else { $Body = @($Results) } } diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecRestoreBackup.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecRestoreBackup.ps1 index 5913bf9275e72..fb26f73d997c1 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecRestoreBackup.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecRestoreBackup.ps1 @@ -14,7 +14,7 @@ function Invoke-ExecRestoreBackup { $AzureTableTypes = @( [string], [int], [long], [double], [bool], [datetime], [guid], [byte[]] ) - $RestrictedTables = @('AccessRoleGroups', 'CustomRoles') # tables that require superadmin to restore + $RestrictedTables = @('AccessRoleGroups', 'AccessIPRanges', 'CustomRoles') # tables that require superadmin to restore # Resolve the calling user's roles, including Entra group-based roles $CallingUser = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($Request.Headers.'x-ms-client-principal')) | ConvertFrom-Json diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-EditUser.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-EditUser.ps1 index 2c631f69154f5..b6f95c20f1127 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-EditUser.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-EditUser.ps1 @@ -190,7 +190,7 @@ function Invoke-EditUser { Write-Host "About to add $($UserObj.userPrincipalName) to $GroupName. Group ID is: $GroupID and type is: $GroupType" try { - if ($GroupType -eq 'Distribution list' -or $GroupType -eq 'Mail-Enabled Security') { + if ($GroupType -eq 'distributionList' -or $GroupType -eq 'security') { Write-Host 'Adding to group via Add-DistributionGroupMember' $Params = @{ Identity = $GroupID; Member = $UserObj.id; BypassSecurityGroupManagerCheck = $true } $null = New-ExoRequest -tenantid $UserObj.tenantFilter -cmdlet 'Add-DistributionGroupMember' -cmdParams $params -UseSystemMailbox $true @@ -222,7 +222,7 @@ function Invoke-EditUser { Write-Host "About to remove $($UserObj.userPrincipalName) from $GroupName. Group ID is: $GroupID and type is: $GroupType" try { - if ($GroupType -eq 'Distribution list' -or $GroupType -eq 'Mail-Enabled Security') { + if ($GroupType -eq 'distributionList' -or $GroupType -eq 'security') { Write-Host 'Removing From group via Remove-DistributionGroupMember' $Params = @{ Identity = $GroupID; Member = $UserObj.id; BypassSecurityGroupManagerCheck = $true } $null = New-ExoRequest -tenantid $UserObj.tenantFilter -cmdlet 'Remove-DistributionGroupMember' -cmdParams $params -UseSystemMailbox $true diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ExecTestRefresh.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ExecTestRefresh.ps1 new file mode 100644 index 0000000000000..19f79b929263e --- /dev/null +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Invoke-ExecTestRefresh.ps1 @@ -0,0 +1,41 @@ +function Invoke-ExecTestRefresh { + <# + .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 + $TestName = $Request.Query.testName ?? $Request.Body.testName + $Function = 'Invoke-CippTest{0}' -f $TestName + if (Get-Command -Name $Function -Module 'CIPPTests' -ErrorAction SilentlyContinue) { + $TestResult = & $Function -Tenant $TenantFilter + $Table = Get-CippTable -tablename 'CippTestResults' + Add-CIPPAzDataTableEntity @Table -Entity $TestResult -Force + $StatusCode = [HttpStatusCode]::OK + $Body = [PSCustomObject]@{ Results = "Successfully updated test $TestName for tenant $TenantFilter"; Metadata = $TestResult } + } else { + return ([HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::NotFound + Body = @{ Message = "Test function not found: $Function" } + }) + } + } catch { + $StatusCode = [HttpStatusCode]::BadRequest + $Body = @{ + Message = "Failed to update test $TestName for $TenantFilter" + Error = Get-CippException -Exception $_ + } + } + + return ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = $Body + }) +} diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListSites.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListSites.ps1 index dd112c3f16eca..d8aae26f59985 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListSites.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListSites.ps1 @@ -12,7 +12,7 @@ function Invoke-ListSites { $TenantFilter = $Request.Query.TenantFilter $Type = $Request.Query.Type - $UserUPN = $Request.Query.UserUPN + $UseReportDB = $Request.Query.UseReportDB if (!$TenantFilter) { return ([HttpResponseContext]@{ @@ -28,6 +28,31 @@ function Invoke-ListSites { }) } + if ($TenantFilter -eq 'AllTenants' -or $UseReportDB -eq 'true') { + try { + if ($Type -eq 'SharePointSiteUsage') { + $GraphRequest = Get-CIPPSharePointSiteUsageReport -TenantFilter $TenantFilter -ErrorAction Stop + } elseif ($Type -eq 'OneDriveUsageAccount') { + $GraphRequest = Get-CIPPOneDriveUsageReport -TenantFilter $TenantFilter -ErrorAction Stop + } + $StatusCode = [HttpStatusCode]::OK + } catch { + $StatusCode = [HttpStatusCode]::InternalServerError + $GraphRequest = $_.Exception.Message + } + + if ($null -ne $GraphRequest) { + if ($Request.query.URLOnly -eq 'true') { + $GraphRequest = $GraphRequest | Where-Object { $null -ne $_.webUrl } + } + + return ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = @($GraphRequest | Sort-Object -Property displayName) + }) + } + } + $Tenant = Get-Tenants -TenantFilter $TenantFilter $TenantId = $Tenant.customerId diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Conditional/Invoke-ExecCAExclusion.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Conditional/Invoke-ExecCAExclusion.ps1 index 47eb7477cc389..f8936e91f4d1a 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Conditional/Invoke-ExecCAExclusion.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Conditional/Invoke-ExecCAExclusion.ps1 @@ -37,7 +37,9 @@ function Invoke-ExecCAExclusion { $VacationGroupName = "Vacation Exclusion - $($Policy.displayName)" $escapedGroupName = $VacationGroupName -replace "'", "''" - $VacationGroups = @(New-GraphGetRequest -uri "https://graph.microsoft.com/beta/groups?`$select=id,displayName&`$filter=displayName eq '$escapedGroupName' and mailEnabled eq false and securityEnabled eq true" -tenantid $TenantFilter) + $groupFilter = "displayName eq '$escapedGroupName' and mailEnabled eq false and securityEnabled eq true" + $encodedGroupFilter = [System.Uri]::EscapeDataString($groupFilter) + $VacationGroups = @(New-GraphGetRequest -uri "https://graph.microsoft.com/beta/groups?`$select=id,displayName&`$filter=$encodedGroupFilter" -tenantid $TenantFilter) $DuplicateGroupWarning = $null if ($VacationGroups.Count -eq 0) { diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/GDAP/Invoke-ListGDAPContracts.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/GDAP/Invoke-ListGDAPContracts.ps1 index 0fdf23c0c47bb..a5d762ea88c42 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/GDAP/Invoke-ListGDAPContracts.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/GDAP/Invoke-ListGDAPContracts.ps1 @@ -13,7 +13,7 @@ function Invoke-ListGDAPContracts { $Uri = "https://graph.microsoft.com/beta/contracts?`$top=$Top" try { - $Results = New-GraphGetRequest -Uri $Uri -tenantid $env:TenantID -NoAuthCheck $true -NoPagination $true -ComplexFilter + $Results = New-GraphGetRequest -Uri $Uri -tenantid $env:TenantID -NoAuthCheck $true -ComplexFilter $Body = @{ Results = @($Results) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/GDAP/Invoke-ListGDAPRelationships.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/GDAP/Invoke-ListGDAPRelationships.ps1 index f5893471754ab..c332fc11236e9 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/GDAP/Invoke-ListGDAPRelationships.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/GDAP/Invoke-ListGDAPRelationships.ps1 @@ -22,7 +22,7 @@ function Invoke-ListGDAPRelationships { if ($Filter) { $Uri = "$Uri&`$filter=$Filter" } - $Results = New-GraphGetRequest -Uri $Uri -tenantid $env:TenantID -NoAuthCheck $true -NoPagination $true -ComplexFilter + $Results = New-GraphGetRequest -Uri $Uri -tenantid $env:TenantID -NoAuthCheck $true -ComplexFilter } $Body = @{ diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/GDAP/Invoke-ListGDAPServicePrincipals.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/GDAP/Invoke-ListGDAPServicePrincipals.ps1 index ff6c3b2ee1829..2b5cda845dcf6 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/GDAP/Invoke-ListGDAPServicePrincipals.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/GDAP/Invoke-ListGDAPServicePrincipals.ps1 @@ -49,7 +49,7 @@ function Invoke-ListGDAPServicePrincipals { $Uri = "https://graph.microsoft.com/beta/servicePrincipals?`$top=$Top&`$select=$Select&`$count=true&`$filter=$Filter" try { - $Results = New-GraphGetRequest -Uri $Uri -tenantid $TenantFilter -NoPagination $true -ComplexFilter + $Results = New-GraphGetRequest -Uri $Uri -tenantid $TenantFilter -ComplexFilter $Body = @{ Results = @($Results) diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ExecUpdateDriftDeviation.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ExecUpdateDriftDeviation.ps1 index d30a4e899fcd5..78c556cf7abef 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ExecUpdateDriftDeviation.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ExecUpdateDriftDeviation.ps1 @@ -43,6 +43,7 @@ function Invoke-ExecUpdateDriftDeviation { if ($Deviation.status -eq 'DeniedRemediate') { $Setting = $Deviation.standardName -replace 'standards\.', '' $StandardTemplate = Get-CIPPTenantAlignment -TenantFilter $TenantFilter | Where-Object -Property standardType -EQ 'drift' + $DriftTemplateId = $StandardTemplate.StandardId if ($Setting -like '*IntuneTemplate*') { $Setting = 'IntuneTemplate' $TemplateId = $Deviation.standardName.split('.') | Select-Object -Index 2 @@ -91,7 +92,8 @@ function Invoke-ExecUpdateDriftDeviation { } $TaskBody = @{ TenantFilter = $TenantFilter - Name = "One Off Drift Remediation: $Setting - $TenantFilter" + Name = "One Off Drift Remediation: $Setting - $TenantFilter - $DriftTemplateId" + Tag = "DriftRemediation_$DriftTemplateId" Command = @{ value = "Invoke-CIPPStandard$Setting" label = "Invoke-CIPPStandard$Setting" @@ -114,7 +116,8 @@ function Invoke-ExecUpdateDriftDeviation { if ($PersistentDeny) { $PersistentTaskBody = @{ TenantFilter = $TenantFilter - Name = "Persistent Drift Remediation: $Setting - $TenantFilter" + Name = "Persistent Drift Remediation: $Setting - $TenantFilter - $DriftTemplateId" + Tag = "DriftRemediation_$DriftTemplateId" Command = @{ value = "Invoke-CIPPStandard$Setting" label = "Invoke-CIPPStandard$Setting" diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-RemoveStandardTemplate.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-RemoveStandardTemplate.ps1 index 8289b20e28294..94444123db7f2 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-RemoveStandardTemplate.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-RemoveStandardTemplate.ps1 @@ -26,7 +26,20 @@ function Invoke-RemoveStandardTemplate { } $Entities = Get-AzDataTableEntity @Table -Filter $Filter Remove-AzDataTableEntity -Force @Table -Entity $Entities + + # Remove any drift remediation scheduled tasks associated with this template + $ScheduledTasksTable = Get-CIPPTable -TableName 'ScheduledTasks' + $SafeTag = ConvertTo-CIPPODataFilterValue -Value "DriftRemediation_$SafeID" + $DriftTasks = Get-CIPPAzDataTableEntity @ScheduledTasksTable -Filter "PartitionKey eq 'ScheduledTask' and Tag eq '$SafeTag'" + foreach ($DriftTask in $DriftTasks) { + Remove-AzDataTableEntity -Force @ScheduledTasksTable -Entity $DriftTask + Write-LogMessage -Headers $Headers -API $APIName -message "Removed drift remediation scheduled task: $($DriftTask.Name)" -Sev Info + } + $Result = "Removed Standards Template named: '$($TemplateName)' with id: $($ID)" + if ($DriftTasks) { + $Result += ". Also removed $(@($DriftTasks).Count) associated drift remediation scheduled task(s)." + } Write-LogMessage -Headers $Headers -API $APIName -message $Result -Sev Info $StatusCode = [HttpStatusCode]::OK } catch { diff --git a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tools/GitHub/Invoke-ListCommunityRepos.ps1 b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tools/GitHub/Invoke-ListCommunityRepos.ps1 index 9c1a246e30261..42ed1cad9c171 100644 --- a/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tools/GitHub/Invoke-ListCommunityRepos.ps1 +++ b/Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tools/GitHub/Invoke-ListCommunityRepos.ps1 @@ -23,7 +23,7 @@ function Invoke-ListCommunityRepos { $Repos = Get-CIPPAzDataTableEntity @Table -Filter $Filter if (!$Request.Query.WriteAccess) { - $CommunityRepos = Join-Path $env:CIPPRootPath 'CommunityRepos.json' + $CommunityRepos = Join-Path $env:CIPPRootPath 'Config\CommunityRepos.json' $DefaultCommunityRepos = [System.IO.File]::ReadAllText($CommunityRepos) | ConvertFrom-Json $DefaultsMissing = $false diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDelegateSentItems.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDelegateSentItems.ps1 index 630fae2c07470..fb2bb660567b6 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDelegateSentItems.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDelegateSentItems.ps1 @@ -51,7 +51,7 @@ function Invoke-CIPPStandardDelegateSentItems { } $Mailboxes = New-CippDbRequest -TenantFilter $Tenant -Type 'Mailboxes' if ($Settings.IncludeUserMailboxes -eq $true) { - $Mailboxes = $Mailboxes | Where-Object { $_.recipientTypeDetails -ne 'DiscoveryMailbox' -and ($_.MessageCopyForSendOnBehalfEnabled -eq $false -or $_.MessageCopyForSentAsEnabled -eq $false) } + $Mailboxes = $Mailboxes | Where-Object { $_.recipientTypeDetails -in @('UserMailbox', 'SharedMailbox') -and ($_.MessageCopyForSendOnBehalfEnabled -eq $false -or $_.MessageCopyForSentAsEnabled -eq $false) } } else { $Mailboxes = $Mailboxes | Where-Object { $_.recipientTypeDetails -eq 'SharedMailbox' -and ($_.MessageCopyForSendOnBehalfEnabled -eq $false -or $_.MessageCopyForSentAsEnabled -eq $false) } } diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDisableEWS.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDisableEWS.ps1 new file mode 100644 index 0000000000000..871aacadaef7a --- /dev/null +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDisableEWS.ps1 @@ -0,0 +1,90 @@ +function Invoke-CIPPStandardDisableEWS { + <# + .FUNCTIONALITY + Internal + .COMPONENT + (APIName) DisableEWS + .SYNOPSIS + (Label) Disable Exchange Web Services + .DESCRIPTION + (Helptext) Disables Exchange Web Services (EWS) organization-wide. This reduces the attack surface by blocking legacy API access to mailbox data. Warning: This may break Office web add-ins on builds older than 16.0.19127. + (DocsDescription) Disables Exchange Web Services (EWS) at the organization level to reduce attack surface. EWS provides cross-platform API access to sensitive Exchange Online data such as emails, meetings, and contacts. If compromised, attackers can access confidential data, send phishing emails, or spoof identities. Disabling EWS also reduces legacy app usage and minimizes exploitable endpoints. Note that this may break first-party features including web add-ins for Word, Excel, PowerPoint, and Outlook on builds older than 16.0.19127. + .NOTES + CAT + Exchange Standards + TAG + EXECUTIVETEXT + Disables Exchange Web Services (EWS) across the organization to reduce attack surface and prevent legacy API access to sensitive mailbox data. This aligns with Microsoft's Baseline Security Mode recommendation to minimize exploitable endpoints while requiring updates to applications that depend on EWS. + ADDEDCOMPONENT + IMPACT + High Impact + ADDEDDATE + 2026-04-28 + POWERSHELLEQUIVALENT + Set-OrganizationConfig -EwsEnabled $false + RECOMMENDEDBY + "CIPP" + REQUIREDCAPABILITIES + "EXCHANGE_S_STANDARD" + "EXCHANGE_S_ENTERPRISE" + "EXCHANGE_S_STANDARD_GOV" + "EXCHANGE_S_ENTERPRISE_GOV" + "EXCHANGE_LITE" + UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block + .LINK + https://docs.cipp.app/user-documentation/tenant/standards/list-standards + #> + + param($Tenant, $Settings) + $TestResult = Test-CIPPStandardLicense -StandardName 'DisableEWS' -TenantFilter $Tenant -RequiredCapabilities @('EXCHANGE_S_STANDARD', 'EXCHANGE_S_ENTERPRISE', 'EXCHANGE_S_STANDARD_GOV', 'EXCHANGE_S_ENTERPRISE_GOV', 'EXCHANGE_LITE') + + if ($TestResult -eq $false) { + return $true + } + + try { + $EwsStatus = (New-ExoRequest -tenantid $Tenant -cmdlet 'Get-OrganizationConfig').EwsEnabled + } catch { + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the DisableEWS state for $Tenant. Error: $ErrorMessage" -Sev Error + return + } + + if ($Settings.remediate -eq $true) { + if ($EwsStatus -eq $false) { + Write-LogMessage -API 'Standards' -Tenant $Tenant -Message 'Exchange Web Services is already disabled.' -Sev Info + } else { + try { + New-ExoRequest -tenantid $Tenant -cmdlet 'Set-OrganizationConfig' -cmdParams @{ EwsEnabled = $false } -UseSystemMailbox $true + Write-LogMessage -API 'Standards' -Tenant $Tenant -Message 'Successfully disabled Exchange Web Services.' -Sev Info + $EwsStatus = $false + } catch { + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Failed to disable Exchange Web Services. Error: $ErrorMessage" -Sev Error + } + } + } + + if ($Settings.alert -eq $true) { + if ($EwsStatus -eq $false) { + Write-LogMessage -API 'Standards' -Tenant $Tenant -Message 'Exchange Web Services is disabled.' -Sev Info + } else { + Write-StandardsAlert -message 'Exchange Web Services is enabled.' -object $EwsStatus -tenant $Tenant -standardName 'DisableEWS' -standardId $Settings.standardId + Write-LogMessage -API 'Standards' -Tenant $Tenant -Message 'Exchange Web Services is enabled.' -Sev Info + } + } + + if ($Settings.report -eq $true) { + $StateIsCorrect = $EwsStatus -eq $false + + $CurrentValue = [PSCustomObject]@{ + DisableEWS = $StateIsCorrect + } + $ExpectedValue = [PSCustomObject]@{ + DisableEWS = $true + } + Set-CIPPStandardsCompareField -FieldName 'standards.DisableEWS' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant + Add-CIPPBPAField -FieldName 'DisableEWS' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $Tenant + } +} diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDisableEntraPortal.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDisableEntraPortal.ps1 index bd774e10251c9..88cfc645fec0b 100644 --- a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDisableEntraPortal.ps1 +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardDisableEntraPortal.ps1 @@ -6,7 +6,7 @@ function Invoke-CIPPStandardDisableEntraPortal { (APIName) DisableEntraPortal .SYNOPSIS (Label) Disables the Entra Portal for standard users - https://docs.cipp.app/user-documentation/tenant/standards/edit-standards + https://docs.cipp.app/user-documentation/tenant/standards/list-standards #> param($Tenant, $Settings) diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSPDisableCustomScripts.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSPDisableCustomScripts.ps1 new file mode 100644 index 0000000000000..61ad08f64475c --- /dev/null +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSPDisableCustomScripts.ps1 @@ -0,0 +1,98 @@ +function Invoke-CIPPStandardSPDisableCustomScripts { + <# + .FUNCTIONALITY + Internal + .COMPONENT + (APIName) SPDisableCustomScripts + .SYNOPSIS + (Label) Disable custom scripts on SharePoint sites + .DESCRIPTION + (Helptext) Prevents users from running custom scripts on SharePoint and OneDrive sites. Custom scripts can modify site behaviors and bypass governance controls. + (DocsDescription) Disables the ability to add and run custom scripts on SharePoint and OneDrive sites at the tenant level. When custom scripts are allowed, governance cannot be enforced, and the capabilities of inserted code cannot be scoped or blocked. Microsoft recommends using the SharePoint Framework instead of custom scripts. + .NOTES + CAT + SharePoint Standards + TAG + EXECUTIVETEXT + Blocks custom scripts from being added to SharePoint and OneDrive sites, enforcing governance controls and preventing unscoped code execution. This aligns with Microsoft's Baseline Security Mode recommendation to permanently remove the ability to add new custom scripts, directing organizations to use the SharePoint Framework instead. + ADDEDCOMPONENT + IMPACT + High Impact + ADDEDDATE + 2026-04-28 + POWERSHELLEQUIVALENT + Set-SPOTenant -CustomScriptsRestrictMode $true + RECOMMENDEDBY + "CIPP" + REQUIREDCAPABILITIES + "SHAREPOINTWAC" + "SHAREPOINTSTANDARD" + "SHAREPOINTENTERPRISE" + "SHAREPOINTENTERPRISE_EDU" + "ONEDRIVE_BASIC" + "ONEDRIVE_ENTERPRISE" + UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block + .LINK + https://docs.cipp.app/user-documentation/tenant/standards/list-standards + #> + + param($Tenant, $Settings) + $TestResult = Test-CIPPStandardLicense -StandardName 'SPDisableCustomScripts' -TenantFilter $Tenant -RequiredCapabilities @('SHAREPOINTWAC', 'SHAREPOINTSTANDARD', 'SHAREPOINTENTERPRISE', 'SHAREPOINTENTERPRISE_EDU', 'ONEDRIVE_BASIC', 'ONEDRIVE_ENTERPRISE') + + if ($TestResult -eq $false) { + return $true + } + + try { + $CurrentState = Get-CIPPSPOTenant -TenantFilter $Tenant + } catch { + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the SPDisableCustomScripts state for $Tenant. Error: $ErrorMessage" -Sev Error + return + } + + if ($null -eq $CurrentState._ObjectIdentity_) { + $ErrorDetail = $CurrentState.ErrorInfo ?? 'No tenant data returned from CSOM query' + Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the SPDisableCustomScripts state for $Tenant. Error: $ErrorDetail" -Sev Error + return + } + + # CSOM property is CustomScriptsRestrictMode (true = scripts blocked) + # NoScriptSite is only a PnP/SPO PowerShell parameter name, not a CSOM property + $StateIsCorrect = ($CurrentState.CustomScriptsRestrictMode -eq $true) + + if ($Settings.remediate -eq $true) { + if ($StateIsCorrect -eq $true) { + Write-LogMessage -API 'Standards' -Tenant $Tenant -Message 'Custom scripts are already disabled on SharePoint sites.' -Sev Info + } else { + try { + $CurrentState | Set-CIPPSPOTenant -Properties @{ CustomScriptsRestrictMode = $true } + Write-LogMessage -API 'Standards' -Tenant $Tenant -Message 'Successfully disabled custom scripts on SharePoint sites.' -Sev Info + } catch { + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Failed to disable custom scripts on SharePoint sites. Error: $ErrorMessage" -Sev Error + } + } + } + + if ($Settings.alert -eq $true) { + if ($StateIsCorrect -eq $true) { + Write-LogMessage -API 'Standards' -Tenant $Tenant -Message 'Custom scripts are disabled on SharePoint sites.' -Sev Info + } else { + Write-StandardsAlert -message 'Custom scripts are enabled on SharePoint sites.' -object $CurrentState -tenant $Tenant -standardName 'SPDisableCustomScripts' -standardId $Settings.standardId + Write-LogMessage -API 'Standards' -Tenant $Tenant -Message 'Custom scripts are enabled on SharePoint sites.' -Sev Info + } + } + + if ($Settings.report -eq $true) { + $CurrentValue = [PSCustomObject]@{ + SPDisableCustomScripts = $StateIsCorrect + } + $ExpectedValue = [PSCustomObject]@{ + SPDisableCustomScripts = $true + } + Set-CIPPStandardsCompareField -FieldName 'standards.SPDisableCustomScripts' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant + Add-CIPPBPAField -FieldName 'SPDisableCustomScripts' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $Tenant + } +} diff --git a/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSPDisableStoreAccess.ps1 b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSPDisableStoreAccess.ps1 new file mode 100644 index 0000000000000..593f40f925014 --- /dev/null +++ b/Modules/CIPPStandards/Public/Standards/Invoke-CIPPStandardSPDisableStoreAccess.ps1 @@ -0,0 +1,97 @@ +function Invoke-CIPPStandardSPDisableStoreAccess { + <# + .FUNCTIONALITY + Internal + .COMPONENT + (APIName) SPDisableStoreAccess + .SYNOPSIS + (Label) Disable SharePoint Store access + .DESCRIPTION + (Helptext) Disables end users from installing applications from the Microsoft Store into SharePoint sites. + (DocsDescription) Removes the ability for end users to install applications directly from the Microsoft Store into SharePoint. This prevents uncontrolled app installations that can increase governance costs and go against organizational policies. + .NOTES + CAT + SharePoint Standards + TAG + EXECUTIVETEXT + Prevents end users from installing applications from the Microsoft Store into SharePoint sites, ensuring that only approved applications are available. This reduces governance overhead and aligns with Microsoft's Baseline Security Mode recommendations. + ADDEDCOMPONENT + IMPACT + Low Impact + ADDEDDATE + 2026-04-28 + POWERSHELLEQUIVALENT + Set-SPOTenant -DisableSharePointStoreAccess $true + RECOMMENDEDBY + "CIPP" + REQUIREDCAPABILITIES + "SHAREPOINTWAC" + "SHAREPOINTSTANDARD" + "SHAREPOINTENTERPRISE" + "SHAREPOINTENTERPRISE_EDU" + "ONEDRIVE_BASIC" + "ONEDRIVE_ENTERPRISE" + UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block + .LINK + https://docs.cipp.app/user-documentation/tenant/standards/list-standards + #> + + param($Tenant, $Settings) + $TestResult = Test-CIPPStandardLicense -StandardName 'SPDisableStoreAccess' -TenantFilter $Tenant -RequiredCapabilities @('SHAREPOINTWAC', 'SHAREPOINTSTANDARD', 'SHAREPOINTENTERPRISE', 'SHAREPOINTENTERPRISE_EDU', 'ONEDRIVE_BASIC', 'ONEDRIVE_ENTERPRISE') + + if ($TestResult -eq $false) { + return $true + } + + try { + $CurrentState = Get-CIPPSPOTenant -TenantFilter $Tenant | + Select-Object _ObjectIdentity_, TenantFilter, DisableSharePointStoreAccess + } catch { + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the SPDisableStoreAccess state for $Tenant. Error: $ErrorMessage" -Sev Error + return + } + + if ($null -eq $CurrentState._ObjectIdentity_) { + $ErrorDetail = $CurrentState.ErrorInfo ?? 'No tenant data returned from CSOM query' + Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the SPDisableStoreAccess state for $Tenant. CSOM error: $ErrorDetail" -Sev Error + return + } + + $StateIsCorrect = ($CurrentState.DisableSharePointStoreAccess -eq $true) + + if ($Settings.remediate -eq $true) { + if ($StateIsCorrect -eq $true) { + Write-LogMessage -API 'Standards' -Tenant $Tenant -Message 'SharePoint Store access is already disabled.' -Sev Info + } else { + try { + $CurrentState | Set-CIPPSPOTenant -Properties @{ DisableSharePointStoreAccess = $true } + Write-LogMessage -API 'Standards' -Tenant $Tenant -Message 'Successfully disabled SharePoint Store access.' -Sev Info + } catch { + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Failed to disable SharePoint Store access. Error: $ErrorMessage" -Sev Error + } + } + } + + if ($Settings.alert -eq $true) { + if ($StateIsCorrect -eq $true) { + Write-LogMessage -API 'Standards' -Tenant $Tenant -Message 'SharePoint Store access is disabled.' -Sev Info + } else { + Write-StandardsAlert -message 'SharePoint Store access is enabled.' -object $CurrentState -tenant $Tenant -standardName 'SPDisableStoreAccess' -standardId $Settings.standardId + Write-LogMessage -API 'Standards' -Tenant $Tenant -Message 'SharePoint Store access is enabled.' -Sev Info + } + } + + if ($Settings.report -eq $true) { + $CurrentValue = [PSCustomObject]@{ + SPDisableStoreAccess = $StateIsCorrect + } + $ExpectedValue = [PSCustomObject]@{ + SPDisableStoreAccess = $true + } + Set-CIPPStandardsCompareField -FieldName 'standards.SPDisableStoreAccess' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant + Add-CIPPBPAField -FieldName 'SPDisableStoreAccess' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $Tenant + } +} diff --git a/Modules/CIPPTests/Public/Tests/CopilotReadiness/Identity/Invoke-CippTestCopilotReady001.ps1 b/Modules/CIPPTests/Public/Tests/CopilotReadiness/Identity/Invoke-CippTestCopilotReady001.ps1 index cce1278c262fc..fb7dd130ff2de 100644 --- a/Modules/CIPPTests/Public/Tests/CopilotReadiness/Identity/Invoke-CippTestCopilotReady001.ps1 +++ b/Modules/CIPPTests/Public/Tests/CopilotReadiness/Identity/Invoke-CippTestCopilotReady001.ps1 @@ -20,13 +20,10 @@ function Invoke-CippTestCopilotReady001 { return } - # LicenseOverview is stored as a single item; unwrap if needed - $Skus = if ($LicenseData.Licenses) { $LicenseData.Licenses } else { $LicenseData } - $EligibleSkus = [System.Collections.Generic.List[object]]::new() $AssignableCount = 0 - foreach ($Sku in $Skus) { + foreach ($Sku in $LicenseData) { $HasQualifyingPlan = $Sku.ServicePlans | Where-Object { $_.servicePlanName -in $PrerequisiteServicePlans } if ($HasQualifyingPlan -and [int]$Sku.TotalLicenses -gt 0) { $EligibleSkus.Add($Sku) | Out-Null diff --git a/Modules/CIPPTests/Public/Tests/CopilotReadiness/Identity/Invoke-CippTestCopilotReady002.ps1 b/Modules/CIPPTests/Public/Tests/CopilotReadiness/Identity/Invoke-CippTestCopilotReady002.ps1 index f83387ef48f48..2be9ef3320ad3 100644 --- a/Modules/CIPPTests/Public/Tests/CopilotReadiness/Identity/Invoke-CippTestCopilotReady002.ps1 +++ b/Modules/CIPPTests/Public/Tests/CopilotReadiness/Identity/Invoke-CippTestCopilotReady002.ps1 @@ -7,8 +7,8 @@ function Invoke-CippTestCopilotReady002 { # Copilot add-on licenses are matched by friendly name (License field) since CIPP's LicenseOverview # caches display names rather than raw SKU part numbers. All Copilot add-on SKUs contain 'Copilot'. - # Service plan anchor: 'M365_COPILOT' is present in all Copilot add-on SKUs. - $CopilotServicePlan = 'M365_COPILOT' + # Service plan anchor: 'COPILOT' is present in all Copilot add-on SKUs. + $CopilotServicePlan = 'COPILOT' try { $LicenseData = Get-CIPPTestData -TenantFilter $Tenant -Type 'LicenseOverview' @@ -18,16 +18,14 @@ function Invoke-CippTestCopilotReady002 { return } - $Skus = if ($LicenseData.Licenses) { $LicenseData.Licenses } else { $LicenseData } - $CopilotLicenses = [System.Collections.Generic.List[object]]::new() $TotalEnabled = 0 $TotalConsumed = 0 $TotalAvailable = 0 - foreach ($Sku in $Skus) { - $IsCopilot = ($Sku.License -like '*Copilot*') -or - ($Sku.ServicePlans | Where-Object { $_.servicePlanName -eq $CopilotServicePlan }) + foreach ($Sku in $LicenseData) { + $IsCopilot = ($Sku.License -match 'Copilot') -or + ($Sku.ServicePlans | Where-Object { $_.servicePlanName -match $CopilotServicePlan }) if ($IsCopilot) { $CopilotLicenses.Add($Sku) | Out-Null $Enabled = [int]$Sku.TotalLicenses @@ -41,7 +39,7 @@ function Invoke-CippTestCopilotReady002 { if ($CopilotLicenses.Count -eq 0) { $Status = 'Failed' $Result = "No Microsoft 365 Copilot add-on licenses were found in this tenant.`n`n" - $Result += "Purchase Microsoft 365 Copilot licenses and assign them to eligible users to enable Copilot features." + $Result += 'Purchase Microsoft 365 Copilot licenses and assign them to eligible users to enable Copilot features.' } elseif ($TotalConsumed -eq 0) { $Status = 'Failed' $Result = "Microsoft 365 Copilot licenses exist (**$TotalEnabled** seats) but **none are assigned** to any users.`n`n" diff --git a/Modules/CippEntrypoints/CippEntrypoints.psm1 b/Modules/CippEntrypoints/CippEntrypoints.psm1 index 05f154b0ee3b8..1f953f3055a3f 100644 --- a/Modules/CippEntrypoints/CippEntrypoints.psm1 +++ b/Modules/CippEntrypoints/CippEntrypoints.psm1 @@ -502,7 +502,7 @@ function Receive-CIPPTimerTrigger { $InstancesTable = Get-CippTable -TableName ('{0}Instances' -f ($FunctionName -replace '-', '')) $Instance = Get-CIPPAzDataTableEntity @InstancesTable -Filter "PartitionKey eq '$($FunctionStatus.OrchestratorId)'" -Property PartitionKey, RowKey, RuntimeStatus if ($Instance.RuntimeStatus -eq 'Running') { - Write-LogMessage -API 'TimerFunction' -message "$($Function.Command) - $($FunctionStatus.OrchestratorId) is still running" -sev Warn -LogData $FunctionStatus + Write-LogMessage -API 'TimerFunction' -message "$($Function.Command) - $($FunctionStatus.OrchestratorId) is still running" -sev Warning -LogData $FunctionStatus Write-Warning "CIPP Timer: $($Function.Command) - $($FunctionStatus.OrchestratorId) is still running, skipping execution" continue } diff --git a/Shared/CIPPSharp/CIPPRestClient.cs b/Shared/CIPPSharp/CIPPRestClient.cs index da1356adf71f5..5b1e8b0208223 100644 --- a/Shared/CIPPSharp/CIPPRestClient.cs +++ b/Shared/CIPPSharp/CIPPRestClient.cs @@ -761,11 +761,18 @@ private sealed class TokenCacheEntry private static readonly ConcurrentDictionary _entries = new(StringComparer.OrdinalIgnoreCase); + // Per-key semaphores to prevent thundering herd / cache stampede. + // When multiple runspaces miss the cache for the same key simultaneously, + // only one acquires a token while the others wait and reuse the result. + private static readonly ConcurrentDictionary _keyLocks = + new(StringComparer.OrdinalIgnoreCase); + private static long _hits; private static long _misses; private static long _sets; private static long _invalidations; private static long _expiredRemovals; + private static long _lockWaits; public static string BuildKey( string tenantId, @@ -862,6 +869,37 @@ public static int CompactExpired(int refreshSkewSeconds = 0, int maxRemovals = 1 return removed; } + /// + /// Acquire a per-key lock to prevent thundering herd on cache miss. + /// Returns true if the lock was acquired within the timeout. + /// After acquiring, the caller should Lookup() again (double-check), + /// then acquire the token and Store() it, then ReleaseLock(). + /// + public static bool AcquireLock(string key, int timeoutMs = 30000) + { + if (string.IsNullOrWhiteSpace(key)) + return false; + + var sem = _keyLocks.GetOrAdd(key, _ => new SemaphoreSlim(1, 1)); + Interlocked.Increment(ref _lockWaits); + return sem.Wait(timeoutMs); + } + + /// + /// Release the per-key lock after token acquisition and Store(). + /// Safe to call even if AcquireLock returned false (no-ops gracefully). + /// + public static void ReleaseLock(string key) + { + if (string.IsNullOrWhiteSpace(key)) + return; + + if (_keyLocks.TryGetValue(key, out var sem)) + { + try { sem.Release(); } catch (SemaphoreFullException) { /* already released */ } + } + } + public static string GetDiagnostics() { return JsonSerializer.Serialize(new @@ -872,6 +910,8 @@ public static string GetDiagnostics() Sets = Interlocked.Read(ref _sets), Invalidations = Interlocked.Read(ref _invalidations), ExpiredRemovals = Interlocked.Read(ref _expiredRemovals), + LockWaits = Interlocked.Read(ref _lockWaits), + ActiveLocks = _keyLocks.Count, }, new JsonSerializerOptions { WriteIndented = true }); } @@ -882,6 +922,7 @@ public static void ResetDiagnostics() Interlocked.Exchange(ref _sets, 0); Interlocked.Exchange(ref _invalidations, 0); Interlocked.Exchange(ref _expiredRemovals, 0); + Interlocked.Exchange(ref _lockWaits, 0); } } } diff --git a/Shared/CIPPSharp/bin/CIPPSharp.dll b/Shared/CIPPSharp/bin/CIPPSharp.dll index edd5471793046..8d10347e22696 100644 Binary files a/Shared/CIPPSharp/bin/CIPPSharp.dll and b/Shared/CIPPSharp/bin/CIPPSharp.dll differ diff --git a/Tools/Update-IntuneCollection.ps1 b/Tools/Update-IntuneCollection.ps1 index c2e2b8b53b7f2..80201a95ae5b2 100644 --- a/Tools/Update-IntuneCollection.ps1 +++ b/Tools/Update-IntuneCollection.ps1 @@ -90,7 +90,7 @@ Set-Location $PSScriptRoot $json = $collection | ConvertTo-Json -Depth 5 # CIPP-API root (used by Compare-CIPPIntuneObject.ps1 at runtime) -$apiPath = Join-Path $PSScriptRoot '..\intuneCollection.json' +$apiPath = Join-Path $PSScriptRoot '..\Config\intuneCollection.json' $json | Set-Content -Path $apiPath -Encoding utf8NoBOM Write-Host "Written: $(Resolve-Path $apiPath)" -ForegroundColor Green diff --git a/host.json b/host.json index e5c89cc14cb2a..fd86e95d90370 100644 --- a/host.json +++ b/host.json @@ -16,7 +16,7 @@ "distributedTracingEnabled": false, "version": "None" }, - "defaultVersion": "10.4.1", + "defaultVersion": "10.4.2", "versionMatchStrategy": "Strict", "versionFailureStrategy": "Fail" }