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.