Skip to content

Commit ee29b1a

Browse files
authored
Merge pull request #940 from KelvinTegelaar/dev
[pull] dev from KelvinTegelaar:dev
2 parents 1b099d6 + 0115569 commit ee29b1a

6 files changed

Lines changed: 196 additions & 38 deletions

File tree

Modules/CIPPCore/Public/Add-CIPPDbItem.ps1

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -49,17 +49,18 @@ function Add-CIPPDbItem {
4949
if ($null -eq $Item) { continue }
5050
$ItemId = $Item.ExternalDirectoryObjectId ?? $Item.id ?? $Item.Identity ?? $Item.skuId ?? $Item.userPrincipalName ?? [guid]::NewGuid().ToString()
5151
$RowKey = "$Type-$ItemId" -replace '[/\\#?]', '_' -replace '[\u0000-\u001F\u007F-\u009F]', ''
52-
[void]$NewRowKeys.Add($RowKey)
53-
$Batch.Add(@{
54-
PartitionKey = $TenantFilter
55-
RowKey = $RowKey
56-
Data = [string]($Item | ConvertTo-Json -Depth 10 -Compress)
57-
Type = $Type
58-
})
59-
if ($Batch.Count -ge 500) {
60-
$null = Add-CIPPAzDataTableEntity @Table -Entity $Batch.ToArray() -Force
61-
$TotalProcessed += $Batch.Count
62-
$Batch.Clear()
52+
if ($NewRowKeys.Add($RowKey)) {
53+
$Batch.Add(@{
54+
PartitionKey = $TenantFilter
55+
RowKey = $RowKey
56+
Data = [string]($Item | ConvertTo-Json -Depth 10 -Compress)
57+
Type = $Type
58+
})
59+
if ($Batch.Count -ge 500) {
60+
$null = Add-CIPPAzDataTableEntity @Table -Entity $Batch.ToArray() -Force
61+
$TotalProcessed += $Batch.Count
62+
$Batch.Clear()
63+
}
6364
}
6465
}
6566
}

Modules/CIPPDB/Public/DBCache/Set-CIPPDBCacheDetectedApps.ps1

