diff --git a/PowerShell_How2_Adv.md b/PowerShell_How2_Adv.md
new file mode 100644
index 0000000..1e2e92c
--- /dev/null
+++ b/PowerShell_How2_Adv.md
@@ -0,0 +1,1620 @@
+# Advanced PowerShell — L3 / SME Reference
+> Deep-dive techniques for senior engineers and subject matter experts. Assumes solid PowerShell fundamentals.
+> Covers automation architecture, performance, security, internals, and enterprise-scale patterns.
+
+---
+
+## Table of Contents
+1. [Script Architecture & Module Design](#script-architecture--module-design)
+2. [Advanced Functions & Parameter Binding](#advanced-functions--parameter-binding)
+3. [The PowerShell Pipeline — Internals](#the-powershell-pipeline--internals)
+4. [Classes & Object-Oriented PowerShell](#classes--object-oriented-powershell)
+5. [Runspaces & Parallel Execution](#runspaces--parallel-execution)
+6. [Desired State Configuration (DSC)](#desired-state-configuration-dsc)
+7. [Just Enough Administration (JEA)](#just-enough-administration-jea)
+8. [Secrets & Credential Management](#secrets--credential-management)
+9. [PowerShell Remoting — Advanced Patterns](#powershell-remoting--advanced-patterns)
+10. [Logging, Auditing & Transcription](#logging-auditing--transcription)
+11. [Error Handling — Production Patterns](#error-handling--production-patterns)
+12. [Regular Expressions in PowerShell](#regular-expressions-in-powershell)
+13. [Working with APIs & REST](#working-with-apis--rest)
+14. [Working with Databases](#working-with-databases)
+15. [XML, JSON & YAML Manipulation](#xml-json--yaml-manipulation)
+16. [Performance Optimisation](#performance-optimisation)
+17. [Security & Constrained Language Mode](#security--constrained-language-mode)
+18. [Working with .NET Directly](#working-with-net-directly)
+19. [PowerShell Providers & PSDrives](#powershell-providers--psdrives)
+20. [Enterprise Automation Patterns](#enterprise-automation-patterns)
+21. [Debugging & Diagnostics](#debugging--diagnostics)
+22. [CI/CD Integration for PowerShell](#cicd-integration-for-powershell)
+
+---
+
+## Script Architecture & Module Design
+
+### Module Structure (Production Standard)
+```
+MyModule/
+├── MyModule.psd1 # Module manifest (the "front door")
+├── MyModule.psm1 # Root module — loads everything
+├── Public/ # Exported functions (visible to users)
+│ ├── Get-ServerHealth.ps1
+│ └── Set-ServerConfig.ps1
+├── Private/ # Internal helper functions (not exported)
+│ ├── Write-Log.ps1
+│ └── Test-Connectivity.ps1
+├── Classes/ # PS classes (loaded before functions)
+│ └── ServerConfig.ps1
+├── Data/ # Static data, config templates
+│ └── defaults.psd1
+└── Tests/ # Pester tests
+ ├── Get-ServerHealth.Tests.ps1
+ └── Set-ServerConfig.Tests.ps1
+```
+
+### Module Manifest (psd1) — Key Fields
+```powershell
+@{
+ ModuleVersion = '2.1.0'
+ GUID = 'a1b2c3d4-...' # New-Guid to generate
+ Author = 'Infra Team'
+ Description = 'Enterprise server management module'
+ PowerShellVersion = '5.1'
+ RootModule = 'MyModule.psm1'
+
+ # Only export what you intend to be public API
+ FunctionsToExport = @('Get-ServerHealth', 'Set-ServerConfig')
+ CmdletsToExport = @()
+ VariablesToExport = @()
+ AliasesToExport = @()
+
+ # Declare external module dependencies
+ RequiredModules = @(
+ @{ ModuleName = 'ActiveDirectory'; ModuleVersion = '1.0.0' }
+ )
+
+ PrivateData = @{
+ PSData = @{
+ Tags = @('Infrastructure', 'Server', 'Automation')
+ ProjectUri = 'https://github.com/corp/MyModule'
+ }
+ }
+}
+```
+
+### Root Module Loader (psm1)
+```powershell
+# Load classes FIRST — functions may depend on them
+foreach ($class in Get-ChildItem "$PSScriptRoot\Classes\*.ps1") {
+ . $class.FullName
+}
+
+# Load private helpers
+foreach ($private in Get-ChildItem "$PSScriptRoot\Private\*.ps1") {
+ . $private.FullName
+}
+
+# Load public functions
+foreach ($public in Get-ChildItem "$PSScriptRoot\Public\*.ps1") {
+ . $public.FullName
+}
+
+# Export only Public functions — Private stay hidden
+Export-ModuleMember -Function (Get-ChildItem "$PSScriptRoot\Public\*.ps1").BaseName
+```
+> **Why this pattern?** It gives you clean separation of public API vs internals, makes Pester testing straightforward, and lets multiple developers work on different functions without merge conflicts on a single large .psm1 file.
+
+---
+
+## Advanced Functions & Parameter Binding
+
+### Full CmdletBinding Template
+```powershell
+function Invoke-ServerMaintenance {
+ [CmdletBinding(
+ SupportsShouldProcess = $true, # Enables -WhatIf and -Confirm
+ ConfirmImpact = 'High', # Triggers confirm prompt by default
+ DefaultParameterSetName = 'ByName'
+ )]
+ param (
+ # Parameter sets let one function handle multiple input patterns
+ [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName,
+ ParameterSetName = 'ByName', Position = 0)]
+ [ValidateNotNullOrEmpty()]
+ [string[]]$ComputerName,
+
+ [Parameter(Mandatory, ParameterSetName = 'BySession')]
+ [System.Management.Automation.Runspaces.PSSession[]]$Session,
+
+ [Parameter()]
+ [ValidateSet('Reboot','DrainConnections','RunChecks')]
+ [string]$Action = 'RunChecks',
+
+ [Parameter()]
+ [ValidateRange(1, 300)]
+ [int]$TimeoutSeconds = 60,
+
+ [Parameter()]
+ [ValidateScript({
+ if (Test-Path $_) { $true }
+ else { throw "Log path '$_' does not exist." }
+ })]
+ [string]$LogPath,
+
+ # Switch params — presence means $true
+ [switch]$Force
+ )
+
+ begin {
+ # Runs ONCE before pipeline input — initialise, open connections, etc.
+ Write-Verbose "[$($MyInvocation.MyCommand)] Starting — Action: $Action"
+ $results = [System.Collections.Generic.List[PSObject]]::new()
+ }
+
+ process {
+ # Runs ONCE PER PIPELINE OBJECT
+ foreach ($computer in $ComputerName) {
+ # ShouldProcess check — respects -WhatIf and -Confirm
+ if ($PSCmdlet.ShouldProcess($computer, "Perform $Action")) {
+ try {
+ # ... do work ...
+ $results.Add([PSCustomObject]@{
+ ComputerName = $computer
+ Action = $Action
+ Success = $true
+ Timestamp = Get-Date
+ })
+ }
+ catch {
+ Write-Error "Failed on $computer`: $_"
+ }
+ }
+ }
+ }
+
+ end {
+ # Runs ONCE after all pipeline input — finalise, close connections, output
+ Write-Verbose "[$($MyInvocation.MyCommand)] Completed. Processed $($results.Count) servers."
+ $results
+ }
+}
+```
+
+### Dynamic Parameters
+```powershell
+# Dynamic params are added at runtime based on other parameter values
+function Get-EnvironmentConfig {
+ [CmdletBinding()]
+ param (
+ [ValidateSet('Production','Staging','Dev')]
+ [string]$Environment
+ )
+
+ DynamicParam {
+ $paramDictionary = [System.Management.Automation.RuntimeDefinedParameterDictionary]::new()
+
+ if ($Environment -eq 'Production') {
+ $attr = [System.Management.Automation.ParameterAttribute]@{ Mandatory = $true }
+ $validateSet = [System.Management.Automation.ValidateSetAttribute]::new('FullAudit','ReadOnly')
+ $collection = [System.Collections.ObjectModel.Collection[System.Attribute]]::new()
+ $collection.Add($attr)
+ $collection.Add($validateSet)
+
+ $dynParam = [System.Management.Automation.RuntimeDefinedParameter]::new(
+ 'AccessMode', [string], $collection
+ )
+ $paramDictionary.Add('AccessMode', $dynParam)
+ }
+ return $paramDictionary
+ }
+
+ process {
+ $accessMode = $PSBoundParameters['AccessMode']
+ Write-Output "Environment: $Environment | AccessMode: $accessMode"
+ }
+}
+```
+
+### Parameter Validation Attributes (Full List)
+```powershell
+[ValidateNotNull()] # Not $null
+[ValidateNotNullOrEmpty()] # Not $null or ""
+[ValidateSet('A','B','C')] # Must be one of these values
+[ValidateRange(1, 100)] # Numeric range
+[ValidateLength(3, 50)] # String length range
+[ValidatePattern('^[A-Z]{3}\d{4}$')] # Must match regex
+[ValidateCount(1, 10)] # Array element count
+[ValidateScript({ $_ -gt 0 })] # Custom validation logic
+[AllowNull()] # Explicitly permit $null
+[AllowEmptyString()] # Explicitly permit ""
+[AllowEmptyCollection()] # Explicitly permit @()
+```
+
+---
+
+## The PowerShell Pipeline — Internals
+
+### How the Pipeline Actually Works
+```powershell
+# The pipeline streams objects one at a time — it does NOT buffer everything first.
+# This is crucial for memory efficiency with large datasets.
+
+# BAD — loads ALL 100k objects into memory, then sorts
+Get-Content "C:\Logs\huge.log" | Sort-Object # OOM risk on large files
+
+# GOOD — stream-process without full load into memory
+Get-Content "C:\Logs\huge.log" | ForEach-Object {
+ if ($_ -match "ERROR") { $_ }
+}
+```
+
+### WriteObject vs Return in the Pipeline
+```powershell
+function Get-Items {
+ process {
+ # $PSCmdlet.WriteObject() sends to pipeline WITHOUT collecting into array
+ # This is what PowerShell does internally when you just output an object
+ foreach ($i in 1..5) {
+ [PSCustomObject]@{ Id = $i; Value = $i * 2 }
+ # ^ Each object hits the pipeline immediately — next cmdlet can process it
+ }
+
+ # Using 'return' or wrapping in @() forces array buffering — avoid in hot loops
+ }
+}
+```
+
+### SteppablePipeline — Proxy Functions
+```powershell
+# Wrap an existing cmdlet to intercept/modify its pipeline behaviour
+function Get-Process {
+ [CmdletBinding()]
+ param([string]$Name)
+
+ begin {
+ # Get metadata for the real Get-Process
+ $wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand(
+ 'Microsoft.PowerShell.Management\Get-Process', [System.Management.Automation.CommandTypes]::Cmdlet
+ )
+ $scriptCmd = { & $wrappedCmd @PSBoundParameters }
+ $steppable = $scriptCmd.GetSteppablePipeline($myInvocation.CommandOrigin)
+ $steppable.Begin($PSCmdlet)
+ }
+ process {
+ # Intercept — add custom logic here before/after forwarding
+ $steppable.Process($_)
+ }
+ end {
+ $steppable.End()
+ }
+}
+```
+> Proxy functions are powerful for **adding logging, telemetry, or parameter defaults** to built-in cmdlets without breaking existing scripts that call them.
+
+---
+
+## Classes & Object-Oriented PowerShell
+
+### Full Class Pattern
+```powershell
+# Classes must be defined BEFORE they are used (dot-source or load in begin{})
+
+class ServerConfig {
+ # Properties with types
+ [string] $Hostname
+ [string] $Environment
+ [int] $MaxConnections
+ [bool] $IsProduction
+ [datetime] $LastChecked
+
+ # Hidden property — not shown in default output
+ hidden [string] $_internalId
+
+ # Static property — shared across all instances
+ static [string] $DefaultEnvironment = 'Development'
+
+ # Constructor
+ ServerConfig([string]$hostname, [string]$environment) {
+ $this.Hostname = $hostname
+ $this.Environment = $environment
+ $this.MaxConnections = 100
+ $this.IsProduction = ($environment -eq 'Production')
+ $this.LastChecked = Get-Date
+ $this._internalId = [System.Guid]::NewGuid().ToString()
+ }
+
+ # Default constructor
+ ServerConfig() : base() {
+ $this.Environment = [ServerConfig]::DefaultEnvironment
+ }
+
+ # Method
+ [bool] IsHealthy() {
+ return (Test-Connection $this.Hostname -Count 1 -Quiet)
+ }
+
+ # Static method
+ static [ServerConfig] FromHashtable([hashtable]$data) {
+ $config = [ServerConfig]::new($data.Hostname, $data.Environment)
+ $config.MaxConnections = $data.MaxConnections ?? 100
+ return $config
+ }
+
+ # Override ToString for readable output
+ [string] ToString() {
+ return "[$($this.Environment)] $($this.Hostname)"
+ }
+}
+
+# Inheritance
+class WebServerConfig : ServerConfig {
+ [int] $HttpPort = 80
+ [int] $HttpsPort = 443
+ [string] $SiteName
+
+ WebServerConfig([string]$hostname, [string]$siteName) : base($hostname, 'Production') {
+ $this.SiteName = $siteName
+ }
+
+ [bool] IsSslConfigured() {
+ return (Test-NetConnection $this.Hostname -Port $this.HttpsPort -WarningAction SilentlyContinue).TcpTestSucceeded
+ }
+}
+
+# Usage
+$srv = [ServerConfig]::new('SERVER01', 'Production')
+$web = [WebServerConfig]::new('WEB01', 'MainSite')
+$web.IsHealthy()
+$web.IsSslConfigured()
+```
+
+### Implementing Interfaces via Classes
+```powershell
+# PowerShell classes can implement .NET interfaces
+class LogEntry : System.IComparable {
+ [datetime] $Timestamp
+ [string] $Message
+ [string] $Severity
+
+ LogEntry([string]$message, [string]$severity) {
+ $this.Timestamp = Get-Date
+ $this.Message = $message
+ $this.Severity = $severity
+ }
+
+ # Required by IComparable — enables Sort-Object on collections
+ [int] CompareTo([object]$other) {
+ return $this.Timestamp.CompareTo(([LogEntry]$other).Timestamp)
+ }
+}
+
+$entries = @(
+ [LogEntry]::new("System started", "INFO")
+ [LogEntry]::new("Disk low", "WARN")
+)
+$entries | Sort-Object # Now works correctly via IComparable
+```
+
+---
+
+## Runspaces & Parallel Execution
+
+### ForEach-Object -Parallel (PowerShell 7+)
+```powershell
+# Clean and simple for most parallel work
+$servers = @("SERVER01","SERVER02","SERVER03","SERVER04","SERVER05")
+
+$results = $servers | ForEach-Object -Parallel {
+ $server = $_
+ $ping = Test-Connection $server -Count 1 -Quiet
+ [PSCustomObject]@{
+ Server = $server
+ Online = $ping
+ Time = Get-Date
+ }
+} -ThrottleLimit 10 # Max concurrent threads (default 5)
+
+# Pass variables into parallel scope with $using:
+$timeout = 30
+$servers | ForEach-Object -Parallel {
+ Test-NetConnection $_ -Port 443 -InformationLevel Quiet -WarningAction SilentlyContinue
+ # Access outer scope variable
+ Start-Sleep -Milliseconds $using:timeout
+} -ThrottleLimit 5
+```
+
+### Runspace Pools (PowerShell 5.1 Compatible)
+```powershell
+# More control, works on PS 5.1 — needed in enterprise environments
+$scriptBlock = {
+ param($Server, $Timeout)
+ try {
+ $result = Test-Connection $Server -Count 1 -Quiet -ErrorAction Stop
+ [PSCustomObject]@{ Server = $Server; Online = $result; Error = $null }
+ }
+ catch {
+ [PSCustomObject]@{ Server = $Server; Online = $false; Error = $_.Exception.Message }
+ }
+}
+
+$servers = @("SERVER01","SERVER02","SERVER03","SERVER04","SERVER05")
+$maxThreads = 10
+
+# Create and open the runspace pool
+$pool = [RunspaceFactory]::CreateRunspacePool(1, $maxThreads)
+$pool.Open()
+
+# Spin up all jobs
+$jobs = foreach ($server in $servers) {
+ $ps = [PowerShell]::Create()
+ $ps.RunspacePool = $pool
+ [void]$ps.AddScript($scriptBlock)
+ [void]$ps.AddArgument($server)
+ [void]$ps.AddArgument(30)
+ [PSCustomObject]@{
+ PowerShell = $ps
+ Handle = $ps.BeginInvoke()
+ Server = $server
+ }
+}
+
+# Collect results as they complete
+$results = foreach ($job in $jobs) {
+ $job.PowerShell.EndInvoke($job.Handle)
+ $job.PowerShell.Dispose()
+}
+
+$pool.Close()
+$pool.Dispose()
+
+$results | Format-Table -AutoSize
+```
+> Runspace pools are the **gold standard for high-performance parallel work in PS 5.1**. Unlike Start-Job (which spawns new processes), runspaces share the same process — much lower overhead. Ideal for scanning hundreds of servers simultaneously.
+
+### Thread-Safe Collections for Parallel Work
+```powershell
+# Regular arrays/lists are NOT thread-safe — use these in parallel scenarios
+$bag = [System.Collections.Concurrent.ConcurrentBag[PSObject]]::new()
+$queue = [System.Collections.Concurrent.ConcurrentQueue[string]]::new()
+$dict = [System.Collections.Concurrent.ConcurrentDictionary[string,object]]::new()
+
+# Example: thread-safe result collection
+$servers | ForEach-Object -Parallel {
+ $results = $using:bag
+ $results.Add([PSCustomObject]@{ Server = $_; Time = Get-Date })
+} -ThrottleLimit 10
+```
+
+---
+
+## Desired State Configuration (DSC)
+
+### DSC Configuration — Real-World Example
+```powershell
+Configuration WebServerBaseline {
+ param (
+ [string[]]$ComputerName = 'localhost',
+ [PSCredential]$Credential
+ )
+
+ Import-DscResource -ModuleName PSDesiredStateConfiguration
+ Import-DscResource -ModuleName xWebAdministration
+
+ Node $ComputerName {
+
+ # Ensure IIS is installed
+ WindowsFeature IIS {
+ Ensure = 'Present'
+ Name = 'Web-Server'
+ }
+
+ WindowsFeature IISManagement {
+ Ensure = 'Present'
+ Name = 'Web-Mgmt-Tools'
+ DependsOn = '[WindowsFeature]IIS'
+ }
+
+ # Ensure a service is running
+ Service W3SVC {
+ Name = 'W3SVC'
+ State = 'Running'
+ StartupType = 'Automatic'
+ DependsOn = '[WindowsFeature]IIS'
+ }
+
+ # Ensure a file exists with specific content
+ File AppConfig {
+ Ensure = 'Present'
+ DestinationPath = 'C:\inetpub\wwwroot\app.config'
+ Contents = ''
+ Type = 'File'
+ }
+
+ # Registry setting
+ Registry TLS12 {
+ Ensure = 'Present'
+ Key = 'HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.2\Server'
+ ValueName = 'Enabled'
+ ValueData = '1'
+ ValueType = 'Dword'
+ }
+ }
+}
+
+# Compile to MOF
+WebServerBaseline -ComputerName SERVER01, SERVER02 -OutputPath C:\DSC\WebBaseline
+
+# Apply — push mode
+Start-DscConfiguration -Path C:\DSC\WebBaseline -ComputerName SERVER01 -Wait -Verbose -Force
+
+# Test compliance without changing anything
+Test-DscConfiguration -ComputerName SERVER01
+
+# Get current state vs desired state
+Get-DscConfiguration -CimSession (New-CimSession SERVER01)
+Get-DscConfigurationStatus -CimSession (New-CimSession SERVER01)
+```
+
+---
+
+## Just Enough Administration (JEA)
+
+> JEA creates **constrained PowerShell endpoints** that let users run specific commands with elevated privileges — without giving them full admin rights or interactive sessions.
+
+### Role Capability File
+```powershell
+# Create the role capability file template
+New-PSRoleCapabilityFile -Path "C:\JEA\Roles\HelpDesk.psrc"
+```
+
+```powershell
+# HelpDesk.psrc — what this role is allowed to do
+@{
+ # Visible cmdlets — everything else is hidden
+ VisibleCmdlets = @(
+ 'Get-Service',
+ 'Get-Process',
+ 'Get-EventLog',
+ @{ Name = 'Restart-Service'; Parameters = @{ Name = 'Name'; ValidateSet = 'Spooler','W3SVC','WinRM' } },
+ @{ Name = 'Stop-Process'; Parameters = @{ Name = 'Name'; ValidatePattern = '^(notepad|calc)$' } }
+ )
+
+ # Functions you define below that are available
+ VisibleFunctions = @('Get-ServerSummary')
+
+ # External executables permitted
+ VisibleExternalCommands = @('C:\Windows\System32\ipconfig.exe')
+
+ # PowerShell providers visible (restrict to FileSystem only if needed)
+ VisibleProviders = @('FileSystem', 'Registry')
+
+ # Define helper functions inline
+ FunctionDefinitions = @(
+ @{
+ Name = 'Get-ServerSummary'
+ ScriptBlock = {
+ [PSCustomObject]@{
+ Hostname = $env:COMPUTERNAME
+ Uptime = (Get-Date) - (gcim Win32_OperatingSystem).LastBootUpTime
+ TopCPU = Get-Process | Sort-Object CPU -Descending | Select-Object -First 3 Name, CPU
+ }
+ }
+ }
+ )
+}
+```
+
+### Session Configuration File
+```powershell
+# Create the session config
+New-PSSessionConfigurationFile -Path "C:\JEA\HelpDeskSession.pssc" `
+ -SessionType RestrictedRemoteServer `
+ -RunAsVirtualAccount `
+ -RoleDefinitions @{
+ 'DOMAIN\HelpDesk' = @{ RoleCapabilities = 'HelpDesk' }
+ 'DOMAIN\L2Support' = @{ RoleCapabilities = 'HelpDesk', 'NetworkAdmin' }
+ } `
+ -TranscriptDirectory 'C:\JEA\Transcripts' `
+ -LanguageMode ConstrainedLanguage
+
+# Register the endpoint on each managed server
+Register-PSSessionConfiguration -Name 'HelpDeskEndpoint' `
+ -Path 'C:\JEA\HelpDeskSession.pssc' -Force
+
+# Test it — connect as a regular user
+Enter-PSSession -ComputerName SERVER01 -ConfigurationName HelpDeskEndpoint
+
+# Audit what a user can do at an endpoint
+$session = New-PSSession -ComputerName SERVER01 -ConfigurationName HelpDeskEndpoint
+Invoke-Command -Session $session -ScriptBlock { Get-Command }
+```
+
+---
+
+## Secrets & Credential Management
+
+### SecretManagement Module (Modern Standard)
+```powershell
+# Install
+Install-Module Microsoft.PowerShell.SecretManagement
+Install-Module Microsoft.PowerShell.SecretStore # Local encrypted vault
+
+# Register a vault
+Register-SecretVault -Name 'CorpVault' -ModuleName 'Microsoft.PowerShell.SecretStore'
+
+# Store secrets
+Set-Secret -Name 'SQLServiceAccount' -Secret (Get-Credential)
+Set-Secret -Name 'APIToken' -Secret 'abc123supersecret'
+
+# Retrieve in scripts — no plain text, no hard-coding
+$cred = Get-Secret -Name 'SQLServiceAccount'
+$apiToken = Get-Secret -Name 'APIToken' -AsPlainText
+
+# List all secrets (names only — values stay encrypted)
+Get-SecretInfo -Vault 'CorpVault'
+```
+
+### Encrypting Strings for Script Storage
+```powershell
+# Encrypt — only the SAME USER on the SAME MACHINE can decrypt (DPAPI)
+$secureString = Read-Host "Enter secret" -AsSecureString
+$encrypted = ConvertFrom-SecureString $secureString
+$encrypted | Out-File "C:\Scripts\.secret" # Store encrypted string
+
+# Decrypt at runtime
+$encrypted = Get-Content "C:\Scripts\.secret"
+$secure = ConvertTo-SecureString $encrypted
+$credential = New-Object System.Management.Automation.PSCredential("svcAccount", $secure)
+
+# For cross-machine/service-account use — AES key approach
+$key = (1..32 | ForEach-Object { Get-Random -Minimum 0 -Maximum 256 })
+$key | Export-Clixml "C:\Secure\aes.key" # Protect this file with NTFS permissions!
+
+$encrypted = ConvertFrom-SecureString $secureString -Key $key
+$decrypted = ConvertTo-SecureString $encrypted -Key (Import-Clixml "C:\Secure\aes.key")
+```
+> **Never** store plain-text passwords in scripts. Use SecretManagement for modern deployments, or DPAPI/AES encryption as a fallback. Protect AES key files with strict NTFS ACLs.
+
+---
+
+## PowerShell Remoting — Advanced Patterns
+
+### ConstrainedLanguage Remoting & Custom Endpoints
+```powershell
+# Create a custom endpoint with specific module access
+Register-PSSessionConfiguration -Name 'InfraTools' `
+ -StartupScript 'C:\Scripts\LoadInfraModule.ps1' `
+ -RunAsCredential 'DOMAIN\svcInfraTools' `
+ -AccessMode Remote `
+ -Force
+
+# Connect to a named endpoint
+Enter-PSSession -ComputerName SERVER01 -ConfigurationName 'InfraTools'
+```
+
+### SSH Tunnelling for Remoting
+```powershell
+# Forward local port 15985 to remote server's WinRM port via SSH jump host
+# Run in a separate terminal / background job
+ssh -L 15985:TARGET_SERVER:5985 jumphost.corp.com -N
+
+# Now connect through the tunnel
+$session = New-PSSession -ComputerName localhost -Port 15985 -Credential (Get-Credential)
+Invoke-Command -Session $session -ScriptBlock { hostname }
+```
+
+### Fan-Out Pattern — Run Against Many Servers Efficiently
+```powershell
+function Invoke-ParallelRemote {
+ param (
+ [string[]]$ComputerName,
+ [scriptblock]$ScriptBlock,
+ [PSCredential]$Credential,
+ [int]$BatchSize = 50,
+ [int]$ThrottleLimit = 32
+ )
+
+ # Process in batches to avoid overwhelming WinRM
+ $batches = for ($i = 0; $i -lt $ComputerName.Count; $i += $BatchSize) {
+ , ($ComputerName[$i..([Math]::Min($i + $BatchSize - 1, $ComputerName.Count - 1))])
+ }
+
+ foreach ($batch in $batches) {
+ $params = @{
+ ComputerName = $batch
+ ScriptBlock = $ScriptBlock
+ ThrottleLimit = $ThrottleLimit
+ ErrorAction = 'SilentlyContinue'
+ }
+ if ($Credential) { $params['Credential'] = $Credential }
+
+ Invoke-Command @params
+ }
+}
+
+# Usage — hit 500 servers without melting WinRM
+$allServers = Get-ADComputer -Filter {OperatingSystem -like "*Server*"} | Select-Object -Expand Name
+Invoke-ParallelRemote -ComputerName $allServers -ScriptBlock {
+ [PSCustomObject]@{
+ Server = $env:COMPUTERNAME
+ FreeGB = [math]::Round((Get-PSDrive C).Free / 1GB, 1)
+ Uptime = ((Get-Date) - (gcim Win32_OperatingSystem).LastBootUpTime).Days
+ }
+}
+```
+
+---
+
+## Logging, Auditing & Transcription
+
+### Structured Logging Function
+```powershell
+enum LogLevel { DEBUG; INFO; WARN; ERROR; FATAL }
+
+function Write-StructuredLog {
+ [CmdletBinding()]
+ param (
+ [Parameter(Mandatory)][string]$Message,
+ [LogLevel]$Level = [LogLevel]::INFO,
+ [string]$LogFile = "C:\Logs\$($MyInvocation.ScriptName -replace '.*\\').log",
+ [hashtable]$Properties = @{}
+ )
+
+ $entry = [ordered]@{
+ Timestamp = (Get-Date).ToString('yyyy-MM-ddTHH:mm:ss.fffZ')
+ Level = $Level.ToString()
+ Host = $env:COMPUTERNAME
+ User = $env:USERNAME
+ PID = $PID
+ Message = $Message
+ }
+ $Properties.GetEnumerator() | ForEach-Object { $entry[$_.Key] = $_.Value }
+
+ # JSON to log file — machine-parseable by Splunk, ELK, etc.
+ $entry | ConvertTo-Json -Compress | Add-Content $LogFile
+
+ # Also write to console at appropriate stream
+ switch ($Level) {
+ 'DEBUG' { Write-Debug $Message }
+ 'INFO' { Write-Verbose $Message }
+ 'WARN' { Write-Warning $Message }
+ 'ERROR' { Write-Error $Message }
+ 'FATAL' { Write-Error $Message; throw $Message }
+ }
+}
+
+# Usage
+Write-StructuredLog "Starting deployment" -Level INFO -Properties @{ Server = "WEB01"; Version = "2.1" }
+Write-StructuredLog "Disk space low" -Level WARN -Properties @{ FreeGB = 2.1; Threshold = 5 }
+```
+
+### Enable PowerShell Script Block Logging (Group Policy / Registry)
+```powershell
+# Enable on a target server — logs ALL executed script blocks to Event Log
+Invoke-Command -ComputerName SERVER01 -ScriptBlock {
+ $path = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging'
+ New-Item $path -Force | Out-Null
+ Set-ItemProperty $path -Name EnableScriptBlockLogging -Value 1
+ Set-ItemProperty $path -Name EnableScriptBlockInvocationLogging -Value 1
+}
+
+# Read script block logs (Event ID 4104)
+Get-WinEvent -ComputerName SERVER01 -FilterHashtable @{
+ LogName = 'Microsoft-Windows-PowerShell/Operational'
+ Id = 4104
+} | Select-Object TimeCreated, @{N="Script";E={$_.Properties[2].Value}} | Select-Object -First 20
+```
+
+### Transcript Logging
+```powershell
+# Start transcript — logs all input and output
+Start-Transcript -Path "C:\Logs\session-$(Get-Date -Format yyyyMMdd-HHmmss).log" -Append
+
+# Configure automatic transcription via Group Policy or registry
+$path = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\Transcription'
+New-Item $path -Force | Out-Null
+Set-ItemProperty $path -Name EnableTranscripting -Value 1
+Set-ItemProperty $path -Name EnableInvocationHeader -Value 1
+Set-ItemProperty $path -Name OutputDirectory -Value '\\LOGSERVER\PSTranscripts$'
+```
+
+---
+
+## Error Handling — Production Patterns
+
+### Hierarchical Error Handling
+```powershell
+function Invoke-SafeOperation {
+ [CmdletBinding()]
+ param([string]$ComputerName, [scriptblock]$Operation)
+
+ $ErrorActionPreference = 'Stop' # Make ALL errors terminating within function
+
+ try {
+ Invoke-Command -ComputerName $ComputerName -ScriptBlock $Operation
+ }
+ catch [System.Management.Automation.Remoting.PSRemotingTransportException] {
+ # Specific catch for WinRM/connectivity failures
+ Write-StructuredLog "Cannot reach $ComputerName — WinRM transport failure" -Level WARN
+ Write-Error "Transport failure: $($_.Exception.Message)" -ErrorAction Continue
+ }
+ catch [System.UnauthorizedAccessException] {
+ Write-StructuredLog "Access denied to $ComputerName" -Level ERROR
+ throw # Re-throw — caller needs to know
+ }
+ catch {
+ # Capture full error context for diagnostics
+ $errorDetail = [PSCustomObject]@{
+ Message = $_.Exception.Message
+ Type = $_.Exception.GetType().FullName
+ ScriptLine = $_.InvocationInfo.ScriptLineNumber
+ Command = $_.InvocationInfo.MyCommand
+ StackTrace = $_.ScriptStackTrace
+ }
+ Write-StructuredLog "Unhandled error on $ComputerName" -Level ERROR -Properties @{
+ ErrorType = $errorDetail.Type
+ ErrorMsg = $errorDetail.Message
+ }
+ throw
+ }
+ finally {
+ # Always runs — cleanup, close connections, release locks
+ Write-Verbose "Operation on $ComputerName completed (success or fail)"
+ }
+}
+```
+
+### $ErrorActionPreference vs -ErrorAction
+```powershell
+# $ErrorActionPreference = global default for the scope
+$ErrorActionPreference = 'Stop' # All errors terminating in this scope
+
+# -ErrorAction = per-cmdlet override
+Get-Item "badpath" -ErrorAction SilentlyContinue # Suppress THIS error only
+
+# -ErrorVariable = capture errors without stopping
+Get-Item "badpath" -ErrorAction SilentlyContinue -ErrorVariable capturedErr
+if ($capturedErr) { Write-Warning "Item not found: $capturedErr" }
+
+# $? = boolean success of last command
+Get-Service "FakeService" -ErrorAction SilentlyContinue
+if (-not $?) { Write-Warning "Last command failed" }
+```
+
+---
+
+## Regular Expressions in PowerShell
+
+```powershell
+# -match operator — sets $Matches automatic variable
+"Server: WEB01 IP: 10.0.0.50" -match 'IP:\s+(\d{1,3}(?:\.\d{1,3}){3})'
+$Matches[0] # Full match: "IP: 10.0.0.50"
+$Matches[1] # Capture group 1: "10.0.0.50"
+
+# Named capture groups — much cleaner than numbered groups
+"EventID: 4625 Source: Security" -match 'EventID:\s+(?\d+)\s+Source:\s+(?\w+)'
+$Matches['id'] # "4625"
+$Matches['source'] # "Security"
+
+# -replace with regex
+"Server_PROD_WEB01" -replace '_(?PROD|DEV|UAT)_', ' [$env:env] '
+
+# Select-String — regex on file content (like grep)
+Select-String -Path "C:\Logs\*.log" -Pattern "ERROR|FATAL" -CaseSensitive
+Select-String -Path "C:\Logs\app.log" -Pattern '(?\d{4}-\d{2}-\d{2}).*ERROR' |
+ ForEach-Object { $_.Matches[0].Groups['date'].Value }
+
+# [regex] class — when you need full regex engine control
+$regex = [regex]::new('(?\d{1,3}(?:\.\d{1,3}){3})', 'IgnoreCase,Compiled')
+$content = Get-Content "C:\Logs\firewall.log" -Raw
+$regex.Matches($content) | ForEach-Object { $_.Groups['ip'].Value } | Sort-Object -Unique
+
+# Replace with a MatchEvaluator (transform each match)
+$result = [regex]::Replace("server01,server02,server03", '\b\w+\b', {
+ param($match)
+ $match.Value.ToUpper()
+})
+# Result: "SERVER01,SERVER02,SERVER03"
+```
+
+---
+
+## Working with APIs & REST
+
+### Invoke-RestMethod — Full Pattern
+```powershell
+# Build reusable API client function
+function Invoke-CorpAPI {
+ [CmdletBinding()]
+ param (
+ [string]$Endpoint,
+ [string]$Method = 'GET',
+ [hashtable]$Body = @{},
+ [string]$BaseUri = 'https://api.corp.com/v2'
+ )
+
+ # Retrieve token from SecretManagement
+ $token = Get-Secret -Name 'CorpAPIToken' -AsPlainText
+
+ $params = @{
+ Uri = "$BaseUri/$Endpoint"
+ Method = $Method
+ Headers = @{
+ 'Authorization' = "Bearer $token"
+ 'Content-Type' = 'application/json'
+ 'X-Request-ID' = [System.Guid]::NewGuid().ToString()
+ }
+ UseBasicParsing = $true
+ ErrorAction = 'Stop'
+ }
+
+ if ($Method -in 'POST','PUT','PATCH') {
+ $params['Body'] = ($Body | ConvertTo-Json -Depth 10)
+ }
+
+ try {
+ $response = Invoke-RestMethod @params
+ return $response
+ }
+ catch {
+ $statusCode = $_.Exception.Response.StatusCode.Value__
+ $detail = $_.ErrorDetails.Message | ConvertFrom-Json -ErrorAction SilentlyContinue
+ throw "API error $statusCode on $Endpoint`: $($detail.message ?? $_.Exception.Message)"
+ }
+}
+
+# Pagination pattern
+function Get-AllPages {
+ param([string]$Endpoint)
+ $page = 1
+ $results = [System.Collections.Generic.List[PSObject]]::new()
+ do {
+ $response = Invoke-CorpAPI -Endpoint "$Endpoint`?page=$page&per_page=100"
+ $results.AddRange($response.items)
+ $page++
+ } while ($response.has_more)
+ return $results
+}
+```
+
+### Working with Graph API (Microsoft 365 / Azure AD)
+```powershell
+# Authenticate with client credentials (app registration)
+function Get-GraphToken {
+ param($TenantId, $ClientId, $ClientSecret)
+
+ $body = @{
+ grant_type = 'client_credentials'
+ client_id = $ClientId
+ client_secret = $ClientSecret
+ scope = 'https://graph.microsoft.com/.default'
+ }
+ $response = Invoke-RestMethod "https://login.microsoftonline.com/$TenantId/oauth2/v2.0/token" `
+ -Method POST -Body $body
+ return $response.access_token
+}
+
+$token = Get-GraphToken -TenantId $tid -ClientId $cid -ClientSecret $secret
+$headers = @{ Authorization = "Bearer $token" }
+
+# Get all users with specific properties
+$users = Invoke-RestMethod `
+ "https://graph.microsoft.com/v1.0/users?`$select=displayName,mail,jobTitle&`$top=999" `
+ -Headers $headers
+
+# Handle nextLink pagination
+$allUsers = [System.Collections.Generic.List[PSObject]]::new()
+$uri = "https://graph.microsoft.com/v1.0/users?`$top=999"
+do {
+ $page = Invoke-RestMethod $uri -Headers $headers
+ $allUsers.AddRange($page.value)
+ $uri = $page.'@odata.nextLink'
+} while ($uri)
+```
+
+---
+
+## Working with Databases
+
+### SQL Server — System.Data.SqlClient
+```powershell
+function Invoke-SqlQuery {
+ param (
+ [string]$ServerInstance,
+ [string]$Database,
+ [string]$Query,
+ [hashtable]$Parameters = @{},
+ [PSCredential]$Credential
+ )
+
+ $connString = if ($Credential) {
+ "Server=$ServerInstance;Database=$Database;User Id=$($Credential.UserName);Password=$($Credential.GetNetworkCredential().Password);"
+ } else {
+ "Server=$ServerInstance;Database=$Database;Integrated Security=SSPI;TrustServerCertificate=True;"
+ }
+
+ $conn = [System.Data.SqlClient.SqlConnection]::new($connString)
+ $conn.Open()
+
+ try {
+ $cmd = $conn.CreateCommand()
+ $cmd.CommandText = $Query
+ $cmd.CommandTimeout = 60
+
+ # Parameterised query — prevents SQL injection
+ foreach ($param in $Parameters.GetEnumerator()) {
+ $cmd.Parameters.AddWithValue("@$($param.Key)", $param.Value) | Out-Null
+ }
+
+ $adapter = [System.Data.SqlClient.SqlDataAdapter]::new($cmd)
+ $dataset = [System.Data.DataSet]::new()
+ $adapter.Fill($dataset) | Out-Null
+ return $dataset.Tables[0]
+ }
+ finally {
+ $conn.Close()
+ $conn.Dispose()
+ }
+}
+
+# Usage
+$results = Invoke-SqlQuery -ServerInstance "SQL01\PROD" -Database "CMDB" `
+ -Query "SELECT ServerName, Environment, Owner FROM Servers WHERE Environment = @env" `
+ -Parameters @{ env = 'Production' }
+
+$results | Format-Table
+```
+
+---
+
+## XML, JSON & YAML Manipulation
+
+### XML — Full Manipulation
+```powershell
+# Load XML
+[xml]$config = Get-Content "C:\Config\app.config"
+
+# Navigate with dot notation
+$config.configuration.appSettings.add | Where-Object { $_.key -eq "DBServer" }
+
+# XPath queries — much more powerful for complex queries
+$nodes = $config.SelectNodes("//appSettings/add[@key='DBServer']")
+
+# Modify and save
+$node = $config.SelectSingleNode("//appSettings/add[@key='DBServer']")
+$node.SetAttribute("value", "NEWSQL01")
+$config.Save("C:\Config\app.config")
+
+# Create XML from scratch
+$xml = [System.Xml.XmlDocument]::new()
+$root = $xml.CreateElement("Servers")
+$xml.AppendChild($root) | Out-Null
+
+$serverNode = $xml.CreateElement("Server")
+$serverNode.SetAttribute("name", "WEB01")
+$serverNode.SetAttribute("role", "Web")
+$root.AppendChild($serverNode) | Out-Null
+
+$xml.Save("C:\Config\servers.xml")
+```
+
+### JSON — Deep Manipulation
+```powershell
+# Parse JSON with depth (default depth is only 2 — a common gotcha)
+$data = Get-Content "config.json" -Raw | ConvertFrom-Json -Depth 20
+
+# Modify nested properties
+$data.servers | Where-Object { $_.env -eq 'prod' } | ForEach-Object {
+ $_.maxConnections = 500
+}
+
+# Convert back — always set -Depth to avoid truncation
+$data | ConvertTo-Json -Depth 20 | Set-Content "config.json"
+
+# Compare two JSON configs
+$a = Get-Content "config_old.json" -Raw | ConvertFrom-Json
+$b = Get-Content "config_new.json" -Raw | ConvertFrom-Json
+Compare-Object ($a | ConvertTo-Json -Depth 10 -Compress) ($b | ConvertTo-Json -Depth 10 -Compress)
+```
+
+---
+
+## Performance Optimisation
+
+### Measure and Profile
+```powershell
+# Basic timing
+$time = Measure-Command { Get-ADUser -Filter * }
+Write-Host "Took: $($time.TotalSeconds)s"
+
+# Compare approaches
+$methods = @{
+ 'Pipeline filter' = { Get-Process | Where-Object { $_.CPU -gt 10 } }
+ 'Direct filter' = { Get-Process | Where-Object CPU -gt 10 }
+ 'Method filter' = { (Get-Process).Where({ $_.CPU -gt 10 }) }
+}
+$methods.GetEnumerator() | ForEach-Object {
+ $t = Measure-Command $_.Value
+ [PSCustomObject]@{ Method = $_.Key; Ms = [math]::Round($t.TotalMilliseconds, 2) }
+} | Sort-Object Ms
+```
+
+### Key Optimisation Patterns
+```powershell
+# 1. Use .Where() and .ForEach() instead of pipeline for in-memory collections
+$large = 1..100000
+# Slow — pipeline overhead per element
+$large | Where-Object { $_ % 2 -eq 0 } | ForEach-Object { $_ * 2 }
+# Fast — method-based, no pipeline overhead
+$large.Where({ $_ % 2 -eq 0 }).ForEach({ $_ * 2 })
+
+# 2. StringBuilder for string concatenation in loops
+$sb = [System.Text.StringBuilder]::new()
+1..10000 | ForEach-Object { $sb.AppendLine("Line $_") | Out-Null }
+$result = $sb.ToString()
+# Never use $str += "..." in a loop — creates a new string object every iteration
+
+# 3. Generic List instead of array for growing collections
+# Slow — array re-allocation on every +=
+$arr = @(); 1..10000 | ForEach-Object { $arr += $_ }
+# Fast — List amortised O(1) append
+$list = [System.Collections.Generic.List[int]]::new()
+1..10000 | ForEach-Object { $list.Add($_) }
+
+# 4. Filter server-side, not client-side
+# Slow — gets ALL users, filters locally
+Get-ADUser -Filter * | Where-Object { $_.Department -eq "IT" }
+# Fast — filter at the directory level
+Get-ADUser -Filter { Department -eq "IT" }
+Get-ADUser -LDAPFilter "(&(objectClass=user)(department=IT))"
+
+# 5. Select-Object -First/Last stops the pipeline early
+Get-ChildItem C:\Windows -Recurse | Select-Object -First 1 # Stops after finding first match
+
+# 6. Use $null = or [void] to suppress pipeline output (faster than | Out-Null)
+$null = $list.Add(42) # Fast
+[void]$list.Add(42) # Also fast
+$list.Add(42) | Out-Null # Slower — creates pipeline
+```
+
+---
+
+## Security & Constrained Language Mode
+
+### Check and Set Language Mode
+```powershell
+$ExecutionContext.SessionState.LanguageMode # FullLanguage | ConstrainedLanguage | RestrictedLanguage | NoLanguage
+
+# What's blocked in ConstrainedLanguage mode:
+# - Add-Type
+# - [System.Type]::GetType()
+# - New-Object for most .NET types
+# - Direct .NET method calls on many types
+# - Script block literals assigned to variables in certain ways
+
+# Test if you're in constrained mode
+if ($ExecutionContext.SessionState.LanguageMode -ne 'FullLanguage') {
+ Write-Warning "Running in $($ExecutionContext.SessionState.LanguageMode) — some operations unavailable"
+}
+```
+
+### AMSI (Antimalware Scan Interface) Awareness
+```powershell
+# AMSI scans PowerShell script blocks before execution in PS 5.1+
+# Your scripts must not trigger AV signatures — use clear, well-documented code in production
+# Check if AMSI is active:
+[Ref].Assembly.GetType('System.Management.Automation.AmsiUtils') | Out-Null
+# If no error, AMSI is present
+
+# Corporate environments often have AMSI providers — test your automation scripts
+# against corporate AV in dev/staging before prod deployment
+```
+
+### Code Signing
+```powershell
+# Sign a script with a code signing certificate
+$cert = Get-ChildItem Cert:\CurrentUser\My -CodeSigningCert | Select-Object -First 1
+Set-AuthenticodeSignature -FilePath "C:\Scripts\Deploy.ps1" -Certificate $cert
+
+# Verify signature
+Get-AuthenticodeSignature "C:\Scripts\Deploy.ps1"
+
+# Set execution policy to only allow signed scripts
+Set-ExecutionPolicy AllSigned -Scope LocalMachine
+
+# Verify all scripts in a directory are signed
+Get-ChildItem "C:\Scripts\*.ps1" | Get-AuthenticodeSignature |
+ Where-Object { $_.Status -ne 'Valid' } |
+ Select-Object Path, Status
+```
+
+---
+
+## Working with .NET Directly
+
+```powershell
+# Reflection — inspect .NET types at runtime
+[System.AppDomain]::CurrentDomain.GetAssemblies() |
+ Where-Object { $_.FullName -like "*Automation*" }
+
+# Load a .NET assembly
+Add-Type -Path "C:\Libs\CustomLib.dll"
+Add-Type -AssemblyName "System.Net.Http"
+
+# Create .NET objects
+$httpClient = [System.Net.Http.HttpClient]::new()
+$uri = [System.Uri]::new("https://api.example.com")
+
+# Compile C# inline — extend PowerShell with custom .NET code
+Add-Type -TypeDefinition @"
+using System;
+using System.Runtime.InteropServices;
+
+public class NativeMethods {
+ [DllImport("kernel32.dll", SetLastError = true)]
+ public static extern bool SetConsoleTitle(string lpConsoleTitle);
+
+ [DllImport("advapi32.dll", SetLastError = true)]
+ public static extern bool OpenProcessToken(IntPtr ProcessHandle, uint DesiredAccess, out IntPtr TokenHandle);
+}
+"@ -Language CSharp
+
+[NativeMethods]::SetConsoleTitle("My Admin Console")
+
+# Generic collections with type safety
+$serverDict = [System.Collections.Generic.Dictionary[string, PSObject]]::new()
+$serverDict['WEB01'] = [PSCustomObject]@{ CPU = 42; Mem = 8 }
+
+# Use LINQ via reflection (PS 7 makes this cleaner)
+$list = [System.Collections.Generic.List[int]]@(5, 2, 8, 1, 9, 3)
+[System.Linq.Enumerable]::OrderBy($list, [Func[int,int]]{ $args[0] })
+```
+
+---
+
+## PowerShell Providers & PSDrives
+
+```powershell
+# List all providers
+Get-PSProvider
+
+# Create custom PSDrive for any provider
+New-PSDrive -Name HKCU_Run -PSProvider Registry -Root HKCU:\Software\Microsoft\Windows\CurrentVersion\Run
+Get-ChildItem HKCU_Run:
+
+New-PSDrive -Name Logs -PSProvider FileSystem -Root \\LOGSERVER\Logs$ -Credential (Get-Credential)
+Get-ChildItem Logs:\
+
+# Write a custom provider (skeleton — implement System.Management.Automation.Provider)
+# Useful for exposing non-filesystem data as a navigable hierarchy (config APIs, CMDB, etc.)
+# Full implementation requires inheriting from NavigationCmdletProvider or ContainerCmdletProvider
+```
+
+---
+
+## Enterprise Automation Patterns
+
+### Idempotent Configuration Function
+```powershell
+# Always safe to run multiple times — checks before acting
+function Set-ServerBaseline {
+ [CmdletBinding(SupportsShouldProcess)]
+ param([string]$ComputerName)
+
+ Invoke-Command -ComputerName $ComputerName -ScriptBlock {
+ # Pattern: Test → Report → Change (only if needed)
+ $changes = [System.Collections.Generic.List[string]]::new()
+
+ # TLS 1.0 disabled?
+ $tls10 = Get-ItemProperty "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.0\Server" `
+ -Name Enabled -ErrorAction SilentlyContinue
+ if ($tls10.Enabled -ne 0) {
+ Set-ItemProperty "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.0\Server" `
+ -Name Enabled -Value 0 -Type DWORD -Force
+ $changes.Add("Disabled TLS 1.0")
+ }
+
+ # SMBv1 disabled?
+ $smb1 = Get-SmbServerConfiguration | Select-Object -Expand EnableSMB1Protocol
+ if ($smb1) {
+ Set-SmbServerConfiguration -EnableSMB1Protocol $false -Force
+ $changes.Add("Disabled SMBv1")
+ }
+
+ [PSCustomObject]@{
+ Server = $env:COMPUTERNAME
+ Changes = if ($changes.Count) { $changes -join '; ' } else { 'Already compliant' }
+ }
+ }
+}
+```
+
+### Configuration Data Pattern (Separation of Code and Data)
+```powershell
+# config\servers.psd1 — data only, no logic
+@{
+ Production = @{
+ WebServers = @("WEB01","WEB02","WEB03")
+ DBServers = @("SQL01","SQL02")
+ MaxThreads = 32
+ LogLevel = "WARN"
+ }
+ Staging = @{
+ WebServers = @("WEB-STG01")
+ DBServers = @("SQL-STG01")
+ MaxThreads = 8
+ LogLevel = "DEBUG"
+ }
+}
+
+# automation\deploy.ps1 — logic only, reads data
+$config = Import-PowerShellDataFile "C:\Config\servers.psd1"
+$env = $config[$TargetEnvironment]
+
+Invoke-ParallelRemote -ComputerName $env.WebServers -ScriptBlock {
+ # ... deployment logic ...
+} -ThrottleLimit $env.MaxThreads
+```
+
+### Retry with Exponential Backoff
+```powershell
+function Invoke-WithRetry {
+ param (
+ [scriptblock]$ScriptBlock,
+ [int]$MaxRetries = 5,
+ [int]$InitialDelayMs = 500,
+ [double]$Backoff = 2.0,
+ [type[]]$RetryOn = @([System.Exception])
+ )
+
+ $attempt = 0
+ $delay = $InitialDelayMs
+
+ while ($true) {
+ try {
+ return & $ScriptBlock
+ }
+ catch {
+ $attempt++
+ $shouldRetry = $RetryOn | Where-Object { $_.IsAssignableFrom($_.Exception.GetType()) }
+
+ if ($attempt -ge $MaxRetries -or -not $shouldRetry) {
+ Write-Error "Failed after $attempt attempt(s): $_"
+ throw
+ }
+
+ Write-Warning "Attempt $attempt failed. Retrying in $($delay)ms... ($_)"
+ Start-Sleep -Milliseconds $delay
+ $delay = [int]($delay * $Backoff)
+ }
+ }
+}
+
+# Usage
+Invoke-WithRetry -MaxRetries 5 -ScriptBlock {
+ Invoke-RestMethod "https://flaky-api.corp.com/data"
+}
+```
+
+---
+
+## Debugging & Diagnostics
+
+### PowerShell Debugger
+```powershell
+# Set a breakpoint on a line
+Set-PSBreakpoint -Script "C:\Scripts\Deploy.ps1" -Line 42
+
+# Set a breakpoint on a variable change
+Set-PSBreakpoint -Variable "targetServer" -Mode ReadWrite
+
+# Set a breakpoint on a command
+Set-PSBreakpoint -Command "Invoke-RestMethod"
+
+# Conditional breakpoint
+Set-PSBreakpoint -Script "C:\Scripts\Deploy.ps1" -Line 42 -Action {
+ if ($server -eq "PROD01") { break }
+}
+
+# List and manage breakpoints
+Get-PSBreakpoint
+Disable-PSBreakpoint -Id 1
+Remove-PSBreakpoint -Id 1
+
+# Trace execution — shows every line as it runs
+Set-PSDebug -Trace 2 # 2 = trace + variable assignments
+Get-Service
+Set-PSDebug -Off
+
+# Trace specific script block
+Trace-Command -Name ParameterBinding -Expression { Get-Service -Name "W3SVC" } -PSHost
+```
+
+### Verbose & Debug Stream Control
+```powershell
+# Write to different streams
+Write-Verbose "Detailed progress info" # Stream 4 — shown with -Verbose
+Write-Debug "Debug detail" # Stream 5 — shown with -Debug
+Write-Information "Structured info" -Tags "Audit" # Stream 6
+
+# Redirect streams to file for investigation
+.\script.ps1 -Verbose 4>&1 | Tee-Object -FilePath "C:\Logs\verbose.log"
+
+# Capture all streams
+.\script.ps1 *>&1 | Out-File "C:\Logs\all-streams.log"
+
+# Redirect error stream to success stream (lets you capture errors in a variable)
+$output = .\script.ps1 2>&1
+$errors = $output | Where-Object { $_ -is [System.Management.Automation.ErrorRecord] }
+```
+
+---
+
+## CI/CD Integration for PowerShell
+
+### Pester — Unit Testing
+```powershell
+# Get-ServerHealth.Tests.ps1
+BeforeAll {
+ # Load the module under test
+ Import-Module "$PSScriptRoot\..\MyModule.psd1" -Force
+}
+
+Describe "Get-ServerHealth" {
+ Context "When server is reachable" {
+ BeforeAll {
+ # Mock external dependencies — don't hit real infrastructure in tests
+ Mock Test-Connection { return $true }
+ Mock Get-CimInstance {
+ return [PSCustomObject]@{
+ FreePhysicalMemory = 4GB / 1KB
+ LastBootUpTime = (Get-Date).AddDays(-5)
+ }
+ }
+ }
+
+ It "Should return a result object" {
+ $result = Get-ServerHealth -ComputerName "FAKESERVER"
+ $result | Should -Not -BeNullOrEmpty
+ }
+
+ It "Should report server as online" {
+ $result = Get-ServerHealth -ComputerName "FAKESERVER"
+ $result.Online | Should -BeTrue
+ }
+
+ It "Should calculate uptime in days" {
+ $result = Get-ServerHealth -ComputerName "FAKESERVER"
+ $result.UptimeDays | Should -BeGreaterThan 0
+ }
+ }
+
+ Context "When server is unreachable" {
+ BeforeAll { Mock Test-Connection { return $false } }
+
+ It "Should return Online as false" {
+ $result = Get-ServerHealth -ComputerName "DEADSERVER"
+ $result.Online | Should -BeFalse
+ }
+ }
+}
+
+# Run tests with coverage report
+$config = New-PesterConfiguration
+$config.Run.Path = ".\Tests"
+$config.CodeCoverage.Enabled = $true
+$config.CodeCoverage.Path = ".\Public\*.ps1"
+$config.Output.Verbosity = "Detailed"
+Invoke-Pester -Configuration $config
+```
+
+### Azure DevOps / GitHub Actions Pipeline Task
+```yaml
+# azure-pipelines.yml snippet for PowerShell module CI
+- task: PowerShell@2
+ displayName: 'Run PSScriptAnalyzer'
+ inputs:
+ targetType: inline
+ script: |
+ Install-Module PSScriptAnalyzer -Force -Scope CurrentUser
+ $results = Invoke-ScriptAnalyzer -Path ./MyModule -Recurse -Severity Error,Warning
+ if ($results) {
+ $results | Format-Table -AutoSize
+ throw "PSScriptAnalyzer found $($results.Count) issues"
+ }
+
+- task: PowerShell@2
+ displayName: 'Run Pester Tests'
+ inputs:
+ targetType: inline
+ script: |
+ Install-Module Pester -MinimumVersion 5.0 -Force -Scope CurrentUser
+ $config = New-PesterConfiguration
+ $config.Run.Path = './Tests'
+ $config.TestResult.Enabled = $true
+ $config.TestResult.OutputPath = '$(Agent.TempDirectory)/pester.xml'
+ $result = Invoke-Pester -Configuration $config -PassThru
+ if ($result.FailedCount -gt 0) { throw "$($result.FailedCount) tests failed" }
+
+- task: PublishTestResults@2
+ inputs:
+ testResultsFormat: NUnit
+ testResultsFiles: '$(Agent.TempDirectory)/pester.xml'
+```
+
+### PSScriptAnalyzer — Lint & Best Practice Enforcement
+```powershell
+Install-Module PSScriptAnalyzer -Scope CurrentUser
+
+# Analyse a script or module
+Invoke-ScriptAnalyzer -Path "C:\Scripts\Deploy.ps1"
+Invoke-ScriptAnalyzer -Path "C:\Modules\MyModule" -Recurse
+
+# Custom rules configuration — PSScriptAnalyzerSettings.psd1
+@{
+ Severity = @('Error', 'Warning')
+ ExcludeRules = @('PSAvoidUsingWriteHost') # Suppress specific rules
+ IncludeRules = @(
+ 'PSAvoidUsingPlainTextForPassword',
+ 'PSUseShouldProcessForStateChangingFunctions',
+ 'PSUseApprovedVerbs',
+ 'PSAvoidGlobalVars'
+ )
+}
+
+# Apply custom settings
+Invoke-ScriptAnalyzer -Path . -Recurse -Settings "C:\Config\PSScriptAnalyzerSettings.psd1"
+
+# Auto-fix certain violations
+Invoke-ScriptAnalyzer -Path ".\script.ps1" -Fix
+```
+
+---
+
+## Quick Reference — Advanced Operators & Syntax
+
+```powershell
+# Null coalescing (PS 7+)
+$value = $data.Setting ?? "default"
+
+# Null conditional member access (PS 7+)
+$len = $str?.Length # Returns null if $str is null, no error
+
+# Ternary operator (PS 7+)
+$label = $isProduction ? "PROD" : "NON-PROD"
+
+# Pipeline chain operators (PS 7+)
+Start-Service W3SVC && Write-Host "Started" || Write-Host "Failed"
+
+# ForEach-Object -Parallel with timeout
+$servers | ForEach-Object -Parallel { ... } -ThrottleLimit 10 -TimeoutSeconds 60
+
+# Splatting — cleaner than long lines
+$params = @{
+ ComputerName = "SERVER01"
+ ScriptBlock = { hostname }
+ Credential = $cred
+ ErrorAction = 'Stop'
+}
+Invoke-Command @params
+
+# Multiple assignment
+$a, $b, $c = 1, 2, 3
+$first, $rest = 1, 2, 3, 4, 5 # $first = 1; $rest = 2,3,4,5
+
+# Hash table merging (PS 7+)
+$base = @{ A = 1; B = 2 }
+$overlay = @{ B = 99; C = 3 }
+$merged = $base + $overlay # { A=1; B=99; C=3 } — overlay wins
+
+# Script block as first-class value
+$validators = @{
+ IsPositive = { param($n) $n -gt 0 }
+ IsServer = { param($s) $s -match '^SERVER\d+$' }
+}
+& $validators['IsServer'] "SERVER01" # True
+```
+
+---
+
+*Last updated: 2026 | PowerShell 5.1 / 7.x — notes indicate version-specific features*