Skip to content

Commit 680460a

Browse files
authored
Merge pull request #851 from KelvinTegelaar/dev
[pull] dev from KelvinTegelaar:dev
2 parents a4f249c + 1db85d2 commit 680460a

21 files changed

Lines changed: 66579 additions & 40603 deletions
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
function Enable-CIPPMDEConnector {
2+
<#
3+
.SYNOPSIS
4+
Provisions the Microsoft Defender for Endpoint Intune connector for a tenant.
5+
.DESCRIPTION
6+
Checks whether the MDE mobile threat defense connector (partnerState) is already 'available' or 'enabled'.
7+
If not, iterates through regional MDE API portal endpoints until one succeeds, then verifies
8+
the connector state afterwards. Endpoints are ordered so that the tenant's likely region
9+
(based on org countryLetterCode) is tried first.
10+
.PARAMETER TenantFilter
11+
The tenant domain or ID to provision the connector for.
12+
.FUNCTIONALITY
13+
Internal
14+
#>
15+
[CmdletBinding()]
16+
param(
17+
[Parameter(Mandatory = $true)]
18+
[string]$TenantFilter
19+
)
20+
21+
# MDE connector ID is fixed across all tenants
22+
$ConnectorId = 'fc780465-2017-40d4-a0c5-307022471b92'
23+
$ConnectorUri = "https://graph.microsoft.com/beta/deviceManagement/mobileThreatDefenseConnectors/$ConnectorId"
24+
25+
# All known regional provisioning endpoints
26+
$AllEndpoints = @(
27+
'mde-rsp-apiportal-prd-eus.securitycenter.windows.com'
28+
'mde-rsp-apiportal-prd-eus3.securitycenter.windows.com'
29+
'mde-rsp-apiportal-prd-cus.securitycenter.windows.com'
30+
'mde-rsp-apiportal-prd-cus3.securitycenter.windows.com'
31+
'mde-rsp-apiportal-prd-weu.securitycenter.windows.com'
32+
'mde-rsp-apiportal-prd-weu3.securitycenter.windows.com'
33+
'mde-rsp-apiportal-prd-neu.securitycenter.windows.com'
34+
'mde-rsp-apiportal-prd-neu3.securitycenter.windows.com'
35+
'mde-rsp-apiportal-prd-uks.securitycenter.windows.com'
36+
'mde-rsp-apiportal-prd-ukw.securitycenter.windows.com'
37+
'mde-rsp-apiportal-prd-aue.securitycenter.windows.com'
38+
'mde-rsp-apiportal-prd-aus.securitycenter.windows.com'
39+
'mde-rsp-apiportal-prd-aec0a.securitycenter.windows.com'
40+
'mde-rsp-apiportal-prd-aen0a.securitycenter.windows.com'
41+
'mde-rsp-apiportal-prd-ins0a.securitycenter.windows.com'
42+
'mde-rsp-apiportal-prd-inc0a.securitycenter.windows.com'
43+
'mde-rsp-apiportal-prd-sww0a.securitycenter.windows.com'
44+
'mde-rsp-apiportal-prd-swn0a.securitycenter.windows.com'
45+
)
46+
47+
# Country code -> likely regional endpoint prefixes (used to prioritize, not restrict)
48+
$RegionPriority = @{
49+
'US' = @('eus', 'eus3', 'cus', 'cus3')
50+
'CA' = @('eus', 'eus3', 'cus', 'cus3')
51+
'GB' = @('uks', 'ukw')
52+
'AU' = @('aue', 'aus', 'aec0a', 'aen0a')
53+
'IN' = @('ins0a', 'inc0a')
54+
'SE' = @('sww0a', 'swn0a')
55+
'DE' = @('weu', 'weu3')
56+
'FR' = @('weu', 'weu3')
57+
'NL' = @('weu', 'weu3')
58+
'BE' = @('weu', 'weu3')
59+
'AT' = @('weu', 'weu3')
60+
'CH' = @('weu', 'weu3')
61+
'IE' = @('neu', 'neu3')
62+
'FI' = @('neu', 'neu3')
63+
'NO' = @('neu', 'neu3')
64+
'DK' = @('neu', 'neu3')
65+
}
66+
67+
# Check current connector state
68+
try {
69+
$ConnectorState = New-GraphGetRequest -uri $ConnectorUri -tenantid $TenantFilter
70+
} catch {
71+
$ErrorMessage = Get-CippException -Exception $_
72+
Write-LogMessage -API 'MDEConnector' -tenant $TenantFilter -message "Failed to retrieve MDE connector state. Error: $($ErrorMessage.NormalizedError)" -Sev Error -LogData $ErrorMessage
73+
throw "Failed to retrieve MDE connector state for $TenantFilter. Error: $($ErrorMessage.NormalizedError)"
74+
}
75+
76+
if ($ConnectorState.partnerState -in @('available', 'enabled')) {
77+
Write-LogMessage -API 'MDEConnector' -tenant $TenantFilter -message 'MDE Intune connector is already in available state.' -Sev Info
78+
return [PSCustomObject]@{
79+
Success = $true
80+
AlreadyDone = $true
81+
PartnerState = $ConnectorState.partnerState
82+
}
83+
}
84+
85+
# Build a prioritized endpoint list based on tenant country
86+
$PrioritizedEndpoints = [System.Collections.Generic.List[string]]::new()
87+
try {
88+
$OrgInfo = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/organization' -tenantid $TenantFilter
89+
$CountryCode = $OrgInfo.countryLetterCode
90+
if ($CountryCode -and $RegionPriority.ContainsKey($CountryCode)) {
91+
$PrefixHints = $RegionPriority[$CountryCode]
92+
foreach ($endpoint in $AllEndpoints) {
93+
foreach ($prefix in $PrefixHints) {
94+
if ($endpoint -like "*-prd-$prefix.*") {
95+
$PrioritizedEndpoints.Add($endpoint)
96+
break
97+
}
98+
}
99+
}
100+
}
101+
Write-Information "MDE connector provisioning for $TenantFilter (country: $CountryCode): prioritized $($PrioritizedEndpoints.Count) regional endpoint(s)"
102+
} catch {
103+
Write-Information "Could not retrieve org country for $TenantFilter - will try all endpoints"
104+
}
105+
106+
# Append remaining endpoints that weren't already prioritized
107+
foreach ($endpoint in $AllEndpoints) {
108+
if ($endpoint -notin $PrioritizedEndpoints) {
109+
$PrioritizedEndpoints.Add($endpoint)
110+
}
111+
}
112+
113+
# Try each endpoint until one succeeds
114+
$ProvisionBody = '{"timeout":60000}'
115+
$ProvisionScope = 'https://api.securitycenter.windows.com/.default'
116+
$SuccessfulEndpoint = $null
117+
118+
foreach ($endpoint in $PrioritizedEndpoints) {
119+
$ProvisionUri = "https://$endpoint/api/cloud/portal/onboarding/intune/provision"
120+
try {
121+
Write-Information "Attempting MDE provisioning for $TenantFilter via $endpoint"
122+
$null = New-GraphPOSTRequest -uri $ProvisionUri -tenantid $TenantFilter -body $ProvisionBody -scope $ProvisionScope
123+
$SuccessfulEndpoint = $endpoint
124+
Write-LogMessage -API 'MDEConnector' -tenant $TenantFilter -message "MDE Intune connector provisioned successfully via $endpoint" -Sev Info
125+
break
126+
} catch {
127+
$ErrorMessage = Get-CippException -Exception $_
128+
Write-Information "Endpoint $endpoint failed for $TenantFilter`: $($ErrorMessage.NormalizedError)"
129+
}
130+
}
131+
132+
if (-not $SuccessfulEndpoint) {
133+
$Msg = "Failed to provision MDE Intune connector for $TenantFilter - all regional endpoints were unsuccessful."
134+
Write-LogMessage -API 'MDEConnector' -tenant $TenantFilter -message $Msg -Sev Error
135+
throw $Msg
136+
}
137+
138+
# Verify the connector state after provisioning
139+
try {
140+
$UpdatedState = New-GraphGetRequest -uri $ConnectorUri -tenantid $TenantFilter
141+
} catch {
142+
$UpdatedState = $null
143+
}
144+
145+
return [PSCustomObject]@{
146+
Success = $UpdatedState.partnerState -in @('available', 'enabled')
147+
AlreadyDone = $false
148+
Endpoint = $SuccessfulEndpoint
149+
PartnerState = $UpdatedState.partnerState
150+
}
151+
}

Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Mailbox Permissions/Push-GetCalendarPermissionsBatch.ps1

