Intune-Management.psm1
|
<#
Module: Intune-Management.psm1 Author: Thiago Beier Description: Functions for managing Intune ImportPFX Connector display name. #> function Get-IntuneConnectorServices { $results = Get-CimInstance -ClassName Win32_Service | Where-Object { $_.DisplayName -like "PFX*" } | Select-Object DisplayName, State, StartName if ($results) { Write-Host "" Write-Host "Intune Certificate Connector Services Status" -ForegroundColor Green $results | Format-Table -AutoSize } else { Write-Warning "Intune Certificate Connector Services Status not found." } } # Modules required by this module. Microsoft.Graph.Beta.DeviceManagement.Administration # depends on Microsoft.Graph.Authentication, but both are listed so each is # validated and, if needed, installed explicitly. $script:RequiredGraphModules = @( 'Microsoft.Graph.Authentication', 'Microsoft.Graph.Beta.DeviceManagement.Administration' ) # Validate and install everything required to talk to Microsoft Graph. # By default this installs / uses the LATEST version published by Microsoft. function Install-GraphPrerequisite { [CmdletBinding()] param ( # Modules that must be present before connecting. [string[]]$RequiredModules = $script:RequiredGraphModules, # Optional pin. When omitted the latest available version is used/installed. [string]$RequiredVersion, # Force a check for (and install of) the newest version on the PSGallery. [switch]$Update ) Write-Host "Validating prerequisites..." -ForegroundColor Cyan # Prefer machine-wide (AllUsers) install when elevated, so the modules land # in a path that is always on PSModulePath. A per-user (CurrentUser) install # can go to a redirected Documents folder that PS 5.1 does not search, which # makes the freshly "installed" module undiscoverable. $isAdmin = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent() ).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) $installScope = if ($isAdmin) { 'AllUsers' } else { 'CurrentUser' } Write-Host " Install scope: $installScope (elevated: $isAdmin)." # 1. TLS 1.2 - required by the PowerShell Gallery on Windows PowerShell 5.1. try { if ([Net.ServicePointManager]::SecurityProtocol -notmatch 'Tls12') { [Net.ServicePointManager]::SecurityProtocol = ` [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 Write-Host " Enabled TLS 1.2 for this session." } } catch { Write-Warning " Could not enforce TLS 1.2: $_" } # 2. NuGet package provider - required by Install-Module. try { $nuget = Get-PackageProvider -Name NuGet -ErrorAction SilentlyContinue if (-not $nuget -or $nuget.Version -lt [version]'2.8.5.201') { Write-Host " Installing NuGet package provider..." Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -Scope $installScope -ErrorAction Stop | Out-Null } } catch { Write-Warning " Could not install NuGet provider (offline?): $_" } # 3. Trust the PSGallery so installs run non-interactively. try { $gallery = Get-PSRepository -Name PSGallery -ErrorAction SilentlyContinue if ($gallery -and $gallery.InstallationPolicy -ne 'Trusted') { Set-PSRepository -Name PSGallery -InstallationPolicy Trusted -ErrorAction Stop Write-Host " Set PSGallery as a trusted repository." } } catch { Write-Warning " Could not set PSGallery to Trusted: $_" } # 4. Ensure each required module is installed (latest unless pinned). foreach ($mod in $RequiredModules) { $installed = Get-Module -ListAvailable -Name $mod $needsInstall = $false if (-not $installed) { $needsInstall = $true } elseif ($RequiredVersion -and -not ($installed.Version -contains [version]$RequiredVersion)) { $needsInstall = $true } elseif ($Update) { # Compare highest installed against the gallery's latest. try { $latest = (Find-Module -Name $mod -ErrorAction Stop).Version $highest = ($installed.Version | Sort-Object -Descending)[0] if ([version]$latest -gt [version]$highest) { $needsInstall = $true } } catch { Write-Warning " Could not check latest version of $mod (offline?): $_" } } if ($needsInstall) { $target = if ($RequiredVersion) { "version $RequiredVersion" } else { "latest version" } Write-Host " Installing $mod ($target) to $installScope scope..." $installParams = @{ Name = $mod Force = $true Scope = $installScope AllowClobber = $true Confirm = $false ErrorAction = 'Stop' } if ($RequiredVersion) { $installParams['RequiredVersion'] = $RequiredVersion } try { Install-Module @installParams } catch { Write-Error " Failed to install $mod. It is not present and could not be downloaded (offline?): $_" return $false } # Confirm the just-installed module is actually discoverable on the # module path. If a per-user install landed in a redirected folder # that is not on PSModulePath, retry once to AllUsers when elevated. $found = Get-Module -ListAvailable -Name $mod | Where-Object { -not $RequiredVersion -or $_.Version -eq [version]$RequiredVersion } if (-not $found -and $installScope -eq 'CurrentUser' -and $isAdmin) { Write-Warning " $mod not discoverable after CurrentUser install. Retrying to AllUsers..." $installParams['Scope'] = 'AllUsers' try { Install-Module @installParams } catch { Write-Error " Retry failed: $_"; return $false } $found = Get-Module -ListAvailable -Name $mod | Where-Object { -not $RequiredVersion -or $_.Version -eq [version]$RequiredVersion } } if (-not $found) { Write-Error " $mod$(if($RequiredVersion){" v$RequiredVersion"}) installed but not found on PSModulePath. Check that '$installScope' module path is on `$env:PSModulePath." return $false } } else { $ver = ($installed.Version | Sort-Object -Descending)[0] Write-Host " $mod present (v$ver)." } } Write-Host "Prerequisites OK." -ForegroundColor Green return $true } # Import the required Graph modules and authenticate. Tries multiple sign-in # methods so authentication succeeds across environments/versions. function Connect-ToMsGraph { [CmdletBinding()] param ( # Scopes required to read/update the Intune NDES/PFX connector. [string[]]$Scopes = @('DeviceManagementConfiguration.ReadWrite.All'), # Use device code flow first (useful on servers with no usable browser). [switch]$UseDeviceCode, # Optional pin. When omitted the latest installed version is used. [string]$GraphVersion, # Check the gallery for and install a newer Graph version before connecting. [switch]$Update ) # 1. Validate and install prerequisites (latest by default). $prereqParams = @{} if ($GraphVersion) { $prereqParams['RequiredVersion'] = $GraphVersion } if ($Update) { $prereqParams['Update'] = $true } if (-not (Install-GraphPrerequisite @prereqParams)) { Write-Error "Prerequisite validation failed. Aborting." return $false } # 2. Import the modules. A specific version is imported only when pinned; # otherwise the latest installed version is loaded. try { Write-Host "Importing Microsoft Graph modules..." foreach ($mod in $script:RequiredGraphModules) { if ($GraphVersion) { Import-Module $mod -RequiredVersion $GraphVersion -Force -ErrorAction Stop } else { Import-Module $mod -Force -ErrorAction Stop } } } catch { Write-Error "Failed to import Microsoft Graph modules: $_" return $false } # 3. Authenticate. if (-not (Invoke-GraphSignIn -Scopes $Scopes -PreferDeviceCode:$UseDeviceCode)) { # Last resort: on Windows PowerShell 5.1, Graph SDK builds after 2.19.0 # have a regression that breaks delegated sign-in (device code -> # NullReferenceException, interactive -> AADSTS900144). If the latest # failed and the caller did not pin a version, retry once with the last # known-good 2.19.0 so authentication still succeeds. $knownGood = '2.19.0' if (-not $GraphVersion -and $PSVersionTable.PSEdition -eq 'Desktop') { Write-Warning "Latest Microsoft Graph failed to authenticate on Windows PowerShell 5.1. Retrying with known-good v$knownGood..." if (Install-GraphPrerequisite -RequiredVersion $knownGood) { try { foreach ($mod in $script:RequiredGraphModules) { Remove-Module $mod -Force -ErrorAction SilentlyContinue Import-Module $mod -RequiredVersion $knownGood -Force -ErrorAction Stop } Invoke-GraphSignIn -Scopes $Scopes -PreferDeviceCode:$UseDeviceCode | Out-Null } catch { Write-Warning "Fallback to v$knownGood failed: $_" } } } } # 4. Verify the connection actually succeeded and has the required scopes. $context = $null try { $context = Get-MgContext -ErrorAction Stop } catch { $context = $null } if (-not $context) { Write-Error "Not connected to Microsoft Graph. Aborting. (On Windows PowerShell 5.1 with the latest Graph SDK, use PowerShell 7 or run with -GraphVersion 2.19.0.)" return $false } $stillMissing = $Scopes | Where-Object { $_ -notin $context.Scopes } if ($stillMissing) { Write-Error "Connected, but missing required scope(s): $($stillMissing -join ', '). Aborting." return $false } Write-Host "Connected to Microsoft Graph as $($context.Account)." -ForegroundColor Green return $true } # Resilient sign-in helper: reuses a valid session, otherwise tries device code # and interactive browser flows until one succeeds. function Invoke-GraphSignIn { [CmdletBinding()] param ( [string[]]$Scopes, [switch]$PreferDeviceCode ) # On PS 5.1, Get-MgContext throws 'SessionNotInitialized' instead of # returning $null when there is no active session. Wrap it so callers get # a clean $null. $existingContext = $null try { $existingContext = Get-MgContext -ErrorAction Stop } catch { $existingContext = $null } # A context can exist but lack the required scopes (e.g. left over from a # previous half-failed sign-in). Reusing it makes the Graph cmdlets fall # back to an interactive prompt, so force a clean reconnect instead. if ($existingContext) { $missing = $Scopes | Where-Object { $_ -notin $existingContext.Scopes } if (-not $missing) { Write-Host "Already authenticated to Microsoft Graph with required scopes." -ForegroundColor Green return $true } Write-Host "Existing Graph session is missing required scopes. Reconnecting..." try { Disconnect-MgGraph -ErrorAction SilentlyContinue | Out-Null } catch { } } # Try each method until one produces a valid context. Always prompts the user. $methods = if ($PreferDeviceCode) { @(@{ Name = 'device code'; DeviceCode = $true }, @{ Name = 'interactive browser'; DeviceCode = $false }) } else { @(@{ Name = 'interactive browser'; DeviceCode = $false }, @{ Name = 'device code'; DeviceCode = $true }) } foreach ($method in $methods) { Write-Host "Connecting to Microsoft Graph using $($method.Name)..." if ($method.DeviceCode) { Write-Host "You will be prompted to sign in. Open https://microsoft.com/devicelogin and enter the code shown below." -ForegroundColor Yellow } try { $connectParams = @{ Scopes = $Scopes NoWelcome = $true } if ($method.DeviceCode) { $connectParams['UseDeviceCode'] = $true } Connect-MgGraph @connectParams $probe = $null try { $probe = Get-MgContext -ErrorAction Stop } catch { $probe = $null } if ($probe) { return $true } } catch { Write-Warning "Sign-in via $($method.Name) failed: $($_.Exception.Message)" } } return $false } function Get-IntuneCertificateConnectorInfo { [CmdletBinding()] param () # Search for the connector Write-Host "Searching for Certificate Connector for Microsoft Intune..." $script:IntuneCertConnectorInfo = Get-ItemProperty HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\* | Where-Object { $_.DisplayName -eq "Certificate Connector for Microsoft Intune" } | Select-Object DisplayName, DisplayVersion, Publisher, InstallDate if ($script:IntuneCertConnectorInfo) { Write-Host "Certificate Connector for Microsoft Intune found." } else { Write-Host "Certificate Connector for Microsoft Intune not found." $script:IntuneCertConnectorInfo = @() } return $script:IntuneCertConnectorInfo } # Example usage #Get-IntuneCertificateConnectorInfo # Now you can reuse $script:IntuneCertConnectorInfo in other functions or logic function Update-IntuneConnectorDisplayName { [CmdletBinding()] param ( # Use device code flow for sign-in (useful on servers with no browser). [switch]$UseDeviceCode, # Pin the Microsoft Graph SDK version used for sign-in (e.g. '2.19.0'). [string]$GraphVersion, # Check the gallery for and install the newest Graph SDK before connecting. [switch]$Update ) # Ensure the Microsoft Graph module is imported and we are authenticated. $connectSplat = @{ UseDeviceCode = $UseDeviceCode } if ($GraphVersion) { $connectSplat['GraphVersion'] = $GraphVersion } if ($Update) { $connectSplat['Update'] = $true } $connected = Connect-ToMsGraph @connectSplat if (-not $connected) { Write-Warning "Aborting: could not authenticate to Microsoft Graph." return } # The registry holds the thumbprint of the encryption certificate this # server's connector is actively using. This is the authoritative way to # identify THIS connector, especially after an uninstall/reinstall leaves # stale "Microsoft Intune ImportPFX Connector CA" certificates behind. $registryKey = Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\MicrosoftIntune\PFXCertificateConnector" -ErrorAction SilentlyContinue $registryThumbprint = $registryKey.EncryptionCertThumbprint Write-Host "" if (-not $registryThumbprint) { Write-Warning "Intune Certificate Connector not installed on this server (registry EncryptionCertThumbprint not found)." Write-Host "" return } # Find the certificate that matches the registry thumbprint (rather than # guessing the 'newest' connector cert, which is unreliable when several # connector certs are present from previous installs). $result = Get-ChildItem -Path Cert:\LocalMachine\My | Where-Object { $_.Thumbprint -eq $registryThumbprint } | Select-Object -First 1 -Property NotBefore, NotAfter, Subject, Thumbprint if (-not $result) { Write-Warning "No certificate in LocalMachine\My matches the connector thumbprint '$registryThumbprint'. The connector certificate may be missing." Write-Host "" return } Write-Host "Certificate thumbprint matches registry key ($registryThumbprint)." # The certificate subject CN is this connector's NDES/PFX connector ID. $ndesConnectorId = $result.Subject -replace "^CN=", "" Write-Host "Connector ID for this server: $ndesConnectorId" # Get connector info from Microsoft Graph Beta API $intuneCertConnector = Get-MgBetaDeviceManagementNdeConnector -NdesConnectorId $ndesConnectorId -ErrorAction SilentlyContinue if (-not $intuneCertConnector) { Write-Warning "Connector '$ndesConnectorId' was not found in Intune. Verify you signed in to the correct tenant." Write-Host "" return } Write-Host "Existing Connector DisplayName: $($intuneCertConnector.DisplayName)" # Check if the connector ID matches if ($intuneCertConnector.Id -eq $ndesConnectorId) { if ($intuneCertConnector.DisplayName -like "*$env:COMPUTERNAME*") { Write-Host "Intune Certificate Connector is up to date" } else { $combined = "$env:COMPUTERNAME" + "_" + $intuneCertConnector.DisplayName $params = @{ id = $intuneCertConnector.Id displayName = $combined } Write-Host "Updating Intune Certificate Connector DisplayName to '$combined'..." Update-MgBetaDeviceManagementNdeConnector -NdesConnectorId $ndesConnectorId -BodyParameter $params Write-Host "Updated Connector DisplayName to '$combined'." -ForegroundColor Green } } else { Write-Warning "Connector ID mismatch." } Write-Host "" } # NOTE: Do NOT run any commands on import. # This module only defines functions. # Run installation, import, and authentication commands outside the module. |