Lines changed: 47 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -19,25 +19,58 @@ function Set-CIPPDBCacheDetectedApps {
1919
try {
2020
Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching detected apps' -sev Debug
2121

22-
# Fetch all detected apps for the tenant
23-
$DetectedApps = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/deviceManagement/detectedApps' -tenantid $TenantFilter
24-
if (!$DetectedApps) { $DetectedApps = @() }
22+
# Step 1: Get first page with noPaginate to avoid sequential chase, and read @odata.count
23+
$FirstPageResult = New-GraphBulkRequest -Requests @(
24+
[PSCustomObject]@{
25+
id = 'detectedApps-0'
26+
method = 'GET'
27+
url = 'deviceManagement/detectedApps'
28+
}
29+
) -tenantid $TenantFilter -NoPaginateIds @('detectedApps-0')
30+
31+
$FirstResponse = ($FirstPageResult | Where-Object { $_.id -eq 'detectedApps-0' }).body
32+
$TotalCount = $FirstResponse.'@odata.count'
33+
$DetectedApps = [System.Collections.Generic.List[PSCustomObject]]::new()
34+
foreach ($app in $FirstResponse.value) { $DetectedApps.Add($app) }
35+
36+
Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "DetectedApps total count: $TotalCount, first page: $($DetectedApps.Count)" -sev Debug
37+
38+
# Step 2: If more pages exist, pre-calculate all skip offsets and fire as batches
39+
if ($FirstResponse.'@odata.nextLink' -and $TotalCount -gt 50) {
40+
$SkipRequests = [System.Collections.Generic.List[PSCustomObject]]::new()
41+
for ($skip = 50; $skip -lt $TotalCount; $skip += 50) {
42+
$SkipRequests.Add([PSCustomObject]@{
43+
id = "detectedApps-$skip"
44+
method = 'GET'
45+
url = "deviceManagement/detectedApps?`$skip=$skip"
46+
})
47+
}
48+
Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Fetching $($SkipRequests.Count) remaining pages in bulk" -sev Debug
2549

26-
if (($DetectedApps | Measure-Object).Count -eq 0) {
27-
Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'No detected apps found' -sev Debug
50+
# New-GraphBulkRequest auto-batches into groups of 20, NoPaginateIds prevents chasing empty nextLinks
51+
$SkipResults = New-GraphBulkRequest -Requests @($SkipRequests) -tenantid $TenantFilter -NoPaginateIds @($SkipRequests.id)
52+
53+
foreach ($Result in $SkipResults) {
54+
if ($Result.status -eq 200 -and $Result.body.value) {
55+
foreach ($app in $Result.body.value) { $DetectedApps.Add($app) }
56+
}
57+
}
58+
}
59+
60+
Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Retrieved $($DetectedApps.Count) detected apps (expected $TotalCount)" -sev Debug
61+
62+
if ($DetectedApps.Count -eq 0) {
2863
Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'DetectedApps' -Data @()
2964
Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'DetectedApps' -Data @() -Count
3065
return
3166
}
3267

33-
# Build bulk request for devices that have each detected app
34-
$DeviceRequests = $DetectedApps | ForEach-Object {
35-
if ($_.id) {
36-
[PSCustomObject]@{
37-
id = $_.id
38-
method = 'GET'
39-
url = "deviceManagement/detectedApps('$($_.id)')/managedDevices"
40-
}
68+
# Step 3: Bulk fetch managed devices for each app (unchanged from original)
69+
$DeviceRequests = $DetectedApps | Where-Object { $_.id } | ForEach-Object {
70+
[PSCustomObject]@{
71+
id = $_.id
72+
method = 'GET'
73+
url = "deviceManagement/detectedApps('$($_.id)')/managedDevices"
4174
}
4275
}
4376

@@ -48,11 +81,7 @@ function Set-CIPPDBCacheDetectedApps {
4881
# Add devices to each detected app object
4982
$DetectedAppsWithDevices = foreach ($App in $DetectedApps) {
5083
$Devices = Get-GraphBulkResultByID -Results $DeviceResults -ID $App.id -Value
51-
if ($Devices) {
52-
$App | Add-Member -NotePropertyName 'managedDevices' -NotePropertyValue $Devices -Force
53-
} else {
54-
$App | Add-Member -NotePropertyName 'managedDevices' -NotePropertyValue @() -Force
55-
}
84+
$App | Add-Member -NotePropertyName 'managedDevices' -NotePropertyValue ($Devices ?? @()) -Force
5685
$App
5786
}
5887

Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-AddJITAdminTemplate.ps1

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,9 @@ function Invoke-AddJITAdminTemplate {
7878
templateName = $TemplateName
7979
defaultForTenant = $DefaultForTenant
8080
defaultRoles = $Request.Body.defaultRoles
81+
defaultGroups = $Request.Body.defaultGroups
82+
defaultUseRoles = [bool]$Request.Body.defaultUseRoles
83+
defaultUseGroups = [bool]$Request.Body.defaultUseGroups
8184
defaultDuration = $Request.Body.defaultDuration
8285
defaultExpireAction = $Request.Body.defaultExpireAction
8386
defaultNotificationActions = $Request.Body.defaultNotificationActions
@@ -92,6 +95,11 @@ function Invoke-AddJITAdminTemplate {
9295
$TemplateObject.defaultUserAction = $DefaultUserAction
9396
}
9497

98+
# Add existing user selection when "select" action is specified
99+
if ($DefaultUserAction -eq 'select' -and $Request.Body.defaultExistingUser) {
100+
$TemplateObject.defaultExistingUser = $Request.Body.defaultExistingUser
101+
}
102+
95103
# Add user detail fields when "create" action is specified
96104
if ($DefaultUserAction -eq 'create') {
97105
# These fields can be saved for both AllTenants and specific tenant templates

Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-EditJITAdminTemplate.ps1

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,9 @@ function Invoke-EditJITAdminTemplate {
9494
templateName = $TemplateName
9595
defaultForTenant = $DefaultForTenant
9696
defaultRoles = $Request.Body.defaultRoles
97+
defaultGroups = $Request.Body.defaultGroups
98+
defaultUseRoles = [bool]$Request.Body.defaultUseRoles
99+
defaultUseGroups = [bool]$Request.Body.defaultUseGroups
97100
defaultDuration = $Request.Body.defaultDuration
98101
defaultExpireAction = $Request.Body.defaultExpireAction
99102
defaultNotificationActions = $Request.Body.defaultNotificationActions
@@ -110,6 +113,11 @@ function Invoke-EditJITAdminTemplate {
110113
$TemplateObject.defaultUserAction = $DefaultUserAction
111114
}
112115

116+
# Add existing user selection when "select" action is specified
117+
if ($DefaultUserAction -eq 'select' -and $Request.Body.defaultExistingUser) {
118+
$TemplateObject.defaultExistingUser = $Request.Body.defaultExistingUser
119+
}
120+
113121
# Add user detail fields when "create" action is specified
114122
if ($DefaultUserAction -eq 'create') {
115123
# These fields can be saved for both AllTenants and specific tenant templates

Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ExecOffboardTenant.ps1

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,10 @@ function Invoke-ExecOffboardTenant {
4747
}
4848
})
4949

50-
$BulkResults = New-GraphBulkRequest -Requests $BulkRequests -tenantid $TenantFilter
50+
$null = New-GraphBulkRequest -Requests $BulkRequests -tenantid $TenantFilter
5151

5252
$results.Add('Successfully removed guest users')
53-
Write-LogMessage -headers $Request.Headers -API $APIName -message 'CSP Guest users were removed' -Sev 'Info' -tenant $TenantFilter
53+
Write-LogMessage -headers $Headers -API $APIName -message 'CSP Guest users were removed' -Sev 'Info' -tenant $TenantFilter
5454
} else {
5555
$results.Add('No guest users found to remove')
5656
}
@@ -84,15 +84,15 @@ function Invoke-ExecOffboardTenant {
8484
$property = $_
8585
$propertyContacts = $orgContacts.($($property))
8686

87-
if ($propertyContacts -and ($domains -notcontains ($propertyContacts | ForEach-Object { $_.Split('@')[1] }))) {
87+
if ($propertyContacts -and ($propertyContacts | Where-Object { $domains -contains $_.Split('@')[1] })) {
8888
$newPropertyContent = [System.Collections.Generic.List[object]]($propertyContacts | Where-Object { $domains -notcontains $_.Split('@')[1] })
8989

9090
$patchContactBody = if (!($newPropertyContent)) { "{ `"$($property)`" : [] }" } else { [pscustomobject]@{ $property = $newPropertyContent } | ConvertTo-Json }
9191

9292
try {
93-
New-GraphPostRequest -type PATCH -body $patchContactBody -Uri "https://graph.microsoft.com/v1.0/organization/$($orgContacts.id)" -tenantid $TenantFilter -ContentType 'application/json'
93+
$null = New-GraphPostRequest -type PATCH -body $patchContactBody -Uri "https://graph.microsoft.com/v1.0/organization/$($orgContacts.id)" -tenantid $TenantFilter
9494
$Results.Add("Successfully removed notification contacts from $($property): $(($propertyContacts | Where-Object { $domains -contains $_.Split('@')[1] }))")
95-
Write-LogMessage -headers $Request.Headers -API $APIName -message "Contacts were removed from $($property)" -Sev 'Info' -tenant $TenantFilter
95+
Write-LogMessage -headers $Headers -API $APIName -message "Contacts were removed from $($property)" -Sev 'Info' -tenant $TenantFilter
9696
} catch {
9797
$Errors.Add("Failed to update property $($property): $($_.Exception.message)")
9898
}
@@ -110,14 +110,14 @@ function Invoke-ExecOffboardTenant {
110110
$DomainTable = Get-CIPPTable -Table 'Domains'
111111
$Filter = "TenantGUID eq '{0}'" -f $TenantId
112112
$DomainEntries = Get-CIPPAzDataTableEntity @DomainTable -Filter $Filter
113-
113+
114114
if ($DomainEntries) {
115115
$DomainCount = ($DomainEntries | Measure-Object).Count
116116
foreach ($Domain in $DomainEntries) {
117117
Remove-AzDataTableEntity @DomainTable -Entity $Domain
118118
}
119119
$Results.Add("Successfully removed $DomainCount Domain Analyser entries")
120-
Write-LogMessage -headers $Request.Headers -API $APIName -message "Removed $DomainCount Domain Analyser entries" -Sev 'Info' -tenant $TenantFilter
120+
Write-LogMessage -headers $Headers -API $APIName -message "Removed $DomainCount Domain Analyser entries" -Sev 'Info' -tenant $TenantFilter
121121
} else {
122122
$Results.Add('No Domain Analyser data found for this tenant')
123123
}
@@ -149,7 +149,7 @@ function Invoke-ExecOffboardTenant {
149149
try {
150150
$null = (New-GraphPostRequest -type 'DELETE' -Uri "https://graph.microsoft.com/v1.0/serviceprincipals/$($_.id)" -tenantid $TenantFilter)
151151
$Results.Add("Successfully removed app $($_.displayName)")
152-
Write-LogMessage -headers $Request.Headers -API $APIName -message "App $($_.displayName) was removed" -Sev 'Info' -tenant $TenantFilter
152+
Write-LogMessage -headers $Headers -API $APIName -message "App $($_.displayName) was removed" -Sev 'Info' -tenant $TenantFilter
153153
} catch {
154154
#$Results.Add("Failed to removed app $($_.displayName)")
155155
$Errors.Add("Failed to removed app $($_.displayName)")
@@ -170,7 +170,7 @@ function Invoke-ExecOffboardTenant {
170170
try {
171171
$null = (New-GraphPostRequest -type 'POST' -Uri "https://graph.microsoft.com/v1.0/tenantRelationships/delegatedAdminRelationships/$($_.id)/requests" -body '{"action":"terminate"}' -ContentType 'application/json' -tenantid $env:TenantID)
172172
$Results.Add("Successfully terminated GDAP relationship $($_.displayName) from tenant $TenantFilter")
173-
Write-LogMessage -headers $Request.Headers -API $APIName -message "GDAP Relationship $($_.displayName) has been terminated" -Sev 'Info' -tenant $TenantFilter
173+
Write-LogMessage -headers $Headers -API $APIName -message "GDAP Relationship $($_.displayName) has been terminated" -Sev 'Info' -tenant $TenantFilter
174174

175175
} catch {
176176
$($_.Exception.message)
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
function Invoke-CIPPStandardAdminSSPR {
2+
<#
3+
.FUNCTIONALITY
4+
Internal
5+
.COMPONENT
6+
(APIName) AdminSSPR
7+
.SYNOPSIS
8+
(Label) Set administrator Self-Service Password Reset state
9+
.DESCRIPTION
10+
(Helptext) Controls whether administrators are allowed to use Self-Service Password Reset through the Microsoft Entra authorization policy.
11+
(DocsDescription) Configures the allowedToUseSSPR property on the Microsoft Entra authorization policy. Microsoft documents this property as controlling whether administrators of the tenant can use Self-Service Password Reset. Use this standard to explicitly enable or disable administrator SSPR based on your security policy.
12+
.NOTES
13+
CAT
14+
Entra (AAD) Standards
15+
TAG
16+
"EIDSCA.AP01"
17+
"EIDSCAAP01"
18+
"ZTNA21842"
19+
EXECUTIVETEXT
20+
Controls whether tenant administrators can reset their own passwords through Self-Service Password Reset. Disabling this capability forces privileged accounts through more controlled recovery processes and reduces the risk of self-service recovery being misused on administrative identities.
21+
ADDEDCOMPONENT
22+
{"type":"autoComplete","multiple":false,"creatable":false,"label":"Select value","name":"standards.AdminSSPR.state","options":[{"label":"Enabled","value":"enabled"},{"label":"Disabled","value":"disabled"}]}
23+
IMPACT
24+
Low Impact
25+
ADDEDDATE
26+
2026-04-21
27+
POWERSHELLEQUIVALENT
28+
Update-MgBetaPolicyAuthorizationPolicy
29+
RECOMMENDEDBY
30+
"CIPP"
31+
UPDATECOMMENTBLOCK
32+
Run the Tools\Update-StandardsComments.ps1 script to update this comment block
33+
.LINK
34+
https://docs.cipp.app/user-documentation/tenant/standards/list-standards
35+
#>
36+
37+
param($Tenant, $Settings)
38+
39+
$StateValue = $Settings.state.value ?? $Settings.state
40+
if ([string]::IsNullOrWhiteSpace($StateValue)) {
41+
Write-LogMessage -API 'Standards' -tenant $Tenant -message 'AdminSSPR: Invalid state parameter set.' -sev Error
42+
return
43+
}
44+
45+
switch ($StateValue.ToLowerInvariant()) {
46+
'enabled' {
47+
$DesiredValue = $true
48+
$DesiredLabel = 'enabled'
49+
}
50+
'disabled' {
51+
$DesiredValue = $false
52+
$DesiredLabel = 'disabled'
53+
}
54+
default {
55+
Write-LogMessage -API 'Standards' -tenant $Tenant -message "AdminSSPR: Unsupported state value '$StateValue'." -sev Error
56+
return
57+
}
58+
}
59+
60+
try {
61+
$CurrentState = New-GraphGetRequest -Uri 'https://graph.microsoft.com/beta/policies/authorizationPolicy/authorizationPolicy' -tenantid $Tenant
62+
} catch {
63+
$ErrorMessage = Get-CippException -Exception $_
64+
Write-LogMessage -API 'Standards' -tenant $Tenant -message "Could not get the AdminSSPR state for $Tenant. Error: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage
65+
return
66+
}
67+
68+
$CurrentStateValue = [bool]$CurrentState.allowedToUseSSPR
69+
$StateIsCorrect = ($CurrentStateValue -eq $DesiredValue)
70+
71+
$CurrentValue = [PSCustomObject]@{
72+
allowedToUseSSPR = $CurrentStateValue
73+
}
74+
$ExpectedValue = [PSCustomObject]@{
75+
allowedToUseSSPR = $DesiredValue
76+
}
77+
78+
if ($Settings.remediate -eq $true) {
79+
if ($StateIsCorrect -eq $true) {
80+
Write-LogMessage -API 'Standards' -tenant $Tenant -message "Administrator SSPR is already $DesiredLabel." -sev Info
81+
} else {
82+
try {
83+
$Body = @{ allowedToUseSSPR = $DesiredValue } | ConvertTo-Json -Compress -Depth 10
84+
$null = New-GraphPOSTRequest -Uri 'https://graph.microsoft.com/beta/policies/authorizationPolicy/authorizationPolicy' -tenantid $Tenant -Type PATCH -Body $Body
85+
Write-LogMessage -API 'Standards' -tenant $Tenant -message "Successfully set administrator SSPR to $DesiredLabel." -sev Info
86+
87+
$CurrentState.allowedToUseSSPR = $DesiredValue
88+
$CurrentStateValue = $DesiredValue
89+
$StateIsCorrect = $true
90+
} catch {
91+
$ErrorMessage = Get-CippException -Exception $_
92+
Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to set administrator SSPR to $DesiredLabel. Error: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage
93+
}
94+
}
95+
}
96+
97+
if ($Settings.alert -eq $true) {
98+
if ($StateIsCorrect -eq $true) {
99+
Write-LogMessage -API 'Standards' -tenant $Tenant -message "Administrator SSPR is $DesiredLabel as configured." -sev Info
100+
} else {
101+
$CurrentLabel = if ($CurrentStateValue) { 'enabled' } else { 'disabled' }
102+
$AlertMessage = "Administrator SSPR is currently $CurrentLabel but should be $DesiredLabel."
103+
Write-StandardsAlert -message $AlertMessage -object $CurrentState -tenant $Tenant -standardName 'AdminSSPR' -standardId $Settings.standardId
104+
Write-LogMessage -API 'Standards' -tenant $Tenant -message $AlertMessage -sev Info
105+
}
106+
}
107+
108+
if ($Settings.report -eq $true) {
109+
Set-CIPPStandardsCompareField -FieldName 'standards.AdminSSPR' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant
110+
Add-CIPPBPAField -FieldName 'AdminSSPR' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $Tenant
111+
}
112+
}

0 commit comments

Comments
 (0)