PowerShell - Building Defensive Scripts to Respond to Threats

In modern Windows environments, PowerShell is both a powerful administrative tool and a frequent vector for attacker persistence. Rather than avoiding it, security teams can harness PowerShell to build defensive, automated response scripts that detect and neutralize threats in real time.

This article explores practical techniques for creating battle-tested defensive PowerShell scripts, complete with code samples you can deploy today.


Why Defensive PowerShell?

  • Native to Windows - No third-party dependencies
  • Deep system access - Query processes, services, registry, event logs
  • Automation-ready - Integrate with SIEM, EDR, or run via scheduled tasks
  • Rapid prototyping - Respond faster than waiting for vendor patches

Note: Always run defensive scripts with least privilege. Use constrained endpoints or Just Enough Administration (JEA) where possible.


Core Principles of Defensive Scripting

  1. Detect suspicious behavior
  2. Validate findings to reduce false positives
  3. Respond safely and proportionally
  4. Log everything for forensics
  5. Fail gracefully - never crash the system

1. Detecting Suspicious Processes

Attackers often use encoded commands, living-off-the-land binaries (LOLBins), or obfuscated PowerShell.

Sample: Detect Base64-Encoded PowerShell Commands

# Detect-EncodedPowerShell.ps1
$encodedPattern = '[A-Za-z0-9+/]{20,}={0,2}'  # Rough Base64 detection
 
Get-WinEvent -FilterHashtable @{
    LogName = 'Microsoft-Windows-PowerShell/Operational'
    ID = 4104
} -ErrorAction SilentlyContinue | ForEach-Object {
    $scriptBlock = $_.Properties[2].Value
    if ($scriptBlock -match $encodedPattern) {
        $decoded = try { [Text.Encoding]::Unicode.GetString([Convert]::FromBase64String(($scriptBlock -replace '\s',''))) } catch { $null }
        if ($decoded) {
            [PSCustomObject]@{
                Time        = $_.TimeCreated
                User        = $_.Properties[0].Value
                EncodedCmd  = $scriptBlock.Substring(0, [Math]::Min(100, $scriptBlock.Length))
                Decoded     = $decoded.Substring(0, [Math]::Min(200, $decoded.Length))
                Suspicious  = $true
            }
        }
    }
} | Where-Object Suspicious | Export-Csv "EncodedPowerShell_Alert_$(Get-Date -Format 'yyyyMMdd_HHmm').csv" -NoTypeInformation

Deploy via: Scheduled task (every 15 mins) or SIEM forwarder


2. Automated Threat Isolation

Once a threat is confirmed, isolate the host or user.

Sample: Quarantine a Compromised User Session

# Quarantine-User.ps1
param([string]$Username)
 
$session = Get-CimInstance Win32_LogonSession | Where-Object {
    (Get-CimAssociatedInstance $_ -ResultClassName Win32_Account).Name -eq $Username
}
 
if ($session) {
    # Disable local account
    Disable-LocalUser -Name $Username -ErrorAction SilentlyContinue
    
    # Kill user processes
    Get-Process | Where-Object { $_.SessionId -in $session.LogonId } | Stop-Process -Force
    
    # Log off user
    logoff $session.LogonId /server:localhost
    
    Write-Host "[+] User $Username quarantined." -ForegroundColor Green
    Add-Content -Path "C:\IR\quarantine.log" -Value "[$(Get-Date)] Quarantined: $Username"
}

Use case: Trigger from EDR alert or SIEM rule


3. Registry Persistence Hunt & Neutralize

Attackers love Run keys and WMI event subscriptions.

Sample: Remove Malicious Run Keys

# Clean-RunKeys.ps1
$runKeys = @(
    'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run'
    'HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Run'
    'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run'
)
 
$suspicious = @()
 
