🏠 Home / Hub

Lesson 3 — PowerShell Administration

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.

1. PowerShell Fundamentals

Cmdlet Syntax: Verb-Noun

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

Execution Policy

# 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

2. Variables, Pipelines, and Filtering

# 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

3. Import-Module ActiveDirectory

# 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

4. AD User Management Cmdlets

Query Users

# 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 Users

# 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"

Modify and Manage Users

# 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"

5. AD Group and Computer Cmdlets

# --- 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

6. PowerShell Remoting

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

7. WMI / CIM Queries

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

8. Practical Scripts

Script 1: Bulk User Creation from CSV

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
    }
}

Script 2: Find Inactive Users (90 Days)

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 }

Script 3: Get All Domain Computers with OS Version

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."

Script 4: Get Disk Space on Multiple Servers

$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" }

9. Scheduled Tasks via PowerShell

# 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

Lesson 3 Complete

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.

Next: IIS Web Server →

📌 Study Checklist