core/modules/monkeyjob/public/Invoke-MonkeyJob.ps1
# Monkey365 - the PowerShell Cloud Security Tool for Azure and Microsoft 365 (copyright 2022) by Juan Garrido # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. Function Invoke-MonkeyJob{ <# .SYNOPSIS .DESCRIPTION .INPUTS .OUTPUTS .EXAMPLE .NOTES Author : Juan Garrido Twitter : @tr1ana File Name : Invoke-MonkeyJob Version : 1.0 .LINK https://github.com/silverhack/monkey365 #> [cmdletbinding(DefaultParameterSetName='ScriptBlock')] Param ( [Parameter(Mandatory=$True,position=0,ParameterSetName='ScriptBlock')] [System.Management.Automation.ScriptBlock]$ScriptBlock, [Parameter(Mandatory=$True, ParameterSetName = 'Command')] [String]$Command, [Parameter(Mandatory=$false, HelpMessage="arguments")] [Object]$Arguments, [Parameter(Mandatory=$false,ValueFromPipeline=$true)] $InputObject, [Parameter(HelpMessage="Variables to import into runspace")] [Object]$ImportVariables, [Parameter(HelpMessage="runspace")] [System.Management.Automation.Runspaces.RunspacePool]$Runspacepool, [Parameter(HelpMessage="modules to import into sessionState")] [Object]$ImportModules, [Parameter(HelpMessage="commands to import into sessionState")] [Object]$ImportCommands, [Parameter(HelpMessage="commands as AST to import into sessionState")] [Object]$ImportCommandAst, [Parameter(HelpMessage="Startup scripts (*ps1 files) to execute")] [System.Object[]]$StartUpScripts, [Parameter(HelpMessage="Minimum number of runspaces")] [ValidateRange(1,65535)] [int32]$MinThrottle = 1, [Parameter(HelpMessage="Maximum number of runspaces")] [ValidateRange(1,65535)] [int32]$Throttle = 2, [Parameter(HelpMessage="BatchSize")] [int32]$BatchSize = 80, [Parameter(HelpMessage="Pause between batchs in milliseconds")] [int32]$BatchSleep = 0, [Parameter(HelpMessage="Timeout before a thread stops trying to gather the information")] [ValidateRange(1,65535)] [int32]$Timeout = 10, [Parameter(HelpMessage="Increase Sleep Timer in seconds between child objects")] [ValidateRange(1,65535)] [int32]$SleepTimer = 5, [Parameter(HelpMessage="Increase Sleep Timer in seconds between child objects")] [ValidateRange(1,65535)] [int32]$MaxQueue = 10, [Parameter(HelpMessage="ApartmentState of the thread")] [ValidateSet("STA","MTA")] [String]$ApartmentState = "STA", [Parameter(HelpMessage="Reuse runspacePool")] [Switch]$ReuseRunspacePool, [Parameter(Mandatory=$False, HelpMessage='ThrowOnRunspaceOpenError')] [Switch]$ThrowOnRunspaceOpenError ) Begin{ $Verbose = $False; $Debug = $False; $InformationAction = 'SilentlyContinue' if($PSBoundParameters.ContainsKey('Verbose') -and $PSBoundParameters.Verbose){ $Verbose = $True } if($PSBoundParameters.ContainsKey('Debug') -and $PSBoundParameters.Debug){ $DebugPreference = 'Continue' $Debug = $True } if($PSBoundParameters.ContainsKey('InformationAction')){ $InformationAction = $PSBoundParameters['InformationAction'] } if($PSBoundParameters.ContainsKey('ReuseRunspacePool') -and $PSBoundParameters['ReuseRunspacePool'].IsPresent){ $reuseRSP = $True } else{ $reuseRSP = $false } if (-not $PSBoundParameters.ContainsKey('ThrowOnRunspaceOpenError')) { $ThrowOnRunspaceOpenError = $False } if( -not $PSBoundParameters.ContainsKey('MaxQueue') ) { $MaxQueue = 3 * $MaxQueue } else { $MaxQueue = $MaxQueue } if($PSBoundParameters.ContainsKey('Runspacepool') -and $PSBoundParameters['Runspacepool']){ if($Runspacepool.RunspacePoolStateInfo.State -eq [System.Management.Automation.Runspaces.RunspaceState]::BeforeOpen){ #Open runspace Write-Verbose $script:messages.OpenRunspaceMessage $Runspacepool.Open() } } Else{ #Create a new runspacePool $localparams = @{ ImportVariables = $ImportVariables; ImportModules = $ImportModules; ImportCommands = $ImportCommands; ImportCommandsAst = $ImportCommandAst; ApartmentState = $ApartmentState; MinThrottle = $MinThrottle; Throttle = $Throttle; StartUpScripts = $StartUpScripts; ThrowOnRunspaceOpenError = $ThrowOnRunspaceOpenError; Verbose = $Verbose; Debug = $Debug; InformationAction = $InformationAction; } #Get runspace pool $Runspacepool = New-RunspacePool @localparams if($null -ne $Runspacepool -and $Runspacepool -is [System.Management.Automation.Runspaces.RunspacePool]){ #Open runspace Write-Verbose $script:messages.OpenRunspaceMessage $Runspacepool.Open() #Add RunspacePool to array if($null -ne (Get-Variable -Name MonkeyRSP -ErrorAction Ignore)){ [void]$MonkeyRSP.Add($Runspacepool); } } } #Set Monkeyjobs variable $MyMonkeyJobs = [System.Collections.Generic.List[System.Management.Automation.PSObject]]::new() #Set timers, vars $Timer = [system.diagnostics.stopwatch]::StartNew() $SubTimer = [system.diagnostics.stopwatch]::StartNew() [int]$script:jobsCollected = 0 if($PSBoundParameters.ContainsKey('Timeout') -and $PSBoundParameters['TimeOut']){ #TimeOut is in Milliseconds [int]$Timeout = $PSBoundParameters['TimeOut'] * 1000 } else{ #TimeOut is in Milliseconds [int]$Timeout = 10 * 1000 } } Process{ if($null -ne $Runspacepool -and $Runspacepool.RunspacePoolStateInfo.State -eq [System.Management.Automation.Runspaces.RunspaceState]::Opened){ #Get scriptblock if any $param = @{ RunspacePool = $Runspacepool; Arguments = $Arguments; } if($PSBoundParameters.ContainsKey('InputObject') -and $PSBoundParameters['InputObject']){ [void]$param.Add('InputObject',$PSBoundParameters['InputObject']) } if($PSCmdlet.ParameterSetName -eq 'ScriptBlock'){ if($InputObject){ $sb = Set-ScriptBlock -ScriptBlock $ScriptBlock -AddInputObject } else{ $sb = Set-ScriptBlock -ScriptBlock $ScriptBlock } [void]$param.Add('ScriptBlock',$sb) } elseif($PSCmdlet.ParameterSetName -eq 'Command'){ [void]$param.Add('Command',$Command) } #Get new PowerShell Object $Pipeline = New-PowerShellObject @param if($Pipeline){ #Set Job name $jobName = ("MonkeyTask{0}" -f (Get-Random -Maximum 1000 -Minimum 1)); #Create a new Job $Job = [MonkeyJob]::new($Pipeline,$jobName); #Get new MonkeyJob object $newJob = New-MonkeyJobObject if($newJob -and $null -ne $Job){ #Populate job $newJob.RunspacePoolId = $Pipeline.RunspacePool.InstanceId; $newJob.Name = $jobName; $newJob.Job = $Job; if($PSCmdlet.ParameterSetName -eq 'ScriptBlock'){ $newJob.Command = $scriptblock.ToString(); } elseif($PSCmdlet.ParameterSetName -eq 'Command'){ $p = @{ Command = $Command; InputObject = $InputObject; Arguments = $Arguments; } $cmd = Format-Command @p if($cmd){ $newJob.Command = $cmd.ToString(); } } #Add to list [void]$MyMonkeyJobs.Add($newJob); [void]$MonkeyJobs.Add($newJob); } } } else{ if($Runspacepool.RunspacePoolStateInfo.State -ne [System.Management.Automation.Runspaces.RunspaceState]::Opened){ Write-Error ($script:messages.RunspaceError) return } else{ Write-Error ($script:messages.UnknownError) return } } } End{ try{ for($NumJob = 0 ; $NumJob -lt $MyMonkeyJobs.Count; $NumJob++){ $MonkeyJob = $MyMonkeyJobs.Item($NumJob) #Start Job $MonkeyJob.Task = $MonkeyJob.Job.StartTask(); #Check if maxQueue if($NumJob -ge $MaxQueue){ Write-Verbose ($script:messages.TimeSpentInvokeBatchMessage -f ($NumJob / $BatchSize), $SubTimer.Elapsed.ToString()) $SubTimer.Reset();$SubTimer.Start() $p = @{ Jobs = $MyMonkeyJobs; BatchSize = $BatchSize; Timeout = $Timeout; Jobscollected = ([ref]$Script:jobsCollected); } Watch-MonkeyJob @p $SubTimer.Stop() Write-Verbose ($script:messages.TimeSpentCollectBatchMessage -f ($NumJob / $BatchSize), $SubTimer.Elapsed.ToString()) $SubTimer.Reset() if($BatchSleep){ Write-Verbose ($script:messages.SleepMessage -f $BatchSleep) Start-Sleep -Milliseconds $BatchSleep } $SubTimer.Start() $MaxQueue += $BatchSize } } #All jobs are invoked at this time, just collect all of them Write-Verbose "Invoked all Jobs, Collecting the last jobs that are running" #Collect all jobs While ((@($MyMonkeyJobs | Where-Object {$_.Job.State -eq [System.Management.Automation.JobState]::Running})).count -gt 0){ #We want to collect all the Jobs, so just double the BatchSize $BS = (@($MyMonkeyJobs | Where-Object {$_.Job.State -eq [System.Management.Automation.JobState]::Running})).count * 2 $p = @{ Jobs = $MyMonkeyJobs; BatchSize = $BS; Timeout = $Timeout; Jobscollected = ([ref]$Script:jobsCollected); } Watch-MonkeyJob @p } } catch{ Write-Error ("MonkeyJob Error: {0}" -f $_) } finally{ #Get Data $completedJobs = $MyMonkeyJobs | Where-Object {$_.Job.State -eq [System.Management.Automation.JobState]::Completed} #Receive jobs $completedJobs | Receive-MonkeyJob #Clean objects if($MyMonkeyJobs.Count -gt 0){ Write-Verbose ($script:messages.TerminateJobMessage -f $MyMonkeyJobs.Count) foreach($MonkeyJob in $MyMonkeyJobs){ #Get potential exceptions $JobStatus = $MonkeyJob.Job.JobStatus(); if($JobStatus.Error.Count -gt 0){ foreach($exception in $JobStatus.Error.GetEnumerator()){ $JobError = [ordered]@{ Id = $MonkeyJob.Id; callStack = (Get-PSCallStack | Select-Object -First 1); ErrorStr = $exception.Exception.Message; Exception = $exception; } #Add exception to ref $errObj = New-Object PSObject -Property $JobError if($null -ne (Get-Variable -Name MonkeyJobErrors -ErrorAction Ignore)){ [void]$MonkeyJobErrors.Add($errObj) } } } #Clean MonkeyJob object #$MonkeyJob.Job.InnerJob.Stop(); $MonkeyJob.Job.InnerJob.Dispose(); if(!$ReuseRunspacePool){ #$MonkeyJob.Job.InnerJob.RunspacePool.Close(); $MonkeyJob.Job.InnerJob.RunspacePool.Dispose(); } if($MonkeyJob.Job.State -ne [System.Management.Automation.JobState]::Stopped){ $MonkeyJob.Job.StopJob(); } $MonkeyJob.Job.Dispose(); if($null -ne $MonkeyJob.Task){ $MonkeyJob.Task.Dispose(); $MonkeyJob.Task = $null; } [void]$MonkeyJobs.Remove($MonkeyJob) #Perform garbage collection [gc]::Collect() } } #Stop timer If($Timer.Isrunning){ Write-Verbose "Exiting script" Write-Verbose ("Jobs Collected: {0}" -f $script:jobsCollected) Write-Verbose ("Time took to Invoke and Complete the Jobs : {0}" -f $Timer.Elapsed.ToString()) $Timer.Stop() } #Dispose RunspacePool if(!$reuseRSP -and $null -ne $Runspacepool -and $Runspacepool -is [System.Management.Automation.Runspaces.RunspacePool]){ Write-Verbose $script:messages.CloseRunspaceMessage #https://github.com/PowerShell/PowerShell/issues/5746 #$Runspacepool.Close() $Runspacepool.Dispose() } #collect garbage #[gc]::Collect() [System.GC]::GetTotalMemory($true) | out-null } } } |