Skip to content

Commit 93e34a4

Browse files
authored
Merge pull request #1080 from KelvinTegelaar/dev
[pull] dev from KelvinTegelaar:dev
2 parents 49c14f2 + 4f2dad2 commit 93e34a4

7 files changed

Lines changed: 360 additions & 2 deletions

File tree

Config/FeatureFlags.json

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,5 +71,35 @@
7171
"/cipp/advanced/diagnostics"
7272
],
7373
"Hidden": true
74+
},
75+
{
76+
"Id": "BackendSettings",
77+
"Name": "Backend Settings",
78+
"Description": "Backend settings page showing Azure resource URLs. Hidden on hosted instances.",
79+
"Enabled": true,
80+
"AllowUserToggle": false,
81+
"Timers": [],
82+
"Endpoints": [
83+
"ExecBackendURLs"
84+
],
85+
"Pages": [
86+
"/cipp/settings/backend"
87+
],
88+
"Hidden": true
89+
},
90+
{
91+
"Id": "FunctionOffloading",
92+
"Name": "Function Offloading",
93+
"Description": "Function offloading configuration page. Hidden in CIPP-NG.",
94+
"Enabled": true,
95+
"AllowUserToggle": false,
96+
"Timers": [],
97+
"Endpoints": [
98+
"ExecOffloadFunctions"
99+
],
100+
"Pages": [
101+
"/cipp/advanced/super-admin/function-offloading"
102+
],
103+
"Hidden": true
74104
}
75105
]

