msgraphProxy.psm1
|
$script:ModuleRoot = $PSScriptRoot function Clear-MsGraphProxySystemProxy { <# .SYNOPSIS Clears a Windows system-proxy registration left behind by Dev Proxy. .DESCRIPTION Dev Proxy registers itself as the Windows system HTTP/HTTPS proxy while running, and only unregisters that as part of its own graceful shutdown. If Dev Proxy has to be force-killed instead, that unregistration never runs, and Windows is left pointed at a dead proxy port, breaking every proxy-aware application on the machine. This is a last-resort safety net that mirrors what Dev Proxy's own graceful shutdown does, called by Stop-MsGraphProxy only when the graceful stop failed. .EXAMPLE PS C:\> Clear-MsGraphProxySystemProxy Disables the Windows system proxy if Dev Proxy left it enabled. #> [CmdletBinding()] param () if (-not $IsWindows) { return } $key = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Internet Settings' $current = Get-ItemProperty -Path $key -Name ProxyEnable -ErrorAction SilentlyContinue if (-not $current -or $current.ProxyEnable -eq 0) { return } Set-ItemProperty -Path $key -Name ProxyEnable -Value 0 if (-not ('MsGraphProxyModule.WinInet' -as [type])) { Add-Type -Namespace MsGraphProxyModule -Name WinInet -MemberDefinition @' [DllImport("wininet.dll", SetLastError = true)] public static extern bool InternetSetOption(IntPtr hInternet, int dwOption, IntPtr lpBuffer, int dwBufferLength); '@ } [MsGraphProxyModule.WinInet]::InternetSetOption([IntPtr]::Zero, 39, [IntPtr]::Zero, 0) | Out-Null [MsGraphProxyModule.WinInet]::InternetSetOption([IntPtr]::Zero, 37, [IntPtr]::Zero, 0) | Out-Null Write-Warning 'Cleared a stale Windows system-proxy registration left behind by Dev Proxy.' } function Get-MsGraphProxyEntraIDLicensePreset { <# .SYNOPSIS Builds a graphSchemaMockPlugin subscribedSkus entry for a named Entra ID license tier. .DESCRIPTION Maester's Get-MtLicenseInformation (and anything else that inspects subscribedSkus the same way) detects the tenant's Entra ID license by checking for specific servicePlanId GUIDs in priority order: P2, then Governance, then P1, falling back to Free if none match. This returns a subscribedSkus entry carrying the right GUID for the requested tier, for Start-MsGraphProxy's -EntraIDLicense to feed into New-MsGraphProxyCIConfigFile -SubscribedSkus. .PARAMETER License The Entra ID license tier to build a preset for. .EXAMPLE PS C:\> Get-MsGraphProxyEntraIDLicensePreset -License P2 Returns a subscribedSkus entry for Entra ID P2. #> [CmdletBinding()] param ( [Parameter(Mandatory)] [ValidateSet('Free', 'P1', 'P2', 'Governance')] [string] $License ) # Real Microsoft Entra ID service plan GUIDs - see # https://learn.microsoft.com/entra/identity/users/licensing-service-plan-reference switch ($License) { 'P2' { [pscustomobject]@{ skuPartNumber = 'AAD_PREMIUM_P2' capabilityStatus = 'Enabled' servicePlans = @( [pscustomobject]@{ servicePlanId = 'eec0eb4f-6444-4f95-aba0-50c24d67f998' servicePlanName = 'AAD_PREMIUM_P2' } ) } } 'Governance' { [pscustomobject]@{ skuPartNumber = 'AAD_PREMIUM_GOVERNANCE' capabilityStatus = 'Enabled' servicePlans = @( [pscustomobject]@{ servicePlanId = 'e866a266-3cff-43a3-acca-0c90a7e00c8b' servicePlanName = 'Entra_Identity_Governance' } ) } } 'P1' { [pscustomobject]@{ skuPartNumber = 'AAD_PREMIUM' capabilityStatus = 'Enabled' servicePlans = @( [pscustomobject]@{ servicePlanId = '41781fb2-bc02-4b7c-bd55-b576c07bb09d' servicePlanName = 'AAD_PREMIUM' } ) } } 'Free' { [pscustomobject]@{ skuPartNumber = 'EXCHANGESTANDARD' capabilityStatus = 'Enabled' servicePlans = @( [pscustomobject]@{ servicePlanId = '9aaf7827-d63c-4b61-89c3-182f06f82e5c' servicePlanName = 'EXCHANGE_S_STANDARD' } ) } } } } function Get-MsGraphProxyExePath { <# .SYNOPSIS Resolves the local path to the cached Dev Proxy executable. .DESCRIPTION Looks up the self-contained Dev Proxy build for the current operating system inside the module's local binary cache, installed there by Install-MsGraphProxy, and returns the full path to its executable. .EXAMPLE PS C:\> Get-MsGraphProxyExePath Returns the full path to the cached devproxy executable, throwing if it hasn't been installed yet. #> [CmdletBinding()] param () $rid = $script:MsGraphProxyRid ?? (Get-MsGraphProxyRid) $exeName = if ($IsWindows) { 'devproxy.exe' } else { 'devproxy' } $ridRoot = Join-Path -Path $script:MsGraphProxyBinRoot -ChildPath $rid $exePath = Join-Path -Path $ridRoot -ChildPath $exeName if (-not (Test-Path -Path $exePath)) { throw "Dev Proxy isn't installed for $rid. Run Install-MsGraphProxy first." } $exePath } function Get-MsGraphProxyRid { <# .SYNOPSIS Resolves the .NET runtime identifier for the current operating system. .DESCRIPTION Maps the current operating system to the runtime identifier (RID) used to name the published, self-contained Dev Proxy binaries this module downloads and runs, for example "win-x64" or "linux-x64". macOS resolves to "osx-arm64" or "osx-x64" depending on processor architecture - only osx-arm64 is actually built/published today (it's what GitHub Actions' macos-latest runners are), so Install-MsGraphProxy on an Intel Mac will fail clearly with "no release asset found" rather than this function silently mismapping it. .EXAMPLE PS C:\> Get-MsGraphProxyRid Returns the RID matching the current operating system, e.g. "win-x64". #> [CmdletBinding()] [OutputType([string])] param () if ($IsWindows) { return 'win-x64' } if ($IsLinux) { return 'linux-x64' } if ($IsMacOS) { if ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture -eq [System.Runtime.InteropServices.Architecture]::Arm64) { return 'osx-arm64' } return 'osx-x64' } throw 'msgraphProxy has no published Dev Proxy build for this operating system.' } function New-MsGraphProxyCIConfigFile { <# .SYNOPSIS Derives a modified copy of a devproxyrc.json - certificate auto-install disabled for CI, and/or config overrides like a specific Entra ID license. .DESCRIPTION Used by Start-MsGraphProxy to produce a working copy of devproxyrc.json whenever -CI or -EntraIDLicense is passed, rather than mutating the config file bundled with the module itself. -CI disables certificate auto-install on Windows, which is what lets Dev Proxy's proxy port actually bind in a non-interactive session - see the code comment where it's applied for why, and why it's only ever done under -CI specifically. -SubscribedSkus overwrites graphSchemaMockPlugin.subscribedSkus, which is how Start-MsGraphProxy's -EntraIDLicense picks which license tier the mocked tenant reports (see that plugin's own config shape). The copy is written next to the original config file, not to a temp directory: plugin settings like schemaFilePath/mocksFile are relative paths resolved against the config file's own directory, so keeping the copy alongside the original preserves those references. .PARAMETER ConfigFile Path to the source devproxyrc.json to derive a copy from. .PARAMETER CI Also disable certificate auto-install on Windows - see DESCRIPTION. .PARAMETER SubscribedSkus Replaces graphSchemaMockPlugin.subscribedSkus from the source config. Only applied if -SubscribedSkus is passed. .PARAMETER WhatIf If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. .PARAMETER Confirm If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. .EXAMPLE PS C:\> New-MsGraphProxyCIConfigFile -ConfigFile 'C:\proxy\devproxyrc.json' -CI Returns an object with the generated config's path and the proxy port it declares (or Dev Proxy's default of 8000 if unset). #> [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Low')] param ( [Parameter(Mandatory)] [string] $ConfigFile, [switch] $CI, [object[]] $SubscribedSkus ) $config = Get-Content -Raw -Path $ConfigFile | ConvertFrom-Json -AsHashtable if ($CI -and $IsWindows) { $config['installCert'] = $false } if ($PSBoundParameters.ContainsKey('SubscribedSkus')) { if (-not $config.ContainsKey('graphSchemaMockPlugin')) { $config['graphSchemaMockPlugin'] = @{} } $config['graphSchemaMockPlugin']['subscribedSkus'] = @($SubscribedSkus) } $proxyPort = if ($config.ContainsKey('port')) { [int]$config['port'] } else { 8000 } $ciConfigFile = Join-Path -Path (Split-Path -Path $ConfigFile -Parent) -ChildPath 'msgraphproxy-ci-devproxyrc.json' if ($PSCmdlet.ShouldProcess($ciConfigFile, 'Write derived Dev Proxy config')) { $config | ConvertTo-Json -Depth 10 | Set-Content -Path $ciConfigFile } [pscustomobject]@{ ConfigFile = $ciConfigFile ProxyPort = $proxyPort } } function Receive-MsGraphProxyRecording { <# .SYNOPSIS Stops Dev Proxy's active recording and collects the resulting reports. .DESCRIPTION Calls Dev Proxy's control API to stop recording, which synchronously triggers its reporting plugins (such as GraphMinimalPermissionsPlugin and ExecutionSummaryPlugin) to analyze what was recorded. Because JsonReporter is enabled in this module's bundled configuration, those plugins write their results as JSON files into Dev Proxy's working directory; this function reads them, parses them, deletes them, and returns them as a single object keyed by report name. .PARAMETER ApiPort Port of Dev Proxy's control API. .PARAMETER WorkingDirectory The directory Dev Proxy was started in, where report files are written. .EXAMPLE PS C:\> Receive-MsGraphProxyRecording -ApiPort 8897 -WorkingDirectory 'C:\bin\win-x64' Stops recording and returns any reports Dev Proxy generated. #> [CmdletBinding()] param ( [Parameter(Mandatory)] [int] $ApiPort, [Parameter(Mandatory)] [string] $WorkingDirectory ) $reportFilter = 'GraphMinimalPermissions*_JsonReporter.json' Get-ChildItem -Path $WorkingDirectory -Filter $reportFilter -ErrorAction SilentlyContinue | Remove-Item -Force -ErrorAction SilentlyContinue try { Invoke-RestMethod -Method Post -Uri "http://127.0.0.1:$ApiPort/proxy" ` -ContentType 'application/json' -Body '{"recording":false}' -TimeoutSec 30 | Out-Null } catch { Write-Verbose "Stopping the recording via the API failed: $_" return $null } $reportFiles = Get-ChildItem -Path $WorkingDirectory -Filter $reportFilter -ErrorAction SilentlyContinue if (-not $reportFiles) { return $null } $reports = [ordered]@{} foreach ($reportFile in $reportFiles) { $reportName = $reportFile.BaseName -replace '_JsonReporter$', '' $reports[$reportName] = Get-Content -Path $reportFile.FullName -Raw | ConvertFrom-Json #Remove-Item -Path $reportFile.FullName -Force -ErrorAction SilentlyContinue } [pscustomobject]$reports } function Wait-MsGraphProxyControlApi { <# .SYNOPSIS Waits for Dev Proxy's control API to start responding. .DESCRIPTION Polls the control API (plain HTTP, no TLS involved) until it responds or the timeout elapses. Dev Proxy needs a moment after being started to bind this endpoint, so callers that need to know it's actually up - before fetching its root certificate, for example - poll rather than assume a fixed delay is enough. .PARAMETER ApiPort Port of Dev Proxy's control API. .PARAMETER TimeoutSeconds How long to keep polling before giving up. .EXAMPLE PS C:\> Wait-MsGraphProxyControlApi -ApiPort 8897 Returns $true once the control API responds, or $false after 30 seconds. #> [CmdletBinding()] [OutputType([bool])] param ( [Parameter(Mandatory)] [int] $ApiPort, [int] $TimeoutSeconds = 30 ) $deadline = (Get-Date).AddSeconds($TimeoutSeconds) while ((Get-Date) -lt $deadline) { try { Invoke-RestMethod -Uri "http://127.0.0.1:$ApiPort/proxy" -TimeoutSec 5 | Out-Null return $true } catch { Write-Verbose "Control API not ready yet: $_" Start-Sleep -Seconds 1 } } return $false } function Wait-MsGraphProxyPort { <# .SYNOPSIS Waits for Dev Proxy's actual proxy port to start accepting connections. .DESCRIPTION Waits and checks that the proxy port has started listening and is ready for connections .PARAMETER ProxyPort The proxy's own listening port (not the control API port). .PARAMETER TimeoutSeconds How long to keep polling before giving up. .EXAMPLE PS C:\> Wait-MsGraphProxyPort -ProxyPort 8000 Returns $true once the proxy port accepts a connection, or $false after 30 seconds. #> [CmdletBinding()] [OutputType([bool])] param ( [Parameter(Mandatory)] [int] $ProxyPort, [int] $TimeoutSeconds = 30 ) $deadline = (Get-Date).AddSeconds($TimeoutSeconds) while ((Get-Date) -lt $deadline) { try { $tcpClient = [System.Net.Sockets.TcpClient]::new() try { $tcpClient.Connect('127.0.0.1', $ProxyPort) return $true } finally { $tcpClient.Close() } } catch { Write-Verbose "Proxy port not ready yet: $_" Start-Sleep -Milliseconds 250 } } return $false } function Get-MsGraphProxyStatus { <# .SYNOPSIS Reports whether the Dev Proxy process started by Start-MsGraphProxy is still running. .DESCRIPTION Reads the state file written by Start-MsGraphProxy and checks whether the process it recorded is still alive. Returns an object with: Running - whether the process is currently alive Id - its process ID ConfigFile - the devproxyrc.json it was started with ExePath - path to the Dev Proxy executable ApiPort - its control-API port Recording - whether it's currently recording StartedAt - when it was started If Start-MsGraphProxy was never called (or Stop-MsGraphProxy already cleaned up), all of these are $false/$null. .EXAMPLE PS C:\> Get-MsGraphProxyStatus Returns an object describing whether Dev Proxy is currently running. .LINK https://mynster-it.dk/docs/modules/msgraphProxy/commands/Get-MsGraphProxyStatus #> [CmdletBinding()] param () if (-not (Test-Path -Path $script:MsGraphProxyStateFile)) { return [pscustomobject]@{ Running = $false Id = $null ConfigFile = $null ExePath = $null ApiPort = $null Recording = $false StartedAt = $null } } $state = Get-Content -Path $script:MsGraphProxyStateFile -Raw | ConvertFrom-Json $process = if ($state.Id) { Get-Process -Id $state.Id -ErrorAction SilentlyContinue } [pscustomobject]@{ Running = [bool]$process Id = $state.Id ConfigFile = $state.ConfigFile ExePath = $state.ExePath ApiPort = $state.ApiPort Recording = [bool]$state.Recording StartedAt = $state.StartedAt } } function Install-MsGraphProxy { <# .SYNOPSIS Downloads and installs the self-contained Dev Proxy build this module wraps. .DESCRIPTION Downloads the zipped, self-contained Dev Proxy build (bundled with this module's GraphSchemaMockPlugin and EntraTokenMockPlugin extensions) from this repository's latest GitHub release, and extracts it into the module's local binary cache - no separate DOTNET installation needed on this machine. If a build for the target RID is already cached, this does nothing unless -Force is passed. Start-MsGraphProxy calls this automatically the first time it needs to, so you normally don't need to call it yourself. .PARAMETER Rid The DOTNET runtime identifier to install a build for. Defaults to the RID matching the current operating system. .PARAMETER Force Reinstall even if a build for this RID is already cached. .EXAMPLE PS C:\> Install-MsGraphProxy Downloads and installs the Dev Proxy build matching the current OS. .EXAMPLE PS C:\> Install-MsGraphProxy -Force Re-downloads and reinstalls the Dev Proxy build, replacing whatever is already cached. .LINK https://mynster-it.dk/docs/modules/msgraphProxy/commands/Install-MsGraphProxy #> [CmdletBinding()] param ( [string] $Rid = ($script:MsGraphProxyRid ?? (Get-MsGraphProxyRid)), [switch] $Force ) $ridRoot = Join-Path -Path $script:MsGraphProxyBinRoot -ChildPath $Rid $exeName = if ($IsWindows) { 'devproxy.exe' } else { 'devproxy' } $exePath = Join-Path -Path $ridRoot -ChildPath $exeName if ((Test-Path -Path $exePath) -and -not $Force) { Write-Verbose "Dev Proxy for $Rid is already installed at $ridRoot." return $exePath } $releaseUri = "https://api.github.com/repos/$($script:MsGraphProxyGitHubRepo)/releases/latest" $release = Invoke-RestMethod -Uri $releaseUri -Headers @{ 'User-Agent' = 'msgraphProxy' } $asset = $release.assets | Where-Object Name -Like "*$Rid*.zip" | Select-Object -First 1 if (-not $asset) { throw "No release asset found for $Rid in release $($release.tag_name). Available assets: $($release.assets.name -join ', ')" } $zipPath = Join-Path -Path ([System.IO.Path]::GetTempPath()) -ChildPath $asset.name Invoke-WebRequest -Uri $asset.browser_download_url -OutFile $zipPath -UseBasicParsing if (Test-Path -Path $ridRoot) { Remove-Item -Path $ridRoot -Recurse -Force } New-Item -Path $ridRoot -ItemType Directory -Force | Out-Null Expand-Archive -Path $zipPath -DestinationPath $ridRoot -Force Remove-Item -Path $zipPath -Force if ($IsLinux -or $IsMacOS) { & chmod +x $exePath } Write-Verbose "Installed Dev Proxy $($release.tag_name) for $Rid to $ridRoot." $exePath } function Install-MsGraphProxyCertificate { <# .SYNOPSIS Trusts the running Dev Proxy instance's root CA certificate for the current user. .DESCRIPTION Fetches Dev Proxy's root CA certificate from its control API and trusts it for the current OS user, so HTTPS clients accept the certificates Dev Proxy generates for intercepted requests without any client-side accommodation (like skipping certificate validation). Supported on Windows (via certutil), Linux (via update-ca-certificates) and macOS (via the current user's login keychain). This is best-effort, not guaranteed: trusting a certificate can require an interactive confirmation dialog, which will never resolve in a non-interactive session (most commonly hit via Start-MsGraphProxy -CI). Rather than hang waiting for it, this function waits up to 15 seconds and then returns $false with a warning instead of throwing, so callers can decide for themselves whether to fall back to skipping certificate validation in their own requests. On a genuine interactive desktop session, trust normally succeeds and the confirmation dialog (Windows only) can just be answered. .PARAMETER ApiPort Port of Dev Proxy's control API. .EXAMPLE PS C:\> Install-MsGraphProxyCertificate Fetches and trusts the root certificate of the Dev Proxy instance using the default control-API port. .LINK https://mynster-it.dk/docs/modules/msgraphProxy/commands/Install-MsGraphProxyCertificate #> [CmdletBinding()] [OutputType([bool])] param ( [int] $ApiPort = $script:MsGraphProxyDefaultApiPort ) if (-not $IsWindows -and -not $IsLinux -and -not $IsMacOS) { Write-Warning 'Trusting the Dev Proxy root certificate automatically is only implemented for Windows, Linux and macOS.' return $false } $certPath = Join-Path -Path ([System.IO.Path]::GetTempPath()) -ChildPath 'msgraphproxy-devproxy-ca.crt' try { Invoke-WebRequest -Uri "http://127.0.0.1:$ApiPort/proxy/rootCertificate?format=crt" -OutFile $certPath -TimeoutSec 15 } catch { Write-Warning "Couldn't fetch the Dev Proxy root certificate: $_" return $false } try { if ($IsWindows) { $psi = [System.Diagnostics.ProcessStartInfo]::new('certutil.exe') foreach ($arg in @('-addstore', '-f', '-user', 'Root', $certPath)) { $psi.ArgumentList.Add($arg) } $psi.RedirectStandardOutput = $true $psi.RedirectStandardError = $true $psi.UseShellExecute = $false $process = [System.Diagnostics.Process]::Start($psi) if (-not $process.WaitForExit(15000)) { $process.Kill() Write-Warning 'Trusting the Dev Proxy root certificate timed out, likely waiting on an interactive confirmation prompt that nothing could answer. HTTPS clients may need to skip certificate validation instead.' return $false } if ($process.ExitCode -ne 0) { $errorOutput = $process.StandardError.ReadToEnd() Write-Warning "certutil failed to trust the Dev Proxy root certificate (exit $($process.ExitCode)): $errorOutput" return $false } } elseif ($IsLinux) { $isRoot = (& id -u) -eq '0' $dest = '/usr/local/share/ca-certificates/msgraphproxy-devproxy.crt' if ($isRoot) { Copy-Item -Path $certPath -Destination $dest update-ca-certificates | Out-Null } else { sudo cp $certPath $dest sudo update-ca-certificates | Out-Null } if ($LASTEXITCODE -ne 0) { Write-Warning "update-ca-certificates failed (exit $LASTEXITCODE) to trust the Dev Proxy root certificate." return $false } } else { $keychain = Join-Path -Path $HOME -ChildPath 'Library/Keychains/login.keychain-db' $psi = [System.Diagnostics.ProcessStartInfo]::new('security') foreach ($arg in @('add-trusted-cert', '-r', 'trustRoot', '-k', $keychain, $certPath)) { $psi.ArgumentList.Add($arg) } $psi.RedirectStandardOutput = $true $psi.RedirectStandardError = $true $psi.UseShellExecute = $false $process = [System.Diagnostics.Process]::Start($psi) if (-not $process.WaitForExit(15000)) { $process.Kill() Write-Warning 'Trusting the Dev Proxy root certificate timed out, likely waiting on a keychain authorization prompt that nothing could answer. HTTPS clients may need to skip certificate validation instead.' return $false } if ($process.ExitCode -ne 0) { $errorOutput = $process.StandardError.ReadToEnd() Write-Warning "security failed to trust the Dev Proxy root certificate (exit $($process.ExitCode)): $errorOutput" return $false } } } finally { Remove-Item -Path $certPath -Force -ErrorAction SilentlyContinue } Write-Verbose 'Dev Proxy root certificate trusted.' $true } function Start-MsGraphProxy { <# .SYNOPSIS Starts the self-contained Dev Proxy build in its own process. .DESCRIPTION Launches the cached Dev Proxy executable against a devproxyrc.json configuration, and tracks the resulting process so Stop-MsGraphProxy and Get-MsGraphProxyStatus can find it again later, even from a different PowerShell session. If Dev Proxy hasn't been installed yet for this OS, it's installed automatically first (see Install-MsGraphProxy). Recording starts automatically with the proxy, so Stop-MsGraphProxy can stop it again and return the resulting reports (such as minimal Graph permissions) as an object. Pass -NoRecord to opt out. While running, Dev Proxy intercepts and mocks calls to the hosts listed in its "urlsToWatch" configuration (Microsoft Graph and the Entra ID token endpoint, by default), tunnelling everything else through untouched. .PARAMETER ConfigFile Path to a devproxyrc.json/.yaml configuration file. Defaults to the configuration bundled with this module. .PARAMETER ApiPort Port for Dev Proxy's control API, used by Stop-MsGraphProxy for a graceful shutdown. Defaults to Dev Proxy's own default port, 8897. .PARAMETER NoRecord Don't start recording automatically. Without this switch, Dev Proxy starts recording immediately so Stop-MsGraphProxy has something to stop and report on. .PARAMETER Force Start a new instance even if one is already tracked as running. .PARAMETER CI Configure Dev Proxy for a non-interactive session (CI pipelines, Pester runs, etc.) instead of normal interactive use: sets HTTP_PROXY/HTTPS_PROXY for the current session so Graph calls route through the proxy, and trusts Dev Proxy's root certificate automatically on a best-effort basis (see the returned object's CertificateTrusted property, and Install-MsGraphProxyCertificate's help for what "best-effort" means). .PARAMETER EntraIDLicense Which Entra ID license tier the mocked tenant's subscribedSkus should report - Free, P1, P2 or Governance. Defaults to P2, so license-gated checks (e.g. Maester's Get-MtLicenseInformation) see a licensed tenant out of the box. Pass -EntraIDLicense explicitly to pick a different tier. .PARAMETER WhatIf If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. .PARAMETER Confirm If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. .EXAMPLE PS C:\> Start-MsGraphProxy Starts Dev Proxy using the configuration bundled with this module, recording from the start. .EXAMPLE PS C:\> Start-MsGraphProxy -ConfigFile 'C:\proxy\devproxyrc.json' -ApiPort 9000 Starts Dev Proxy with a custom configuration and control-API port. .EXAMPLE PS C:\> Start-MsGraphProxy -CI Starts Dev Proxy configured for a CI pipeline: no certificate prompt to block startup, HTTP_PROXY/HTTPS_PROXY set for the current process, and its root certificate trusted automatically where possible. .LINK https://mynster-it.dk/docs/modules/msgraphProxy/commands/Start-MsGraphProxy #> [CmdletBinding(SupportsShouldProcess)] param ( [string] $ConfigFile = $script:MsGraphProxyDefaultConfigFile, [int] $ApiPort = $script:MsGraphProxyDefaultApiPort, [switch] $NoRecord, [switch] $Force, [switch] $CI, [ValidateSet('Free', 'P1', 'P2', 'Governance')] [string] $EntraIDLicense = 'P2' ) $existing = Get-MsGraphProxyStatus if ($existing.Running -and -not $Force) { Write-Warning "Dev Proxy is already running (PID $($existing.Id)). Use -Force to start another instance anyway." return $existing } if (-not (Test-Path -Path $ConfigFile)) { throw "Config file not found: $ConfigFile. Pass -ConfigFile explicitly, or run Install-MsGraphProxy if this is the bundled default." } $resolvedConfigFile = (Resolve-Path -Path $ConfigFile).Path if (-not $PSCmdlet.ShouldProcess('Dev Proxy', 'Start')) { return } try { $exePath = Get-MsGraphProxyExePath } catch { Write-Verbose 'Dev Proxy is not installed yet; installing it now.' $exePath = Install-MsGraphProxy } $proxyPort = 8000 if ($CI -or $PSBoundParameters.ContainsKey('EntraIDLicense')) { $licensePreset = Get-MsGraphProxyEntraIDLicensePreset -License $EntraIDLicense $derivedConfig = New-MsGraphProxyCIConfigFile -ConfigFile $resolvedConfigFile -CI:$CI -SubscribedSkus $licensePreset $resolvedConfigFile = $derivedConfig.ConfigFile $proxyPort = $derivedConfig.ProxyPort } else { $rawConfig = Get-Content -Raw -Path $resolvedConfigFile | ConvertFrom-Json -AsHashtable if ($rawConfig.ContainsKey('port')) { $proxyPort = [int]$rawConfig['port'] } } $processArgs = @('--config-file', "`"$resolvedConfigFile`"", '--api-port', $ApiPort) if (-not $NoRecord) { $processArgs += '--record' } if (Test-Path -Path $script:MsGraphProxyStdOutLog) { Remove-Item -Path $script:MsGraphProxyStdOutLog -Force } if (Test-Path -Path $script:MsGraphProxyStdErrLog) { Remove-Item -Path $script:MsGraphProxyStdErrLog -Force } $params = @{ FilePath = $exePath ArgumentList = $processArgs WorkingDirectory = $(Split-Path -Path $exePath -Parent) PassThru = $true RedirectStandardOutput = $script:MsGraphProxyStdOutLog RedirectStandardError = $script:MsGraphProxyStdErrLog } $process = Start-Process @params [pscustomobject]@{ Id = $process.Id ConfigFile = $resolvedConfigFile ExePath = $exePath ApiPort = $ApiPort Recording = -not $NoRecord StartedAt = (Get-Date).ToString('o') } | ConvertTo-Json | Set-Content -Path $script:MsGraphProxyStateFile Write-Verbose "Dev Proxy started (PID $($process.Id)) using $resolvedConfigFile" $result = Get-MsGraphProxyStatus $ready = Wait-MsGraphProxyControlApi -ApiPort $ApiPort if ($ready) { $ready = Wait-MsGraphProxyPort -ProxyPort $proxyPort } if (-not $ready) { Write-Warning 'Dev Proxy did not become ready to serve requests in time.' } if ($CI) { $proxyUri = "http://127.0.0.1:$proxyPort" foreach ($name in 'HTTP_PROXY', 'HTTPS_PROXY', 'http_proxy', 'https_proxy') { [System.Environment]::SetEnvironmentVariable($name, $proxyUri, 'Process') } if ($env:GITHUB_ENV) { foreach ($name in 'HTTP_PROXY', 'HTTPS_PROXY', 'http_proxy', 'https_proxy') { Add-Content -Path $env:GITHUB_ENV -Value "$name=$proxyUri" } } [System.Net.Http.HttpClient]::DefaultProxy = [System.Net.WebProxy]::new($proxyUri) $certificateTrusted = $false if ($ready) { $certificateTrusted = Install-MsGraphProxyCertificate -ApiPort $ApiPort } else { Write-Warning 'Skipping automatic certificate trust since Dev Proxy never became ready.' } $result = $result | Add-Member -NotePropertyName CertificateTrusted -NotePropertyValue $certificateTrusted -PassThru } $result } function Stop-MsGraphProxy { <# .SYNOPSIS Stops the Dev Proxy process started by Start-MsGraphProxy. .DESCRIPTION If Dev Proxy is recording, first stops the recording through its control API. That triggers its reporting plugins (Graph minimal permissions, execution summary) to analyze what was recorded; their results are returned as part of the result object, under Recording. Then asks Dev Proxy to shut down gracefully through its control API. If it doesn't stop within -TimeoutSeconds, the process is force-killed instead, and any Windows system-proxy registration is cleared manually (a graceful shutdown does this on its own). .PARAMETER TimeoutSeconds How long to wait for a graceful shutdown before falling back to killing the process outright. .PARAMETER WhatIf If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. .PARAMETER Confirm If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. .EXAMPLE PS C:\> Stop-MsGraphProxy Stops the running Dev Proxy instance and returns any recorded reports. .LINK https://mynster-it.dk/docs/modules/msgraphProxy/commands/Stop-MsGraphProxy #> [CmdletBinding(SupportsShouldProcess)] param ( [int] $TimeoutSeconds = 10 ) $status = Get-MsGraphProxyStatus if (-not $status.Running) { Write-Warning 'Dev Proxy is not running.' Remove-Item -Path $script:MsGraphProxyStateFile -Force -ErrorAction SilentlyContinue Clear-MsGraphProxySystemProxy return } if (-not $PSCmdlet.ShouldProcess("Dev Proxy (PID $($status.Id))", 'Stop')) { return } $apiPort = $status.ApiPort if (-not $apiPort) { $apiPort = $script:MsGraphProxyDefaultApiPort } $recording = $null if ($status.Recording -and $status.ExePath) { $recording = Receive-MsGraphProxyRecording -ApiPort $apiPort -WorkingDirectory (Split-Path -Path $status.ExePath -Parent) } $stoppedGracefully = $false try { Invoke-RestMethod -Method Post -Uri "http://127.0.0.1:$apiPort/proxy/stopProxy" -TimeoutSec 5 | Out-Null $deadline = (Get-Date).AddSeconds($TimeoutSeconds) while ((Get-Date) -lt $deadline) { if (-not (Get-Process -Id $status.Id -ErrorAction SilentlyContinue)) { $stoppedGracefully = $true break } Start-Sleep -Milliseconds 250 } } catch { Write-Verbose "Graceful stop via the API failed: $_" } if (-not $stoppedGracefully) { Write-Warning "Dev Proxy didn't stop gracefully via its API; forcing termination and clearing the Windows system proxy." Stop-Process -Id $status.Id -Force -ErrorAction SilentlyContinue Clear-MsGraphProxySystemProxy } Remove-Item -Path $script:MsGraphProxyStateFile -Force -ErrorAction SilentlyContinue Write-Verbose "Dev Proxy (PID $($status.Id)) stopped." [pscustomobject]@{ Id = $status.Id StoppedAt = (Get-Date).ToString('o') Recording = $recording } } # Commands run on module import go here # E.g. Argument Completers could be placed here # Module-wide variables $script:MsGraphProxyConfigRoot = Join-Path -Path $script:ModuleRoot -ChildPath 'config' $script:MsGraphProxyDefaultConfigFile = Join-Path -Path $script:MsGraphProxyConfigRoot -ChildPath 'devproxyrc.json' $dataRoot = if ($IsWindows) { $env:LOCALAPPDATA } elseif ($env:XDG_DATA_HOME) { $env:XDG_DATA_HOME } else { Join-Path -Path $HOME -ChildPath '.local/share' } $script:MsGraphProxyBinRoot = Join-Path -Path $dataRoot -ChildPath 'msgraphProxy' -AdditionalChildPath 'bin' $script:MsGraphProxyStateFile = Join-Path -Path ([System.IO.Path]::GetTempPath()) -ChildPath 'msgraphproxy-module-state.json' $script:MsGraphProxyStdOutLog = Join-Path -Path ([System.IO.Path]::GetTempPath()) -ChildPath 'msgraphproxy-devproxy-stdout.log' $script:MsGraphProxyStdErrLog = Join-Path -Path ([System.IO.Path]::GetTempPath()) -ChildPath 'msgraphproxy-devproxy-stderr.log' $script:MsGraphProxyDefaultApiPort = 8897 $script:MsGraphProxyGitHubRepo = 'Mynster9361/msgraphProxy' $script:MsGraphProxyRid = try { Get-MsGraphProxyRid } catch { $null } Export-ModuleMember -Function 'Get-MsGraphProxyStatus','Install-MsGraphProxy','Install-MsGraphProxyCertificate','Start-MsGraphProxy','Stop-MsGraphProxy' |