[CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$OutputDir, [Parameter(Mandatory = $true)] [ValidateSet('pre-remediation', 'post-remediation')] [string]$Phase, [switch]$CleanupKnownIndicators ) $ErrorActionPreference = 'Continue' $ProgressPreference = 'SilentlyContinue' $IncidentStart = [datetime]'2026-08-11T00:00:00' $Instance = 'fa12121053c2d7fe' $ServiceName = 'ScreenConnect Client (fa12121053c2d7fe)' $RelayHost = 'relay.rupilure.top' $RelayIp = '74.120.121.48' $KnownHashes = @( '1786e22352f7936c9d28b1f94e3e84994191f88d4cee78abef11c0ff9b4cc4b9', '220aec6ce4e45fbaf2f807bab0478680c342f8eaf99bd95de726eae730d6926b', 'e75eca7a5b879736fbd4413e59bf2ad311b526d29bb0200871123d1cbc2e8655', '70540e6547c48bcd46c6f2a8d4e200d8b3b30e638cf5c338ff70c3d85cf9b520', '69f8d818abe17849a3a9b0f55ec0b5db98b4ba8449d91a081019e6d3a7c3c44d', '090578574f598d901a38ae4dd92927e4fef15fd24d69b60370e9bf771c90b4a2' ) $RemoteToolPattern = '(?i)screenconnect|connectwise|anydesk|rustdesk|teamviewer|splashtop|atera|syncro|meshcentral|dwservice|logmein|gotoassist|supremo|ultraviewer|remotepc|bomgar|beyondtrust|tacticalrmm|level\.io|action1|pulseway' $SuspiciousCommandPattern = '(?i)-enc(odedcommand)?\b|frombase64string|downloadstring|invoke-webrequest|iex\s*\(|mshta(?:\.exe)?|rundll32(?:\.exe)?|regsvr32(?:\.exe)?|wscript(?:\.exe)?|cscript(?:\.exe)?|powershell(?:\.exe)?.*(-w\s+hidden|-windowstyle\s+hidden)|relay\.rupilure\.top|fa12121053c2d7fe' $UserWritablePattern = '(?i)\\Users\\|\\ProgramData\\|\\Windows\\Temp\\|\\AppData\\|\\Downloads\\|\\Desktop\\|\\Public\\' # Split out of the pattern above so a task can be judged on WHAT IT DOES # separately from WHICH INTERPRETER it runs. Windows built-ins legitimately run # rundll32; none of them need an encoded payload or a URL. $SuspiciousArgumentPattern = '(?i)-e(nc|ncoded|ncodedcommand)?\s+[A-Za-z0-9+/=]{16,}|-enc(odedcommand)?\b|frombase64string|downloadstring|downloadfile|invoke-webrequest|invoke-expression|iex\s*\(|-w\s+hidden|-windowstyle\s+hidden|https?://|\\\\[^\\]+\\|relay\.rupilure\.top|fa12121053c2d7fe' $LolBinPattern = '(?i)\b(mshta|rundll32|regsvr32|wscript|cscript|powershell|pwsh|certutil|bitsadmin|curl|wget)(\.exe)?\b' # The recent-file sweep walks %ProgramData%, which is where this tool puts what # it quarantines. Without this the post-remediation report raises the payloads # it just contained as live Critical findings and reads like a failed cleanup. # Never allowed to end up empty: StartsWith('') is true for every path, which # would quietly downgrade every Critical file finding to a quarantine note. $RemediationRoot = if ($env:ProgramData) { Join-Path $env:ProgramData 'Argus-Omni-ScreenConnect-Remediation' } else { 'C:\ProgramData\Argus-Omni-ScreenConnect-Remediation' } New-Item -ItemType Directory -Force -Path $OutputDir | Out-Null $Findings = [System.Collections.Generic.List[object]]::new() $Errors = [System.Collections.Generic.List[object]]::new() $CleanupActions = [System.Collections.Generic.List[object]]::new() $FileFactCache = @{} function Add-Finding { param([string]$Severity, [string]$Category, [string]$Title, [string]$Evidence, [string]$Recommendation) $Findings.Add([pscustomobject]@{ Severity = $Severity Category = $Category Title = $Title Evidence = $Evidence Recommendation = $Recommendation }) } function Add-AuditError { param([string]$Stage, [object]$Exception) $Errors.Add([pscustomobject]@{ Stage = $Stage; Error = [string]$Exception }) } function Save-Csv { param([string]$Name, [object[]]$Data) $path = Join-Path $OutputDir $Name $items = @($Data | Where-Object { $null -ne $_ }) if ($items.Count -gt 0) { $items | Export-Csv -NoTypeInformation -Encoding UTF8 -Path $path } else { [System.IO.File]::WriteAllText($path, '', [System.Text.UTF8Encoding]::new($false)) } } function Get-PathFromCommand { param([string]$Command) if ([string]::IsNullOrWhiteSpace($Command)) { return $null } $expanded = [Environment]::ExpandEnvironmentVariables($Command.Trim()) if ($expanded.StartsWith('"')) { $end = $expanded.IndexOf('"', 1) if ($end -gt 1) { return $expanded.Substring(1, $end - 1) } } if ($expanded -match '^(.*?\.(?:exe|dll|sys|com|bat|cmd|ps1|vbs|js|msi))\b') { return $Matches[1] } return ($expanded -split '\s+')[0] } function Get-FileFacts { param([string]$Path) $result = [ordered]@{ Path = $Path; Exists = $false; SHA256 = ''; Signature = ''; Signer = ''; LastWriteTime = $null } if ([string]::IsNullOrWhiteSpace($Path) -or -not (Test-Path -LiteralPath $Path -PathType Leaf)) { return [pscustomobject]$result } $cacheKey = $Path.ToLowerInvariant() if ($FileFactCache.ContainsKey($cacheKey)) { return $FileFactCache[$cacheKey] } try { $item = Get-Item -LiteralPath $Path -Force $result.Exists = $true $result.LastWriteTime = $item.LastWriteTime $result.SHA256 = (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash.ToLowerInvariant() $signature = Get-AuthenticodeSignature -LiteralPath $Path $result.Signature = [string]$signature.Status $result.Signer = [string]$signature.SignerCertificate.Subject } catch { Add-AuditError "file-facts:$Path" $_ } $object = [pscustomobject]$result $FileFactCache[$cacheKey] = $object $object } function Has-KnownMarker { param([string]$Text) if ([string]::IsNullOrWhiteSpace($Text)) { return $false } return $Text -match "(?i)$([regex]::Escape($Instance))|$([regex]::Escape($RelayHost))|$([regex]::Escape($RelayIp))" } function Add-CleanupAction { param([string]$Category, [string]$Target, [string]$Result) $CleanupActions.Add([pscustomobject]@{ Time = (Get-Date).ToString('o') Category = $Category Target = $Target Result = $Result }) } # Everything this script removes is written out first. A responder who later # needs to prove what the intrusion set had installed cannot recover a deleted # task registration or a cleared registry value from the removal log alone. function Save-CleanupEvidence { param([string]$Category, [string]$Name, [object]$Content) try { $folder = Join-Path $OutputDir 'cleanup-evidence' New-Item -ItemType Directory -Path $folder -Force | Out-Null $safe = ($Name -replace '[^A-Za-z0-9._-]', '_') if ($safe.Length -gt 120) { $safe = $safe.Substring(0, 120) } $file = Join-Path $folder ("{0}-{1}-{2}.txt" -f $Category, $safe, [guid]::NewGuid().ToString('N').Substring(0, 8)) $text = if ($Content -is [string]) { $Content } else { $Content | Format-List * | Out-String -Width 4096 } [System.IO.File]::WriteAllText($file, $text, [System.Text.UTF8Encoding]::new($false)) # Callers refuse to remove anything when this returns empty, so the # success answer has to mean the bytes are actually on disk. if (-not (Test-Path -LiteralPath $file)) { throw "evidence file was not written: $file" } return $file } catch { Add-AuditError "cleanup-evidence:$Category/$Name" $_ return '' } } # Two bulk queries instead of two per rule. Measured on a Windows 11 guest with # 798 rules: every filter in 2.6 s, against 3.0 s PER RULE fetched individually. # At 550 enabled rules that is a projected 27 minutes, and it stalled a real # audit long enough to look like a hang. function Get-FirewallFilterMap { param([ValidateSet('Application', 'Port')][string]$Kind) $map = @{} try { $all = if ($Kind -eq 'Application') { Get-NetFirewallApplicationFilter -All -ErrorAction Stop } else { Get-NetFirewallPortFilter -All -ErrorAction Stop } foreach ($filter in $all) { $map[[string]$filter.InstanceID] = $filter } } catch { Add-AuditError "firewall-filters:$Kind" $_ } return $map } function Remove-KnownPersistence { foreach ($task in Get-ScheduledTask -ErrorAction SilentlyContinue) { try { $actionText = (@($task.Actions) | ForEach-Object { "$($_.Execute) $($_.Arguments) $($_.WorkingDirectory)" }) -join ' ' if (-not (Has-KnownMarker "$($task.TaskPath)$($task.TaskName) $actionText")) { continue } $definition = $null try { $definition = Export-ScheduledTask -TaskPath $task.TaskPath -TaskName $task.TaskName -ErrorAction Stop } catch { $definition = $task | Format-List * | Out-String -Width 4096 } $evidence = Save-CleanupEvidence ScheduledTask "$($task.TaskPath)$($task.TaskName)" ([string]$definition) if (-not $evidence) { Add-AuditError "cleanup-skipped-no-evidence:ScheduledTask/$($task.TaskPath)$($task.TaskName)" 'evidence write failed; task left in place'; continue } Disable-ScheduledTask -TaskPath $task.TaskPath -TaskName $task.TaskName -ErrorAction SilentlyContinue | Out-Null Unregister-ScheduledTask -TaskPath $task.TaskPath -TaskName $task.TaskName -Confirm:$false -ErrorAction Stop Add-CleanupAction ScheduledTask "$($task.TaskPath)$($task.TaskName)" Removed } catch { Add-AuditError "cleanup-scheduled-task:$($task.TaskPath)$($task.TaskName)" $_ } } foreach ($key in $AutorunKeys) { try { if (-not (Test-Path $key)) { continue } $item = Get-ItemProperty $key } catch { Add-AuditError "cleanup-autorun:$key" $_; continue } foreach ($property in $item.PSObject.Properties) { try { if ($property.Name -match '^PS') { continue } if (-not (Has-KnownMarker ([string]$property.Value))) { continue } $evidence = Save-CleanupEvidence Autorun "$key-$($property.Name)" "$key`n$($property.Name) = $([string]$property.Value)" if (-not $evidence) { Add-AuditError "cleanup-skipped-no-evidence:Autorun/$key::$($property.Name)" 'evidence write failed; value left in place'; continue } Remove-ItemProperty -Path $key -Name $property.Name -Force -ErrorAction Stop Add-CleanupAction Autorun "$key::$($property.Name)" Removed } catch { Add-AuditError "cleanup-autorun:$key::$($property.Name)" $_ } } } foreach ($image in Get-ChildItem 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options' -ErrorAction SilentlyContinue) { try { $debugger = (Get-ItemProperty $image.PSPath -Name Debugger -ErrorAction SilentlyContinue).Debugger if (-not (Has-KnownMarker ([string]$debugger))) { continue } $evidence = Save-CleanupEvidence IFEO $image.PSChildName "$($image.PSPath)`nDebugger = $([string]$debugger)" if (-not $evidence) { Add-AuditError "cleanup-skipped-no-evidence:IFEO/$($image.PSChildName)" 'evidence write failed; debugger left in place'; continue } Remove-ItemProperty -LiteralPath $image.PSPath -Name Debugger -Force -ErrorAction Stop Add-CleanupAction IFEO $image.PSChildName Removed } catch { Add-AuditError "cleanup-ifeo:$($image.PSChildName)" $_ } } try { $knownWmiPaths = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) $knownWmiObjects = [System.Collections.Generic.List[object]]::new() # Out-String wraps at the host width by default, which can split a relay # name or instance id across lines and hide it from the marker test. foreach ($class in 'CommandLineEventConsumer', 'ActiveScriptEventConsumer', '__EventFilter') { foreach ($entry in Get-CimInstance -Namespace root/subscription -ClassName $class -ErrorAction SilentlyContinue) { if (Has-KnownMarker ($entry | Format-List * | Out-String -Width 4096)) { [void]$knownWmiPaths.Add([string]$entry.CimSystemProperties.Path) $knownWmiObjects.Add($entry) } } } foreach ($binding in Get-CimInstance -Namespace root/subscription -ClassName __FilterToConsumerBinding -ErrorAction SilentlyContinue) { try { $bindingText = $binding | Format-List * | Out-String -Width 4096 $referencesKnownObject = $false foreach ($path in $knownWmiPaths) { if ($bindingText.IndexOf($path, [System.StringComparison]::OrdinalIgnoreCase) -ge 0) { $referencesKnownObject = $true break } } if (-not ((Has-KnownMarker $bindingText) -or $referencesKnownObject)) { continue } $evidence = Save-CleanupEvidence WMI "binding-$($binding.CimSystemProperties.Path)" $bindingText if (-not $evidence) { Add-AuditError "cleanup-skipped-no-evidence:WMI/$($binding.CimSystemProperties.Path)" 'evidence write failed; binding left in place'; continue } Remove-CimInstance -InputObject $binding -ErrorAction Stop Add-CleanupAction WMI "__FilterToConsumerBinding $($binding.CimSystemProperties.Path)" Removed } catch { Add-AuditError "cleanup-wmi-binding:$($binding.CimSystemProperties.Path)" $_ } } foreach ($entry in $knownWmiObjects) { try { $evidence = Save-CleanupEvidence WMI "$($entry.CimClass.CimClassName)-$($entry.CimSystemProperties.Path)" $entry if (-not $evidence) { Add-AuditError "cleanup-skipped-no-evidence:WMI/$($entry.CimSystemProperties.Path)" 'evidence write failed; object left in place'; continue } Remove-CimInstance -InputObject $entry -ErrorAction Stop Add-CleanupAction WMI "$($entry.CimClass.CimClassName) $($entry.CimSystemProperties.Path)" Removed } catch { Add-AuditError "cleanup-wmi:$($entry.CimSystemProperties.Path)" $_ } } } catch { Add-AuditError 'cleanup-wmi' $_ } $cleanupAppFilters = Get-FirewallFilterMap 'Application' foreach ($rule in Get-NetFirewallRule -ErrorAction SilentlyContinue) { try { $app = $cleanupAppFilters[[string]$rule.InstanceID] if (-not (Has-KnownMarker "$($rule.Name) $($rule.DisplayName) $($rule.Description) $($app.Program)")) { continue } $evidence = Save-CleanupEvidence FirewallRule $rule.Name (($rule | Format-List * | Out-String -Width 4096) + ($app | Format-List * | Out-String -Width 4096)) if (-not $evidence) { Add-AuditError "cleanup-skipped-no-evidence:FirewallRule/$($rule.Name)" 'evidence write failed; rule left in place'; continue } Remove-NetFirewallRule -Name $rule.Name -ErrorAction Stop Add-CleanupAction FirewallRule $rule.DisplayName Removed } catch { Add-AuditError "cleanup-firewall:$($rule.Name)" $_ } } try { $preference = Get-MpPreference -ErrorAction Stop foreach ($path in @($preference.ExclusionPath)) { try { if (-not (Has-KnownMarker ([string]$path))) { continue } $evidence = Save-CleanupEvidence DefenderExclusion "path-$path" "ExclusionPath = $path" if (-not $evidence) { Add-AuditError "cleanup-skipped-no-evidence:DefenderExclusion/$path" 'evidence write failed; exclusion left in place'; continue } Remove-MpPreference -ExclusionPath $path -ErrorAction Stop Add-CleanupAction DefenderExclusion "Path: $path" Removed } catch { Add-AuditError "cleanup-defender-exclusion-path:$path" $_ } } foreach ($process in @($preference.ExclusionProcess)) { try { if (-not (Has-KnownMarker ([string]$process))) { continue } $evidence = Save-CleanupEvidence DefenderExclusion "process-$process" "ExclusionProcess = $process" if (-not $evidence) { Add-AuditError "cleanup-skipped-no-evidence:DefenderExclusion/$process" 'evidence write failed; exclusion left in place'; continue } Remove-MpPreference -ExclusionProcess $process -ErrorAction Stop Add-CleanupAction DefenderExclusion "Process: $process" Removed } catch { Add-AuditError "cleanup-defender-exclusion-process:$process" $_ } } } catch { Add-AuditError 'cleanup-defender-exclusions' $_ } $startupQuarantine = Join-Path $OutputDir 'quarantine-startup' foreach ($folder in $StartupFolders) { foreach ($file in Get-ChildItem -LiteralPath $folder -Force -File -ErrorAction SilentlyContinue) { try { $target = $file.FullName $arguments = '' if ($file.Extension -eq '.lnk') { $shortcut = (New-Object -ComObject WScript.Shell).CreateShortcut($file.FullName) $target = [string]$shortcut.TargetPath $arguments = [string]$shortcut.Arguments } $facts = Get-FileFacts $file.FullName if (-not ((Has-KnownMarker "$($file.FullName) $target $arguments") -or ($KnownHashes -contains $facts.SHA256))) { continue } $evidence = Save-CleanupEvidence StartupFile $file.Name ( "Path = $($file.FullName)`nTarget = $target`nArguments = $arguments`n" + "SHA256 = $($facts.SHA256)`nSignature = $($facts.Signature)`nLastWriteTime = $($file.LastWriteTime)" ) if (-not $evidence) { Add-AuditError "cleanup-skipped-no-evidence:StartupFile/$($file.FullName)" 'evidence write failed; file left in place'; continue } New-Item -ItemType Directory -Path $startupQuarantine -Force | Out-Null $destination = Join-Path $startupQuarantine ("{0}-{1}" -f ([guid]::NewGuid().ToString('N')), $file.Name) Move-Item -LiteralPath $file.FullName -Destination $destination -Force -ErrorAction Stop Add-CleanupAction StartupFile $file.FullName "Quarantined to $destination" } catch { Add-AuditError "cleanup-startup-file:$($file.FullName)" $_ } } } } $AutorunKeys = @( 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Run', 'HKLM:\Software\Microsoft\Windows\CurrentVersion\RunOnce', 'HKLM:\Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Run', 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Run', 'HKCU:\Software\Microsoft\Windows\CurrentVersion\RunOnce', 'HKLM:\Software\Microsoft\Windows NT\CurrentVersion\Winlogon', 'HKLM:\Software\Microsoft\Windows NT\CurrentVersion\Windows' ) $StartupFolders = @( "$env:ProgramData\Microsoft\Windows\Start Menu\Programs\Startup", "$env:APPDATA\Microsoft\Windows\Start Menu\Programs\Startup" ) | Where-Object { $_ -and (Test-Path -LiteralPath $_) } if ($CleanupKnownIndicators) { Remove-KnownPersistence Save-Csv 'cleanup-actions.csv' $CleanupActions } try { Get-ComputerInfo | Out-File -Encoding UTF8 (Join-Path $OutputDir 'computer-info.txt') systeminfo.exe | Out-File -Encoding UTF8 (Join-Path $OutputDir 'systeminfo.txt') } catch { Add-AuditError 'system-info' $_ } $ProcessRows = @() $ProcessFacts = @{} # This audit and the cscript wrapper that launched it are running while the # audit enumerates processes. Neither is a finding about the host. $SelfProcessIds = [System.Collections.Generic.List[int]]::new() $SelfProcessIds.Add([int]$PID) try { $selfProcess = Get-CimInstance Win32_Process -Filter "ProcessId=$PID" -ErrorAction Stop if ($selfProcess) { $SelfProcessIds.Add([int]$selfProcess.ParentProcessId) } } catch { Add-AuditError 'self-process-identity' $_ } try { foreach ($process in Get-CimInstance Win32_Process) { $path = [string]$process.ExecutablePath $facts = if ($path) { Get-FileFacts $path } else { $null } if ($facts -and $path) { $ProcessFacts[[int]$process.ProcessId] = $facts } $row = [pscustomobject]@{ PID = [int]$process.ProcessId ParentPID = [int]$process.ParentProcessId Name = [string]$process.Name Path = $path CommandLine = [string]$process.CommandLine CreationDate = $process.CreationDate SHA256 = if ($facts) { $facts.SHA256 } else { '' } Signature = if ($facts) { $facts.Signature } else { '' } Signer = if ($facts) { $facts.Signer } else { '' } } $ProcessRows += $row $combined = "$($row.Path) $($row.CommandLine)" # Whether a process IS remote-access software is a question about the # executable, not about strings in its arguments. This audit writes # under a folder whose name contains the product it hunts, so matching # the command line made the tool report itself as a remote-access tool # on every clean host. Marker and hash detection stay on the full # command line, where a campaign argument genuinely belongs. $identity = "$($row.Name) $($row.Path)" $isSelf = $SelfProcessIds -contains $row.PID # Both tests need their own parentheses. Written as one bare call the # parser hands '-or', the hash list and the digest to Has-KnownMarker as # extra arguments, and every payload whose path carries no marker string # silently stops being reported. if ((Has-KnownMarker $combined) -or ($KnownHashes -contains $row.SHA256)) { Add-Finding Critical Process 'Known RAT process or payload is running' $combined 'Contain the host and terminate the process.' } elseif ($identity -match $RemoteToolPattern) { Add-Finding High Process 'Remote-access software process requires validation' $combined 'Confirm it is approved and review its account and session logs.' } elseif (-not $isSelf -and $combined -match $SuspiciousCommandPattern -and $combined -match $UserWritablePattern) { Add-Finding High Process 'Suspicious command from a user-writable path' $combined 'Quarantine after validation and investigate its parent process.' } } } catch { Add-AuditError 'processes' $_ } Save-Csv 'processes.csv' $ProcessRows $ConnectionRows = @() try { # $PID is a constant automatic variable. Assigning the owning process id to # it threw on the first connection, the catch below swallowed it, and the # audit shipped an empty tcp-connections.csv with no relay finding at all -- # losing the single strongest piece of evidence this script collects. $ProcessById = @{} foreach ($processRow in $ProcessRows) { $ProcessById[$processRow.PID] = $processRow } foreach ($connection in Get-NetTCPConnection) { $owningProcessId = [int]$connection.OwningProcess $process = $ProcessById[$owningProcessId] $facts = $ProcessFacts[$owningProcessId] $row = [pscustomobject]@{ State = [string]$connection.State LocalAddress = [string]$connection.LocalAddress LocalPort = [int]$connection.LocalPort RemoteAddress = [string]$connection.RemoteAddress RemotePort = [int]$connection.RemotePort PID = $owningProcessId Process = [string]$process.Name Path = [string]$process.Path SHA256 = [string]$facts.SHA256 Signature = [string]$facts.Signature } $ConnectionRows += $row if ($row.RemoteAddress -eq $RelayIp -or (Has-KnownMarker "$($row.Path) $($process.CommandLine)")) { Add-Finding Critical Network 'Connection associated with the known RAT' ($row | ConvertTo-Json -Compress) 'Isolate the host and terminate the owning process.' } elseif ($row.State -eq 'Established' -and $row.RemoteAddress -notmatch '^(127\.|::1$|0\.0\.0\.0$|::$)') { if ($row.Path -match $UserWritablePattern -and $row.Signature -ne 'Valid') { Add-Finding High Network 'Unsigned user-writable process has an established connection' ($row | ConvertTo-Json -Compress) 'Contain and investigate the process and destination.' } } elseif ($row.State -eq 'Listen' -and $row.LocalAddress -notmatch '^(127\.|::1$)') { if ($row.Path -match $UserWritablePattern -or $row.Process -match $RemoteToolPattern) { Add-Finding High Network 'Suspicious externally reachable listener' ($row | ConvertTo-Json -Compress) 'Validate the listener and block it if unauthorized.' } } } } catch { Add-AuditError 'tcp-connections' $_ } Save-Csv 'tcp-connections.csv' $ConnectionRows try { Get-NetUDPEndpoint | Select-Object LocalAddress, LocalPort, OwningProcess, CreationTime | Export-Csv -NoTypeInformation -Encoding UTF8 (Join-Path $OutputDir 'udp-endpoints.csv') ipconfig.exe /displaydns | Out-File -Encoding UTF8 (Join-Path $OutputDir 'dns-cache.txt') netstat.exe -abno | Out-File -Encoding UTF8 (Join-Path $OutputDir 'netstat-abno.txt') } catch { Add-AuditError 'network-endpoints' $_ } try { quser.exe | Out-File -Encoding UTF8 (Join-Path $OutputDir 'interactive-sessions.txt') Get-SmbSession | Select-Object ClientComputerName, ClientUserName, NumOpens, SecondsExists, SecondsIdle | Export-Csv -NoTypeInformation -Encoding UTF8 (Join-Path $OutputDir 'smb-sessions.csv') Get-BitsTransfer -AllUsers | Select-Object DisplayName, JobState, OwnerAccount, CreationTime, ModificationTime, TransferType, RemoteName, LocalName | Export-Csv -NoTypeInformation -Encoding UTF8 (Join-Path $OutputDir 'bits-transfers.csv') $firewallLog = "$env:SystemRoot\System32\LogFiles\Firewall\pfirewall.log" if (Test-Path -LiteralPath $firewallLog) { Copy-Item -LiteralPath $firewallLog -Destination (Join-Path $OutputDir 'windows-firewall.log') -Force } } catch { Add-AuditError 'connection-history-sources' $_ } $ServiceRows = @() try { foreach ($service in Get-CimInstance Win32_Service) { $path = Get-PathFromCommand ([string]$service.PathName) $facts = Get-FileFacts $path $row = [pscustomobject]@{ Name = [string]$service.Name DisplayName = [string]$service.DisplayName State = [string]$service.State StartMode = [string]$service.StartMode StartName = [string]$service.StartName PathName = [string]$service.PathName FilePath = $path SHA256 = $facts.SHA256 Signature = $facts.Signature Signer = $facts.Signer LastWriteTime = $facts.LastWriteTime } $ServiceRows += $row $combined = "$($row.Name) $($row.DisplayName) $($row.PathName)" if ($row.Name -eq $ServiceName -or (Has-KnownMarker $combined) -or $KnownHashes -contains $row.SHA256) { Add-Finding Critical Service 'Known RAT service persistence' $combined 'Remove using the evidence-bound remediation.' } elseif ($combined -match $RemoteToolPattern) { Add-Finding High Service 'Remote-access service requires validation' $combined 'Confirm this service is approved and review its management account.' } elseif ($row.StartMode -eq 'Auto' -and $row.FilePath -match $UserWritablePattern -and $row.Signature -ne 'Valid') { Add-Finding High Service 'Unsigned automatic service from a user-writable path' $combined 'Disable and quarantine after validation.' } elseif ($row.StartMode -eq 'Auto' -and $facts.LastWriteTime -ge $IncidentStart -and $row.Signature -ne 'Valid') { Add-Finding Medium Service 'Recently modified unsigned automatic service' $combined 'Review installation provenance and service events.' } } } catch { Add-AuditError 'services' $_ } Save-Csv 'services.csv' $ServiceRows $TaskRows = @() $BaselineTaskCount = 0 try { foreach ($task in Get-ScheduledTask) { foreach ($action in @($task.Actions)) { $combined = "$($action.Execute) $($action.Arguments) $($action.WorkingDirectory)" $imageFacts = Get-FileFacts (Get-PathFromCommand ([string]$action.Execute)) $row = [pscustomobject]@{ TaskPath = [string]$task.TaskPath TaskName = [string]$task.TaskName State = [string]$task.State Author = [string]$task.Author Execute = [string]$action.Execute Arguments = [string]$action.Arguments WorkingDirectory = [string]$action.WorkingDirectory Signature = $imageFacts.Signature Signer = $imageFacts.Signer } $TaskRows += $row # Windows ships dozens of built-in tasks that legitimately run # rundll32 and friends, and reporting each as High buried the real # signal: a pristine Windows 11 produced twelve High findings with # nothing to act on. So a Microsoft-signed image under the # OS-managed task path is treated as the baseline. # # TaskPath and Author are attacker-chosen, and a signed LOLBin will # run whatever it is told to. The baseline is therefore decided # LAST: suspicious arguments and user-writable targets are matched # first and always win, so signature plus location can never become # an allowlist for the arguments. A task planted under \Microsoft\ # that runs signed powershell.exe -EncodedCommand is still High. $isSignedMicrosoftBuiltin = ($row.TaskPath -like '\Microsoft\*') -and ($imageFacts.Signature -eq 'Valid') -and ($imageFacts.Signer -match 'Microsoft') if (Has-KnownMarker $combined) { Add-Finding Critical ScheduledTask 'Scheduled task contains a known RAT marker' ($row | ConvertTo-Json -Compress) 'Disable, export, and remove the task.' } elseif ($combined -match $SuspiciousArgumentPattern) { Add-Finding High ScheduledTask 'Scheduled task has suspicious execution arguments' ($row | ConvertTo-Json -Compress) 'Validate and remove if unauthorized.' } elseif ($combined -match $UserWritablePattern) { Add-Finding Medium ScheduledTask 'Scheduled task executes from a user-writable path' ($row | ConvertTo-Json -Compress) 'Validate ownership and signature.' } elseif ($isSignedMicrosoftBuiltin) { $BaselineTaskCount++ } elseif ($combined -match $LolBinPattern) { Add-Finding High ScheduledTask 'Scheduled task runs a script interpreter outside the signed baseline' ($row | ConvertTo-Json -Compress) 'Validate and remove if unauthorized.' } } } if ($BaselineTaskCount -gt 0) { Add-Finding Info ScheduledTask 'Signed Microsoft built-in tasks were not individually reported' "$BaselineTaskCount task actions under \Microsoft\ run a validly Microsoft-signed image." 'Review scheduled-tasks.csv if a baseline task is suspected of being altered.' } } catch { Add-AuditError 'scheduled-tasks' $_ } Save-Csv 'scheduled-tasks.csv' $TaskRows $AutorunRows = @() foreach ($key in $AutorunKeys) { try { if (-not (Test-Path $key)) { continue } $item = Get-ItemProperty $key foreach ($property in $item.PSObject.Properties) { if ($property.Name -match '^PS') { continue } $value = [string]$property.Value $row = [pscustomobject]@{ Key = $key; Name = $property.Name; Value = $value } $AutorunRows += $row if (Has-KnownMarker $value) { Add-Finding Critical Autorun 'Autorun contains a known RAT marker' ($row | ConvertTo-Json -Compress) 'Export and remove the value.' } elseif ($value -match $SuspiciousCommandPattern -or $value -match $UserWritablePattern) { Add-Finding High Autorun 'Suspicious autorun value' ($row | ConvertTo-Json -Compress) 'Validate and remove if unauthorized.' } } } catch { Add-AuditError "autorun:$key" $_ } } Save-Csv 'autoruns.csv' $AutorunRows try { $IfeoRows = Get-ChildItem 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options' -ErrorAction SilentlyContinue | ForEach-Object { $debugger = (Get-ItemProperty $_.PSPath -Name Debugger -ErrorAction SilentlyContinue).Debugger if ($debugger) { [pscustomobject]@{ Image = $_.PSChildName; Debugger = [string]$debugger } } } Save-Csv 'ifeo-debuggers.csv' $IfeoRows foreach ($row in @($IfeoRows)) { Add-Finding High IFEO 'Image File Execution Options debugger persistence' ($row | ConvertTo-Json -Compress) 'Validate and remove if unauthorized.' } } catch { Add-AuditError 'ifeo' $_ } try { $WmiRows = @() foreach ($class in '__EventFilter', 'CommandLineEventConsumer', 'ActiveScriptEventConsumer', '__FilterToConsumerBinding') { foreach ($entry in Get-CimInstance -Namespace root/subscription -ClassName $class -ErrorAction SilentlyContinue) { $text = ($entry | Format-List * | Out-String -Width 4096).Trim() $WmiRows += [pscustomobject]@{ Class = $class; Data = $text } if (Has-KnownMarker $text) { Add-Finding Critical WMI "Permanent WMI subscription carries a known RAT marker: $class" $text 'Remove using the evidence-bound remediation.' } else { Add-Finding Medium WMI "Permanent WMI subscription present: $class" $text 'Validate against the build baseline and remove if unauthorized.' } } } Save-Csv 'wmi-persistence.csv' $WmiRows } catch { Add-AuditError 'wmi-persistence' $_ } try { $StartupRows = foreach ($folder in $StartupFolders) { foreach ($file in Get-ChildItem -LiteralPath $folder -Force -File -ErrorAction SilentlyContinue) { $target = $file.FullName $arguments = '' if ($file.Extension -eq '.lnk') { $shortcut = (New-Object -ComObject WScript.Shell).CreateShortcut($file.FullName) $target = [string]$shortcut.TargetPath $arguments = [string]$shortcut.Arguments } $facts = Get-FileFacts $file.FullName [pscustomobject]@{ StartupFolder = $folder; FullName = $file.FullName; Target = $target; Arguments = $arguments Length = $file.Length; CreationTime = $file.CreationTime; LastWriteTime = $file.LastWriteTime SHA256 = $facts.SHA256; Signature = $facts.Signature; Signer = $facts.Signer } } } Save-Csv 'startup-folders.csv' $StartupRows foreach ($row in @($StartupRows)) { if ((Has-KnownMarker "$($row.FullName) $($row.Target) $($row.Arguments)") -or $KnownHashes -contains $row.SHA256) { Add-Finding Critical Startup 'Startup-folder persistence contains a known incident marker' ($row | ConvertTo-Json -Compress) 'Quarantine using the evidence-bound remediation.' } elseif ($row.LastWriteTime -ge $IncidentStart) { Add-Finding Medium Startup 'Recently modified startup-folder file' ($row | ConvertTo-Json -Compress) 'Validate and quarantine if unauthorized.' } } } catch { Add-AuditError 'startup-folders' $_ } try { $Users = Get-LocalUser | Select-Object Name, Enabled, LastLogon, PasswordLastSet, PrincipalSource, SID $Admins = Get-LocalGroupMember -SID 'S-1-5-32-544' | Select-Object Name, ObjectClass, PrincipalSource, SID Save-Csv 'local-users.csv' $Users Save-Csv 'local-administrators.csv' $Admins } catch { Add-AuditError 'local-accounts' $_ net.exe user | Out-File -Encoding UTF8 (Join-Path $OutputDir 'local-users-fallback.txt') net.exe localgroup Administrators | Out-File -Encoding UTF8 (Join-Path $OutputDir 'local-admins-fallback.txt') } try { $Defender = Get-MpPreference $Defender | Select-Object DisableRealtimeMonitoring, DisableBehaviorMonitoring, DisableScriptScanning, DisableIOAVProtection, DisableIntrusionPreventionSystem, ExclusionPath, ExclusionProcess, ExclusionExtension, AttackSurfaceReductionRules_Ids, AttackSurfaceReductionRules_Actions | ConvertTo-Json -Depth 5 | Out-File -Encoding UTF8 (Join-Path $OutputDir 'defender-preferences.json') if ($Defender.DisableRealtimeMonitoring -or $Defender.DisableBehaviorMonitoring -or $Defender.DisableScriptScanning) { Add-Finding High Defender 'Microsoft Defender protection is disabled' ($Defender | Select-Object DisableRealtimeMonitoring, DisableBehaviorMonitoring, DisableScriptScanning | ConvertTo-Json -Compress) 'Restore the settings after checking organizational policy.' } if (@($Defender.ExclusionPath).Count -or @($Defender.ExclusionProcess).Count -or @($Defender.ExclusionExtension).Count) { Add-Finding Medium Defender 'Microsoft Defender exclusions require review' (($Defender | Select-Object ExclusionPath, ExclusionProcess, ExclusionExtension) | ConvertTo-Json -Depth 4 -Compress) 'Remove exclusions that are not organization-approved.' } } catch { Add-AuditError 'defender' $_ } try { $appFilters = Get-FirewallFilterMap 'Application' $portFilters = Get-FirewallFilterMap 'Port' $FirewallRows = foreach ($rule in (Get-NetFirewallRule | Where-Object Enabled -eq True)) { $app = $appFilters[[string]$rule.InstanceID] $port = $portFilters[[string]$rule.InstanceID] [pscustomobject]@{ DisplayName = $rule.DisplayName; Direction = $rule.Direction; Action = $rule.Action Profile = $rule.Profile; Program = $app.Program; Protocol = $port.Protocol LocalPort = $port.LocalPort; RemotePort = $port.RemotePort } } Save-Csv 'enabled-firewall-rules.csv' $FirewallRows netsh.exe advfirewall export (Join-Path $OutputDir 'firewall-policy.wfw') | Out-Null } catch { Add-AuditError 'firewall' $_ } try { Get-NetIPConfiguration -Detailed | Format-List * | Out-File -Encoding UTF8 (Join-Path $OutputDir 'ip-configuration.txt') Get-DnsClientServerAddress | Export-Csv -NoTypeInformation -Encoding UTF8 (Join-Path $OutputDir 'dns-servers.csv') Get-NetRoute | Export-Csv -NoTypeInformation -Encoding UTF8 (Join-Path $OutputDir 'routes.csv') netsh.exe winhttp show proxy | Out-File -Encoding UTF8 (Join-Path $OutputDir 'winhttp-proxy.txt') Get-ItemProperty 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Internet Settings' | Select-Object ProxyEnable, ProxyServer, AutoConfigURL, AutoDetect | ConvertTo-Json | Out-File -Encoding UTF8 (Join-Path $OutputDir 'user-proxy.json') Get-Content "$env:SystemRoot\System32\drivers\etc\hosts" -ErrorAction SilentlyContinue | Out-File -Encoding UTF8 (Join-Path $OutputDir 'hosts.txt') } catch { Add-AuditError 'network-configuration' $_ } try { $Rdp = Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server' -ErrorAction SilentlyContinue [pscustomobject]@{ RdpEnabled = ($Rdp.fDenyTSConnections -eq 0) TermServiceState = (Get-Service TermService -ErrorAction SilentlyContinue).Status OpenSshState = (Get-Service sshd -ErrorAction SilentlyContinue).Status } | ConvertTo-Json | Out-File -Encoding UTF8 (Join-Path $OutputDir 'remote-access-state.json') } catch { Add-AuditError 'remote-access-state' $_ } try { $UninstallPaths = @( 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*' 'HKLM:\Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*' 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*' ) $InstalledRemoteTools = Get-ItemProperty -Path $UninstallPaths -ErrorAction SilentlyContinue | Where-Object { "$($_.DisplayName) $($_.Publisher)" -match $RemoteToolPattern } | Select-Object DisplayName, DisplayVersion, Publisher, InstallDate, InstallLocation, UninstallString, PSPath Save-Csv 'remote-access-software.csv' $InstalledRemoteTools foreach ($row in @($InstalledRemoteTools)) { Add-Finding High Software 'Installed remote-access software requires validation' ($row | ConvertTo-Json -Compress) 'Confirm it is approved and review its access logs.' } } catch { Add-AuditError 'installed-remote-tools' $_ } $RecentRows = @() $ScanRoots = @( $env:ProgramData, $env:TEMP, "$env:SystemRoot\Temp", "$env:USERPROFILE\Downloads", "$env:USERPROFILE\Desktop", "$env:USERPROFILE\Documents", $env:LOCALAPPDATA, $env:APPDATA ) | Where-Object { $_ -and (Test-Path -LiteralPath $_) } | Select-Object -Unique try { $RecentSeen = 0 :RootScan foreach ($root in $ScanRoots) { foreach ($file in Get-ChildItem -LiteralPath $root -Force -File -Recurse -ErrorAction SilentlyContinue | Where-Object { $_.LastWriteTime -ge $IncidentStart -and $_.Extension -match '^\.(exe|dll|sys|ps1|vbs|vbe|js|jse|bat|cmd|com|scr|msi|zip|lnk)$' }) { if ($RecentSeen -ge 8000) { Add-Finding Medium Audit 'Recent-file scan safety cap reached' 'The audit inspected 8,000 recent executable or script files.' 'Review the listed files and run an offline EDR scan for complete disk coverage.' break RootScan } $RecentSeen++ $facts = Get-FileFacts $file.FullName $row = [pscustomobject]@{ Root = $root; Path = $file.FullName; Length = $file.Length CreationTime = $file.CreationTime; LastWriteTime = $file.LastWriteTime SHA256 = $facts.SHA256; Signature = $facts.Signature; Signer = $facts.Signer } $RecentRows += $row $held = $row.Path.StartsWith($RemediationRoot, [System.StringComparison]::OrdinalIgnoreCase) if (($KnownHashes -contains $row.SHA256 -or (Has-KnownMarker $row.Path)) -and $held) { Add-Finding Info File 'Campaign artifact held in this tool''s quarantine' ($row | ConvertTo-Json -Compress) 'Retain as evidence, then delete once the incident is closed.' } elseif ($KnownHashes -contains $row.SHA256 -or (Has-KnownMarker $row.Path)) { Add-Finding Critical File 'Known malicious or campaign-linked file' ($row | ConvertTo-Json -Compress) 'Quarantine using the evidence-bound remediation.' } elseif ($held) { continue } elseif ($file.Extension -match '^\.(exe|dll|sys|com|scr)$' -and $row.Signature -ne 'Valid') { Add-Finding Medium File 'Recent unsigned executable content' ($row | ConvertTo-Json -Compress) 'Validate provenance and quarantine if unauthorized.' } } } } catch { Add-AuditError 'recent-files' $_ } Save-Csv 'recent-executable-files.csv' $RecentRows function Export-Events { param([string]$Name, [string]$LogName, [int[]]$Ids, [int]$Maximum = 5000) try { Get-WinEvent -FilterHashtable @{ LogName = $LogName; Id = $Ids; StartTime = $IncidentStart } -ErrorAction Stop | Select-Object -First $Maximum | Select-Object TimeCreated, Id, LevelDisplayName, ProviderName, MachineName, Message | Export-Csv -NoTypeInformation -Encoding UTF8 (Join-Path $OutputDir $Name) } catch { if ([string]$_ -match 'No events were found') { Save-Csv $Name @() } else { Add-AuditError "events:$LogName" $_ } } } Export-Events 'events-system-persistence.csv' 'System' @(7045, 7036, 7040) Export-Events 'events-security-account-persistence.csv' 'Security' @(1102, 4697, 4720, 4722, 4724, 4732, 4738) Export-Events 'events-task-scheduler.csv' 'Microsoft-Windows-TaskScheduler/Operational' @(106, 140, 141, 200, 201) Export-Events 'events-powershell.csv' 'Microsoft-Windows-PowerShell/Operational' @(4103, 4104) Export-Events 'events-defender.csv' 'Microsoft-Windows-Windows Defender/Operational' @(1116, 1117, 5001, 5004, 5007) Export-Events 'events-wmi.csv' 'Microsoft-Windows-WMI-Activity/Operational' @(5857, 5858, 5859, 5860, 5861) Export-Events 'events-security-network-logons.csv' 'Security' @(4624, 4625, 4648, 5156, 5157) 10000 Export-Events 'events-rdp-connections.csv' 'Microsoft-Windows-TerminalServices-RemoteConnectionManager/Operational' @(1149) Export-Events 'events-rdp-sessions.csv' 'Microsoft-Windows-TerminalServices-LocalSessionManager/Operational' @(21, 22, 24, 25, 39, 40) Export-Events 'events-winrm.csv' 'Microsoft-Windows-WinRM/Operational' @(6, 91, 142, 169) Export-Events 'events-dns-client.csv' 'Microsoft-Windows-DNS-Client/Operational' @(3006, 3008, 3010, 3018) 10000 Export-Events 'events-openssh.csv' 'OpenSSH/Operational' @(4, 5) $severityOrder = @{ Critical = 0; High = 1; Medium = 2; Low = 3; Info = 4 } $SortedFindings = $Findings | Sort-Object @{ Expression = { $severityOrder[$_.Severity] } }, Category, Title Save-Csv 'findings.csv' $SortedFindings Save-Csv 'audit-errors.csv' $Errors ConvertTo-Json -InputObject @($SortedFindings) -Depth 5 | Out-File -Encoding UTF8 (Join-Path $OutputDir 'findings.json') $Counts = $SortedFindings | Group-Object Severity | ForEach-Object { "$($_.Name)=$($_.Count)" } $Summary = @( "Phase: $Phase" "Incident search start: $IncidentStart" "Generated: $(Get-Date -Format o)" "Findings: $($SortedFindings.Count) ($($Counts -join ', '))" "Audit errors: $($Errors.Count)" "" "Critical and high findings:" ) $Summary += $SortedFindings | Where-Object Severity -in @('Critical', 'High') | ForEach-Object { "[$($_.Severity)] $($_.Category): $($_.Title) | $($_.Evidence)" } $Summary | Out-File -Encoding UTF8 (Join-Path $OutputDir 'SUMMARY.txt') if ($Errors.Count -gt 0) { exit 2 } exit 0