Skip to content

Commit dc8362a

Browse files
authored
Merge pull request #105 from KelvinTegelaar/master
[pull] master from KelvinTegelaar:master
2 parents 6418477 + b58324a commit dc8362a

File tree

201 files changed

+14068
-1369
lines changed

Some content is hidden

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

201 files changed

+14068
-1369
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

ConversionTable.csv

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

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
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
function Get-CIPPAlertLowTenantAlignment {
2+
<#
3+
.SYNOPSIS
4+
Alert for low tenant alignment percentage
5+
.DESCRIPTION
6+
This alert checks tenant alignment scores against standards templates and alerts when the alignment percentage falls below the specified threshold.
7+
.PARAMETER TenantFilter
8+
The tenant to check alignment for
9+
.PARAMETER InputValue
10+
The minimum alignment percentage threshold (0-100). Default is 80.
11+
.FUNCTIONALITY
12+
Entrypoint
13+
.EXAMPLE
14+
Get-CIPPAlertLowTenantAlignment -TenantFilter "contoso.onmicrosoft.com" -InputValue 75
15+
#>
16+
[CmdletBinding()]
17+
param (
18+
[Parameter(Mandatory)]
19+
$TenantFilter,
20+
[Alias('input')]
21+
[ValidateRange(0, 100)]
22+
[int]$InputValue = 99
23+
)
24+
25+
try {
26+
# Get tenant alignment data using the new function
27+
$AlignmentData = Get-CIPPTenantAlignment -TenantFilter $TenantFilter
28+
29+
if (-not $AlignmentData) {
30+
Write-AlertMessage -tenant $TenantFilter -message "No alignment data found for tenant $TenantFilter. This may indicate no standards templates are configured or applied to this tenant."
31+
return
32+
}
33+
34+
$LowAlignmentAlerts = $AlignmentData | Where-Object { $_.AlignmentScore -lt $InputValue } | ForEach-Object {
35+
[PSCustomObject]@{
36+
TenantFilter = $_.TenantFilter
37+
StandardName = $_.StandardName
38+
StandardId = $_.StandardId
39+
AlignmentScore = $_.AlignmentScore
40+
LicenseMissingPercentage = $_.LicenseMissingPercentage
41+
LatestDataCollection = $_.LatestDataCollection
42+
}
43+
}
44+
45+
if ($LowAlignmentAlerts.Count -gt 0) {
46+
Write-AlertTrace -cmdletName $MyInvocation.MyCommand -tenantFilter $TenantFilter -data $LowAlignmentAlerts
47+
}
48+
49+
} catch {
50+
Write-AlertMessage -tenant $TenantFilter -message "Could not get tenant alignment data for $TenantFilter`: $(Get-NormalizedError -message $_.Exception.message)"
51+
}
52+
}

Modules/CIPPCore/Public/Authentication/Get-CIPPHttpFunctions.ps1

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
function Get-CIPPHttpFunctions {
2-
Param(
2+
param(
33
[switch]$ByRole,
44
[switch]$ByRoleGroup
55
)
@@ -8,7 +8,7 @@ function Get-CIPPHttpFunctions {
88
$Functions = Get-Command -Module CIPPCore | Where-Object { $_.Visibility -eq 'Public' -and $_.Name -match 'Invoke-*' }
99
$Results = foreach ($Function in $Functions) {
1010
$Help = Get-Help $Function
11-
if ($Help.Functionality -ne 'Entrypoint') { continue }
11+
if ($Help.Functionality -notmatch 'Entrypoint') { continue }
1212
if ($Help.Role -eq 'Public') { continue }
1313
[PSCustomObject]@{
1414
Function = $Function.Name

0 commit comments

Comments
 (0)