diff --git a/Modules/CIPPCore/Private/ConvertTo-StringList.ps1 b/Modules/CIPPCore/Private/ConvertTo-StringList.ps1 new file mode 100644 index 000000000000..2e680b4d604c --- /dev/null +++ b/Modules/CIPPCore/Private/ConvertTo-StringList.ps1 @@ -0,0 +1,109 @@ +function ConvertTo-StringList { + <# + .SYNOPSIS + Turns encoded list data into something you can foreach over. + + .DESCRIPTION + String input: if it is JSON (object or array), it is converted first; otherwise comma/semicolon/newline + splitting applies. Other shapes: wrapper objects, or an existing array/list. After conversion, the return + value is always foreach-able (empty collection is @()). Arrays and IList instances are returned + as-is — they are already foreach-able without conversion. + This exists because front end multi-value input is annoying to deal with. + + .PARAMETER InputObject + Encoded list (string/JSON), wrapper object, or an existing array/list. + + .PARAMETER PropertyNames + On hashtables/PSCustomObjects, property names to read in order. + + .OUTPUTS + Always an enumerable suitable for: foreach ($item in (ConvertTo-StringList ...)) { } + #> + [CmdletBinding()] + param( + [Parameter(Position = 0)] + [Alias('Input', 'Value')] + [AllowNull()] + $InputObject, + + [string[]]$PropertyNames = @('Items', 'Value') + ) + + # Output must be foreach-able; $null input yields empty collection. + if ($null -eq $InputObject) { + return @() + } + + if ($InputObject -is [string]) { + $s = $InputObject.Trim() + if (-not $s) { + return @() + } + if ($s.StartsWith('[') -or $s.StartsWith('{')) { + try { + $parsed = $s | ConvertFrom-Json -ErrorAction Stop + return ConvertTo-StringList -InputObject $parsed -PropertyNames $PropertyNames + } catch { + } + } + return @( + $s -split '[,;\r\n]+' | ForEach-Object { $_.Trim() } | Where-Object { $_ } + ) + } + + if ($InputObject -is [Array]) { + return $InputObject + } + if ($InputObject -is [System.Collections.IList] -and $InputObject -isnot [string]) { + return $InputObject + } + + if ($InputObject -is [hashtable]) { + if ($InputObject.Count -eq 0) { + return @() + } + foreach ($name in $PropertyNames) { + if ($InputObject.ContainsKey($name)) { + return ConvertTo-StringList -InputObject $InputObject[$name] -PropertyNames $PropertyNames + } + } + foreach ($p in $InputObject.GetEnumerator() | Sort-Object { $_.Key }) { + $v = $p.Value + if ($null -eq $v) { continue } + if ($v -is [string] -or ($v -is [System.Collections.IEnumerable] -and $v -isnot [hashtable] -and $v -isnot [pscustomobject])) { + return ConvertTo-StringList -InputObject $v -PropertyNames @() + } + } + $single = "$InputObject".Trim() + if ($single) { + return , @($single) + } + return @() + } + + if ($InputObject -is [pscustomobject]) { + foreach ($name in $PropertyNames) { + if ($InputObject.PSObject.Properties.Name -contains $name) { + return ConvertTo-StringList -InputObject $InputObject.$name -PropertyNames $PropertyNames + } + } + foreach ($p in $InputObject.PSObject.Properties) { + $v = $p.Value + if ($null -eq $v) { continue } + if ($v -is [string] -or ($v -is [System.Collections.IEnumerable] -and $v -isnot [hashtable] -and $v -isnot [pscustomobject])) { + return ConvertTo-StringList -InputObject $v -PropertyNames @() + } + } + $single = "$InputObject".Trim() + if ($single) { + return , @($single) + } + return @() + } + + $t = "$InputObject".Trim() + if ($t) { + return , @($t) + } + return @() +} 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..dc9298da4356 100644 --- a/Modules/CIPPCore/Public/Add-CIPPW32ScriptApplication.ps1 +++ b/Modules/CIPPCore/Public/Add-CIPPW32ScriptApplication.ps1 @@ -17,7 +17,9 @@ function Add-CIPPW32ScriptApplication { - publisher: Publisher name - installScript (required): PowerShell install script content (plaintext) - uninstallScript: PowerShell uninstall script content (plaintext) - - detectionPath (required): Full path to the file or folder to detect (e.g., 'C:\\Program Files\\MyApp') + - detectionScript: PowerShell detection script content (plaintext). Takes priority over file detection. + Script should write output to STDOUT and exit 0 when app is detected (installed). + - detectionPath: Full path to the file or folder to detect (e.g., 'C:\\Program Files\\MyApp') - detectionFile: File name to detect (optional, for folder path detection) - detectionType: 'exists', 'modifiedDate', 'createdDate', 'version', 'sizeInMB' (default: 'exists') - check32BitOn64System: Boolean, check 32-bit registry/paths on 64-bit systems (default: false) @@ -69,9 +71,20 @@ function Add-CIPPW32ScriptApplication { $FileName = $ChocoXml.ApplicationInfo.FileName $UnencryptedSize = [int64]$ChocoXml.ApplicationInfo.UnencryptedContentSize - # Build detection rules - if ($Properties.detectionPath) { - # Determine if this is a file or folder detection + # Build detection rules — detection script takes priority, then file detection, then marker file fallback + if ($Properties.detectionScript) { + # PowerShell script detection: script should write to STDOUT and exit 0 when detected + $DetectionScriptContent = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($Properties.detectionScript)) + $DetectionRules = @( + @{ + '@odata.type' = '#microsoft.graph.win32LobAppPowerShellScriptDetection' + scriptContent = $DetectionScriptContent + enforceSignatureCheck = if ($null -ne $Properties.enforceSignatureCheck) { [bool]$Properties.enforceSignatureCheck } else { $false } + runAs32Bit = if ($null -ne $Properties.runAs32Bit) { [bool]$Properties.runAs32Bit } else { $false } + } + ) + } elseif ($Properties.detectionPath) { + # File system detection $DetectionRule = @{ '@odata.type' = '#microsoft.graph.win32LobAppFileSystemDetection' check32BitOn64System = if ($null -ne $Properties.check32BitOn64System) { [bool]$Properties.check32BitOn64System } else { $false } @@ -84,7 +97,6 @@ function Add-CIPPW32ScriptApplication { $DetectionRule['fileOrFolderName'] = $Properties.detectionFile } else { # Folder/File detection (full path) - # Split the path into directory and file/folder name $PathItem = Split-Path $Properties.detectionPath -Leaf $ParentPath = Split-Path $Properties.detectionPath -Parent @@ -149,7 +161,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 +185,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/Alerts/Get-CIPPAlertLongLivedAppCredentials.ps1 b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertLongLivedAppCredentials.ps1 new file mode 100644 index 000000000000..ab3a5bfb9302 --- /dev/null +++ b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertLongLivedAppCredentials.ps1 @@ -0,0 +1,58 @@ +function Get-CIPPAlertLongLivedAppCredentials { + <# + .FUNCTIONALITY + Entrypoint + #> + [CmdletBinding()] + Param ( + [Parameter(Mandatory = $false)] + [Alias('input')] + $InputValue, + $TenantFilter + ) + + $MaxMonths = $InputValue + + try { + $Apps = @(New-GraphGetRequest -uri "https://graph.microsoft.com/beta/applications?`$select=id,appId,displayName,passwordCredentials,keyCredentials&`$top=999" -tenantid $TenantFilter -AsApp $true) + + $NowUtc = [datetime]::UtcNow + $DatePartition = (Get-Date -UFormat '%Y%m%d').ToString() + $CredTypeMap = @( + @{ Property = 'passwordCredentials'; TypeLabel = 'Secret' } + @{ Property = 'keyCredentials'; TypeLabel = 'Certificate' } + ) + + foreach ($App in @($Apps)) { + foreach ($ct in $CredTypeMap) { + $credList = $App.($ct.Property) + if (-not $credList) { continue } + foreach ($Cred in @($credList)) { + $startUtc = ([datetime]$Cred.startDateTime).ToUniversalTime() + $endUtc = ([datetime]$Cred.endDateTime).ToUniversalTime() + if ($endUtc -le $startUtc -or $endUtc -le $NowUtc) { continue } + $months = (New-TimeSpan -Start $startUtc -End $endUtc).TotalDays / 30.4375 + if ($months -gt $MaxMonths) { + $keyId = if ($Cred.keyId) { "$($Cred.keyId)" } else { 'unknown' } + $tracePartition = "$DatePartition-$($App.id)-$keyId" -replace '[/\\#?]', '_' + $oneFinding = [PSCustomObject]@{ + AppDisplayName = $App.displayName + AppId = $App.appId + CredentialType = $ct.TypeLabel + CredentialName = $Cred.displayName + KeyId = $Cred.keyId + StartDateTime = $Cred.startDateTime + EndDateTime = $Cred.endDateTime + ValidityMonths = [math]::Round([double]$months, 2) + MaxMonthsAllowed = $MaxMonths + } + Write-AlertTrace -cmdletName $MyInvocation.MyCommand -tenantFilter $TenantFilter -data $oneFinding -PartitionKey $tracePartition + } + } + } + } + } catch { + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + Write-LogMessage -API 'Alerts' -tenant $TenantFilter -message "Excessive secret validity alert failed: $ErrorMessage" -sev 'Error' + } +} diff --git a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertRoleEscalableGroups.ps1 b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertRoleEscalableGroups.ps1 new file mode 100644 index 000000000000..d9afcce955eb --- /dev/null +++ b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertRoleEscalableGroups.ps1 @@ -0,0 +1,129 @@ +function Get-CIPPAlertRoleEscalableGroups { + <# + .SYNOPSIS + Flags non-role-assignable groups in Entra directory role paths. + + .DESCRIPTION + Scans Entra directory role assignments where the role principal is a group. Flags: + 1) direct role-assigned groups that are not marked isAssignableToRole, and + 2) non-role-assignable nested groups found via transitive group membership under a role-assigned group. + Findings include path type, role-assigned group details, impacted group details, and human-readable role name. + + .FUNCTIONALITY + Entrypoint + #> + [CmdletBinding()] + param ( + [Parameter(Mandatory = $false)] + [Alias('input')] + $InputValue, + $TenantFilter + ) + + try { + $groups = @(New-GraphGetRequest -uri "https://graph.microsoft.com/beta/groups?`$select=id,displayName,isAssignableToRole&`$top=999" -tenantid $TenantFilter) + if (-not $groups -or $groups.Count -eq 0) { + Write-Information "Get-CIPPAlertRoleEscalableGroups: no groups returned for $TenantFilter" + return + } + $groupById = @{} + foreach ($g in $groups) { + if (-not $g.id) { continue } + $groupById["$($g.id)"] = $g + } + + $roleDefinitions = @(New-GraphGetRequest -uri "https://graph.microsoft.com/beta/roleManagement/directory/roleDefinitions?`$select=id,displayName&`$top=999" -tenantid $TenantFilter) + $roleDefById = @{} + foreach ($rd in $roleDefinitions) { + if (-not $rd.id) { continue } + $roleDefById["$($rd.id)"] = $rd + } + + $roleAssignments = @(New-GraphGetRequest -uri "https://graph.microsoft.com/beta/roleManagement/directory/roleAssignments?`$select=id,principalId,roleDefinitionId,directoryScopeId,appScopeId&`$top=999" -tenantid $TenantFilter) + if (-not $roleAssignments -or $roleAssignments.Count -eq 0) { + Write-Information "Get-CIPPAlertRoleEscalableGroups: no role assignments returned for $TenantFilter" + return + } + + $findings = [System.Collections.Generic.List[object]]::new() + $seen = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) + $transitiveNestedGroupIdsByRoot = @{} + + foreach ($assignment in $roleAssignments) { + $principalId = "$($assignment.principalId)" + if (-not $principalId) { continue } + + if (-not $groupById.ContainsKey($principalId)) { continue } + $rootGroup = $groupById[$principalId] + if (-not $rootGroup) { continue } + + $roleDefId = "$($assignment.roleDefinitionId)" + $roleDef = if ($roleDefId) { $roleDefById[$roleDefId] } else { $null } + $roleName = if ($roleDef -and $roleDef.displayName) { $roleDef.displayName } else { 'Unknown role' } + $scope = if ($assignment.directoryScopeId) { "$($assignment.directoryScopeId)" } elseif ($assignment.appScopeId) { "$($assignment.appScopeId)" } else { '/' } + + if ($rootGroup.isAssignableToRole -ne $true) { + $dedupeKey = "D|$principalId|$roleDefId|$scope" + if (-not $seen.Add($dedupeKey)) { continue } + + $findings.Add([PSCustomObject]@{ + PathType = 'Direct' + RoleAssignedGroupId = $rootGroup.id + RoleAssignedGroupDisplayName = $rootGroup.displayName + GroupDisplayName = $rootGroup.displayName + GroupId = $rootGroup.id + IsAssignableToRole = $rootGroup.isAssignableToRole + RoleName = $roleName + Risk = 'High' + Reason = 'Group has directory role assignment while not marked role-assignable; group owners/admins have an indirect role-escalation path.' + }) + } + + if (-not $transitiveNestedGroupIdsByRoot.ContainsKey($principalId)) { + $nestedIds = [System.Collections.Generic.List[string]]::new() + try { + $transitive = @(New-GraphGetRequest -uri "https://graph.microsoft.com/beta/groups/$principalId/transitiveMembers/microsoft.graph.group?`$select=id" -tenantid $TenantFilter) + foreach ($m in $transitive) { + $mid = "$($m.id)" + if (-not $mid -or $mid -eq $principalId) { continue } + $nestedIds.Add($mid) + } + } catch { + Write-Information "Get-CIPPAlertRoleEscalableGroups: transitiveMembers failed for group $principalId — $($_.Exception.Message)" + } + $transitiveNestedGroupIdsByRoot[$principalId] = $nestedIds + } + + foreach ($nestedId in @($transitiveNestedGroupIdsByRoot[$principalId])) { + if (-not $groupById.ContainsKey($nestedId)) { continue } + $nestedGroup = $groupById[$nestedId] + if (-not $nestedGroup) { continue } + if ($nestedGroup.isAssignableToRole -eq $true) { continue } + + $dedupeKey = "N|$nestedId|$roleDefId|$scope|$principalId" + if (-not $seen.Add($dedupeKey)) { continue } + + $findings.Add([PSCustomObject]@{ + PathType = 'Nested' + RoleAssignedGroupId = $rootGroup.id + RoleAssignedGroupDisplayName = $rootGroup.displayName + GroupDisplayName = $nestedGroup.displayName + GroupId = $nestedGroup.id + IsAssignableToRole = $nestedGroup.isAssignableToRole + RoleName = $roleName + Risk = 'High' + Reason = 'Non-role-assignable group is nested (transitive member) under a group that has a directory role; group owners/admins can add members who inherit high privileged membership.' + }) + } + } + + if ($findings.Count -gt 0) { + Write-AlertTrace -cmdletName $MyInvocation.MyCommand -tenantFilter $TenantFilter -data @($findings) + } else { + Write-LogMessage -API 'Alerts' -tenant $TenantFilter -message "Role-escalable groups alert: no role-escalation group paths found" -sev 'Information' + } + } catch { + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + Write-LogMessage -API 'Alerts' -tenant $TenantFilter -message "Role-escalable groups alert failed: $ErrorMessage" -sev 'Error' + } +} diff --git a/Modules/CIPPCore/Public/Authentication/New-CIPPAPIConfig.ps1 b/Modules/CIPPCore/Public/Authentication/New-CIPPAPIConfig.ps1 index 0ddd57a18fd2..6bbaaa8515d9 100644 --- a/Modules/CIPPCore/Public/Authentication/New-CIPPAPIConfig.ps1 +++ b/Modules/CIPPCore/Public/Authentication/New-CIPPAPIConfig.ps1 @@ -81,7 +81,19 @@ function New-CIPPAPIConfig { $APIIdUrl = New-GraphPOSTRequest -uri "https://graph.microsoft.com/v1.0/applications/$($APIApp.id)" -AsApp $true -NoAuthCheck $true -type PATCH -body "{`"identifierUris`":[`"api://$($APIApp.appId)`"]}" -maxRetries 3 Write-Information 'Adding serviceprincipal' $Step = 'Creating Service Principal' - $ServicePrincipal = New-GraphPOSTRequest -uri 'https://graph.microsoft.com/v1.0/serviceprincipals' -AsApp $true -NoAuthCheck $true -type POST -body "{`"accountEnabled`":true,`"appId`":`"$($APIApp.appId)`",`"displayName`":`"$AppName`",`"tags`":[`"WindowsAzureActiveDirectoryIntegratedApp`",`"AppServiceIntegratedApp`"]}" -maxRetries 3 + $ServicePrincipalBody = "{`"accountEnabled`":true,`"appId`":`"$($APIApp.appId)`",`"displayName`":`"$AppName`",`"tags`":[`"WindowsAzureActiveDirectoryIntegratedApp`",`"AppServiceIntegratedApp`"]}" + for ($Attempt = 1; $Attempt -le 4; $Attempt++) { + try { + $ServicePrincipal = New-GraphPOSTRequest -uri 'https://graph.microsoft.com/v1.0/serviceprincipals' -AsApp $true -NoAuthCheck $true -type POST -body $ServicePrincipalBody -maxRetries 3 + break + } catch { + if ($Attempt -lt 4) { + Start-Sleep -Seconds 1 + continue + } + throw + } + } Write-LogMessage -headers $Headers -API $APINAME -tenant 'None '-message "Created CIPP-API App with name '$($APIApp.displayName)'." -Sev 'info' } } 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/Activity Triggers/Applications/Push-UploadApplication.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Applications/Push-UploadApplication.ps1 index b643716861a1..5db9617a82d8 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Applications/Push-UploadApplication.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Applications/Push-UploadApplication.ps1 @@ -139,6 +139,7 @@ function Push-UploadApplication { if ($AppConfig.description) { $Properties['description'] = $AppConfig.description } if ($AppConfig.publisher) { $Properties['publisher'] = $AppConfig.publisher } if ($AppConfig.uninstallScript) { $Properties['uninstallScript'] = $AppConfig.uninstallScript } + if ($AppConfig.detectionScript) { $Properties['detectionScript'] = $AppConfig.detectionScript } if ($AppConfig.detectionPath) { $Properties['detectionPath'] = $AppConfig.detectionPath } if ($AppConfig.detectionFile) { $Properties['detectionFile'] = $AppConfig.detectionFile } if ($AppConfig.runAsAccount) { $Properties['runAsAccount'] = $AppConfig.runAsAccount } diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Domain Analyser/Push-DomainAnalyserTenant.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Domain Analyser/Push-DomainAnalyserTenant.ps1 index 3111bbe2461f..7a5c8cfac91a 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Domain Analyser/Push-DomainAnalyserTenant.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Domain Analyser/Push-DomainAnalyserTenant.ps1 @@ -132,7 +132,7 @@ function Push-DomainAnalyserTenant { } } } - Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Compress -Depth 5) + Start-CIPPOrchestrator -InputObject $InputObject Write-Host "Started analysis for $DomainCount tenant domains in $($Tenant.defaultDomainName)" Write-LogMessage -Tenant $Tenant.defaultDomainName -TenantId $Tenant.customerId -API 'DomainAnalyser' -message "Started analysis for $DomainCount tenant domains" -sev Info } catch { diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheApplyBatch.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheApplyBatch.ps1 index 513ce4235499..52b60b72436a 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheApplyBatch.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheApplyBatch.ps1 @@ -51,7 +51,7 @@ function Push-CIPPDBCacheApplyBatch { } } - $InstanceId = Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Depth 10 -Compress) + $InstanceId = Start-CIPPOrchestrator -InputObject $InputObject Write-Information "Started flat cache execution orchestrator with ID = '$InstanceId' for $($AllTasks.Count) tasks" return @{ diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPOffboardingComplete.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPOffboardingComplete.ps1 index faca0853ea6f..c848c0b14723 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPOffboardingComplete.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPOffboardingComplete.ps1 @@ -107,7 +107,7 @@ function Push-CIPPOffboardingComplete { # Send post-execution alerts if configured if ($TaskInfo.PostExecution -and $ProcessedResults) { - Send-CIPPScheduledTaskAlert -Results $ProcessedResults -TaskInfo $TaskInfo -TenantFilter $TenantFilter + Send-CIPPScheduledTaskAlert -Results $ProcessedResults -TaskInfo $TaskInfo -TenantFilter $TenantFilter -TaskType 'User Offboarding' } } Write-LogMessage -API 'Offboarding' -tenant $TenantFilter -message "Offboarding completed for $Username" -sev Info -headers $Headers diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecScheduledCommand.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecScheduledCommand.ps1 index 951c48ccdc31..1c32829a0dfc 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecScheduledCommand.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecScheduledCommand.ps1 @@ -258,7 +258,7 @@ function Push-ExecScheduledCommand { $commandParameters['TaskInfo'] = $task } - Write-Information "Starting task: $($Item.Command) for tenant: $Tenant with parameters: $($commandParameters | ConvertTo-Json)" + Write-Information "Starting task: $($Item.Command) for tenant: $Tenant with parameters: $($commandParameters | ConvertTo-Json -Depth 10)" $results = & $Item.Command @commandParameters } catch { $results = "Task Failed: $($_.Exception.Message)" diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-OrchestratorBatchItems.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-OrchestratorBatchItems.ps1 new file mode 100644 index 000000000000..def82c30a355 --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-OrchestratorBatchItems.ps1 @@ -0,0 +1,23 @@ +function Push-OrchestratorBatchItems { + <# + .FUNCTIONALITY + Entrypoint + #> + [CmdletBinding()] + param($Item) + + if ($Item.Parameters.BatchId) { + $Table = Get-CippTable -TableName 'CippOrchestratorBatch' + $Entities = Get-CIPPAzDataTableEntity @Table -Filter "PartitionKey eq '$($Item.Parameters.BatchId)'" + $BatchItems = [system.Collections.Generic.List[object]]::new() + $Entities | ForEach-Object { + $Item = $_.BatchItem | ConvertFrom-Json + $BatchItems.Add($Item) + } + Remove-AzDataTableEntity @Table -Entity $Entities -Force + Write-Information "Retrieved $($BatchItems.Count) batch items for BatchId: $($Item.Parameters.BatchId)" + } else { + $BatchItems = [system.Collections.Generic.List[object]]::new() + } + return $BatchItems +} diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-SchedulerCIPPNotifications.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-SchedulerCIPPNotifications.ps1 index bc1d438f1a99..3bbcb1130688 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-SchedulerCIPPNotifications.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-SchedulerCIPPNotifications.ps1 @@ -109,7 +109,8 @@ function Push-SchedulerCIPPNotifications { if (![string]::IsNullOrEmpty($config.webhook)) { if ($Currentlog) { $JSONContent = $Currentlog | ConvertTo-Json -Compress - Send-CIPPAlert -Type 'webhook' -JSONContent $JSONContent -TenantFilter $Tenant -APIName 'Alerts' + $Title = "Logbook Notification: Alerts found starting at $((Get-Date).AddMinutes(-15))" + Send-CIPPAlert -Type 'webhook' -Title $Title -JSONContent $JSONContent -TenantFilter $Tenant -APIName 'Alerts' -SchemaSource 'Logbook Notification' -InvokingCommand 'Push-SchedulerCIPPNotifications' -UseStandardizedSchema:$([boolean]$Config.UseStandardizedSchema) $UpdateLogs = $CurrentLog | ForEach-Object { $_.sentAsAlert = $true; $_ } if ($UpdateLogs) { Add-CIPPAzDataTableEntity @Table -Entity $UpdateLogs -Force } } @@ -118,7 +119,8 @@ function Push-SchedulerCIPPNotifications { $Data = $CurrentStandardsLogs $JSONContent = New-CIPPAlertTemplate -Data $Data -Format 'json' -InputObject 'table' -CIPPURL $CIPPURL $CurrentStandardsLogs | ConvertTo-Json -Compress - Send-CIPPAlert -Type 'webhook' -JSONContent $JSONContent -TenantFilter $Tenant -APIName 'Alerts' + $Title = "Standards Notification: Out of sync standards detected" + Send-CIPPAlert -Type 'webhook' -Title $Title -JSONContent $JSONContent -TenantFilter $Tenant -APIName 'Alerts' -SchemaSource 'Standards Notification' -InvokingCommand 'Push-SchedulerCIPPNotifications' -UseStandardizedSchema:$([boolean]$Config.UseStandardizedSchema) $updateStandards = $CurrentStandardsLogs | ForEach-Object { if ($_.PSObject.Properties.Name -contains 'sentAsAlert') { $_.sentAsAlert = $true diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Standards/Push-CIPPStandardsApplyBatch.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Standards/Push-CIPPStandardsApplyBatch.ps1 index da334d1686a2..b8c7ca70d114 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Standards/Push-CIPPStandardsApplyBatch.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Standards/Push-CIPPStandardsApplyBatch.ps1 @@ -29,9 +29,8 @@ function Push-CIPPStandardsApplyBatch { SkipLog = $true } | ConvertTo-Json -Depth 25 -Compress Write-Host "Standards InputObject: $InputObject" - $InstanceId = Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject $InputObject + $InstanceId = Start-CIPPOrchestrator -InputObject $InputObject Write-Information "Started standards apply orchestrator with ID = '$InstanceId'" - } catch { Write-Warning "Error in standards apply batch aggregation: $($_.Exception.Message)" } diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Tests/Push-CIPPTestsApplyBatch.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Tests/Push-CIPPTestsApplyBatch.ps1 index 3f9abfa2b984..aa12830a0a06 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Tests/Push-CIPPTestsApplyBatch.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Tests/Push-CIPPTestsApplyBatch.ps1 @@ -41,7 +41,7 @@ function Push-CIPPTestsApplyBatch { SkipLog = $true } - $InstanceId = Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Depth 10 -Compress) + $InstanceId = Start-CIPPOrchestrator -InputObject $InputObject Write-Information "Started flat tests execution orchestrator with ID = '$InstanceId' for $($AllTasks.Count) tasks" return @{ diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Webhooks/Push-AuditLogProcessingBatch.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Webhooks/Push-AuditLogProcessingBatch.ps1 new file mode 100644 index 000000000000..43a76a7c20df --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Webhooks/Push-AuditLogProcessingBatch.ps1 @@ -0,0 +1,59 @@ +function Push-AuditLogProcessingBatch { + <# + .SYNOPSIS + Builds the batch of audit log processing tasks from the webhook cache table. + .DESCRIPTION + Called as a QueueFunction activity by the AuditLogProcessingOrchestrator. + Loads CacheWebhooks in pages, groups by tenant, and returns batch items + for AuditLogTenantProcess activities. Running in an activity isolates + the memory usage from the timer function. + .FUNCTIONALITY + Entrypoint + #> + param($Item) + + $WebhookCacheTable = Get-CippTable -TableName 'CacheWebhooks' + $AllBatchItems = [System.Collections.Generic.List[object]]::new() + $TotalRows = 0 + $PageSize = 20000 + $Skip = 0 + + do { + $WebhookCache = Get-CIPPAzDataTableEntity @WebhookCacheTable -First $PageSize -Skip $Skip + $PageCount = $WebhookCache.Count + $TenantGroups = $WebhookCache | Group-Object -Property PartitionKey + $WebhookCache = $null + + if ($TenantGroups) { + $TotalRows += ($TenantGroups | Measure-Object -Property Count -Sum).Sum + foreach ($TenantGroup in $TenantGroups) { + $TenantFilter = $TenantGroup.Name + $RowIds = @($TenantGroup.Group.RowKey) + for ($i = 0; $i -lt $RowIds.Count; $i += 500) { + $BatchRowIds = $RowIds[$i..([Math]::Min($i + 499, $RowIds.Count - 1))] + $AllBatchItems.Add([PSCustomObject]@{ + TenantFilter = $TenantFilter + RowIds = $BatchRowIds + FunctionName = 'AuditLogTenantProcess' + }) + } + } + $TenantGroups = $null + } + + if ($PageCount -lt $PageSize) { break } + $Skip += $PageSize + } while ($PageCount -eq $PageSize) + + if ($AllBatchItems.Count -gt 0) { + $ProcessQueue = New-CippQueueEntry -Name 'Audit Logs Process' -Reference 'AuditLogsProcess' -TotalTasks $TotalRows + foreach ($BatchItem in $AllBatchItems) { + $BatchItem | Add-Member -MemberType NoteProperty -Name QueueId -Value $ProcessQueue.RowKey -Force + } + Write-Information "AuditLogProcessingBatch: $($AllBatchItems.Count) batch items across $TotalRows rows" + } else { + Write-Information 'AuditLogProcessingBatch: no webhook cache entries found' + } + + return $AllBatchItems.ToArray() +} diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ExecAddAlert.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ExecAddAlert.ps1 index 10d24e146044..8b5fb5fb8535 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ExecAddAlert.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ExecAddAlert.ps1 @@ -8,32 +8,18 @@ function Invoke-ExecAddAlert { [CmdletBinding()] param($Request, $TriggerMetadata) $Headers = $Request.Headers - + $TenantFilter = $Request.Body.tenantFilter ?? $env:TenantID $Severity = 'Alert' $Result = if ($Request.Body.sendEmailNow -or $Request.Body.sendWebhookNow -eq $true -or $Request.Body.writeLog -eq $true -or $Request.Body.sendPsaNow -eq $true) { - $sev = ([pscustomobject]$Request.body.Severity).value -join (',') - if ($Request.body.email -or $Request.body.webhook) { - Write-Host 'found config, setting' - $config = @{ - email = $Request.body.email - webhook = $Request.body.webhook - onepertenant = $Request.body.onePerTenant - logsToInclude = $Request.body.logsToInclude - sendtoIntegration = $true - sev = $sev - } - Write-Host "setting notification config to $($config | ConvertTo-Json)" - $Results = Set-cippNotificationConfig @Config - Write-Host $Results - } $Title = 'CIPP Notification Test' if ($Request.Body.sendEmailNow -eq $true) { $CIPPAlert = @{ Type = 'email' Title = $Title HTMLContent = $Request.Body.text + TenantFilter = $TenantFilter } Send-CIPPAlert @CIPPAlert } @@ -46,6 +32,7 @@ function Invoke-ExecAddAlert { Type = 'webhook' Title = $Title JSONContent = $JSONContent + TenantFilter = $TenantFilter } Send-CIPPAlert @CIPPAlert } @@ -54,6 +41,7 @@ function Invoke-ExecAddAlert { Type = 'psa' Title = $Title HTMLContent = $Request.Body.text + TenantFilter = $TenantFilter } Send-CIPPAlert @CIPPAlert } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Extensions/Invoke-ExecExtensionClearHIBPKey.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Extensions/Invoke-ExecExtensionClearHIBPKey.ps1 new file mode 100644 index 000000000000..c7a6f656047d --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Extensions/Invoke-ExecExtensionClearHIBPKey.ps1 @@ -0,0 +1,22 @@ +function Invoke-ExecExtensionClearHIBPKey { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + CIPP.Extension.ReadWrite + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $Results = try { + Remove-ExtensionAPIKey -Extension 'HIBP' | Out-Null + 'Successfully cleared the HIBP API key.' + } catch { + "Failed to clear the HIBP API key" + } + + return ([HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::OK + Body = @{'Results' = $Results } + }) +} diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Scheduler/Invoke-AddScheduledItem.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Scheduler/Invoke-AddScheduledItem.ps1 index 48bccf1b6582..f3443d1471c6 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Scheduler/Invoke-AddScheduledItem.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Scheduler/Invoke-AddScheduledItem.ps1 @@ -33,6 +33,12 @@ function Invoke-AddScheduledItem { Clear = $true } $null = Test-CIPPRerun @RerunParams + # Clear ExecutedTime so the one-time task rerun guard in Push-ExecScheduledCommand does not block re-execution + $null = Update-AzDataTableEntity -Force @Table -Entity @{ + PartitionKey = $ExistingTask.PartitionKey + RowKey = $ExistingTask.RowKey + ExecutedTime = '' + } $Result = Add-CIPPScheduledTask -RowKey $Request.Body.RowKey -RunNow -Headers $Headers } else { $ScheduledTask = @{ diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecNotificationConfig.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecNotificationConfig.ps1 index feafd5eae3f0..46e3341eb2c6 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecNotificationConfig.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecNotificationConfig.ps1 @@ -9,12 +9,20 @@ Function Invoke-ExecNotificationConfig { param($Request, $TriggerMetadata) $sev = ([pscustomobject]$Request.body.Severity).value -join (',') $config = @{ - email = $Request.body.email - webhook = $Request.body.webhook - onepertenant = $Request.body.onePerTenant - logsToInclude = $Request.body.logsToInclude - sendtoIntegration = $Request.body.sendtoIntegration - sev = $sev + email = $Request.body.email + webhook = $Request.body.webhook + webhookAuthType = $Request.body.webhookAuthType.value + webhookAuthToken = $Request.body.webhookAuthToken + webhookAuthUsername = $Request.body.webhookAuthUsername + webhookAuthPassword = $Request.body.webhookAuthPassword + webhookAuthHeaderName = $Request.body.webhookAuthHeaderName + webhookAuthHeaderValue = $Request.body.webhookAuthHeaderValue + webhookAuthHeaders = $Request.body.webhookAuthHeaders + onepertenant = $Request.body.onePerTenant + logsToInclude = $Request.body.logsToInclude + sendtoIntegration = $Request.body.sendtoIntegration + UseStandardizedSchema = [boolean]$Request.body.UseStandardizedSchema + sev = $sev } $Results = Set-cippNotificationConfig @Config $body = [pscustomobject]@{'Results' = $Results } 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/Email-Exchange/Administration/Invoke-ExecScheduleOOOVacation.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecScheduleOOOVacation.ps1 index 22582d8ac03f..79fcbea7d920 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecScheduleOOOVacation.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecScheduleOOOVacation.ps1 @@ -26,12 +26,36 @@ function Invoke-ExecScheduleOOOVacation { $UserDisplay = ($UserUPNs | Select-Object -First 3) -join ', ' if ($UserUPNs.Count -gt 3) { $UserDisplay += " (+$($UserUPNs.Count - 3) more)" } + # Convert epoch timestamps to datetime strings for Scheduled mode + $StartTimeStr = [DateTimeOffset]::FromUnixTimeSeconds([int64]$StartDate).DateTime.ToString() + $EndTimeStr = [DateTimeOffset]::FromUnixTimeSeconds([int64]$EndDate).DateTime.ToString() + $SharedParams = [PSCustomObject]@{ TenantFilter = $TenantFilter Users = $UserUPNs InternalMessage = $InternalMessage ExternalMessage = $ExternalMessage APIName = $APIName + StartTime = $StartTimeStr + EndTime = $EndTimeStr + } + + # Calendar options — conditionally add when truthy in the request body + if ($Request.Body.CreateOOFEvent) { + $SharedParams | Add-Member -NotePropertyName 'CreateOOFEvent' -NotePropertyValue $true + } + if (-not [string]::IsNullOrWhiteSpace($Request.Body.OOFEventSubject)) { + $SharedParams | Add-Member -NotePropertyName 'OOFEventSubject' -NotePropertyValue $Request.Body.OOFEventSubject + } + if ($Request.Body.AutoDeclineFutureRequestsWhenOOF) { + $SharedParams | Add-Member -NotePropertyName 'AutoDeclineFutureRequestsWhenOOF' -NotePropertyValue $true + } + if ($Request.Body.DeclineEventsForScheduledOOF) { + $SharedParams | Add-Member -NotePropertyName 'DeclineEventsForScheduledOOF' -NotePropertyValue $true + $SharedParams | Add-Member -NotePropertyName 'DeclineAllEventsForScheduledOOF' -NotePropertyValue $true + } + if (-not [string]::IsNullOrWhiteSpace($Request.Body.DeclineMeetingMessage)) { + $SharedParams | Add-Member -NotePropertyName 'DeclineMeetingMessage' -NotePropertyValue $Request.Body.DeclineMeetingMessage } # Add task — enables OOO with messages at start date diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecSetOoO.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecSetOoO.ps1 index 07fc25199911..a9df5705c33f 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecSetOoO.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecSetOoO.ps1 @@ -51,8 +51,27 @@ function Invoke-ExecSetOoO { $EndTime = $Request.Body.EndTime -match '^\d+$' ? [DateTimeOffset]::FromUnixTimeSeconds([int]$Request.Body.EndTime).DateTime : $Request.Body.EndTime $SplatParams.StartTime = $StartTime $SplatParams.EndTime = $EndTime + + # Calendar options — only pass when explicitly provided in the request body + if ($null -ne $Request.Body.CreateOOFEvent) { + $SplatParams.CreateOOFEvent = [bool]$Request.Body.CreateOOFEvent + } + if (-not [string]::IsNullOrWhiteSpace($Request.Body.OOFEventSubject)) { + $SplatParams.OOFEventSubject = $Request.Body.OOFEventSubject + } + if ($null -ne $Request.Body.AutoDeclineFutureRequestsWhenOOF) { + $SplatParams.AutoDeclineFutureRequestsWhenOOF = [bool]$Request.Body.AutoDeclineFutureRequestsWhenOOF + } + if ($null -ne $Request.Body.DeclineEventsForScheduledOOF) { + $SplatParams.DeclineEventsForScheduledOOF = [bool]$Request.Body.DeclineEventsForScheduledOOF + $SplatParams.DeclineAllEventsForScheduledOOF = [bool]$Request.Body.DeclineEventsForScheduledOOF + } + if (-not [string]::IsNullOrWhiteSpace($Request.Body.DeclineMeetingMessage)) { + $SplatParams.DeclineMeetingMessage = $Request.Body.DeclineMeetingMessage + } } + Write-Information "Setting Out of Office with the following parameters: $($SplatParams | ConvertTo-Json -Depth 10)" $Results = Set-CIPPOutOfOffice @SplatParams $StatusCode = [HttpStatusCode]::OK } catch { diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-AddWin32ScriptApp.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-AddWin32ScriptApp.ps1 index bdb797cbdd88..3ee3383f71df 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-AddWin32ScriptApp.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-AddWin32ScriptApp.ps1 @@ -49,6 +49,7 @@ function Invoke-AddWin32ScriptApp { uninstallScript = $Win32ScriptApp.uninstallScript detectionPath = $Win32ScriptApp.detectionPath detectionFile = $Win32ScriptApp.detectionFile + detectionScript = $Win32ScriptApp.detectionScript runAsAccount = if ($Win32ScriptApp.InstallAsSystem) { 'system' } else { 'user' } deviceRestartBehavior = if ($Win32ScriptApp.DisableRestart) { 'suppress' } else { 'allow' } runAs32Bit = [bool]$Win32ScriptApp.runAs32Bit diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-AddDefenderDeployment.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-AddDefenderDeployment.ps1 index f5a9b6d7609a..dbc451867880 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-AddDefenderDeployment.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-AddDefenderDeployment.ps1 @@ -18,377 +18,35 @@ function Invoke-AddDefenderDeployment { $DefenderExclusions = $Request.Body.Exclusion $ASR = $Request.Body.ASR $EDR = $Request.Body.EDR + $Results = foreach ($tenant in $Tenants) { try { if ($Compliance) { - $ConnectorStatus = Enable-CIPPMDEConnector -TenantFilter $tenant - if (!$ConnectorStatus.Success) { - "$($tenant): Failed to enable MDE Connector - $($ConnectorStatus.ErrorMessage)" - continue - } else { - "$($tenant): MDE Connector is $($ConnectorStatus.PartnerState)" - } - - - $SettingsObject = @{ - id = 'fc780465-2017-40d4-a0c5-307022471b92' - androidEnabled = [bool]$Compliance.ConnectAndroid - iosEnabled = [bool]$Compliance.ConnectIos - windowsEnabled = [bool]$Compliance.Connectwindows - macEnabled = [bool]$Compliance.ConnectMac - partnerUnsupportedOsVersionBlocked = [bool]$Compliance.BlockunsupportedOS - partnerUnresponsivenessThresholdInDays = 7 - allowPartnerToCollectIOSApplicationMetadata = [bool]$Compliance.ConnectIosCompliance - allowPartnerToCollectIOSPersonalApplicationMetadata = [bool]$Compliance.ConnectIosCompliance - androidDeviceBlockedOnMissingPartnerData = [bool]$Compliance.androidDeviceBlockedOnMissingPartnerData - iosDeviceBlockedOnMissingPartnerData = [bool]$Compliance.iosDeviceBlockedOnMissingPartnerData - windowsDeviceBlockedOnMissingPartnerData = [bool]$Compliance.windowsDeviceBlockedOnMissingPartnerData - macDeviceBlockedOnMissingPartnerData = [bool]$Compliance.macDeviceBlockedOnMissingPartnerData - androidMobileApplicationManagementEnabled = [bool]$Compliance.ConnectAndroidCompliance - iosMobileApplicationManagementEnabled = [bool]$Compliance.appSync - windowsMobileApplicationManagementEnabled = [bool]$Compliance.windowsMobileApplicationManagementEnabled - allowPartnerToCollectIosCertificateMetadata = [bool]$Compliance.allowPartnerToCollectIosCertificateMetadata - allowPartnerToCollectIosPersonalCertificateMetadata = [bool]$Compliance.allowPartnerToCollectIosPersonalCertificateMetadata - microsoftDefenderForEndpointAttachEnabled = [bool]$true - } - $SettingsObj = $SettingsObject | ConvertTo-Json -Compress - $ConnectorUri = 'https://graph.microsoft.com/beta/deviceManagement/mobileThreatDefenseConnectors/fc780465-2017-40d4-a0c5-307022471b92' - $ConnectorExists = $false - $SettingsMatch = $false - try { - $ExistingSettings = New-GraphGETRequest -uri $ConnectorUri -tenantid $tenant - $ConnectorExists = $true - - # Check if any setting doesn't match - $SettingsMatch = $true - foreach ($key in $SettingsObject.Keys) { - if ($ExistingSettings.$key -ne $SettingsObject[$key]) { - $SettingsMatch = $false - break - } - } - } catch { - $ConnectorExists = $false - } - if ($SettingsMatch) { - "Defender Intune Configuration already correct and active for $($tenant). Skipping" - } elseif ($ConnectorExists) { - $null = New-GraphPOSTRequest -uri $ConnectorUri -tenantid $tenant -type PATCH -body $SettingsObj -AsApp $true - "$($tenant): Successfully updated Defender Compliance and Reporting settings." - } else { - $null = New-GraphPOSTRequest -uri 'https://graph.microsoft.com/beta/deviceManagement/mobileThreatDefenseConnectors/' -tenantid $tenant -type POST -body $SettingsObj -AsApp $true - "$($tenant): Successfully created Defender Compliance and Reporting settings." - } + Set-CIPPDefenderCompliancePolicy -TenantFilter $tenant -Compliance $Compliance -Headers $Headers -APIName $APIName } - - if ($PolicySettings) { - $Settings = switch ($PolicySettings) { - { $_.ScanArchives } { - @{'@odata.type' = '#microsoft.graph.deviceManagementConfigurationSetting'; settingInstance = @{ '@odata.type' = '#microsoft.graph.deviceManagementConfigurationChoiceSettingInstance'; settingDefinitionId = 'device_vendor_msft_policy_config_defender_allowarchivescanning'; choiceSettingValue = @{'@odata.type' = '#microsoft.graph.deviceManagementConfigurationChoiceSettingValue'; value = 'device_vendor_msft_policy_config_defender_allowarchivescanning_1'; settingValueTemplateReference = @{settingValueTemplateId = '9ead75d4-6f30-4bc5-8cc5-ab0f999d79f0' } }; settingInstanceTemplateReference = @{settingInstanceTemplateId = '7c5c9cde-f74d-4d11-904f-de4c27f72d89' } } } - } { $_.AllowBehavior } { - @{ '@odata.type' = '#microsoft.graph.deviceManagementConfigurationSetting'; settingInstance = @{'@odata.type' = '#microsoft.graph.deviceManagementConfigurationChoiceSettingInstance' ; settingDefinitionId = 'device_vendor_msft_policy_config_defender_allowbehaviormonitoring' ; choiceSettingValue = @{'@odata.type' = '#microsoft.graph.deviceManagementConfigurationChoiceSettingValue'; value = 'device_vendor_msft_policy_config_defender_allowbehaviormonitoring_1'; settingValueTemplateReference = @{settingValueTemplateId = '905921da-95e2-4a10-9e30-fe5540002ce1' } }; settingInstanceTemplateReference = @{settingInstanceTemplateId = '8eef615a-1aa0-46f4-a25a-12cbe65de5ab' } } } - } { $_.AllowCloudProtection } { - @{'@odata.type' = '#microsoft.graph.deviceManagementConfigurationSetting'; settingInstance = @{'@odata.type' = '#microsoft.graph.deviceManagementConfigurationChoiceSettingInstance' ; settingDefinitionId = 'device_vendor_msft_policy_config_defender_allowcloudprotection'; choiceSettingValue = @{ '@odata.type' = '#microsoft.graph.deviceManagementConfigurationChoiceSettingValue' ; value = 'device_vendor_msft_policy_config_defender_allowcloudprotection_1'; settingValueTemplateReference = @{settingValueTemplateId = '16fe8afd-67be-4c50-8619-d535451a500c' } }; settingInstanceTemplateReference = @{settingInstanceTemplateId = '7da139f1-9b7e-407d-853a-c2e5037cdc70' } } } - } { $_.AllowEmailScanning } { - @{'@odata.type' = '#microsoft.graph.deviceManagementConfigurationSetting'; settingInstance = @{'@odata.type' = '#microsoft.graph.deviceManagementConfigurationChoiceSettingInstance'; settingDefinitionId = 'device_vendor_msft_policy_config_defender_allowemailscanning' ; choiceSettingValue = @{'@odata.type' = '#microsoft.graph.deviceManagementConfigurationChoiceSettingValue' ; value = 'device_vendor_msft_policy_config_defender_allowemailscanning_1'; settingValueTemplateReference = @{settingValueTemplateId = 'fdf107fd-e13b-4507-9d8f-db4d93476af9' } }; settingInstanceTemplateReference = @{settingInstanceTemplateId = 'b0d9ee81-de6a-4750-86d7-9397961c9852' } } } - } { $_.AllowFullScanNetwork } { - @{'@odata.type' = '#microsoft.graph.deviceManagementConfigurationSetting'; settingInstance = @{ '@odata.type' = '#microsoft.graph.deviceManagementConfigurationChoiceSettingInstance' ; settingDefinitionId = 'device_vendor_msft_policy_config_defender_allowfullscanonmappednetworkdrives' ; choiceSettingValue = @{ '@odata.type' = '#microsoft.graph.deviceManagementConfigurationChoiceSettingValue' ; value = 'device_vendor_msft_policy_config_defender_allowfullscanonmappednetworkdrives_1' ; settingValueTemplateReference = @{settingValueTemplateId = '3e920b10-3773-4ac5-957e-e5573aec6d04' } } ; settingInstanceTemplateReference = @{settingInstanceTemplateId = 'dac47505-f072-48d6-9f23-8d93262d58ed' } } } - } { $_.AllowFullScanRemovable } { - @{'@odata.type' = '#microsoft.graph.deviceManagementConfigurationSetting'; settingInstance = @{ '@odata.type' = '#microsoft.graph.deviceManagementConfigurationChoiceSettingInstance' ; settingDefinitionId = 'device_vendor_msft_policy_config_defender_allowfullscanremovabledrivescanning' ; choiceSettingValue = @{'@odata.type' = '#microsoft.graph.deviceManagementConfigurationChoiceSettingValue'; value = 'device_vendor_msft_policy_config_defender_allowfullscanremovabledrivescanning_1' ; settingValueTemplateReference = @{settingValueTemplateId = '366c5727-629b-4a81-b50b-52f90282fa2c' } } ; settingInstanceTemplateReference = @{settingInstanceTemplateId = 'fb36e70b-5bc9-488a-a949-8ea3ac1634d5' } } } - } { $_.AllowDownloadable } { - @{ '@odata.type' = '#microsoft.graph.deviceManagementConfigurationSetting'; settingInstance = @{'@odata.type' = '#microsoft.graph.deviceManagementConfigurationChoiceSettingInstance' ; settingDefinitionId = 'device_vendor_msft_policy_config_defender_allowioavprotection' ; choiceSettingValue = @{'@odata.type' = '#microsoft.graph.deviceManagementConfigurationChoiceSettingValue' ; value = 'device_vendor_msft_policy_config_defender_allowioavprotection_1'; settingValueTemplateReference = @{settingValueTemplateId = 'df4e6cbd-f7ff-41c8-88cd-fa25264a237e' } }; settingInstanceTemplateReference = @{settingInstanceTemplateId = 'fa06231d-aed4-4601-b631-3a37e85b62a0' } } } - } { $_.AllowRealTime } { - @{'@odata.type' = '#microsoft.graph.deviceManagementConfigurationSetting' ; settingInstance = @{'@odata.type' = '#microsoft.graph.deviceManagementConfigurationChoiceSettingInstance' ; settingDefinitionId = 'device_vendor_msft_policy_config_defender_allowrealtimemonitoring'; choiceSettingValue = @{ '@odata.type' = '#microsoft.graph.deviceManagementConfigurationChoiceSettingValue'; value = 'device_vendor_msft_policy_config_defender_allowrealtimemonitoring_1'; settingValueTemplateReference = @{settingValueTemplateId = '0492c452-1069-4b91-9363-93b8e006ab12' } } ; settingInstanceTemplateReference = @{settingInstanceTemplateId = 'f0790e28-9231-4d37-8f44-84bb47ca1b3e' } } } - } { $_.AllowNetwork } { - @{ '@odata.type' = '#microsoft.graph.deviceManagementConfigurationSetting' ; settingInstance = @{ '@odata.type' = '#microsoft.graph.deviceManagementConfigurationChoiceSettingInstance'; settingDefinitionId = 'device_vendor_msft_policy_config_defender_allowscanningnetworkfiles' ; choiceSettingValue = @{'@odata.type' = '#microsoft.graph.deviceManagementConfigurationChoiceSettingValue'; value = 'device_vendor_msft_policy_config_defender_allowscanningnetworkfiles_1' ; settingValueTemplateReference = @{settingValueTemplateId = '7b8c858c-a17d-4623-9e20-f34b851670ce' } }; settingInstanceTemplateReference = @{settingInstanceTemplateId = 'f8f28442-0a6b-4b52-b42c-d31d9687c1cf' } } } - } { $_.AllowScriptScan } { - @{ '@odata.type' = '#microsoft.graph.deviceManagementConfigurationSetting' ; settingInstance = @{ '@odata.type' = '#microsoft.graph.deviceManagementConfigurationChoiceSettingInstance' ; settingDefinitionId = 'device_vendor_msft_policy_config_defender_allowscriptscanning'; choiceSettingValue = @{'@odata.type' = '#microsoft.graph.deviceManagementConfigurationChoiceSettingValue' ; value = 'device_vendor_msft_policy_config_defender_allowscriptscanning_1'; settingValueTemplateReference = @{settingValueTemplateId = 'ab9e4320-c953-4067-ac9a-be2becd06b4a' } } ; settingInstanceTemplateReference = @{settingInstanceTemplateId = '000cf176-949c-4c08-a5d4-90ed43718db7' } } } - } { $_.AllowUI } { - @{ '@odata.type' = '#microsoft.graph.deviceManagementConfigurationSetting' ; settingInstance = @{ '@odata.type' = '#microsoft.graph.deviceManagementConfigurationChoiceSettingInstance' ; settingDefinitionId = 'device_vendor_msft_policy_config_defender_allowuseruiaccess' ; choiceSettingValue = @{ '@odata.type' = '#microsoft.graph.deviceManagementConfigurationChoiceSettingValue' ; value = 'device_vendor_msft_policy_config_defender_allowuseruiaccess_1' ; settingValueTemplateReference = @{settingValueTemplateId = '4b6c9739-4449-4006-8e5f-3049136470ea' } }; settingInstanceTemplateReference = @{settingInstanceTemplateId = '0170a900-b0bc-4ccc-b7ce-dda9be49189b' } } } - } { $_.CheckSigs } { - @{ '@odata.type' = '#microsoft.graph.deviceManagementConfigurationSetting' ; settingInstance = @{ '@odata.type' = '#microsoft.graph.deviceManagementConfigurationChoiceSettingInstance' ; settingDefinitionId = 'device_vendor_msft_policy_config_defender_checkforsignaturesbeforerunningscan' ; choiceSettingValue = @{ '@odata.type' = '#microsoft.graph.deviceManagementConfigurationChoiceSettingValue' ; value = 'device_vendor_msft_policy_config_defender_checkforsignaturesbeforerunningscan_1' ; settingValueTemplateReference = @{settingValueTemplateId = '010779d1-edd4-441d-8034-89ad57a863fe' } } ; settingInstanceTemplateReference = @{settingInstanceTemplateId = '4fea56e3-7bb6-4ad3-88c6-e364dd2f97b9' } } } - } { $_.DisableCatchupFullScan } { - @{ '@odata.type' = '#microsoft.graph.deviceManagementConfigurationSetting' ; settingInstance = @{ '@odata.type' = '#microsoft.graph.deviceManagementConfigurationChoiceSettingInstance' ; settingDefinitionId = 'device_vendor_msft_policy_config_defender_disablecatchupfullscan'; choiceSettingValue = @{'@odata.type' = '#microsoft.graph.deviceManagementConfigurationChoiceSettingValue'; value = 'device_vendor_msft_policy_config_defender_disablecatchupfullscan_1' ; settingValueTemplateReference = @{settingValueTemplateId = '1b26092f-48c4-447b-99d4-e9c501542f1c' } } ; settingInstanceTemplateReference = @{settingInstanceTemplateId = 'f881b08c-f047-40d2-b7d9-3dde7ce9ef64' } } } - } { $_.DisableCatchupQuickScan } { - @{ '@odata.type' = '#microsoft.graph.deviceManagementConfigurationSetting'; settingInstance = @{'@odata.type' = '#microsoft.graph.deviceManagementConfigurationChoiceSettingInstance' ; settingDefinitionId = 'device_vendor_msft_policy_config_defender_disablecatchupquickscan' ; choiceSettingValue = @{ '@odata.type' = '#microsoft.graph.deviceManagementConfigurationChoiceSettingValue' ; value = 'device_vendor_msft_policy_config_defender_disablecatchupquickscan_1' ; settingValueTemplateReference = @{settingValueTemplateId = 'd263ced7-0d23-4095-9326-99c8b3f5d35b' } } ; settingInstanceTemplateReference = @{settingInstanceTemplateId = 'dabf6781-9d5d-42da-822a-d4327aa2bdd1' } } } - } { $_.EnableNetworkProtection } { - @{ '@odata.type' = '#microsoft.graph.deviceManagementConfigurationSetting'; settingInstance = @{'@odata.type' = '#microsoft.graph.deviceManagementConfigurationChoiceSettingInstance'; settingDefinitionId = 'device_vendor_msft_policy_config_defender_enablenetworkprotection' ; choiceSettingValue = @{ '@odata.type' = '#microsoft.graph.deviceManagementConfigurationChoiceSettingValue' ; value = "device_vendor_msft_policy_config_defender_enablenetworkprotection_$($_.EnableNetworkProtection.value)" ; settingValueTemplateReference = @{settingValueTemplateId = 'ee58fb51-9ae5-408b-9406-b92b643f388a' } } ; settingInstanceTemplateReference = @{settingInstanceTemplateId = 'f53ab20e-8af6-48f5-9fa1-46863e1e517e' } } } - } { $_.LowCPU } { - @{ '@odata.type' = '#microsoft.graph.deviceManagementConfigurationSetting' ; settingInstance = @{ '@odata.type' = '#microsoft.graph.deviceManagementConfigurationChoiceSettingInstance' ; settingDefinitionId = 'device_vendor_msft_policy_config_defender_enablelowcpupriority' ; choiceSettingValue = @{'@odata.type' = '#microsoft.graph.deviceManagementConfigurationChoiceSettingValue'; value = 'device_vendor_msft_policy_config_defender_enablelowcpupriority_1' ; settingValueTemplateReference = @{settingValueTemplateId = '045a4a13-deee-4e24-9fe4-985c9357680d' } } ; settingInstanceTemplateReference = @{settingInstanceTemplateId = 'cdeb96cf-18f5-4477-a710-0ea9ecc618af' } } } - } { $_.CloudBlockLevel } { - @{ '@odata.type' = '#microsoft.graph.deviceManagementConfigurationSetting' ; settingInstance = @{ '@odata.type' = '#microsoft.graph.deviceManagementConfigurationChoiceSettingInstance'; settingDefinitionId = 'device_vendor_msft_policy_config_defender_cloudblocklevel'; settingInstanceTemplateReference = @{'@odata.type' = '#microsoft.graph.deviceManagementConfigurationSettingInstanceTemplateReference'; settingInstanceTemplateId = 'c7a37009-c16e-4145-84c8-89a8c121fb15' }; choiceSettingValue = @{'@odata.type' = '#microsoft.graph.deviceManagementConfigurationChoiceSettingValue'; value = "device_vendor_msft_policy_config_defender_cloudblocklevel_$($_.CloudBlockLevel.value ?? '0')"; settingValueTemplateReference = @{'@odata.type' = '#microsoft.graph.deviceManagementConfigurationSettingValueTemplateReference'; settingValueTemplateId = '517b4e84-e933-42b9-b92f-00e640b1a82d' } } } } - } { $_.AvgCPULoadFactor } { - @{ '@odata.type' = '#microsoft.graph.deviceManagementConfigurationSetting' ; settingInstance = @{ '@odata.type' = '#microsoft.graph.deviceManagementConfigurationSimpleSettingInstance' ; settingDefinitionId = 'device_vendor_msft_policy_config_defender_avgcpuloadfactor' ; settingInstanceTemplateReference = @{'@odata.type' = '#microsoft.graph.deviceManagementConfigurationSettingInstanceTemplateReference' ; settingInstanceTemplateId = '816cc03e-8f96-4cba-b14f-2658d031a79a' } ; simpleSettingValue = @{'@odata.type' = '#microsoft.graph.deviceManagementConfigurationIntegerSettingValue'; value = ($_.AvgCPULoadFactor ?? 50); settingValueTemplateReference = @{'@odata.type' = '#microsoft.graph.deviceManagementConfigurationSettingValueTemplateReference'; settingValueTemplateId = '37195fb1-3743-4c8e-a0ce-b6fae6fa3acd' } } } } - } { $_.CloudExtendedTimeout } { - @{ '@odata.type' = '#microsoft.graph.deviceManagementConfigurationSetting' ; settingInstance = @{ '@odata.type' = '#microsoft.graph.deviceManagementConfigurationSimpleSettingInstance'; settingDefinitionId = 'device_vendor_msft_policy_config_defender_cloudextendedtimeout'; settingInstanceTemplateReference = @{ '@odata.type' = '#microsoft.graph.deviceManagementConfigurationSettingInstanceTemplateReference'; settingInstanceTemplateId = 'f61c2788-14e4-4e80-a5a7-bf2ff5052f63' }; simpleSettingValue = @{ '@odata.type' = '#microsoft.graph.deviceManagementConfigurationIntegerSettingValue'; value = ($_.CloudExtendedTimeout ?? 50); settingValueTemplateReference = @{ '@odata.type' = '#microsoft.graph.deviceManagementConfigurationSettingValueTemplateReference'; settingValueTemplateId = '608f1561-b603-46bd-bf5f-0b9872002f75' } } } } - } { $_.SignatureUpdateInterval } { - @{ '@odata.type' = '#microsoft.graph.deviceManagementConfigurationSetting'; settingInstance = @{ '@odata.type' = '#microsoft.graph.deviceManagementConfigurationSimpleSettingInstance'; settingDefinitionId = 'device_vendor_msft_policy_config_defender_signatureupdateinterval'; settingInstanceTemplateReference = @{ '@odata.type' = '#microsoft.graph.deviceManagementConfigurationSettingInstanceTemplateReference'; settingInstanceTemplateId = '89879f27-6b7d-44d4-a08e-0a0de3e9663d' }; simpleSettingValue = @{ '@odata.type' = '#microsoft.graph.deviceManagementConfigurationIntegerSettingValue'; value = ($_.SignatureUpdateInterval ?? 8); settingValueTemplateReference = @{ '@odata.type' = '#microsoft.graph.deviceManagementConfigurationSettingValueTemplateReference'; settingValueTemplateId = '0af6bbed-a74a-4d08-8587-b16b10b774cb' } } } } - } { $_.MeteredConnectionUpdates } { - @{ '@odata.type' = '#microsoft.graph.deviceManagementConfigurationSetting'; settingInstance = @{ '@odata.type' = '#microsoft.graph.deviceManagementConfigurationChoiceSettingInstance'; settingDefinitionId = 'device_vendor_msft_defender_configuration_meteredconnectionupdates'; settingInstanceTemplateReference = @{ '@odata.type' = '#microsoft.graph.deviceManagementConfigurationSettingInstanceTemplateReference'; settingInstanceTemplateId = '7e3aaffb-309f-46de-8cd7-25c1a3b19e5b' }; choiceSettingValue = @{ '@odata.type' = '#microsoft.graph.deviceManagementConfigurationChoiceSettingValue'; value = 'device_vendor_msft_defender_configuration_meteredconnectionupdates_1'; settingValueTemplateReference = @{ '@odata.type' = '#microsoft.graph.deviceManagementConfigurationSettingValueTemplateReference'; settingValueTemplateId = '20cf972c-be3f-4bc1-93d3-781829d55233' } } } } - } { $_.AllowOnAccessProtection } { - @{ '@odata.type' = '#microsoft.graph.deviceManagementConfigurationSetting'; settingInstance = @{ '@odata.type' = '#microsoft.graph.deviceManagementConfigurationChoiceSettingInstance'; settingDefinitionId = 'device_vendor_msft_policy_config_defender_allowonaccessprotection'; settingInstanceTemplateReference = @{ '@odata.type' = '#microsoft.graph.deviceManagementConfigurationSettingInstanceTemplateReference'; settingInstanceTemplateId = 'afbc322b-083c-4281-8242-ebbb91398b41' }; choiceSettingValue = @{ '@odata.type' = '#microsoft.graph.deviceManagementConfigurationChoiceSettingValue'; value = "device_vendor_msft_policy_config_defender_allowonaccessprotection_$($_.AllowOnAccessProtection.value ?? '1')"; settingValueTemplateReference = @{ '@odata.type' = '#microsoft.graph.deviceManagementConfigurationSettingValueTemplateReference'; settingValueTemplateId = 'ed077fee-9803-44f3-b045-aab34d8e6d52' } } } } - } { $_.DisableLocalAdminMerge } { - @{ '@odata.type' = '#microsoft.graph.deviceManagementConfigurationSetting'; settingInstance = @{ '@odata.type' = '#microsoft.graph.deviceManagementConfigurationChoiceSettingInstance'; settingDefinitionId = 'device_vendor_msft_defender_configuration_disablelocaladminmerge'; settingInstanceTemplateReference = @{ '@odata.type' = '#microsoft.graph.deviceManagementConfigurationSettingInstanceTemplateReference'; settingInstanceTemplateId = '5f9a9c65-dea7-4987-a5f5-b28cfd9762ba' }; choiceSettingValue = @{ '@odata.type' = '#microsoft.graph.deviceManagementConfigurationChoiceSettingValue'; value = 'device_vendor_msft_defender_configuration_disablelocaladminmerge_1'; settingValueTemplateReference = @{ '@odata.type' = '#microsoft.graph.deviceManagementConfigurationSettingValueTemplateReference'; settingValueTemplateId = '3a9774b2-3143-47eb-bbca-d73c0ace2b7e' } } } } - } { $_.SubmitSamplesConsent } { - @{ '@odata.type' = '#microsoft.graph.deviceManagementConfigurationSetting'; settingInstance = @{ '@odata.type' = '#microsoft.graph.deviceManagementConfigurationChoiceSettingInstance'; settingDefinitionId = 'device_vendor_msft_policy_config_defender_submitsamplesconsent'; settingInstanceTemplateReference = @{ '@odata.type' = '#microsoft.graph.deviceManagementConfigurationSettingInstanceTemplateReference'; settingInstanceTemplateId = 'bc47ce7d-a251-4cae-a8a2-6e8384904ab7' }; choiceSettingValue = @{ '@odata.type' = '#microsoft.graph.deviceManagementConfigurationChoiceSettingValue'; value = "device_vendor_msft_policy_config_defender_submitsamplesconsent_$($_.SubmitSamplesConsent.value ?? '2')"; settingValueTemplateReference = @{ '@odata.type' = '#microsoft.graph.deviceManagementConfigurationSettingValueTemplateReference'; settingValueTemplateId = '826ed4b6-e04f-4975-9d23-6f0904b0d87e' } } } } - } { $_.Remediation } { - @{ - '@odata.type' = '#microsoft.graph.deviceManagementConfigurationSetting'; settingInstance = @{'@odata.type' = '#microsoft.graph.deviceManagementConfigurationGroupSettingCollectionInstance'; settingDefinitionId = 'device_vendor_msft_policy_config_defender_threatseveritydefaultaction'; settingInstanceTemplateReference = @{'@odata.type' = '#microsoft.graph.deviceManagementConfigurationSettingInstanceTemplateReference'; settingInstanceTemplateId = 'f6394bc5-6486-4728-b510-555f5c161f2b' } - groupSettingCollectionValue = @(@{'@odata.type' = '#microsoft.graph.deviceManagementConfigurationGroupSettingValue' - children = @( - if ($_.Remediation.Low) { @{'@odata.type' = '#microsoft.graph.deviceManagementConfigurationChoiceSettingInstance'; settingDefinitionId = 'device_vendor_msft_policy_config_defender_threatseveritydefaultaction_lowseveritythreats'; choiceSettingValue = @{'@odata.type' = '#microsoft.graph.deviceManagementConfigurationChoiceSettingValue'; value = "device_vendor_msft_policy_config_defender_threatseveritydefaultaction_lowseveritythreats_$($_.Remediation.Low.value)" } } } - if ($_.Remediation.Moderate) { @{'@odata.type' = '#microsoft.graph.deviceManagementConfigurationChoiceSettingInstance'; settingDefinitionId = 'device_vendor_msft_policy_config_defender_threatseveritydefaultaction_moderateseveritythreats'; choiceSettingValue = @{'@odata.type' = '#microsoft.graph.deviceManagementConfigurationChoiceSettingValue'; value = "device_vendor_msft_policy_config_defender_threatseveritydefaultaction_moderateseveritythreats_$($_.Remediation.Moderate.value)" } } } - if ($_.Remediation.High) { @{'@odata.type' = '#microsoft.graph.deviceManagementConfigurationChoiceSettingInstance'; settingDefinitionId = 'device_vendor_msft_policy_config_defender_threatseveritydefaultaction_highseveritythreats'; choiceSettingValue = @{'@odata.type' = '#microsoft.graph.deviceManagementConfigurationChoiceSettingValue'; value = "device_vendor_msft_policy_config_defender_threatseveritydefaultaction_highseveritythreats_$($_.Remediation.High.value)" } } } - if ($_.Remediation.Severe) { @{'@odata.type' = '#microsoft.graph.deviceManagementConfigurationChoiceSettingInstance'; settingDefinitionId = 'device_vendor_msft_policy_config_defender_threatseveritydefaultaction_severethreats'; choiceSettingValue = @{'@odata.type' = '#microsoft.graph.deviceManagementConfigurationChoiceSettingValue'; value = "device_vendor_msft_policy_config_defender_threatseveritydefaultaction_severethreats_$($_.Remediation.Severe.value)" } } } - ) - } - ) - } - } - } - - } - $CheckExisting = New-GraphGETRequest -uri 'https://graph.microsoft.com/beta/deviceManagement/configurationPolicies' -tenantid $tenant - Write-Host ($CheckExisting | ConvertTo-Json) - if ('Default AV Policy' -in $CheckExisting.Name) { - "$($tenant): AV Policy already exists. Skipping" - } else { - $PolBody = ConvertTo-Json -Depth 10 -Compress -InputObject @{ - name = 'Default AV Policy' - description = '' - platforms = 'windows10' - technologies = 'mdm,microsoftSense' - roleScopeTagIds = @('0') - templateReference = @{templateId = '804339ad-1553-4478-a742-138fb5807418_1' } - settings = @($Settings) - } - - Write-Information ($PolBody) - - $PolicyRequest = New-GraphPOSTRequest -uri 'https://graph.microsoft.com/beta/deviceManagement/configurationPolicies' -tenantid $tenant -type POST -body $PolBody - if ($PolicySettings.AssignTo -ne 'None') { - $AssignBody = if ($PolicySettings.AssignTo -ne 'AllDevicesAndUsers') { '{"assignments":[{"id":"","target":{"@odata.type":"#microsoft.graph.' + $($PolicySettings.AssignTo) + 'AssignmentTarget"}}]}' } else { '{"assignments":[{"id":"","target":{"@odata.type":"#microsoft.graph.allDevicesAssignmentTarget"}},{"id":"","target":{"@odata.type":"#microsoft.graph.allLicensedUsersAssignmentTarget"}}]}' } - $null = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/deviceManagement/configurationPolicies('$($PolicyRequest.id)')/assign" -tenantid $tenant -type POST -body $AssignBody - Write-LogMessage -headers $Headers -API $APINAME -tenant $($tenant) -message "Assigned policy $($DisplayName) to $($PolicySettings.AssignTo)" -Sev 'Info' - } - "$($tenant): Successfully set Default AV Policy settings" - } + Set-CIPPDefenderAVPolicy -TenantFilter $tenant -PolicySettings $PolicySettings -Headers $Headers -APIName $APIName } if ($ASR) { - # Fallback to block mode - $Mode = $ASR.Mode ?? 'block' - $ASRSettings = switch ($ASR) { - { $_.BlockObfuscatedScripts } { @{'@odata.type' = '#microsoft.graph.deviceManagementConfigurationChoiceSettingInstance' ; settingDefinitionId = 'device_vendor_msft_policy_config_defender_attacksurfacereductionrules_blockexecutionofpotentiallyobfuscatedscripts' ; choiceSettingValue = @{ '@odata.type' = '#microsoft.graph.deviceManagementConfigurationchoiceSettingValue'; ; value = "device_vendor_msft_policy_config_defender_attacksurfacereductionrules_blockexecutionofpotentiallyobfuscatedscripts_$Mode" } } } - { $_.BlockAdobeChild } { @{'@odata.type' = '#microsoft.graph.deviceManagementConfigurationChoiceSettingInstance' ; settingDefinitionId = 'device_vendor_msft_policy_config_defender_attacksurfacereductionrules_blockadobereaderfromcreatingchildprocesses' ; choiceSettingValue = @{ '@odata.type' = '#microsoft.graph.deviceManagementConfigurationchoiceSettingValue'; ; value = "device_vendor_msft_policy_config_defender_attacksurfacereductionrules_blockadobereaderfromcreatingchildprocesses_$Mode" } } } - { $_.BlockWin32Macro } { @{'@odata.type' = '#microsoft.graph.deviceManagementConfigurationChoiceSettingInstance' ; settingDefinitionId = 'device_vendor_msft_policy_config_defender_attacksurfacereductionrules_blockwin32apicallsfromofficemacros' ; choiceSettingValue = @{ '@odata.type' = '#microsoft.graph.deviceManagementConfigurationchoiceSettingValue'; ; value = "device_vendor_msft_policy_config_defender_attacksurfacereductionrules_blockwin32apicallsfromofficemacros_$Mode" } } } - { $_.BlockCredentialStealing } { @{'@odata.type' = '#microsoft.graph.deviceManagementConfigurationChoiceSettingInstance' ; settingDefinitionId = 'device_vendor_msft_policy_config_defender_attacksurfacereductionrules_blockcredentialstealingfromwindowslocalsecurityauthoritysubsystem' ; choiceSettingValue = @{'@odata.type' = '#microsoft.graph.deviceManagementConfigurationchoiceSettingValue' ; value = "device_vendor_msft_policy_config_defender_attacksurfacereductionrules_blockcredentialstealingfromwindowslocalsecurityauthoritysubsystem_$Mode" } } } - { $_.BlockPSExec } { @{ '@odata.type' = '#microsoft.graph.deviceManagementConfigurationChoiceSettingInstance'; settingDefinitionId = 'device_vendor_msft_policy_config_defender_attacksurfacereductionrules_blockprocesscreationsfrompsexecandwmicommands'; choiceSettingValue = @{ '@odata.type' = '#microsoft.graph.deviceManagementConfigurationchoiceSettingValue' ; value = "device_vendor_msft_policy_config_defender_attacksurfacereductionrules_blockprocesscreationsfrompsexecandwmicommands_$Mode" } } } - { $_.WMIPersistence } { @{'@odata.type' = '#microsoft.graph.deviceManagementConfigurationChoiceSettingInstance' ; settingDefinitionId = 'device_vendor_msft_policy_config_defender_attacksurfacereductionrules_blockpersistencethroughwmieventsubscription' ; choiceSettingValue = @{'@odata.type' = '#microsoft.graph.deviceManagementConfigurationchoiceSettingValue' ; value = "device_vendor_msft_policy_config_defender_attacksurfacereductionrules_blockpersistencethroughwmieventsubscription_$Mode" } } } - { $_.BlockOfficeExes } { @{ '@odata.type' = '#microsoft.graph.deviceManagementConfigurationChoiceSettingInstance' ; settingDefinitionId = 'device_vendor_msft_policy_config_defender_attacksurfacereductionrules_blockofficeapplicationsfromcreatingexecutablecontent' ; choiceSettingValue = @{ '@odata.type' = '#microsoft.graph.deviceManagementConfigurationchoiceSettingValue' ; value = "device_vendor_msft_policy_config_defender_attacksurfacereductionrules_blockofficeapplicationsfromcreatingexecutablecontent_$Mode" } } } - { $_.BlockOfficeApps } { @{ '@odata.type' = '#microsoft.graph.deviceManagementConfigurationChoiceSettingInstance' ; settingDefinitionId = 'device_vendor_msft_policy_config_defender_attacksurfacereductionrules_blockofficeapplicationsfrominjectingcodeintootherprocesses' ; choiceSettingValue = @{'@odata.type' = '#microsoft.graph.deviceManagementConfigurationchoiceSettingValue' ; value = "device_vendor_msft_policy_config_defender_attacksurfacereductionrules_blockofficeapplicationsfrominjectingcodeintootherprocesses_$Mode" } } } - { $_.BlockYoungExe } { @{'@odata.type' = '#microsoft.graph.deviceManagementConfigurationChoiceSettingInstance' ; settingDefinitionId = 'device_vendor_msft_policy_config_defender_attacksurfacereductionrules_blockexecutablefilesrunningunlesstheymeetprevalenceagetrustedlistcriterion' ; choiceSettingValue = @{ '@odata.type' = '#microsoft.graph.deviceManagementConfigurationchoiceSettingValue' ; value = "device_vendor_msft_policy_config_defender_attacksurfacereductionrules_blockexecutablefilesrunningunlesstheymeetprevalenceagetrustedlistcriterion_$Mode" } } } - { $_.blockJSVB } { @{'@odata.type' = '#microsoft.graph.deviceManagementConfigurationChoiceSettingInstance' ; settingDefinitionId = 'device_vendor_msft_policy_config_defender_attacksurfacereductionrules_blockjavascriptorvbscriptfromlaunchingdownloadedexecutablecontent' ; choiceSettingValue = @{ '@odata.type' = '#microsoft.graph.deviceManagementConfigurationchoiceSettingValue' ; value = "device_vendor_msft_policy_config_defender_attacksurfacereductionrules_blockjavascriptorvbscriptfromlaunchingdownloadedexecutablecontent_$Mode" } } } - { $_.BlockWebshellForServers } { @{'@odata.type' = '#microsoft.graph.deviceManagementConfigurationChoiceSettingInstance' ; settingDefinitionId = 'device_vendor_msft_policy_config_defender_attacksurfacereductionrules_blockwebshellcreationforservers' ; choiceSettingValue = @{ '@odata.type' = '#microsoft.graph.deviceManagementConfigurationchoiceSettingValue' ; value = "device_vendor_msft_policy_config_defender_attacksurfacereductionrules_blockwebshellcreationforservers_$Mode" } } } - { $_.blockOfficeComChild } { @{'@odata.type' = '#microsoft.graph.deviceManagementConfigurationChoiceSettingInstance' ; settingDefinitionId = 'device_vendor_msft_policy_config_defender_attacksurfacereductionrules_blockofficecommunicationappfromcreatingchildprocesses' ; choiceSettingValue = @{'@odata.type' = '#microsoft.graph.deviceManagementConfigurationchoiceSettingValue' ; value = "device_vendor_msft_policy_config_defender_attacksurfacereductionrules_blockofficecommunicationappfromcreatingchildprocesses_$Mode" } } } - { $_.BlockSystemTools } { @{'@odata.type' = '#microsoft.graph.deviceManagementConfigurationChoiceSettingInstance' ; settingDefinitionId = 'device_vendor_msft_policy_config_defender_attacksurfacereductionrules_blockuseofcopiedorimpersonatedsystemtools' ; choiceSettingValue = @{'@odata.type' = '#microsoft.graph.deviceManagementConfigurationchoiceSettingValue' ; value = "device_vendor_msft_policy_config_defender_attacksurfacereductionrules_blockuseofcopiedorimpersonatedsystemtools_$Mode" } } } - { $_.blockOfficeChild } { @{'@odata.type' = '#microsoft.graph.deviceManagementConfigurationChoiceSettingInstance' ; settingDefinitionId = 'device_vendor_msft_policy_config_defender_attacksurfacereductionrules_blockallofficeapplicationsfromcreatingchildprocesses' ; choiceSettingValue = @{ '@odata.type' = '#microsoft.graph.deviceManagementConfigurationchoiceSettingValue' ; value = "device_vendor_msft_policy_config_defender_attacksurfacereductionrules_blockallofficeapplicationsfromcreatingchildprocesses_$Mode" } } } - { $_.BlockUntrustedUSB } { @{ '@odata.type' = '#microsoft.graph.deviceManagementConfigurationChoiceSettingInstance' ; settingDefinitionId = 'device_vendor_msft_policy_config_defender_attacksurfacereductionrules_blockuntrustedunsignedprocessesthatrunfromusb'; choiceSettingValue = @{ '@odata.type' = '#microsoft.graph.deviceManagementConfigurationchoiceSettingValue' ; value = "device_vendor_msft_policy_config_defender_attacksurfacereductionrules_blockuntrustedunsignedprocessesthatrunfromusb_$Mode" } } } - { $_.EnableRansomwareVac } { @{ '@odata.type' = '#microsoft.graph.deviceManagementConfigurationChoiceSettingInstance'; settingDefinitionId = 'device_vendor_msft_policy_config_defender_attacksurfacereductionrules_useadvancedprotectionagainstransomware'; choiceSettingValue = @{ '@odata.type' = '#microsoft.graph.deviceManagementConfigurationchoiceSettingValue'; value = "device_vendor_msft_policy_config_defender_attacksurfacereductionrules_useadvancedprotectionagainstransomware_$Mode" } } } - { $_.BlockExesMail } { @{ '@odata.type' = '#microsoft.graph.deviceManagementConfigurationChoiceSettingInstance'; settingDefinitionId = 'device_vendor_msft_policy_config_defender_attacksurfacereductionrules_blockexecutablecontentfromemailclientandwebmail' ; choiceSettingValue = @{ '@odata.type' = '#microsoft.graph.deviceManagementConfigurationchoiceSettingValue' ; value = "device_vendor_msft_policy_config_defender_attacksurfacereductionrules_blockexecutablecontentfromemailclientandwebmail_$Mode" } } } - { $_.BlockUnsignedDrivers } { @{'@odata.type' = '#microsoft.graph.deviceManagementConfigurationChoiceSettingInstance'; settingDefinitionId = 'device_vendor_msft_policy_config_defender_attacksurfacereductionrules_blockabuseofexploitedvulnerablesigneddrivers'; choiceSettingValue = @{'@odata.type' = '#microsoft.graph.deviceManagementConfigurationchoiceSettingValue'; value = "device_vendor_msft_policy_config_defender_attacksurfacereductionrules_blockabuseofexploitedvulnerablesigneddrivers_$Mode" } } } - { $_.BlockSafeMode } { @{'@odata.type' = '#microsoft.graph.deviceManagementConfigurationChoiceSettingInstance'; settingDefinitionId = 'device_vendor_msft_policy_config_defender_attacksurfacereductionrules_blockrebootingmachineinsafemode'; choiceSettingValue = @{'@odata.type' = '#microsoft.graph.deviceManagementConfigurationchoiceSettingValue'; value = "device_vendor_msft_policy_config_defender_attacksurfacereductionrules_blockrebootingmachineinsafemode_$Mode" } } } - - } - $ASRbody = ConvertTo-Json -Depth 15 -Compress -InputObject @{ - name = 'ASR Default rules' - description = '' - platforms = 'windows10' - technologies = 'mdm,microsoftSense' - roleScopeTagIds = @('0') - templateReference = @{templateId = 'e8c053d6-9f95-42b1-a7f1-ebfd71c67a4b_1' } - settings = @(@{ - '@odata.type' = '#microsoft.graph.deviceManagementConfigurationSetting' - settingInstance = @{ - '@odata.type' = '#microsoft.graph.deviceManagementConfigurationGroupSettingCollectionInstance' - settingDefinitionId = 'device_vendor_msft_policy_config_defender_attacksurfacereductionrules' - groupSettingCollectionValue = @(@{children = $ASRSettings }) - settingInstanceTemplateReference = @{settingInstanceTemplateId = '19600663-e264-4c02-8f55-f2983216d6d7' } - } - }) - } - $CheckExististingASR = New-GraphGETRequest -uri 'https://graph.microsoft.com/beta/deviceManagement/configurationPolicies' -tenantid $tenant - if ('ASR Default rules' -in $CheckExististingASR.Name) { - "$($tenant): ASR Policy already exists. Skipping" - } else { - Write-Host $ASRbody - if (($ASRSettings | Measure-Object).Count -gt 0) { - $ASRRequest = New-GraphPOSTRequest -uri 'https://graph.microsoft.com/beta/deviceManagement/configurationPolicies' -tenantid $tenant -type POST -body $ASRbody - Write-Host ($ASRRequest.id) - if ($ASR.AssignTo -and $ASR.AssignTo -ne 'none') { - $AssignBody = if ($ASR.AssignTo -ne 'AllDevicesAndUsers') { '{"assignments":[{"id":"","target":{"@odata.type":"#microsoft.graph.' + $($asr.AssignTo) + 'AssignmentTarget"}}]}' } else { '{"assignments":[{"id":"","target":{"@odata.type":"#microsoft.graph.allDevicesAssignmentTarget"}},{"id":"","target":{"@odata.type":"#microsoft.graph.allLicensedUsersAssignmentTarget"}}]}' } - $null = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/deviceManagement/configurationPolicies('$($ASRRequest.id)')/assign" -tenantid $tenant -type POST -body $AssignBody - Write-LogMessage -headers $Headers -API $APINAME -tenant $($tenant) -message "Assigned policy $($DisplayName) to $($ASR.AssignTo)" -Sev 'Info' - } - "$($tenant): Successfully added ASR Settings" - } - } + Set-CIPPDefenderASRPolicy -TenantFilter $tenant -ASR $ASR -Headers $Headers -APIName $APIName } if ($EDR) { - $EDRSettings = switch ($EDR) { - { $_.SampleSharing } { - @{ - '@odata.type' = '#microsoft.graph.deviceManagementConfigurationSetting' - settingInstance = @{ - '@odata.type' = '#microsoft.graph.deviceManagementConfigurationChoiceSettingInstance' - settingDefinitionId = 'device_vendor_msft_windowsadvancedthreatprotection_configuration_samplesharing' - choiceSettingValue = @{ - settingValueTemplateReference = @{settingValueTemplateId = 'f72c326c-7c5b-4224-b890-0b9b54522bd9' } - '@odata.type' = '#microsoft.graph.deviceManagementConfigurationChoiceSettingValue' - 'value' = 'device_vendor_msft_windowsadvancedthreatprotection_configuration_samplesharing_1' - } - settingInstanceTemplateReference = @{settingInstanceTemplateId = '6998c81e-2814-4f5e-b492-a6159128a97b' } - } - } - } - - { $_.Config } { - @{ - '@odata.type' = '#microsoft.graph.deviceManagementConfigurationSetting' - settingInstance = @{ - '@odata.type' = '#microsoft.graph.deviceManagementConfigurationChoiceSettingInstance' - settingDefinitionId = 'device_vendor_msft_windowsadvancedthreatprotection_configurationtype' - choiceSettingValue = @{ - '@odata.type' = '#microsoft.graph.deviceManagementConfigurationChoiceSettingValue' - 'value' = 'device_vendor_msft_windowsadvancedthreatprotection_configurationtype_autofromconnector' - settingValueTemplateReference = @{settingValueTemplateId = 'e5c7c98c-c854-4140-836e-bd22db59d651' } - children = @(@{'@odata.type' = '#microsoft.graph.deviceManagementConfigurationSimpleSettingInstance' ; settingDefinitionId = 'device_vendor_msft_windowsadvancedthreatprotection_onboarding_fromconnector' ; simpleSettingValue = @{'@odata.type' = '#microsoft.graph.deviceManagementConfigurationSecretSettingValue' ; value = 'Microsoft ATP connector enabled'; valueState = 'NotEncrypted' } } ) - } - - settingInstanceTemplateReference = @{settingInstanceTemplateId = '23ab0ea3-1b12-429a-8ed0-7390cf699160' } - } - } - - } - } - if (($EDRSettings | Measure-Object).Count -gt 0) { - $EDRbody = ConvertTo-Json -Depth 15 -Compress -InputObject @{ - name = 'EDR Configuration' - description = '' - platforms = 'windows10' - technologies = 'mdm,microsoftSense' - roleScopeTagIds = @('0') - templateReference = @{templateId = '0385b795-0f2f-44ac-8602-9f65bf6adede_1' } - settings = @($EDRSettings) - } - Write-Host ( $EDRbody) - $CheckExististingEDR = New-GraphGETRequest -uri 'https://graph.microsoft.com/beta/deviceManagement/configurationPolicies' -tenantid $tenant | Where-Object -Property Name -EQ 'EDR Configuration' - if ('EDR Configuration' -in $CheckExististingEDR.Name) { - "$($tenant): EDR Policy already exists. Skipping" - } else { - $EDRRequest = New-GraphPOSTRequest -uri 'https://graph.microsoft.com/beta/deviceManagement/configurationPolicies' -tenantid $tenant -type POST -body $EDRbody - # Assign if needed - if ($EDR.AssignTo -and $EDR.AssignTo -ne 'none') { - $AssignBody = if ($EDR.AssignTo -ne 'AllDevicesAndUsers') { '{"assignments":[{"id":"","target":{"@odata.type":"#microsoft.graph.' + $($EDR.AssignTo) + 'AssignmentTarget"}}]}' } else { '{"assignments":[{"id":"","target":{"@odata.type":"#microsoft.graph.allDevicesAssignmentTarget"}},{"id":"","target":{"@odata.type":"#microsoft.graph.allLicensedUsersAssignmentTarget"}}]}' } - $null = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/deviceManagement/configurationPolicies('$($EDRRequest.id)')/assign" -tenantid $tenant -type POST -body $AssignBody - Write-LogMessage -headers $Headers -API $APIName -tenant $($tenant) -message "Assigned EDR policy $($DisplayName) to $($EDR.AssignTo)" -Sev 'Info' - } - "$($tenant): Successfully added EDR Settings" - } - } + Set-CIPPDefenderEDRPolicy -TenantFilter $tenant -EDR $EDR -Headers $Headers -APIName $APIName } - # Exclusion Policy Section if ($DefenderExclusions) { - $ExclusionAssignTo = $DefenderExclusions.AssignTo - if ($DefenderExclusions.excludedExtensions) { - $ExcludedExtensions = $DefenderExclusions.excludedExtensions | Where-Object { $_ -and $_.Trim() } | ForEach-Object { - @{ '@odata.type' = '#microsoft.graph.deviceManagementConfigurationStringSettingValue'; value = $_ } - } - } - if ($DefenderExclusions.excludedPaths) { - $ExcludedPaths = $DefenderExclusions.excludedPaths | Where-Object { $_ -and $_.Trim() } | ForEach-Object { - @{ '@odata.type' = '#microsoft.graph.deviceManagementConfigurationStringSettingValue'; value = $_ } - } - } - if ($DefenderExclusions.excludedProcesses) { - $ExcludedProcesses = $DefenderExclusions.excludedProcesses | Where-Object { $_ -and $_.Trim() } | ForEach-Object { - @{ '@odata.type' = '#microsoft.graph.deviceManagementConfigurationStringSettingValue'; value = $_ } - } - } - $ExclusionSettings = [System.Collections.Generic.List[System.Object]]::new() - if ($ExcludedExtensions.Count -gt 0) { - $ExclusionSettings.Add(@{ - id = '2' - settingInstance = @{ - '@odata.type' = '#microsoft.graph.deviceManagementConfigurationSimpleSettingCollectionInstance' - settingDefinitionId = 'device_vendor_msft_policy_config_defender_excludedextensions' - settingInstanceTemplateReference = @{ settingInstanceTemplateId = 'c203725b-17dc-427b-9470-673a2ce9cd5e' } - simpleSettingCollectionValue = @($ExcludedExtensions) - } - }) - } - if ($ExcludedPaths.Count -gt 0) { - $ExclusionSettings.Add(@{ - id = '1' - settingInstance = @{ - '@odata.type' = '#microsoft.graph.deviceManagementConfigurationSimpleSettingCollectionInstance' - settingDefinitionId = 'device_vendor_msft_policy_config_defender_excludedpaths' - settingInstanceTemplateReference = @{ settingInstanceTemplateId = 'aaf04adc-c639-464f-b4a7-152e784092e8' } - simpleSettingCollectionValue = @($ExcludedPaths) - } - }) - } - if ($ExcludedProcesses.Count -gt 0) { - $ExclusionSettings.Add(@{ - id = '0' - settingInstance = @{ - '@odata.type' = '#microsoft.graph.deviceManagementConfigurationSimpleSettingCollectionInstance' - settingDefinitionId = 'device_vendor_msft_policy_config_defender_excludedprocesses' - settingInstanceTemplateReference = @{ settingInstanceTemplateId = '96b046ed-f138-4250-9ae0-b0772a93d16f' } - simpleSettingCollectionValue = @($ExcludedProcesses) - } - }) - } - if ($ExclusionSettings.Count -gt 0) { - $ExclusionBody = ConvertTo-Json -Depth 15 -Compress -InputObject @{ - name = 'Default AV Exclusion Policy' - displayName = 'Default AV Exclusion Policy' - settings = @($ExclusionSettings) - platforms = 'windows10' - technologies = 'mdm,microsoftSense' - templateReference = @{ - templateId = '45fea5e9-280d-4da1-9792-fb5736da0ca9_1' - templateFamily = 'endpointSecurityAntivirus' - templateDisplayName = 'Microsoft Defender Antivirus exclusions' - templateDisplayVersion = 'Version 1' - } - } - $CheckExistingExclusion = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/deviceManagement/configurationPolicies' -tenantid $tenant - if ('Default AV Exclusion Policy' -in $CheckExistingExclusion.Name) { - "$($tenant): Exclusion Policy already exists. Skipping" - } else { - $ExclusionRequest = New-GraphPOSTRequest -uri 'https://graph.microsoft.com/beta/deviceManagement/configurationPolicies' -tenantid $tenant -type POST -body $ExclusionBody - if ($ExclusionAssignTo -and $ExclusionAssignTo -ne 'none') { - $AssignBody = if ($ExclusionAssignTo -ne 'AllDevicesAndUsers') { '{"assignments":[{"id":"","target":{"@odata.type":"#microsoft.graph.' + $($ExclusionAssignTo) + 'AssignmentTarget"}}]}' } else { '{"assignments":[{"id":"","target":{"@odata.type":"#microsoft.graph.allDevicesAssignmentTarget"}},{"id":"","target":{"@odata.type":"#microsoft.graph.allLicensedUsersAssignmentTarget"}}]}' } - $null = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/deviceManagement/configurationPolicies('$($ExclusionRequest.id)')/assign" -tenantid $tenant -type POST -body $AssignBody - Write-LogMessage -headers $Headers -API $APIName -tenant $tenant -message "Assigned Exclusion policy to $($ExclusionAssignTo)" -Sev 'Info' - } - "$($tenant): Successfully set Default AV Exclusion Policy settings" - } - } + Set-CIPPDefenderExclusionPolicy -TenantFilter $tenant -DefenderExclusions $DefenderExclusions -Headers $Headers -APIName $APIName } } catch { "Failed to add policy for $($tenant): $($_.Exception.Message)" - Write-LogMessage -headers $Headers -API $APIName -tenant $tenant -message "Failed adding policy $($DisplayName). Error: $($_.Exception.Message)" -Sev 'Error' + Write-LogMessage -headers $Headers -API $APIName -tenant $tenant -message "Failed adding Defender policy. Error: $($_.Exception.Message)" -Sev 'Error' continue } - } - return ([HttpResponseContext]@{ StatusCode = [HttpStatusCode]::OK Body = @{'Results' = @($Results) } }) } + diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-AddUserDefaults.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-AddUserDefaults.ps1 index 63e6e2b50cfc..64361ed734a5 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-AddUserDefaults.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-AddUserDefaults.ps1 @@ -31,6 +31,14 @@ function Invoke-AddUserDefaults { $Request.Body.usernameFormat.value } + $UsernameSpaceHandling = if ($Request.Body.usernameSpaceHandling -is [string]) { + $Request.Body.usernameSpaceHandling + } else { + $Request.Body.usernameSpaceHandling.value + } + + $UsernameSpaceReplacement = $Request.Body.usernameSpaceReplacement + $PrimDomain = if ($Request.Body.primDomain -is [string]) { $Request.Body.primDomain } else { @@ -73,37 +81,63 @@ function Invoke-AddUserDefaults { $SetSponsor = $Request.Body.setSponsor $CopyFrom = $Request.Body.copyFrom + # Fetch group memberships from source user if provided + $GroupMemberships = @() + if ($Request.Body.sourceUserId -and $TenantFilter) { + try { + Write-Host "Fetching group memberships from source user: $($Request.Body.sourceUserId)" + $SourceGroups = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/users/$($Request.Body.sourceUserId)/memberOf" -tenantid $TenantFilter + $GroupMemberships = @($SourceGroups | Where-Object { + $_.'@odata.type' -eq '#microsoft.graph.group' -and + $_.groupTypes -notcontains 'DynamicMembership' -and + $_.onPremisesSyncEnabled -ne $true + } | ForEach-Object { + @{ + id = $_.id + displayName = $_.displayName + mailEnabled = $_.mailEnabled + groupTypes = $_.groupTypes + } + }) + Write-Host "Found $($GroupMemberships.Count) group memberships to store in template" + } catch { + Write-Warning "Failed to fetch group memberships: $($_.Exception.Message)" + } + } + # Create template object with all fields from CippAddEditUser $TemplateObject = @{ - tenantFilter = $TenantFilter - templateName = $TemplateName - defaultForTenant = [bool]$DefaultForTenant - givenName = $GivenName - surname = $Surname - displayName = $DisplayName - usernameFormat = $UsernameFormat - primDomain = $PrimDomain - addedAliases = $AddedAliases - Autopassword = $Autopassword - password = $Password - MustChangePass = $MustChangePass - usageLocation = $UsageLocation - licenses = $Licenses - removeLicenses = $RemoveLicenses - jobTitle = $JobTitle - streetAddress = $StreetAddress - city = $City - state = $State - postalCode = $PostalCode - country = $Country - companyName = $CompanyName - department = $Department - mobilePhone = $MobilePhone - businessPhones = $BusinessPhones - otherMails = $OtherMails - setManager = $SetManager - setSponsor = $SetSponsor - copyFrom = $CopyFrom + tenantFilter = $TenantFilter + templateName = $TemplateName + defaultForTenant = [bool]$DefaultForTenant + givenName = $GivenName + surname = $Surname + displayName = $DisplayName + usernameFormat = $UsernameFormat + usernameSpaceHandling = $UsernameSpaceHandling + usernameSpaceReplacement = $UsernameSpaceReplacement + primDomain = $PrimDomain + addedAliases = $AddedAliases + Autopassword = $Autopassword + password = $Password + MustChangePass = $MustChangePass + usageLocation = $UsageLocation + licenses = $Licenses + removeLicenses = $RemoveLicenses + jobTitle = $JobTitle + streetAddress = $StreetAddress + city = $City + state = $State + postalCode = $PostalCode + country = $Country + companyName = $CompanyName + department = $Department + mobilePhone = $MobilePhone + businessPhones = $BusinessPhones + otherMails = $OtherMails + setManager = $SetManager + setSponsor = $SetSponsor + copyFrom = $CopyFrom } # Use existing GUID if editing, otherwise generate new one @@ -124,7 +158,7 @@ function Invoke-AddUserDefaults { $Action = if ($Request.Body.GUID) { 'Updated' } else { 'Created' } $Result = "$Action User Default Template '$($TemplateName)' with GUID $GUID" - Write-LogMessage -headers $Headers -API $APIName -message $Result -Sev 'Info' + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $Result -Sev 'Info' $StatusCode = [HttpStatusCode]::OK } catch { 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-ExecUpdateDriftDeviation.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ExecUpdateDriftDeviation.ps1 index 0d6b2b52c592..ac8466631555 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ExecUpdateDriftDeviation.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ExecUpdateDriftDeviation.ps1 @@ -58,6 +58,14 @@ function Invoke-ExecUpdateDriftDeviation { $Settings = $StandardTemplate } else { $StandardTemplate = $StandardTemplate.standardSettings.$Setting + # If the addedComponent values are stored nested under standards. instead of + # flat on the object, promote them to the top level so the standard function can read them. + if ($StandardTemplate.standards -and $StandardTemplate.standards.$Setting) { + foreach ($Prop in $StandardTemplate.standards.$Setting.PSObject.Properties) { + $StandardTemplate | Add-Member -MemberType NoteProperty -Name $Prop.Name -Value $Prop.Value -Force + } + $StandardTemplate.PSObject.Properties.Remove('standards') + } $StandardTemplate | Add-Member -MemberType NoteProperty -Name 'remediate' -Value $true -Force $StandardTemplate | Add-Member -MemberType NoteProperty -Name 'report' -Value $true -Force $Settings = $StandardTemplate 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/Invoke-ListNotificationConfig.ps1 b/Modules/CIPPCore/Public/Entrypoints/Invoke-ListNotificationConfig.ps1 index 4d9f4f11cfdb..a1312b27ad66 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Invoke-ListNotificationConfig.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Invoke-ListNotificationConfig.ps1 @@ -16,7 +16,7 @@ Function Invoke-ListNotificationConfig { $Config = @{} } #$config | Add-Member -NotePropertyValue @() -NotePropertyName 'logsToInclude' -Force - $config.logsToInclude = @(([pscustomobject]$config | Select-Object * -ExcludeProperty schedule, type, tenantid, onepertenant, sendtoIntegration, partitionkey, rowkey, tenant, ETag, email, logsToInclude, Severity, Alert, Info, Error, timestamp, webhook, includeTenantId).psobject.properties.name) + $config.logsToInclude = @(([pscustomobject]$config | Select-Object * -ExcludeProperty schedule, type, tenantid, onepertenant, sendtoIntegration, partitionkey, rowkey, tenant, ETag, email, logsToInclude, Severity, Alert, Info, Error, timestamp, webhook, includeTenantId, UseStandardizedSchema, webhookAuthType, webhookAuthToken, webhookAuthUsername, webhookAuthPassword, webhookAuthHeaderName, webhookAuthHeaderValue, webhookAuthHeaders).psobject.properties.name) if (!$config.logsToInclude) { $config.logsToInclude = @('None') } diff --git a/Modules/CIPPCore/Public/Entrypoints/Invoke-ListRoles.ps1 b/Modules/CIPPCore/Public/Entrypoints/Invoke-ListRoles.ps1 index 5d4eff68a7bd..b56819cc9b16 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Invoke-ListRoles.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Invoke-ListRoles.ps1 @@ -11,10 +11,25 @@ function Invoke-ListRoles { $TenantFilter = $Request.Query.tenantFilter try { - [System.Collections.Generic.List[PSCustomObject]]$Roles = New-GraphGetRequest -uri "https://graph.microsoft.com/v1.0/directoryRoles?`$expand=members" -tenantid $TenantFilter + [System.Collections.Generic.List[PSCustomObject]]$Roles = New-GraphGetRequest -uri 'https://graph.microsoft.com/v1.0/directoryRoles' -tenantid $TenantFilter + + $MemberRequests = $Roles | ForEach-Object { + @{ + id = $_.id + method = 'GET' + url = "/directoryRoles/$($_.id)/members" + } + } + $MemberResponses = New-GraphBulkRequest -Requests $MemberRequests -tenantid $TenantFilter -Version 'v1.0' + + $MemberMap = @{} + foreach ($Response in $MemberResponses) { + $MemberMap[$Response.id] = $Response.body.value + } + $GraphRequest = foreach ($Role in $Roles) { - $Members = if ($Role.members) { - $Role.members | ForEach-Object { [PSCustomObject]@{ + $Members = if ($MemberMap[$Role.id]) { + $MemberMap[$Role.id] | ForEach-Object { [PSCustomObject]@{ displayName = $_.displayName userPrincipalName = $_.userPrincipalName id = $_.id diff --git a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-AuditLogIngestion.ps1 b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-AuditLogIngestion.ps1 index f11d8f9396a3..c1a3be02fde9 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-AuditLogIngestion.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-AuditLogIngestion.ps1 @@ -35,38 +35,38 @@ function Start-AuditLogIngestion { $ConfigEntry } - $TenantList = Get-Tenants -IncludeErrors - $TenantsToProcess = [System.Collections.Generic.List[object]]::new() + $TenantList = Get-Tenants -IncludeErrors + $TenantsToProcess = [System.Collections.Generic.List[object]]::new() - foreach ($Tenant in $TenantList) { - # Check if tenant has any webhook rules and collect content types - $TenantInConfig = $false - $MatchingConfigs = [System.Collections.Generic.List[object]]::new() + foreach ($Tenant in $TenantList) { + # Check if tenant has any webhook rules and collect content types + $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') { - $TenantInConfig = $true - $MatchingConfigs.Add($ConfigEntry) - } - } + 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') { + $TenantInConfig = $true + $MatchingConfigs.Add($ConfigEntry) + } + } - if ($TenantInConfig -and $MatchingConfigs.Count -gt 0) { - # Extract unique content types from webhook rules (e.g., Audit.Exchange, Audit.SharePoint) - $ContentTypes = @($MatchingConfigs | Select-Object -Property type | Where-Object { $_.type } | Sort-Object -Property type -Unique | ForEach-Object { $_.type }) + if ($TenantInConfig -and $MatchingConfigs.Count -gt 0) { + # Extract unique content types from webhook rules (e.g., Audit.Exchange, Audit.SharePoint) + $ContentTypes = @($MatchingConfigs | Select-Object -Property type | Where-Object { $_.type } | Sort-Object -Property type -Unique | ForEach-Object { $_.type }) - if ($ContentTypes.Count -gt 0) { - $TenantsToProcess.Add([PSCustomObject]@{ - defaultDomainName = $Tenant.defaultDomainName - customerId = $Tenant.customerId - ContentTypes = $ContentTypes - }) - } - } - } if ($TenantsToProcess.Count -eq 0) { + if ($ContentTypes.Count -gt 0) { + $TenantsToProcess.Add([PSCustomObject]@{ + defaultDomainName = $Tenant.defaultDomainName + customerId = $Tenant.customerId + ContentTypes = $ContentTypes + }) + } + } + } if ($TenantsToProcess.Count -eq 0) { Write-Information 'No tenants configured for audit log ingestion' return } @@ -74,14 +74,14 @@ function Start-AuditLogIngestion { Write-Information "Audit Log Ingestion: Processing $($TenantsToProcess.Count) tenants" if ($PSCmdlet.ShouldProcess('Start-AuditLogIngestion', 'Starting Audit Log Ingestion')) { - $Queue = New-CippQueueEntry -Name 'Audit Logs Ingestion' -Reference 'AuditLogsIngestion' -TotalTasks $TenantsToProcess.Count - $Batch = $TenantsToProcess | Select-Object @{Name = 'TenantFilter'; Expression = { $_.defaultDomainName } }, @{Name = 'TenantId'; Expression = { $_.customerId } }, @{Name = 'ContentTypes'; Expression = { $_.ContentTypes } }, @{Name = 'QueueId'; Expression = { $Queue.RowKey } }, @{Name = 'FunctionName'; Expression = { 'AuditLogIngestion' } } - $InputObject = [PSCustomObject]@{ + $Queue = New-CippQueueEntry -Name 'Audit Logs Ingestion' -Reference 'AuditLogsIngestion' -TotalTasks $TenantsToProcess.Count + $Batch = $TenantsToProcess | Select-Object @{Name = 'TenantFilter'; Expression = { $_.defaultDomainName } }, @{Name = 'TenantId'; Expression = { $_.customerId } }, @{Name = 'ContentTypes'; Expression = { $_.ContentTypes } }, @{Name = 'QueueId'; Expression = { $Queue.RowKey } }, @{Name = 'FunctionName'; Expression = { 'AuditLogIngestion' } } + $InputObject = [PSCustomObject]@{ OrchestratorName = 'AuditLogsIngestion' Batch = @($Batch) SkipLog = $true } - Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Depth 5 -Compress) + Start-CIPPOrchestrator -InputObject $InputObject Write-Information "Started audit log ingestion orchestration for $($TenantsToProcess.Count) tenants" } } catch { diff --git a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-AuditLogOrchestrator.ps1 b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-AuditLogOrchestrator.ps1 index 7bbba7946425..3dc0df94ba5c 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-AuditLogOrchestrator.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-AuditLogOrchestrator.ps1 @@ -31,7 +31,7 @@ function Start-AuditLogOrchestrator { Batch = @($Batch) SkipLog = $true } - Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Depth 5 -Compress) + Start-CIPPOrchestrator -InputObject $InputObject } } } catch { diff --git a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-AuditLogProcessingOrchestrator.ps1 b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-AuditLogProcessingOrchestrator.ps1 index 2a1427d33028..3ce7172f9f51 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-AuditLogProcessingOrchestrator.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-AuditLogProcessingOrchestrator.ps1 @@ -6,56 +6,16 @@ function Start-AuditLogProcessingOrchestrator { .FUNCTIONALITY Entrypoint #> - [CmdletBinding(SupportsShouldProcess = $true)] + [CmdletBinding()] param() - Write-Information 'Starting audit log processing in batches of 1000, per tenant' - $WebhookCacheTable = Get-CippTable -TableName 'CacheWebhooks' - - $DataTableQuery = @{ - First = 20000 - Skip = 0 - } - - do { - $WebhookCache = Get-CIPPAzDataTableEntity @WebhookCacheTable @DataTableQuery - $TenantGroups = $WebhookCache | Group-Object -Property PartitionKey - - if ($TenantGroups) { - Write-Information "Processing webhook cache for $($TenantGroups.Count) tenants" - #Write-Warning "AuditLogJobs are: $($TenantGroups.Count) tenants. Tenants: $($TenantGroups.name | ConvertTo-Json -Compress) " - #Write-Warning "Here are the groups: $($TenantGroups | ConvertTo-Json -Compress)" - $ProcessQueue = New-CippQueueEntry -Name 'Audit Logs Process' -Reference 'AuditLogsProcess' -TotalTasks ($TenantGroups | Measure-Object -Property Count -Sum).Sum - $ProcessBatch = foreach ($TenantGroup in $TenantGroups) { - $TenantFilter = $TenantGroup.Name - $RowIds = @($TenantGroup.Group.RowKey) - for ($i = 0; $i -lt $RowIds.Count; $i += 500) { - Write-Host "Processing $TenantFilter with $($RowIds.Count) row IDs. We're processing id $($RowIds[$i]) to $($RowIds[[Math]::Min($i + 499, $RowIds.Count - 1)])" - $BatchRowIds = $RowIds[$i..([Math]::Min($i + 499, $RowIds.Count - 1))] - [PSCustomObject]@{ - TenantFilter = $TenantFilter - RowIds = $BatchRowIds - QueueId = $ProcessQueue.RowKey - FunctionName = 'AuditLogTenantProcess' - } - } - } - if ($ProcessBatch) { - $ProcessInputObject = [PSCustomObject]@{ - OrchestratorName = 'AuditLogTenantProcess' - Batch = @($ProcessBatch) - SkipLog = $true - } - Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($ProcessInputObject | ConvertTo-Json -Depth 5 -Compress) - Write-Information "Started audit log processing orchestration with $($ProcessBatch.Count) batches" - } + Write-Information 'Starting audit log processing orchestrator' + $InputObject = [PSCustomObject]@{ + OrchestratorName = 'AuditLogTenantProcess' + QueueFunction = [PSCustomObject]@{ + FunctionName = 'AuditLogProcessingBatch' } - - if ($WebhookCache.Count -lt 20000) { - Write-Information 'No more rows to process' - break - } - Write-Information "Processed $($WebhookCache.Count) rows" - $DataTableQuery.Skip += 20000 - Write-Information "Getting next batch of $($DataTableQuery.First) rows" - } while ($WebhookCache.Count -eq 20000) + SkipLog = $true + } + Start-CIPPOrchestrator -InputObject $InputObject + Write-Information 'Audit log processing orchestrator started' } diff --git a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-AuditLogSearchCreation.ps1 b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-AuditLogSearchCreation.ps1 index a8c64da33fec..655d8ebb26f0 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) } @@ -65,7 +68,7 @@ function Start-AuditLogSearchCreation { OrchestratorName = 'AuditLogSearchCreation' SkipLog = $true } - Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Depth 5 -Compress) + Start-CIPPOrchestrator -InputObject $InputObject Write-Information "Started Audit Log search creation orchestrator with $($Batch.Count) tenants" } else { Write-Information 'No tenants found for Audit Log search creation' diff --git a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-CIPPOrchestrator.ps1 b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-CIPPOrchestrator.ps1 index ef944134eef3..148e851b9e34 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-CIPPOrchestrator.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-CIPPOrchestrator.ps1 @@ -34,6 +34,7 @@ function Start-CIPPOrchestrator { [switch]$CallerIsQueueTrigger ) $OrchestratorTable = Get-CippTable -TableName 'CippOrchestratorInput' + $BatchTable = Get-CippTable -TableName 'CippOrchestratorBatch' # If already running in processor context (e.g., timer trigger) and we have an InputObject, # start orchestration directly without queuing @@ -42,6 +43,29 @@ function Start-CIPPOrchestrator { if ($InputObject -and -not $OrchestratorTriggerDisabled) { Write-Information 'Running in processor context - starting orchestration directly' + if ($InputObject.Batch) { + # Store batch items separately to enable querying and tracking + $BatchGuid = (New-Guid).Guid.ToString() + foreach ($BatchItem in $InputObject.Batch) { + $BatchEntity = @{ + PartitionKey = $BatchGuid + RowKey = (New-Guid).Guid.ToString() + BatchItem = [string]($BatchItem | ConvertTo-Json -Depth 10 -Compress) + } + Add-CIPPAzDataTableEntity @BatchTable -Entity $BatchEntity -Force + } + + # Remove batch from main input object to reduce size + $InputObject.PSObject.Properties.Remove('Batch') + + # Add queue function reference to retrieve batch items in orchestrator + $InputObject | Add-Member -NotePropertyName 'QueueFunction' -NotePropertyValue @{ + FunctionName = 'OrchestratorBatchItems' + Parameters = @{ + BatchId = $BatchGuid + } + } + } try { $InstanceId = Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Depth 10 -Compress) Write-Information "Orchestration started with instance ID: $InstanceId" @@ -70,7 +94,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 { @@ -88,6 +112,29 @@ function Start-CIPPOrchestrator { # Store the input object in table storage $Guid = (New-Guid).Guid.ToString() + if ($InputObject.Batch) { + # Store batch items separately to enable querying and tracking + foreach ($BatchItem in $InputObject.Batch) { + $BatchEntity = @{ + PartitionKey = $Guid + RowKey = (New-Guid).Guid.ToString() + BatchItem = [string]($BatchItem | ConvertTo-Json -Depth 10 -Compress) + } + Add-CIPPAzDataTableEntity @BatchTable -Entity $BatchEntity -Force + } + + # Remove batch from main input object to reduce size + $InputObject.PSObject.Properties.Remove('Batch') + + # Add queue function reference to retrieve batch items in orchestrator + $InputObject.QueueFunction = @{ + FunctionName = 'OrchestratorBatchItems' + Parameters = @{ + BatchId = $Guid + } + } + } + $StoredInput = @{ PartitionKey = 'Input' RowKey = $Guid diff --git a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-SchedulerOrchestrator.ps1 b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-SchedulerOrchestrator.ps1 index 3d5d8ff1ea00..ffe617013723 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-SchedulerOrchestrator.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-SchedulerOrchestrator.ps1 @@ -61,6 +61,6 @@ function Start-SchedulerOrchestrator { } if ($PSCmdlet.ShouldProcess('Start-ScheduleOrchestrator', 'Starting Scheduler Orchestrator')) { - Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Depth 5 -Compress) + Start-CIPPOrchestrator -InputObject $InputObject } } diff --git a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-TenantDynamicGroupOrchestrator.ps1 b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-TenantDynamicGroupOrchestrator.ps1 index 49d1e6d6e12d..34466083897d 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-TenantDynamicGroupOrchestrator.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-TenantDynamicGroupOrchestrator.ps1 @@ -31,7 +31,7 @@ function Start-TenantDynamicGroupOrchestrator { SkipLog = $true } if ($PSCmdlet.ShouldProcess('Start-TenantDynamicGroupOrchestrator', 'Starting Tenant Dynamic Group Orchestrator')) { - Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Depth 5 -Compress) + Start-CIPPOrchestrator -InputObject $InputObject } } else { Write-Information 'No tenants require permissions update' diff --git a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-UpdatePermissionsOrchestrator.ps1 b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-UpdatePermissionsOrchestrator.ps1 index 0ccd44cd0c62..ea206ff721cf 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-UpdatePermissionsOrchestrator.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-UpdatePermissionsOrchestrator.ps1 @@ -63,7 +63,7 @@ function Start-UpdatePermissionsOrchestrator { OrchestratorName = 'UpdatePermissionsOrchestrator' Batch = @($TenantBatch) } - Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Depth 5 -Compress) + Start-CIPPOrchestrator -InputObject $InputObject } else { Write-Information 'No tenants require permissions update' } diff --git a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-UserTasksOrchestrator.ps1 b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-UserTasksOrchestrator.ps1 index 7601c1d7222c..50cde233df00 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-UserTasksOrchestrator.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-UserTasksOrchestrator.ps1 @@ -198,7 +198,7 @@ function Start-UserTasksOrchestrator { if ($PSCmdlet.ShouldProcess('Start-UserTasksOrchestrator', 'Starting Single-Tenant Tasks Orchestrator')) { try { - $OrchestratorId = Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Depth 10 -Compress) + $OrchestratorId = Start-CIPPOrchestrator -InputObject $InputObject Write-Information "Single-tenant orchestrator started for $TenantName with ID: $OrchestratorId" } catch { Write-Warning "Failed to start single-tenant orchestrator for $TenantName : $($_.Exception.Message)" @@ -257,7 +257,7 @@ function Start-UserTasksOrchestrator { if ($PSCmdlet.ShouldProcess('Start-UserTasksOrchestrator', 'Starting Multi-Tenant Task Orchestrator')) { try { - $OrchestratorId = Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Depth 10 -Compress) + $OrchestratorId = Start-CIPPOrchestrator -InputObject $InputObject Write-Information "Multi-tenant orchestrator started for $($ParentTask.Name) with ID: $OrchestratorId" } catch { Write-Warning "Failed to start multi-tenant orchestrator for $($ParentTask.Name): $($_.Exception.Message)" diff --git a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-WebhookOrchestrator.ps1 b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-WebhookOrchestrator.ps1 index 1ba797d3e10d..205c722afffb 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-WebhookOrchestrator.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-WebhookOrchestrator.ps1 @@ -30,8 +30,7 @@ function Start-WebhookOrchestrator { SkipLog = $true } if ($PSCmdlet.ShouldProcess('Start-WebhookOrchestrator', 'Starting Webhook Orchestrator')) { - Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Depth 5 -Compress) - + Start-CIPPOrchestrator -InputObject $InputObject } } catch { Write-LogMessage -API 'Webhooks' -message 'Error processing webhooks' -sev Error -LogData (Get-CippException -Exception $_) diff --git a/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-TableCleanup.ps1 b/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-TableCleanup.ps1 index 771e9d66363c..cb6a46fed8e4 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-TableCleanup.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-TableCleanup.ps1 @@ -98,5 +98,5 @@ function Start-TableCleanup { SkipLog = $true } - Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Depth 5 -Compress) + Start-CIPPOrchestrator -InputObject $InputObject } 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-CIPPOutOfOffice.ps1 b/Modules/CIPPCore/Public/Get-CIPPOutOfOffice.ps1 index c5b0af944f3c..fd80a389be8f 100644 --- a/Modules/CIPPCore/Public/Get-CIPPOutOfOffice.ps1 +++ b/Modules/CIPPCore/Public/Get-CIPPOutOfOffice.ps1 @@ -10,11 +10,17 @@ function Get-CIPPOutOfOffice { try { $OutOfOffice = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-MailboxAutoReplyConfiguration' -cmdParams @{Identity = $UserID } -Anchor $UserID $Results = @{ - AutoReplyState = $OutOfOffice.AutoReplyState - StartTime = $OutOfOffice.StartTime.ToString('yyyy-MM-dd HH:mm') - EndTime = $OutOfOffice.EndTime.ToString('yyyy-MM-dd HH:mm') - InternalMessage = $OutOfOffice.InternalMessage - ExternalMessage = $OutOfOffice.ExternalMessage + AutoReplyState = $OutOfOffice.AutoReplyState + StartTime = $OutOfOffice.StartTime ? $OutOfOffice.StartTime.ToString('yyyy-MM-dd HH:mm') : $null + EndTime = $OutOfOffice.EndTime ? $OutOfOffice.EndTime.ToString('yyyy-MM-dd HH:mm') : $null + InternalMessage = $OutOfOffice.InternalMessage + ExternalMessage = $OutOfOffice.ExternalMessage + CreateOOFEvent = $OutOfOffice.CreateOOFEvent + OOFEventSubject = $OutOfOffice.OOFEventSubject + AutoDeclineFutureRequestsWhenOOF = $OutOfOffice.AutoDeclineFutureRequestsWhenOOF + DeclineEventsForScheduledOOF = $OutOfOffice.DeclineEventsForScheduledOOF + DeclineAllEventsForScheduledOOF = $OutOfOffice.DeclineAllEventsForScheduledOOF + DeclineMeetingMessage = $OutOfOffice.DeclineMeetingMessage } | ConvertTo-Json return $Results } catch { 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/New-CIPPGroup.ps1 b/Modules/CIPPCore/Public/New-CIPPGroup.ps1 index ea08927acaa8..aa014c8f3b80 100644 --- a/Modules/CIPPCore/Public/New-CIPPGroup.ps1 +++ b/Modules/CIPPCore/Public/New-CIPPGroup.ps1 @@ -78,10 +78,13 @@ function New-CIPPGroup { } # Determine if we should generate a mailNickname with a GUID, or use the username field - if (-not $GroupObject.Username) { + if (-not $GroupObject.Username -or $NormalizedGroupType -in @('Generic', 'AzureRole')) { $MailNickname = (New-Guid).guid.substring(0, 10) } else { - $MailNickname = $GroupObject.Username + $MailNickname = ($GroupObject.Username -split '@')[0] -replace '[^a-zA-Z0-9_-]', '' + if ([String]::IsNullOrEmpty($MailNickname)) { + $MailNickname = (New-Guid).guid + } } Write-LogMessage -API $APIName -tenant $TenantFilter -message "Creating group $($GroupObject.displayName) of type $NormalizedGroupType$(if ($NeedsEmail) { " with email $Email" })" -Sev Info diff --git a/Modules/CIPPCore/Public/New-CIPPStandardizedWebhookSchema.ps1 b/Modules/CIPPCore/Public/New-CIPPStandardizedWebhookSchema.ps1 new file mode 100644 index 000000000000..77ae5aa73b6f --- /dev/null +++ b/Modules/CIPPCore/Public/New-CIPPStandardizedWebhookSchema.ps1 @@ -0,0 +1,84 @@ +function New-CIPPStandardizedWebhookSchema { + <# + .SYNOPSIS + Builds a standardized webhook alert payload. + + .DESCRIPTION + Converts legacy alert payloads (string/object/array) into a stable, versioned JSON schema + for webhook consumers. + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Title, + + [Parameter(Mandatory = $true)] + [string]$TenantFilter, + + [Parameter(Mandatory = $false)] + $Payload, + + [Parameter(Mandatory = $false)] + [string]$Source = 'CIPP', + + [Parameter(Mandatory = $false)] + [string]$InvokingCommand + ) + + $NormalizedPayload = $null + + if ($null -eq $Payload) { + $NormalizedPayload = [pscustomobject]@{} + } elseif ($Payload -is [string]) { + if (Test-Json -Json $Payload -ErrorAction SilentlyContinue) { + $NormalizedPayload = $Payload | ConvertFrom-Json -Depth 50 + } else { + $NormalizedPayload = [pscustomobject]@{ + message = $Payload + } + } + } else { + $NormalizedPayload = $Payload + } + + $AlertCount = if ($NormalizedPayload -is [array]) { $NormalizedPayload.Count } else { 1 } + + $DetectedInvokingCommand = $null + + if ($NormalizedPayload -is [array] -and $NormalizedPayload.Count -gt 0) { + if ($NormalizedPayload[0].PSObject.Properties.Name -contains 'API') { + $ApiList = @($NormalizedPayload | Where-Object { $_.API } | Select-Object -ExpandProperty API -Unique) + if ($ApiList.Count -gt 0) { + $DetectedInvokingCommand = $ApiList -join ', ' + } + } + } elseif ($NormalizedPayload -isnot [string] -and $null -ne $NormalizedPayload) { + if ($NormalizedPayload.PSObject.Properties.Name -contains 'task' -and $NormalizedPayload.task) { + if ($NormalizedPayload.task.PSObject.Properties.Name -contains 'command' -and $NormalizedPayload.task.command) { + $DetectedInvokingCommand = [string]$NormalizedPayload.task.command + } + } + + if (-not $DetectedInvokingCommand -and $NormalizedPayload.PSObject.Properties.Name -contains 'TaskInfo' -and $NormalizedPayload.TaskInfo) { + if ($NormalizedPayload.TaskInfo.PSObject.Properties.Name -contains 'Command' -and $NormalizedPayload.TaskInfo.Command) { + $DetectedInvokingCommand = [string]$NormalizedPayload.TaskInfo.Command + } + } + + } + + $ResolvedInvokingCommand = if (![string]::IsNullOrWhiteSpace($InvokingCommand)) { $InvokingCommand } elseif ($DetectedInvokingCommand) { $DetectedInvokingCommand } else { $null } + + $SchemaObject = [ordered]@{ + schemaVersion = '1.0' + source = $Source + invoking = $ResolvedInvokingCommand + title = $Title + tenant = $TenantFilter + generatedAt = [datetime]::UtcNow.ToString('o') + alertCount = $AlertCount + payload = $NormalizedPayload + } + + return [pscustomobject]$SchemaObject +} diff --git a/Modules/CIPPCore/Public/New-CIPPUserTask.ps1 b/Modules/CIPPCore/Public/New-CIPPUserTask.ps1 index fb831949ccaa..d8dce48e5021 100644 --- a/Modules/CIPPCore/Public/New-CIPPUserTask.ps1 +++ b/Modules/CIPPCore/Public/New-CIPPUserTask.ps1 @@ -20,6 +20,22 @@ function New-CIPPUserTask { try { if ($UserObj.licenses.value) { + # Filter out licenses with no available units + try { + $LicenseOverview = Get-CIPPLicenseOverview -TenantFilter $UserObj.tenantFilter + $FilteredLicenses = @(foreach ($LicenseId in $UserObj.licenses.value) { + $Sku = $LicenseOverview | Where-Object { $_.skuId -eq $LicenseId } + if ($Sku -and [int]$Sku.availableUnits -le 0) { + $null = $Results.Add("Skipped license $($Sku.License): no available units") + Write-LogMessage -headers $Headers -API $APIName -tenant $UserObj.tenantFilter -message "Skipped license $($Sku.License) for $($CreationResults.Username): no available units" -Sev 'Warn' + } else { + $LicenseId + } + }) + $UserObj.licenses = [PSCustomObject]@{ value = $FilteredLicenses } + } catch { + Write-Warning "Failed to check available licenses: $($_.Exception.Message)" + } if ($UserObj.sherwebLicense.value) { $null = Set-SherwebSubscription -Headers $Headers -TenantFilter $UserObj.tenantFilter -SKU $UserObj.sherwebLicense.value -Add 1 $null = $Results.Add('Added Sherweb License, scheduling assignment') @@ -68,6 +84,21 @@ function New-CIPPUserTask { $CopyFrom.Error | ForEach-Object { $Results.Add($_) } } + if ($UserObj.groupMemberships -and ($UserObj.groupMemberships | Measure-Object).Count -gt 0) { + Write-Host "Adding user to $(@($UserObj.groupMemberships).Count) groups from template" + foreach ($Group in $UserObj.groupMemberships) { + try { + $GroupType = if ($Group.groupTypes -contains 'Unified') { 'Microsoft 365' } + elseif ($Group.mailEnabled) { 'Distribution list' } + else { 'Security' } + Add-CIPPGroupMember -Headers $Headers -GroupType $GroupType -GroupId $Group.id -Member $CreationResults.Username -TenantFilter $UserObj.tenantFilter -APIName $APIName + $Results.Add("Added user to group from template: $($Group.displayName)") + } catch { + $Results.Add("Failed to add to group $($Group.displayName): $($_.Exception.Message)") + } + } + } + if ($UserObj.setManager) { $ManagerResult = Set-CIPPManager -User $CreationResults.Username -Manager $UserObj.setManager.value -TenantFilter $UserObj.tenantFilter -Headers $Headers $Results.Add($ManagerResult) 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/Remove-CippKeyVaultSecret.ps1 b/Modules/CIPPCore/Public/Remove-CippKeyVaultSecret.ps1 new file mode 100644 index 000000000000..64464ee7f72e --- /dev/null +++ b/Modules/CIPPCore/Public/Remove-CippKeyVaultSecret.ps1 @@ -0,0 +1,58 @@ +function Remove-CippKeyVaultSecret { + <# + .SYNOPSIS + Deletes a secret from Azure Key Vault using REST API (no Az.KeyVault module required) + + .DESCRIPTION + Lightweight replacement for Remove-AzKeyVaultSecret that uses REST API directly. + + .PARAMETER VaultName + Name of the Key Vault. If not provided, derives from WEBSITE_DEPLOYMENT_ID environment variable. + + .PARAMETER Name + Name of the secret to delete. + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $false)] + [string]$VaultName, + + [Parameter(Mandatory = $true)] + [string]$Name + ) + + try { + if (-not $VaultName) { + if ($env:WEBSITE_DEPLOYMENT_ID) { + $VaultName = ($env:WEBSITE_DEPLOYMENT_ID -split '-')[0] + } else { + throw 'VaultName not provided and WEBSITE_DEPLOYMENT_ID environment variable not set' + } + } + + $token = Get-CIPPAzIdentityToken -ResourceUrl 'https://vault.azure.net' + $uri = "https://$VaultName.vault.azure.net/secrets/$Name`?api-version=7.4" + + $response = Invoke-RestMethod -Uri $uri -Headers @{ Authorization = "Bearer $token" } -Method Delete -ErrorAction Stop + + return @{ + Name = $Name + VaultName = $VaultName + Id = $response.recoveryId + Deleted = $true + } + } catch { + if ($_.Exception.Message -match '404|NotFound') { + return @{ + Name = $Name + VaultName = $VaultName + Id = $null + Deleted = $false + Status = 'NotFound' + } + } + + Write-Error "Failed to delete secret '$Name' from vault '$VaultName': $($_.Exception.Message)" + throw + } +} diff --git a/Modules/CIPPCore/Public/Send-CIPPAlert.ps1 b/Modules/CIPPCore/Public/Send-CIPPAlert.ps1 index 5965003d9ec5..57b498617372 100644 --- a/Modules/CIPPCore/Public/Send-CIPPAlert.ps1 +++ b/Modules/CIPPCore/Public/Send-CIPPAlert.ps1 @@ -10,9 +10,12 @@ function Send-CIPPAlert { $altEmail, $altWebhook, $APIName = 'Send Alert', + $SchemaSource, + $InvokingCommand, $Headers, $TableName, - $RowKey = [string][guid]::NewGuid() + $RowKey = [string][guid]::NewGuid(), + [switch]$UseStandardizedSchema ) Write-Information 'Shipping Alert' $Table = Get-CIPPTable -TableName SchedulerConfig @@ -101,58 +104,183 @@ function Send-CIPPAlert { if ($Type -eq 'webhook') { Write-Information 'Trying to send webhook' + $GetWebhookSecret = { + param( + [string]$SecretName + ) + + if ($env:AzureWebJobsStorage -eq 'UseDevelopmentStorage=true' -or $env:NonLocalHostAzurite -eq 'true') { + $DevSecretsTable = Get-CIPPTable -tablename 'DevSecrets' + return (Get-CIPPAzDataTableEntity @DevSecretsTable -Filter "PartitionKey eq '$SecretName' and RowKey eq '$SecretName'").APIKey + } + + $KeyVaultName = ($env:WEBSITE_DEPLOYMENT_ID -split '-')[0] + return (Get-CippKeyVaultSecret -VaultName $KeyVaultName -Name $SecretName -AsPlainText) + } + + $RequestHeaders = @{} + if ($Headers -is [hashtable]) { + foreach ($HeaderName in $Headers.Keys) { + $RequestHeaders[$HeaderName] = $Headers[$HeaderName] + } + } + + $WebhookAuthType = [string]$Config.webhookAuthType + switch ($WebhookAuthType.ToLowerInvariant()) { + 'bearer' { + $WebhookAuthToken = [string]$Config.webhookAuthToken + if ($WebhookAuthToken -eq 'SentToKeyVault') { + $WebhookAuthToken = & $GetWebhookSecret -SecretName 'CIPPNotificationsWebhookAuthToken' + } + if (![string]::IsNullOrWhiteSpace($WebhookAuthToken)) { + $RequestHeaders['Authorization'] = "Bearer $WebhookAuthToken" + } + } + 'basic' { + $WebhookAuthPassword = [string]$Config.webhookAuthPassword + if ($WebhookAuthPassword -eq 'SentToKeyVault') { + $WebhookAuthPassword = & $GetWebhookSecret -SecretName 'CIPPNotificationsWebhookAuthPassword' + } + if (![string]::IsNullOrWhiteSpace($Config.webhookAuthUsername) -and ![string]::IsNullOrWhiteSpace($WebhookAuthPassword)) { + $BasicAuthBytes = [System.Text.Encoding]::UTF8.GetBytes("$($Config.webhookAuthUsername):$WebhookAuthPassword") + $RequestHeaders['Authorization'] = 'Basic {0}' -f [System.Convert]::ToBase64String($BasicAuthBytes) + } + } + 'apikey' { + $WebhookAuthHeaderValue = [string]$Config.webhookAuthHeaderValue + if ($WebhookAuthHeaderValue -eq 'SentToKeyVault') { + $WebhookAuthHeaderValue = & $GetWebhookSecret -SecretName 'CIPPNotificationsWebhookAuthHeaderValue' + } + if (![string]::IsNullOrWhiteSpace($Config.webhookAuthHeaderName) -and ![string]::IsNullOrWhiteSpace($WebhookAuthHeaderValue)) { + $RequestHeaders[$Config.webhookAuthHeaderName] = $WebhookAuthHeaderValue + } + } + 'customheaders' { + $WebhookAuthHeaders = [string]$Config.webhookAuthHeaders + if ($WebhookAuthHeaders -eq 'SentToKeyVault') { + $WebhookAuthHeaders = & $GetWebhookSecret -SecretName 'CIPPNotificationsWebhookAuthHeaders' + } + if (![string]::IsNullOrWhiteSpace($WebhookAuthHeaders)) { + try { + $CustomHeaders = $WebhookAuthHeaders | ConvertFrom-Json -AsHashtable + if ($CustomHeaders -is [hashtable]) { + foreach ($HeaderName in $CustomHeaders.Keys) { + if (![string]::IsNullOrWhiteSpace([string]$HeaderName)) { + $RequestHeaders[[string]$HeaderName] = [string]$CustomHeaders[$HeaderName] + } + } + } + } catch { + Write-LogMessage -API 'Webhook Alerts' -message 'Webhook custom headers JSON is invalid. Continuing without custom auth headers.' -tenant $TenantFilter -sev warning + } + } + } + } + $ExtensionTable = Get-CIPPTable -TableName Extensionsconfig - $Configuration = ((Get-CIPPAzDataTableEntity @ExtensionTable).config | ConvertFrom-Json) + $ExtensionConfig = Get-CIPPAzDataTableEntity @ExtensionTable + # Check if config exists and is not null before parsing + if ($ExtensionConfig.config -and -not [string]::IsNullOrWhiteSpace($ExtensionConfig.config)) { + $Configuration = $ExtensionConfig.config | ConvertFrom-Json + } else { + $Configuration = $null + } if ($Configuration.CFZTNA.WebhookEnabled -eq $true -and $Configuration.CFZTNA.Enabled -eq $true) { $CFAPIKey = Get-ExtensionAPIKey -Extension 'CFZTNA' - $Headers = @{'CF-Access-Client-Id' = $Configuration.CFZTNA.ClientId; 'CF-Access-Client-Secret' = "$CFAPIKey" } + $RequestHeaders['CF-Access-Client-Id'] = $Configuration.CFZTNA.ClientId + $RequestHeaders['CF-Access-Client-Secret'] = "$CFAPIKey" Write-Information 'CF-Access-Client-Id and CF-Access-Client-Secret headers added to webhook API request' + } + + $UseStandardizedWebhookSchema = [boolean]$Config.UseStandardizedSchema + if ($PSBoundParameters.ContainsKey('UseStandardizedSchema')) { + $UseStandardizedWebhookSchema = [boolean]$UseStandardizedSchema + } + + $EffectiveTitle = if ([string]::IsNullOrWhiteSpace($Title)) { + '{0} - {1} - Webhook Alert' -f $APIName, $TenantFilter } else { - $Headers = $null + $Title } - $ReplacedContent = Get-CIPPTextReplacement -TenantFilter $TenantFilter -Text $JSONContent -EscapeForJson + $EffectiveSchemaSource = if (![string]::IsNullOrWhiteSpace($SchemaSource)) { + $SchemaSource + } elseif (![string]::IsNullOrWhiteSpace($APIName)) { + $APIName + } else { + 'CIPP' + } + + $WebhookContent = if ($UseStandardizedWebhookSchema) { + New-CIPPStandardizedWebhookSchema -Title $EffectiveTitle -TenantFilter $TenantFilter -Payload $JSONContent -Source $EffectiveSchemaSource -InvokingCommand $InvokingCommand + } else { + $JSONContent + } + + if ($WebhookContent -isnot [string]) { + $WebhookContent = $WebhookContent | ConvertTo-Json -Compress -Depth 50 + } + + $ReplacedContent = Get-CIPPTextReplacement -TenantFilter $TenantFilter -Text $WebhookContent -EscapeForJson try { if (![string]::IsNullOrWhiteSpace($Config.webhook) -or ![string]::IsNullOrWhiteSpace($AltWebhook)) { - if ($PSCmdlet.ShouldProcess($Config.webhook, 'Sending webhook')) { - $webhook = if ($AltWebhook) { $AltWebhook } else { $Config.webhook } + $webhook = if ($AltWebhook) { $AltWebhook } else { $Config.webhook } + if ($PSCmdlet.ShouldProcess($webhook, 'Sending webhook')) { + $RestMethod = @{ + Uri = $webhook + Method = 'POST' + ContentType = 'application/json' + StatusCodeVariable = 'WebhookStatusCode' + SkipHttpErrorCheck = $true + } + if ($RequestHeaders.Count -gt 0) { + $RestMethod['Headers'] = $RequestHeaders + } switch -wildcard ($webhook) { '*webhook.office.com*' { - $TeamsBody = [PSCustomObject]@{ - text = "You've setup your alert policies to be alerted whenever specific events happen. We've found some of these events in the log.

$ReplacedContent" - } | ConvertTo-Json -Compress - $WebhookResponse = Invoke-RestMethod -Uri $webhook -Method POST -ContentType 'Application/json' -Body $TeamsBody -StatusCodeVariable WebhookStatusCode -SkipHttpErrorCheck + if ($UseStandardizedWebhookSchema) { + $RestMethod['Body'] = $ReplacedContent + $WebhookResponse = Invoke-RestMethod @RestMethod + } else { + $TeamsBody = [PSCustomObject]@{ + text = "You've setup your alert policies to be alerted whenever specific events happen. We've found some of these events in the log.

$ReplacedContent" + } | ConvertTo-Json -Compress + $RestMethod['Body'] = $TeamsBody + $WebhookResponse = Invoke-RestMethod @RestMethod + } } '*discord.com*' { - $DiscordBody = [PSCustomObject]@{ - content = "You've setup your alert policies to be alerted whenever specific events happen. We've found some of these events in the log. ``````$ReplacedContent``````" - } | ConvertTo-Json -Compress - $WebhookResponse = Invoke-RestMethod -Uri $webhook -Method POST -ContentType 'Application/json' -Body $DiscordBody -StatusCodeVariable WebhookStatusCode -SkipHttpErrorCheck + if ($UseStandardizedWebhookSchema) { + $RestMethod['Body'] = $ReplacedContent + $WebhookResponse = Invoke-RestMethod @RestMethod + } else { + $DiscordBody = [PSCustomObject]@{ + content = "You've setup your alert policies to be alerted whenever specific events happen. We've found some of these events in the log. ``````$ReplacedContent``````" + } | ConvertTo-Json -Compress + $RestMethod['Body'] = $DiscordBody + $WebhookResponse = Invoke-RestMethod @RestMethod + } } '*slack.com*' { - $SlackBlocks = Get-SlackAlertBlocks -JSONBody $JSONContent - if ($SlackBlocks.blocks) { - $SlackBody = $SlackBlocks | ConvertTo-Json -Depth 10 -Compress + if ($UseStandardizedWebhookSchema) { + $RestMethod['Body'] = $ReplacedContent + $WebhookResponse = Invoke-RestMethod @RestMethod } else { - $SlackBody = [PSCustomObject]@{ - text = "You've setup your alert policies to be alerted whenever specific events happen. We've found some of these events in the log. ``````$ReplacedContent``````" - } | ConvertTo-Json -Compress + $SlackBlocks = Get-SlackAlertBlocks -JSONBody $JSONContent + if ($SlackBlocks.blocks) { + $SlackBody = $SlackBlocks | ConvertTo-Json -Depth 10 -Compress + } else { + $SlackBody = [PSCustomObject]@{ + text = "You've setup your alert policies to be alerted whenever specific events happen. We've found some of these events in the log. ``````$ReplacedContent``````" + } | ConvertTo-Json -Compress + } + $RestMethod['Body'] = $SlackBody + $WebhookResponse = Invoke-RestMethod @RestMethod } - $WebhookResponse = Invoke-RestMethod -Uri $webhook -Method POST -ContentType 'Application/json' -Body $SlackBody -StatusCodeVariable WebhookStatusCode -SkipHttpErrorCheck } default { - $RestMethod = @{ - Uri = $webhook - Method = 'POST' - ContentType = 'application/json' - Body = $ReplacedContent - StatusCodeVariable = 'WebhookStatusCode' - SkipHttpErrorCheck = $true - } - if ($Headers) { - $RestMethod['Headers'] = $Headers - } + $RestMethod['Body'] = $ReplacedContent $WebhookResponse = Invoke-RestMethod @RestMethod } } diff --git a/Modules/CIPPCore/Public/Send-CIPPScheduledTaskAlert.ps1 b/Modules/CIPPCore/Public/Send-CIPPScheduledTaskAlert.ps1 index 1f8b163fe714..df1f5c4cbc35 100644 --- a/Modules/CIPPCore/Public/Send-CIPPScheduledTaskAlert.ps1 +++ b/Modules/CIPPCore/Public/Send-CIPPScheduledTaskAlert.ps1 @@ -71,6 +71,11 @@ function Send-CIPPScheduledTaskAlert { Write-Information 'Scheduler: Sending the results to configured targets.' + $NotificationTable = Get-CIPPTable -TableName SchedulerConfig + $NotificationFilter = "RowKey eq 'CippNotifications' and PartitionKey eq 'CippNotifications'" + $NotificationConfig = [pscustomobject](Get-CIPPAzDataTableEntity @NotificationTable -Filter $NotificationFilter) + $UseStandardizedSchema = [boolean]$NotificationConfig.UseStandardizedSchema + # Send to configured alert targets switch -wildcard ($TaskInfo.PostExecution) { '*psa*' { @@ -80,14 +85,34 @@ function Send-CIPPScheduledTaskAlert { Send-CIPPAlert -Type 'email' -Title $title -HTMLContent $HTML -TenantFilter $TenantFilter } '*webhook*' { - $Webhook = [PSCustomObject]@{ - 'tenantId' = $TenantInfo.customerId - 'Tenant' = $TenantFilter - 'TaskInfo' = $TaskInfo - 'Results' = $Results - 'AlertComment' = $TaskInfo.AlertComment + $Webhook = if ($UseStandardizedSchema) { + [PSCustomObject]@{ + tenantId = $TenantInfo.customerId + tenant = $TenantFilter + taskType = $TaskType + task = [PSCustomObject]@{ + id = $TaskInfo.RowKey + name = $TaskInfo.Name + command = $TaskInfo.Command + state = $TaskInfo.TaskState + reference = $TaskInfo.Reference + scheduled = $TaskInfo.ScheduledTime + executed = $TaskInfo.ExecutedTime + partition = $TaskInfo.PartitionKey + } + results = $Results + alertComment = $TaskInfo.AlertComment + } + } else { + [PSCustomObject]@{ + tenantId = $TenantInfo.customerId + Tenant = $TenantFilter + TaskInfo = $TaskInfo + Results = $Results + AlertComment = $TaskInfo.AlertComment + } } - Send-CIPPAlert -Type 'webhook' -Title $title -TenantFilter $TenantFilter -JSONContent $($Webhook | ConvertTo-Json -Depth 20) + Send-CIPPAlert -Type 'webhook' -Title $title -TenantFilter $TenantFilter -JSONContent $($Webhook | ConvertTo-Json -Depth 20) -APIName 'Scheduled Task Alerts' -SchemaSource $TaskType -InvokingCommand $TaskInfo.Command -UseStandardizedSchema:$UseStandardizedSchema } } 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/Set-CIPPDBCacheMailboxes.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheMailboxes.ps1 index c6b5579b6390..6b12a1f3c7dc 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheMailboxes.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheMailboxes.ps1 @@ -26,7 +26,7 @@ function Set-CIPPDBCacheMailboxes { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching mailboxes' -sev Debug # Get mailboxes with select properties - $Select = 'id,ExchangeGuid,ArchiveGuid,UserPrincipalName,DisplayName,PrimarySMTPAddress,RecipientType,RecipientTypeDetails,EmailAddresses,WhenSoftDeleted,IsInactiveMailbox,ForwardingSmtpAddress,DeliverToMailboxAndForward,ForwardingAddress,HiddenFromAddressListsEnabled,ExternalDirectoryObjectId,MessageCopyForSendOnBehalfEnabled,MessageCopyForSentAsEnabled,GrantSendOnBehalfTo' + $Select = 'id,ExchangeGuid,ArchiveGuid,UserPrincipalName,DisplayName,PrimarySMTPAddress,RecipientType,RecipientTypeDetails,EmailAddresses,WhenSoftDeleted,IsInactiveMailbox,ForwardingSmtpAddress,DeliverToMailboxAndForward,ForwardingAddress,HiddenFromAddressListsEnabled,ExternalDirectoryObjectId,MessageCopyForSendOnBehalfEnabled,MessageCopyForSentAsEnabled,GrantSendOnBehalfTo,PersistedCapabilities,LitigationHoldEnabled,LitigationHoldDate,LitigationHoldDuration,ComplianceTagHoldApplied,RetentionHoldEnabled,InPlaceHolds,RetentionPolicy' $ExoRequest = @{ tenantid = $TenantFilter cmdlet = 'Get-Mailbox' @@ -52,6 +52,14 @@ function Set-CIPPDBCacheMailboxes { ExternalDirectoryObjectId, MessageCopyForSendOnBehalfEnabled, MessageCopyForSentAsEnabled, + LitigationHoldEnabled, + LitigationHoldDate, + LitigationHoldDuration, + @{ Name = 'LicensedForLitigationHold'; Expression = { ($_.PersistedCapabilities -contains 'EXCHANGE_S_ARCHIVE_ADDON' -or $_.PersistedCapabilities -contains 'BPOS_S_ArchiveAddOn' -or $_.PersistedCapabilities -contains 'EXCHANGE_S_ENTERPRISE' -or $_.PersistedCapabilities -contains 'BPOS_S_DlpAddOn' -or $_.PersistedCapabilities -contains 'BPOS_S_Enterprise') } }, + ComplianceTagHoldApplied, + RetentionHoldEnabled, + InPlaceHolds, + RetentionPolicy, GrantSendOnBehalfTo)) } @@ -168,7 +176,7 @@ function Set-CIPPDBCacheMailboxes { } } Write-Information "Starting permissions caching orchestrator with $($PermissionBatches.Count) batches" - Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($PermissionInputObject | ConvertTo-Json -Compress -Depth 5) + Start-CIPPOrchestrator -InputObject $PermissionInputObject Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Started permission caching orchestrator with $($PermissionBatches.Count) batches" -sev Debug } @@ -185,7 +193,7 @@ function Set-CIPPDBCacheMailboxes { } } Write-Information "Starting rules caching orchestrator with $($RuleBatches.Count) batches" - Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($RuleInputObject | ConvertTo-Json -Compress -Depth 5) + Start-CIPPOrchestrator -InputObject $RuleInputObject Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Started rules caching orchestrator with $($RuleBatches.Count) batches" -sev Debug } diff --git a/Modules/CIPPCore/Public/Set-CIPPDefenderASRPolicy.ps1 b/Modules/CIPPCore/Public/Set-CIPPDefenderASRPolicy.ps1 new file mode 100644 index 000000000000..7dd9ef5571e8 --- /dev/null +++ b/Modules/CIPPCore/Public/Set-CIPPDefenderASRPolicy.ps1 @@ -0,0 +1,89 @@ +function Set-CIPPDefenderASRPolicy { + <# + .FUNCTIONALITY + Internal + #> + [CmdletBinding()] + param( + [string]$TenantFilter, + $ASR, + $Headers, + [string]$APIName + ) + + # Fallback to block mode + $Mode = $ASR.Mode ?? 'block' + + # Lookup table: ASR input property name -> Graph settingDefinitionId suffix + $ASRRuleMap = [ordered]@{ + BlockObfuscatedScripts = 'blockexecutionofpotentiallyobfuscatedscripts' + BlockAdobeChild = 'blockadobereaderfromcreatingchildprocesses' + BlockWin32Macro = 'blockwin32apicallsfromofficemacros' + BlockCredentialStealing = 'blockcredentialstealingfromwindowslocalsecurityauthoritysubsystem' + BlockPSExec = 'blockprocesscreationsfrompsexecandwmicommands' + WMIPersistence = 'blockpersistencethroughwmieventsubscription' + BlockOfficeExes = 'blockofficeapplicationsfromcreatingexecutablecontent' + BlockOfficeApps = 'blockofficeapplicationsfrominjectingcodeintootherprocesses' + BlockYoungExe = 'blockexecutablefilesrunningunlesstheymeetprevalenceagetrustedlistcriterion' + blockJSVB = 'blockjavascriptorvbscriptfromlaunchingdownloadedexecutablecontent' + BlockWebshellForServers = 'blockwebshellcreationforservers' + blockOfficeComChild = 'blockofficecommunicationappfromcreatingchildprocesses' + BlockSystemTools = 'blockuseofcopiedorimpersonatedsystemtools' + blockOfficeChild = 'blockallofficeapplicationsfromcreatingchildprocesses' + BlockUntrustedUSB = 'blockuntrustedunsignedprocessesthatrunfromusb' + EnableRansomwareVac = 'useadvancedprotectionagainstransomware' + BlockExesMail = 'blockexecutablecontentfromemailclientandwebmail' + BlockUnsignedDrivers = 'blockabuseofexploitedvulnerablesigneddrivers' + BlockSafeMode = 'blockrebootingmachineinsafemode' + } + + $ASRPrefix = 'device_vendor_msft_policy_config_defender_attacksurfacereductionrules' + $ASRSettings = foreach ($Rule in $ASRRuleMap.GetEnumerator()) { + if ($ASR.($Rule.Key)) { + $DefinitionId = "${ASRPrefix}_$($Rule.Value)" + @{ + '@odata.type' = '#microsoft.graph.deviceManagementConfigurationChoiceSettingInstance' + settingDefinitionId = $DefinitionId + choiceSettingValue = @{ + '@odata.type' = '#microsoft.graph.deviceManagementConfigurationChoiceSettingValue' + value = "${DefinitionId}_${Mode}" + } + } + } + } + + $ASRbody = ConvertTo-Json -Depth 15 -Compress -InputObject @{ + name = 'ASR Default rules' + description = '' + platforms = 'windows10' + technologies = 'mdm,microsoftSense' + roleScopeTagIds = @('0') + templateReference = @{templateId = 'e8c053d6-9f95-42b1-a7f1-ebfd71c67a4b_1' } + settings = @(@{ + '@odata.type' = '#microsoft.graph.deviceManagementConfigurationSetting' + settingInstance = @{ + '@odata.type' = '#microsoft.graph.deviceManagementConfigurationGroupSettingCollectionInstance' + settingDefinitionId = 'device_vendor_msft_policy_config_defender_attacksurfacereductionrules' + groupSettingCollectionValue = @(@{children = $ASRSettings }) + settingInstanceTemplateReference = @{settingInstanceTemplateId = '19600663-e264-4c02-8f55-f2983216d6d7' } + } + }) + } + + $CheckExistingASR = New-GraphGETRequest -uri 'https://graph.microsoft.com/beta/deviceManagement/configurationPolicies' -tenantid $TenantFilter + if ('ASR Default rules' -in $CheckExistingASR.Name) { + "$($TenantFilter): ASR Policy already exists. Skipping" + } else { + Write-Host $ASRbody + if (($ASRSettings | Measure-Object).Count -gt 0) { + $ASRRequest = New-GraphPOSTRequest -uri 'https://graph.microsoft.com/beta/deviceManagement/configurationPolicies' -tenantid $TenantFilter -type POST -body $ASRbody + Write-Host ($ASRRequest.id) + if ($ASR.AssignTo -and $ASR.AssignTo -ne 'none') { + $AssignBody = if ($ASR.AssignTo -ne 'AllDevicesAndUsers') { '{"assignments":[{"id":"","target":{"@odata.type":"#microsoft.graph.' + $($ASR.AssignTo) + 'AssignmentTarget"}}]}' } else { '{"assignments":[{"id":"","target":{"@odata.type":"#microsoft.graph.allDevicesAssignmentTarget"}},{"id":"","target":{"@odata.type":"#microsoft.graph.allLicensedUsersAssignmentTarget"}}]}' } + $null = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/deviceManagement/configurationPolicies('$($ASRRequest.id)')/assign" -tenantid $TenantFilter -type POST -body $AssignBody + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Assigned policy $($DisplayName) to $($ASR.AssignTo)" -Sev 'Info' + } + "$($TenantFilter): Successfully added ASR Settings" + } + } +} diff --git a/Modules/CIPPCore/Public/Set-CIPPDefenderAVPolicy.ps1 b/Modules/CIPPCore/Public/Set-CIPPDefenderAVPolicy.ps1 new file mode 100644 index 000000000000..5f86f00addc3 --- /dev/null +++ b/Modules/CIPPCore/Public/Set-CIPPDefenderAVPolicy.ps1 @@ -0,0 +1,182 @@ +function Set-CIPPDefenderAVPolicy { + <# + .FUNCTIONALITY + Internal + #> + [CmdletBinding()] + param( + [string]$TenantFilter, + $PolicySettings, + $Headers, + [string]$APIName + ) + + # Builds a choice-type setting entry + function New-AVChoiceSetting { + param([string]$DefinitionId, [string]$InstanceTemplateId, [string]$ValueTemplateId, [string]$Value) + @{ + '@odata.type' = '#microsoft.graph.deviceManagementConfigurationSetting' + settingInstance = @{ + '@odata.type' = '#microsoft.graph.deviceManagementConfigurationChoiceSettingInstance' + settingDefinitionId = $DefinitionId + settingInstanceTemplateReference = @{ settingInstanceTemplateId = $InstanceTemplateId } + choiceSettingValue = @{ + '@odata.type' = '#microsoft.graph.deviceManagementConfigurationChoiceSettingValue' + value = $Value + settingValueTemplateReference = @{ settingValueTemplateId = $ValueTemplateId } + } + } + } + } + + # Builds an integer-type setting entry + function New-AVIntegerSetting { + param([string]$DefinitionId, [string]$InstanceTemplateId, [string]$ValueTemplateId, [int]$Value) + @{ + '@odata.type' = '#microsoft.graph.deviceManagementConfigurationSetting' + settingInstance = @{ + '@odata.type' = '#microsoft.graph.deviceManagementConfigurationSimpleSettingInstance' + settingDefinitionId = $DefinitionId + settingInstanceTemplateReference = @{ + '@odata.type' = '#microsoft.graph.deviceManagementConfigurationSettingInstanceTemplateReference' + settingInstanceTemplateId = $InstanceTemplateId + } + simpleSettingValue = @{ + '@odata.type' = '#microsoft.graph.deviceManagementConfigurationIntegerSettingValue' + value = $Value + settingValueTemplateReference = @{ + '@odata.type' = '#microsoft.graph.deviceManagementConfigurationSettingValueTemplateReference' + settingValueTemplateId = $ValueTemplateId + } + } + } + } + } + + $DP = 'device_vendor_msft_policy_config_defender' + $DA = 'device_vendor_msft_defender_configuration' + + # Boolean choice settings: value is always _1 + # property -> [definitionId, instanceTemplateId, valueTemplateId] + $BoolChoiceMap = [ordered]@{ + ScanArchives = @("${DP}_allowarchivescanning", '7c5c9cde-f74d-4d11-904f-de4c27f72d89', '9ead75d4-6f30-4bc5-8cc5-ab0f999d79f0') + AllowBehavior = @("${DP}_allowbehaviormonitoring", '8eef615a-1aa0-46f4-a25a-12cbe65de5ab', '905921da-95e2-4a10-9e30-fe5540002ce1') + AllowCloudProtection = @("${DP}_allowcloudprotection", '7da139f1-9b7e-407d-853a-c2e5037cdc70', '16fe8afd-67be-4c50-8619-d535451a500c') + AllowEmailScanning = @("${DP}_allowemailscanning", 'b0d9ee81-de6a-4750-86d7-9397961c9852', 'fdf107fd-e13b-4507-9d8f-db4d93476af9') + AllowFullScanNetwork = @("${DP}_allowfullscanonmappednetworkdrives", 'dac47505-f072-48d6-9f23-8d93262d58ed', '3e920b10-3773-4ac5-957e-e5573aec6d04') + AllowFullScanRemovable = @("${DP}_allowfullscanremovabledrivescanning", 'fb36e70b-5bc9-488a-a949-8ea3ac1634d5', '366c5727-629b-4a81-b50b-52f90282fa2c') + AllowDownloadable = @("${DP}_allowioavprotection", 'fa06231d-aed4-4601-b631-3a37e85b62a0', 'df4e6cbd-f7ff-41c8-88cd-fa25264a237e') + AllowRealTime = @("${DP}_allowrealtimemonitoring", 'f0790e28-9231-4d37-8f44-84bb47ca1b3e', '0492c452-1069-4b91-9363-93b8e006ab12') + AllowNetwork = @("${DP}_allowscanningnetworkfiles", 'f8f28442-0a6b-4b52-b42c-d31d9687c1cf', '7b8c858c-a17d-4623-9e20-f34b851670ce') + AllowScriptScan = @("${DP}_allowscriptscanning", '000cf176-949c-4c08-a5d4-90ed43718db7', 'ab9e4320-c953-4067-ac9a-be2becd06b4a') + AllowUI = @("${DP}_allowuseruiaccess", '0170a900-b0bc-4ccc-b7ce-dda9be49189b', '4b6c9739-4449-4006-8e5f-3049136470ea') + CheckSigs = @("${DP}_checkforsignaturesbeforerunningscan", '4fea56e3-7bb6-4ad3-88c6-e364dd2f97b9', '010779d1-edd4-441d-8034-89ad57a863fe') + DisableCatchupFullScan = @("${DP}_disablecatchupfullscan", 'f881b08c-f047-40d2-b7d9-3dde7ce9ef64', '1b26092f-48c4-447b-99d4-e9c501542f1c') + DisableCatchupQuickScan = @("${DP}_disablecatchupquickscan", 'dabf6781-9d5d-42da-822a-d4327aa2bdd1', 'd263ced7-0d23-4095-9326-99c8b3f5d35b') + LowCPU = @("${DP}_enablelowcpupriority", 'cdeb96cf-18f5-4477-a710-0ea9ecc618af', '045a4a13-deee-4e24-9fe4-985c9357680d') + MeteredConnectionUpdates = @("${DA}_meteredconnectionupdates", '7e3aaffb-309f-46de-8cd7-25c1a3b19e5b', '20cf972c-be3f-4bc1-93d3-781829d55233') + DisableLocalAdminMerge = @("${DA}_disablelocaladminmerge", '5f9a9c65-dea7-4987-a5f5-b28cfd9762ba', '3a9774b2-3143-47eb-bbca-d73c0ace2b7e') + } + + # Integer settings: property -> [definitionId, instanceTemplateId, valueTemplateId, defaultValue] + $IntegerMap = [ordered]@{ + AvgCPULoadFactor = @("${DP}_avgcpuloadfactor", '816cc03e-8f96-4cba-b14f-2658d031a79a', '37195fb1-3743-4c8e-a0ce-b6fae6fa3acd', 50) + CloudExtendedTimeout = @("${DP}_cloudextendedtimeout", 'f61c2788-14e4-4e80-a5a7-bf2ff5052f63', '608f1561-b603-46bd-bf5f-0b9872002f75', 50) + SignatureUpdateInterval = @("${DP}_signatureupdateinterval", '89879f27-6b7d-44d4-a08e-0a0de3e9663d', '0af6bbed-a74a-4d08-8587-b16b10b774cb', 8) + } + + $Settings = [System.Collections.Generic.List[object]]::new() + + # Boolean choice settings + foreach ($Entry in $BoolChoiceMap.GetEnumerator()) { + if ($PolicySettings.($Entry.Key)) { + $DefId, $InstId, $ValId = $Entry.Value + $Settings.Add((New-AVChoiceSetting -DefinitionId $DefId -InstanceTemplateId $InstId -ValueTemplateId $ValId -Value "${DefId}_1")) + } + } + + # Dynamic choice settings (value derived from a sub-property) + if ($PolicySettings.EnableNetworkProtection) { + $DefId = "${DP}_enablenetworkprotection" + $Settings.Add((New-AVChoiceSetting -DefinitionId $DefId -InstanceTemplateId 'f53ab20e-8af6-48f5-9fa1-46863e1e517e' -ValueTemplateId 'ee58fb51-9ae5-408b-9406-b92b643f388a' -Value "${DefId}_$($PolicySettings.EnableNetworkProtection.value)")) + } + if ($PolicySettings.CloudBlockLevel) { + $DefId = "${DP}_cloudblocklevel" + $Settings.Add((New-AVChoiceSetting -DefinitionId $DefId -InstanceTemplateId 'c7a37009-c16e-4145-84c8-89a8c121fb15' -ValueTemplateId '517b4e84-e933-42b9-b92f-00e640b1a82d' -Value "${DefId}_$($PolicySettings.CloudBlockLevel.value ?? '0')")) + } + if ($PolicySettings.AllowOnAccessProtection) { + $DefId = "${DP}_allowonaccessprotection" + $Settings.Add((New-AVChoiceSetting -DefinitionId $DefId -InstanceTemplateId 'afbc322b-083c-4281-8242-ebbb91398b41' -ValueTemplateId 'ed077fee-9803-44f3-b045-aab34d8e6d52' -Value "${DefId}_$($PolicySettings.AllowOnAccessProtection.value ?? '1')")) + } + if ($PolicySettings.SubmitSamplesConsent) { + $DefId = "${DP}_submitsamplesconsent" + $Settings.Add((New-AVChoiceSetting -DefinitionId $DefId -InstanceTemplateId 'bc47ce7d-a251-4cae-a8a2-6e8384904ab7' -ValueTemplateId '826ed4b6-e04f-4975-9d23-6f0904b0d87e' -Value "${DefId}_$($PolicySettings.SubmitSamplesConsent.value ?? '2')")) + } + + # Integer settings + foreach ($Entry in $IntegerMap.GetEnumerator()) { + if ($PolicySettings.($Entry.Key)) { + $DefId, $InstId, $ValId, $Default = $Entry.Value + $Settings.Add((New-AVIntegerSetting -DefinitionId $DefId -InstanceTemplateId $InstId -ValueTemplateId $ValId -Value ($PolicySettings.($Entry.Key) ?? $Default))) + } + } + + # Remediation (unique nested group structure) + if ($PolicySettings.Remediation) { + $RemPrefix = "${DP}_threatseveritydefaultaction" + $RemediationChildren = @( + if ($PolicySettings.Remediation.Low) { + @{ '@odata.type' = '#microsoft.graph.deviceManagementConfigurationChoiceSettingInstance'; settingDefinitionId = "${RemPrefix}_lowseveritythreats"; choiceSettingValue = @{ '@odata.type' = '#microsoft.graph.deviceManagementConfigurationChoiceSettingValue'; value = "${RemPrefix}_lowseveritythreats_$($PolicySettings.Remediation.Low.value)" } } + } + if ($PolicySettings.Remediation.Moderate) { + @{ '@odata.type' = '#microsoft.graph.deviceManagementConfigurationChoiceSettingInstance'; settingDefinitionId = "${RemPrefix}_moderateseveritythreats"; choiceSettingValue = @{ '@odata.type' = '#microsoft.graph.deviceManagementConfigurationChoiceSettingValue'; value = "${RemPrefix}_moderateseveritythreats_$($PolicySettings.Remediation.Moderate.value)" } } + } + if ($PolicySettings.Remediation.High) { + @{ '@odata.type' = '#microsoft.graph.deviceManagementConfigurationChoiceSettingInstance'; settingDefinitionId = "${RemPrefix}_highseveritythreats"; choiceSettingValue = @{ '@odata.type' = '#microsoft.graph.deviceManagementConfigurationChoiceSettingValue'; value = "${RemPrefix}_highseveritythreats_$($PolicySettings.Remediation.High.value)" } } + } + if ($PolicySettings.Remediation.Severe) { + @{ '@odata.type' = '#microsoft.graph.deviceManagementConfigurationChoiceSettingInstance'; settingDefinitionId = "${RemPrefix}_severethreats"; choiceSettingValue = @{ '@odata.type' = '#microsoft.graph.deviceManagementConfigurationChoiceSettingValue'; value = "${RemPrefix}_severethreats_$($PolicySettings.Remediation.Severe.value)" } } + } + ) + $Settings.Add(@{ + '@odata.type' = '#microsoft.graph.deviceManagementConfigurationSetting' + settingInstance = @{ + '@odata.type' = '#microsoft.graph.deviceManagementConfigurationGroupSettingCollectionInstance' + settingDefinitionId = $RemPrefix + settingInstanceTemplateReference = @{ + '@odata.type' = '#microsoft.graph.deviceManagementConfigurationSettingInstanceTemplateReference' + settingInstanceTemplateId = 'f6394bc5-6486-4728-b510-555f5c161f2b' + } + groupSettingCollectionValue = @( + @{ + '@odata.type' = '#microsoft.graph.deviceManagementConfigurationGroupSettingValue' + children = $RemediationChildren + } + ) + } + }) + } + + $CheckExisting = New-GraphGETRequest -uri 'https://graph.microsoft.com/beta/deviceManagement/configurationPolicies' -tenantid $TenantFilter + if ('Default AV Policy' -in $CheckExisting.Name) { + "$($TenantFilter): AV Policy already exists. Skipping" + } else { + $PolBody = ConvertTo-Json -Depth 10 -Compress -InputObject @{ + name = 'Default AV Policy' + description = '' + platforms = 'windows10' + technologies = 'mdm,microsoftSense' + roleScopeTagIds = @('0') + templateReference = @{ templateId = '804339ad-1553-4478-a742-138fb5807418_1' } + settings = @($Settings) + } + + $PolicyRequest = New-GraphPOSTRequest -uri 'https://graph.microsoft.com/beta/deviceManagement/configurationPolicies' -tenantid $TenantFilter -type POST -body $PolBody + if ($PolicySettings.AssignTo -ne 'None') { + $AssignBody = if ($PolicySettings.AssignTo -ne 'AllDevicesAndUsers') { '{"assignments":[{"id":"","target":{"@odata.type":"#microsoft.graph.' + $($PolicySettings.AssignTo) + 'AssignmentTarget"}}]}' } else { '{"assignments":[{"id":"","target":{"@odata.type":"#microsoft.graph.allDevicesAssignmentTarget"}},{"id":"","target":{"@odata.type":"#microsoft.graph.allLicensedUsersAssignmentTarget"}}]}' } + $null = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/deviceManagement/configurationPolicies('$($PolicyRequest.id)')/assign" -tenantid $TenantFilter -type POST -body $AssignBody + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Assigned AV policy to $($PolicySettings.AssignTo)" -Sev 'Info' + } + "$($TenantFilter): Successfully set Default AV Policy settings" + } +} diff --git a/Modules/CIPPCore/Public/Set-CIPPDefenderCompliancePolicy.ps1 b/Modules/CIPPCore/Public/Set-CIPPDefenderCompliancePolicy.ps1 new file mode 100644 index 000000000000..9ffa6ab75743 --- /dev/null +++ b/Modules/CIPPCore/Public/Set-CIPPDefenderCompliancePolicy.ps1 @@ -0,0 +1,71 @@ +function Set-CIPPDefenderCompliancePolicy { + <# + .FUNCTIONALITY + Internal + #> + [CmdletBinding()] + param( + [string]$TenantFilter, + $Compliance, + $Headers, + [string]$APIName + ) + + $ConnectorStatus = Enable-CIPPMDEConnector -TenantFilter $TenantFilter + if (!$ConnectorStatus.Success) { + "$($TenantFilter): Failed to enable MDE Connector - $($ConnectorStatus.ErrorMessage)" + return + } else { + "$($TenantFilter): MDE Connector is $($ConnectorStatus.PartnerState)" + } + + $SettingsObject = @{ + id = 'fc780465-2017-40d4-a0c5-307022471b92' + androidEnabled = [bool]$Compliance.ConnectAndroid + iosEnabled = [bool]$Compliance.ConnectIos + windowsEnabled = [bool]$Compliance.Connectwindows + macEnabled = [bool]$Compliance.ConnectMac + partnerUnsupportedOsVersionBlocked = [bool]$Compliance.BlockunsupportedOS + partnerUnresponsivenessThresholdInDays = 7 + allowPartnerToCollectIOSApplicationMetadata = [bool]$Compliance.ConnectIosCompliance + allowPartnerToCollectIOSPersonalApplicationMetadata = [bool]$Compliance.ConnectIosCompliance + androidDeviceBlockedOnMissingPartnerData = [bool]$Compliance.androidDeviceBlockedOnMissingPartnerData + iosDeviceBlockedOnMissingPartnerData = [bool]$Compliance.iosDeviceBlockedOnMissingPartnerData + windowsDeviceBlockedOnMissingPartnerData = [bool]$Compliance.windowsDeviceBlockedOnMissingPartnerData + macDeviceBlockedOnMissingPartnerData = [bool]$Compliance.macDeviceBlockedOnMissingPartnerData + androidMobileApplicationManagementEnabled = [bool]$Compliance.ConnectAndroidCompliance + iosMobileApplicationManagementEnabled = [bool]$Compliance.appSync + windowsMobileApplicationManagementEnabled = [bool]$Compliance.windowsMobileApplicationManagementEnabled + allowPartnerToCollectIosCertificateMetadata = [bool]$Compliance.allowPartnerToCollectIosCertificateMetadata + allowPartnerToCollectIosPersonalCertificateMetadata = [bool]$Compliance.allowPartnerToCollectIosPersonalCertificateMetadata + microsoftDefenderForEndpointAttachEnabled = [bool]$true + } + $SettingsObj = $SettingsObject | ConvertTo-Json -Compress + $ConnectorUri = 'https://graph.microsoft.com/beta/deviceManagement/mobileThreatDefenseConnectors/fc780465-2017-40d4-a0c5-307022471b92' + $ConnectorExists = $false + $SettingsMatch = $false + try { + $ExistingSettings = New-GraphGETRequest -uri $ConnectorUri -tenantid $TenantFilter + $ConnectorExists = $true + + $SettingsMatch = $true + foreach ($key in $SettingsObject.Keys) { + if ($ExistingSettings.$key -ne $SettingsObject[$key]) { + $SettingsMatch = $false + break + } + } + } catch { + $ConnectorExists = $false + } + + if ($SettingsMatch) { + "Defender Intune Configuration already correct and active for $($TenantFilter). Skipping" + } elseif ($ConnectorExists) { + $null = New-GraphPOSTRequest -uri $ConnectorUri -tenantid $TenantFilter -type PATCH -body $SettingsObj -AsApp $true + "$($TenantFilter): Successfully updated Defender Compliance and Reporting settings." + } else { + $null = New-GraphPOSTRequest -uri 'https://graph.microsoft.com/beta/deviceManagement/mobileThreatDefenseConnectors/' -tenantid $TenantFilter -type POST -body $SettingsObj -AsApp $true + "$($TenantFilter): Successfully created Defender Compliance and Reporting settings." + } +} diff --git a/Modules/CIPPCore/Public/Set-CIPPDefenderEDRPolicy.ps1 b/Modules/CIPPCore/Public/Set-CIPPDefenderEDRPolicy.ps1 new file mode 100644 index 000000000000..dbb17d6d55af --- /dev/null +++ b/Modules/CIPPCore/Public/Set-CIPPDefenderEDRPolicy.ps1 @@ -0,0 +1,83 @@ +function Set-CIPPDefenderEDRPolicy { + <# + .FUNCTIONALITY + Internal + #> + [CmdletBinding()] + param( + [string]$TenantFilter, + $EDR, + $Headers, + [string]$APIName + ) + + $EDRSettings = [System.Collections.Generic.List[object]]::new() + + if ($EDR.SampleSharing) { + $EDRSettings.Add(@{ + '@odata.type' = '#microsoft.graph.deviceManagementConfigurationSetting' + settingInstance = @{ + '@odata.type' = '#microsoft.graph.deviceManagementConfigurationChoiceSettingInstance' + settingDefinitionId = 'device_vendor_msft_windowsadvancedthreatprotection_configuration_samplesharing' + settingInstanceTemplateReference = @{ settingInstanceTemplateId = '6998c81e-2814-4f5e-b492-a6159128a97b' } + choiceSettingValue = @{ + '@odata.type' = '#microsoft.graph.deviceManagementConfigurationChoiceSettingValue' + value = 'device_vendor_msft_windowsadvancedthreatprotection_configuration_samplesharing_1' + settingValueTemplateReference = @{ settingValueTemplateId = 'f72c326c-7c5b-4224-b890-0b9b54522bd9' } + } + } + }) + } + + if ($EDR.Config) { + $EDRSettings.Add(@{ + '@odata.type' = '#microsoft.graph.deviceManagementConfigurationSetting' + settingInstance = @{ + '@odata.type' = '#microsoft.graph.deviceManagementConfigurationChoiceSettingInstance' + settingDefinitionId = 'device_vendor_msft_windowsadvancedthreatprotection_configurationtype' + settingInstanceTemplateReference = @{ settingInstanceTemplateId = '23ab0ea3-1b12-429a-8ed0-7390cf699160' } + choiceSettingValue = @{ + '@odata.type' = '#microsoft.graph.deviceManagementConfigurationChoiceSettingValue' + value = 'device_vendor_msft_windowsadvancedthreatprotection_configurationtype_autofromconnector' + settingValueTemplateReference = @{ settingValueTemplateId = 'e5c7c98c-c854-4140-836e-bd22db59d651' } + children = @( + @{ + '@odata.type' = '#microsoft.graph.deviceManagementConfigurationSimpleSettingInstance' + settingDefinitionId = 'device_vendor_msft_windowsadvancedthreatprotection_onboarding_fromconnector' + simpleSettingValue = @{ + '@odata.type' = '#microsoft.graph.deviceManagementConfigurationSecretSettingValue' + value = 'Microsoft ATP connector enabled' + valueState = 'NotEncrypted' + } + } + ) + } + } + }) + } + + if (($EDRSettings | Measure-Object).Count -gt 0) { + $EDRbody = ConvertTo-Json -Depth 15 -Compress -InputObject @{ + name = 'EDR Configuration' + description = '' + platforms = 'windows10' + technologies = 'mdm,microsoftSense' + roleScopeTagIds = @('0') + templateReference = @{templateId = '0385b795-0f2f-44ac-8602-9f65bf6adede_1' } + settings = @($EDRSettings) + } + Write-Host ($EDRbody) + $CheckExistingEDR = New-GraphGETRequest -uri 'https://graph.microsoft.com/beta/deviceManagement/configurationPolicies' -tenantid $TenantFilter | Where-Object -Property Name -EQ 'EDR Configuration' + if ($CheckExistingEDR) { + "$($TenantFilter): EDR Policy already exists. Skipping" + } else { + $EDRRequest = New-GraphPOSTRequest -uri 'https://graph.microsoft.com/beta/deviceManagement/configurationPolicies' -tenantid $TenantFilter -type POST -body $EDRbody + if ($EDR.AssignTo -and $EDR.AssignTo -ne 'none') { + $AssignBody = if ($EDR.AssignTo -ne 'AllDevicesAndUsers') { '{"assignments":[{"id":"","target":{"@odata.type":"#microsoft.graph.' + $($EDR.AssignTo) + 'AssignmentTarget"}}]}' } else { '{"assignments":[{"id":"","target":{"@odata.type":"#microsoft.graph.allDevicesAssignmentTarget"}},{"id":"","target":{"@odata.type":"#microsoft.graph.allLicensedUsersAssignmentTarget"}}]}' } + $null = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/deviceManagement/configurationPolicies('$($EDRRequest.id)')/assign" -tenantid $TenantFilter -type POST -body $AssignBody + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Assigned EDR policy $($DisplayName) to $($EDR.AssignTo)" -Sev 'Info' + } + "$($TenantFilter): Successfully added EDR Settings" + } + } +} diff --git a/Modules/CIPPCore/Public/Set-CIPPDefenderExclusionPolicy.ps1 b/Modules/CIPPCore/Public/Set-CIPPDefenderExclusionPolicy.ps1 new file mode 100644 index 000000000000..7976e4aabab5 --- /dev/null +++ b/Modules/CIPPCore/Public/Set-CIPPDefenderExclusionPolicy.ps1 @@ -0,0 +1,93 @@ +function Set-CIPPDefenderExclusionPolicy { + <# + .FUNCTIONALITY + Internal + #> + [CmdletBinding()] + param( + [string]$TenantFilter, + $DefenderExclusions, + $Headers, + [string]$APIName + ) + + $ExclusionAssignTo = $DefenderExclusions.AssignTo + if ($DefenderExclusions.excludedExtensions) { + $ExcludedExtensions = $DefenderExclusions.excludedExtensions | Where-Object { $_ -and $_.Trim() } | ForEach-Object { + @{ '@odata.type' = '#microsoft.graph.deviceManagementConfigurationStringSettingValue'; value = $_ } + } + } + if ($DefenderExclusions.excludedPaths) { + $ExcludedPaths = $DefenderExclusions.excludedPaths | Where-Object { $_ -and $_.Trim() } | ForEach-Object { + @{ '@odata.type' = '#microsoft.graph.deviceManagementConfigurationStringSettingValue'; value = $_ } + } + } + if ($DefenderExclusions.excludedProcesses) { + $ExcludedProcesses = $DefenderExclusions.excludedProcesses | Where-Object { $_ -and $_.Trim() } | ForEach-Object { + @{ '@odata.type' = '#microsoft.graph.deviceManagementConfigurationStringSettingValue'; value = $_ } + } + } + + $ExclusionSettings = [System.Collections.Generic.List[System.Object]]::new() + if ($ExcludedExtensions.Count -gt 0) { + $ExclusionSettings.Add(@{ + id = '2' + settingInstance = @{ + '@odata.type' = '#microsoft.graph.deviceManagementConfigurationSimpleSettingCollectionInstance' + settingDefinitionId = 'device_vendor_msft_policy_config_defender_excludedextensions' + settingInstanceTemplateReference = @{ settingInstanceTemplateId = 'c203725b-17dc-427b-9470-673a2ce9cd5e' } + simpleSettingCollectionValue = @($ExcludedExtensions) + } + }) + } + if ($ExcludedPaths.Count -gt 0) { + $ExclusionSettings.Add(@{ + id = '1' + settingInstance = @{ + '@odata.type' = '#microsoft.graph.deviceManagementConfigurationSimpleSettingCollectionInstance' + settingDefinitionId = 'device_vendor_msft_policy_config_defender_excludedpaths' + settingInstanceTemplateReference = @{ settingInstanceTemplateId = 'aaf04adc-c639-464f-b4a7-152e784092e8' } + simpleSettingCollectionValue = @($ExcludedPaths) + } + }) + } + if ($ExcludedProcesses.Count -gt 0) { + $ExclusionSettings.Add(@{ + id = '0' + settingInstance = @{ + '@odata.type' = '#microsoft.graph.deviceManagementConfigurationSimpleSettingCollectionInstance' + settingDefinitionId = 'device_vendor_msft_policy_config_defender_excludedprocesses' + settingInstanceTemplateReference = @{ settingInstanceTemplateId = '96b046ed-f138-4250-9ae0-b0772a93d16f' } + simpleSettingCollectionValue = @($ExcludedProcesses) + } + }) + } + + if ($ExclusionSettings.Count -gt 0) { + $ExclusionBody = ConvertTo-Json -Depth 15 -Compress -InputObject @{ + name = 'Default AV Exclusion Policy' + displayName = 'Default AV Exclusion Policy' + settings = @($ExclusionSettings) + platforms = 'windows10' + technologies = 'mdm,microsoftSense' + templateReference = @{ + templateId = '45fea5e9-280d-4da1-9792-fb5736da0ca9_1' + templateFamily = 'endpointSecurityAntivirus' + templateDisplayName = 'Microsoft Defender Antivirus exclusions' + templateDisplayVersion = 'Version 1' + } + } + $CheckExistingExclusion = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/deviceManagement/configurationPolicies' -tenantid $TenantFilter + if ('Default AV Exclusion Policy' -in $CheckExistingExclusion.Name) { + "$($TenantFilter): Exclusion Policy already exists. Skipping" + } else { + $ExclusionRequest = New-GraphPOSTRequest -uri 'https://graph.microsoft.com/beta/deviceManagement/configurationPolicies' -tenantid $TenantFilter -type POST -body $ExclusionBody + if ($ExclusionAssignTo -and $ExclusionAssignTo -ne 'none') { + $AssignBody = if ($ExclusionAssignTo -ne 'AllDevicesAndUsers') { '{"assignments":[{"id":"","target":{"@odata.type":"#microsoft.graph.' + $($ExclusionAssignTo) + 'AssignmentTarget"}}]}' } else { '{"assignments":[{"id":"","target":{"@odata.type":"#microsoft.graph.allDevicesAssignmentTarget"}},{"id":"","target":{"@odata.type":"#microsoft.graph.allLicensedUsersAssignmentTarget"}}]}' } + $null = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/deviceManagement/configurationPolicies('$($ExclusionRequest.id)')/assign" -tenantid $TenantFilter -type POST -body $AssignBody + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Assigned Exclusion policy to $($ExclusionAssignTo)" -Sev 'Info' + } + "$($TenantFilter): Successfully set Default AV Exclusion Policy settings" + } + } +} diff --git a/Modules/CIPPCore/Public/Set-CIPPGDAPInviteGroups.ps1 b/Modules/CIPPCore/Public/Set-CIPPGDAPInviteGroups.ps1 index fe0f4465f179..10be7db62f73 100644 --- a/Modules/CIPPCore/Public/Set-CIPPGDAPInviteGroups.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPGDAPInviteGroups.ps1 @@ -58,7 +58,7 @@ function Set-CIPPGDAPInviteGroups { SkipLog = $true } #Write-Information ($InputObject | ConvertTo-Json) - $InstanceId = Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject (ConvertTo-Json -InputObject $InputObject -Depth 5 -Compress) + $InstanceId = Start-CIPPOrchestrator -InputObject $InputObject Write-Information "Started GDAP Invite orchestration with ID = '$InstanceId'" return $InstanceId } diff --git a/Modules/CIPPCore/Public/Set-CIPPNotificationConfig.ps1 b/Modules/CIPPCore/Public/Set-CIPPNotificationConfig.ps1 index 7c6acb63d582..3679e4a89c06 100644 --- a/Modules/CIPPCore/Public/Set-CIPPNotificationConfig.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPNotificationConfig.ps1 @@ -3,28 +3,88 @@ function Set-CIPPNotificationConfig { param ( $email, $webhook, + $webhookAuthType, + $webhookAuthToken, + $webhookAuthUsername, + $webhookAuthPassword, + $webhookAuthHeaderName, + $webhookAuthHeaderValue, + $webhookAuthHeaders, $onepertenant, $logsToInclude, $sendtoIntegration, $sev, + [boolean]$UseStandardizedSchema, $APIName = 'Set Notification Config' ) - $results = try { + try { $Table = Get-CIPPTable -TableName SchedulerConfig + $ExistingConfig = Get-CIPPAzDataTableEntity @Table -Filter "PartitionKey eq 'CippNotifications' and RowKey eq 'CippNotifications'" + + $StoreWebhookSecret = { + param( + [string]$SecretName, + [string]$SecretValue + ) + + if ($env:AzureWebJobsStorage -eq 'UseDevelopmentStorage=true' -or $env:NonLocalHostAzurite -eq 'true') { + $DevSecretsTable = Get-CIPPTable -tablename 'DevSecrets' + $Secret = [PSCustomObject]@{ + 'PartitionKey' = $SecretName + 'RowKey' = $SecretName + 'APIKey' = $SecretValue + } + Add-CIPPAzDataTableEntity @DevSecretsTable -Entity $Secret -Force | Out-Null + } else { + $KeyVaultName = ($env:WEBSITE_DEPLOYMENT_ID -split '-')[0] + Set-CippKeyVaultSecret -VaultName $KeyVaultName -Name $SecretName -SecretValue (ConvertTo-SecureString -AsPlainText -Force -String $SecretValue) | Out-Null + } + } + + $WebhookSecretMap = @( + @{ Field = 'webhookAuthToken'; SecretName = 'CIPPNotificationsWebhookAuthToken'; Value = [string]$webhookAuthToken } + @{ Field = 'webhookAuthPassword'; SecretName = 'CIPPNotificationsWebhookAuthPassword'; Value = [string]$webhookAuthPassword } + @{ Field = 'webhookAuthHeaderValue'; SecretName = 'CIPPNotificationsWebhookAuthHeaderValue'; Value = [string]$webhookAuthHeaderValue } + @{ Field = 'webhookAuthHeaders'; SecretName = 'CIPPNotificationsWebhookAuthHeaders'; Value = [string]$webhookAuthHeaders } + ) + + $WebhookSecretMarkers = @{} + foreach ($SecretInfo in $WebhookSecretMap) { + $IncomingValue = [string]$SecretInfo.Value + $ExistingValue = [string]$ExistingConfig.($SecretInfo.Field) + + if (![string]::IsNullOrWhiteSpace($IncomingValue) -and $IncomingValue -ne 'SentToKeyVault') { + & $StoreWebhookSecret -SecretName $SecretInfo.SecretName -SecretValue $IncomingValue + $WebhookSecretMarkers[$SecretInfo.Field] = 'SentToKeyVault' + } elseif ($IncomingValue -eq 'SentToKeyVault' -or $ExistingValue -eq 'SentToKeyVault') { + $WebhookSecretMarkers[$SecretInfo.Field] = 'SentToKeyVault' + } else { + $WebhookSecretMarkers[$SecretInfo.Field] = '' + } + } + $SchedulerConfig = @{ - 'tenant' = 'Any' - 'tenantid' = 'TenantId' - 'type' = 'CIPPNotifications' - 'schedule' = 'Every 15 minutes' - 'Severity' = [string]$sev - 'email' = "$($email)" - 'webhook' = "$($webhook)" - 'onePerTenant' = [boolean]$onePerTenant - 'sendtoIntegration' = [boolean]$sendtoIntegration - 'includeTenantId' = $true - 'PartitionKey' = 'CippNotifications' - 'RowKey' = 'CippNotifications' + 'tenant' = 'Any' + 'tenantid' = 'TenantId' + 'type' = 'CIPPNotifications' + 'schedule' = 'Every 15 minutes' + 'Severity' = [string]$sev + 'email' = "$($email)" + 'webhook' = "$($webhook)" + 'webhookAuthType' = "$($webhookAuthType)" + 'webhookAuthToken' = "$($WebhookSecretMarkers.webhookAuthToken)" + 'webhookAuthUsername' = "$($webhookAuthUsername)" + 'webhookAuthPassword' = "$($WebhookSecretMarkers.webhookAuthPassword)" + 'webhookAuthHeaderName' = "$($webhookAuthHeaderName)" + 'webhookAuthHeaderValue' = "$($WebhookSecretMarkers.webhookAuthHeaderValue)" + 'webhookAuthHeaders' = "$($WebhookSecretMarkers.webhookAuthHeaders)" + 'onePerTenant' = [boolean]$onePerTenant + 'sendtoIntegration' = [boolean]$sendtoIntegration + 'UseStandardizedSchema' = [boolean]$UseStandardizedSchema + 'includeTenantId' = $true + 'PartitionKey' = 'CippNotifications' + 'RowKey' = 'CippNotifications' } foreach ($logvalue in [pscustomobject]$logsToInclude) { $SchedulerConfig[([pscustomobject]$logvalue.value)] = $true diff --git a/Modules/CIPPCore/Public/Set-CIPPOutOfoffice.ps1 b/Modules/CIPPCore/Public/Set-CIPPOutOfoffice.ps1 index 19f7fae9e445..ab71ea5ca769 100644 --- a/Modules/CIPPCore/Public/Set-CIPPOutOfoffice.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPOutOfoffice.ps1 @@ -12,7 +12,13 @@ function Set-CIPPOutOfOffice { $APIName = 'Set Out of Office', $Headers, $StartTime, - $EndTime + $EndTime, + [bool]$CreateOOFEvent, + [string]$OOFEventSubject, + [bool]$AutoDeclineFutureRequestsWhenOOF, + [bool]$DeclineEventsForScheduledOOF, + [bool]$DeclineAllEventsForScheduledOOF, + [string]$DeclineMeetingMessage ) try { @@ -38,6 +44,26 @@ function Set-CIPPOutOfOffice { $EndTime = $EndTime ? $EndTime : (Get-Date $StartTime).AddDays(7) $CmdParams.StartTime = $StartTime $CmdParams.EndTime = $EndTime + + # Calendar options — only included when explicitly provided + if ($PSBoundParameters.ContainsKey('CreateOOFEvent')) { + $CmdParams.CreateOOFEvent = $CreateOOFEvent + } + if ($PSBoundParameters.ContainsKey('OOFEventSubject')) { + $CmdParams.OOFEventSubject = $OOFEventSubject + } + if ($PSBoundParameters.ContainsKey('AutoDeclineFutureRequestsWhenOOF')) { + $CmdParams.AutoDeclineFutureRequestsWhenOOF = $AutoDeclineFutureRequestsWhenOOF + } + if ($PSBoundParameters.ContainsKey('DeclineEventsForScheduledOOF')) { + $CmdParams.DeclineEventsForScheduledOOF = $DeclineEventsForScheduledOOF + } + if ($PSBoundParameters.ContainsKey('DeclineAllEventsForScheduledOOF')) { + $CmdParams.DeclineAllEventsForScheduledOOF = $DeclineAllEventsForScheduledOOF + } + if ($PSBoundParameters.ContainsKey('DeclineMeetingMessage')) { + $CmdParams.DeclineMeetingMessage = $DeclineMeetingMessage + } } $null = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Set-MailboxAutoReplyConfiguration' -cmdParams $CmdParams -Anchor $UserID diff --git a/Modules/CIPPCore/Public/Set-CIPPVacationOOO.ps1 b/Modules/CIPPCore/Public/Set-CIPPVacationOOO.ps1 index 9ae967996977..320dc1957515 100644 --- a/Modules/CIPPCore/Public/Set-CIPPVacationOOO.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPVacationOOO.ps1 @@ -6,7 +6,15 @@ function Set-CIPPVacationOOO { [string]$InternalMessage, [string]$ExternalMessage, [string]$APIName = 'OOO Vacation Mode', - $Headers + $Headers, + $StartTime, + $EndTime, + [bool]$CreateOOFEvent, + [string]$OOFEventSubject, + [bool]$AutoDeclineFutureRequestsWhenOOF, + [bool]$DeclineEventsForScheduledOOF, + [bool]$DeclineAllEventsForScheduledOOF, + [string]$DeclineMeetingMessage ) $Results = [System.Collections.Generic.List[string]]::new() @@ -14,22 +22,57 @@ function Set-CIPPVacationOOO { foreach ($upn in $Users) { if ([string]::IsNullOrWhiteSpace($upn)) { continue } try { + # Use Scheduled when StartTime/EndTime are provided (vacation always has dates), + # otherwise fall back to Enabled for backwards compatibility with in-flight tasks + $State = if ($Action -eq 'Add') { + if ($PSBoundParameters.ContainsKey('StartTime') -and $PSBoundParameters.ContainsKey('EndTime')) { 'Scheduled' } else { 'Enabled' } + } else { 'Disabled' } + $SplatParams = @{ UserID = $upn TenantFilter = $TenantFilter - State = if ($Action -eq 'Add') { 'Enabled' } else { 'Disabled' } + State = $State APIName = $APIName Headers = $Headers } - # Only pass messages on Add — Remove only disables, preserving any messages - # the user may have updated themselves during vacation + if ($Action -eq 'Add') { + # Pass start/end times when available + if ($PSBoundParameters.ContainsKey('StartTime')) { + $SplatParams.StartTime = $StartTime + } + if ($PSBoundParameters.ContainsKey('EndTime')) { + $SplatParams.EndTime = $EndTime + } + + # Only pass messages on Add — Remove only disables, preserving any messages + # the user may have updated themselves during vacation if (-not [string]::IsNullOrWhiteSpace($InternalMessage)) { $SplatParams.InternalMessage = $InternalMessage } if (-not [string]::IsNullOrWhiteSpace($ExternalMessage)) { $SplatParams.ExternalMessage = $ExternalMessage } + + # Calendar options — pass through when explicitly provided + if ($PSBoundParameters.ContainsKey('CreateOOFEvent')) { + $SplatParams.CreateOOFEvent = $CreateOOFEvent + } + if ($PSBoundParameters.ContainsKey('OOFEventSubject')) { + $SplatParams.OOFEventSubject = $OOFEventSubject + } + if ($PSBoundParameters.ContainsKey('AutoDeclineFutureRequestsWhenOOF')) { + $SplatParams.AutoDeclineFutureRequestsWhenOOF = $AutoDeclineFutureRequestsWhenOOF + } + if ($PSBoundParameters.ContainsKey('DeclineEventsForScheduledOOF')) { + $SplatParams.DeclineEventsForScheduledOOF = $DeclineEventsForScheduledOOF + } + if ($PSBoundParameters.ContainsKey('DeclineAllEventsForScheduledOOF')) { + $SplatParams.DeclineAllEventsForScheduledOOF = $DeclineAllEventsForScheduledOOF + } + if ($PSBoundParameters.ContainsKey('DeclineMeetingMessage')) { + $SplatParams.DeclineMeetingMessage = $DeclineMeetingMessage + } } $result = Set-CIPPOutOfOffice @SplatParams $Results.Add($result) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployCheckChromeExtension.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployCheckChromeExtension.ps1 index 921fe6d5db0f..b26cededae21 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployCheckChromeExtension.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployCheckChromeExtension.ps1 @@ -7,35 +7,41 @@ function Invoke-CIPPStandardDeployCheckChromeExtension { .SYNOPSIS (Label) Deploy Check Chrome Extension .DESCRIPTION - (Helptext) Deploys the Check Chrome extension via Intune OMA-URI custom policies for both Chrome and Edge browsers with configurable settings. Chrome ID: benimdeioplgkhanklclahllklceahbe, Edge ID: knepjpocdagponkonnbggpcnhnaikajg - (DocsDescription) Creates Intune OMA-URI custom policies that automatically install and configure the Check Chrome extension on managed devices for both Google Chrome and Microsoft Edge browsers. This ensures the extension is deployed consistently across all corporate devices with customizable settings. + (Helptext) Deploys the Check by CyberDrain extension via a Win32 script app in Intune for both Chrome and Edge browsers with configurable settings. Chrome ID: benimdeioplgkhanklclahllklceahbe, Edge ID: knepjpocdagponkonnbggpcnhnaikajg + (DocsDescription) Creates an Intune Win32 script application that writes registry keys to install and configure the Check by CyberDrain extension on managed devices for both Google Chrome and Microsoft Edge browsers. Uses a PowerShell detection script to enforce configuration drift — when settings change in CIPP the app is automatically redeployed. .NOTES CAT Intune Standards TAG EXECUTIVETEXT - Automatically deploys the Check browser extension across all company devices with configurable security and branding settings, ensuring consistent security monitoring and compliance capabilities. This extension provides enhanced security features and monitoring tools that help protect against threats while maintaining user productivity. + Automatically deploys the Check by CyberDrain extension across all company devices with configurable security and branding settings, ensuring consistent security monitoring and compliance capabilities. This extension provides enhanced security features and monitoring tools that help protect against threats while maintaining user productivity. ADDEDCOMPONENT - {"type":"switch","name":"standards.DeployCheckChromeExtension.enableValidPageBadge","label":"Enable valid page badge","defaultValue":true} + {"type":"switch","name":"standards.DeployCheckChromeExtension.showNotifications","label":"Show notifications","defaultValue":true} + {"type":"switch","name":"standards.DeployCheckChromeExtension.enableValidPageBadge","label":"Enable valid page badge","defaultValue":false} {"type":"switch","name":"standards.DeployCheckChromeExtension.enablePageBlocking","label":"Enable page blocking","defaultValue":true} - {"type":"switch","name":"standards.DeployCheckChromeExtension.enableCippReporting","label":"Enable CIPP reporting","defaultValue":true} - {"type":"textField","name":"standards.DeployCheckChromeExtension.cippServerUrl","label":"CIPP Server URL","placeholder":"https://YOUR-CIPP-SERVER-URL","required":false} + {"type":"switch","name":"standards.DeployCheckChromeExtension.forceToolbarPin","label":"Force pin extension to toolbar","defaultValue":true} + {"type":"switch","name":"standards.DeployCheckChromeExtension.enableCippReporting","label":"Enable CIPP reporting","defaultValue":false} {"type":"textField","name":"standards.DeployCheckChromeExtension.customRulesUrl","label":"Custom Rules URL","placeholder":"https://YOUR-CIPP-SERVER-URL/rules.json","required":false} - {"type":"number","name":"standards.DeployCheckChromeExtension.updateInterval","label":"Update interval (hours)","defaultValue":12} + {"type":"number","name":"standards.DeployCheckChromeExtension.updateInterval","label":"Update interval (hours)","defaultValue":24} {"type":"switch","name":"standards.DeployCheckChromeExtension.enableDebugLogging","label":"Enable debug logging","defaultValue":false} + {"type":"switch","name":"standards.DeployCheckChromeExtension.enableGenericWebhook","label":"Enable generic webhook","defaultValue":false} + {"type":"textField","name":"standards.DeployCheckChromeExtension.webhookUrl","label":"Webhook URL","placeholder":"https://webhook.example.com/endpoint","required":false} + {"type":"autoComplete","multiple":true,"creatable":true,"required":false,"label":"Webhook Events","name":"standards.DeployCheckChromeExtension.webhookEvents","placeholder":"e.g. pageBlocked, pageAllowed"} + {"type":"autoComplete","multiple":true,"creatable":true,"required":false,"label":"URL Allowlist","name":"standards.DeployCheckChromeExtension.urlAllowlist","placeholder":"e.g. https://example.com/*"} {"type":"textField","name":"standards.DeployCheckChromeExtension.companyName","label":"Company Name","placeholder":"YOUR-COMPANY","required":false} + {"type":"textField","name":"standards.DeployCheckChromeExtension.companyURL","label":"Company URL","placeholder":"https://yourcompany.com","required":false} {"type":"textField","name":"standards.DeployCheckChromeExtension.productName","label":"Product Name","placeholder":"YOUR-PRODUCT-NAME","required":false} {"type":"textField","name":"standards.DeployCheckChromeExtension.supportEmail","label":"Support Email","placeholder":"support@yourcompany.com","required":false} - {"type":"textField","name":"standards.DeployCheckChromeExtension.primaryColor","label":"Primary Color","placeholder":"#0044CC","required":false} + {"type":"textField","name":"standards.DeployCheckChromeExtension.primaryColor","label":"Primary Color","placeholder":"#F77F00","required":false} {"type":"textField","name":"standards.DeployCheckChromeExtension.logoUrl","label":"Logo URL","placeholder":"https://yourcompany.com/logo.png","required":false} - {"name":"AssignTo","label":"Who should this policy be assigned to?","type":"radio","options":[{"label":"Do not assign","value":"On"},{"label":"Assign to all users","value":"allLicensedUsers"},{"label":"Assign to all devices","value":"AllDevices"},{"label":"Assign to all users and devices","value":"AllDevicesAndUsers"},{"label":"Assign to Custom Group","value":"customGroup"}]} + {"name":"AssignTo","label":"Who should this app be assigned to?","type":"radio","options":[{"label":"Do not assign","value":"On"},{"label":"Assign to all users","value":"allLicensedUsers"},{"label":"Assign to all devices","value":"AllDevices"},{"label":"Assign to all users and devices","value":"AllDevicesAndUsers"},{"label":"Assign to Custom Group","value":"customGroup"}]} {"type":"textField","required":false,"name":"customGroup","label":"Enter the custom group name if you selected 'Assign to Custom Group'. Wildcards are allowed."} IMPACT Low Impact ADDEDDATE 2025-09-18 POWERSHELLEQUIVALENT - New-GraphPostRequest -uri 'https://graph.microsoft.com/beta/deviceManagement/configurationPolicies' + Add-CIPPW32ScriptApplication RECOMMENDEDBY "CIPP" UPDATECOMMENTBLOCK @@ -55,176 +61,350 @@ function Invoke-CIPPStandardDeployCheckChromeExtension { return $true } - Write-Information "Running Deploy Check Chrome Extension standard for tenant $($Tenant)." + Write-Information "Running Check by CyberDrain standard for tenant $($Tenant)." - # Chrome and Edge extension IDs for the Check extension + ########################################################################## + # Configuration values + ########################################################################## $ChromeExtensionId = 'benimdeioplgkhanklclahllklceahbe' $EdgeExtensionId = 'knepjpocdagponkonnbggpcnhnaikajg' - - # Policy names - $ChromePolicyName = 'Deploy Check Chrome Extension (Chrome)' - $EdgePolicyName = 'Deploy Check Chrome Extension (Edge)' + $AppDisplayName = 'Check by CyberDrain - Browser Extension' # CIPP Url $CippConfigTable = Get-CippTable -tablename Config $CippConfig = Get-CIPPAzDataTableEntity @CippConfigTable -Filter "PartitionKey eq 'InstanceProperties' and RowKey eq 'CIPPURL'" $CIPPURL = 'https://{0}' -f $CippConfig.Value - # Get configuration values with defaults - $ShowNotifications = $Settings.showNotifications ?? $true - $EnableValidPageBadge = $Settings.enableValidPageBadge ?? $true - $EnablePageBlocking = $Settings.enablePageBlocking ?? $true - $EnableCippReporting = $Settings.enableCippReporting ?? $true + # Settings with defaults + $ShowNotifications = [int][bool]($Settings.showNotifications ?? $true) + $EnableValidPageBadge = [int][bool]($Settings.enableValidPageBadge ?? $false) + $EnablePageBlocking = [int][bool]($Settings.enablePageBlocking ?? $true) + $ForceToolbarPin = [int][bool]($Settings.forceToolbarPin ?? $true) + $EnableCippReporting = [int][bool]($Settings.enableCippReporting ?? $false) $CippServerUrl = $CIPPURL $CippTenantId = $Tenant - $CustomRulesUrl = $Settings.customRulesUrl - $UpdateInterval = $Settings.updateInterval ?? 24 - $EnableDebugLogging = $Settings.enableDebugLogging ?? $false - $CompanyName = $Settings.companyName - $ProductName = $Settings.productName - $SupportEmail = $Settings.supportEmail - $PrimaryColor = $Settings.primaryColor ?? '#F77F00' - $LogoUrl = $Settings.logoUrl - - # Create extension settings JSON - $ChromeExtensionSettings = @{ - $ChromeExtensionId = @{ - installation_mode = 'force_installed' - update_url = 'https://clients2.google.com/service/update2/crx' - settings = @{ - showNotifications = $ShowNotifications - enableValidPageBadge = $EnableValidPageBadge - enablePageBlocking = $EnablePageBlocking - enableCippReporting = $EnableCippReporting - cippServerUrl = $CippServerUrl - cippTenantId = $CippTenantId - customRulesUrl = $CustomRulesUrl - updateInterval = $UpdateInterval - enableDebugLogging = $EnableDebugLogging - customBranding = @{ - companyName = $CompanyName - productName = $ProductName - supportEmail = $SupportEmail - primaryColor = $PrimaryColor - logoUrl = $LogoUrl - } - } - } - } | ConvertTo-Json -Depth 10 - - $EdgeExtensionSettings = @{ - $EdgeExtensionId = @{ - installation_mode = 'force_installed' - update_url = 'https://edge.microsoft.com/extensionwebstorebase/v1/crx' - settings = @{ - showNotifications = $ShowNotifications - enableValidPageBadge = $EnableValidPageBadge - enablePageBlocking = $EnablePageBlocking - enableCippReporting = $EnableCippReporting - cippServerUrl = $CippServerUrl - cippTenantId = $CippTenantId - customRulesUrl = $CustomRulesUrl - updateInterval = $UpdateInterval - enableDebugLogging = $EnableDebugLogging - customBranding = @{ - companyName = $CompanyName - productName = $ProductName - supportEmail = $SupportEmail - primaryColor = $PrimaryColor - logoUrl = $LogoUrl - } - } - } - } | ConvertTo-Json -Depth 10 - - # Create Chrome OMA-URI policy JSON - $ChromePolicyJSON = @{ - '@odata.type' = '#microsoft.graph.windows10CustomConfiguration' - displayName = $ChromePolicyName - description = 'Deploys and configures the Check Chrome extension for Google Chrome browsers' - omaSettings = @( - @{ - '@odata.type' = '#microsoft.graph.omaSettingString' - displayName = 'Chrome Extension Settings' - description = 'Configure Check Chrome extension settings' - omaUri = './Device/Vendor/MSFT/Policy/Config/Chrome~Policy~googlechrome/ExtensionSettings' - value = $ChromeExtensionSettings - } - ) - } | ConvertTo-Json -Depth 20 - - # Create Edge OMA-URI policy JSON - $EdgePolicyJSON = @{ - '@odata.type' = '#microsoft.graph.windows10CustomConfiguration' - displayName = $EdgePolicyName - description = 'Deploys and configures the Check Chrome extension for Microsoft Edge browsers' - omaSettings = @( - @{ - '@odata.type' = '#microsoft.graph.omaSettingString' - displayName = 'Edge Extension Settings' - description = 'Configure Check Chrome extension settings' - omaUri = './Device/Vendor/MSFT/Policy/Config/Edge/ExtensionSettings' - value = $EdgeExtensionSettings - } - ) - } | ConvertTo-Json -Depth 20 + $CustomRulesUrl = $Settings.customRulesUrl ?? '' + $UpdateInterval = [int]($Settings.updateInterval ?? 24) + $EnableDebugLogging = [int][bool]($Settings.enableDebugLogging ?? $false) + $EnableGenericWebhook = [int][bool]($Settings.enableGenericWebhook ?? $false) + $WebhookUrl = $Settings.webhookUrl ?? '' + $WebhookEvents = @($Settings.webhookEvents | ForEach-Object { $_.value ?? $_ } | Where-Object { $_ }) + $UrlAllowlist = @($Settings.urlAllowlist | ForEach-Object { $_.value ?? $_ } | Where-Object { $_ }) + $CompanyName = $Settings.companyName ?? '' + $CompanyURL = $Settings.companyURL ?? '' + $ProductName = $Settings.productName ?? '' + $SupportEmail = $Settings.supportEmail ?? '' + $PrimaryColor = if ($Settings.primaryColor) { $Settings.primaryColor } else { '#F77F00' } + $LogoUrl = $Settings.logoUrl ?? '' + + ########################################################################## + # Build the install script (writes registry keys - matches upstream Deploy-Windows-Chrome-and-Edge.ps1) + ########################################################################## + $InstallScript = @" +# Check Chrome Extension - Install Script (generated by CIPP) +`$chromeExtensionId = '$ChromeExtensionId' +`$edgeExtensionId = '$EdgeExtensionId' + +# Extension settings per browser +`$browsers = @( + @{ + ExtensionId = `$chromeExtensionId + UpdateUrl = 'https://clients2.google.com/service/update2/crx' + ManagedStorageKey = "HKLM:\SOFTWARE\Policies\Google\Chrome\3rdparty\extensions\`$chromeExtensionId\policy" + ExtSettingsKey = "HKLM:\SOFTWARE\Policies\Google\Chrome\ExtensionSettings\`$chromeExtensionId" + ToolbarProp = 'toolbar_pin' + ToolbarPinned = 'force_pinned' + ToolbarUnpinned = 'default_unpinned' + }, + @{ + ExtensionId = `$edgeExtensionId + UpdateUrl = 'https://edge.microsoft.com/extensionwebstorebase/v1/crx' + ManagedStorageKey = "HKLM:\SOFTWARE\Policies\Microsoft\Edge\3rdparty\extensions\`$edgeExtensionId\policy" + ExtSettingsKey = "HKLM:\SOFTWARE\Policies\Microsoft\Edge\ExtensionSettings\`$edgeExtensionId" + ToolbarProp = 'toolbar_state' + ToolbarPinned = 'force_shown' + ToolbarUnpinned = 'hidden' + } +) + +foreach (`$b in `$browsers) { + # Managed storage - core settings + if (!(Test-Path `$b.ManagedStorageKey)) { New-Item -Path `$b.ManagedStorageKey -Force | Out-Null } + New-ItemProperty -Path `$b.ManagedStorageKey -Name 'showNotifications' -PropertyType DWord -Value $ShowNotifications -Force | Out-Null + New-ItemProperty -Path `$b.ManagedStorageKey -Name 'enableValidPageBadge' -PropertyType DWord -Value $EnableValidPageBadge -Force | Out-Null + New-ItemProperty -Path `$b.ManagedStorageKey -Name 'enablePageBlocking' -PropertyType DWord -Value $EnablePageBlocking -Force | Out-Null + New-ItemProperty -Path `$b.ManagedStorageKey -Name 'enableCippReporting' -PropertyType DWord -Value $EnableCippReporting -Force | Out-Null + New-ItemProperty -Path `$b.ManagedStorageKey -Name 'cippServerUrl' -PropertyType String -Value '$CippServerUrl' -Force | Out-Null + New-ItemProperty -Path `$b.ManagedStorageKey -Name 'cippTenantId' -PropertyType String -Value '$CippTenantId' -Force | Out-Null + New-ItemProperty -Path `$b.ManagedStorageKey -Name 'customRulesUrl' -PropertyType String -Value '$CustomRulesUrl' -Force | Out-Null + New-ItemProperty -Path `$b.ManagedStorageKey -Name 'updateInterval' -PropertyType DWord -Value $UpdateInterval -Force | Out-Null + New-ItemProperty -Path `$b.ManagedStorageKey -Name 'enableDebugLogging' -PropertyType DWord -Value $EnableDebugLogging -Force | Out-Null + + # Managed storage - customBranding subkey + `$brandingKey = "`$(`$b.ManagedStorageKey)\customBranding" + if (!(Test-Path `$brandingKey)) { New-Item -Path `$brandingKey -Force | Out-Null } + New-ItemProperty -Path `$brandingKey -Name 'companyName' -PropertyType String -Value '$($CompanyName -replace "'", "''")' -Force | Out-Null + New-ItemProperty -Path `$brandingKey -Name 'companyURL' -PropertyType String -Value '$($CompanyURL -replace "'", "''")' -Force | Out-Null + New-ItemProperty -Path `$brandingKey -Name 'productName' -PropertyType String -Value '$($ProductName -replace "'", "''")' -Force | Out-Null + New-ItemProperty -Path `$brandingKey -Name 'supportEmail' -PropertyType String -Value '$($SupportEmail -replace "'", "''")' -Force | Out-Null + New-ItemProperty -Path `$brandingKey -Name 'primaryColor' -PropertyType String -Value '$PrimaryColor' -Force | Out-Null + New-ItemProperty -Path `$brandingKey -Name 'logoUrl' -PropertyType String -Value '$($LogoUrl -replace "'", "''")' -Force | Out-Null + + # Managed storage - genericWebhook subkey + `$webhookKey = "`$(`$b.ManagedStorageKey)\genericWebhook" + if (!(Test-Path `$webhookKey)) { New-Item -Path `$webhookKey -Force | Out-Null } + New-ItemProperty -Path `$webhookKey -Name 'enabled' -PropertyType DWord -Value $EnableGenericWebhook -Force | Out-Null + New-ItemProperty -Path `$webhookKey -Name 'url' -PropertyType String -Value '$($WebhookUrl -replace "'", "''")' -Force | Out-Null + + # Managed storage - genericWebhook\events subkey + `$webhookEventsKey = "`$(`$b.ManagedStorageKey)\genericWebhook\events" + if (Test-Path `$webhookEventsKey) { Remove-Item -Path `$webhookEventsKey -Recurse -Force | Out-Null } +$(if ($WebhookEvents.Count -gt 0) { + " if (!(Test-Path `$webhookEventsKey)) { New-Item -Path `$webhookEventsKey -Force | Out-Null }`n" + $i = 1 + foreach ($evt in $WebhookEvents) { + " New-ItemProperty -Path `$webhookEventsKey -Name '$i' -PropertyType String -Value '$($evt -replace "'", "''")' -Force | Out-Null`n" + $i++ + } +}) + # Managed storage - urlAllowlist subkey + `$allowlistKey = "`$(`$b.ManagedStorageKey)\urlAllowlist" + if (Test-Path `$allowlistKey) { Remove-Item -Path `$allowlistKey -Recurse -Force | Out-Null } +$(if ($UrlAllowlist.Count -gt 0) { + " if (!(Test-Path `$allowlistKey)) { New-Item -Path `$allowlistKey -Force | Out-Null }`n" + $i = 1 + foreach ($url in $UrlAllowlist) { + " New-ItemProperty -Path `$allowlistKey -Name '$i' -PropertyType String -Value '$($url -replace "'", "''")' -Force | Out-Null`n" + $i++ + } +}) + # Extension settings (installation + toolbar) + if (!(Test-Path `$b.ExtSettingsKey)) { New-Item -Path `$b.ExtSettingsKey -Force | Out-Null } + New-ItemProperty -Path `$b.ExtSettingsKey -Name 'installation_mode' -PropertyType String -Value 'force_installed' -Force | Out-Null + New-ItemProperty -Path `$b.ExtSettingsKey -Name 'update_url' -PropertyType String -Value `$b.UpdateUrl -Force | Out-Null + if ($ForceToolbarPin -eq 1) { + New-ItemProperty -Path `$b.ExtSettingsKey -Name `$b.ToolbarProp -PropertyType String -Value `$b.ToolbarPinned -Force | Out-Null + } else { + New-ItemProperty -Path `$b.ExtSettingsKey -Name `$b.ToolbarProp -PropertyType String -Value `$b.ToolbarUnpinned -Force | Out-Null + } +} + +Write-Output 'Check Chrome Extension registry keys configured successfully.' +"@ + + ########################################################################## + # Build the uninstall script (removes registry keys - matches upstream Remove-Windows-Chrome-and-Edge.ps1) + ########################################################################## + $UninstallScript = @" +# Check Chrome Extension - Uninstall Script (generated by CIPP) +`$chromeExtensionId = '$ChromeExtensionId' +`$edgeExtensionId = '$EdgeExtensionId' + +`$keysToRemove = @( + "HKLM:\SOFTWARE\Policies\Google\Chrome\3rdparty\extensions\`$chromeExtensionId", + "HKLM:\SOFTWARE\Policies\Google\Chrome\ExtensionSettings\`$chromeExtensionId", + "HKLM:\SOFTWARE\Policies\Microsoft\Edge\3rdparty\extensions\`$edgeExtensionId", + "HKLM:\SOFTWARE\Policies\Microsoft\Edge\ExtensionSettings\`$edgeExtensionId" +) + +foreach (`$key in `$keysToRemove) { + if (Test-Path `$key) { + Remove-Item -Path `$key -Recurse -Force -ErrorAction SilentlyContinue + Write-Output "Removed: `$key" + } +} + +Write-Output 'Check Chrome Extension registry keys removed.' +"@ + + ########################################################################## + # Build the detection script + # Checks that critical DWORD values match expected config. When settings + # change in CIPP the detection script body changes, so Intune sees a new + # app version and redeploys automatically. + ########################################################################## + $DetectionScript = @" +# Check Chrome Extension - Detection Script (generated by CIPP) +`$chromeKey = 'HKLM:\SOFTWARE\Policies\Google\Chrome\3rdparty\extensions\$ChromeExtensionId\policy' +`$edgeKey = 'HKLM:\SOFTWARE\Policies\Microsoft\Edge\3rdparty\extensions\$EdgeExtensionId\policy' + +# Verify both managed storage keys exist +if (!(Test-Path `$chromeKey) -or !(Test-Path `$edgeKey)) { exit 1 } + +# Helper to check a registry value matches expected +function Test-RegValue(`$Path, `$Name, `$Expected) { + `$val = (Get-ItemProperty -Path `$Path -Name `$Name -ErrorAction SilentlyContinue).`$Name + return (`$null -ne `$val -and `$val -eq `$Expected) +} + +foreach (`$key in @(`$chromeKey, `$edgeKey)) { + # Core DWORD settings + if (!(Test-RegValue `$key 'showNotifications' $ShowNotifications)) { exit 1 } + if (!(Test-RegValue `$key 'enableValidPageBadge' $EnableValidPageBadge)) { exit 1 } + if (!(Test-RegValue `$key 'enablePageBlocking' $EnablePageBlocking)) { exit 1 } + if (!(Test-RegValue `$key 'enableCippReporting' $EnableCippReporting)) { exit 1 } + if (!(Test-RegValue `$key 'updateInterval' $UpdateInterval)) { exit 1 } + if (!(Test-RegValue `$key 'enableDebugLogging' $EnableDebugLogging)) { exit 1 } + + # Core string settings + if (!(Test-RegValue `$key 'cippServerUrl' '$CippServerUrl')) { exit 1 } + if (!(Test-RegValue `$key 'cippTenantId' '$CippTenantId')) { exit 1 } + if (!(Test-RegValue `$key 'customRulesUrl' '$CustomRulesUrl')) { exit 1 } + + # customBranding subkey + `$brandingKey = "`$key\customBranding" + if (!(Test-Path `$brandingKey)) { exit 1 } + if (!(Test-RegValue `$brandingKey 'companyName' '$($CompanyName -replace "'", "''")')) { exit 1 } + if (!(Test-RegValue `$brandingKey 'companyURL' '$($CompanyURL -replace "'", "''")')) { exit 1 } + if (!(Test-RegValue `$brandingKey 'productName' '$($ProductName -replace "'", "''")')) { exit 1 } + if (!(Test-RegValue `$brandingKey 'supportEmail' '$($SupportEmail -replace "'", "''")')) { exit 1 } + if (!(Test-RegValue `$brandingKey 'primaryColor' '$PrimaryColor')) { exit 1 } + if (!(Test-RegValue `$brandingKey 'logoUrl' '$($LogoUrl -replace "'", "''")')) { exit 1 } + + # genericWebhook subkey + `$webhookKey = "`$key\genericWebhook" + if (!(Test-Path `$webhookKey)) { exit 1 } + if (!(Test-RegValue `$webhookKey 'enabled' $EnableGenericWebhook)) { exit 1 } + if (!(Test-RegValue `$webhookKey 'url' '$($WebhookUrl -replace "'", "''")')) { exit 1 } + + # genericWebhook\events subkey — verify exact count and values + `$eventsKey = "`$key\genericWebhook\events" +$(if ($WebhookEvents.Count -gt 0) { + " if (!(Test-Path `$eventsKey)) { exit 1 }`n" + $i = 1 + foreach ($evt in $WebhookEvents) { + " if (!(Test-RegValue `$eventsKey '$i' '$($evt -replace "'", "''")')) { exit 1 }`n" + $i++ + } + " `$eventsCount = (Get-Item `$eventsKey).Property.Count`n" + " if (`$eventsCount -ne $($WebhookEvents.Count)) { exit 1 }`n" +} else { + " if (Test-Path `$eventsKey) {`n" + " `$eventsCount = (Get-Item `$eventsKey).Property.Count`n" + " if (`$eventsCount -gt 0) { exit 1 }`n" + " }`n" +}) + # urlAllowlist subkey — verify exact count and values + `$allowlistKey = "`$key\urlAllowlist" +$(if ($UrlAllowlist.Count -gt 0) { + " if (!(Test-Path `$allowlistKey)) { exit 1 }`n" + $i = 1 + foreach ($url in $UrlAllowlist) { + " if (!(Test-RegValue `$allowlistKey '$i' '$($url -replace "'", "''")')) { exit 1 }`n" + $i++ + } + " `$allowlistCount = (Get-Item `$allowlistKey).Property.Count`n" + " if (`$allowlistCount -ne $($UrlAllowlist.Count)) { exit 1 }`n" +} else { + " if (Test-Path `$allowlistKey) {`n" + " `$allowlistCount = (Get-Item `$allowlistKey).Property.Count`n" + " if (`$allowlistCount -gt 0) { exit 1 }`n" + " }`n" +}) +} + +# Verify extension settings keys exist +`$chromeExtSettings = 'HKLM:\SOFTWARE\Policies\Google\Chrome\ExtensionSettings\$ChromeExtensionId' +`$edgeExtSettings = 'HKLM:\SOFTWARE\Policies\Microsoft\Edge\ExtensionSettings\$EdgeExtensionId' +if (!(Test-Path `$chromeExtSettings) -or !(Test-Path `$edgeExtSettings)) { exit 1 } + +Write-Output 'Check Chrome Extension is correctly configured.' +exit 0 +"@ + + ########################################################################## + # Legacy OMA-URI policy cleanup + ########################################################################## + $LegacyPolicyNames = @( + 'Deploy Check Chrome Extension (Chrome)', + 'Deploy Check Chrome Extension (Edge)' + ) 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 } + ########################################################################## + # Check for existing Win32 app + ########################################################################## + $Baseuri = 'https://graph.microsoft.com/beta/deviceAppManagement/mobileApps' + $ExistingApps = New-GraphGetRequest -Uri "$Baseuri`?`$filter=displayName eq '$AppDisplayName'&`$select=id,displayName" -tenantid $Tenant | Where-Object { + $_.'@odata.type' -eq '#microsoft.graph.win32LobApp' + } + $AppExists = ($null -ne $ExistingApps -and @($ExistingApps).Count -gt 0) if ($Settings.remediate -eq $true) { - # Handle assignment configuration $AssignTo = $Settings.AssignTo ?? 'AllDevices' - $ExcludeGroup = $Settings.ExcludeGroup + if ($Settings.customGroup) { $AssignTo = $Settings.customGroup } - # Handle custom group assignment - if ($Settings.customGroup) { - $AssignTo = $Settings.customGroup + # Clean up legacy OMA-URI configuration policies from the old approach + $LegacyPolicies = New-GraphGetRequest -Uri 'https://graph.microsoft.com/beta/deviceManagement/deviceConfigurations?$select=id,displayName' -tenantid $Tenant | Where-Object { + $_.displayName -in $LegacyPolicyNames + } + if ($LegacyPolicies) { + $DeleteRequests = @($LegacyPolicies | ForEach-Object { + @{ + id = "delete-$($_.id)" + method = 'DELETE' + url = "deviceManagement/deviceConfigurations/$($_.id)" + } + }) + $BulkResults = New-GraphBulkRequest -tenantid $Tenant -Requests $DeleteRequests + foreach ($Policy in $LegacyPolicies) { + $Result = $BulkResults | Where-Object { $_.id -eq "delete-$($Policy.id)" } + if ($Result.status -match '^2') { + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Removed legacy OMA-URI policy: $($Policy.displayName)" -sev Info + } else { + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to remove legacy OMA-URI policy: $($Policy.displayName) - $($Result.body.error.message)" -sev Warn + } + } } - # Deploy Chrome policy - 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 - } else { - Write-LogMessage -API 'Standards' -tenant $Tenant -message 'Check Chrome Extension policy for Chrome already exists, skipping creation' -sev Info + if ($AppExists) { + # App exists — delete and recreate to pick up any script changes + foreach ($ExistingApp in @($ExistingApps)) { + $null = New-GraphPostRequest -Uri "$Baseuri/$($ExistingApp.id)" -Type DELETE -tenantid $Tenant + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Removed existing $AppDisplayName app to redeploy with updated settings" -sev Info + } + Start-Sleep -Seconds 2 } - # Deploy Edge policy - 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 - } else { - Write-LogMessage -API 'Standards' -tenant $Tenant -message 'Check Chrome Extension policy for Edge already exists, skipping creation' -sev Info + # Deploy the Win32 script app + $AppProperties = [PSCustomObject]@{ + displayName = $AppDisplayName + description = 'Deploys and configures the Check by CyberDrain phishing protection extension for Chrome and Edge browsers. Managed by CIPP.' + publisher = 'CIPP' + installScript = $InstallScript + uninstallScript = $UninstallScript + detectionScript = $DetectionScript + runAsAccount = 'system' + deviceRestartBehavior = 'suppress' + } + + $NewApp = Add-CIPPW32ScriptApplication -TenantFilter $Tenant -Properties $AppProperties + + if ($NewApp -and $AssignTo -ne 'On') { + Start-Sleep -Milliseconds 500 + Set-CIPPAssignedApplication -ApplicationId $NewApp.Id -TenantFilter $Tenant -GroupName $AssignTo -Intent 'Required' -AppType 'Win32Lob' -APIName 'Standards' } + + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Successfully deployed $AppDisplayName" -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 ($AppExists) { + Write-LogMessage -API 'Standards' -tenant $Tenant -message "$AppDisplayName is deployed" -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 + Write-StandardsAlert -message "$AppDisplayName is not deployed" -object @{ AppName = $AppDisplayName } -tenant $Tenant -standardName 'DeployCheckChromeExtension' -standardId $Settings.standardId + Write-LogMessage -API 'Standards' -tenant $Tenant -message "$AppDisplayName is not deployed" -sev Alert } } if ($Settings.report -eq $true) { - $StateIsCorrect = $ChromePolicyExists -and $EdgePolicyExists + $StateIsCorrect = $AppExists $ExpectedValue = [PSCustomObject]@{ - ChromePolicyDeployed = $true - EdgePolicyDeployed = $true + AppDeployed = $true } $CurrentValue = [PSCustomObject]@{ - ChromePolicyDeployed = $ChromePolicyExists - EdgePolicyDeployed = $EdgePolicyExists + AppDeployed = [bool]$AppExists } Set-CIPPStandardsCompareField -FieldName 'standards.DeployCheckChromeExtension' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant Add-CIPPBPAField -FieldName 'DeployCheckChromeExtension' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $Tenant @@ -232,10 +412,10 @@ function Invoke-CIPPStandardDeployCheckChromeExtension { } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message - Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to deploy Check Chrome Extension policies. Error: $ErrorMessage" -sev Error + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to deploy $AppDisplayName. Error: $ErrorMessage" -sev Error if ($Settings.alert -eq $true) { - Write-StandardsAlert -message "Failed to deploy Check Chrome Extension policies: $ErrorMessage" -object @{ 'Error' = $ErrorMessage } -tenant $Tenant -standardName 'DeployCheckChromeExtension' -standardId $Settings.standardId + Write-StandardsAlert -message "Failed to deploy ${AppDisplayName}: $ErrorMessage" -object @{ 'Error' = $ErrorMessage } -tenant $Tenant -standardName 'DeployCheckChromeExtension' -standardId $Settings.standardId } if ($Settings.report -eq $true) { 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/Test-CIPPAccessTenant.ps1 b/Modules/CIPPCore/Public/Test-CIPPAccessTenant.ps1 index 271ce8a186b0..3795ccead20c 100644 --- a/Modules/CIPPCore/Public/Test-CIPPAccessTenant.ps1 +++ b/Modules/CIPPCore/Public/Test-CIPPAccessTenant.ps1 @@ -40,7 +40,7 @@ function Test-CIPPAccessTenant { OrchestratorName = 'CippAccessTenantTest' SkipLog = $true } - $null = Start-NewOrchestration -FunctionName CIPPOrchestrator -InputObject ($InputObject | ConvertTo-Json -Depth 10) + $null = Start-CIPPOrchestrator -InputObject $InputObject $Results = "Queued $($TenantList.Count) tenants for access checks" } else { diff --git a/Modules/CIPPCore/Public/Webhooks/Invoke-CIPPWebhookProcessing.ps1 b/Modules/CIPPCore/Public/Webhooks/Invoke-CIPPWebhookProcessing.ps1 index 90257fe4a22d..d1f1b139413d 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 @@ -135,6 +144,9 @@ function Invoke-CippWebhookProcessing { Title = $GenerateJSON.Title JSONContent = $JsonContent TenantFilter = $TenantFilter + APIName = 'Audit Log Alerts' + SchemaSource = 'Audit Log Alert' + InvokingCommand = 'Start-AuditLogProcessingOrchestrator' } Write-Host 'Sending Webhook Content' Send-CIPPAlert @CippAlert 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) diff --git a/Modules/CippExtensions/Public/Extension Functions/Remove-ExtensionAPIKey.ps1 b/Modules/CippExtensions/Public/Extension Functions/Remove-ExtensionAPIKey.ps1 new file mode 100644 index 000000000000..3e42100a218f --- /dev/null +++ b/Modules/CippExtensions/Public/Extension Functions/Remove-ExtensionAPIKey.ps1 @@ -0,0 +1,33 @@ +function Remove-ExtensionAPIKey { + <# + .FUNCTIONALITY + Internal + #> + param( + [Parameter(Mandatory = $true)] + [string]$Extension + ) + + $Var = "Ext_$Extension" + if ($env:AzureWebJobsStorage -eq 'UseDevelopmentStorage=true' -or $env:NonLocalHostAzurite -eq 'true') { + $DevSecretsTable = Get-CIPPTable -tablename 'DevSecrets' + $DevSecretRows = Get-AzDataTableEntity @DevSecretsTable -Filter "PartitionKey eq '$Extension'" + if ($DevSecretRows) { + Remove-AzDataTableEntity @DevSecretsTable -Entity @($DevSecretRows) -Force -ErrorAction Stop + Write-Information "Deleted $(@($DevSecretRows).Count) DevSecrets row(s) for '$Extension'." + } else { + Write-Information "No existing DevSecrets row found for '$Extension' to delete." + } + } else { + $keyvaultname = ($env:WEBSITE_DEPLOYMENT_ID -split '-')[0] + try { + $null = Remove-CippKeyVaultSecret -VaultName $keyvaultname -Name $Extension + } catch { + Write-Warning "Unable to delete secret '$Extension' from '$keyvaultname'" + } + } + + Remove-Item -Path "env:$Var" -Force -ErrorAction SilentlyContinue + + return $true +} diff --git a/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneExtensionScheduler.ps1 b/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneExtensionScheduler.ps1 index 6842752f197f..5ecdc7c992fc 100644 --- a/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneExtensionScheduler.ps1 +++ b/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneExtensionScheduler.ps1 @@ -48,7 +48,7 @@ function Invoke-NinjaOneExtensionScheduler { Batch = @($Batch) } #Write-Host ($InputObject | ConvertTo-Json) - $InstanceId = Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Depth 5 -Compress) + $InstanceId = Start-CIPPOrchestrator -InputObject $InputObject Write-Host "Started permissions orchestration with ID = '$InstanceId'" } @@ -90,7 +90,7 @@ function Invoke-NinjaOneExtensionScheduler { Batch = @($Batch) } #Write-Host ($InputObject | ConvertTo-Json) - $InstanceId = Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Depth 5 -Compress) + $InstanceId = Start-CIPPOrchestrator -InputObject $InputObject Write-Host "Started permissions orchestration with ID = '$InstanceId'" } diff --git a/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneOrgMapping.ps1 b/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneOrgMapping.ps1 index d6c8b7e1ad08..8acf6cc7e184 100644 --- a/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneOrgMapping.ps1 +++ b/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneOrgMapping.ps1 @@ -108,7 +108,7 @@ function Invoke-NinjaOneOrgMapping { Batch = @($Batch) } #Write-Host ($InputObject | ConvertTo-Json) - $InstanceId = Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Depth 5 -Compress) + $InstanceId = Start-CIPPOrchestrator -InputObject $InputObject Write-Host "Started permissions orchestration with ID = '$InstanceId'" } } diff --git a/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneSync.ps1 b/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneSync.ps1 index 24f64e54d351..53ea09e80a83 100644 --- a/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneSync.ps1 +++ b/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneSync.ps1 @@ -20,7 +20,7 @@ function Invoke-NinjaOneSync { Batch = @($Batch) } #Write-Host ($InputObject | ConvertTo-Json) - $InstanceId = Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Depth 5 -Compress) + $InstanceId = Start-CIPPOrchestrator -InputObject $InputObject Write-Host "Started permissions orchestration with ID = '$InstanceId'" }