Public/Test-METPrerequisites.ps1

function Test-METPrerequisites {
    [CmdletBinding()]
    param()

    $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.0.0'; 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  }
        [PSCustomObject]@{ Name = 'Pester';                           Min = '5.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

    foreach ($c in $checks) {
        $color = switch -Wildcard ($c.Status) {
            'OK'     { 'Green' }
            'Fail*'  { 'Red'   }
            default  { 'Yellow'}
        }
        $tag = if ($c.Optional) { ' [optional]' } else { '' }
        Write-Host (" {0,-42} {1}" -f "$($c.Component)$tag", $c.Status) -ForegroundColor $color
        if ($c.Notes) {
            Write-Host (" {0,-42} {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 ''
    }

    return $checks
}