diff --git a/CIPPTimers.json b/CIPPTimers.json index e527285dd78f..7c03fd75d678 100644 --- a/CIPPTimers.json +++ b/CIPPTimers.json @@ -1,4 +1,12 @@ [ + { + "Command": "Start-DurableCleanup", + "Description": "Timer function to cleanup durable functions", + "Cron": "0 */15 * * * *", + "Priority": 0, + "RunOnProcessor": true, + "IsSystem": true + }, { "Command": "Start-UserTasksOrchestrator", "Description": "Orchestrator to process user scheduled tasks", diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecBECRemediate.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecBECRemediate.ps1 index cfa67b8bd043..f8bd672e0c43 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecBECRemediate.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecBECRemediate.ps1 @@ -27,8 +27,9 @@ Function Invoke-ExecBECRemediate { $Step = 'Disable Account' Set-CIPPSignInState -userid $username -AccountEnabled $false -tenantFilter $TenantFilter -APIName $APINAME -ExecutingUser $User $Step = 'Revoke Sessions' - Revoke-CIPPSessions -userid $SuspectUser -username $request.body.username -ExecutingUser $User -APIName $APINAME -tenantFilter $TenantFilter - + Revoke-CIPPSessions -userid $SuspectUser -username $username -ExecutingUser $User -APIName $APINAME -tenantFilter $TenantFilter + $Step = 'Remove MFA methods' + Remove-CIPPUserMFA -UserPrincipalName $username -TenantFilter $TenantFilter -ExecutingUser $User $Step = 'Disable Inbox Rules' $Rules = New-ExoRequest -anchor $username -tenantid $TenantFilter -cmdlet 'Get-InboxRule' -cmdParams @{Mailbox = $username; IncludeHidden = $true } $RuleDisabled = 0 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 881f35afbf93..6c59d1cd9346 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 @@ -17,37 +17,7 @@ Function Invoke-ExecResetMFA { $TenantFilter = $Request.Query.TenantFilter $UserID = $Request.Query.ID try { - Write-Host "Getting auth methods for $UserID" - $AuthMethods = New-GraphGetRequest -uri "https://graph.microsoft.com/v1.0/users/$UserID/authentication/methods" -tenantid $TenantFilter -AsApp $true - $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 $UserID, $MethodType, $Method.id) - }) - } - } - if (($Requests | Measure-Object).Count -eq 0) { - $Results = [pscustomobject]@{'Results' = "No MFA methods found for user $($Request.Query.ID)" } - Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ - StatusCode = [HttpStatusCode]::OK - Body = $Results - }) - return - } - - $Results = New-GraphBulkRequest -Requests $Requests -tenantid $TenantFilter -asapp $true -erroraction stop - - - if ($Results.status -eq 204) { - $Results = [pscustomobject]@{'Results' = "Successfully completed request. User $($Request.Query.ID) must supply MFA at next logon" } - } else { - $FailedAuthMethods = (($Results | Where-Object { $_.status -ne 204 }).id -split '-')[0] -join ', ' - $Results = [pscustomobject]@{'Results' = "Failed to reset MFA methods for $FailedAuthMethods" } - } + $Results = Remove-CIPPUserMFA -UserPrincipalName $UserID -TenantFilter $TenantFilter -ExecutingUser $request.headers.'x-ms-client-principal' } catch { $Results = [pscustomobject]@{'Results' = "Failed to reset MFA methods for $($Request.Query.ID): $(Get-NormalizedError -message $_.Exception.Message)" } Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -message "Failed to reset MFA for user $($Request.Query.ID): $($_.Exception.Message)" -Sev 'Error' -LogData (Get-CippException -Exception $_) diff --git a/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-DurableCleanup.ps1 b/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-DurableCleanup.ps1 new file mode 100644 index 000000000000..41b1d55c2081 --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-DurableCleanup.ps1 @@ -0,0 +1,61 @@ +function Start-DurableCleanup { + <# + .SYNOPSIS + Start the durable cleanup process. + + .DESCRIPTION + Look for orchestrators running for more than the specified time and terminate them. Also, clear any queues that have items for that function app. + + .PARAMETER MaxDuration + The maximum duration an orchestrator can run before being terminated. + + .FUNCTIONALITY + Internal + #> + + [CmdletBinding(SupportsShouldProcess = $true)] + param( + [int]$MaxDuration = 3600 + ) + $WarningPreference = 'SilentlyContinue' + $StorageContext = New-AzStorageContext -ConnectionString $env:AzureWebJobsStorage + $TargetTime = (Get-Date).ToUniversalTime().AddSeconds(-$MaxDuration) + $Context = New-AzDataTableContext -ConnectionString $env:AzureWebJobsStorage + $InstancesTables = Get-AzDataTable -Context $Context | Where-Object { $_ -match 'Instances' } + + $CleanupCount = 0 + $QueueCount = 0 + foreach ($Table in $InstancesTables) { + $Table = Get-CippTable -TableName $Table + $ClearQueues = $false + $FunctionName = $Table.TableName -replace 'Instances', '' + $Orchestrators = Get-CIPPAzDataTableEntity @Table -Filter "RuntimeStatus eq 'Running'" | Select-Object * -ExcludeProperty Input + $Orchestrators | Where-Object { $_.CreatedTime.DateTime -lt $TargetTime } | ForEach-Object { + $CreatedTime = [DateTime]::SpecifyKind($_.CreatedTime.DateTime, [DateTimeKind]::Utc) + $TimeSpan = New-TimeSpan -Start $CreatedTime -End (Get-Date).ToUniversalTime() + $RunningDuration = [math]::Round($TimeSpan.TotalMinutes, 2) + Write-Information "Orchestrator: $($_.PartitionKey), created: $CreatedTime, running for: $RunningDuration minutes" + $ClearQueues = $true + $_.RuntimeStatus = 'Failed' + if ($PSCmdlet.ShouldProcess($_.PartitionKey, 'Terminate Orchestrator')) { + $Orchestrator = Get-CIPPAzDataTableEntity @Table -PartitionKey $_.PartitionKey -RowKey $_.RowKey + $Orchestrator.RuntimeStatus = 'Failed' + Update-AzDataTableEntity @Table -Entity $Orchestrator + $CleanupCount++ + } + } + + if ($ClearQueues) { + $Queues = Get-AzStorageQueue -Context $StorageContext -Name ('{0}*' -f $FunctionName) | Select-Object -Property Name, ApproximateMessageCount, QueueClient + $RunningQueues = $Queues | Where-Object { $_.ApproximateMessageCount -gt 0 } + foreach ($Queue in $RunningQueues) { + Write-Information "- Removing queue: $($Queue.Name), message count: $($Queue.ApproximateMessageCount)" + if ($PSCmdlet.ShouldProcess($Queue.Name, 'Clear Queue')) { + $Queue.QueueClient.ClearMessagesAsync() | Out-Null + } + $QueueCount++ + } + } + } + Write-Information "Cleanup complete. $CleanupCount orchestrators were terminated. $QueueCount queues were cleared." +} diff --git a/Modules/CIPPCore/Public/Invoke-CIPPStandardsRun.ps1 b/Modules/CIPPCore/Public/Invoke-CIPPStandardsRun.ps1 index de30f5f78e99..cb3aaf4793ff 100644 --- a/Modules/CIPPCore/Public/Invoke-CIPPStandardsRun.ps1 +++ b/Modules/CIPPCore/Public/Invoke-CIPPStandardsRun.ps1 @@ -6,19 +6,26 @@ function Invoke-CIPPStandardsRun { [string]$TenantFilter = 'allTenants', [switch]$Force ) - Write-Host "Starting process for standards - $($tenantFilter)" + Write-Information "Starting process for standards - $($tenantFilter)" $AllTasks = Get-CIPPStandards -TenantFilter $TenantFilter if ($Force.IsPresent) { - Write-Host 'Clearing Rerun Cache' + Write-Information 'Clearing Rerun Cache' foreach ($Task in $AllTasks) { $null = Test-CIPPRerun -Type Standard -Tenant $Task.Tenant -API $Task.Standard -Clear } } + $TaskCount = ($AllTasks | Measure-Object).Count + if ($TaskCount -eq 0) { + Write-Information "No tasks found for tenant filter '$TenantFilter'" + return + } + + Write-Information "Found $TaskCount tasks for tenant filter '$TenantFilter'" #For each item in our object, run the queue. - $Queue = New-CippQueueEntry -Name "Applying Standards ($TenantFilter)" -TotalTasks ($AllTasks | Measure-Object).Count + $Queue = New-CippQueueEntry -Name "Applying Standards ($TenantFilter)" -TotalTasks $TaskCount $InputObject = [PSCustomObject]@{ OrchestratorName = 'StandardsOrchestrator' @@ -31,7 +38,9 @@ function Invoke-CIPPStandardsRun { } } + Write-Information 'Starting standards orchestrator' $InstanceId = Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Depth 5 -Compress) - Write-Host "Started orchestration with ID = '$InstanceId'" + Write-Information "Started orchestration with ID = '$InstanceId'" #$Orchestrator = New-OrchestrationCheckStatusResponse -Request $Request -InstanceId $InstanceId + return $InstanceId } diff --git a/Modules/CIPPCore/Public/Remove-CIPPUserMFA.ps1 b/Modules/CIPPCore/Public/Remove-CIPPUserMFA.ps1 new file mode 100644 index 000000000000..99d141ea9bc5 --- /dev/null +++ b/Modules/CIPPCore/Public/Remove-CIPPUserMFA.ps1 @@ -0,0 +1,65 @@ +function Remove-CIPPUserMFA { + <# + .SYNOPSIS + Remove MFA methods for a user + + .DESCRIPTION + Remove MFA methods for a user using bulk requests to the Microsoft Graph API + + .PARAMETER UserPrincipalName + UserPrincipalName of the user to remove MFA methods for + + .PARAMETER TenantFilter + Tenant where the user resides + + .EXAMPLE + Remove-CIPPUserMFA -UserPrincipalName testuser@contoso.com -TenantFilter contoso.com + + #> + [CmdletBinding(SupportsShouldProcess = $true)] + Param( + [Parameter(Mandatory = $true)] + [string]$UserPrincipalName, + [Parameter(Mandatory = $true)] + [string]$TenantFilter, + [Parameter(Mandatory = $false)] + [string]$ExecutingUser = 'CIPP' + ) + + Write-Information "Getting auth methods for $UserPrincipalName" + $AuthMethods = New-GraphGetRequest -uri "https://graph.microsoft.com/v1.0/users/$UserPrincipalName/authentication/methods" -tenantid $TenantFilter -AsApp $true + $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) + }) + } + } + if (($Requests | Measure-Object).Count -eq 0) { + Write-LogMessage -API 'Remove-CIPPUserMFA' -tenant $TenantFilter -message "No MFA methods found for user $UserPrincipalName" -sev 'Info' + $Results = [pscustomobject]@{'Results' = "No MFA methods found for user $($Request.Query.ID)" } + Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::OK + Body = $Results + }) + return + } + + if ($PSCmdlet.ShouldProcess("Remove MFA methods for $UserPrincipalName")) { + $Results = New-GraphBulkRequest -Requests $Requests -tenantid $TenantFilter -asapp $true -erroraction stop + if ($Results.status -eq 204) { + Write-LogMessage -API 'Remove-CIPPUserMFA' -tenant $TenantFilter -message "Successfully removed MFA methods for user $UserPrincipalName" -sev 'Info' + $Results = [pscustomobject]@{'Results' = "Successfully completed request. User $($Request.Query.ID) must supply MFA at next logon" } + } else { + $FailedAuthMethods = (($Results | Where-Object { $_.status -ne 204 }).id -split '-')[0] -join ', ' + Write-LogMessage -API 'Remove-CIPPUserMFA' -tenant $TenantFilter -message "Failed to remove MFA methods for $FailedAuthMethods" -sev 'Error' + $Results = [pscustomobject]@{'Results' = "Failed to reset MFA methods for $FailedAuthMethods" } + } + } + + return $Results +} diff --git a/Modules/CippEntrypoints/CippEntrypoints.psm1 b/Modules/CippEntrypoints/CippEntrypoints.psm1 index 34859bbae563..5aa18dee11e2 100644 --- a/Modules/CippEntrypoints/CippEntrypoints.psm1 +++ b/Modules/CippEntrypoints/CippEntrypoints.psm1 @@ -229,6 +229,7 @@ function Receive-CIPPTimerTrigger { } } catch { $Status = 'Failed' + Write-Information "Error in CIPPTimer for $($Function.Command): $($_.Exception.Message)" } $FunctionStatus.LastOccurrence = $UtcNow $FunctionStatus.Status = $Status