Modules/CIPPActivityTriggers/Public/Entrypoints/Activity Triggers/Tests/Push-CIPPTestsList.ps1

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,18 @@ function Push-CIPPTestsList {
3939
# Function discovery happens inside Invoke-CIPPTestCollection via Get-Command (path-independent).
4040
$Suites = @('ZTNA', 'ORCA', 'EIDSCA', 'CISA', 'CIS', 'SMB1001', 'CopilotReadiness', 'GenericTests', 'Custom', 'E8')
4141

42+
# Optional caller-supplied suite filter (e.g. a Custom-only run). When present, restrict
43+
# the emitted suites to the requested subset so we don't spin up every suite unnecessarily.
44+
if ($Item.Suites) {
45+
$Requested = @($Item.Suites)
46+
$Suites = @($Suites | Where-Object { $_ -in $Requested })
47+
if ($Suites.Count -eq 0) {
48+
Write-Information "No suites matched the requested filter ($($Requested -join ', ')) for tenant $TenantFilter. Skipping."
49+
return @()
50+
}
51+
Write-Information "Suite filter applied for $TenantFilter — running: $($Suites -join ', ')"
52+
}
53+
4254
$Tasks = foreach ($Suite in $Suites) {
4355
[PSCustomObject]@{
4456
FunctionName = 'CIPPTestCollection'

Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-CIPPDBTestsRun.ps1

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,12 @@ function Start-CIPPDBTestsRun {
1515
[string]$TenantFilter = 'allTenants',
1616

1717
[Parameter(Mandatory = $false)]
18-
[switch]$Force
18+
[switch]$Force,
19+
20+
# Optional subset of suites to run (e.g. 'Custom'). Omit to run every suite.
21+
# Suite names must match the ValidateSet in Invoke-CIPPTestCollection.
22+
[Parameter(Mandatory = $false)]
23+
[string[]]$Suites
1924
)
2025

2126
Write-Information "Starting tests run for tenant: $TenantFilter"
@@ -64,11 +69,16 @@ function Start-CIPPDBTestsRun {
6469
# The tenants below were already filtered by data presence above, so we pass
6570
# SkipDbCheck=$true to avoid a redundant CountsOnly round-trip per tenant.
6671
$Batch = foreach ($Tenant in $AllTenantsList) {
67-
@{
72+
$ListItem = @{
6873
FunctionName = 'CIPPTestsList'
6974
TenantFilter = $Tenant
7075
SkipDbCheck = $true
7176
}
77+
# Propagate an optional suite filter so Phase 1 emits only the requested suites.
78+
if ($Suites) {
79+
$ListItem.Suites = @($Suites)
80+
}
81+
$ListItem
7282
}
7383

7484
Write-Information "Built batch of $($Batch.Count) tenant test list activities"
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
function Get-CIPPTestResultsTenants {
2+
<#
3+
.SYNOPSIS
4+
Retrieves CIPP test results across one, many, or all tenants with flexible filtering.
5+
6+
.DESCRIPTION
7+
Queries the shared CippTestResults table server-side (partition / row / status / type /
8+
risk / category) so a single test can be compared across every tenant in one call. This is
9+
the cross-tenant counterpart to Get-CIPPTestResults (which is scoped to a single tenant).
10+
11+
Custom test rows are enriched with their definition (Description / ReturnType /
12+
MarkdownTemplate) from the CustomPowershellScripts table so the off-canvas detail renders
13+
identically to the per-tenant dashboard. Every row is stamped with its tenant identity
14+
(Tenant / TenantId / TenantName) and a serialisable LastRun timestamp for display.
15+
16+
.PARAMETER TenantFilter
17+
One or more tenant domains. Omit, or pass 'AllTenants' / 'allTenants', to query every tenant.
18+
19+
.PARAMETER TestId
20+
One or more test IDs (the row's RowKey), e.g. 'CustomScript-<guid>'.
21+
22+
.PARAMETER Status
23+
One or more statuses to filter on (Passed / Failed / Investigate / Skipped / Informational).
24+
25+
.PARAMETER TestType
26+
Restrict to a single test type (Identity / Devices / Custom).
27+
28+
.PARAMETER Risk
29+
Restrict to a single risk level (High / Medium / Low).
30+
31+
.PARAMETER Category
32+
Restrict to a single category.
33+
34+
.EXAMPLE
35+
Get-CIPPTestResultsTenants -TestId 'CustomScript-1234' -TestType 'Custom'
36+
#>
37+
[CmdletBinding()]
38+
param(
39+
[Parameter(Mandatory = $false)]
40+
[string[]]$TenantFilter,
41+
42+
[Parameter(Mandatory = $false)]
43+
[string[]]$TestId,
44+
45+
[Parameter(Mandatory = $false)]
46+
[string[]]$Status,
47+
48+
[Parameter(Mandatory = $false)]
49+
[string]$TestType,
50+
51+
[Parameter(Mandatory = $false)]
52+
[string]$Risk,
53+
54+
[Parameter(Mandatory = $false)]
55+
[string]$Category
56+
)
57+
58+
$Table = Get-CippTable -tablename 'CippTestResults'
59+
60+
# Build a single OData filter so all narrowing happens server-side. A cross-partition scan
61+
# (no PartitionKey clause) is acceptable here because it is always bounded by a RowKey/Status/
62+
# TestType clause when driven from the UI.
63+
$FilterParts = [System.Collections.Generic.List[string]]::new()
64+
65+
$AllTenants = (-not $TenantFilter) -or ($TenantFilter -contains 'AllTenants') -or ($TenantFilter -contains 'allTenants')
66+
if (-not $AllTenants) {
67+
$TenantClause = (@($TenantFilter | Where-Object { $_ }) | ForEach-Object { "PartitionKey eq '$_'" }) -join ' or '
68+
if ($TenantClause) { $FilterParts.Add("($TenantClause)") }
69+
}
70+
if ($TestId) {
71+
$TestClause = (@($TestId | Where-Object { $_ }) | ForEach-Object { "RowKey eq '$_'" }) -join ' or '
72+
if ($TestClause) { $FilterParts.Add("($TestClause)") }
73+
}
74+
if ($Status) {
75+
$StatusClause = (@($Status | Where-Object { $_ }) | ForEach-Object { "Status eq '$_'" }) -join ' or '
76+
if ($StatusClause) { $FilterParts.Add("($StatusClause)") }
77+
}
78+
if ($TestType) { $FilterParts.Add("TestType eq '$TestType'") }
79+
if ($Risk) { $FilterParts.Add("Risk eq '$Risk'") }
80+
if ($Category) { $FilterParts.Add("Category eq '$Category'") }
81+
82+
$Filter = $FilterParts -join ' and '
83+
84+
$GetParams = @{}
85+
if ($Filter) { $GetParams.Filter = $Filter }
86+
$Results = @(Get-CIPPAzDataTableEntity @Table @GetParams)
87+
88+
if ($Results.Count -eq 0) { return @() }
89+
90+
# Map tenant domain (PartitionKey) -> tenant identity for display and access control.
91+
$TenantLookup = @{}
92+
try {
93+
foreach ($Tenant in (Get-Tenants -IncludeErrors)) {
94+
if ($Tenant.defaultDomainName) {
95+
$TenantLookup[$Tenant.defaultDomainName] = $Tenant
96+
}
97+
}
98+
} catch {
99+
Write-Warning "Get-CIPPTestResultsTenants: failed to load tenant list: $($_.Exception.Message)"
100+
}
101+
102+
# Enrich Custom rows with their definition (latest version per ScriptGuid), mirroring the
103+
# per-tenant enrichment in Invoke-ListTests so the shared off-canvas renders identically.
104+
$CustomMeta = @{}
105+
if (@($Results | Where-Object { $_.TestType -eq 'Custom' }).Count -gt 0) {
106+
$ScriptTable = Get-CippTable -tablename 'CustomPowershellScripts'
107+
$Scripts = @(Get-CIPPAzDataTableEntity @ScriptTable -Filter "PartitionKey eq 'CustomScript'")
108+
$LatestByGuid = @{}
109+
foreach ($Script in $Scripts) {
110+
if (-not $Script.ScriptGuid) { continue }
111+
$Existing = $LatestByGuid[$Script.ScriptGuid]
112+
if (-not $Existing -or [int]$Script.Version -gt [int]$Existing.Version) {
113+
$LatestByGuid[$Script.ScriptGuid] = $Script
114+
}
115+
}
116+
foreach ($Script in $LatestByGuid.Values) {
117+
# Treat a missing Enabled property as enabled, matching Invoke-CIPPTestCollection.
118+
# A disabled (or deleted) script can still have stale results in the table; surfacing
119+
# this lets the UI show that the data no longer reflects an active test.
120+
$EnabledProp = $Script.PSObject.Properties['Enabled']
121+
$CustomMeta[$Script.ScriptGuid] = [PSCustomObject]@{
122+
ScriptName = $Script.ScriptName ?? ''
123+
Description = $Script.Description ?? ''
124+
ReturnType = $Script.ReturnType ?? 'JSON'
125+
MarkdownTemplate = $Script.MarkdownTemplate ?? ''
126+
Enabled = (-not $EnabledProp) -or [bool]$EnabledProp.Value
127+
}
128+
}
129+
}
130+
131+
foreach ($Result in $Results) {
132+
$TenantInfo = $TenantLookup[$Result.PartitionKey]
133+
$Result | Add-Member -NotePropertyName 'Tenant' -NotePropertyValue $Result.PartitionKey -Force
134+
$Result | Add-Member -NotePropertyName 'TenantId' -NotePropertyValue ($TenantInfo.customerId ?? '') -Force
135+
$Result | Add-Member -NotePropertyName 'TenantName' -NotePropertyValue ($TenantInfo.displayName ?? $Result.PartitionKey) -Force
136+
137+
# Surface the Azure entity timestamp as a stable, serialisable last-run field.
138+
$LastRun = $Result.Timestamp
139+
if ($LastRun -is [DateTimeOffset]) {
140+
$LastRun = $LastRun.UtcDateTime.ToString('o')
141+
} elseif ($LastRun) {
142+
$LastRun = [string]$LastRun
143+
}
144+
$Result | Add-Member -NotePropertyName 'LastRun' -NotePropertyValue $LastRun -Force
145+
146+
if ($Result.TestType -eq 'Custom') {
147+
$ScriptGuid = ($Result.RowKey -replace '^CustomScript-', '')
148+
if (-not [string]::IsNullOrWhiteSpace($ScriptGuid) -and $CustomMeta.ContainsKey($ScriptGuid)) {
149+
$Meta = $CustomMeta[$ScriptGuid]
150+
$Result | Add-Member -NotePropertyName 'Description' -NotePropertyValue $Meta.Description -Force
151+
$Result | Add-Member -NotePropertyName 'ReturnType' -NotePropertyValue $Meta.ReturnType -Force
152+
$Result | Add-Member -NotePropertyName 'MarkdownTemplate' -NotePropertyValue $Meta.MarkdownTemplate -Force
153+
$Result | Add-Member -NotePropertyName 'Enabled' -NotePropertyValue $Meta.Enabled -Force
154+
if ([string]::IsNullOrWhiteSpace($Result.Name) -and $Meta.ScriptName) {
155+
$Result | Add-Member -NotePropertyName 'Name' -NotePropertyValue $Meta.ScriptName -Force
156+
}
157+
} else {
158+
# Result exists but the script no longer does (deleted). The data is stale — flag
159+
# it as not enabled so the column stays populated and truthful.
160+
$Result | Add-Member -NotePropertyName 'Enabled' -NotePropertyValue $false -Force
161+
}
162+
}
163+
}
164+
165+
return $Results
166+
}

Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ListFeatureFlags.ps1

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,18 @@ function Invoke-ListFeatureFlags {
2424
elseIf ($Flag.Id -eq 'AppInsights') {
2525
$Flag.Enabled = $false
2626
}
27+
elseIf ($Flag.Id -eq 'FunctionOffloading') {
28+
$Flag.Enabled = $false
29+
}
30+
}
31+
}
32+
33+
# Hosted instances hide the backend settings page (Azure resource URLs)
34+
if ($env:CIPP_HOSTED -eq 'true') {
35+
foreach ($Flag in $FeatureFlags) {
36+
if ($Flag.Id -eq 'BackendSettings') {
37+
$Flag.Enabled = $false
38+
}
2739
}
2840
}
2941

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
function Invoke-ExecCustomTestRun {
2+
<#
3+
.SYNOPSIS
4+
Triggers a Custom test-suite run for one or all tenants.
5+
6+
.DESCRIPTION
7+
Re-runs the enabled Custom tests against the most recent cached data for the requested
8+
tenant(s) by starting the standard tests orchestration with a Custom-only suite filter.
9+
Used by the cross-tenant Custom Test Report to refresh results on demand.
10+
11+
.FUNCTIONALITY
12+
Entrypoint,AnyTenant
13+
14+
.ROLE
15+
Tenant.Tests.ReadWrite
16+
#>
17+
param($Request, $TriggerMetadata)
18+
19+
$APIName = $TriggerMetadata.FunctionName
20+
Write-LogMessage -headers $Request.Headers -API $APIName -message 'Accessed this API' -Sev 'Debug'
21+
22+
$TenantFilter = 'allTenants'
23+
try {
24+
$TenantFilterRaw = $Request.Query.tenantFilter ?? $Request.Body.tenantFilter
25+
# Accept a plain string, a {value} object, or a single-element array of either.
26+
if ($TenantFilterRaw -is [array]) { $TenantFilterRaw = $TenantFilterRaw | Select-Object -First 1 }
27+
if ($TenantFilterRaw -is [pscustomobject]) { $TenantFilterRaw = $TenantFilterRaw.value }
28+
if (-not [string]::IsNullOrWhiteSpace($TenantFilterRaw)) { $TenantFilter = [string]$TenantFilterRaw }
29+
if ($TenantFilter -eq 'AllTenants') { $TenantFilter = 'allTenants' }
30+
31+
Write-LogMessage -API $APIName -tenant $TenantFilter -message "Starting Custom test run for: $TenantFilter" -sev Info
32+
$null = Start-CIPPDBTestsRun -TenantFilter $TenantFilter -Suites 'Custom' -Force
33+
34+
$Scope = if ($TenantFilter -eq 'allTenants') { 'all tenants' } else { $TenantFilter }
35+
$StatusCode = [HttpStatusCode]::OK
36+
$Body = [PSCustomObject]@{ Results = "Successfully started a custom test run for $Scope. Results will populate here as each tenant completes." }
37+
} catch {
38+
$ErrorMessage = Get-CippException -Exception $_
39+
Write-LogMessage -API $APIName -tenant $TenantFilter -message "Failed to start custom test run: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage
40+
$StatusCode = [HttpStatusCode]::BadRequest
41+
$Body = [PSCustomObject]@{ Results = "Failed to start custom test run: $($ErrorMessage.NormalizedError)" }
42+
}
43+
44+
return ([HttpResponseContext]@{
45+
StatusCode = $StatusCode
46+
Body = $Body
47+
})
48+
}

0 commit comments

Comments
 (0)