Public/Test-METPrerequisites.ps1
|
function Test-METPrerequisites { <# .SYNOPSIS Checks the local PowerShell version and required/optional modules before running MET. .DESCRIPTION Verifies the PowerShell version (7.4+) and each dependency MET needs: the required ExchangeOnlineManagement module at its minimum version, and the optional Microsoft.Graph.Identity.SignIns, Microsoft.Graph.Groups and MicrosoftTeams modules - a missing or outdated optional module is reported but does not fail the check, since Connect-METSession already degrades gracefully when Graph or Teams are unavailable. On non-Windows platforms it also reports whether a DNS fallback tool (dig or nslookup) is available for the DMARC/SPF checks, which otherwise rely on Resolve-DnsName. Results are always printed to the console as a coloured table. Nothing is returned by default; -PassThru additionally returns the check objects, and -Quiet suppresses the console table and returns a single boolean instead, for use in an `if` condition or a CI gate that only needs a yes/no answer. .PARAMETER PassThru Also returns the underlying per-component check objects (Component, Required, Installed, Optional, Status, Notes) to the pipeline, in addition to the console table. .PARAMETER Quiet Suppresses the console table and returns $true or $false instead, answering whether all required (non-optional) prerequisites are met. Without this switch, the function's return value is not itself a reliable yes/no answer - a non-empty array is always truthy in PowerShell, so `if (Test-METPrerequisites)` would evaluate to $true even when a required prerequisite is failing. .OUTPUTS None by default (console output only). PSCustomObject[] with -PassThru. System.Boolean with -Quiet. .EXAMPLE Test-METPrerequisites Prints a coloured table of every required and optional dependency, with install commands for anything missing or below its minimum version. .EXAMPLE if (-not (Test-METPrerequisites -Quiet)) { throw 'MET prerequisites are not met - run Test-METPrerequisites for details.' } The CI-gate pattern: fail fast, with no console table, before attempting to connect. .EXAMPLE $checks = Test-METPrerequisites -PassThru $checks | Where-Object Status -like 'Fail*' Captures the per-component results as objects (in addition to the printed table) so the failing ones can be filtered and inspected programmatically. #> [CmdletBinding(PositionalBinding = $false)] param( [Parameter()] [switch] $PassThru, # A Test- verb should answer a yes/no question. Without this, # if (Test-METPrerequisites) is always true - a non-empty array is truthy. [Parameter()] [switch] $Quiet ) $checks = [System.Collections.Generic.List[PSCustomObject]]::new() # ── PowerShell version ─────────────────────────────────────────────────── $psVer = $PSVersionTable.PSVersion $psPass = $psVer -ge [version]'7.4' $checks.Add([PSCustomObject]@{ Component = 'PowerShell' Required = '7.4+' Installed = $psVer.ToString() Optional = $false Status = if ($psPass) { 'OK' } else { 'Fail' } Notes = if ($psPass) { '' } else { 'Download from https://aka.ms/powershell' } }) # ── Modules ────────────────────────────────────────────────────────────── $moduleChecks = @( [PSCustomObject]@{ Name = 'ExchangeOnlineManagement'; Min = '3.7.2'; Optional = $false } # Graph is optional: Connect-METSession treats a missing module or a # failed connection as non-fatal, and Expand-METGroupMembership falls # back to Exchange Online cmdlets. [PSCustomObject]@{ Name = 'Microsoft.Graph.Identity.SignIns'; Min = '2.0.0'; Optional = $true } [PSCustomObject]@{ Name = 'Microsoft.Graph.Groups'; Min = '2.0.0'; Optional = $true } [PSCustomObject]@{ Name = 'MicrosoftTeams'; Min = '6.0.0'; Optional = $true } ) foreach ($m in $moduleChecks) { $found = Get-Module -ListAvailable -Name $m.Name | Sort-Object Version -Descending | Select-Object -First 1 $versionOk = $found -and ($found.Version -ge [version]$m.Min) $status = if ($versionOk) { 'OK' } elseif ($m.Optional -and -not $found) { 'Not installed (optional)' } elseif ($m.Optional -and $found) { 'Upgrade needed (optional)' } elseif (-not $found) { 'Fail - not installed' } else { "Fail - installed $($found.Version), need $($m.Min)+" } $notes = if (-not $versionOk -and -not $m.Optional) { "Install-Module '$($m.Name)' -MinimumVersion '$($m.Min)' -Scope CurrentUser" } elseif (-not $versionOk -and $m.Optional) { $reason = switch -Wildcard ($m.Name) { 'Microsoft.Graph.*' { 'group expansion falls back to Exchange Online cmdlets without it' } 'MicrosoftTeams' { 'Teams checks only' } default { 'optional' } } "Install-Module '$($m.Name)' -MinimumVersion '$($m.Min)' -Scope CurrentUser ($reason)" } else { '' } $checks.Add([PSCustomObject]@{ Component = $m.Name Required = "$($m.Min)+" Installed = if ($found) { $found.Version.ToString() } else { '-' } Optional = $m.Optional Status = $status Notes = $notes }) } # ── Platform note ──────────────────────────────────────────────────────── if ($IsWindows -eq $false) { $digCommand = Get-Command dig -CommandType Application -ErrorAction SilentlyContinue $nslookupCommand = Get-Command nslookup -CommandType Application -ErrorAction SilentlyContinue $checks.Add([PSCustomObject]@{ Component = 'Platform (DNS)' Required = 'dig, nslookup, or DNS-over-HTTPS fallback' Installed = if ($digCommand) { 'dig found' } elseif ($nslookupCommand) { 'nslookup found' } else { 'DNS-over-HTTPS fallback' } Optional = $false Status = 'OK' Notes = 'DMARC (EXO001) and SPF (EXO003) use dig or nslookup when available, and otherwise fall back to DNS-over-HTTPS on non-Windows.' }) } # ── Display ────────────────────────────────────────────────────────────── Write-Host '' Write-Host ' MET Prerequisite Check' -ForegroundColor Cyan Write-Host ' ─────────────────────────────────────────────────────' -ForegroundColor Cyan $labelWidth = (@($checks | ForEach-Object { "$($_.Component)$(if ($_.Optional) { ' [optional]' })".Length }) | Measure-Object -Maximum).Maximum + 2 foreach ($c in $checks) { $color = switch -Wildcard ($c.Status) { 'OK' { 'Green' } 'Fail*' { 'Red' } default { 'Yellow'} } $tag = if ($c.Optional) { ' [optional]' } else { '' } Write-Host (" {0,-$labelWidth} {1}" -f "$($c.Component)$tag", $c.Status) -ForegroundColor $color if ($c.Notes) { Write-Host (" {0,-$labelWidth} {1}" -f '', $c.Notes) -ForegroundColor DarkGray } } Write-Host '' $required = $checks | Where-Object { -not $_.Optional } $anyRequiredFail = $required | Where-Object { $_.Status -like 'Fail*' } if ($anyRequiredFail) { Write-Warning "$(@($anyRequiredFail).Count) required prerequisite(s) not met. See notes above for install commands." } else { Write-Host ' All required prerequisites are satisfied.' -ForegroundColor Green Write-Host '' } if ($Quiet) { return -not [bool]$anyRequiredFail } if ($PassThru) { return $checks } } |