PowerShell is the primary automation and administration language for Windows Server. It is object-oriented, consistent, and scales from a single command to enterprise-wide automation. This lesson covers the fundamentals through to practical AD administration scripts.
Every PowerShell cmdlet follows the Verb-Noun pattern. Approved verbs include: Get, Set, New, Remove, Enable, Disable, Start, Stop, Invoke, Export, Import, Add, Move, Copy, Test, Install, Uninstall.
# Discovery commands — learn PowerShell from within PowerShell Get-Command # List all cmdlets available Get-Command -Verb Get # All cmdlets starting with Get- Get-Command -Noun Service # All cmdlets that work with Services Get-Command *AD* # Wildcard search — find AD-related cmdlets Get-Command -Module ActiveDirectory # All cmdlets in the AD module # Help system — always read the help! Get-Help Get-ADUser # Basic help Get-Help Get-ADUser -Detailed # Detailed help with parameter descriptions Get-Help Get-ADUser -Examples # Practical usage examples Get-Help Get-ADUser -Online # Open Microsoft docs in browser Update-Help # Download latest help files from Microsoft # Discover object properties and methods Get-Service | Get-Member # What properties and methods does a service object have? Get-ADUser Administrator | Get-Member -MemberType Property # AD User properties
# Check current execution policy Get-ExecutionPolicy Get-ExecutionPolicy -List # Show policy for all scopes # Scope hierarchy: MachinePolicy > UserPolicy > Process > CurrentUser > LocalMachine # Set policy for the local machine (run as admin): Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope LocalMachine # Execution policy values: # Restricted — No scripts (default on workstations) # AllSigned — Only scripts signed by a trusted publisher # RemoteSigned — Local scripts run freely; downloaded scripts must be signed # Unrestricted — All scripts run (not recommended for production) # Bypass — Nothing blocked (use only in specific automated scenarios) # Run a single script bypassing policy (does not change permanent setting) PowerShell.exe -ExecutionPolicy Bypass -File C:\Scripts\myscript.ps1
# Variables — prefix with $
$serverName = "SRV-DC01"
$port = 443
$servers = @("SRV-DC01","SRV-WEB01","SRV-FILE01") # Array
$config = @{Name="Web01"; IP="192.168.1.50"} # Hashtable
# The pipeline — pass objects from one cmdlet to the next with |
Get-Service | Where-Object {$_.Status -eq "Running"} | Select-Object Name, Status | Sort-Object Name
# Where-Object — filter objects
Get-Process | Where-Object {$_.CPU -gt 100} # Processes using >100% CPU
Get-ADUser -Filter * | Where-Object {$_.Enabled -eq $false} # Disabled users
# Select-Object — choose which properties to show
Get-ADUser -Filter * -Properties * | Select-Object Name, SamAccountName, LastLogonDate, Enabled
# Sort-Object
Get-ADComputer -Filter * | Sort-Object Name
# Format-Table — tabular output (great for viewing in console)
Get-Service | Format-Table Name, Status, StartType -AutoSize
# Format-List — vertical list output (shows all properties)
Get-ADUser jsmith -Properties * | Format-List *
# Export to CSV for reporting
Get-ADUser -Filter * -Properties DisplayName,EmailAddress,Department |
Select-Object Name, SamAccountName, EmailAddress, Department |
Export-Csv -Path "C:\Reports\users.csv" -NoTypeInformation -Encoding UTF8
# Out-GridView — interactive sortable/filterable grid (great for ad-hoc queries)
Get-ADUser -Filter * -Properties Department | Select-Object Name, SamAccountName, Department | Out-GridView
# Measure — count results
(Get-ADUser -Filter {Enabled -eq $true}).Count
# The ActiveDirectory module is available on DCs automatically. # On workstations/member servers, install RSAT: Add-WindowsCapability -Online -Name Rsat.ActiveDirectory.DS-LDS.Tools~~~~0.0.1.0 # Load the module Import-Module ActiveDirectory # Verify it loaded and see all available cmdlets Get-Command -Module ActiveDirectory | Measure-Object # Should show 147+ cmdlets Get-Module ActiveDirectory | Select-Object Name, Version
# Get a specific user (basic properties)
Get-ADUser -Identity jsmith
# Get a specific user with ALL properties loaded
Get-ADUser -Identity jsmith -Properties *
# Get all users in the domain
Get-ADUser -Filter *
# Get all users in a specific OU
Get-ADUser -Filter * -SearchBase "OU=IT,OU=_Lab,DC=lab,DC=local"
# Filter users by attribute
Get-ADUser -Filter {Department -eq "IT"} -Properties Department
Get-ADUser -Filter {Enabled -eq $true} -Properties LastLogonDate
Get-ADUser -Filter {PasswordExpired -eq $true} | Select-Object Name, SamAccountName
# Find users who haven't logged in for 90 days
$90days = (Get-Date).AddDays(-90)
Get-ADUser -Filter {LastLogonDate -lt $90days -and Enabled -eq $true} `
-Properties LastLogonDate |
Select-Object Name, SamAccountName, LastLogonDate |
Sort-Object LastLogonDate |
Export-Csv "C:\Reports\inactive_users.csv" -NoTypeInformation
# Create a single new user New-ADUser ` -Name "John Smith" ` -GivenName "John" ` -Surname "Smith" ` -SamAccountName "jsmith" ` -UserPrincipalName "jsmith@lab.local" ` -EmailAddress "john.smith@company.com" ` -Department "IT" ` -Title "Systems Administrator" ` -Path "OU=IT,OU=Users,OU=_Lab,DC=lab,DC=local" ` -AccountPassword (ConvertTo-SecureString "TempP@ss123!" -AsPlainText -Force) ` -ChangePasswordAtLogon $true ` -Enabled $true ` -Description "IT Department Admin"
# Update user attributes Set-ADUser -Identity jsmith -Department "IT" -Title "Senior Admin" -Office "Building A" # Enable / Disable accounts Enable-ADAccount -Identity jsmith Disable-ADAccount -Identity jsmith # Unlock a locked-out account Unlock-ADAccount -Identity jsmith # Reset password (force change at next logon) Set-ADAccountPassword -Identity jsmith ` -NewPassword (ConvertTo-SecureString "NewP@ss456!" -AsPlainText -Force) -Reset Set-ADUser -Identity jsmith -ChangePasswordAtLogon $true # Set account expiration Set-ADAccountExpiration -Identity jsmith -DateTime "2025-12-31" # Remove account (with confirmation prompt) Remove-ADUser -Identity jsmith # Without confirmation: Remove-ADUser -Identity jsmith -Confirm:$false # Move user to different OU Move-ADObject -Identity (Get-ADUser jsmith).DistinguishedName ` -TargetPath "OU=HR,OU=Users,OU=_Lab,DC=lab,DC=local"
# --- GROUP CMDLETS ---
# Get group info
Get-ADGroup -Identity "Domain Admins"
Get-ADGroup -Filter {GroupScope -eq "Global"} | Select-Object Name, GroupScope, GroupCategory
# Create a new group
New-ADGroup -Name "GG-IT-Staff" -GroupScope Global -GroupCategory Security `
-Path "OU=Groups,OU=_Lab,DC=lab,DC=local" -Description "IT Department Staff"
# Add members
Add-ADGroupMember -Identity "GG-IT-Staff" -Members "jsmith","bjones"
# Remove a member
Remove-ADGroupMember -Identity "GG-IT-Staff" -Members "bjones" -Confirm:$false
# List all members of a group (recursive — includes nested group members)
Get-ADGroupMember -Identity "Domain Admins" -Recursive | Select-Object Name, objectClass
# Find all groups a user belongs to (direct + nested)
(Get-ADUser jsmith -Properties MemberOf).MemberOf
# OR get all groups recursively:
Get-ADPrincipalGroupMembership jsmith | Select-Object Name, GroupScope
# --- COMPUTER CMDLETS ---
# Find all computers in domain
Get-ADComputer -Filter * -Properties OperatingSystem, LastLogonDate |
Select-Object Name, OperatingSystem, LastLogonDate | Sort-Object Name
# Find all servers (filter by OS name)
Get-ADComputer -Filter {OperatingSystem -like "*Server*"} `
-Properties OperatingSystem, IPv4Address |
Select-Object Name, OperatingSystem, IPv4Address
# Move computer to different OU
$comp = Get-ADComputer -Identity "LAPTOP-JSMITH"
Move-ADObject -Identity $comp.DistinguishedName `
-TargetPath "OU=Laptops,OU=Computers,OU=_Lab,DC=lab,DC=local"
# Find computers not logged in for 60 days
$60days = (Get-Date).AddDays(-60)
Get-ADComputer -Filter {LastLogonDate -lt $60days -and Enabled -eq $true} `
-Properties LastLogonDate | Select-Object Name, LastLogonDate
PowerShell Remoting (WinRM) lets you run commands on remote computers without physically logging in. It is essential for managing multiple servers.
# --- SETUP (run on each server to be managed) ---
# Enable PowerShell Remoting
Enable-PSRemoting -Force
# On non-domain machines, add trusted hosts (domain machines trust each other automatically)
Set-Item WSMan:\localhost\Client\TrustedHosts -Value "*" -Force
# Verify WinRM is running
Get-Service WinRM | Select-Object Status
Test-WSMan -ComputerName SRV-DC01 # Test connectivity
# --- ONE-TO-ONE: Interactive remote session ---
Enter-PSSession -ComputerName SRV-DC01
# You are now in the remote session — prompt changes to: [SRV-DC01]: PS C:\>
# Run commands as if sitting at that server
Get-Service | Where-Object {$_.Status -eq "Stopped"}
Exit-PSSession # Return to local machine
# --- ONE-TO-MANY: Run a command on multiple servers simultaneously ---
$servers = @("SRV-DC01","SRV-WEB01","SRV-FILE01")
# Get disk space on all servers at once
Invoke-Command -ComputerName $servers -ScriptBlock {
Get-PSDrive -PSProvider FileSystem |
Select-Object Name, @{N="FreeGB";E={[math]::Round($_.Free/1GB,2)}},
@{N="UsedGB";E={[math]::Round($_.Used/1GB,2)}}
} | Format-Table PSComputerName, Name, FreeGB, UsedGB -AutoSize
# Restart a service on multiple servers
Invoke-Command -ComputerName $servers -ScriptBlock {
Restart-Service -Name "Spooler" -Force
}
# Run a script file remotely
Invoke-Command -ComputerName SRV-DC01 -FilePath "C:\Scripts\check_health.ps1"
# Persistent session (reuse the connection for multiple commands)
$session = New-PSSession -ComputerName SRV-DC01
Invoke-Command -Session $session -ScriptBlock {hostname}
Invoke-Command -Session $session -ScriptBlock {Get-Service | Measure-Object}
Remove-PSSession -Session $session # Close the session when done
CIM (Common Information Model) cmdlets query hardware and OS information. Get-CimInstance is the modern replacement for Get-WmiObject (deprecated in PowerShell 7).
# Operating System info
Get-CimInstance Win32_OperatingSystem |
Select-Object Caption, Version, OSArchitecture, LastBootUpTime,
@{N="FreeRAM_GB";E={[math]::Round($_.FreePhysicalMemory/1MB,2)}}
# CPU info
Get-CimInstance Win32_Processor |
Select-Object Name, NumberOfCores, NumberOfLogicalProcessors, MaxClockSpeed
# RAM info
Get-CimInstance Win32_PhysicalMemory |
Select-Object BankLabel, Manufacturer, @{N="Size_GB";E={$_.Capacity/1GB}}
# Disk info
Get-CimInstance Win32_LogicalDisk -Filter "DriveType=3" |
Select-Object DeviceID,
@{N="Size_GB";E={[math]::Round($_.Size/1GB,1)}},
@{N="FreeSpace_GB";E={[math]::Round($_.FreeSpace/1GB,1)}},
@{N="Used_Pct";E={[math]::Round(($_.Size-$_.FreeSpace)/$_.Size*100,1)}}
# Network adapters with IP
Get-CimInstance Win32_NetworkAdapterConfiguration -Filter "IPEnabled=True" |
Select-Object Description, IPAddress, DefaultIPGateway, DNSServerSearchOrder
# Installed software
Get-CimInstance Win32_Product | Select-Object Name, Version, InstallDate | Sort-Object Name
# Running processes (CIM alternative to Get-Process)
Get-CimInstance Win32_Process |
Select-Object Name, ProcessId, @{N="MemMB";E={[math]::Round($_.WorkingSetSize/1MB,1)}} |
Sort-Object MemMB -Descending | Select-Object -First 15
# Run CIM queries remotely
Get-CimInstance Win32_OperatingSystem -ComputerName SRV-DC01 |
Select-Object PSComputerName, Caption, LastBootUpTime
Create a CSV file at C:\Scripts\new_users.csv with headers: FirstName, LastName, Department, Title, OU
# new_users.csv format:
# FirstName,LastName,Department,Title,OU
# Alice,Johnson,IT,Systems Admin,OU=IT,OU=Users,OU=_Lab,DC=lab,DC=local
# Bob,Williams,HR,HR Manager,OU=HR,OU=Users,OU=_Lab,DC=lab,DC=local
Import-Module ActiveDirectory
$defaultPassword = ConvertTo-SecureString "Welcome1!" -AsPlainText -Force
$csvPath = "C:\Scripts\new_users.csv"
Import-Csv -Path $csvPath | ForEach-Object {
$firstName = $_.FirstName
$lastName = $_.LastName
$samAccount = ($firstName.Substring(0,1) + $lastName).ToLower() # e.g., ajohnson
$upn = "$samAccount@lab.local"
$fullName = "$firstName $lastName"
try {
New-ADUser `
-Name $fullName `
-GivenName $firstName `
-Surname $lastName `
-SamAccountName $samAccount `
-UserPrincipalName $upn `
-Department $_.Department `
-Title $_.Title `
-Path $_.OU `
-AccountPassword $defaultPassword `
-ChangePasswordAtLogon $true `
-Enabled $true
Write-Host "Created: $fullName ($samAccount)" -ForegroundColor Green
}
catch {
Write-Host "FAILED: $fullName — $($_.Exception.Message)" -ForegroundColor Red
}
}
Import-Module ActiveDirectory
$cutoffDate = (Get-Date).AddDays(-90)
$reportPath = "C:\Reports\inactive_users_$(Get-Date -Format 'yyyyMMdd').csv"
$inactiveUsers = Get-ADUser -Filter {
Enabled -eq $true -and
LastLogonDate -lt $cutoffDate -and
PasswordNeverExpires -eq $false
} -Properties LastLogonDate, Department, Manager |
Select-Object Name, SamAccountName, LastLogonDate, Department,
@{N="ManagerName";E={(Get-ADUser $_.Manager -ErrorAction SilentlyContinue).Name}} |
Sort-Object LastLogonDate
$inactiveUsers | Export-Csv $reportPath -NoTypeInformation -Encoding UTF8
Write-Host "Found $($inactiveUsers.Count) inactive users. Report saved to $reportPath"
# Optional: Disable the inactive accounts after review
# $inactiveUsers | ForEach-Object { Disable-ADAccount -Identity $_.SamAccountName }
Import-Module ActiveDirectory
Get-ADComputer -Filter * -Properties OperatingSystem, OperatingSystemVersion,
LastLogonDate, IPv4Address |
Select-Object Name,
@{N="OS";E={$_.OperatingSystem}},
@{N="OSVersion";E={$_.OperatingSystemVersion}},
IPv4Address,
@{N="LastSeen";E={$_.LastLogonDate}} |
Sort-Object OS, Name |
Export-Csv "C:\Reports\computers.csv" -NoTypeInformation -Encoding UTF8
Write-Host "Computer inventory exported."
$servers = @("SRV-DC01","SRV-WEB01","SRV-FILE01")
$warnThreshold = 20 # Warn if less than 20% free
$results = @()
foreach ($server in $servers) {
try {
$disks = Get-CimInstance Win32_LogicalDisk -ComputerName $server `
-Filter "DriveType=3" -ErrorAction Stop
foreach ($disk in $disks) {
$freePercent = [math]::Round(($disk.FreeSpace / $disk.Size) * 100, 1)
$results += [PSCustomObject]@{
Server = $server
Drive = $disk.DeviceID
SizeGB = [math]::Round($disk.Size / 1GB, 1)
FreeGB = [math]::Round($disk.FreeSpace / 1GB, 1)
FreePct = $freePercent
Status = if ($freePercent -lt $warnThreshold) {"WARNING"} else {"OK"}
}
}
}
catch {
$results += [PSCustomObject]@{
Server = $server; Drive = "ERROR"; Status = $_.Exception.Message
}
}
}
$results | Format-Table -AutoSize
$results | Where-Object {$_.Status -eq "WARNING"} |
ForEach-Object { Write-Warning "LOW DISK: $($_.Server) $($_.Drive) — $($_.FreePct)% free" }
# Create a daily disk-space report scheduled task $action = New-ScheduledTaskAction ` -Execute "PowerShell.exe" ` -Argument "-NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -File C:\Scripts\disk_report.ps1" $trigger = New-ScheduledTaskTrigger -Daily -At "06:00AM" $settings = New-ScheduledTaskSettingsSet ` -StartWhenAvailable ` -RunOnlyIfNetworkAvailable ` -ExecutionTimeLimit (New-TimeSpan -Minutes 30) Register-ScheduledTask ` -TaskName "Daily Disk Space Report" ` -TaskPath "\Custom\" ` -Action $action ` -Trigger $trigger ` -Settings $settings ` -RunLevel Highest ` -User "SYSTEM" # Useful task management commands Get-ScheduledTask -TaskPath "\Custom\" | Select-Object TaskName, State, LastRunTime, LastTaskResult Start-ScheduledTask -TaskName "Daily Disk Space Report" -TaskPath "\Custom\" Export-ScheduledTask -TaskName "Daily Disk Space Report" -TaskPath "\Custom\" | Out-File task.xml
You now have a solid foundation in PowerShell for Windows Server administration — from basic cmdlets and pipelines, through AD management, remoting, WMI queries, and practical automation scripts.