core/modules/monkeylogger/core/engine/logger.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 New-Logger{ <# .SYNOPSIS .DESCRIPTION .INPUTS .OUTPUTS .EXAMPLE .NOTES Author : Juan Garrido Twitter : @tr1ana File Name : New-Logger Version : 1.0 .LINK https://github.com/silverhack/monkey365 #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "", Scope="Function")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseOutputTypeCorrectly", "", Scope="Function")] [CmdletBinding()] Param ( [parameter(Mandatory= $false, HelpMessage= "Loggers")] [Array]$Loggers=@(), [parameter(Mandatory= $false, HelpMessage= "Initial path")] [String]$InitialPath, [parameter(Mandatory= $false, HelpMessage= "Queue logger")] [System.Collections.Concurrent.BlockingCollection`1[System.Management.Automation.InformationRecord]]$LogQueue ) Begin{ #Check informationAction if(-NOT $PSBoundParameters.ContainsKey('informationAction')){ $PSBoundParameters.Add('informationAction',$InformationPreference); } #Check Debug If($PSBoundParameters.ContainsKey('Debug') -and $PSBoundParameters.Debug){ $verbosity=@{Debug=$true} $DebugPreference = 'Continue' } Else{ $verbosity=@{Debug=$false} } If($PSBoundParameters.ContainsKey('Verbose') -and $PSBoundParameters.Verbose){ $verbosity.Add("Verbose",$true) $VerbosePreference = 'Continue' } Else{ $verbosity.Add("Verbose",$false) } #Check Log Queue If(-NOT $PSBoundParameters.ContainsKey('LogQueue')){ New-Variable -Name LogQueue -Scope Script ` -Value ([System.Collections.Concurrent.BlockingCollection[System.Management.Automation.InformationRecord]]::new()) -Force } Else{ New-Variable -Name LogQueue -Scope Script -Value $PSBoundParameters['LogQueue'] -Force } #Create object $logger = New-Object -Type PSObject -Property @{ path = Split-Path -Path $PSCmdlet.MyInvocation.PSCommandPath -Parent CallStack = (Get-PSCallStack | Select-Object -First 1); Callers = @() is_enabled = $false func_definitions = @() validation_functions = @() helper_functions = @() informationAction = $PSBoundParameters.informationAction verbosity= $verbosity Verbose = $verbosity.Verbose Debug = $verbosity.Debug DebugPreference = $DebugPreference VerbosePreference = $VerbosePreference loggers = $Loggers enabled_loggers = $null rootPath = $null; initialPath = $InitialPath } #Set informationAction, debug and verbose variables New-Variable -Name monkeyloggerinfoAction -Scope Script -Value $PSBoundParameters.informationAction -Force #New-Variable -Name informationAction -Scope Script -Value $informationAction -Force New-Variable -Name Debug -Scope Script -Value $verbosity.Debug -Force New-Variable -Name Verbose -Scope Script -Value $verbosity.Verbose -Force #Import write-information wrapper function #Load configuration $logger | Add-Member -Type ScriptMethod -Name loadConf -Value { #reset callers array $this.Callers = @() #Load configuration files $conf_path = ("{0}/targets" -f $this.path) #$conf_files = Get-ChildItem -Path $conf_path -Filter '*.json' $conf_files = [System.IO.Directory]::EnumerateFiles($conf_path,"*.json",[System.IO.SearchOption]::TopDirectoryOnly) foreach($conf_file in $conf_files){ $this.Callers += (Get-Content $conf_file -Raw) | ConvertFrom-Json } #Load output scripts $conf_path = ("{0}/output" -f $this.path) $output_callers = [System.IO.Directory]::EnumerateFiles($conf_path,"*.ps1",[System.IO.SearchOption]::TopDirectoryOnly) #$output_callers = Get-ChildItem -Path $conf_path -Filter '*.ps1' if($null -ne $output_callers){ #$this.func_definitions = Get-AstFunctionsFromFile -Files $output_callers $this.func_definitions = Get-AstFunction -Objects $output_callers } #Add validation functions $validators_path = ("{0}/core/init" -f $this.path) $validator_functions = [System.IO.Directory]::EnumerateFiles($validators_path,"*.ps1",[System.IO.SearchOption]::TopDirectoryOnly) #$validator_functions = Get-ChildItem -Path $validators_path -Filter '*.ps1' if($null -ne $validator_functions){ #$this.validation_functions = Get-AstFunctionsFromFile -Files $validator_functions $this.validation_functions = Get-AstFunction -Objects $validator_functions } #Add helpers functions $helpers_path = ("{0}/core/helpers" -f $this.path) $helpers_functions = [System.IO.Directory]::EnumerateFiles($helpers_path,"*.ps1",[System.IO.SearchOption]::TopDirectoryOnly) #$helpers_functions = Get-ChildItem -Path $helpers_path -Filter '*.ps1' if($null -ne $helpers_functions){ #$this.helper_functions = Get-AstFunctionsFromFile -Files $helpers_functions $this.helper_functions = Get-AstFunction -Objects $helpers_functions } } #Add init loggers method $logger | Add-Member -Type ScriptMethod -Name init_loggers -Value { $enabled_loggers = @() try{ $msg = [hashtable] @{ MessageData = "Initializing loggers" InformationAction = $this.informationAction CallStack = $logger.CallStack ForeGroundColor = "Green" tags = @('MonkeyLog') } Write-Information @msg foreach($new_logger in $this.loggers.GetEnumerator()){ #Check if should validate conf Try{ $should_validate = @($this.Callers).Where({$null -ne $_ -and $_.name -eq $new_logger.type}) | ` Select-Object -ExpandProperty validate ` -ErrorAction SilentlyContinue If($null -ne $should_validate){ $_function = @($logger.validation_functions).Where({$null -ne $_ -and $_.Name -eq $should_validate}) If($_function.Count -gt 0){ $validate_function = $_function.Body.GetScriptBlock() } } If($new_logger.configuration -and $null -ne $validate_function){ $config = Initialize-Configuration -Configuration $new_logger.configuration if($null -ne $config){ $status = Invoke-Command -ScriptBlock $validate_function -ArgumentList $config if($status -eq $false){ continue; } } } $internal_func = @($this.Callers).Where({$_.name -eq $new_logger.type}) | Select-Object -ExpandProperty function #check if internal function exists $exists = @($this.func_definitions).Where({$_.name -eq $internal_func}) If($internal_func -and $exists.Count -gt 0){ $new_logger | Add-Member -Type NoteProperty -name function -value $internal_func -Force $enabled_loggers+=$new_logger } } Catch{ $msg = [hashtable] @{ MessageData = $_.Exception.Message InformationAction = $this.informationAction CallStack = $this.CallStack ForeGroundColor = "Red" tags = @('MonkeyLog') } Write-Debug @msg } } } catch{ $msg = [hashtable] @{ Message = $_ InformationAction = $this.informationAction CallStack = $this.CallStack ForeGroundColor = "Yellow" tags = @('MonkeyLog') } Write-Error @msg } #Add enabled loggers $this.enabled_loggers = $enabled_loggers } #Add init method $logger | Add-Member -Type ScriptMethod -Name init -Value { #Check if log is enabled if($this.is_enabled){ $msg = [hashtable] @{ MessageData = "Log is already configured and active" InformationAction = $this.informationAction CallStack = $this.CallStack ForeGroundColor = "Yellow" tags = @('MonkeyLog') } Write-Information @msg return } #Initialize vars <# If($null -eq (Get-Variable -Name LogQueue -Scope Script -ErrorAction Ignore)){ New-Variable -Name LogQueue -Scope Script ` -Value ([System.Collections.Concurrent.BlockingCollection[System.Management.Automation.InformationRecord]]::new()) -Force } #> New-Variable -Name MonkeyLogRunspace -Scope Script -Option ReadOnly ` -Value ([hashtable]::Synchronized(@{ })) -Force New-Variable -Name _handle -Scope Script -Option ReadOnly ` -Value ([System.Threading.ManualResetEventSlim]::new($false)) -Force New-Variable -Name enabled_loggers -Scope Script -Value $this.enabled_loggers -Force $session_vars = @{ "LogQueue"=$LogQueue; "_handle"=$_handle; "enabled_loggers" = $this.enabled_loggers; } #Add DebugPreference and VerbosePreference to session state If($this.Debug){ [void]$session_vars.Add('DebugPreference','Continue') } If($this.Verbose){ [void]$session_vars.Add('VerbosePreference','Continue') } # $Script:InitialSessionState = New-LoggerSessionState -ImportVariables $session_vars -ApartmentState MTA #Setup runspace $Script:MonkeyLogRunspace.Runspace = [runspacefactory]::CreateRunspace($Host,$Script:InitialSessionState) $Script:MonkeyLogRunspace.Runspace.Name = 'Monkey365LogRunspace' $Script:MonkeyLogRunspace.Runspace.Open() $Script:MonkeyLogRunspace.Runspace.SessionStateProxy.SetVariable('verbosity', $this.verbosity) $Script:MonkeyLogRunspace.Runspace.SessionStateProxy.SetVariable('Debug', $this.Debug) $Script:MonkeyLogRunspace.Runspace.SessionStateProxy.SetVariable('Verbose', $this.Verbose) $Script:MonkeyLogRunspace.Runspace.SessionStateProxy.SetVariable('InformationAction', $this.informationAction) #Set location if($PSBoundParameters.ContainsKey('InitialPath') -and $PSBoundParameters['InitialPath']){ $Script:MonkeyLogRunspace.Runspace.SessionStateProxy.Path.SetLocation($InitialPath); } Try{ # Add the functions into the runspace @($this.func_definitions).Where({$null -ne $_}).Foreach( { [void]$Script:MonkeyLogRunspace.Runspace.SessionStateProxy.InvokeProvider.Item.Set( 'function:\{0}' -f $_.Name, $_.Body.GetScriptBlock()) } ) #Add the Write-Information/Debug/Warning/Verbose functions to sessionStateProxy $proxy_fncs = @('Write-Information','Write-Warning','Write-Debug','Write-Verbose','Write-Error') foreach($p_fnc in $proxy_fncs){ $_fnc = Get-Content ("function:\{0}" -f $p_fnc) if($null -ne $_fnc){ [void]$Script:MonkeyLogRunspace.Runspace.SessionStateProxy.InvokeProvider.Item.Set( 'function:\{0}' -f $_fnc.Ast.Name, $_fnc.Ast.Body.GetScriptBlock()) } } # Add helper functions into the runspace @($this.helper_functions).Where({$null -ne $_}).Foreach( { [void]$Script:MonkeyLogRunspace.Runspace.SessionStateProxy.InvokeProvider.Item.Set( 'function:\{0}' -f $_.Name, $_.Body.GetScriptBlock()) } ) } Catch{ $msg = [hashtable] @{ MessageData = $_.Exception.Message InformationAction = $this.informationAction CallStack = $this.CallStack ForeGroundColor = "Red" tags = @('MonkeyLog') } Write-Error @msg } $_handle.Set(); # # Spawn Logging Consumer $Consumer = { try{ # Lock LogQueue [System.Threading.Monitor]::Enter($LogQueue) $lock = $true foreach ($Log in $LogQueue.GetConsumingEnumerable()) { if($Log.type -eq '*' -or [string]::IsNullOrEmpty($Log.type)){ $enabled_channels = $enabled_loggers } else{ #Get channels $enabled_channels = $enabled_loggers | Where-Object {$_.type.ToLower() -in $Log.type.ToLower()} } foreach($channel in $enabled_channels){ try{ $function = Get-Content ("function:\{0}" -f $channel.function) if($null -ne $function){ $ArgumentList = @{Log=$Log;Configuration=$channel.configuration} $param = @{ ScriptBlock = {.$function @ArgumentList} } Invoke-Command @param } } catch{ $msg = [hashtable] @{ MessageData = $_.Exception.Message InformationAction = $this.informationAction CallStack = $this.CallStack ForeGroundColor = "Red" tags = @('MonkeyLog') } Write-Debug @msg } } } } catch{ $msg = [hashtable] @{ MessageData = $_.Exception.Message InformationAction = $this.informationAction CallStack = $this.CallStack ForeGroundColor = "Red" tags = @('MonkeyLog') } Write-Debug @msg } finally{ if($lock){ #Release lock [System.Threading.Monitor]::Exit($LogQueue) } } } $Script:MonkeyLogRunspace.Powershell = [System.Management.Automation.PowerShell]::Create().AddScript($Consumer) $Script:MonkeyLogRunspace.Powershell.Runspace = $Script:MonkeyLogRunspace.Runspace $Script:MonkeyLogRunspace.Handle = $Script:MonkeyLogRunspace.Powershell.BeginInvoke() if(-NOT $_handle.Wait([TimeSpan]::FromSeconds(5))){ $msg = [hashtable] @{ MessageData = "Unable to start Log" CallStack = $this.CallStack ForeGroundColor = "Red" tags = @('MonkeyLog') } Write-Warning @msg $this.stop() } Else{ If($_handle.isSet){ $msg = [hashtable] @{ MessageData = "Log enabled" InformationAction = $this.informationAction CallStack = $this.CallStack ForeGroundColor = "Green" tags = @('MonkeyLog') } Write-Information @msg $_handle.Dispose() #Set log status $this.is_enabled = $true } } } #Add stop method $logger | Add-Member -Type ScriptMethod -Name stop -Value { $msg = [hashtable] @{ MessageData = "Stopping loggers" InformationAction = $this.informationAction CallStack = $this.CallStack ForeGroundColor = "Green" tags = @('MonkeyLog') } Write-Information @msg #Check if log is stopped if($this.is_enabled -eq $false){ $msg = [hashtable] @{ MessageData = "Log is already stopped" InformationAction = $this.informationAction CallStack = $this.CallStack ForeGroundColor = "Yellow" tags = @('MonkeyLog') } Write-Information @msg return } $LogQueue.CompleteAdding() $LogQueue.Dispose() [void] $Script:MonkeyLogRunspace.Powershell.EndInvoke($Script:MonkeyLogRunspace.Handle) [void] $Script:MonkeyLogRunspace.Powershell.Dispose() #Closing runspace #[void] $Script:MonkeyLogRunspace.Runspace.Close() [void] $Script:MonkeyLogRunspace.Runspace.Dispose() #Remove environment variables #Remove-Variable -Scope Script -Force -Name LogQueue -ErrorAction SilentlyContinue Remove-Variable -Scope Script -Force -Name LogQueue -ErrorAction SilentlyContinue Remove-Variable -Scope Script -Force -Name MonkeyLogRunspace -ErrorAction SilentlyContinue Remove-Variable -Scope Script -Force -Name _handle -ErrorAction SilentlyContinue Remove-Variable -Scope Script -Force -Name enabled_loggers -ErrorAction SilentlyContinue Remove-Variable -Scope Script -Force -Name logger -ErrorAction SilentlyContinue Remove-Variable -Scope Script -Force -Name monkeyloggerinfoAction -ErrorAction SilentlyContinue Remove-Variable -Scope Script -Force -Name informationAction -ErrorAction Ignore Remove-Variable -Scope Script -Force -Name Debug -ErrorAction SilentlyContinue Remove-Variable -Scope Script -Force -Name Verbose -ErrorAction SilentlyContinue #Set log disabled $this.is_enabled = $false $msg = [hashtable] @{ MessageData = "Logger stopped" InformationAction = $this.informationAction CallStack = $this.CallStack ForeGroundColor = "Green" tags = @('MonkeyLog') } Write-Information @msg } } Process{ if($null -eq (Get-Variable -Name LogQueue -ErrorAction Ignore) -or $null -eq (Get-Variable -Name logger -ErrorAction Ignore)){ #Load configuration $logger.loadConf() #Initialize loggers $logger.init_loggers() #Init runspace $logger.init() } else{ $msg = [hashtable] @{ MessageData = "Log is already configured and active" InformationAction = $logger.informationAction CallStack = $logger.CallStack ForeGroundColor = "Yellow" tags = @('MonkeyLog') } Write-Warning @msg } } End{ if($logger.is_enabled){ Set-Variable logger -Value $logger -Scope Script -Force return $true } else{ return $null } } } |