Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/powershell/ZeroTrustAssessment.psd1
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ FunctionsToExport = 'Connect-ZtAssessment', 'Disconnect-ZtAssessment',
'Get-ZtTestStatistics', 'Invoke-ZtAssessment',
'Invoke-ZtGraphRequest', 'Invoke-ZtAzureRequest',
'Invoke-ZtAzureResourceGraphRequest', 'Clear-ZtRequiredModule',
'Get-ZtCurrentLicense'
'Get-ZtCurrentLicense', 'Stop-ZtReportServer'

# Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export.
CmdletsToExport = @()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
function Install-ZtBrowserUrlCapture {
<#
.SYNOPSIS
Installs a temporary xdg-open/open wrapper that captures the auth URL MSAL passes to the browser.
.DESCRIPTION
In container environments, MSAL opens the browser via xdg-open (Linux) or open (macOS) with a
URL containing PKCE code_challenge, state, and a dynamic localhost port. This function creates
a thin wrapper script that logs the URL to a temp file before forwarding to the real browser
opener, enabling a clickable fallback link to be displayed in the terminal.
.OUTPUTS
[hashtable] State needed by Start-ZtBrowserUrlWatcher and Remove-ZtBrowserUrlCapture,
or $null if the wrapper could not be installed.
#>
[CmdletBinding()]
[OutputType([hashtable])]
param ()

if (-not ($IsLinux -or $IsMacOS)) { return $null }

$browserCmd = if ($IsLinux) { 'xdg-open' } else { 'open' }
$resolvedCmd = Get-Command -Name $browserCmd -ErrorAction Ignore
if (-not $resolvedCmd) {
Write-PSFMessage -Message "Cannot install URL capture: '$browserCmd' not found on PATH." -Level Debug
return $null
}
$realBrowserPath = $resolvedCmd.Source

$authUrlFile = [System.IO.Path]::GetTempFileName()
$wrapperDirName = "zt-browser-wrapper-$([System.Guid]::NewGuid().ToString('N'))"
$wrapperDir = Join-Path -Path ([System.IO.Path]::GetTempPath()) -ChildPath $wrapperDirName
$null = New-Item -Path $wrapperDir -ItemType Directory -Force
$wrapperPath = Join-Path -Path $wrapperDir -ChildPath $browserCmd

# Wrapper: save the URL to the temp file, then call the real browser opener
$wrapperContent = @"
#!/bin/bash
echo "`$1" > "$authUrlFile"
exec "$realBrowserPath" "`$@"
"@
Set-Content -Path $wrapperPath -Value $wrapperContent -Force
chmod +x $wrapperPath

# Prepend wrapper dir to PATH so MSAL finds our wrapper first
$env:PATH = "${wrapperDir}:$($env:PATH)"

Write-PSFMessage -Message "Installed browser URL capture wrapper at $wrapperPath (real: $realBrowserPath)." -Level Debug

return @{
AuthUrlFile = $authUrlFile
WrapperPath = $wrapperPath
WrapperDir = $wrapperDir
RealBrowserPath = $realBrowserPath
}
}
43 changes: 43 additions & 0 deletions src/powershell/private/utility/browser/Open-ZtFile.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
function Open-ZtFile {
<#
.SYNOPSIS
Opens a file or URL in the default application, with container-aware fallback.
.DESCRIPTION
On Linux, uses xdg-open. On macOS, uses open. On Windows, uses Invoke-Item.
Returns $true if the open command ran without error.
.PARAMETER Path
The file path or URL to open.
.OUTPUTS
[bool] True if the open command ran successfully.
#>
[CmdletBinding()]
[OutputType([bool])]
param (
[Parameter(Mandatory)]
[string] $Path
)

if ($IsLinux -or $IsMacOS) {
$openCmd = if ($IsLinux) { 'xdg-open' } else { 'open' }
if (Get-Command -Name $openCmd -ErrorAction Ignore) {
try {
& $openCmd $Path 2>&1 | Out-Null
return $true
}
catch {
Write-PSFMessage -Message "Failed to open with ${openCmd}: $_" -Level Debug
}
}
}
else {
try {
Invoke-Item $Path | Out-Null
return $true
}
catch {
Write-PSFMessage -Message "Failed to open with Invoke-Item: $_" -Level Debug
}
}

return $false
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
function Remove-ZtBrowserUrlCapture {
<#
.SYNOPSIS
Cleans up the browser URL capture wrapper, temp file, and background watcher.
.DESCRIPTION
Reverses the PATH modification, removes the wrapper directory and temp file,
and disposes the background PowerShell watcher. Call this in a finally block
after Connect-MgGraph completes.
.PARAMETER CaptureState
The hashtable returned by Install-ZtBrowserUrlCapture. May be $null (no-op).
.PARAMETER Watcher
The PowerShell instance returned by Start-ZtBrowserUrlWatcher. May be $null (no-op).
#>
[CmdletBinding()]
param (
[AllowNull()]
[hashtable] $CaptureState,

[AllowNull()]
[powershell] $Watcher
)

if ($CaptureState) {
if ($CaptureState.WrapperDir) {
$env:PATH = ($env:PATH -split ':' | Where-Object { $_ -ne $CaptureState.WrapperDir }) -join ':'
Remove-Item -Path $CaptureState.WrapperDir -Recurse -Force -ErrorAction Ignore
}
if ($CaptureState.AuthUrlFile) {
Remove-Item -Path $CaptureState.AuthUrlFile -Force -ErrorAction Ignore
}
Write-PSFMessage -Message "Removed browser URL capture wrapper." -Level Debug
}

if ($Watcher) {
$Watcher.Dispose()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
function Start-ZtBrowserUrlWatcher {
<#
.SYNOPSIS
Starts a background PowerShell instance that watches for the captured auth URL and prints it.
.DESCRIPTION
After Install-ZtBrowserUrlCapture sets up the wrapper, call this function before
Connect-MgGraph to start a background watcher. When MSAL invokes xdg-open, the wrapper
writes the URL to a temp file, and the watcher detects it and prints it to the console
as a clickable fallback link.
.PARAMETER CaptureState
The hashtable returned by Install-ZtBrowserUrlCapture.
.OUTPUTS
[powershell] The background PowerShell instance (dispose it during cleanup).
#>
[CmdletBinding()]
[OutputType([powershell])]
param (
[Parameter(Mandatory)]
[hashtable] $CaptureState
)

$watcher = [powershell]::Create()
$null = $watcher.AddScript({
param($authUrlFile)
for ($i = 0; $i -lt 100; $i++) {
Start-Sleep -Milliseconds 100
if ((Test-Path $authUrlFile) -and (Get-Item $authUrlFile).Length -gt 0) {
$capturedUrl = (Get-Content -Path $authUrlFile -Raw).Trim()
if ($capturedUrl -match '^https://') {
[Console]::WriteLine(" If the browser did not open, copy and paste this link:")
[Console]::WriteLine(" `u{1f517} $capturedUrl")
}
break
}
}
}).AddArgument($CaptureState.AuthUrlFile)
$null = $watcher.BeginInvoke()

return $watcher
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
function Get-ZtContainerAuthFailureHint {
<#
.SYNOPSIS
Returns a user-friendly hint when Graph auth succeeds but no context is established.
.DESCRIPTION
When Connect-MgGraph completes without error but Get-MgContext returns null,
this typically means the MSAL callback (browser redirect to localhost) did not
reach the container. This function returns an appropriate message based on
whether the session is running in a container.
.OUTPUTS
[string] A hint message suggesting next steps.
#>
[CmdletBinding()]
[OutputType([string])]
param ()

$msg = "Connect-MgGraph completed but no auth context was established."

$authReadiness = $script:ZtContainerAuthReadiness
if ($authReadiness -and $authReadiness.IsContainer) {
$msg += " The browser authentication callback may not have reached this container. Try using device code flow: Connect-ZtAssessment -UseDeviceCode"
}

return $msg
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
function Initialize-ZtContainerBrowserShim {
<#
.SYNOPSIS
Ensures xdg-open is available for MSAL's browser launch in container environments.
.OUTPUTS
[bool] True if a browser helper is available.
#>
[CmdletBinding()]
[OutputType([bool])]
param ()

# Reset shim tracking state
$script:ZtBrowserShimState = $null

$xdgOpen = Get-Command -Name 'xdg-open' -ErrorAction Ignore

if ($xdgOpen) {
Write-Host -Object ' ✅ Browser helper: xdg-open available.' -ForegroundColor Green
Write-PSFMessage -Message "xdg-open found at '$($xdgOpen.Source)'." -Level Debug
return $true
}

# Try $VSCODE_BROWSER first (set by VS Code remote), then $BROWSER
$browserHelper = $null
foreach ($envVar in @('VSCODE_BROWSER', 'BROWSER')) {
$candidate = [System.Environment]::GetEnvironmentVariable($envVar)
if (-not $candidate) { continue }

# The variable may be a full path, a bare command name, or include arguments.
# Extract the first token (the executable) for resolution.
$tokens = $candidate -split '\s+', 2
$exePath = $tokens[0]

# Resolve via Get-Command (handles both paths and command names on PATH)
$resolved = Get-Command -Name $exePath -ErrorAction Ignore
if ($resolved) {
$remainder = if ($tokens.Count -gt 1) { ' ' + $tokens[1] } else { '' }
$browserHelper = "$($resolved.Source)$remainder"
break
}
else {
Write-PSFMessage -Message "Browser helper from `$$envVar ('$exePath') not found on PATH." -Level Debug
}
}

if ($browserHelper) {
Write-PSFMessage -Message "xdg-open not found. Creating shim from browser helper '$browserHelper'." -Level Verbose

# Split the browser helper into executable and arguments so the shim
# works correctly when $browserHelper contains spaces or extra args.
$helperTokens = $browserHelper -split '\s+', 2
$helperExe = $helperTokens[0]
$helperArgs = if ($helperTokens.Count -gt 1) { $helperTokens[1] } else { '' }

$shimContent = @"
#!/bin/sh
exec "$helperExe" $helperArgs "`$@"
"@

try {
$shimPath = '/usr/local/bin/xdg-open'
$shimContent | & sudo -n tee $shimPath > $null 2>&1
& sudo -n chmod +x $shimPath 2>&1 > $null
}
catch {
Write-PSFMessage -Message "Failed to create system xdg-open shim: $_" -Level Debug
}

if (Get-Command -Name 'xdg-open' -ErrorAction Ignore) {
Write-Host -Object ' ✅ Browser helper: created xdg-open shim.' -ForegroundColor Green
$script:ZtBrowserShimState = @{ ShimPath = $shimPath; AddedPathEntry = $null }
return $true
}

try {
$userBinDir = Join-Path -Path $HOME -ChildPath '.local/bin'
$userShimPath = Join-Path -Path $userBinDir -ChildPath 'xdg-open'

if (-not (Test-Path -Path $userBinDir)) {
$null = New-Item -Path $userBinDir -ItemType Directory -Force
}

Set-Content -Path $userShimPath -Value $shimContent -NoNewline
& chmod +x $userShimPath 2>&1 > $null

$addedPathEntry = $null
$pathEntries = $env:PATH -split ':'
if ($pathEntries -notcontains $userBinDir) {
$env:PATH = "${userBinDir}:$($env:PATH)"
$addedPathEntry = $userBinDir
}
}
catch {
Write-PSFMessage -Message "Failed to create user xdg-open shim: $_" -Level Debug
}

if (Get-Command -Name 'xdg-open' -ErrorAction Ignore) {
Write-Host -Object ' ✅ Browser helper: created xdg-open shim.' -ForegroundColor Green
$script:ZtBrowserShimState = @{ ShimPath = $userShimPath; AddedPathEntry = $addedPathEntry }
return $true
}

Write-Host -Object " ⚠️ Browser helper: could not create xdg-open shim, and `$BROWSER alone may not be sufficient." -ForegroundColor Yellow
return $false
}

Write-Host -Object ' ❌ Browser helper: no xdg-open, $VSCODE_BROWSER, or $BROWSER available.' -ForegroundColor Red
return $false
}
Loading