Lines changed: 67 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ function Push-GetCalendarPermissionsBatch {
44
Process a batch of calendar permission queries
55
66
.DESCRIPTION
7-
Queries calendar permissions for a batch of mailboxes
7+
Queries calendar permissions for a batch of mailboxes.
8+
Uses a folder name cache to avoid the expensive Get-MailboxFolderStatistics call
9+
on subsequent runs. First run discovers and caches the locale-specific calendar
10+
folder name; all future runs skip that call entirely (50% fewer Exchange requests).
811
912
.FUNCTIONALITY
1013
Entrypoint
@@ -19,39 +22,83 @@ function Push-GetCalendarPermissionsBatch {
1922
try {
2023
Write-Information "Processing calendar permissions batch $BatchNumber of $TotalBatches for tenant $TenantFilter with $($Mailboxes.Count) mailboxes"
2124

25+
# Load cached calendar folder names for this tenant
26+
$FolderCacheTable = Get-CippTable -tablename 'CalendarFolderCache'
27+
$CachedFolders = @{}
28+
try {
29+
30+
$CacheEntries = Get-CIPPAzDataTableEntity @FolderCacheTable -Filter "PartitionKey eq '$TenantFilter'"
31+
foreach ($Entry in $CacheEntries) {
32+
$CachedFolders[$Entry.RowKey] = $Entry.FolderName
33+
}
34+
Write-Host "CAL Cached Folders count is $($CachedFolders.count)"
35+
} catch {
36+
Write-Information "Could not load folder name cache for $TenantFilter, will discover all folder names"
37+
}
38+
39+
$CacheHits = 0
40+
$CacheMisses = 0
41+
$NewCacheEntries = [System.Collections.Generic.List[hashtable]]::new()
2242
$AllCalendarPermissions = [System.Collections.Generic.List[object]]::new()
2343

2444
foreach ($MailboxUPN in $Mailboxes) {
2545
try {
26-
# Step 1: Get the calendar folder name (locale-specific)
27-
$GetCalParam = @{Identity = $MailboxUPN; FolderScope = 'Calendar' }
28-
$CalendarFolder = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-MailboxFolderStatistics' -anchor $MailboxUPN -cmdParams $GetCalParam | Select-Object -First 1
29-
30-
if ($CalendarFolder -and $CalendarFolder.name) {
31-
# Step 2: Get calendar permissions using the folder name
32-
$CalParam = @{Identity = "$($MailboxUPN):\$($CalendarFolder.name)" }
33-
$CalendarPermissions = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-MailboxFolderPermission' -anchor $MailboxUPN -cmdParams $CalParam -UseSystemMailbox $true
34-
35-
# Normalize the results
36-
foreach ($Perm in $CalendarPermissions) {
37-
$AllCalendarPermissions.Add([PSCustomObject]@{
38-
id = [guid]::NewGuid().ToString()
39-
Identity = $Perm.Identity
40-
User = $Perm.User
41-
AccessRights = $Perm.AccessRights
42-
FolderName = $Perm.FolderName
46+
# Check cache for folder name
47+
$FolderName = $CachedFolders[$MailboxUPN]
48+
49+
if (-not $FolderName) {
50+
# Cache miss — discover the locale-specific calendar folder name
51+
$CacheMisses++
52+
$GetCalParam = @{Identity = $MailboxUPN; FolderScope = 'Calendar' }
53+
$CalendarFolder = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-MailboxFolderStatistics' -anchor $MailboxUPN -cmdParams $GetCalParam | Select-Object -First 1
54+
55+
if ($CalendarFolder -and $CalendarFolder.name) {
56+
$FolderName = $CalendarFolder.name
57+
# Queue for cache write
58+
$NewCacheEntries.Add(@{
59+
PartitionKey = $TenantFilter
60+
RowKey = $MailboxUPN
61+
FolderName = $FolderName
4362
})
63+
} else {
64+
Write-Information "No calendar folder found for mailbox $MailboxUPN"
65+
continue
4466
}
4567
} else {
46-
Write-Information "No calendar folder found for mailbox $MailboxUPN"
68+
$CacheHits++
69+
}
70+
71+
# Get calendar permissions using the folder name
72+
$CalParam = @{Identity = "$($MailboxUPN):\$($FolderName)" }
73+
$CalendarPermissions = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-MailboxFolderPermission' -anchor $MailboxUPN -cmdParams $CalParam -UseSystemMailbox $true
74+
75+
# Normalize the results
76+
foreach ($Perm in $CalendarPermissions) {
77+
$AllCalendarPermissions.Add([PSCustomObject]@{
78+
id = [guid]::NewGuid().ToString()
79+
Identity = $Perm.Identity
80+
User = $Perm.User
81+
AccessRights = $Perm.AccessRights
82+
FolderName = $Perm.FolderName
83+
})
4784
}
4885
} catch {
4986
Write-Information "Failed to get calendar permissions for $MailboxUPN : $($_.Exception.Message)"
5087
# Continue processing other mailboxes
5188
}
5289
}
5390

54-
Write-Information "Completed calendar permissions batch $BatchNumber of $TotalBatches - processed $($Mailboxes.Count) mailboxes: $($AllCalendarPermissions.Count) calendar permissions"
91+
# Persist newly discovered folder names to cache
92+
if ($NewCacheEntries.Count -gt 0) {
93+
try {
94+
Add-CIPPAzDataTableEntity @FolderCacheTable -Entity $NewCacheEntries -Force
95+
Write-Information "Cached $($NewCacheEntries.Count) new calendar folder names for $TenantFilter"
96+
} catch {
97+
Write-Information "Failed to write folder name cache for $TenantFilter : $($_.Exception.Message)"
98+
}
99+
}
100+
101+
Write-Information "Completed calendar permissions batch $BatchNumber of $TotalBatches - processed $($Mailboxes.Count) mailboxes: $($AllCalendarPermissions.Count) permissions (cache hits: $CacheHits, misses: $CacheMisses)"
55102

56103
# Return results grouped by command type for consistency with mailbox permissions
57104
return @{

Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,14 +35,23 @@ function Push-CIPPDBCacheData {
3535
# Build grouped collection tasks — one activity per license category instead of one per cache type
3636
$Tasks = [System.Collections.Generic.List[object]]::new()
3737

38-
# Graph collection always runs (no special license needed) — 26 cache types in one activity
38+
# Graph collection always runs (no special license needed) — 25 cache types in one activity
3939
$Tasks.Add(@{
4040
FunctionName = 'ExecCIPPDBCache'
4141
CollectionType = 'Graph'
4242
TenantFilter = $TenantFilter
4343
QueueId = $QueueId
4444
QueueName = "DB Cache Graph - $TenantFilter"
4545
})
46+
# MFAState runs as its own activity — it makes 6+ API calls, bulk group/role member
47+
# resolution, and O(users × policies) CPU work that can take minutes on large tenants
48+
$Tasks.Add(@{
49+
FunctionName = 'ExecCIPPDBCache'
50+
Name = 'MFAState'
51+
TenantFilter = $TenantFilter
52+
QueueId = $QueueId
53+
QueueName = "DB Cache MFAState - $TenantFilter"
54+
})
4655

4756
# Exchange collections — split into config (quick policy calls), data (usage reports), and mailboxes (heavy, spawns permission/rule child orchestrators)
4857
if ($ExchangeCapable) {

Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-AddDefenderDeployment.ps1

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ function Invoke-AddDefenderDeployment {
1111
$APIName = $Request.Params.CIPPEndpoint
1212
$Headers = $Request.Headers
1313

14-
1514
$Tenants = ($Request.Body.selectedTenants).value
1615
if ('AllTenants' -in $Tenants) { $Tenants = (Get-Tenants -IncludeErrors).defaultDomainName }
1716
$Compliance = $Request.Body.Compliance
@@ -22,6 +21,14 @@ function Invoke-AddDefenderDeployment {
2221
$Results = foreach ($tenant in $Tenants) {
2322
try {
2423
if ($Compliance) {
24+
$ConnectorStatus = Enable-CIPPMDEConnector -TenantFilter $tenant
25+
if (!$ConnectorStatus.Success) {
26+
"$($tenant): Failed to enable MDE Connector - $($ConnectorStatus.ErrorMessage)"
27+
continue
28+
} else {
29+
"$($tenant): MDE Connector is $($ConnectorStatus.PartnerState). Attempting to set compliance and reporting settings..."
30+
}
31+
2532
$SettingsObject = @{
2633
id = 'fc780465-2017-40d4-a0c5-307022471b92'
2734
androidEnabled = [bool]$Compliance.ConnectAndroid

Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-AddUser.ps1

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,11 @@ function Invoke-AddUser {
6161
$body = [pscustomobject] @{
6262
'Results' = @(
6363
$CreationResults.Results[0],
64-
$CreationResults.Results[1],
64+
@{
65+
'resultText' = $CreationResults.Results[1]
66+
'copyField' = $CreationResults.Username
67+
'state' = 'success'
68+
},
6569
@{
6670
'resultText' = $CreationResults.Results[2]
6771
'copyField' = $CreationResults.password

Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-AddUserDefaults.ps1

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -106,8 +106,8 @@ function Invoke-AddUserDefaults {
106106
copyFrom = $CopyFrom
107107
}
108108

109-
# Generate GUID for the template
110-
$GUID = (New-Guid).GUID
109+
# Use existing GUID if editing, otherwise generate new one
110+
$GUID = if ($Request.Body.GUID) { $Request.Body.GUID } else { (New-Guid).GUID }
111111

112112
# Convert to JSON
113113
$JSON = ConvertTo-Json -InputObject $TemplateObject -Depth 100 -Compress
@@ -122,7 +122,8 @@ function Invoke-AddUserDefaults {
122122
GUID = "$GUID"
123123
}
124124

125-
$Result = "Created User Default Template '$($TemplateName)' with GUID $GUID"
125+
$Action = if ($Request.Body.GUID) { 'Updated' } else { 'Created' }
126+
$Result = "$Action User Default Template '$($TemplateName)' with GUID $GUID"
126127
Write-LogMessage -headers $Headers -API $APIName -message $Result -Sev 'Info'
127128
$StatusCode = [HttpStatusCode]::OK
128129

Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecJITAdmin.ps1

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,14 @@ function Invoke-ExecJITAdmin {
183183
'UserPrincipalName' = $Username
184184
}
185185
Roles = $Request.Body.AdminRoles.value
186-
Action = 'AddRoles'
186+
Groups = $Request.Body.GroupMemberships.value
187+
Action = if ($Request.Body.AdminRoles.value -and $Request.Body.GroupMemberships.value) {
188+
'AddRolesAndGroups'
189+
} elseif ($Request.Body.GroupMemberships.value) {
190+
'AddGroups'
191+
} else {
192+
'AddRoles'
193+
}
187194
Reason = $Request.Body.Reason
188195
Expiration = $Expiration
189196
StartDate = $Start
@@ -238,6 +245,7 @@ function Invoke-ExecJITAdmin {
238245
'UserPrincipalName' = $Username
239246
}
240247
Roles = $Request.Body.AdminRoles.value
248+
Groups = $Request.Body.GroupMemberships.value
241249
Reason = $Request.Body.Reason
242250
Action = $Request.Body.ExpireAction.value
243251
}

Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListJITAdmin.ps1

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
$BulkRequests.Add(@{
3333
id = $User.id
3434
method = 'GET'
35-
url = "users/$($User.id)/memberOf/microsoft.graph.directoryRole/?`$select=id,displayName"
35+
url = "users/$($User.id)/memberOf?`$select=id,displayName"
3636
})
3737
}
3838
$RoleResults = New-GraphBulkRequest -tenantid $TenantFilter -Requests @($BulkRequests)

Modules/CIPPCore/Public/GraphHelper/New-GraphGetRequest.ps1

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ function New-GraphGetRequest {
110110
StatusCode = $Data.StatusCode
111111
StatusDescription = $Data.StatusDescription
112112
Content = $Content
113+
Headers = $Data.Headers
113114
}
114115
$nextURL = $null
115116
} elseif ($CountOnly) {

0 commit comments

Comments
 (0)