In this module
IR1.7 PowerShell for Incident Response
PowerShell for Incident Response
The tool that is always available — even when nothing else is
You are responding to an incident on a system where KAPE has not been deployed. Velociraptor is not installed. You have no forensic tools on this endpoint. But you have PowerShell — and with PowerShell, you can collect running processes, network connections, scheduled tasks, service lists, registry values, event log entries, and file metadata. It is not as efficient as purpose-built forensic tools, but it is universally available on every Windows system and it can keep the investigation moving while you deploy the proper toolkit.
PowerShell is not a forensic tool in the same way KAPE and EZTools are forensic tools. It does not parse binary artifacts or build timelines. What PowerShell provides is direct access to the operating system and the M365 management plane — the ability to query, collect, and act on any system the investigator can authenticate to.
# Enable PowerShell remoting on the forensic workstation
# Run as Administrator
Enable-PSRemoting -Force
# Verify WinRM service is running
Get-Service WinRM | Select-Object Status, StartType
# Expected: Running, Automatic
# Test connectivity to a remote system before attempting a session
Test-WSMan -ComputerName "DESKTOP-NGE042"
# If this fails: the target's WinRM is not enabled, the firewall
# blocks port 5985, or the target is offline
# Open a remote session (Kerberos auth in domain environments)
$session = New-PSSession -ComputerName "DESKTOP-NGE042" -Credential (Get-Credential)
# Run commands remotely — the ScriptBlock executes on the target
Invoke-Command -Session $session -ScriptBlock {
Get-Process | Sort-Object CPU -Descending | Select-Object -First 10
}
# Run commands on MULTIPLE systems simultaneously
$targets = @("DESKTOP-NGE042", "DESKTOP-NGE001", "SERVER-NGE-DC01")
Invoke-Command -ComputerName $targets -Credential $cred -ScriptBlock {
Get-ScheduledTask | Where-Object { $_.State -eq 'Ready' -and
$_.Actions.Execute -match 'powershell|cmd|wscript' }
}
# Results include PSComputerName — you can see which system each result came from
# Close the session when done
Remove-PSSession $session# Configure TrustedHosts for non-domain environments
Set-Item WSMan:\localhost\Client\TrustedHosts -Value "192.168.1.50,192.168.1.51" -Force
# Connect using IP address with explicit credentials
$session = New-PSSession -ComputerName "192.168.1.50" -Credential (Get-Credential) -Authentication Negotiate# Install M365 management modules for IR
# Run as Administrator
# Exchange Online (email forensics — IR9)
Install-Module -Name ExchangeOnlineManagement -Force
# Microsoft Graph (Entra ID investigation — IR8, IR11)
Install-Module -Name Microsoft.Graph -Force
# Azure AD (legacy — some IR scripts still use this)
Install-Module -Name AzureAD -Force
# Verify installations
Get-Module -ListAvailable ExchangeOnlineManagement
Get-Module -ListAvailable Microsoft.Graph
Get-Module -ListAvailable AzureAD
# Connect to Exchange Online (test connection)
Connect-ExchangeOnline -UserPrincipalName admin@yourtenant.onmicrosoft.com
# Run a test command
Get-Mailbox | Select-Object -First 5 DisplayName, UserPrincipalName
Disconnect-ExchangeOnline -Confirm:$false# Microsoft Graph connection with investigation-appropriate scopes
# Each scope grants specific API access — use minimum necessary
# For identity investigation (IR8):
Connect-MgGraph -Scopes "User.Read.All", "AuditLog.Read.All", "Directory.Read.All"
# For identity containment (IR8, IR14):
Connect-MgGraph -Scopes "User.ReadWrite.All"
# For OAuth app investigation (IR11):
Connect-MgGraph -Scopes "Application.Read.All", "ServicePrincipalEndpoint.Read.All"
# For comprehensive IR (all of the above):
Connect-MgGraph -Scopes "User.ReadWrite.All", "AuditLog.Read.All", "Directory.Read.All", "Application.Read.All"
# Verify the connection and available scopes
Get-MgContext | Select-Object Scopes, Account, TenantId# Check installed module versions
Get-Module -ListAvailable ExchangeOnlineManagement, Microsoft.Graph, AzureAD |
Select-Object Name, Version | Format-Table
# Update modules to latest (run before a major investigation)
Update-Module ExchangeOnlineManagement -Force
Update-Module Microsoft.Graph -Force
# If Update-Module fails (common with Microsoft.Graph due to
# submodule dependencies), uninstall and reinstall:
# Uninstall-Module Microsoft.Graph -AllVersions -Force
# Install-Module Microsoft.Graph -Force# Collect running processes with command lines and network connections
# Use when KAPE/Velociraptor are unavailable
Get-Process | Select-Object Id, ProcessName, Path, StartTime,
@{Name='CommandLine'; Expression={(Get-CimInstance Win32_Process -Filter "ProcessId=$($_.Id)").CommandLine}} |
Export-Csv "C:\IR\Evidence\processes.csv" -NoTypeInformation
# Collect active network connections with owning process
Get-NetTCPConnection |
Where-Object { $_.State -eq 'Established' } |
Select-Object LocalAddress, LocalPort, RemoteAddress, RemotePort,
@{Name='Process'; Expression={(Get-Process -Id $_.OwningProcess -ErrorAction SilentlyContinue).ProcessName}} |
Export-Csv "C:\IR\Evidence\connections.csv" -NoTypeInformation# Revoke all active sessions for a compromised user
# Forces re-authentication — breaks stolen session tokens
Connect-MgGraph -Scopes "User.ReadWrite.All"
Revoke-MgUserSignInSession -UserId "jmorrison@northgateeng.com"
# Disable the account (prevents new sign-ins)
Update-MgUser -UserId "jmorrison@northgateeng.com" -AccountEnabled:$false
# Reset the password (invalidates cached credentials)
$newPassword = @{
Password = [System.Web.Security.Membership]::GeneratePassword(16, 4)
ForceChangePasswordNextSignIn = $true
}
Update-MgUser -UserId "jmorrison@northgateeng.com" -PasswordProfile $newPassword# Remove malicious inbox rules created by the attacker
Connect-ExchangeOnline -UserPrincipalName admin@yourtenant.onmicrosoft.com
# List all inbox rules for the compromised user
Get-InboxRule -Mailbox "jmorrison@northgateeng.com" | Format-List Name, Description, Enabled, ForwardTo, RedirectTo, DeleteMessage
# Remove a specific malicious rule (attacker-created forwarding)
Remove-InboxRule -Mailbox "jmorrison@northgateeng.com" -Identity "Update Rule" -Confirm:$false
# Check for mail forwarding (SMTP forwarding, not inbox rules)
Get-Mailbox "jmorrison@northgateeng.com" | Select-Object ForwardingSmtpAddress, ForwardingAddress, DeliverToMailboxAndForward# Search mailbox audit log for the compromised user
# Identify what the attacker accessed
Search-UnifiedAuditLog -StartDate "2026-03-15" -EndDate "2026-03-16" -UserIds "jmorrison@northgateeng.com" -Operations MailItemsAccessed, Send, MoveToDeletedItems, New-InboxRule -ResultSize 5000 |
Select-Object CreationDate, UserIds, Operations, AuditData |
Export-Csv "C:\IR\Evidence\mailbox_audit.csv" -NoTypeInformation# Comprehensive endpoint evidence collection via PowerShell
# Run as Administrator on the target system (or remotely via Invoke-Command)
$outputDir = "C:\IR\Evidence\PS_Collection_$(Get-Date -Format 'yyyyMMdd_HHmm')"
New-Item -ItemType Directory -Path $outputDir -Force
# Scheduled tasks — persistence mechanism (T1053.005)
Get-ScheduledTask | Where-Object { $_.State -ne 'Disabled' } |
Select-Object TaskName, TaskPath, State,
@{Name='Actions'; Expression={($_.Actions | ForEach-Object { $_.Execute + " " + $_.Arguments }) -join "; "}},
@{Name='Triggers'; Expression={($_.Triggers | ForEach-Object { $_.GetType().Name }) -join "; "}} |
Export-Csv "$outputDir\scheduled_tasks.csv" -NoTypeInformation
# Services — persistence and lateral movement artifacts
Get-CimInstance Win32_Service |
Select-Object Name, DisplayName, State, StartMode, PathName, StartName |
Export-Csv "$outputDir\services.csv" -NoTypeInformation
# Startup programs — persistence via Run keys and Startup folders
Get-CimInstance Win32_StartupCommand |
Select-Object Name, Command, Location, User |
Export-Csv "$outputDir\startup_programs.csv" -NoTypeInformation
# Local user accounts — look for attacker-created accounts
Get-LocalUser | Select-Object Name, Enabled, LastLogon, PasswordLastSet,
Description, SID |
Export-Csv "$outputDir\local_users.csv" -NoTypeInformation
# Local group membership — check Administrators group for unauthorized members
Get-LocalGroupMember -Group "Administrators" |
Select-Object Name, ObjectClass, PrincipalSource |
Export-Csv "$outputDir\local_admins.csv" -NoTypeInformation
# DNS cache — reveals recently resolved domains (C2 indicators)
Get-DnsClientCache |
Select-Object Entry, RecordName, RecordType, Data, TimeToLive |
Export-Csv "$outputDir\dns_cache.csv" -NoTypeInformation
# Recent event log entries — last 24 hours of Security log
Get-WinEvent -FilterHashtable @{LogName='Security'; StartTime=(Get-Date).AddHours(-24)} -MaxEvents 10000 |
Select-Object TimeCreated, Id, LevelDisplayName, Message |
Export-Csv "$outputDir\security_events_24h.csv" -NoTypeInformation
# PowerShell ScriptBlock log — what PowerShell commands were executed
Get-WinEvent -FilterHashtable @{LogName='Microsoft-Windows-PowerShell/Operational'; Id=4104} -MaxEvents 500 -ErrorAction SilentlyContinue |
Select-Object TimeCreated, @{Name='ScriptBlock'; Expression={$_.Properties[2].Value}} |
Export-Csv "$outputDir\powershell_scriptblocks.csv" -NoTypeInformation
Write-Host "Collection complete: $outputDir" -ForegroundColor Green
Get-ChildItem $outputDir | ForEach-Object { Write-Host " $($_.Name) — $([math]::Round($_.Length/1KB, 1)) KB" }# Batch evidence collection across multiple endpoints
# Use when scoping an incident across the organization
$targetComputers = @(
"DESKTOP-NGE001",
"DESKTOP-NGE042",
"LAPTOP-NGE015",
"SERVER-NGE-DC01"
)
$credential = Get-Credential -Message "Enter domain admin credentials for remote collection"
$iocTaskName = "ChromeUpdate" # Suspicious scheduled task from initial investigation
$results = Invoke-Command -ComputerName $targetComputers -Credential $credential -ScriptBlock {
param($taskName)
$task = Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue
[PSCustomObject]@{
ComputerName = $env:COMPUTERNAME
TaskFound = $null -ne $task
TaskState = if ($task) { $task.State } else { "N/A" }
TaskAction = if ($task) { ($task.Actions | ForEach-Object { $_.Execute }) -join "; " } else { "N/A" }
LastRunTime = if ($task) { (Get-ScheduledTaskInfo -TaskName $taskName -ErrorAction SilentlyContinue).LastRunTime } else { "N/A" }
}
} -ArgumentList $iocTaskName -ErrorAction SilentlyContinue
$results | Format-Table -AutoSize
$results | Export-Csv "C:\IR\Evidence\scope_check_$(Get-Date -Format 'yyyyMMdd').csv" -NoTypeInformation
# Report: which endpoints have the indicator?
$affected = ($results | Where-Object { $_.TaskFound -eq $true }).Count
Write-Host "`n$affected of $($targetComputers.Count) endpoints have the '$iocTaskName' scheduled task" -ForegroundColor $(if ($affected -gt 0) { 'Red' } else { 'Green' })# Entra ID investigation: check for attacker-created persistence
Connect-MgGraph -Scopes "Application.Read.All", "AuditLog.Read.All"
# Check for recently registered applications (attacker may register
# a malicious app for persistent OAuth access)
Get-MgApplication -Filter "createdDateTime ge 2026-03-14T00:00:00Z" |
Select-Object DisplayName, AppId, CreatedDateTime,
@{Name='Owners'; Expression={(Get-MgApplicationOwner -ApplicationId $_.Id).AdditionalProperties.userPrincipalName -join "; "}} |
Format-Table -AutoSize
# Check for service principal credentials (attacker may add a
# secret to an existing service principal for persistence)
Get-MgServicePrincipal -All | ForEach-Object {
$sp = $_
$creds = Get-MgServicePrincipal -ServicePrincipalId $sp.Id -Property "passwordCredentials,keyCredentials"
if ($creds.PasswordCredentials -or $creds.KeyCredentials) {
[PSCustomObject]@{
DisplayName = $sp.DisplayName
AppId = $sp.AppId
PasswordCredentials = ($creds.PasswordCredentials | Measure-Object).Count
KeyCredentials = ($creds.KeyCredentials | Measure-Object).Count
LatestCredential = ($creds.PasswordCredentials | Sort-Object StartDateTime -Descending | Select-Object -First 1).StartDateTime
}
}
} | Where-Object { $_.LatestCredential -gt (Get-Date).AddDays(-7) } |
Format-Table -AutoSize
# Check for suspicious MFA method additions
# Attacker may add their own phone number or authenticator app
Get-MgUserAuthenticationMethod -UserId "jmorrison@northgateeng.com" |
Select-Object Id, @{Name='Type'; Expression={$_.AdditionalProperties.'@odata.type'}}# Isolate a compromised device via Defender for Endpoint API
# This blocks all network connections except to the Defender service
# The device can still be managed and investigated remotely
$deviceId = "abc123def456" # Device ID from Defender portal
$body = @{
Comment = "INC-NE-2026-0315-001: Isolating compromised workstation for investigation"
IsolationType = "Full" # Full = block all traffic. Selective = allow Outlook/Teams
} | ConvertTo-Json
Invoke-MgGraphRequest -Method POST -Uri "https://api.securitycenter.microsoft.com/api/machines/$deviceId/isolate" -Body $body -ContentType "application/json"# IR Script: Complete Identity Containment
# Save as: C:\IR\Tools\Scripts\Contain-Identity.ps1
# Usage: .\Contain-Identity.ps1 -UserPrincipalName "jmorrison@northgateeng.com" -CaseID "INC-NE-2026-0315-001"
#
# This script performs the complete identity containment sequence
# for a compromised M365 account in the correct order:
param(
[Parameter(Mandatory)][string]$UserPrincipalName,
[Parameter(Mandatory)][string]$CaseID
)
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss UTC" -AsUTC
$log = @("=== Identity Containment Log ===", "Case: $CaseID", "Target: $UserPrincipalName", "Started: $timestamp", "")
Write-Host "=== Containing $UserPrincipalName ===" -ForegroundColor Red
# Step 1: Revoke all active sessions (breaks stolen tokens immediately)
Connect-MgGraph -Scopes "User.ReadWrite.All" -NoWelcome
Revoke-MgUserSignInSession -UserId $UserPrincipalName
$log += "$(Get-Date -Format HH:mm:ss) — Sessions revoked"
Write-Host " [1/5] Sessions revoked" -ForegroundColor Yellow
# Step 2: Disable the account (prevents new sign-ins)
Update-MgUser -UserId $UserPrincipalName -AccountEnabled:$false
$log += "$(Get-Date -Format HH:mm:ss) — Account disabled"
Write-Host " [2/5] Account disabled" -ForegroundColor Yellow
# Step 3: Reset password (invalidates cached credentials)
$newPw = -join ((65..90) + (97..122) + (48..57) + (33..38) | Get-Random -Count 20 | ForEach-Object { [char]$_ })
$pwProfile = @{ Password = $newPw; ForceChangePasswordNextSignIn = $true }
Update-MgUser -UserId $UserPrincipalName -PasswordProfile $pwProfile
$log += "$(Get-Date -Format HH:mm:ss) — Password reset (new pw in secure log)"
Write-Host " [3/5] Password reset" -ForegroundColor Yellow
# Step 4: Check and remove suspicious inbox rules
Connect-ExchangeOnline -UserPrincipalName admin@yourtenant.onmicrosoft.com -ShowBanner:$false
$rules = Get-InboxRule -Mailbox $UserPrincipalName
$suspiciousRules = $rules | Where-Object {
$_.ForwardTo -or $_.RedirectTo -or $_.ForwardAsAttachmentTo -or $_.DeleteMessage -eq $true
}
foreach ($rule in $suspiciousRules) {
Remove-InboxRule -Mailbox $UserPrincipalName -Identity $rule.Identity -Confirm:$false
$log += "$(Get-Date -Format HH:mm:ss) — Removed inbox rule: $($rule.Name) (Forward: $($rule.ForwardTo))"
Write-Host " [4/5] Removed suspicious rule: $($rule.Name)" -ForegroundColor Yellow
}
if (-not $suspiciousRules) {
$log += "$(Get-Date -Format HH:mm:ss) — No suspicious inbox rules found"
Write-Host " [4/5] No suspicious inbox rules found" -ForegroundColor Green
}
# Step 5: Check for SMTP forwarding
$mbx = Get-Mailbox $UserPrincipalName
if ($mbx.ForwardingSmtpAddress) {
Set-Mailbox $UserPrincipalName -ForwardingSmtpAddress $null -ForwardingAddress $null
$log += "$(Get-Date -Format HH:mm:ss) — Removed SMTP forwarding: $($mbx.ForwardingSmtpAddress)"
Write-Host " [5/5] Removed SMTP forwarding" -ForegroundColor Yellow
} else {
$log += "$(Get-Date -Format HH:mm:ss) — No SMTP forwarding configured"
Write-Host " [5/5] No SMTP forwarding found" -ForegroundColor Green
}
Disconnect-ExchangeOnline -Confirm:$false
Disconnect-MgGraph
# Write containment log
$log += "", "Completed: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss UTC' -AsUTC)"
$logPath = "C:\IR\Cases\$CaseID\Notes\containment_$($UserPrincipalName.Replace('@','_')).txt"
$log | Out-File $logPath -Force
Write-Host "`nContainment complete. Log: $logPath" -ForegroundColor GreenBuild it: Test your IR PowerShell capabilities
Run the process collection and network connection commands on your for...
Run the process collection and network connection commands on your forensic workstation. Export to CSV and open in Timeline Explorer. If you have a Developer Tenant, connect to Exchange Online and list the mailboxes — these are the mailboxes you will investigate in IR9. Install the Microsoft Graph module and test the connection — this is the authentication path for IR8 and IR11. Every module you can run now is a module you don't need to debug during an investigation.
Beyond this investigation
The techniques taught in this subsection apply beyond the specific scenario presented here. The same evidence sources, tools, and analytical methods are used across ransomware, BEC, insider threat, and APT investigations — the context changes but the methodology is consistent.
You discover evidence that the attacker has been in the environment for 90 days. The CISO asks: 'Why did our SOC not detect this sooner?' How do you answer constructively?
Answer with facts, not defensiveness. 'The attacker used [specific techniques] that our current detection rules do not cover. The investigation identified [N] detection gaps — [list the specific ATT&CK techniques that were not detected]. The IR-to-DE handoff includes these gaps as detection engineering sprint items. Estimated time to close: [N weeks].' This answer is honest (we missed it), specific (here is what we missed and why), and forward-looking (here is how we fix it). The PIR action items transform the detection failure into a measurable improvement program.