GraphModuleStatus.psm1
|
############################################################################## # GraphModuleStatus # Shows the status of Microsoft Graph PowerShell modules on profile load ############################################################################## # Path to the update script (bundled with module) $script:UpdateScriptPath = Join-Path $PSScriptRoot "Update-MicrosoftGraph.ps1" # Path to user preferences file (persists across module updates) $script:PreferencesPath = Join-Path $env:APPDATA "GraphModuleStatus\preferences.json" ############################################################################## # Get-GraphModulePreferences / Save-GraphModulePreferences # Internal helpers for persistent user preferences ############################################################################## function Get-GraphModulePreferences { if (Test-Path -Path $script:PreferencesPath) { try { $Json = Get-Content -Path $script:PreferencesPath -Raw -Encoding utf8 | ConvertFrom-Json return $Json } catch { return [PSCustomObject]@{ DismissedInstallOffers = @() } } } return [PSCustomObject]@{ DismissedInstallOffers = @() } } function Save-GraphModulePreferences { param([PSCustomObject]$Preferences) $Dir = Split-Path -Path $script:PreferencesPath -Parent if (-not (Test-Path -Path $Dir)) { New-Item -Path $Dir -ItemType Directory -Force | Out-Null } $Preferences | ConvertTo-Json -Depth 5 | Out-File -FilePath $script:PreferencesPath -Encoding utf8 -Force } ############################################################################## # Get-LatestPSGalleryVersion # Fast version check via URL redirect — no download, 5-second timeout # Adapted from Entra-PIM module (github.com/markdomansky/Entra-PIM) ############################################################################## function Get-LatestPSGalleryVersion { param( [Parameter(Mandatory)] [string]$Name ) try { $Url = "https://www.powershellgallery.com/packages/$Name" try { $null = Invoke-WebRequest -Uri $Url -UseBasicParsing -MaximumRedirection 0 -TimeoutSec 5 -ErrorAction Stop } catch { if ($_.Exception.Response -and $_.Exception.Response.Headers) { try { $Location = $_.Exception.Response.Headers.GetValues('Location') | Select-Object -First 1 if ($Location) { return [version](Split-Path -Path $Location -Leaf) } } catch { } } } } catch { } return $null } ############################################################################## # Install-GraphModules # Internal helper — scope selection, elevation, and installation ############################################################################## function Install-GraphModules { param( [Parameter(Mandatory)] [array]$Modules ) # If more than one module, let the user choose which to install $ModulesToInstall = $Modules if ($Modules.Count -gt 1) { Write-Host " Which modules would you like to install?" -ForegroundColor Cyan Write-Host "" $i = 1 foreach ($Mod in $Modules) { Write-Host " [$i] $($Mod.Name)$(if ($Mod.AvailableVersion) { " v$($Mod.AvailableVersion)" })" -ForegroundColor White $i++ } Write-Host " [A] All (default)" -ForegroundColor White Write-Host "" $ModChoice = Read-Host " Enter your choice" if ($ModChoice -match '^\d+$') { $Index = [int]$ModChoice - 1 if ($Index -ge 0 -and $Index -lt $Modules.Count) { $ModulesToInstall = @($Modules[$Index]) } else { Write-Host " Invalid choice. Installing all modules." -ForegroundColor DarkGray } } # Any other input (A, Enter, etc.) installs all — no action needed Write-Host "" } Write-Host " Install scope:" -ForegroundColor Cyan Write-Host " [1] All Users (Recommended)" -ForegroundColor White Write-Host " [2] Current User Only" -ForegroundColor White Write-Host "" $ScopeChoice = Read-Host " Enter your choice (1-2) [default: 1]" $InstallScope = if ($ScopeChoice -eq "2") { "CurrentUser" } else { "AllUsers" } # All Users requires elevation — auto-launch an elevated session if needed if ($InstallScope -eq "AllUsers") { $IsAdmin = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) if (-not $IsAdmin) { Write-Host "" Write-Host " All Users requires Administrator rights." -ForegroundColor Yellow Write-Host " Launching elevated session to complete the installation..." -ForegroundColor Yellow Write-Host "" # Write install commands to a temp script — avoids -Command quoting issues $TempScript = [System.IO.Path]::GetTempFileName() -replace '\.tmp$', '.ps1' $ScriptLines = @( "Write-Host ''", "Write-Host ' Installing Microsoft Graph modules for All Users...' -ForegroundColor Cyan", "Write-Host ''" ) foreach ($Mod in $ModulesToInstall) { $ScriptLines += "Write-Host ' Installing $($Mod.Name)...' -ForegroundColor Yellow" $ScriptLines += "Write-Host ' (This installs all sub-modules and may take several minutes)' -ForegroundColor Gray" $ScriptLines += "Write-Host ''" $ScriptLines += "Install-PSResource -Name '$($Mod.Name)' -Scope AllUsers -TrustRepository -AcceptLicense" $ScriptLines += "Write-Host ''" $ScriptLines += "Write-Host ' $($Mod.Name) installed successfully.' -ForegroundColor Green" $ScriptLines += "Write-Host ''" } $ScriptLines += "Write-Host ' Done. Open a new PowerShell window before using Graph commands.' -ForegroundColor Green" $ScriptLines += "Write-Host ''" $ScriptLines += "Read-Host ' Press Enter to close'" $ScriptLines += "Remove-Item -Path '$TempScript' -Force -ErrorAction SilentlyContinue" $ScriptLines | Out-File -FilePath $TempScript -Encoding utf8 # Use the current process executable so elevation uses the same PS version $PwshExe = (Get-Process -Id $PID).MainModule.FileName try { Start-Process $PwshExe -Verb RunAs -ArgumentList "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", $TempScript Write-Host " Elevated installer launched — check the new window for progress." -ForegroundColor Cyan Write-Host "" } catch { Remove-Item -Path $TempScript -Force -ErrorAction SilentlyContinue Write-Host " ERROR: Could not launch elevated session - $_" -ForegroundColor Red Write-Host " Run PowerShell as Administrator and call Get-GraphModuleStatus again." -ForegroundColor Yellow Write-Host "" } return } } $ScopeDisplay = if ($InstallScope -eq "AllUsers") { "All Users" } else { "Current User Only" } Write-Host "" Write-Host " Modules will be installed for: $ScopeDisplay" -ForegroundColor Gray Write-Host "" $InstallSuccess = 0 $InstallFailed = 0 foreach ($Mod in $ModulesToInstall) { Write-Host " Installing $($Mod.Name)$(if ($Mod.AvailableVersion) { " v$($Mod.AvailableVersion)" })..." -ForegroundColor Yellow Write-Host " (This installs all sub-modules and may take several minutes)" -ForegroundColor Gray Write-Host "" try { Install-PSResource -Name $Mod.Name -Scope $InstallScope -TrustRepository -AcceptLicense -ErrorAction Stop Write-Host " $($Mod.Name) installed successfully." -ForegroundColor Green $InstallSuccess++ } catch { Write-Host " ERROR: Failed to install $($Mod.Name) - $_" -ForegroundColor Red $InstallFailed++ } Write-Host "" } Write-Host " Install complete: $InstallSuccess succeeded, $InstallFailed failed." -ForegroundColor $(if ($InstallFailed -gt 0) { "Yellow" } else { "Green" }) Write-Host "" if ($InstallSuccess -gt 0) { Write-Host " Open a new PowerShell window before running any Graph commands." -ForegroundColor Yellow Write-Host "" } } ############################################################################## # Get-GraphModuleStatus # Shows installed vs available versions of Graph modules ############################################################################## Function Get-GraphModuleStatus { <# .SYNOPSIS Shows the status of Microsoft Graph modules (installed vs available versions) .DESCRIPTION Checks Microsoft.Graph and Microsoft.Graph.Beta modules and displays their current installed version compared to the latest available version in PSGallery. When updates are available, prompts to run the update script. .PARAMETER Silent If specified, suppresses output and returns objects instead. .PARAMETER NoPrompt If specified, does not prompt to run the update script when updates are available. .EXAMPLE Get-GraphModuleStatus .EXAMPLE Get-GraphModuleStatus -Silent .EXAMPLE Get-GraphModuleStatus -NoPrompt .LINK https://github.com/yourusername/GraphModuleStatus #> [CmdletBinding()] param( [switch]$Silent, [switch]$NoPrompt ) $Modules = @( @{ Name = "Microsoft.Graph"; Display = "Microsoft.Graph" }, @{ Name = "Microsoft.Graph.Beta"; Display = "Microsoft.Graph.Beta" } ) $Results = @() if (-not $Silent) { Write-Host "" } foreach ($Module in $Modules) { # Check both CurrentUser and AllUsers scopes $Installed = @( Get-InstalledPSResource -Name $Module.Name -Scope CurrentUser -ErrorAction SilentlyContinue Get-InstalledPSResource -Name $Module.Name -Scope AllUsers -ErrorAction SilentlyContinue ) | Sort-Object Version -Descending | Select-Object -First 1 $Status = [PSCustomObject]@{ Name = $Module.Name InstalledVersion = $null AvailableVersion = $null UpdateAvailable = $false Installed = $false } if ($Installed) { $Status.Installed = $true $Status.InstalledVersion = $Installed.Version.ToString() # Check for updates via fast URL redirect (no download needed) $LatestVersion = Get-LatestPSGalleryVersion -Name $Module.Name if ($LatestVersion) { $Status.AvailableVersion = $LatestVersion.ToString() $Status.UpdateAvailable = ($LatestVersion -gt [version]$Status.InstalledVersion) } if (-not $Silent) { if ($Status.UpdateAvailable) { # Update available Write-Host " [$($Module.Display)]" -ForegroundColor White -NoNewline Write-Host " v$($Status.InstalledVersion) " -ForegroundColor Yellow -NoNewline Write-Host "→" -ForegroundColor DarkGray -NoNewline Write-Host " v$($Status.AvailableVersion)" -ForegroundColor Green } else { # Up to date Write-Host " [$($Module.Display)]" -ForegroundColor Cyan -NoNewline Write-Host " v$($Status.InstalledVersion) " -NoNewline Write-Host "●" -ForegroundColor Green -NoNewline Write-Host " Current" -ForegroundColor DarkGray } } } else { # Not installed - check what's available on PSGallery via fast URL redirect $LatestVersion = Get-LatestPSGalleryVersion -Name $Module.Name if ($LatestVersion) { $Status.AvailableVersion = $LatestVersion.ToString() } if (-not $Silent) { Write-Host " [$($Module.Display)]" -ForegroundColor DarkGray -NoNewline Write-Host " ○ Not installed" -ForegroundColor Red -NoNewline if ($Status.AvailableVersion) { Write-Host " (v$($Status.AvailableVersion) available on PSGallery)" -ForegroundColor DarkGray } else { Write-Host "" } } } $Results += $Status } if (-not $Silent) { Write-Host "" } # Check if any updates are available and prompt user $UpdatesAvailable = $Results | Where-Object { $_.UpdateAvailable -eq $true } if ($UpdatesAvailable -and -not $Silent -and -not $NoPrompt) { if (Test-Path -Path $script:UpdateScriptPath) { $Response = Read-Host " Update available. Run Update-GraphModule now? (Y/N)" if ($Response -eq 'Y' -or $Response -eq 'y') { Write-Host "" Update-GraphModule } else { Write-Host "" } } } # If none are installed, offer to install them now $NoneInstalled = -not ($Results | Where-Object { $_.Installed -eq $true }) $NotInstalledModules = @($Results | Where-Object { -not $_.Installed }) if ($NoneInstalled -and $NotInstalledModules.Count -gt 0 -and -not $Silent -and -not $NoPrompt) { Write-Host " No Microsoft Graph modules are installed." -ForegroundColor Yellow $HasVersionInfo = @($NotInstalledModules | Where-Object { $null -ne $_.AvailableVersion }) if ($HasVersionInfo.Count -gt 0) { Write-Host " The following versions are available from PSGallery:" -ForegroundColor Yellow } Write-Host "" foreach ($Mod in $NotInstalledModules) { Write-Host " [$($Mod.Name)]" -ForegroundColor White -NoNewline if ($null -ne $Mod.AvailableVersion) { Write-Host " v$($Mod.AvailableVersion)" -ForegroundColor Cyan } else { Write-Host " (latest)" -ForegroundColor DarkGray } } Write-Host "" $InstallPrompt = Read-Host " Would you like to install them now? (Y/N)" if ($InstallPrompt -match '^[Yy]') { Write-Host "" Install-GraphModules -Modules $NotInstalledModules } } # If some modules are installed but others are not, offer to install missing ones # Declining dismisses the offer permanently (stored in user preferences) $SomeInstalled = ($Results | Where-Object { $_.Installed }) -and $NotInstalledModules.Count -gt 0 if ($SomeInstalled -and -not $Silent -and -not $NoPrompt) { $Prefs = Get-GraphModulePreferences $Dismissed = @($Prefs.DismissedInstallOffers) # Filter out modules the user has previously declined $OfferedModules = @($NotInstalledModules | Where-Object { $Dismissed -notcontains $_.Name }) foreach ($Mod in $OfferedModules) { $VersionInfo = if ($Mod.AvailableVersion) { " (v$($Mod.AvailableVersion) available)" } else { "" } $Response = Read-Host " $($Mod.Name) is not installed$VersionInfo. Install it? (Y/N)" if ($Response -match '^[Yy]') { Write-Host "" Install-GraphModules -Modules @($Mod) } else { # Save the dismissal so we don't ask again $Dismissed += $Mod.Name $Prefs.DismissedInstallOffers = $Dismissed Save-GraphModulePreferences -Preferences $Prefs Write-Host "" } } } if ($Silent) { return $Results } } ############################################################################## # Update-GraphModule # Runs the update script to clean install/reinstall Graph modules ############################################################################## Function Update-GraphModule { <# .SYNOPSIS Runs the Microsoft Graph module update script .DESCRIPTION Invokes the Update-MicrosoftGraph.ps1 script which performs a clean uninstall and reinstall of Microsoft Graph modules. .EXAMPLE Update-GraphModule .LINK https://github.com/yourusername/GraphModuleStatus #> [CmdletBinding()] param() if (Test-Path -Path $script:UpdateScriptPath) { Write-Host "Running Microsoft Graph update script..." -ForegroundColor Cyan Write-Host "" & $script:UpdateScriptPath } else { Write-Host "Update script not found at: $script:UpdateScriptPath" -ForegroundColor Red Write-Host "Please ensure the script exists or update the path in the module." -ForegroundColor Yellow } } ############################################################################## # Add-GraphModuleStatusToProfile # Adds GraphModuleStatus check to PowerShell profile ############################################################################## Function Add-GraphModuleStatusToProfile { <# .SYNOPSIS Add GraphModuleStatus check to PowerShell Profile .DESCRIPTION Adds the GraphModuleStatus module import and status check to your PowerShell profile so it runs automatically when you start PowerShell. Needs to be executed separately for PowerShell v5 and v7. .EXAMPLE Add-GraphModuleStatusToProfile .LINK https://github.com/yourusername/GraphModuleStatus #> [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium')] param() $ProfileContent = @" # GraphModuleStatus: Check Microsoft Graph module status on startup Import-Module -Name GraphModuleStatus -ErrorAction SilentlyContinue Get-GraphModuleStatus "@ if (-not (Test-Path -Path $Profile)) { # No Profile found Write-Host "No PowerShell Profile exists. Creating new Profile with GraphModuleStatus setup." -ForegroundColor Yellow $ProfileContent | Out-File -FilePath $Profile -Encoding utf8 -Force Write-Host "Profile created at: $Profile" -ForegroundColor Green } else { # Profile found $ExistingContent = Get-Content -Path $Profile -Encoding utf8 -Raw $Match = $ExistingContent | Where-Object { $_ -match "GraphModuleStatus" } if ($Match) { # GraphModuleStatus already in Profile Write-Host "GraphModuleStatus is already in your PowerShell Profile." -ForegroundColor Yellow } else { # GraphModuleStatus not in Profile Write-Host "Adding GraphModuleStatus to existing PowerShell Profile..." -ForegroundColor Yellow Add-Content -Path $Profile -Value $ProfileContent -Encoding utf8 Write-Host "GraphModuleStatus added to: $Profile" -ForegroundColor Green } } } ############################################################################## # Remove-GraphModuleStatusFromProfile # Removes GraphModuleStatus from PowerShell profile ############################################################################## Function Remove-GraphModuleStatusFromProfile { <# .SYNOPSIS Remove GraphModuleStatus from PowerShell Profile .DESCRIPTION Removes the GraphModuleStatus module import and status check from your PowerShell profile. .EXAMPLE Remove-GraphModuleStatusFromProfile .LINK https://github.com/yourusername/GraphModuleStatus #> [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium')] param() if (-not (Test-Path -Path $Profile)) { Write-Host "No PowerShell Profile exists." -ForegroundColor Yellow return } $Content = Get-Content -Path $Profile -Encoding utf8 $NewContent = $Content | Where-Object { $_ -notmatch "GraphModuleStatus" -and $_ -notmatch "# GraphModuleStatus:" } if ($Content.Count -ne $NewContent.Count) { $NewContent | Out-File -FilePath $Profile -Encoding utf8 -Force Write-Host "GraphModuleStatus removed from PowerShell Profile." -ForegroundColor Green } else { Write-Host "GraphModuleStatus was not found in your PowerShell Profile." -ForegroundColor Yellow } } ############################################################################## # Module Load Message ############################################################################## if (-not (Test-Path -Path $Profile)) { Write-Host "Tip: Run Add-GraphModuleStatusToProfile to check Graph module status on startup." -ForegroundColor DarkGray } else { $Content = Get-Content -Path $Profile -Encoding utf8 -Raw -ErrorAction SilentlyContinue if ($Content -notmatch "GraphModuleStatus") { Write-Host "Tip: Run Add-GraphModuleStatusToProfile to check Graph module status on startup." -ForegroundColor DarkGray } } |