foreach ($key in $runKeys) {
    Get-Item $key -ErrorAction SilentlyContinue | ForEach-Object {
        Get-ItemProperty $_.PSPath | Select-Object * -ExcludeProperty PS* | ForEach-Object {
            $_.PSObject.Properties | Where-Object { $_.Value -is [string] } | ForEach-Object {
                if ($_.Value -match 'powershell|cmd|wscript|cscript|mshta' -and $_.Value -notmatch 'Windows Defender|Microsoft') {
                    $suspicious += [PSCustomObject]@{
                        Hive  = $key
                        Name  = $_.Name
                        Value = $_.Value
                    }
                    Remove-ItemProperty -Path $key -Name $_.Name -Force -ErrorAction SilentlyContinue
                }
            }
        }
    }
}
 
if ($suspicious) {
    $suspicious | Export-Csv "SuspiciousRunKeys_$(Get-Date -Format 'yyyyMMdd_HHmm').csv" -NoTypeInformation
    Write-Warning "Removed $($suspicious.Count) suspicious Run entries."
}

4. Network Anomaly Response

Block C2 domains or IPs on the Windows Firewall.

Sample: Block Known Bad IPs

# Block-C2IP.ps1
$badIPs = @(
    "185.117.118.99"   # Example Cobalt Strike C2
    "192.168.50.100"
)
 
$ruleName = "IR_Block_C2"
 
# Remove old rule
Remove-NetFirewallRule -DisplayName $ruleName -ErrorAction SilentlyContinue
 
# Create new blocking rule
New-NetFirewallRule -DisplayName $ruleName `
    -Direction Outbound -Action Block `
    -RemoteAddress $badIPs
 
Write-Host "[+] Blocked $($badIPs.Count) C2 IPs via Windows Firewall." -ForegroundColor Cyan

Enhance with: Pull IPs from Threat Intel API (e.g., VirusTotal, AbuseIPDB)


5. Full Incident Response Playbook Script

Combine detection + response in one script.

# IR-Playbook.ps1
param([switch]$AutoRemediate)
 
# Step 1: Collect artifacts
$artifacts = @()
$artifacts += Get-Process | Select-Object Name, Id, Path, CommandLine
$artifacts += Get-NetTCPConnection | Where-Object State -eq 'Established' | Select-Object LocalAddress, RemoteAddress, RemotePort
 
# Step 2: Hunt for threats
$threats = Get-WinEvent -FilterHashtable @{LogName='Security'; ID=4688} -MaxEvents 100 | Where-Object {
    $_.Message -match 'powershell.*-enc|cmd.*\/c.*powershell'
}
 
if ($threats -and $AutoRemediate) {
    # Kill malicious processes
    $threats | ForEach-Object {
        $procId = ($_.Message -split 'Process ID: ')[1].Split('`')[0].Trim()
        Stop-Process -Id $procId -Force -ErrorAction SilentlyContinue
    }
}
 
# Step 3: Report
$reportPath = "C:\IR\Report_$(Get-Date -Format 'yyyyMMdd_HHmmss').json"
@{
    Timestamp = Get-Date
    ThreatsFound = $threats.Count
    Processes = $artifacts
} | ConvertTo-Json -Depth 5 | Out-File $reportPath
 
Write-Host "IR Playbook complete. Report: $reportPath" -ForegroundColor Yellow

Run with: .\IR-Playbook.ps1 -AutoRemediate


Best Practices for Defensive Scripts

PracticeWhy
Sign your scriptsPrevent tampering & enable AppLocker
Use -ErrorAction Stop in try/catchProper error handling
Log to Event Log or SIEMCentralized forensics
Test in sandboxAvoid self-inflicted outages
Version controlTrack changes (Git)

Deployment Strategies

  1. Scheduled Tasks - Run every 15-60 mins
  2. EDR/SIEM Triggers - Push script via RMM or orchestration
  3. USB "IR Stick" - Preloaded scripts for air-gapped response
  4. Constrained Endpoint - Limit script scope via JEA

Conclusion

PowerShell is not the enemy — poorly written or unmonitored PowerShell is.

By building defensive, automated, and auditable scripts, you turn a common attack tool into your strongest incident response ally.

Start small: Deploy one script (e.g., Run key cleaner) today.
Scale smart: Integrate with your SOC workflow.

Stay vigilant. Automate defense. Respond faster.


Defend. Detect. Respond. With PowerShell.