From 4aca1e6ad56f10c096518dff57d89b41a32dc04c Mon Sep 17 00:00:00 2001 From: fadwen <110697945+fadwen@users.noreply.github.com> Date: Thu, 8 Oct 2026 13:21:31 -0700 Subject: [PATCH] fix(win32): warn about powershell.exe inside the batch file an install command names Invoke-IntuneWin32AppTest warned about the 32-bit host only when "powershell" appeared on the install or uninstall command line. A powershell.exe call inside the batch file the command line names, the common "install.cmd" wrapper, runs 32-bit just the same from the agent's 32-bit cmd.exe, so HKLM:\SOFTWARE and Program Files are redirected there without the command line showing it; the harness reproduced the miss (the post-install detection failed) but said nothing about why. When the command line itself has no powershell call and its first token is a .cmd or .bat file in the content folder, the file is scanned for the same pattern and the warning names the file and line. The help's launch paragraph mentions both places the warning comes from. --- CHANGELOG.md | 4 +++ Public/Invoke-IntuneWin32AppTest.ps1 | 22 ++++++++++++-- .../Invoke-IntuneWin32AppTest.Tests.ps1 | 29 +++++++++++++++++++ .../Invoke-IntuneWin32AppTest.md | 3 +- en-US/IntuneScriptLab-Help.xml | 3 +- 5 files changed, 57 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5eec5de..9192f2e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,10 @@ release notes. `NoDefaultCurrentDirectoryInExePath` is set in the caller's environment, a per-user shell setting that stops `cmd.exe` looking in its working folder. The agent's `cmd.exe` runs without the caller's profile, so the harness now starts its child processes without the variable. +- `Invoke-IntuneWin32AppTest` warned about the 32-bit host only when `powershell` appeared on the + install or uninstall command line. A `powershell.exe` call inside the batch file the command + line names (`install.cmd`) runs 32-bit just the same, from the agent's 32-bit `cmd.exe`, and now + gets the same warning, naming the file and line. ## [0.29.0] - 2026-10-07 diff --git a/Public/Invoke-IntuneWin32AppTest.ps1 b/Public/Invoke-IntuneWin32AppTest.ps1 index 85c850a..f5fd711 100644 --- a/Public/Invoke-IntuneWin32AppTest.ps1 +++ b/Public/Invoke-IntuneWin32AppTest.ps1 @@ -132,11 +132,29 @@ function Invoke-IntuneWin32AppTest { # cmd.exe, as the agent's own 32-bit process does function Invoke-ContentCommand { param([string]$Phase, [string]$Content, [string]$CommandLine) - if ($CommandLine -match '(?i)(^|[\s"])powershell(\.exe)?([\s"]|$)') { + $source = (Resolve-Path -LiteralPath $Content).ProviderPath + $hostPattern = '(?i)(^|[\s"])powershell(\.exe)?([\s"]|$)' + if ($CommandLine -match $hostPattern) { $warnings.Add("powershell.exe in the $Phase command runs the 32-bit host (the agent is a 32-bit " + 'process); HKLM:\SOFTWARE and Program Files are redirected there') } - $source = (Resolve-Path -LiteralPath $Content).ProviderPath + else { + # The same host hides inside a batch file the command line names: install.cmd calling + # powershell.exe runs it 32-bit too, from the agent's 32-bit cmd.exe, without the + # command line showing it + $first = if ($CommandLine -match '^\s*"([^"]+)"') { $Matches[1] } + else { ($CommandLine.Trim() -split '\s+', 2)[0] } + $batch = if ($first -match '(?i)\.(cmd|bat)$') { Join-Path -Path $source -ChildPath $first } + else { $null } + if ($batch -and (Test-Path -LiteralPath $batch -PathType Leaf)) { + $hit = Select-String -LiteralPath $batch -Pattern $hostPattern | Select-Object -First 1 + if ($hit) { + $warnings.Add("powershell.exe in $first (line $($hit.LineNumber)), which the $Phase " + + 'command runs through the 32-bit cmd.exe, is the 32-bit host (the agent is a 32-bit ' + + 'process); HKLM:\SOFTWARE and Program Files are redirected there') + } + } + } $runId = [guid]::NewGuid().ToString('N') # Another account cannot reach the caller's temp folder; its copy lives under ProgramData $cache = if ($Credential) { diff --git a/Tests/Unit/Public/Invoke-IntuneWin32AppTest.Tests.ps1 b/Tests/Unit/Public/Invoke-IntuneWin32AppTest.Tests.ps1 index 2eaf773..d135eb5 100644 --- a/Tests/Unit/Public/Invoke-IntuneWin32AppTest.Tests.ps1 +++ b/Tests/Unit/Public/Invoke-IntuneWin32AppTest.Tests.ps1 @@ -217,6 +217,35 @@ Describe 'Invoke-IntuneWin32AppTest' -Tag 'Unit', 'Public' { $result.Warnings -join ' ' | Should-BeLikeString '*powershell.exe*32-bit host*' } + It 'warns about powershell.exe inside the batch file the install command names' { + Set-Content -Path (Join-Path $script:Fixture.Content 'install.cmd') -Encoding ascii -Value @( + '@echo off' + 'powershell.exe -NoProfile -ExecutionPolicy Bypass -File install.ps1' + ) + $intuneWin32AppTestSplat = @{ + DetectionPath = $script:Fixture.Detection + ContentPath = $script:Fixture.Content + InstallCommand = 'install.cmd' + } + $result = Invoke-IntuneWin32AppTest @intuneWin32AppTestSplat + $result.Warnings -join ' ' | + Should-BeLikeString '*powershell.exe in install.cmd (line 2)*32-bit host*' + } + + It 'does not warn when the batch file the install command names has no powershell call' { + Set-Content -Path (Join-Path $script:Fixture.Content 'install.cmd') -Encoding ascii -Value @( + '@echo off' + 'setup.exe /S' + ) + $intuneWin32AppTestSplat = @{ + DetectionPath = $script:Fixture.Detection + ContentPath = $script:Fixture.Content + InstallCommand = '"install.cmd" /quiet' + } + $result = Invoke-IntuneWin32AppTest @intuneWin32AppTestSplat + $result.Warnings -join ' ' | Should-NotBeLikeString '*32-bit host*' + } + It 'passes -EnforceSignatureCheck to every detection run' { $intuneWin32AppTestSplat = @{ DetectionPath = $script:Fixture.Detection diff --git a/docs/IntuneScriptLab/Invoke-IntuneWin32AppTest.md b/docs/IntuneScriptLab/Invoke-IntuneWin32AppTest.md index 4945b7d..c1574e1 100644 --- a/docs/IntuneScriptLab/Invoke-IntuneWin32AppTest.md +++ b/docs/IntuneScriptLab/Invoke-IntuneWin32AppTest.md @@ -73,7 +73,8 @@ Launch shape, from the observations: the detection script runs in the 64-bit hos default (Win32 default) via the same launch as Invoke-IntuneDetectionTest; the install command runs through a 32-bit cmd.exe, so a bare `powershell.exe` resolves to the x86 host exactly as it does under the agent's own 32-bit process, with the content copy as -working directory. +working directory. A `powershell` call on the command line, or inside the batch file the +command line names, adds a warning about the 32-bit host. Soft/hard reboot codes count as success but are reported. ## EXAMPLES diff --git a/en-US/IntuneScriptLab-Help.xml b/en-US/IntuneScriptLab-Help.xml index 203ea69..f33f4f3 100644 --- a/en-US/IntuneScriptLab-Help.xml +++ b/en-US/IntuneScriptLab-Help.xml @@ -3694,7 +3694,8 @@ Launch shape, from the observations: the detection script runs in the 64-bit hos default (Win32 default) via the same launch as Invoke-IntuneDetectionTest; the install command runs through a 32-bit cmd.exe, so a bare `powershell.exe` resolves to the x86 host exactly as it does under the agent's own 32-bit process, with the content copy as -working directory. +working directory. A `powershell` call on the command line, or inside the batch file the +command line names, adds a warning about the 32-bit host. Soft/hard reboot codes count as success but are reported.