diff --git a/Modules/CIPPCore/Private/Test-CIPPConditionFilter.ps1 b/Modules/CIPPCore/Private/Test-CIPPConditionFilter.ps1 new file mode 100644 index 000000000000..6369094e852d --- /dev/null +++ b/Modules/CIPPCore/Private/Test-CIPPConditionFilter.ps1 @@ -0,0 +1,78 @@ +function Test-CIPPConditionFilter { + <# + .SYNOPSIS + Returns a sanitized PowerShell condition string for an audit log / delta query condition. + .DESCRIPTION + Validates operator and property name against allowlists, sanitizes input values, + then returns a safe condition string suitable for [ScriptBlock]::Create(). + + This replaces the old Invoke-Expression pattern which was vulnerable to code injection + through unsanitized user-controlled fields. + .PARAMETER Condition + A single condition object with Property.label, Operator.value, and Input.value. + .OUTPUTS + [string] A sanitized PowerShell condition string, or $null if validation fails. + .FUNCTIONALITY + Internal + #> + [CmdletBinding()] + [OutputType([string])] + param( + [Parameter(Mandatory = $true)] + $Condition + ) + + # Operator allowlist - only these PowerShell comparison operators are permitted + $AllowedOperators = @( + 'eq', 'ne', 'like', 'notlike', 'match', 'notmatch', + 'gt', 'lt', 'ge', 'le', 'in', 'notin', + 'contains', 'notcontains' + ) + + # Property name validation - only alphanumeric, underscores, and dots allowed + $SafePropertyRegex = [regex]'^[a-zA-Z0-9_.]+$' + + # Value sanitization - block characters that enable code injection + $UnsafeValueRegex = [regex]'[;|`\$\{\}]' + + $propertyName = $Condition.Property.label + $operatorValue = $Condition.Operator.value.ToLower() + $inputValue = $Condition.Input.value + + # Validate operator against allowlist + if ($operatorValue -notin $AllowedOperators) { + Write-Warning "Blocked invalid operator '$($Condition.Operator.value)' in condition for property '$propertyName'" + return $null + } + + # Validate property name to prevent injection via property paths + if (-not $SafePropertyRegex.IsMatch($propertyName)) { + Write-Warning "Blocked invalid property name '$propertyName' in condition" + return $null + } + + # Build sanitized condition string + if ($inputValue -is [array]) { + # Sanitize each array element + $sanitizedItems = foreach ($item in $inputValue) { + $itemStr = [string]$item + if ($UnsafeValueRegex.IsMatch($itemStr)) { + Write-Warning "Blocked unsafe value in array for property '$propertyName': '$itemStr'" + return $null + } + $itemStr -replace "'", "''" + } + if ($null -eq $sanitizedItems) { return $null } + $arrayAsString = $sanitizedItems | ForEach-Object { "'$_'" } + $value = "@($($arrayAsString -join ', '))" + } else { + $valueStr = [string]$inputValue + if ($UnsafeValueRegex.IsMatch($valueStr)) { + Write-Warning "Blocked unsafe value for property '$propertyName': '$valueStr'" + return $null + } + $value = "'$($valueStr -replace "'", "''")'" + } + + return "`$(`$_.$propertyName) -$operatorValue $value" +} diff --git a/Modules/CIPPCore/Private/Test-CIPPDynamicGroupFilter.ps1 b/Modules/CIPPCore/Private/Test-CIPPDynamicGroupFilter.ps1 new file mode 100644 index 000000000000..f8138c972094 --- /dev/null +++ b/Modules/CIPPCore/Private/Test-CIPPDynamicGroupFilter.ps1 @@ -0,0 +1,210 @@ +function Test-CIPPDynamicGroupFilter { + <# + .SYNOPSIS + Returns a sanitized PowerShell condition string for a dynamic tenant group rule. + .DESCRIPTION + Validates all user-controlled inputs (property, operator, values) against allowlists + and sanitizes values before building the condition string. Returns a safe condition + string suitable for use in [ScriptBlock]::Create(). + + This replaces the old pattern of directly interpolating unsanitized user input into + scriptblock strings, which was vulnerable to code injection. + .PARAMETER Rule + A single rule object with .property, .operator, and .value fields. + .PARAMETER TenantGroupMembersCache + Hashtable of group memberships keyed by group ID. + .OUTPUTS + [string] A sanitized PowerShell condition string, or $null if validation fails. + .FUNCTIONALITY + Internal + #> + [CmdletBinding()] + [OutputType([string])] + param( + [Parameter(Mandatory = $true)] + $Rule, + [Parameter(Mandatory = $false)] + [hashtable]$TenantGroupMembersCache = @{} + ) + + $AllowedOperators = @('eq', 'ne', 'like', 'notlike', 'in', 'notin', 'contains', 'notcontains') + $AllowedProperties = @('delegatedAccessStatus', 'availableLicense', 'availableServicePlan', 'tenantGroupMember', 'customVariable') + + # Regex for sanitizing string values - block characters that enable code injection + $SafeValueRegex = [regex]'^[^;|`\$\{\}\(\)]*$' + # Regex for GUID validation + $GuidRegex = [regex]'^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$' + # Regex for safe identifiers (variable names, plan names, etc.) + $SafeIdentifierRegex = [regex]'^[a-zA-Z0-9_.\-\s\(\)]+$' + + $Property = $Rule.property + $Operator = [string]($Rule.operator) + $OperatorLower = $Operator.ToLower() + $Value = $Rule.value + + # Validate operator + if ($OperatorLower -notin $AllowedOperators) { + Write-Warning "Blocked invalid operator '$Operator' in dynamic group rule for property '$Property'" + return $null + } + + # Validate property + if ($Property -notin $AllowedProperties) { + Write-Warning "Blocked invalid property '$Property' in dynamic group rule" + return $null + } + + # Helper: sanitize a single string value for safe embedding in a quoted string + function Protect-StringValue { + param([string]$InputValue) + # Escape single quotes by doubling them (PowerShell string escaping) + $escaped = $InputValue -replace "'", "''" + # Block any remaining injection characters + if (-not $SafeValueRegex.IsMatch($escaped)) { + Write-Warning "Blocked unsafe value: '$InputValue'" + return $null + } + return $escaped + } + + # Helper: sanitize and format an array of string values for embedding in @('a','b') + function Protect-StringArray { + param([array]$InputValues) + $sanitized = foreach ($v in $InputValues) { + $clean = Protect-StringValue -InputValue ([string]$v) + if ($null -eq $clean) { return $null } + "'$clean'" + } + return "@($($sanitized -join ', '))" + } + + switch ($Property) { + 'delegatedAccessStatus' { + $safeValue = Protect-StringValue -InputValue ([string]$Value.value) + if ($null -eq $safeValue) { return $null } + return "`$_.delegatedPrivilegeStatus -$OperatorLower '$safeValue'" + } + 'availableLicense' { + if ($OperatorLower -in @('in', 'notin')) { + $arrayValues = @(if ($Value -is [array]) { $Value.guid } else { @($Value.guid) }) + # Validate each GUID + foreach ($g in $arrayValues) { + if (![string]::IsNullOrEmpty($g) -and -not $GuidRegex.IsMatch($g)) { + Write-Warning "Blocked invalid GUID in availableLicense rule: '$g'" + return $null + } + } + $arrayAsString = ($arrayValues | Where-Object { ![string]::IsNullOrEmpty($_) }) | ForEach-Object { "'$_'" } + if ($OperatorLower -eq 'in') { + return "(`$_.skuId | Where-Object { `$_ -in @($($arrayAsString -join ', ')) }).Count -gt 0" + } else { + return "(`$_.skuId | Where-Object { `$_ -in @($($arrayAsString -join ', ')) }).Count -eq 0" + } + } else { + $guid = [string]$Value.guid + if (![string]::IsNullOrEmpty($guid) -and -not $GuidRegex.IsMatch($guid)) { + Write-Warning "Blocked invalid GUID in availableLicense rule: '$guid'" + return $null + } + return "`$_.skuId -$OperatorLower '$guid'" + } + } + 'availableServicePlan' { + if ($OperatorLower -in @('in', 'notin')) { + $arrayValues = @(if ($Value -is [array]) { $Value.value } else { @($Value.value) }) + foreach ($v in $arrayValues) { + if (![string]::IsNullOrEmpty($v) -and -not $SafeIdentifierRegex.IsMatch($v)) { + Write-Warning "Blocked invalid service plan name: '$v'" + return $null + } + } + $arrayAsString = ($arrayValues | Where-Object { ![string]::IsNullOrEmpty($_) }) | ForEach-Object { "'$_'" } + if ($OperatorLower -eq 'in') { + return "(`$_.servicePlans | Where-Object { `$_ -in @($($arrayAsString -join ', ')) }).Count -gt 0" + } else { + return "(`$_.servicePlans | Where-Object { `$_ -in @($($arrayAsString -join ', ')) }).Count -eq 0" + } + } else { + $safeValue = Protect-StringValue -InputValue ([string]$Value.value) + if ($null -eq $safeValue) { return $null } + return "`$_.servicePlans -$OperatorLower '$safeValue'" + } + } + 'tenantGroupMember' { + if ($OperatorLower -in @('in', 'notin')) { + $ReferencedGroupIds = @($Value.value) + # Validate group IDs are GUIDs + foreach ($gid in $ReferencedGroupIds) { + if (![string]::IsNullOrEmpty($gid) -and -not $GuidRegex.IsMatch($gid)) { + Write-Warning "Blocked invalid group ID in tenantGroupMember rule: '$gid'" + return $null + } + } + + $AllMembers = [System.Collections.Generic.HashSet[string]]::new() + foreach ($GroupId in $ReferencedGroupIds) { + if ($TenantGroupMembersCache.ContainsKey($GroupId)) { + foreach ($MemberId in $TenantGroupMembersCache[$GroupId]) { + [void]$AllMembers.Add($MemberId) + } + } + } + + $MemberArray = $AllMembers | ForEach-Object { "'$_'" } + $MemberArrayString = $MemberArray -join ', ' + + if ($OperatorLower -eq 'in') { + return "`$_.customerId -in @($MemberArrayString)" + } else { + return "`$_.customerId -notin @($MemberArrayString)" + } + } else { + $ReferencedGroupId = [string]$Value.value + if (![string]::IsNullOrEmpty($ReferencedGroupId) -and -not $GuidRegex.IsMatch($ReferencedGroupId)) { + Write-Warning "Blocked invalid group ID: '$ReferencedGroupId'" + return $null + } + return "`$_.customerId -$OperatorLower `$script:TenantGroupMembersCache['$ReferencedGroupId']" + } + } + 'customVariable' { + $VariableName = if ($Value.variableName -is [string]) { + $Value.variableName + } elseif ($Value.variableName.value) { + $Value.variableName.value + } else { + [string]$Value.variableName + } + # Validate variable name - alphanumeric, underscores, hyphens, dots only + if (-not $SafeIdentifierRegex.IsMatch($VariableName)) { + Write-Warning "Blocked invalid custom variable name: '$VariableName'" + return $null + } + $ExpectedValue = Protect-StringValue -InputValue ([string]$Value.value) + if ($null -eq $ExpectedValue) { return $null } + + switch ($OperatorLower) { + 'eq' { + return "(`$_.customVariables.ContainsKey('$VariableName') -and `$_.customVariables['$VariableName'].Value -eq '$ExpectedValue')" + } + 'ne' { + return "(-not `$_.customVariables.ContainsKey('$VariableName') -or `$_.customVariables['$VariableName'].Value -ne '$ExpectedValue')" + } + 'like' { + return "(`$_.customVariables.ContainsKey('$VariableName') -and `$_.customVariables['$VariableName'].Value -like '*$ExpectedValue*')" + } + 'notlike' { + return "(-not `$_.customVariables.ContainsKey('$VariableName') -or `$_.customVariables['$VariableName'].Value -notlike '*$ExpectedValue*')" + } + default { + Write-Warning "Unsupported operator '$OperatorLower' for customVariable" + return $null + } + } + } + default { + Write-Warning "Unknown property type: $Property" + return $null + } + } +} diff --git a/Modules/CIPPCore/Public/Add-CIPPW32ScriptApplication.ps1 b/Modules/CIPPCore/Public/Add-CIPPW32ScriptApplication.ps1 index 5ef92c5a997a..640cb120265a 100644 --- a/Modules/CIPPCore/Public/Add-CIPPW32ScriptApplication.ps1 +++ b/Modules/CIPPCore/Public/Add-CIPPW32ScriptApplication.ps1 @@ -149,7 +149,8 @@ function Add-CIPPW32ScriptApplication { $UninstallScriptId = $null if ($Properties.installScript) { - $InstallScriptContent = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($Properties.installScript)) + $ReplacedInstallScript = Get-CIPPTextReplacement -Text $Properties.installScript -TenantFilter $TenantFilter + $InstallScriptContent = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($ReplacedInstallScript)) $InstallScriptBody = @{ '@odata.type' = '#microsoft.graph.win32LobAppInstallPowerShellScript' displayName = 'install.ps1' @@ -172,7 +173,8 @@ function Add-CIPPW32ScriptApplication { } if ($Properties.uninstallScript) { - $UninstallScriptContent = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($Properties.uninstallScript)) + $ReplacedUninstallScript = Get-CIPPTextReplacement -Text $Properties.uninstallScript -TenantFilter $TenantFilter + $UninstallScriptContent = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($ReplacedUninstallScript)) $UninstallScriptBody = @{ '@odata.type' = '#microsoft.graph.win32LobAppUninstallPowerShellScript' displayName = 'uninstall.ps1' diff --git a/Modules/CIPPCore/Public/DeltaQueries/Test-DeltaQueryConditions.ps1 b/Modules/CIPPCore/Public/DeltaQueries/Test-DeltaQueryConditions.ps1 index a510320b160a..140509db5853 100644 --- a/Modules/CIPPCore/Public/DeltaQueries/Test-DeltaQueryConditions.ps1 +++ b/Modules/CIPPCore/Public/DeltaQueries/Test-DeltaQueryConditions.ps1 @@ -106,35 +106,33 @@ function Test-DeltaQueryConditions { $conditions = $Trigger.Conditions | ConvertFrom-Json | Where-Object { $_.Input.value -ne '' -and $_.Input.value -ne $null } if ($conditions) { - # Initialize collections for condition strings - $conditionStrings = [System.Collections.Generic.List[string]]::new() + # Build human-readable clause for logging $CIPPClause = [System.Collections.Generic.List[string]]::new() + foreach ($condition in $conditions) { + $CIPPClause.Add("$($condition.Property.label) is $($condition.Operator.label) $($condition.Input.value)") + } + Write-Information "Testing delta query conditions: $($CIPPClause -join ' and ')" + # Build sanitized condition strings instead of direct evaluation + $conditionStrings = [System.Collections.Generic.List[string]]::new() + $validConditions = $true foreach ($condition in $conditions) { - # Handle array vs single values - $value = if ($condition.Input.value -is [array]) { - $arrayAsString = $condition.Input.value | ForEach-Object { - "'$_'" - } - "@($($arrayAsString -join ', '))" - } else { - "'$($condition.Input.value)'" + $sanitized = Test-CIPPConditionFilter -Condition $condition + if ($null -eq $sanitized) { + Write-Warning "Skipping due to invalid condition for property '$($condition.Property.label)'" + $validConditions = $false + break } - - # Build PowerShell condition string - $conditionStrings.Add("`$(`$_.$($condition.Property.label)) -$($condition.Operator.value) $value") - $CIPPClause.Add("$($condition.Property.label) is $($condition.Operator.label) $value") + $conditionStrings.Add($sanitized) } - # Join all conditions with AND - $finalCondition = $conditionStrings -join ' -AND ' - - Write-Information "Testing delta query conditions: $finalCondition" - Write-Information "Human readable: $($CIPPClause -join ' and ')" - - # Apply conditions to filter the data using a script block instead of Invoke-Expression - $scriptBlock = [scriptblock]::Create("param(`$_) $finalCondition") - $MatchedData = $Data | Where-Object $scriptBlock + if ($validConditions -and $conditionStrings.Count -gt 0) { + $WhereString = $conditionStrings -join ' -and ' + $WhereBlock = [ScriptBlock]::Create($WhereString) + $MatchedData = $Data | Where-Object $WhereBlock + } else { + $MatchedData = @() + } } else { Write-Information 'No valid conditions found in trigger configuration.' $MatchedData = $Data diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecTenantGroup.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecTenantGroup.ps1 index 73b1a1bce23b..29be42487b60 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecTenantGroup.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecTenantGroup.ps1 @@ -23,6 +23,26 @@ function Invoke-ExecTenantGroup { $dynamicRules = $Request.Body.dynamicRules $ruleLogic = $Request.Body.ruleLogic ?? 'and' + # Validate dynamic rules to prevent code injection + if ($groupType -eq 'dynamic' -and $dynamicRules) { + $AllowedDynamicOperators = @('eq', 'ne', 'like', 'notlike', 'in', 'notin', 'contains', 'notcontains') + $AllowedDynamicProperties = @('delegatedAccessStatus', 'availableLicense', 'availableServicePlan', 'tenantGroupMember', 'customVariable') + foreach ($rule in $dynamicRules) { + if ($rule.operator -and $rule.operator.ToLower() -notin $AllowedDynamicOperators) { + return ([HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::BadRequest + Body = @{ Results = "Invalid operator in dynamic rule: $($rule.operator)" } + }) + } + if ($rule.property -and $rule.property -notin $AllowedDynamicProperties) { + return ([HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::BadRequest + Body = @{ Results = "Invalid property in dynamic rule: $($rule.property)" } + }) + } + } + } + $AllowedGroups = Test-CippAccess -Request $Request -GroupList if ($AllowedGroups -notcontains 'AllGroups') { return ([HttpResponseContext]@{ diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecResetMFA.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecResetMFA.ps1 index 47d72e537885..2c7cc7b4ad9d 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecResetMFA.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecResetMFA.ps1 @@ -1,4 +1,4 @@ -Function Invoke-ExecResetMFA { +function Invoke-ExecResetMFA { <# .FUNCTIONALITY Entrypoint @@ -7,8 +7,8 @@ Function Invoke-ExecResetMFA { #> [CmdletBinding()] param($Request, $TriggerMetadata) - $Headers = $Request.Headers + $Headers = $Request.Headers # Interact with query parameters or the body of the request. $TenantFilter = $Request.Query.tenantFilter ?? $Request.Body.tenantFilter diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Invoke-ListMDEOnboarding.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Invoke-ListMDEOnboarding.ps1 new file mode 100644 index 000000000000..9c87485beb35 --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Invoke-ListMDEOnboarding.ps1 @@ -0,0 +1,54 @@ +function Invoke-ListMDEOnboarding { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Security.Defender.Read + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + $TenantFilter = $Request.Query.tenantFilter + $UseReportDB = $Request.Query.UseReportDB + + try { + if ($UseReportDB -eq 'true') { + try { + $GraphRequest = Get-CIPPMDEOnboardingReport -TenantFilter $TenantFilter -ErrorAction Stop + $StatusCode = [HttpStatusCode]::OK + } catch { + Write-Host "Error retrieving MDE onboarding status from report database: $($_.Exception.Message)" + $StatusCode = [HttpStatusCode]::InternalServerError + $GraphRequest = $_.Exception.Message + } + + return ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = @($GraphRequest) + }) + } + + $ConnectorId = 'fc780465-2017-40d4-a0c5-307022471b92' + $ConnectorUri = "https://graph.microsoft.com/beta/deviceManagement/mobileThreatDefenseConnectors/$ConnectorId" + try { + $ConnectorState = New-GraphGetRequest -uri $ConnectorUri -tenantid $TenantFilter + $PartnerState = $ConnectorState.partnerState + } catch { + $PartnerState = 'unavailable' + } + + $GraphRequest = [PSCustomObject]@{ + Tenant = $TenantFilter + partnerState = $PartnerState + } + $StatusCode = [HttpStatusCode]::OK + } catch { + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + $StatusCode = [HttpStatusCode]::Forbidden + $GraphRequest = $ErrorMessage + } + + return ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = @($GraphRequest) + }) +} diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-AddAlert.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-AddAlert.ps1 index df6818654545..31b38f41bc1e 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-AddAlert.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-AddAlert.ps1 @@ -7,31 +7,61 @@ function Invoke-AddAlert { #> [CmdletBinding()] param($Request, $TriggerMetadata) - # Interact with query parameters or the body of the request. - $Tenants = $Request.Body.tenantFilter - $Conditions = $Request.Body.conditions | ConvertTo-Json -Compress -Depth 10 | Out-String - $TenantsJson = $Tenants | ConvertTo-Json -Compress -Depth 10 | Out-String - $excludedTenantsJson = $Request.Body.excludedTenants | ConvertTo-Json -Compress -Depth 10 | Out-String - $Actions = $Request.Body.actions | ConvertTo-Json -Compress -Depth 10 | Out-String - $RowKey = $Request.Body.RowKey ? $Request.Body.RowKey : (New-Guid).ToString() - $CompleteObject = @{ - Tenants = [string]$TenantsJson - excludedTenants = [string]$excludedTenantsJson - Conditions = [string]$Conditions - Actions = [string]$Actions - type = $Request.Body.logbook.value - RowKey = $RowKey - PartitionKey = 'Webhookv2' - AlertComment = [string]$Request.Body.AlertComment - } - $WebhookTable = Get-CippTable -TableName 'WebhookRules' - Add-CIPPAzDataTableEntity @WebhookTable -Entity $CompleteObject -Force - $Results = "Added Audit Log Alert for $($Tenants.count) tenants. It may take up to four hours before Microsoft starts delivering these alerts." - Write-LogMessage -API 'AddAlert' -message $Results -sev Info -LogData $CompleteObject -headers $Request.Headers - return ([HttpResponseContext]@{ - StatusCode = [HttpStatusCode]::OK - Body = @{ 'Results' = @($Results) } - }) + try { + + $Conditions = $Request.Body.conditions + Write-Information "Received request to add alert with conditions: $($Conditions | ConvertTo-Json -Compress -Depth 10)" + + # Validate conditions to prevent code injection via operator/property fields + $AllowedOperators = @('eq', 'ne', 'like', 'notlike', 'match', 'notmatch', 'gt', 'lt', 'ge', 'le', 'in', 'notin', 'contains', 'notcontains') + $SafePropertyRegex = [regex]'^[a-zA-Z0-9_.]+$' + foreach ($condition in $Conditions) { + if ($condition.Operator.value -and $condition.Operator.value.ToLower() -notin $AllowedOperators) { + return ([HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::BadRequest + Body = @{ error = "Invalid operator: $($condition.Operator.value)" } + }) + } + if ($condition.Property.label -and -not $SafePropertyRegex.IsMatch($condition.Property.label)) { + return ([HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::BadRequest + Body = @{ error = "Invalid property name: $($condition.Property.label)" } + }) + } + } + $Tenants = $Request.Body.tenantFilter + $Conditions = $Request.Body.conditions | ConvertTo-Json -Compress -Depth 10 | Out-String + $TenantsJson = $Tenants | ConvertTo-Json -Compress -Depth 10 | Out-String + $excludedTenantsJson = $Request.Body.excludedTenants | ConvertTo-Json -Compress -Depth 10 | Out-String + $Actions = $Request.Body.actions | ConvertTo-Json -Compress -Depth 10 | Out-String + $RowKey = $Request.Body.RowKey ? $Request.Body.RowKey : (New-Guid).ToString() + $CompleteObject = @{ + Tenants = [string]$TenantsJson + excludedTenants = [string]$excludedTenantsJson + Conditions = [string]$Conditions + Actions = [string]$Actions + type = $Request.Body.logbook.value + RowKey = $RowKey + PartitionKey = 'Webhookv2' + AlertComment = [string]$Request.Body.AlertComment + CustomSubject = [string]$Request.Body.CustomSubject + } + $WebhookTable = Get-CippTable -TableName 'WebhookRules' + Add-CIPPAzDataTableEntity @WebhookTable -Entity $CompleteObject -Force + $Results = "Added Audit Log Alert for $($Tenants.count) tenants. It may take up to four hours before Microsoft starts delivering these alerts." + Write-LogMessage -API 'AddAlert' -message $Results -sev Info -LogData $CompleteObject -headers $Request.Headers + + return ([HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::OK + Body = @{ 'Results' = @($Results) } + }) + } catch { + Write-LogMessage -API 'AddAlert' -message "Error adding alert: $_" -sev Error -headers $Request.Headers + return ([HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::InternalServerError + Body = @{ error = "Failed to add alert: $_" } + }) + } } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-RemoveStandardTemplate.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-RemoveStandardTemplate.ps1 index 283913c6702c..6125cc632780 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-RemoveStandardTemplate.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-RemoveStandardTemplate.ps1 @@ -16,14 +16,15 @@ function Invoke-RemoveStandardTemplate { $ID = $Request.Body.ID ?? $Request.Query.ID try { $Table = Get-CippTable -tablename 'templates' - $Filter = "PartitionKey eq 'StandardsTemplateV2' and (GUID eq '$ID' or RowKey eq '$ID' or OriginalEntityId eq '$ID')" - $ClearRow = Get-CIPPAzDataTableEntity @Table -Filter $Filter -Property PartitionKey, RowKey, ETag, JSON + $Filter = "PartitionKey eq 'StandardsTemplateV2' and (RowKey eq '$ID' or OriginalEntityId eq '$ID' or OriginalEntityId eq guid'$ID')" + $ClearRow = Get-CIPPAzDataTableEntity @Table -Filter $Filter if ($ClearRow.JSON) { $TemplateName = (ConvertFrom-Json -InputObject $ClearRow.JSON -ErrorAction SilentlyContinue).templateName } else { $TemplateName = '' } - Remove-AzDataTableEntity -Force @Table -Entity $ClearRow + $Entities = Get-AzDataTableEntity @Table -Filter $Filter + Remove-AzDataTableEntity -Force @Table -Entity $Entities $Result = "Removed Standards Template named: '$($TemplateName)' with id: $($ID)" Write-LogMessage -Headers $Headers -API $APIName -message $Result -Sev Info $StatusCode = [HttpStatusCode]::OK diff --git a/Modules/CIPPCore/Public/Entrypoints/Invoke-ListLogs.ps1 b/Modules/CIPPCore/Public/Entrypoints/Invoke-ListLogs.ps1 index 907e06857fa7..0612ec3ef23b 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Invoke-ListLogs.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Invoke-ListLogs.ps1 @@ -33,8 +33,7 @@ function Invoke-ListLogs { $TenantList = Get-Tenants -IncludeErrors | Where-Object { $_.customerId -in $AllowedTenants } } - if ($AllowedTenants -contains 'AllTenants' -or ($AllowedTenants -notcontains 'AllTenants' -and ($TenantList.defaultDomainName -contains $Row.Tenant -or $Row.Tenant -eq 'CIPP' -or $TenantList.customerId -contains $Row.TenantId)) ) { - + if ($AllowedTenants -contains 'AllTenants' -or ($AllowedTenants -notcontains 'AllTenants' -and ($TenantList.defaultDomainName -contains $Row.Tenant -or $Row.Tenant -eq 'CIPP' -or $TenantList.customerId -contains $Row.TenantId -or $TenantList.initialDomainName -contains $Row.Tenant)) ) { if ($Row.StandardTemplateId) { $Standard = ($Templates | Where-Object { $_.RowKey -eq $Row.StandardTemplateId }).JSON | ConvertFrom-Json diff --git a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-AuditLogSearchCreation.ps1 b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-AuditLogSearchCreation.ps1 index a8c64da33fec..7f28dac0cbf0 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-AuditLogSearchCreation.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-AuditLogSearchCreation.ps1 @@ -27,18 +27,21 @@ function Start-AuditLogSearchCreation { $StartTime = ($Now.AddSeconds(-$Now.Seconds)).AddHours(-1) $EndTime = $Now.AddSeconds(-$Now.Seconds) - Write-Information 'Audit Logs: Creating new searches' + # Pre-expand tenant groups once per config entry to avoid repeated calls per tenant + foreach ($ConfigEntry in $ConfigEntries) { + $ConfigEntry | Add-Member -MemberType NoteProperty -Name 'ExpandedTenants' -Value (Expand-CIPPTenantGroups -TenantFilter ($ConfigEntry.Tenants)).value -Force + } + + Write-Information "Audit Logs: Building batch for $($TenantList.Count) tenants across $($ConfigEntries.Count) config entries" $Batch = foreach ($Tenant in $TenantList) { - Write-Information "Processing tenant $($Tenant.defaultDomainName) - $($Tenant.customerId)" $TenantInConfig = $false $MatchingConfigs = [System.Collections.Generic.List[object]]::new() foreach ($ConfigEntry in $ConfigEntries) { if ($ConfigEntry.excludedTenants.value -contains $Tenant.defaultDomainName) { continue } - $TenantsList = Expand-CIPPTenantGroups -TenantFilter ($ConfigEntry.Tenants) - if ($TenantsList.value -contains $Tenant.defaultDomainName -or $TenantsList.value -contains 'AllTenants') { + if ($ConfigEntry.ExpandedTenants -contains $Tenant.defaultDomainName -or $ConfigEntry.ExpandedTenants -contains 'AllTenants') { $TenantInConfig = $true $MatchingConfigs.Add($ConfigEntry) } diff --git a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-CIPPOrchestrator.ps1 b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-CIPPOrchestrator.ps1 index ef944134eef3..57f64ac7cde1 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-CIPPOrchestrator.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-CIPPOrchestrator.ps1 @@ -70,7 +70,7 @@ function Start-CIPPOrchestrator { # Clean up the stored input object after starting the orchestration try { - $Entities = Get-AzDataTableEntity @OrchestratorTable -Filter "PartitionKey eq 'Input' and (RowKey eq '$InputObjectGuid' or OriginalEntityId eq '$InputObjectGuid')" -Property PartitionKey, RowKey + $Entities = Get-AzDataTableEntity @OrchestratorTable -Filter "PartitionKey eq 'Input' and (RowKey eq '$InputObjectGuid' or OriginalEntityId eq '$InputObjectGuid' or OriginalEntityId eq guid'$InputObjectGuid')" -Property PartitionKey, RowKey Remove-AzDataTableEntity @OrchestratorTable -Entity $Entities -Force Write-Information "Cleaned up stored input object: $InputObjectGuid" } catch { diff --git a/Modules/CIPPCore/Public/Functions/Get-CIPPTenantAlignment.ps1 b/Modules/CIPPCore/Public/Functions/Get-CIPPTenantAlignment.ps1 index 5ffcaf908254..aedf3b274f98 100644 --- a/Modules/CIPPCore/Public/Functions/Get-CIPPTenantAlignment.ps1 +++ b/Modules/CIPPCore/Public/Functions/Get-CIPPTenantAlignment.ps1 @@ -113,11 +113,36 @@ function Get-CIPPTenantAlignment { $TemplateAssignedTenants = @() $AppliestoAllTenants = $false + # Build excluded tenants list (mirrors Get-CIPPStandards logic, including group expansion) + $ExcludedTenantValues = [System.Collections.Generic.List[string]]::new() + if ($Template.excludedTenants) { + $ExcludeList = if ($Template.excludedTenants -is [System.Collections.IEnumerable] -and -not ($Template.excludedTenants -is [string])) { + $Template.excludedTenants + } else { + @($Template.excludedTenants) + } + foreach ($excludeItem in $ExcludeList) { + $ExcludeValue = $excludeItem.value + if ($excludeItem.type -eq 'Group') { + $GroupMembers = $TenantGroups | Where-Object { $_.Id -eq $ExcludeValue } + if ($GroupMembers -and $GroupMembers.Members) { + foreach ($member in $GroupMembers.Members.defaultDomainName) { + $ExcludedTenantValues.Add($member) + } + } + } else { + if ($ExcludeValue) { $ExcludedTenantValues.Add($ExcludeValue) } + } + } + } + $ExcludedTenantsSet = [System.Collections.Generic.HashSet[string]]::new() + foreach ($item in $ExcludedTenantValues) { [void]$ExcludedTenantsSet.Add($item) } + if ($Template.tenantFilter -and $Template.tenantFilter.Count -gt 0) { # Extract tenant values from the tenantFilter array $TenantValues = [System.Collections.Generic.List[string]]::new() foreach ($filterItem in $Template.tenantFilter) { - if ($filterItem.type -eq 'group') { + if ($filterItem.type -eq 'Group') { # Look up group members by Id (GUID in the value field) $GroupMembers = $TenantGroups | Where-Object { $_.Id -eq $filterItem.value } if ($GroupMembers -and $GroupMembers.Members) { @@ -224,6 +249,10 @@ function Get-CIPPTenantAlignment { } else { $null } foreach ($TenantName in $TenantStandards.Keys) { + # Skip explicitly excluded tenants regardless of AllTenants or specific assignment + if ($ExcludedTenantsSet.Contains($TenantName)) { + continue + } # Check tenant scope with HashSet and cache tenant data if (-not $AppliestoAllTenants) { if ($TemplateAssignedTenantsSet -and -not $TemplateAssignedTenantsSet.Contains($TenantName)) { diff --git a/Modules/CIPPCore/Public/Get-CIPPMDEOnboardingReport.ps1 b/Modules/CIPPCore/Public/Get-CIPPMDEOnboardingReport.ps1 new file mode 100644 index 000000000000..adfc44a8f99b --- /dev/null +++ b/Modules/CIPPCore/Public/Get-CIPPMDEOnboardingReport.ps1 @@ -0,0 +1,56 @@ +function Get-CIPPMDEOnboardingReport { + <# + .SYNOPSIS + Generates an MDE onboarding status report from the CIPP Reporting database + .PARAMETER TenantFilter + The tenant to generate the report for + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter + ) + + try { + if ($TenantFilter -eq 'AllTenants') { + $AllItems = Get-CIPPDbItem -TenantFilter 'allTenants' -Type 'MDEOnboarding' + $Tenants = @($AllItems | Where-Object { $_.RowKey -ne 'MDEOnboarding-Count' } | Select-Object -ExpandProperty PartitionKey -Unique) + + $TenantList = Get-Tenants -IncludeErrors + $Tenants = $Tenants | Where-Object { $TenantList.defaultDomainName -contains $_ } + + $AllResults = [System.Collections.Generic.List[PSCustomObject]]::new() + foreach ($Tenant in $Tenants) { + try { + $TenantResults = Get-CIPPMDEOnboardingReport -TenantFilter $Tenant + foreach ($Result in $TenantResults) { + $Result | Add-Member -NotePropertyName 'Tenant' -NotePropertyValue $Tenant -Force + $AllResults.Add($Result) + } + } catch { + Write-LogMessage -API 'MDEOnboardingReport' -tenant $Tenant -message "Failed to get report for tenant: $($_.Exception.Message)" -sev Warning + } + } + return $AllResults + } + + $Items = Get-CIPPDbItem -TenantFilter $TenantFilter -Type 'MDEOnboarding' | Where-Object { $_.RowKey -ne 'MDEOnboarding-Count' } + if (-not $Items) { + throw 'No MDE onboarding data found in reporting database. Sync the report data first.' + } + + $CacheTimestamp = ($Items | Where-Object { $_.Timestamp } | Sort-Object Timestamp -Descending | Select-Object -First 1).Timestamp + + $AllResults = [System.Collections.Generic.List[PSCustomObject]]::new() + foreach ($Item in $Items) { + $ParsedData = $Item.Data | ConvertFrom-Json + $ParsedData | Add-Member -NotePropertyName 'CacheTimestamp' -NotePropertyValue $CacheTimestamp -Force + $AllResults.Add($ParsedData) + } + + return $AllResults + } catch { + Write-LogMessage -API 'MDEOnboardingReport' -tenant $TenantFilter -message "Failed to generate MDE onboarding report: $($_.Exception.Message)" -sev Error + throw + } +} diff --git a/Modules/CIPPCore/Public/Get-CIPPTextReplacement.ps1 b/Modules/CIPPCore/Public/Get-CIPPTextReplacement.ps1 index a86ce4751645..7798ab8a4047 100644 --- a/Modules/CIPPCore/Public/Get-CIPPTextReplacement.ps1 +++ b/Modules/CIPPCore/Public/Get-CIPPTextReplacement.ps1 @@ -12,7 +12,7 @@ function Get-CIPPTextReplacement { Get-CIPPTextReplacement -TenantFilter 'contoso.com' -Text 'Hello %tenantname%' #> param ( - [string]$TenantFilter, + [string]$TenantFilter = $env:TenantID, $Text, [switch]$EscapeForJson ) diff --git a/Modules/CIPPCore/Public/GraphHelper/New-ExoRequest.ps1 b/Modules/CIPPCore/Public/GraphHelper/New-ExoRequest.ps1 index 93c373c1fa98..19e6aad21817 100644 --- a/Modules/CIPPCore/Public/GraphHelper/New-ExoRequest.ps1 +++ b/Modules/CIPPCore/Public/GraphHelper/New-ExoRequest.ps1 @@ -56,7 +56,7 @@ function New-ExoRequest { } $ExoBody = Get-CIPPTextReplacement -TenantFilter $tenantid -Text $ExoBody -EscapeForJson - $Tenant = Get-Tenants -IncludeErrors | Where-Object { $_.defaultDomainName -eq $tenantid -or $_.customerId -eq $tenantid } + $Tenant = Get-Tenants -IncludeErrors | Where-Object { $_.defaultDomainName -eq $tenantid -or $_.customerId -eq $tenantid -or $_.initialDomainName -eq $tenantid } | Select-Object -First 1 if (-not $Tenant -and $NoAuthCheck -eq $true) { $Tenant = [PSCustomObject]@{ customerId = $tenantid diff --git a/Modules/CIPPCore/Public/Invoke-CIPPDBCacheCollection.ps1 b/Modules/CIPPCore/Public/Invoke-CIPPDBCacheCollection.ps1 index 0cfeb492af04..194ed10330fd 100644 --- a/Modules/CIPPCore/Public/Invoke-CIPPDBCacheCollection.ps1 +++ b/Modules/CIPPCore/Public/Invoke-CIPPDBCacheCollection.ps1 @@ -117,6 +117,7 @@ function Invoke-CIPPDBCacheCollection { 'ManagedDeviceEncryptionStates' 'IntuneAppProtectionPolicies' 'DetectedApps' + 'MDEOnboarding' ) } diff --git a/Modules/CIPPCore/Public/New-CIPPAlertTemplate.ps1 b/Modules/CIPPCore/Public/New-CIPPAlertTemplate.ps1 index b9fba00543b2..923eb9f0b060 100644 --- a/Modules/CIPPCore/Public/New-CIPPAlertTemplate.ps1 +++ b/Modules/CIPPCore/Public/New-CIPPAlertTemplate.ps1 @@ -10,7 +10,8 @@ function New-CIPPAlertTemplate { $CIPPURL, $Tenant, $AuditLogLink, - $AlertComment + $AlertComment, + $CustomSubject ) $Appname = '[{"Application Name":"ACOM Azure Website","Application IDs":"23523755-3a2b-41ca-9315-f81f3f566a95"},{"Application Name":"AEM-DualAuth","Application IDs":"69893ee3-dd10-4b1c-832d-4870354be3d8"},{"Application Name":"ASM Campaign Servicing","Application IDs":"0cb7b9ec-5336-483b-bc31-b15b5788de71"},{"Application Name":"Azure Advanced Threat Protection","Application IDs":"7b7531ad-5926-4f2d-8a1d-38495ad33e17"},{"Application Name":"Azure Data Lake","Application IDs":"e9f49c6b-5ce5-44c8-925d-015017e9f7ad"},{"Application Name":"Azure Lab Services Portal","Application IDs":"835b2a73-6e10-4aa5-a979-21dfda45231c"},{"Application Name":"Azure Portal","Application IDs":"c44b4083-3bb0-49c1-b47d-974e53cbdf3c"},{"Application Name":"AzureSupportCenter","Application IDs":"37182072-3c9c-4f6a-a4b3-b3f91cacffce"},{"Application Name":"Bing","Application IDs":"9ea1ad79-fdb6-4f9a-8bc3-2b70f96e34c7"},{"Application Name":"CPIM Service","Application IDs":"bb2a2e3a-c5e7-4f0a-88e0-8e01fd3fc1f4"},{"Application Name":"CRM Power BI Integration","Application IDs":"e64aa8bc-8eb4-40e2-898b-cf261a25954f"},{"Application Name":"Dataverse","Application IDs":"00000007-0000-0000-c000-000000000000"},{"Application Name":"Enterprise Roaming and Backup","Application IDs":"60c8bde5-3167-4f92-8fdb-059f6176dc0f"},{"Application Name":"IAM Supportability","Application IDs":"a57aca87-cbc0-4f3c-8b9e-dc095fdc8978"},{"Application Name":"IrisSelectionFrontDoor","Application IDs":"16aeb910-ce68-41d1-9ac3-9e1673ac9575"},{"Application Name":"MCAPI Authorization Prod","Application IDs":"d73f4b35-55c9-48c7-8b10-651f6f2acb2e"},{"Application Name":"Media Analysis and Transformation Service","Application IDs":"944f0bd1-117b-4b1c-af26-804ed95e767e
0cd196ee-71bf-4fd6-a57c-b491ffd4fb1e"},{"Application Name":"Microsoft 365 Support Service","Application IDs":"ee272b19-4411-433f-8f28-5c13cb6fd407"},{"Application Name":"Microsoft App Access Panel","Application IDs":"0000000c-0000-0000-c000-000000000000"},{"Application Name":"Microsoft Approval Management","Application IDs":"65d91a3d-ab74-42e6-8a2f-0add61688c74
38049638-cc2c-4cde-abe4-4479d721ed44"},{"Application Name":"Microsoft Authentication Broker","Application IDs":"29d9ed98-a469-4536-ade2-f981bc1d605e"},{"Application Name":"Microsoft Azure CLI","Application IDs":"04b07795-8ddb-461a-bbee-02f9e1bf7b46"},{"Application Name":"Microsoft Azure PowerShell","Application IDs":"1950a258-227b-4e31-a9cf-717495945fc2"},{"Application Name":"Microsoft Bing Search","Application IDs":"cf36b471-5b44-428c-9ce7-313bf84528de"},{"Application Name":"Microsoft Bing Search for Microsoft Edge","Application IDs":"2d7f3606-b07d-41d1-b9d2-0d0c9296a6e8"},{"Application Name":"Microsoft Bing Default Search Engine","Application IDs":"1786c5ed-9644-47b2-8aa0-7201292175b6"},{"Application Name":"Microsoft Defender for Cloud Apps","Application IDs":"3090ab82-f1c1-4cdf-af2c-5d7a6f3e2cc7"},{"Application Name":"Microsoft Docs","Application IDs":"18fbca16-2224-45f6-85b0-f7bf2b39b3f3"},{"Application Name":"Microsoft Dynamics ERP","Application IDs":"00000015-0000-0000-c000-000000000000"},{"Application Name":"Microsoft Edge Insider Addons Prod","Application IDs":"6253bca8-faf2-4587-8f2f-b056d80998a7"},{"Application Name":"Microsoft Exchange Online Protection","Application IDs":"00000007-0000-0ff1-ce00-000000000000"},{"Application Name":"Microsoft Forms","Application IDs":"c9a559d2-7aab-4f13-a6ed-e7e9c52aec87"},{"Application Name":"Microsoft Graph","Application IDs":"00000003-0000-0000-c000-000000000000"},{"Application Name":"Microsoft Intune Web Company Portal","Application IDs":"74bcdadc-2fdc-4bb3-8459-76d06952a0e9"},{"Application Name":"Microsoft Intune Windows Agent","Application IDs":"fc0f3af4-6835-4174-b806-f7db311fd2f3"},{"Application Name":"Microsoft Learn","Application IDs":"18fbca16-2224-45f6-85b0-f7bf2b39b3f3"},{"Application Name":"Microsoft Office","Application IDs":"d3590ed6-52b3-4102-aeff-aad2292ab01c"},{"Application Name":"Microsoft Office 365 Portal","Application IDs":"00000006-0000-0ff1-ce00-000000000000"},{"Application Name":"Microsoft Office Web Apps Service","Application IDs":"67e3df25-268a-4324-a550-0de1c7f97287"},{"Application Name":"Microsoft Online Syndication Partner Portal","Application IDs":"d176f6e7-38e5-40c9-8a78-3998aab820e7"},{"Application Name":"Microsoft password reset service","Application IDs":"93625bc8-bfe2-437a-97e0-3d0060024faa"},{"Application Name":"Microsoft Power BI","Application IDs":"871c010f-5e61-4fb1-83ac-98610a7e9110"},{"Application Name":"Microsoft Storefronts","Application IDs":"28b567f6-162c-4f54-99a0-6887f387bbcc"},{"Application Name":"Microsoft Stream Portal","Application IDs":"cf53fce8-def6-4aeb-8d30-b158e7b1cf83"},{"Application Name":"Microsoft Substrate Management","Application IDs":"98db8bd6-0cc0-4e67-9de5-f187f1cd1b41"},{"Application Name":"Microsoft Support","Application IDs":"fdf9885b-dd37-42bf-82e5-c3129ef5a302"},{"Application Name":"Microsoft Teams","Application IDs":"1fec8e78-bce4-4aaf-ab1b-5451cc387264"},{"Application Name":"Microsoft Teams Services","Application IDs":"cc15fd57-2c6c-4117-a88c-83b1d56b4bbe"},{"Application Name":"Microsoft Teams Web Client","Application IDs":"5e3ce6c0-2b1f-4285-8d4b-75ee78787346"},{"Application Name":"Microsoft Whiteboard Services","Application IDs":"95de633a-083e-42f5-b444-a4295d8e9314"},{"Application Name":"O365 Suite UX","Application IDs":"4345a7b9-9a63-4910-a426-35363201d503"},{"Application Name":"Office 365 Exchange Online","Application IDs":"00000002-0000-0ff1-ce00-000000000000"},{"Application Name":"Office 365 Management","Application IDs":"00b41c95-dab0-4487-9791-b9d2c32c80f2"},{"Application Name":"Office 365 Search Service","Application IDs":"66a88757-258c-4c72-893c-3e8bed4d6899"},{"Application Name":"Office 365 SharePoint Online","Application IDs":"00000003-0000-0ff1-ce00-000000000000"},{"Application Name":"Office Delve","Application IDs":"94c63fef-13a3-47bc-8074-75af8c65887a"},{"Application Name":"Office Online Add-in SSO","Application IDs":"93d53678-613d-4013-afc1-62e9e444a0a5"},{"Application Name":"Office Online Client AAD- Augmentation Loop","Application IDs":"2abdc806-e091-4495-9b10-b04d93c3f040"},{"Application Name":"Office Online Client AAD- Loki","Application IDs":"b23dd4db-9142-4734-867f-3577f640ad0c"},{"Application Name":"Office Online Client AAD- Maker","Application IDs":"17d5e35f-655b-4fb0-8ae6-86356e9a49f5"},{"Application Name":"Office Online Client MSA- Loki","Application IDs":"b6e69c34-5f1f-4c34-8cdf-7fea120b8670"},{"Application Name":"Office Online Core SSO","Application IDs":"243c63a3-247d-41c5-9d83-7788c43f1c43"},{"Application Name":"Office Online Search","Application IDs":"a9b49b65-0a12-430b-9540-c80b3332c127"},{"Application Name":"Office.com","Application IDs":"4b233688-031c-404b-9a80-a4f3f2351f90"},{"Application Name":"Office365 Shell WCSS-Client","Application IDs":"89bee1f7-5e6e-4d8a-9f3d-ecd601259da7"},{"Application Name":"OfficeClientService","Application IDs":"0f698dd4-f011-4d23-a33e-b36416dcb1e6"},{"Application Name":"OfficeHome","Application IDs":"4765445b-32c6-49b0-83e6-1d93765276ca"},{"Application Name":"OfficeShredderWacClient","Application IDs":"4d5c2d63-cf83-4365-853c-925fd1a64357"},{"Application Name":"OMSOctopiPROD","Application IDs":"62256cef-54c0-4cb4-bcac-4c67989bdc40"},{"Application Name":"OneDrive SyncEngine","Application IDs":"ab9b8c07-8f02-4f72-87fa-80105867a763"},{"Application Name":"OneNote","Application IDs":"2d4d3d8e-2be3-4bef-9f87-7875a61c29de"},{"Application Name":"Outlook Mobile","Application IDs":"27922004-5251-4030-b22d-91ecd9a37ea4"},{"Application Name":"Partner Customer Delegated Admin Offline Processor","Application IDs":"a3475900-ccec-4a69-98f5-a65cd5dc5306"},{"Application Name":"Password Breach Authenticator","Application IDs":"bdd48c81-3a58-4ea9-849c-ebea7f6b6360"},{"Application Name":"Power BI Service","Application IDs":"00000009-0000-0000-c000-000000000000"},{"Application Name":"SharedWithMe","Application IDs":"ffcb16e8-f789-467c-8ce9-f826a080d987"},{"Application Name":"SharePoint Online Web Client Extensibility","Application IDs":"08e18876-6177-487e-b8b5-cf950c1e598c"},{"Application Name":"Signup","Application IDs":"b4bddae8-ab25-483e-8670-df09b9f1d0ea"},{"Application Name":"Skype for Business Online","Application IDs":"00000004-0000-0ff1-ce00-000000000000"},{"Application Name":"Sway","Application IDs":"905fcf26-4eb7-48a0-9ff0-8dcc7194b5ba"},{"Application Name":"Universal Store Native Client","Application IDs":"268761a2-03f3-40df-8a8b-c3db24145b6b"},{"Application Name":"Vortex [wsfed enabled]","Application IDs":"5572c4c0-d078-44ce-b81c-6cbf8d3ed39e"},{"Application Name":"Windows Azure Active Directory","Application IDs":"00000002-0000-0000-c000-000000000000"},{"Application Name":"Windows Azure Service Management API","Application IDs":"797f4846-ba00-4fd7-ba43-dac1f8f63013"},{"Application Name":"WindowsDefenderATP Portal","Application IDs":"a3b79187-70b2-4139-83f9-6016c58cd27b"},{"Application Name":"Windows Search","Application IDs":"26a7ee05-5602-4d76-a7ba-eae8b7b67941"},{"Application Name":"Windows Spotlight","Application IDs":"1b3c667f-cde3-4090-b60b-3d2abd0117f0"},{"Application Name":"Windows Store for Business","Application IDs":"45a330b1-b1ec-4cc1-9161-9f03992aa49f"},{"Application Name":"Yammer","Application IDs":"00000005-0000-0ff1-ce00-000000000000"},{"Application Name":"Yammer Web","Application IDs":"c1c74fed-04c9-4704-80dc-9f79a2e515cb"},{"Application Name":"Yammer Web Embed","Application IDs":"e1ef36fd-b883-4dbf-97f0-9ece4b576fc6"}]' | ConvertFrom-Json | Where-Object -Property 'Application IDs' -EQ $data.applicationId # Get the function app root directory by navigating from the module location @@ -103,9 +104,9 @@ function New-CIPPAlertTemplate { } } if ($FoundForwarding -eq $true) { - $Title = "$($TenantFilter) - New forwarding or redirect Rule Detected for $($data.UserId)" + $Title = "$($Tenant) - New forwarding or redirect Rule Detected for $($data.UserId)" } else { - $Title = "$($TenantFilter) - New Rule Detected for $($data.UserId)" + $Title = "$($Tenant) - New Rule Detected for $($data.UserId)" } $RuleTable = ($Data.CIPPParameters | ConvertFrom-Json | ConvertTo-Html -Fragment | Out-String).Replace('', '
') @@ -120,7 +121,7 @@ function New-CIPPAlertTemplate { $AfterButtonText = '

If you believe this is a suspect rule, you can click the button above to start the investigation.

' } 'Set-InboxRule' { - $Title = "$($TenantFilter) - Rule Edit Detected for $($data.UserId)" + $Title = "$($Tenant) - Rule Edit Detected for $($data.UserId)" $RuleTable = ($Data.CIPPParameters | ConvertFrom-Json | ConvertTo-Html -Fragment | Out-String).Replace('
', '
') $IntroText = "

A rule has been edited for the user $($data.UserId). You should check if this rule is not malicious. The rule information can be found in the table below.

$RuleTable" if ($ActionResults) { $IntroText = $IntroText + "

Based on the rule, the following actions have been taken: $($ActionResults -join '
' )

" } @@ -133,7 +134,7 @@ function New-CIPPAlertTemplate { $AfterButtonText = '

If you believe this is a suspect rule, you can click the button above to start the investigation.

' } 'Add member to role.' { - $Title = "$($TenantFilter) - Role change detected for $($data.ObjectId)" + $Title = "$($Tenant) - Role change detected for $($data.ObjectId)" $Table = ($data.CIPPModifiedProperties | ConvertFrom-Json | ConvertTo-Html -Fragment | Out-String).Replace('
', '
') $IntroText = "

$($data.UserId) has added $($data.ObjectId) to the $(($data.'Role.DisplayName')) role. The information about the role can be found in the table below.

$Table" if ($ActionResults) { $IntroText = $IntroText + "

Based on the rule, the following actions have been taken: $($ActionResults -join '
' )

" } @@ -147,7 +148,7 @@ function New-CIPPAlertTemplate { } 'Disable account.' { - $Title = "$($TenantFilter) - $($data.ObjectId) has been disabled" + $Title = "$($Tenant) - $($data.ObjectId) has been disabled" $IntroText = "$($data.ObjectId) has been disabled by $($data.UserId)." if ($ActionResults) { $IntroText = $IntroText + "

Based on the rule, the following actions have been taken: $($ActionResults -join '
' )

" } if ($LocationInfo) { @@ -159,7 +160,7 @@ function New-CIPPAlertTemplate { $AfterButtonText = '

If this is incorrect, use the user management screen to unblock the users sign-in

' } 'Enable account.' { - $Title = "$($TenantFilter) - $($data.ObjectId) has been enabled" + $Title = "$($Tenant) - $($data.ObjectId) has been enabled" $IntroText = "$($data.ObjectId) has been enabled by $($data.UserId)." $ButtonUrl = "$CIPPURL/identity/administration/users?customerId=$($data.OrganizationId)" if ($ActionResults) { $IntroText = $IntroText + "

Based on the rule, the following actions have been taken: $($ActionResults -join '
' )

" } @@ -171,7 +172,7 @@ function New-CIPPAlertTemplate { $AfterButtonText = '

If this is incorrect, use the user management screen to unblock the users sign-in

' } 'Update StsRefreshTokenValidFrom Timestamp.' { - $Title = "$($TenantFilter) - $($data.ObjectId) has had all sessions revoked" + $Title = "$($Tenant) - $($data.ObjectId) has had all sessions revoked" $IntroText = "$($data.ObjectId) has had their sessions revoked by $($data.UserId)." $ButtonUrl = "$CIPPURL/identity/administration/users?customerId=$($data.OrganizationId)" if ($ActionResults) { $IntroText = $IntroText + "

Based on the rule, the following actions have been taken: $($ActionResults -join '
' )

" } @@ -183,7 +184,7 @@ function New-CIPPAlertTemplate { $AfterButtonText = '

If this is incorrect, use the user management screen to unblock the users sign-in

' } 'Disable Strong Authentication.' { - $Title = "$($TenantFilter) - $($data.ObjectId) has been MFA disabled" + $Title = "$($Tenant) - $($data.ObjectId) has been MFA disabled" $IntroText = "$($data.ObjectId) MFA has been disabled by $($data.UserId)." if ($ActionResults) { $IntroText = $IntroText + "

Based on the rule, the following actions have been taken: $($ActionResults -join '
' )

" } if ($LocationInfo) { @@ -195,7 +196,7 @@ function New-CIPPAlertTemplate { $AfterButtonText = '

If this is incorrect, use the user management screen to reenable MFA

' } 'Remove Member from a role.' { - $Title = "$($TenantFilter) - Role change detected for $($data.ObjectId)" + $Title = "$($Tenant) - Role change detected for $($data.ObjectId)" $Table = ($data.CIPPModifiedProperties | ConvertFrom-Json | ConvertTo-Html -Fragment | Out-String).Replace('
', '
') $IntroText = "

$($data.UserId) has removed $($data.ObjectId) to the $(($data.ModifiedProperties | Where-Object -Property Name -EQ 'Role.DisplayName').NewValue) role. The information about the role can be found in the table below.

$Table" if ($ActionResults) { $IntroText = $IntroText + "

Based on the rule, the following actions have been taken: $($ActionResults -join '
' )

" } @@ -210,7 +211,7 @@ function New-CIPPAlertTemplate { } 'Reset user password.' { - $Title = "$($TenantFilter) - $($data.ObjectId) has had their password reset" + $Title = "$($Tenant) - $($data.ObjectId) has had their password reset" $IntroText = "$($data.ObjectId) has had their password reset by $($data.userId)." if ($ActionResults) { $IntroText = $IntroText + "

Based on the rule, the following actions have been taken: $($ActionResults -join '
' )

" } if ($LocationInfo) { @@ -224,7 +225,7 @@ function New-CIPPAlertTemplate { } 'Add service principal.' { if ($Appname) { $AppName = $AppName.'Application Name' } else { $appName = $data.ApplicationId } - $Title = "$($TenantFilter) - Service Principal $($data.ObjectId) has been added." + $Title = "$($Tenant) - Service Principal $($data.ObjectId) has been added." $Table = ($data.ModifiedProperties | ConvertTo-Html -Fragment | Out-String).Replace('
', '
') if ($ActionResults) { $IntroText = $IntroText + "

Based on the rule, the following actions have been taken: $($ActionResults -join '
' )

" } if ($LocationInfo) { @@ -237,7 +238,7 @@ function New-CIPPAlertTemplate { } 'Remove service principal.' { if ($Appname) { $AppName = $AppName.'Application Name' } else { $appName = $data.ApplicationId } - $Title = "$($TenantFilter) - Service Principal $($data.ObjectId) has been removed." + $Title = "$($Tenant) - Service Principal $($data.ObjectId) has been removed." $Table = ($data.CIPPModifiedProperties | ConvertFrom-Json | ConvertTo-Html -Fragment | Out-String).Replace('
', '
') if ($ActionResults) { $IntroText = $IntroText + "

Based on the rule, the following actions have been taken: $($ActionResults -join '
' )

" } if ($LocationInfo) { @@ -251,7 +252,7 @@ function New-CIPPAlertTemplate { 'UserLoggedIn' { $Table = ($data | ConvertTo-Html -Fragment -As List | Out-String).Replace('
', '
') if ($Appname) { $AppName = $AppName.'Application Name' } else { $appName = $data.ApplicationId } - $Title = "$($TenantFilter) - a user has logged on from a location you've set up to receive alerts for." + $Title = "$($Tenant) - a user has logged on from a location you've set up to receive alerts for." $IntroText = "$($data.UserId) ($($data.Userkey)) has logged on from IP $($data.ClientIP) to the application $($Appname). According to our database this is located in $($LocationInfo.Country) - $($LocationInfo.City).

You have set up alerts to be notified when this happens. See the table below for more info.$Table" if ($ActionResults) { $IntroText = $IntroText + "

Based on the rule, the following actions have been taken: $($ActionResults -join '
' )

" } if ($LocationInfo) { @@ -277,6 +278,10 @@ function New-CIPPAlertTemplate { } } + if (![string]::IsNullOrWhiteSpace($CustomSubject)) { + $Title = '{0} - {1}' -f $Tenant, $CustomSubject + } + if ($Format -eq 'html') { return [pscustomobject]@{ title = $Title diff --git a/Modules/CIPPCore/Public/Remove-CIPPUserMFA.ps1 b/Modules/CIPPCore/Public/Remove-CIPPUserMFA.ps1 index 722d0b73a616..bb0c2b64f93f 100644 --- a/Modules/CIPPCore/Public/Remove-CIPPUserMFA.ps1 +++ b/Modules/CIPPCore/Public/Remove-CIPPUserMFA.ps1 @@ -4,7 +4,7 @@ function Remove-CIPPUserMFA { Remove MFA methods for a user .DESCRIPTION - Remove MFA methods for a user using bulk requests to the Microsoft Graph API + Remove MFA methods for a user using individual requests to the Microsoft Graph API .PARAMETER UserPrincipalName UserPrincipalName of the user to remove MFA methods for @@ -17,7 +17,7 @@ function Remove-CIPPUserMFA { #> [CmdletBinding(SupportsShouldProcess = $true)] - Param( + param( [Parameter(Mandatory = $true)] [string]$UserPrincipalName, [Parameter(Mandatory = $true)] @@ -38,42 +38,49 @@ function Remove-CIPPUserMFA { throw $Message } - $Requests = [System.Collections.Generic.List[object]]::new() - foreach ($Method in $AuthMethods) { - if ($Method.'@odata.type' -and $Method.'@odata.type' -ne '#microsoft.graph.passwordAuthenticationMethod') { - $MethodType = ($Method.'@odata.type' -split '\.')[-1] -replace 'Authentication', '' - $Requests.Add(@{ - id = "$MethodType-$($Method.id)" - method = 'DELETE' - url = ('users/{0}/authentication/{1}s/{2}' -f $UserPrincipalName, $MethodType, $Method.id) - }) - } - } + $RemovableMethods = $AuthMethods | Where-Object { $_.'@odata.type' -and $_.'@odata.type' -ne '#microsoft.graph.passwordAuthenticationMethod' } - if (($Requests | Measure-Object).Count -eq 0) { + if (($RemovableMethods | Measure-Object).Count -eq 0) { $Results = "No MFA methods found for user $UserPrincipalName" Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $Results -sev 'Info' return $Results - } else { - if ($PSCmdlet.ShouldProcess("Remove MFA methods for $UserPrincipalName")) { - try { - $Results = New-GraphBulkRequest -Requests $Requests -tenantid $TenantFilter -asapp $true -ErrorAction Stop - if ($Results.status -eq 204) { - $Message = "Successfully removed MFA methods for user $UserPrincipalName. User must supply MFA at next logon" - Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $Message -sev 'Info' - return $Message - } else { - $FailedAuthMethods = (($Results | Where-Object { $_.status -ne 204 }).id -split '-')[0] -join ', ' - $Message = "Failed to remove MFA methods for $FailedAuthMethods on user $UserPrincipalName" - Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $Message -sev 'Error' - throw $Message + } + + if ($PSCmdlet.ShouldProcess("Remove MFA methods for $UserPrincipalName")) { + $Failed = [System.Collections.Generic.List[string]]::new() + $Succeeded = [System.Collections.Generic.List[string]]::new() + foreach ($Method in $RemovableMethods) { + $MethodType = ($Method.'@odata.type' -split '\.')[-1] -replace 'Authentication', '' + switch ($MethodType) { + 'qrCodePinMethod' { + $Uri = 'https://graph.microsoft.com/beta/users/{0}/authentication/{1}' -f $UserPrincipalName, $MethodType + break } + default { + $Uri = 'https://graph.microsoft.com/v1.0/users/{0}/authentication/{1}s/{2}' -f $UserPrincipalName, $MethodType, $Method.id + } + } + try { + $null = New-GraphPOSTRequest -uri $Uri -tenantid $TenantFilter -type DELETE -AsApp $true + $Succeeded.Add($MethodType) } catch { $ErrorMessage = Get-CippException -Exception $_ - $Message = "Failed to remove MFA methods for user $UserPrincipalName. Error: $($ErrorMessage.NormalizedError)" - Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $Message -sev 'Error' -LogData $ErrorMessage - throw $Message + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Failed to remove $MethodType for $UserPrincipalName. Error: $($ErrorMessage.NormalizedError)" -sev 'Error' -LogData $ErrorMessage + $Failed.Add($MethodType) } } + + if ($Failed.Count -gt 0) { + $Message = if ($Succeeded.Count -gt 0) { + "Successfully removed MFA methods ($($Succeeded -join ', ')) for user $UserPrincipalName. However, failed to remove ($($Failed -join ', ')). User may still have MFA methods assigned." + } else { + "Failed to remove MFA methods ($($Failed -join ', ')) for user $UserPrincipalName" + } + throw $Message + } + + $Message = "Successfully removed MFA methods ($($Succeeded -join ', ')) for user $UserPrincipalName. User must supply MFA at next logon" + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $Message -sev 'Info' + return $Message } } diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheMDEOnboarding.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheMDEOnboarding.ps1 new file mode 100644 index 000000000000..867563c0871e --- /dev/null +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheMDEOnboarding.ps1 @@ -0,0 +1,45 @@ +function Set-CIPPDBCacheMDEOnboarding { + <# + .SYNOPSIS + Caches MDE onboarding status for a tenant + .PARAMETER TenantFilter + The tenant to cache MDE onboarding status for + .PARAMETER QueueId + The queue ID to update with total tasks (optional) + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [String]$TenantFilter, + [String]$QueueId + ) + + try { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching MDE onboarding status' -sev Debug + + $ConnectorId = 'fc780465-2017-40d4-a0c5-307022471b92' + $ConnectorUri = "https://graph.microsoft.com/beta/deviceManagement/mobileThreatDefenseConnectors/$ConnectorId" + try { + $ConnectorState = New-GraphGetRequest -uri $ConnectorUri -tenantid $TenantFilter + $PartnerState = $ConnectorState.partnerState + } catch { + $PartnerState = 'unavailable' + } + + $Result = @( + [PSCustomObject]@{ + Tenant = $TenantFilter + partnerState = $PartnerState + RowKey = 'MDEOnboarding' + PartitionKey = $TenantFilter + } + ) + + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'MDEOnboarding' -Data @($Result) + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'MDEOnboarding' -Data @($Result) -Count + + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached MDE onboarding status successfully' -sev Debug + } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache MDE onboarding status: $($_.Exception.Message)" -sev Error -LogData (Get-CippException -Exception $_) + } +} diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployCheckChromeExtension.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployCheckChromeExtension.ps1 index 921fe6d5db0f..3e2c62816a4e 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployCheckChromeExtension.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployCheckChromeExtension.ps1 @@ -169,11 +169,71 @@ function Invoke-CIPPStandardDeployCheckChromeExtension { ) } | ConvertTo-Json -Depth 20 + # Compares OMA-URI settings between expected and existing policy. + # The 'value' field of omaSettingString is itself a JSON string, so we parse it + # before calling Compare-CIPPIntuneObject to avoid false positives from whitespace + # or key-ordering differences introduced by Intune's API response serialization. + function Compare-OMAURISettings { + param( + [Parameter(Mandatory = $true)]$ExpectedSettings, + [Parameter(Mandatory = $true)]$ExistingConfig + ) + $diffs = [System.Collections.Generic.List[PSCustomObject]]::new() + if ($null -eq $ExistingConfig -or $null -eq $ExistingConfig.omaSettings) { return $diffs } + + foreach ($expectedSetting in $ExpectedSettings) { + $existingSetting = $ExistingConfig.omaSettings | Where-Object { $_.omaUri -eq $expectedSetting.omaUri } + if (-not $existingSetting) { + $diffs.Add([PSCustomObject]@{ Property = $expectedSetting.omaUri; ExpectedValue = 'present'; ReceivedValue = 'missing' }) + continue + } + try { + $expectedValue = $expectedSetting.value | ConvertFrom-Json -Depth 20 + $existingValue = $existingSetting.value | ConvertFrom-Json -Depth 20 + $valueDiffs = Compare-CIPPIntuneObject -ReferenceObject $expectedValue -DifferenceObject $existingValue -CompareType 'Device' + foreach ($diff in $valueDiffs) { + $diffs.Add([PSCustomObject]@{ Property = "$($expectedSetting.omaUri).$($diff.Property)"; ExpectedValue = $diff.ExpectedValue; ReceivedValue = $diff.ReceivedValue }) + } + } catch { + # Fall back to string comparison if either value is not valid JSON + if ($expectedSetting.value -ne $existingSetting.value) { + $diffs.Add([PSCustomObject]@{ Property = $expectedSetting.omaUri; ExpectedValue = '[expected value]'; ReceivedValue = '[current value differs]' }) + } + } + } + return $diffs + } + try { - # Check if the policies already exist - $ExistingPolicies = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/deviceManagement/deviceConfigurations' -tenantid $Tenant - $ChromePolicyExists = $ExistingPolicies.value | Where-Object { $_.displayName -eq $ChromePolicyName } - $EdgePolicyExists = $ExistingPolicies.value | Where-Object { $_.displayName -eq $EdgePolicyName } + # Fetch existing policies with full configuration details for OMA-URI drift detection + $ExistingChromePolicy = Get-CIPPIntunePolicy -TemplateType 'Device' -DisplayName $ChromePolicyName -tenantFilter $Tenant + $ExistingEdgePolicy = Get-CIPPIntunePolicy -TemplateType 'Device' -DisplayName $EdgePolicyName -tenantFilter $Tenant + + $ChromePolicyExists = $null -ne $ExistingChromePolicy + $EdgePolicyExists = $null -ne $ExistingEdgePolicy + + # Build expected OMA-URI settings from the generated policy JSON for comparison + $ExpectedChromeSettings = ($ChromePolicyJSON | ConvertFrom-Json).omaSettings + $ExpectedEdgeSettings = ($EdgePolicyJSON | ConvertFrom-Json).omaSettings + + # Detect configuration drift in existing policies + $ChromeDifferences = [System.Collections.Generic.List[PSCustomObject]]::new() + $EdgeDifferences = [System.Collections.Generic.List[PSCustomObject]]::new() + + if ($ExistingChromePolicy) { + # omaSettingString values are encrypted by Intune; decrypt before comparing + $DecryptedChromePolicy = Get-CIPPOmaSettingDecryptedValue -DeviceConfiguration ($ExistingChromePolicy.cippconfiguration | ConvertFrom-Json) -DeviceConfigurationId $ExistingChromePolicy.id -TenantFilter $Tenant + $ChromeDifferences = Compare-OMAURISettings -ExpectedSettings $ExpectedChromeSettings -ExistingConfig $DecryptedChromePolicy + } + + if ($ExistingEdgePolicy) { + # omaSettingString values are encrypted by Intune; decrypt before comparing + $DecryptedEdgePolicy = Get-CIPPOmaSettingDecryptedValue -DeviceConfiguration ($ExistingEdgePolicy.cippconfiguration | ConvertFrom-Json) -DeviceConfigurationId $ExistingEdgePolicy.id -TenantFilter $Tenant + $EdgeDifferences = Compare-OMAURISettings -ExpectedSettings $ExpectedEdgeSettings -ExistingConfig $DecryptedEdgePolicy + } + + $ChromePolicyCompliant = $ChromePolicyExists -and ($ChromeDifferences.Count -eq 0) + $EdgePolicyCompliant = $EdgePolicyExists -and ($EdgeDifferences.Count -eq 0) if ($Settings.remediate -eq $true) { # Handle assignment configuration @@ -185,46 +245,63 @@ function Invoke-CIPPStandardDeployCheckChromeExtension { $AssignTo = $Settings.customGroup } - # Deploy Chrome policy + # Deploy or remediate Chrome policy (create if missing, update if drifted) if (-not $ChromePolicyExists) { $Result = Set-CIPPIntunePolicy -TemplateType 'Device' -Description 'Deploys and configures the Check Chrome extension for Google Chrome browsers' -DisplayName $ChromePolicyName -RawJSON $ChromePolicyJSON -AssignTo $AssignTo -ExcludeGroup $ExcludeGroup -tenantFilter $Tenant Write-LogMessage -API 'Standards' -tenant $Tenant -message "Successfully created Check Chrome Extension policy for Chrome: $ChromePolicyName" -sev Info + } elseif ($ChromeDifferences.Count -gt 0) { + $Result = Set-CIPPIntunePolicy -TemplateType 'Device' -Description 'Deploys and configures the Check Chrome extension for Google Chrome browsers' -DisplayName $ChromePolicyName -RawJSON $ChromePolicyJSON -AssignTo $AssignTo -ExcludeGroup $ExcludeGroup -tenantFilter $Tenant + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Successfully corrected $($ChromeDifferences.Count) drifted OMA-URI setting(s) in Check Chrome Extension policy for Chrome" -sev Info } else { - Write-LogMessage -API 'Standards' -tenant $Tenant -message 'Check Chrome Extension policy for Chrome already exists, skipping creation' -sev Info + Write-LogMessage -API 'Standards' -tenant $Tenant -message 'Check Chrome Extension policy for Chrome is compliant, no changes needed' -sev Info } - # Deploy Edge policy + # Deploy or remediate Edge policy (create if missing, update if drifted) if (-not $EdgePolicyExists) { $Result = Set-CIPPIntunePolicy -TemplateType 'Device' -Description 'Deploys and configures the Check Chrome extension for Microsoft Edge browsers' -DisplayName $EdgePolicyName -RawJSON $EdgePolicyJSON -AssignTo $AssignTo -ExcludeGroup $ExcludeGroup -tenantFilter $Tenant Write-LogMessage -API 'Standards' -tenant $Tenant -message "Successfully created Check Chrome Extension policy for Edge: $EdgePolicyName" -sev Info + } elseif ($EdgeDifferences.Count -gt 0) { + $Result = Set-CIPPIntunePolicy -TemplateType 'Device' -Description 'Deploys and configures the Check Chrome extension for Microsoft Edge browsers' -DisplayName $EdgePolicyName -RawJSON $EdgePolicyJSON -AssignTo $AssignTo -ExcludeGroup $ExcludeGroup -tenantFilter $Tenant + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Successfully corrected $($EdgeDifferences.Count) drifted OMA-URI setting(s) in Check Chrome Extension policy for Edge" -sev Info } else { - Write-LogMessage -API 'Standards' -tenant $Tenant -message 'Check Chrome Extension policy for Edge already exists, skipping creation' -sev Info + Write-LogMessage -API 'Standards' -tenant $Tenant -message 'Check Chrome Extension policy for Edge is compliant, no changes needed' -sev Info } } if ($Settings.alert -eq $true) { - $BothPoliciesExist = $ChromePolicyExists -and $EdgePolicyExists - if ($BothPoliciesExist) { - Write-LogMessage -API 'Standards' -tenant $Tenant -message 'Check Chrome Extension policies are deployed for both Chrome and Edge' -sev Info + if ($ChromePolicyCompliant -and $EdgePolicyCompliant) { + Write-LogMessage -API 'Standards' -tenant $Tenant -message 'Check Chrome Extension policies are deployed and correctly configured for both Chrome and Edge' -sev Info } else { - $MissingPolicies = @() - if (-not $ChromePolicyExists) { $MissingPolicies += 'Chrome' } - if (-not $EdgePolicyExists) { $MissingPolicies += 'Edge' } - Write-StandardsAlert -message "Check Chrome Extension policies are missing for: $($MissingPolicies -join ', ')" -object @{ 'Missing Policies' = $MissingPolicies -join ',' } -tenant $Tenant -standardName 'DeployCheckChromeExtension' -standardId $Settings.standardId - Write-LogMessage -API 'Standards' -tenant $Tenant -message "Check Chrome Extension policies are missing for: $($MissingPolicies -join ', ')" -sev Alert + $Issues = [System.Collections.Generic.List[string]]::new() + if (-not $ChromePolicyExists) { + $Issues.Add('Chrome policy is missing') + } elseif ($ChromeDifferences.Count -gt 0) { + $Issues.Add("Chrome policy OMA-URI settings differ ($($ChromeDifferences.Count) difference(s))") + } + if (-not $EdgePolicyExists) { + $Issues.Add('Edge policy is missing') + } elseif ($EdgeDifferences.Count -gt 0) { + $Issues.Add("Edge policy OMA-URI settings differ ($($EdgeDifferences.Count) difference(s))") + } + Write-StandardsAlert -message "Check Chrome Extension issues: $($Issues -join '; ')" -object @{ Issues = ($Issues -join '; '); ChromeDifferences = $ChromeDifferences; EdgeDifferences = $EdgeDifferences } -tenant $Tenant -standardName 'DeployCheckChromeExtension' -standardId $Settings.standardId + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Check Chrome Extension issues: $($Issues -join '; ')" -sev Alert } } if ($Settings.report -eq $true) { - $StateIsCorrect = $ChromePolicyExists -and $EdgePolicyExists + $StateIsCorrect = $ChromePolicyCompliant -and $EdgePolicyCompliant $ExpectedValue = [PSCustomObject]@{ - ChromePolicyDeployed = $true - EdgePolicyDeployed = $true + ChromePolicyDeployed = $true + ChromePolicyCompliant = $true + EdgePolicyDeployed = $true + EdgePolicyCompliant = $true } $CurrentValue = [PSCustomObject]@{ - ChromePolicyDeployed = $ChromePolicyExists - EdgePolicyDeployed = $EdgePolicyExists + ChromePolicyDeployed = [bool]$ChromePolicyExists + ChromePolicyCompliant = [bool]$ChromePolicyCompliant + EdgePolicyDeployed = [bool]$EdgePolicyExists + EdgePolicyCompliant = [bool]$EdgePolicyCompliant } Set-CIPPStandardsCompareField -FieldName 'standards.DeployCheckChromeExtension' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant Add-CIPPBPAField -FieldName 'DeployCheckChromeExtension' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $Tenant diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardLegacyEmailReportAddins.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardLegacyEmailReportAddins.ps1 index 0876db936f1a..5d36441ec634 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardLegacyEmailReportAddins.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardLegacyEmailReportAddins.ps1 @@ -46,7 +46,7 @@ function Invoke-CIPPStandardLegacyEmailReportAddins { ) try { - $CurrentApps = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/applications&select=addins" -TenantID $Tenant + $CurrentApps = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/applications?`$select=id,addIns" -TenantID $Tenant # Filter to only applications that have the legacy add-ins we're looking for $LegacyProductIds = $LegacyAddins | ForEach-Object { $_.ProductId } @@ -108,7 +108,7 @@ function Invoke-CIPPStandardLegacyEmailReportAddins { # If we performed remediation and need to report/alert, get fresh state if ($RemediationPerformed -and ($Settings.alert -eq $true -or $Settings.report -eq $true)) { try { - $FreshApps = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/applications&select=addins" -TenantID $Tenant + $FreshApps = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/applications?`$select=addIns" -TenantID $Tenant $LegacyProductIds = $LegacyAddins | ForEach-Object { $_.ProductId } $FreshInstalledApps = $FreshApps | Where-Object { $app = $_ diff --git a/Modules/CIPPCore/Public/TenantGroups/Update-CIPPDynamicTenantGroups.ps1 b/Modules/CIPPCore/Public/TenantGroups/Update-CIPPDynamicTenantGroups.ps1 index 185018453aa2..2d2d6ea3e29d 100644 --- a/Modules/CIPPCore/Public/TenantGroups/Update-CIPPDynamicTenantGroups.ps1 +++ b/Modules/CIPPCore/Public/TenantGroups/Update-CIPPDynamicTenantGroups.ps1 @@ -62,115 +62,10 @@ function Update-CIPPDynamicTenantGroups { try { Write-LogMessage -API 'TenantGroups' -message "Processing dynamic group: $($Group.Name)" -sev Info $Rules = @($Group.DynamicRules | ConvertFrom-Json) - # Build a single Where-Object string for AND logic - $WhereConditions = foreach ($Rule in $Rules) { - $Property = $Rule.property - $Operator = $Rule.operator - $Value = $Rule.value - - switch ($Property) { - 'delegatedAccessStatus' { - "`$_.delegatedPrivilegeStatus -$Operator '$($Value.value)'" - } - 'availableLicense' { - if ($Operator -in @('in', 'notin')) { - $arrayValues = if ($Value -is [array]) { $Value.guid } else { @($Value.guid) } - $arrayAsString = $arrayValues | ForEach-Object { "'$_'" } - if ($Operator -eq 'in') { - "(`$_.skuId | Where-Object { `$_ -in @($($arrayAsString -join ', ')) }).Count -gt 0" - } else { - "(`$_.skuId | Where-Object { `$_ -in @($($arrayAsString -join ', ')) }).Count -eq 0" - } - } else { - "`$_.skuId -$Operator '$($Value.guid)'" - } - } - 'availableServicePlan' { - if ($Operator -in @('in', 'notin')) { - $arrayValues = if ($Value -is [array]) { $Value.value } else { @($Value.value) } - $arrayAsString = $arrayValues | ForEach-Object { "'$_'" } - if ($Operator -eq 'in') { - # Keep tenants with ANY of the provided plans - "(`$_.servicePlans | Where-Object { `$_ -in @($($arrayAsString -join ', ')) }).Count -gt 0" - } else { - # Exclude tenants with ANY of the provided plans - "(`$_.servicePlans | Where-Object { `$_ -in @($($arrayAsString -join ', ')) }).Count -eq 0" - } - } else { - "`$_.servicePlans -$Operator '$($Value.value)'" - } - } - 'tenantGroupMember' { - # Get members of the referenced tenant group(s) - if ($Operator -in @('in', 'notin')) { - # Handle array of group IDs - $ReferencedGroupIds = @($Value.value) - - # Collect all unique member customerIds from all referenced groups - $AllMembers = [System.Collections.Generic.HashSet[string]]::new() - foreach ($GroupId in $ReferencedGroupIds) { - if ($script:TenantGroupMembersCache.ContainsKey($GroupId)) { - foreach ($MemberId in $script:TenantGroupMembersCache[$GroupId]) { - [void]$AllMembers.Add($MemberId) - } - } - } - - # Convert to array string for condition - $MemberArray = $AllMembers | ForEach-Object { "'$_'" } - $MemberArrayString = $MemberArray -join ', ' - - if ($Operator -eq 'in') { - "`$_.customerId -in @($MemberArrayString)" - } else { - "`$_.customerId -notin @($MemberArrayString)" - } - } else { - # Single value with other operators - $ReferencedGroupId = $Value.value - "`$_.customerId -$Operator `$script:TenantGroupMembersCache['$ReferencedGroupId']" - } - } - 'customVariable' { - # Custom variable matching - value contains variable name and expected value - # Handle case where variableName might be an object (autocomplete option) or a string - $VariableName = if ($Value.variableName -is [string]) { - $Value.variableName - } elseif ($Value.variableName.value) { - $Value.variableName.value - } else { - $Value.variableName - } - $ExpectedValue = $Value.value - # Escape single quotes in expected value for the condition string - $EscapedExpectedValue = $ExpectedValue -replace "'", "''" - - switch ($Operator) { - 'eq' { - "(`$_.customVariables.ContainsKey('$VariableName') -and `$_.customVariables['$VariableName'].Value -eq '$EscapedExpectedValue')" - } - 'ne' { - "(-not `$_.customVariables.ContainsKey('$VariableName') -or `$_.customVariables['$VariableName'].Value -ne '$EscapedExpectedValue')" - } - 'like' { - "(`$_.customVariables.ContainsKey('$VariableName') -and `$_.customVariables['$VariableName'].Value -like '*$EscapedExpectedValue*')" - } - 'notlike' { - "(-not `$_.customVariables.ContainsKey('$VariableName') -or `$_.customVariables['$VariableName'].Value -notlike '*$EscapedExpectedValue*')" - } - } - } - default { - Write-LogMessage -API 'TenantGroups' -message "Unknown property type: $Property" -sev Warning - $null - } - } - + if (!$Rules -or $Rules.Count -eq 0) { + throw 'No rules found for dynamic group.' } - if (!$WhereConditions) { - throw 'Generating the conditions failed. The conditions seem to be empty.' - } - Write-Information "Generated where conditions: $($WhereConditions | ConvertTo-Json )" + Write-Information "Processing $($Rules.Count) rules for group '$($Group.Name)'" $TenantObj = $AllTenants | ForEach-Object { if ($Rules.property -contains 'availableLicense') { if ($SkuHashtable.ContainsKey($_.customerId)) { @@ -221,10 +116,26 @@ function Update-CIPPDynamicTenantGroups { customVariables = $TenantVariables } } - # Combine all conditions with the specified logic (AND or OR) - $LogicOperator = if ($Group.RuleLogic -eq 'or') { ' -or ' } else { ' -and ' } + # Evaluate rules safely using Test-CIPPDynamicGroupFilter with AND/OR logic + $RuleLogic = if ($Group.RuleLogic -eq 'or') { 'or' } else { 'and' } + + # Build sanitized condition strings from validated rules + $WhereConditions = foreach ($rule in $Rules) { + $condition = Test-CIPPDynamicGroupFilter -Rule $rule -TenantGroupMembersCache $script:TenantGroupMembersCache + if ($null -eq $condition) { + Write-Warning "Skipping invalid rule: $($rule | ConvertTo-Json -Compress)" + continue + } + $condition + } + + if (!$WhereConditions) { + throw 'Generating the conditions failed. All rules were invalid or empty.' + } + + $LogicOperator = if ($RuleLogic -eq 'or') { ' -or ' } else { ' -and ' } $WhereString = $WhereConditions -join $LogicOperator - Write-Information "Evaluating tenants with condition: $WhereString" + Write-Information "Evaluating tenants with sanitized condition: $WhereString" Write-LogMessage -API 'TenantGroups' -message "Evaluating tenants for group '$($Group.Name)' with condition: $WhereString" -sev Info $ScriptBlock = [ScriptBlock]::Create($WhereString) diff --git a/Modules/CIPPCore/Public/Webhooks/Invoke-CIPPWebhookProcessing.ps1 b/Modules/CIPPCore/Public/Webhooks/Invoke-CIPPWebhookProcessing.ps1 index 90257fe4a22d..4ece756421d6 100644 --- a/Modules/CIPPCore/Public/Webhooks/Invoke-CIPPWebhookProcessing.ps1 +++ b/Modules/CIPPCore/Public/Webhooks/Invoke-CIPPWebhookProcessing.ps1 @@ -77,10 +77,19 @@ function Invoke-CippWebhookProcessing { } } + $CustomSubject = $null + if ($Data.CIPPRuleId) { + $WebhookRulesTable = Get-CIPPTable -TableName 'WebhookRules' + $WebhookRule = Get-CIPPAzDataTableEntity @WebhookRulesTable -Filter "PartitionKey eq 'WebhookRule' and RowKey eq '$($Data.CIPPRuleId)'" + if (![string]::IsNullOrEmpty($WebhookRule.CustomSubject)) { + $CustomSubject = $WebhookRule.CustomSubject + } + } + # Save audit log entry to table $LocationInfo = $Data.CIPPLocationInfo | ConvertFrom-Json -ErrorAction SilentlyContinue $AuditRecord = $Data.AuditRecord | ConvertFrom-Json -ErrorAction SilentlyContinue - $GenerateJSON = New-CIPPAlertTemplate -format 'json' -data $Data -ActionResults $ActionResults -CIPPURL $CIPPURL -AlertComment $AlertComment + $GenerateJSON = New-CIPPAlertTemplate -format 'json' -data $Data -ActionResults $ActionResults -CIPPURL $CIPPURL -AlertComment $WebhookRule.AlertComment -CustomSubject $CustomSubject -Tenant $Tenant.defaultDomainName $JsonContent = @{ Title = $GenerateJSON.Title ActionUrl = $GenerateJSON.ButtonUrl diff --git a/Modules/CIPPCore/Public/Webhooks/Test-CIPPAuditLogRules.ps1 b/Modules/CIPPCore/Public/Webhooks/Test-CIPPAuditLogRules.ps1 index 94b7c876fef7..4d35b2a94548 100644 --- a/Modules/CIPPCore/Public/Webhooks/Test-CIPPAuditLogRules.ps1 +++ b/Modules/CIPPCore/Public/Webhooks/Test-CIPPAuditLogRules.ps1 @@ -520,57 +520,82 @@ function Test-CIPPAuditLogRules { if ($TenantFilter -in $Config.Excluded.value) { continue } - $conditions = $Config.Conditions | ConvertFrom-Json | Where-Object { $Config.Input.value -ne '' } + $conditions = $Config.Conditions | ConvertFrom-Json | Where-Object { $_.Input.value -ne '' } $actions = $Config.Actions - $conditionStrings = [System.Collections.Generic.List[string]]::new() $CIPPClause = [System.Collections.Generic.List[string]]::new() - $AddedLocationCondition = $false + + # Build excluded user keys for location-based conditions + $LocationExcludedUserKeys = @() + $HasGeoCondition = $false foreach ($condition in $conditions) { - if ($condition.Property.label -eq 'CIPPGeoLocation' -and !$AddedLocationCondition) { - $conditionStrings.Add("`$_.HasLocationData -eq `$true") - $CIPPClause.Add('HasLocationData is true') - $ExcludedUsers = $ExcludedUsers | Where-Object { $_.Type -eq 'Location' } - # Build single -notin condition against all excluded user keys - $ExcludedUserKeys = @($ExcludedUsers.RowKey) - if ($ExcludedUserKeys.Count -gt 0) { - $conditionStrings.Add("`$(`$_.CIPPUserKey) -notin @('$($ExcludedUserKeys -join "', '")')") - $CIPPClause.Add("CIPPUserKey not in [$($ExcludedUserKeys -join ', ')]") - } - $AddedLocationCondition = $true + if ($condition.Property.label -eq 'CIPPGeoLocation') { + $HasGeoCondition = $true + $LocationExcludedUsers = $ExcludedUsers | Where-Object { $_.Type -eq 'Location' } + $LocationExcludedUserKeys = @($LocationExcludedUsers.RowKey) } - $value = if ($condition.Input.value -is [array]) { - $arrayAsString = $condition.Input.value | ForEach-Object { - "'$_'" - } - "@($($arrayAsString -join ', '))" - } else { "'$($condition.Input.value)'" } - - $conditionStrings.Add("`$(`$_.$($condition.Property.label)) -$($condition.Operator.value) $value") - $CIPPClause.Add("$($condition.Property.label) is $($condition.Operator.label) $value") + $CIPPClause.Add("$($condition.Property.label) is $($condition.Operator.label) $($condition.Input.value)") } - $finalCondition = $conditionStrings -join ' -AND ' [PSCustomObject]@{ - clause = $finalCondition - expectedAction = $actions - CIPPClause = $CIPPClause - AlertComment = $Config.AlertComment + conditions = $conditions + expectedAction = $actions + CIPPClause = $CIPPClause + AlertComment = $Config.AlertComment + HasGeoCondition = $HasGeoCondition + ExcludedUserKeys = $LocationExcludedUserKeys } } } catch { Write-Warning "Error creating where clause: $($_.Exception.Message)" Write-Information $_.InvocationInfo.PositionMessage - #Write-LogMessage -API 'Webhooks' -message 'Error creating where clause' -LogData (Get-CippException -Exception $_) -sev Error -tenant $TenantFilter throw $_ } $MatchedRules = [System.Collections.Generic.List[string]]::new() + $UnsafeValueRegex = [regex]'[;|`\$\{\}]' $DataToProcess = foreach ($clause in $Where) { try { $ClauseStartTime = Get-Date - Write-Warning "Webhook: Processing clause: $($clause.clause)" + Write-Warning "Webhook: Processing conditions: $($clause.CIPPClause -join ' and ')" Write-Information "Webhook: Available operations in data: $(($ProcessedData.Operation | Select-Object -Unique) -join ', ')" - $ReturnedData = $ProcessedData | Where-Object { Invoke-Expression $clause.clause } + + # Build sanitized condition strings instead of direct evaluation + $conditionStrings = [System.Collections.Generic.List[string]]::new() + $validClause = $true + foreach ($condition in $clause.conditions) { + # Add geo-location prerequisites before the condition itself + if ($condition.Property.label -eq 'CIPPGeoLocation') { + $conditionStrings.Add('$_.HasLocationData -eq $true') + if ($clause.ExcludedUserKeys.Count -gt 0) { + $sanitizedKeys = foreach ($key in $clause.ExcludedUserKeys) { + $keyStr = [string]$key + if ($UnsafeValueRegex.IsMatch($keyStr)) { + Write-Warning "Blocked unsafe excluded user key: '$keyStr'" + $validClause = $false + break + } + "'{0}'" -f ($keyStr -replace "'", "''") + } + if (-not $validClause) { break } + $conditionStrings.Add("`$_.CIPPUserKey -notin @($($sanitizedKeys -join ', '))") + } + } + $sanitized = Test-CIPPConditionFilter -Condition $condition + if ($null -eq $sanitized) { + Write-Warning "Skipping rule due to invalid condition for property '$($condition.Property.label)'" + $validClause = $false + break + } + $conditionStrings.Add($sanitized) + } + + if (-not $validClause -or $conditionStrings.Count -eq 0) { + continue + } + + $WhereString = $conditionStrings -join ' -and ' + $WhereBlock = [ScriptBlock]::Create($WhereString) + $ReturnedData = $ProcessedData | Where-Object $WhereBlock if ($ReturnedData) { Write-Warning "Webhook: There is matching data: $(($ReturnedData.operation | Select-Object -Unique) -join ', ')" $ReturnedData = foreach ($item in $ReturnedData) { @@ -583,10 +608,10 @@ function Test-CIPPAuditLogRules { } $ClauseEndTime = Get-Date $ClauseSeconds = ($ClauseEndTime - $ClauseStartTime).TotalSeconds - Write-Warning "Task took $ClauseSeconds seconds for clause: $($clause.clause)" + Write-Warning "Task took $ClauseSeconds seconds for conditions: $($clause.CIPPClause -join ' and ')" $ReturnedData } catch { - Write-Warning "Error processing clause: $($clause.clause): $($_.Exception.Message)" + Write-Warning "Error processing conditions: $($clause.CIPPClause -join ' and '): $($_.Exception.Message)" } } $Results.MatchedRules = @($MatchedRules | Select-Object -Unique)