PowerShell Street Ops¶
Real tasks. No theory. Copy-paste into a terminal and get answers.
Quick Audit of a Windows Server¶
# Top CPU consumers
Get-Process | Sort-Object CPU -Descending | Select-Object -First 10 Name, CPU, Id
# Services that should be running but aren't
Get-Service | Where-Object { $_.StartType -eq "Automatic" -and $_.Status -ne "Running" }
# Disk space
Get-PSDrive -PSProvider FileSystem | Select-Object Name,
@{N='UsedGB';E={[math]::Round($_.Used/1GB,1)}},
@{N='FreeGB';E={[math]::Round($_.Free/1GB,1)}}
# Listening ports
Get-NetTCPConnection -State Listen | Select-Object LocalAddress, LocalPort, OwningProcess
# Uptime
(Get-Date) - (Get-CimInstance Win32_OperatingSystem).LastBootUpTime
# Recent errors (last 24h)
Get-WinEvent -FilterHashtable @{LogName='System'; Level=2; StartTime=(Get-Date).AddHours(-24)} |
Select-Object -First 20 TimeCreated, Id, Message
Gotcha:
Get-WinEventwith-FilterHashtableis orders of magnitude faster thanGet-EventLogwithWhere-Object. The filter runs server-side. Never pipe all events throughWhere-Objecton a remote server — it pulls every event over the network first.One-liner: Quick "is this server healthy" check:
Get-CimInstance Win32_OperatingSystem | Select-Object @{N='Uptime';E={(Get-Date)-$_.LastBootUpTime}}, FreePhysicalMemory, NumberOfProcesses
Remember: PowerShell verb-noun mnemonic: every cmdlet is
Verb-Noun. The "Big Five" verbs cover 80% of ops work: Get (read), Set (change), New (create), Remove (delete), Invoke (run). If you know the noun (e.g.,Service,ADUser,AzVM), pair it with a verb and you probably have a valid command.
Active Directory Queries¶
# Locked accounts
Search-ADAccount -LockedOut | Select-Object Name, SamAccountName
# Unlock a user
Unlock-ADAccount -Identity jdoe
# Disabled users older than 90 days
Get-ADUser -Filter {Enabled -eq $false} -Properties WhenChanged |
Where-Object { $_.WhenChanged -lt (Get-Date).AddDays(-90) } |
Select-Object Name, SamAccountName, WhenChanged
# Domain Admins membership
Get-ADGroupMember "Domain Admins" | Select-Object Name, SamAccountName
# Recursive — catches nested group members (the ones auditors miss)
Get-ADGroupMember "Domain Admins" -Recursive | Select-Object Name, SamAccountName
# Password expiry this week
$cutoff = (Get-Date).AddDays(7)
Get-ADUser -Filter {Enabled -eq $true -and PasswordNeverExpires -eq $false} `
-Properties msDS-UserPasswordExpiryTimeComputed |
Where-Object { [datetime]::FromFileTime($_.'msDS-UserPasswordExpiryTimeComputed') -lt $cutoff }
Bulk Operations¶
$servers = "web01","web02","web03","web04"
# Restart a service across multiple servers
Invoke-Command -ComputerName $servers -ScriptBlock { Restart-Service W3SVC }
# Rename files in bulk
Get-ChildItem *.log | Rename-Item -NewName { $_.Name -replace 'oldprefix','newprefix' }
# Collect disk reports from a fleet
Invoke-Command -ComputerName $servers -ScriptBlock {
Get-PSDrive C | Select-Object @{N='Host';E={$env:COMPUTERNAME}},
@{N='FreeGB';E={[math]::Round($_.Free/1GB,1)}}
} | Sort-Object FreeGB
Debug clue: If
Invoke-Commandhangs, check WinRM:Test-WSMan -ComputerName web01. Common blockers: WinRM service not running, firewall blocking port 5985/5986, or the target not in TrustedHosts. Fix withwinrm quickconfigon the remote orSet-Item WSMan:\localhost\Client\TrustedHosts -Value "web01"locally.
Gotcha:
Invoke-Commanduses WinRM which defaults to port 5985 (HTTP) and 5986 (HTTPS). Many corporate firewalls block these. If SSH is available (Windows 10+/Server 2019+), useEnter-PSSession -HostName server -UserName adminwhich uses OpenSSH instead of WinRM — same PowerShell remoting, different transport.
Windows Event Logs¶
# Application errors (last 48h)
Get-WinEvent -FilterHashtable @{LogName='Application'; Level=1,2;
StartTime=(Get-Date).AddHours(-48)} |
Select-Object TimeCreated, ProviderName, Message | Format-List
# Failed logons (event 4625)
Get-WinEvent -FilterHashtable @{LogName='Security'; Id=4625} -MaxEvents 50 |
ForEach-Object {
$xml = [xml]$_.ToXml()
[PSCustomObject]@{
Time = $_.TimeCreated
Account = ($xml.Event.EventData.Data | Where-Object Name -eq 'TargetUserName').'#text'
Source = ($xml.Event.EventData.Data | Where-Object Name -eq 'IpAddress').'#text'
}
}
# Service crashes (event 7034)
Get-WinEvent -FilterHashtable @{LogName='System'; Id=7034} -MaxEvents 20 |
Select-Object TimeCreated, Message
One-Liners That Replace GUI Clicks¶
# IIS site bound to port 443
Import-Module WebAdministration; Get-WebBinding -Port 443
# Non-default scheduled tasks
Get-ScheduledTask | Where-Object { $_.TaskPath -notlike '\Microsoft\*' }
# Export local admins to CSV
Get-LocalGroupMember -Group "Administrators" | Export-Csv admins.csv -NoTypeInformation
# Check cert expiry on a remote host
$req = [Net.HttpWebRequest]::Create("https://web01.corp.com")
$req.GetResponse() | Out-Null; $req.ServicePoint.Certificate.GetExpirationDateString()
Default trap: PowerShell 5.1 (shipped with Windows) uses .NET Framework and has different behavior from PowerShell 7+ (pwsh) which uses .NET Core.
Invoke-RestMethodTLS defaults differ — PS 5.1 defaults to TLS 1.0, requiring[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12to talk to modern APIs.
Azure Resource Inventory¶
# All VMs with size and state
Get-AzVM -Status | Select-Object Name, ResourceGroupName,
@{N='Size';E={$_.HardwareProfile.VmSize}}, @{N='State';E={$_.PowerState}}
# Orphaned disks (not attached to any VM)
Get-AzDisk | Where-Object { $_.ManagedBy -eq $null } |
Select-Object Name, ResourceGroupName, DiskSizeGB
# Orphaned public IPs
Get-AzPublicIpAddress | Where-Object { $_.IpConfiguration -eq $null } |
Select-Object Name, IpAddress
Bash vs PowerShell Side-by-Side¶
| Task | Bash | PowerShell |
|---|---|---|
| Find large files | find / -size +100M |
Get-ChildItem -Recurse \| ? {$_.Length -gt 100MB} |
| Count lines | wc -l access.log |
(Get-Content access.log).Count |
| Search contents | grep -r "error" /var/log/ |
Select-String -Path /var/log/* -Pattern "error" -Recurse |
| Kill by name | pkill nginx |
Stop-Process -Name nginx |
| Env variable | export FOO=bar |
$env:FOO = "bar" |
| JSON from API | curl -s url \| jq .name |
(Invoke-RestMethod url).name |
Debug clue: When
Invoke-RestMethodfails with "Could not create SSL/TLS secure channel" in PowerShell 5.1, the fix is always the same one-liner at the top of your script:[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12. PowerShell 7 (pwsh) defaults to TLS 1.2+ and does not need this workaround.
Running PowerShell from Linux¶
pwsh -Command "Connect-AzAccount; Get-AzVM | Select Name, Location"
pwsh -File ./audit-azure.ps1
# Inline in a bash script
ORPHANED=$(pwsh -Command '(Get-AzDisk | ? {$_.ManagedBy -eq $null}).Count')
echo "Orphaned disks: $ORPHANED"
Use pwsh on your Linux jumpbox for Azure and M365. Use bash for everything else.
Under the hood:
pwshon Linux is a full .NET runtime. It can import any .NET assembly, which means you can use Azure SDK, AWS SDK, or any NuGet package directly. This makes it more powerful than just "bash for Windows" — it is a cross-platform automation framework.