Skip to content

Commit 5711b7b

Browse files
authored
Merge branch 'KelvinTegelaar:master' into master
2 parents f7e7df9 + 9acc3a2 commit 5711b7b

File tree

384 files changed

+24915
-3122
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

384 files changed

+24915
-3122
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,7 @@ Logs
99
ExcludedTenants
1010
SendNotifications/config.json
1111
.env
12+
13+
14+
# Cursor IDE
15+
.cursor/rules

CIPP-Permissions.json

Lines changed: 804 additions & 0 deletions
Large diffs are not rendered by default.

CIPPTimers.json

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -80,14 +80,6 @@
8080
"RunOnProcessor": true,
8181
"PreferredProcessor": "standards"
8282
},
83-
{
84-
"Id": "5113c66d-c040-42df-9565-39dff90ddd55",
85-
"Command": "Start-CIPPGraphSubscriptionCleanupTimer",
86-
"Description": "Orchestrator to cleanup old Graph subscriptions",
87-
"Cron": "0 0 0 * * *",
88-
"Priority": 5,
89-
"RunOnProcessor": true
90-
},
9183
{
9284
"Id": "97145a1d-28f0-4bb2-b929-5a43517d23cc",
9385
"Command": "Start-SchedulerOrchestrator",

Config/standards.json

Lines changed: 1068 additions & 115 deletions
Large diffs are not rendered by default.

ConversionTable.csv

Lines changed: 172 additions & 98 deletions
Large diffs are not rendered by default.

Modules/CIPPCore/Public/Add-CIPPAlias.ps1

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ function Add-CIPPAlias {
55
$Aliases,
66
$UserprincipalName,
77
$TenantFilter,
8-
$APIName = 'Set Manager',
8+
$APIName = 'Add Alias',
99
$Headers
1010
)
1111

Modules/CIPPCore/Public/Add-CIPPDelegatedPermission.ps1

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,10 @@ function Add-CIPPDelegatedPermission {
114114
$OldScope = ($CurrentDelegatedScopes | Where-Object -Property Resourceid -EQ $svcPrincipalId.id)
115115

116116
if (!$OldScope) {
117+
if ([string]::IsNullOrEmpty($NewScope) -or $NewScope -eq ' ') {
118+
$Results.add("No delegated permissions to add for $($svcPrincipalId.displayName)")
119+
continue
120+
}
117121
try {
118122
$Createbody = @{
119123
clientId = $ourSVCPrincipal.id
@@ -147,6 +151,13 @@ function Add-CIPPDelegatedPermission {
147151
$Results.add("All delegated permissions exist for $($svcPrincipalId.displayName)")
148152
continue
149153
}
154+
155+
if ([string]::IsNullOrEmpty($NewScope) -or $NewScope -eq ' ') {
156+
# No permissions to update
157+
$Results.add("No delegated permissions to update for $($svcPrincipalId.displayName)")
158+
continue
159+
}
160+
150161
$Patchbody = @{
151162
scope = "$NewScope"
152163
} | ConvertTo-Json -Compress

Modules/CIPPCore/Public/Add-CIPPGroupMember.ps1

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,22 @@ function Add-CIPPGroupMember(
77
[string]$APIName = 'Add Group Member'
88
) {
99
try {
10-
if ($member -like '*#EXT#*') { $member = [System.Web.HttpUtility]::UrlEncode($member) }
11-
$MemberIDs = 'https://graph.microsoft.com/v1.0/directoryObjects/' + (New-GraphGetRequest -uri "https://graph.microsoft.com/beta/users/$($member)" -tenantid $TenantFilter).id
12-
$addmemberbody = "{ `"[email protected]`": $(ConvertTo-Json @($MemberIDs)) }"
10+
if ($Member -like '*#EXT#*') { $Member = [System.Web.HttpUtility]::UrlEncode($Member) }
11+
$MemberIDs = 'https://graph.microsoft.com/v1.0/directoryObjects/' + (New-GraphGetRequest -uri "https://graph.microsoft.com/beta/users/$($Member)" -tenantid $TenantFilter).id
12+
$AddMemberBody = "{ `"[email protected]`": $(ConvertTo-Json @($MemberIDs)) }"
1313
if ($GroupType -eq 'Distribution list' -or $GroupType -eq 'Mail-Enabled Security') {
14-
$Params = @{ Identity = $GroupId; Member = $member; BypassSecurityGroupManagerCheck = $true }
15-
$null = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Add-DistributionGroupMember' -cmdParams $params -UseSystemMailbox $true
14+
$Params = @{ Identity = $GroupId; Member = $Member; BypassSecurityGroupManagerCheck = $true }
15+
$null = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Add-DistributionGroupMember' -cmdParams $Params -UseSystemMailbox $true
1616
} else {
17-
$null = New-GraphPostRequest -uri "https://graph.microsoft.com/beta/groups/$($GroupId)" -tenantid $TenantFilter -type patch -body $addmemberbody -Verbose
17+
$null = New-GraphPostRequest -uri "https://graph.microsoft.com/beta/groups/$($GroupId)" -tenantid $TenantFilter -type patch -body $AddMemberBody -Verbose
1818
}
19-
$Message = "Successfully added user $($Member) to $($GroupId)."
20-
Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $Message -Sev 'Info'
21-
return $message
19+
$Results = "Successfully added user $($Member) to $($GroupId)."
20+
Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $Results -Sev 'Info'
21+
return $Results
2222
} catch {
23-
$message = "Failed to add user $($Member) to $($GroupId) - $($_.Exception.Message)"
24-
Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $message -Sev 'error' -LogData (Get-CippException -Exception $_)
25-
return $message
23+
$ErrorMessage = Get-CippException -Exception $_
24+
$Results = "Failed to add user $($Member) to $($GroupId) - $($ErrorMessage.NormalizedError)"
25+
Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $Results -Sev 'error' -LogData $ErrorMessage
26+
throw $Results
2627
}
2728
}

Modules/CIPPCore/Public/Add-CIPPScheduledTask.ps1

Lines changed: 161 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -24,106 +24,178 @@ function Add-CIPPScheduledTask {
2424
$Headers
2525
)
2626

27-
$Table = Get-CIPPTable -TableName 'ScheduledTasks'
28-
29-
if ($RunNow.IsPresent -and $RowKey) {
30-
try {
31-
$Filter = "PartitionKey eq 'ScheduledTask' and RowKey eq '$($RowKey)'"
32-
$ExistingTask = (Get-CIPPAzDataTableEntity @Table -Filter $Filter)
33-
$ExistingTask.ScheduledTime = [int64](([datetime]::UtcNow) - (Get-Date '1/1/1970')).TotalSeconds
34-
$ExistingTask.TaskState = 'Planned'
35-
Add-CIPPAzDataTableEntity @Table -Entity $ExistingTask -Force
36-
Write-LogMessage -headers $Headers -API 'RunNow' -message "Task $($ExistingTask.Name) scheduled to run now" -Sev 'Info' -Tenant $ExistingTask.Tenant
37-
return "Task $($ExistingTask.Name) scheduled to run now"
38-
} catch {
39-
$ErrorMessage = Get-NormalizedError -Message $_.Exception.Message
40-
Write-LogMessage -headers $Headers -API 'RunNow' -message "Could not run task: $ErrorMessage" -Sev 'Error'
41-
return "Could not run task: $ErrorMessage"
42-
}
43-
} else {
44-
if ($DisallowDuplicateName) {
45-
$Filter = "PartitionKey eq 'ScheduledTask' and Name eq '$($Task.Name)'"
46-
$ExistingTask = (Get-CIPPAzDataTableEntity @Table -Filter $Filter)
47-
if ($ExistingTask) {
48-
return "Task with name $($Task.Name) already exists"
27+
try {
28+
29+
$Table = Get-CIPPTable -TableName 'ScheduledTasks'
30+
31+
if ($RunNow.IsPresent -and $RowKey) {
32+
try {
33+
$Filter = "PartitionKey eq 'ScheduledTask' and RowKey eq '$($RowKey)'"
34+
$ExistingTask = (Get-CIPPAzDataTableEntity @Table -Filter $Filter)
35+
$ExistingTask.ScheduledTime = [int64](([datetime]::UtcNow) - (Get-Date '1/1/1970')).TotalSeconds
36+
$ExistingTask.TaskState = 'Planned'
37+
Add-CIPPAzDataTableEntity @Table -Entity $ExistingTask -Force
38+
Write-LogMessage -headers $Headers -API 'RunNow' -message "Task $($ExistingTask.Name) scheduled to run now" -Sev 'Info' -Tenant $ExistingTask.Tenant
39+
return "Task $($ExistingTask.Name) scheduled to run now"
40+
} catch {
41+
$ErrorMessage = Get-NormalizedError -Message $_.Exception.Message
42+
Write-LogMessage -headers $Headers -API 'RunNow' -message "Could not run task: $ErrorMessage" -Sev 'Error'
43+
return "Could not run task: $ErrorMessage"
4944
}
50-
}
45+
} else {
46+
if ($DisallowDuplicateName) {
47+
$Filter = "PartitionKey eq 'ScheduledTask' and Name eq '$($Task.Name)'"
48+
$ExistingTask = (Get-CIPPAzDataTableEntity @Table -Filter $Filter)
49+
if ($ExistingTask) {
50+
return "Task with name $($Task.Name) already exists"
51+
}
52+
}
53+
54+
$propertiesToCheck = @('Webhook', 'Email', 'PSA')
55+
$PostExecutionObject = ($propertiesToCheck | Where-Object { $task.PostExecution.$_ -eq $true })
56+
$PostExecution = $PostExecutionObject ? ($PostExecutionObject -join ',') : ($Task.PostExecution.value -join ',')
57+
$Parameters = [System.Collections.Hashtable]@{}
58+
foreach ($Key in $task.Parameters.PSObject.Properties.Name) {
59+
$Param = $task.Parameters.$Key
60+
61+
if ($null -eq $Param -or $Param -eq '' -or ($Param | Measure-Object).Count -eq 0) {
62+
continue
63+
}
5164

52-
$propertiesToCheck = @('Webhook', 'Email', 'PSA')
53-
$PostExecutionObject = ($propertiesToCheck | Where-Object { $task.PostExecution.$_ -eq $true })
54-
$PostExecution = $PostExecutionObject ? ($PostExecutionObject -join ',') : ($Task.PostExecution.value -join ',')
55-
$Parameters = [System.Collections.Hashtable]@{}
56-
foreach ($Key in $task.Parameters.PSObject.Properties.Name) {
57-
$Param = $task.Parameters.$Key
65+
# handle different object types in params
66+
if ($Param -is [System.Collections.IDictionary] -or $Param[0].Key) {
67+
Write-Information "Parameter $Key is a hashtable"
68+
$ht = @{}
69+
foreach ($p in $Param.GetEnumerator()) {
70+
$ht[$p.Key] = $p.Value
71+
}
72+
$Parameters[$Key] = [PSCustomObject]$ht
73+
Write-Information "Converted $Key to PSObject $($Parameters[$Key] | ConvertTo-Json -Compress)"
74+
} elseif ($Param -is [System.Object[]] -and -not ($Param -is [string])) {
75+
Write-Information "Parameter $Key is an enumerable object"
76+
$Param = $Param | ForEach-Object {
77+
if ($null -eq $_) {
78+
# Skip null entries
79+
return
80+
}
81+
if ($_ -is [System.Collections.IDictionary]) {
82+
[PSCustomObject]$_
83+
} elseif ($_ -is [PSCustomObject]) {
84+
$_
85+
} else {
86+
$_
87+
}
88+
} | Where-Object { $null -ne $_ }
89+
$Parameters[$Key] = $Param
90+
} else {
91+
Write-Information "Parameter $Key is a simple value"
92+
$Parameters[$Key] = $Param
93+
}
94+
}
5895

59-
if ($null -eq $Param -or $Param -eq '' -or ($Param | Measure-Object).Count -eq 0) {
60-
continue
96+
if ($Headers) {
97+
$Parameters.Headers = $Headers | Select-Object -Property 'x-forwarded-for', 'x-ms-client-principal', 'x-ms-client-principal-idp', 'x-ms-client-principal-name'
6198
}
62-
if ($Param -is [System.Collections.IDictionary] -or $Param.Key) {
63-
$ht = @{}
64-
foreach ($p in $Param.GetEnumerator()) {
65-
$ht[$p.Key] = $p.Value
99+
100+
$Parameters = ($Parameters | ConvertTo-Json -Depth 10 -Compress)
101+
$AdditionalProperties = [System.Collections.Hashtable]@{}
102+
foreach ($Prop in $task.AdditionalProperties) {
103+
if ($null -eq $Prop.Value -or $Prop.Value -eq '' -or ($Prop.Value | Measure-Object).Count -eq 0) {
104+
continue
66105
}
67-
$Parameters[$Key] = [PSCustomObject]$ht
106+
$AdditionalProperties[$Prop.Key] = $Prop.Value
107+
}
108+
$AdditionalProperties = ([PSCustomObject]$AdditionalProperties | ConvertTo-Json -Compress)
109+
if ($Parameters -eq 'null') { $Parameters = '' }
110+
if (!$Task.RowKey) {
111+
$RowKey = (New-Guid).Guid
68112
} else {
69-
$Parameters[$Key] = $Param
113+
$RowKey = $Task.RowKey
70114
}
71-
}
72115

73-
if ($Headers) {
74-
$Parameters.Headers = $Headers | Select-Object -Property 'x-forwarded-for', 'x-ms-client-principal', 'x-ms-client-principal-idp', 'x-ms-client-principal-name'
75-
}
116+
$Recurrence = if ([string]::IsNullOrEmpty($task.Recurrence.value)) {
117+
$task.Recurrence
118+
} else {
119+
$task.Recurrence.value
120+
}
76121

77-
$Parameters = ($Parameters | ConvertTo-Json -Depth 10 -Compress)
78-
$AdditionalProperties = [System.Collections.Hashtable]@{}
79-
foreach ($Prop in $task.AdditionalProperties) {
80-
$AdditionalProperties[$Prop.Key] = $Prop.Value
81-
}
82-
$AdditionalProperties = ([PSCustomObject]$AdditionalProperties | ConvertTo-Json -Compress)
83-
if ($Parameters -eq 'null') { $Parameters = '' }
84-
if (!$Task.RowKey) {
85-
$RowKey = (New-Guid).Guid
86-
} else {
87-
$RowKey = $Task.RowKey
88-
}
122+
if ([int64]$task.ScheduledTime -eq 0 -or [string]::IsNullOrEmpty($task.ScheduledTime)) {
123+
$task.ScheduledTime = [int64](([datetime]::UtcNow) - (Get-Date '1/1/1970')).TotalSeconds
124+
}
125+
$excludedTenants = if ($task.excludedTenants.value) {
126+
$task.excludedTenants.value -join ','
127+
}
89128

90-
$Recurrence = if ([string]::IsNullOrEmpty($task.Recurrence.value)) {
91-
$task.Recurrence
92-
} else {
93-
$task.Recurrence.value
94-
}
129+
# Handle tenant filter - support both single tenant and tenant groups
130+
$tenantFilter = $task.TenantFilter.value ? $task.TenantFilter.value : $task.TenantFilter
131+
$originalTenantFilter = $task.TenantFilter
95132

96-
if ([int64]$task.ScheduledTime -eq 0 -or [string]::IsNullOrEmpty($task.ScheduledTime)) {
97-
$task.ScheduledTime = [int64](([datetime]::UtcNow) - (Get-Date '1/1/1970')).TotalSeconds
98-
}
99-
$excludedTenants = if ($task.excludedTenants.value) {
100-
$task.excludedTenants.value -join ','
101-
}
102-
$entity = @{
103-
PartitionKey = [string]'ScheduledTask'
104-
TaskState = [string]'Planned'
105-
RowKey = [string]$RowKey
106-
Tenant = $task.TenantFilter.value ? "$($task.TenantFilter.value)" : "$($task.TenantFilter)"
107-
excludedTenants = [string]$excludedTenants
108-
Name = [string]$task.Name
109-
Command = [string]$task.Command.value
110-
Parameters = [string]$Parameters
111-
ScheduledTime = [string]$task.ScheduledTime
112-
Recurrence = [string]$Recurrence
113-
PostExecution = [string]$PostExecution
114-
AdditionalProperties = [string]$AdditionalProperties
115-
Hidden = [bool]$Hidden
116-
Results = 'Planned'
117-
}
118-
if ($SyncType) {
119-
$entity.SyncType = $SyncType
120-
}
121-
try {
122-
Add-CIPPAzDataTableEntity @Table -Entity $entity -Force
123-
} catch {
124-
$ErrorMessage = Get-NormalizedError -Message $_.Exception.Message
125-
return "Could not add task: $ErrorMessage"
133+
# If tenant filter is a complex object (from form), extract the value
134+
if ($tenantFilter -is [PSCustomObject] -and $tenantFilter.value) {
135+
$originalTenantFilter = $tenantFilter
136+
$tenantFilter = $tenantFilter.value
137+
}
138+
139+
# If tenant filter is a string but still seems to be JSON, try to parse it
140+
if ($tenantFilter -is [string] -and $tenantFilter.StartsWith('{')) {
141+
try {
142+
$parsedTenantFilter = $tenantFilter | ConvertFrom-Json
143+
if ($parsedTenantFilter.value) {
144+
$originalTenantFilter = $parsedTenantFilter
145+
$tenantFilter = $parsedTenantFilter.value
146+
}
147+
} catch {
148+
# If parsing fails, use the string as is
149+
Write-Warning "Could not parse tenant filter JSON: $tenantFilter"
150+
}
151+
}
152+
153+
$entity = @{
154+
PartitionKey = [string]'ScheduledTask'
155+
TaskState = [string]'Planned'
156+
RowKey = [string]$RowKey
157+
Tenant = [string]$tenantFilter
158+
excludedTenants = [string]$excludedTenants
159+
Name = [string]$task.Name
160+
Command = [string]$task.Command.value
161+
Parameters = [string]$Parameters
162+
ScheduledTime = [string]$task.ScheduledTime
163+
Recurrence = [string]$Recurrence
164+
PostExecution = [string]$PostExecution
165+
AdditionalProperties = [string]$AdditionalProperties
166+
Hidden = [bool]$Hidden
167+
Results = 'Planned'
168+
}
169+
170+
# Store the original tenant filter for group expansion during execution
171+
if ($originalTenantFilter -is [PSCustomObject] -and $originalTenantFilter.type -eq 'Group') {
172+
$entity['TenantGroup'] = [string]($originalTenantFilter | ConvertTo-Json -Compress)
173+
} elseif ($originalTenantFilter -is [string] -and $originalTenantFilter.StartsWith('{')) {
174+
# Check if it's a serialized group object
175+
try {
176+
$parsedOriginal = $originalTenantFilter | ConvertFrom-Json
177+
if ($parsedOriginal.type -eq 'Group') {
178+
$entity['TenantGroup'] = [string]$originalTenantFilter
179+
}
180+
} catch {
181+
# Not a JSON object, ignore
182+
}
183+
}
184+
if ($SyncType) {
185+
$entity.SyncType = $SyncType
186+
}
187+
try {
188+
Add-CIPPAzDataTableEntity @Table -Entity $entity -Force
189+
} catch {
190+
$ErrorMessage = Get-NormalizedError -Message $_.Exception.Message
191+
return "Could not add task: $ErrorMessage"
192+
}
193+
return "Successfully added task: $($entity.Name)"
126194
}
127-
return "Successfully added task: $($entity.Name)"
195+
} catch {
196+
Write-Warning "Failed to add scheduled task: $($_.Exception.Message)"
197+
Write-Information $_.InvocationInfo.PositionMessage
198+
$ErrorMessage = Get-NormalizedError -Message $_.Exception.Message
199+
throw "Could not add task: $ErrorMessage"
128200
}
129201
}

Modules/CIPPCore/Public/Alerts/Get-CIPPAlertAppSecretExpiry.ps1

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,22 @@ function Get-CIPPAlertAppSecretExpiry {
1818
return
1919
}
2020

21-
$AlertData = foreach ($App in $applist) {
21+
$AlertData = [System.Collections.Generic.List[PSCustomObject]]::new()
22+
23+
foreach ($App in $applist) {
2224
Write-Host "checking $($App.displayName)"
2325
if ($App.passwordCredentials) {
2426
foreach ($Credential in $App.passwordCredentials) {
2527
if ($Credential.endDateTime -lt (Get-Date).AddDays(30) -and $Credential.endDateTime -gt (Get-Date).AddDays(-7)) {
2628
Write-Host ("Application '{0}' has secrets expiring on {1}" -f $App.displayName, $Credential.endDateTime)
27-
@{ DisplayName = $App.displayName; Expires = $Credential.endDateTime }
29+
30+
$Message = [PSCustomObject]@{
31+
AppName = $App.displayName
32+
AppId = $App.appId
33+
Expires = $Credential.endDateTime
34+
Tenant = $TenantFilter
35+
}
36+
$AlertData.Add($Message)
2837
}
2938
}
3039
}

0 commit comments

Comments
 (0)