├── Examples ├── Example-ExecutionTimeline.ps1 ├── Example-RestoreFromEvtx.ps1 ├── Example-RestoreLocal.ps1 └── Example-RestoreRemote.ps1 ├── License ├── PowerShellManager.psd1 ├── PowerShellManager.psm1 ├── Public ├── Get-PowerShellScriptExecution.ps1 └── Restore-PowerShellScript.ps1 └── readme.md /Examples/Example-ExecutionTimeline.ps1: -------------------------------------------------------------------------------- 1 | Import-Module $PSScriptRoot\..\PowerShellManager.psd1 -Force 2 | 3 | $Output = Get-PowerShellScriptExecution -Type WindowsPowerShell -Verbose 4 | $Output | Format-Table -------------------------------------------------------------------------------- /Examples/Example-RestoreFromEvtx.ps1: -------------------------------------------------------------------------------- 1 | Import-Module $PSScriptRoot\..\PowerShellManager.psd1 -Force 2 | 3 | Restore-PowerShellScript -Path $PSScriptRoot\ScriptsLocal -EventLogPath $Env:UserProfile\Downloads\Test.evtx -Verbose -Format -AddMarkdown -------------------------------------------------------------------------------- /Examples/Example-RestoreLocal.ps1: -------------------------------------------------------------------------------- 1 | Import-Module $PSScriptRoot\..\PowerShellManager.psd1 -Force 2 | 3 | Restore-PowerShellScript -Type WindowsPowerShell -Path $PSScriptRoot\ScriptsLocal -Verbose -Format -AddMarkdown -------------------------------------------------------------------------------- /Examples/Example-RestoreRemote.ps1: -------------------------------------------------------------------------------- 1 | Import-Module $PSScriptRoot\..\PowerShellManager.psd1 -Force 2 | 3 | # Keep in mind AD1/AD2 will do it in parallel 4 | Restore-PowerShellScript -Type WindowsPowerShell -Path $PSScriptRoot\ScriptsRemote -ComputerName AD1, AD2 -Verbose -Format -AddMarkdown -------------------------------------------------------------------------------- /License: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Evotec 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /PowerShellManager.psd1: -------------------------------------------------------------------------------- 1 | @{ 2 | AliasesToExport = @() 3 | Author = 'Przemyslaw Klys' 4 | CmdletsToExport = @() 5 | CompanyName = 'Evotec' 6 | CompatiblePSEditions = @('Desktop', 'Core') 7 | Copyright = '(c) 2011 - 2021 Przemyslaw Klys @ Evotec. All rights reserved.' 8 | Description = 'Project that help restoring malware / run / deleted scripts straight from Event Logs for further analysis' 9 | FunctionsToExport = @('Get-PowerShellScriptExecution', 'Restore-PowerShellScript') 10 | GUID = '759cffa8-9eae-4c4c-a047-e3516e482e4f' 11 | ModuleVersion = '0.1.2' 12 | PowerShellVersion = '5.1' 13 | PrivateData = @{ 14 | PSData = @{ 15 | Tags = @('Windows', 'Restore', 'PowerShellScript', 'Malware', 'EventLog') 16 | ProjectUri = 'https://github.com/EvotecIT/PowerShellManager' 17 | } 18 | } 19 | RequiredModules = @(@{ 20 | ModuleVersion = '1.0.17' 21 | ModuleName = 'PSEventViewer' 22 | Guid = '5df72a79-cdf6-4add-b38d-bcacf26fb7bc' 23 | }, @{ 24 | ModuleVersion = '1.19.1' 25 | ModuleName = 'PSScriptAnalyzer' 26 | Guid = 'd6245802-193d-4068-a631-8863a4342a18' 27 | }) 28 | RootModule = 'PowerShellManager.psm1' 29 | } -------------------------------------------------------------------------------- /PowerShellManager.psm1: -------------------------------------------------------------------------------- 1 | #Get public and private function definition files. 2 | $Public = @( Get-ChildItem -Path $PSScriptRoot\Public\*.ps1 -ErrorAction SilentlyContinue -Recurse ) 3 | $Private = @( Get-ChildItem -Path $PSScriptRoot\Private\*.ps1 -ErrorAction SilentlyContinue -Recurse ) 4 | 5 | $AssemblyFolders = Get-ChildItem -Path $PSScriptRoot\Lib -Directory -ErrorAction SilentlyContinue 6 | if ($AssemblyFolders.BaseName -contains 'Standard') { 7 | $Assembly = @( Get-ChildItem -Path $PSScriptRoot\Lib\Standard\*.dll -ErrorAction SilentlyContinue ) 8 | } else { 9 | if ($PSEdition -eq 'Core') { 10 | $Assembly = @( Get-ChildItem -Path $PSScriptRoot\Lib\Core\*.dll -ErrorAction SilentlyContinue ) 11 | } else { 12 | $Assembly = @( Get-ChildItem -Path $PSScriptRoot\Lib\Default\*.dll -ErrorAction SilentlyContinue ) 13 | } 14 | } 15 | $FoundErrors = @( 16 | Foreach ($Import in @($Assembly)) { 17 | try { 18 | Add-Type -Path $Import.Fullname -ErrorAction Stop 19 | } catch [System.Reflection.ReflectionTypeLoadException] { 20 | Write-Warning "Processing $($Import.Name) Exception: $($_.Exception.Message)" 21 | $LoaderExceptions = $($_.Exception.LoaderExceptions) | Sort-Object -Unique 22 | foreach ($E in $LoaderExceptions) { 23 | Write-Warning "Processing $($Import.Name) LoaderExceptions: $($E.Message)" 24 | } 25 | $true 26 | #Write-Error -Message "StackTrace: $($_.Exception.StackTrace)" 27 | } catch { 28 | Write-Warning "Processing $($Import.Name) Exception: $($_.Exception.Message)" 29 | $LoaderExceptions = $($_.Exception.LoaderExceptions) | Sort-Object -Unique 30 | foreach ($E in $LoaderExceptions) { 31 | Write-Warning "Processing $($Import.Name) LoaderExceptions: $($E.Message)" 32 | } 33 | $true 34 | #Write-Error -Message "StackTrace: $($_.Exception.StackTrace)" 35 | } 36 | } 37 | #Dot source the files 38 | Foreach ($Import in @($Public + $Private)) { 39 | Try { 40 | . $Import.Fullname 41 | } Catch { 42 | Write-Error -Message "Failed to import functions from $($import.Fullname): $_" 43 | $true 44 | } 45 | } 46 | ) 47 | 48 | if ($FoundErrors.Count -gt 0) { 49 | $ModuleName = (Get-ChildItem $PSScriptRoot\*.psd1).BaseName 50 | Write-Warning "Importing module $ModuleName failed. Fix errors before continuing." 51 | break 52 | } 53 | 54 | Export-ModuleMember -Function '*' -Alias '*' -------------------------------------------------------------------------------- /Public/Get-PowerShellScriptExecution.ps1: -------------------------------------------------------------------------------- 1 | function Get-PowerShellScriptExecution { 2 | [cmdletBinding(DefaultParameterSetName = 'Request')] 3 | param( 4 | [Parameter(ParameterSetName = 'Request', Mandatory)][ValidateSet('PowerShell', 'WindowsPowerShell')][string] $Type, 5 | [Parameter(ParameterSetName = 'Request')][string[]] $ComputerName, 6 | [Parameter(ParameterSetName = 'Events')][Array] $Events, 7 | [DateTime] $DateFrom, 8 | [DateTime] $DateTo 9 | ) 10 | if (-not $Events) { 11 | $getEventsSplat = [ordered] @{ 12 | ID = 4100 13 | DateFrom = $DateFrom 14 | DateTo = $DateTo 15 | } 16 | if ($ComputerName) { 17 | $getEventsSplat.Computer = $ComputerName 18 | } 19 | if ($Type -eq 'WindowsPowerShell') { 20 | $getEventsSplat['LogName'] = 'Microsoft-Windows-PowerShell/Operational' 21 | } elseif ($Type -eq 'PowerShell') { 22 | $getEventsSplat['LogName'] = 'PowerShellCore/Operational' 23 | } 24 | if ($EventLogPath) { 25 | Path = $EventLogPath 26 | } 27 | $Events = Get-Events @getEventsSplat -Verbose:$VerbosePreference 28 | } 29 | 30 | foreach ($Event in $Events) { 31 | $ContextInfo = $Event.ContextInfo.Split([Environment]::NewLine) 32 | $EventEntry = [ordered] @{} 33 | foreach ($Entry in $ContextInfo) { 34 | if ($Entry.Trim()) { 35 | $Data = $Entry.Trim() -split '=' 36 | $FieldName = "$($Data[0])".Replace(' ', '') 37 | $EventEntry[$FieldName] = if ($Data[1]) { $Data[1].Trim() } else { $null } 38 | } 39 | } 40 | [PSCustomObject] $EventEntry 41 | } 42 | } -------------------------------------------------------------------------------- /Public/Restore-PowerShellScript.ps1: -------------------------------------------------------------------------------- 1 | function Restore-PowerShellScript { 2 | [cmdletBinding(DefaultParameterSetName = 'Request')] 3 | param( 4 | [Parameter(ParameterSetName = 'Request', Mandatory)][ValidateSet('PowerShell', 'WindowsPowerShell')][string] $Type, 5 | [Parameter(ParameterSetName = 'Request')][string[]] $ComputerName, 6 | [Parameter(ParameterSetName = 'Events')][Array] $Events, 7 | [Parameter(ParameterSetName = 'File')][string] $EventLogPath, 8 | [Parameter(Mandatory)][Alias('FolderPath')][string] $Path, 9 | 10 | [DateTime] $DateFrom, 11 | [DateTime] $DateTo, 12 | [switch] $AddMarkdown, 13 | [switch] $Format, 14 | [switch] $Unblock 15 | ) 16 | if (-not $Events) { 17 | $getEventsSplat = [ordered] @{ 18 | ID = 4103, 4104 19 | DateFrom = $DateFrom 20 | DateTo = $DateTo 21 | } 22 | if ($ComputerName) { 23 | $getEventsSplat.Computer = $ComputerName 24 | } 25 | if ($Type -eq 'WindowsPowerShell') { 26 | $getEventsSplat['LogName'] = 'Microsoft-Windows-PowerShell/Operational' 27 | } elseif ($Type -eq 'PowerShell') { 28 | $getEventsSplat['LogName'] = 'PowerShellCore/Operational' 29 | } 30 | if ($EventLogPath -and (Test-Path -LiteralPath $EventLogPath)) { 31 | $getEventsSplat['Path'] = $EventLogPath 32 | } 33 | $Events = Get-Events @getEventsSplat -Verbose:$VerbosePreference -DisableParallel 34 | } 35 | $FormatterSettings = @{ 36 | IncludeRules = @( 37 | 'PSPlaceOpenBrace', 38 | 'PSPlaceCloseBrace', 39 | 'PSUseConsistentWhitespace', 40 | 'PSUseConsistentIndentation', 41 | 'PSAlignAssignmentStatement', 42 | 'PSUseCorrectCasing' 43 | ) 44 | Rules = @{ 45 | PSPlaceOpenBrace = @{ 46 | Enable = $true 47 | OnSameLine = $true 48 | NewLineAfter = $true 49 | IgnoreOneLineBlock = $true 50 | } 51 | 52 | PSPlaceCloseBrace = @{ 53 | Enable = $true 54 | NewLineAfter = $false 55 | IgnoreOneLineBlock = $true 56 | NoEmptyLineBefore = $false 57 | } 58 | 59 | PSUseConsistentIndentation = @{ 60 | Enable = $true 61 | Kind = 'space' 62 | PipelineIndentation = 'IncreaseIndentationAfterEveryPipeline' 63 | IndentationSize = 4 64 | } 65 | 66 | PSUseConsistentWhitespace = @{ 67 | Enable = $true 68 | CheckInnerBrace = $true 69 | CheckOpenBrace = $true 70 | CheckOpenParen = $true 71 | CheckOperator = $true 72 | CheckPipe = $true 73 | CheckSeparator = $true 74 | } 75 | 76 | PSAlignAssignmentStatement = @{ 77 | Enable = $true 78 | CheckHashtable = $true 79 | } 80 | 81 | PSUseCorrectCasing = @{ 82 | Enable = $true 83 | } 84 | } 85 | } 86 | $Cache = [ordered] @{} 87 | foreach ($U in $Events) { 88 | if ($null -eq $U.ScriptBlockText -or $U.ScriptBlockText -eq 0) { 89 | continue 90 | } 91 | if (-not $Cache[$U.ScriptBlockId]) { 92 | $Cache[$U.ScriptBlockId] = [ordered] @{} 93 | } 94 | $Cache[$U.ScriptBlockId]["0"] = $U 95 | $Cache[$U.ScriptBlockId]["$($U.MessageNumber)"] = $U.ScriptBlockText 96 | } 97 | if (-not (Test-Path -Path $Path)) { 98 | $null = New-Item -ItemType Directory -Path $Path 99 | } 100 | foreach ($ScriptBlockID in $Cache.Keys) { 101 | [int] $ScriptBlockCount = $Cache[$ScriptBlockID]['0'].MessageTotal 102 | [string] $Script = for ($i = 1; $i -le $ScriptBlockCount; $i++) { 103 | $Cache[$ScriptBlockID]["$i"] 104 | } 105 | if ($Format) { 106 | try { 107 | $Script = Invoke-Formatter -ScriptDefinition $Script -Settings $FormatterSettings -ErrorAction Stop 108 | } catch { 109 | Write-Warning "Restore-PowerShellScript - Formatter failed to format. Skipping formatting." 110 | } 111 | } 112 | $FileName = -join ($($Cache[$ScriptBlockID]['0'].MachineName), '_', "$($ScriptBlockID).ps1") 113 | $FilePath = [io.path]::Combine($Path, $FileName) 114 | if ($AddMarkdown) { 115 | @( 116 | '<#' 117 | "RecordID = $($Cache[$ScriptBlockID]['0'].RecordID)" 118 | "LogName = $($Cache[$ScriptBlockID]['0'].LogName)" 119 | 120 | "MessageTotal = $($Cache[$ScriptBlockID]['0'].MessageTotal)" 121 | "MachineName = $($Cache[$ScriptBlockID]['0'].MachineName)" 122 | "UserId = $($Cache[$ScriptBlockID]['0'].UserId)" 123 | "TimeCreated = $($Cache[$ScriptBlockID]['0'].TimeCreated)" 124 | "LevelDisplayName = $($Cache[$ScriptBlockID]['0'].LevelDisplayName)" 125 | '#>' 126 | $Script 127 | ) | Out-File -FilePath $FilePath 128 | } else { 129 | $Script | Out-File -FilePath $FilePath 130 | } 131 | if (-not $Unblock) { 132 | $data = [System.Text.StringBuilder]::new().AppendLine('[ZoneTransfer]').Append('ZoneId=3').ToString() 133 | Set-Content -Path $FilePath -Stream "Zone.Identifier" -Value $data 134 | } 135 | } 136 | } -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 |
6 | 7 | 13 | 14 | 19 | 20 | # PowerShellManager 21 | 22 | Little PowerShell module to extract PowerShell scripts that no longer exists on disk but were run and are still in Event Logs. More information [available on blog post](https://evotec.xyz/restoring-recovering-powershell-scripts-from-event-logs/). 23 | 24 | # Usage 25 | 26 | Extracing PowerShell scripts from Windows PowerShell Event Log and saving it to ScriptsLocal directory in same folder. 27 | Format makes sure the script is formatted and, and AddMarkdown adds additional information to asses where the script is coming from. 28 | 29 | ```powershell 30 | Restore-PowerShellScript -Type WindowsPowerShell -Path $PSScriptRoot\ScriptsLocal -Verbose -Format -AddMarkdown 31 | ``` 32 | 33 | Same as above but with a difference that it scans remote servers (two of them). It does it in parallel. 34 | 35 | ```powershell 36 | # Keep in mind AD1/AD2 will do it in parallel 37 | Restore-PowerShellScript -Type WindowsPowerShell -Path $PSScriptRoot\ScriptsRemote -ComputerName AD1, AD2 -Verbose -Format -AddMarkdown 38 | ``` 39 | 40 | ## To install 41 | 42 | Just install module from PowerShellGallery. 43 | 44 | ```powershell 45 | Install-Module -Name PowerShellManager -AllowClobber -Force 46 | ``` 47 | 48 | Force and AllowClobber aren't necessary, but they do skip errors in case some appear. 49 | 50 | ## And to update 51 | 52 | ```powershell 53 | Update-Module -Name PowerShellManager 54 | ``` 55 | 56 | That's it. Whenever there's a new version, you run the command, and you can enjoy it. Remember that you may need to close, reopen PowerShell session if you have already used module before updating it. 57 | 58 | **The essential thing** is if something works for you on production, keep using it till you test the new version on a test computer. I do changes that may not be big, but big enough that auto-update may break your code. For example, small rename to a parameter and your code stops working! Be responsible! 59 | 60 | ## Changelog 61 | 62 | - 0.1.2 - 2021.01.19 63 | - Fix for reading from file system 64 | - 0.1.1 - 2020.08.28 65 | - Additional security (prevents from accidental execution) 66 | - First release 67 | --------------------------------------------------------------------------------