Skip to content
Merged
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
22 changes: 20 additions & 2 deletions Public/Invoke-IntuneWin32AppTest.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An else rather than a second independent check, so a command line such as powershell.exe -File install.ps1 keeps its one warning and a batch file is only scanned when the command line itself said nothing. Two warnings for the same host would read as two problems.

# 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] }

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The first token is taken from the command line as the portal would see it: a quoted path ("install.cmd" /quiet) or the bare first word. No attempt at full cmd.exe parsing; anything more exotic falls through to no warning, which is the previous behaviour.

else { ($CommandLine.Trim() -split '\s+', 2)[0] }
$batch = if ($first -match '(?i)\.(cmd|bat)$') { Join-Path -Path $source -ChildPath $first }

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only .cmd and .bat are scanned, because those are text the harness can read. A setup.exe or a .vbs wrapper that launches powershell.exe is out of reach, and Join-Path with the source folder keeps a .\install.cmd spelling working as well as a bare name.

else { $null }
if ($batch -and (Test-Path -LiteralPath $batch -PathType Leaf)) {
$hit = Select-String -LiteralPath $batch -Pattern $hostPattern | Select-Object -First 1

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First hit only: one warning per batch file, naming the first line so the reader can find it, rather than one per call. The pattern is the same one applied to the command line, so a powershell.exe token inside a quoted string or at line start is matched the same way in both places.

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) {
Expand Down
29 changes: 29 additions & 0 deletions Tests/Unit/Public/Invoke-IntuneWin32AppTest.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The negative case uses the quoted-with-arguments spelling so it exercises the first-token parsing, not just the absence of a powershell line: the file must be found and scanned, and still produce nothing.

}
$result = Invoke-IntuneWin32AppTest @intuneWin32AppTestSplat
$result.Warnings -join ' ' | Should-NotBeLikeString '*32-bit host*'
}

It 'passes -EnforceSignatureCheck to every detection run' {
$intuneWin32AppTestSplat = @{
DetectionPath = $script:Fixture.Detection
Expand Down
3 changes: 2 additions & 1 deletion docs/IntuneScriptLab/Invoke-IntuneWin32AppTest.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion en-US/IntuneScriptLab-Help.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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.</maml:para>
</maml:description>
<command:syntax>
Expand Down
Loading