From 8b4dccdb33e54bc441e6e56ca3c73eb28a241d83 Mon Sep 17 00:00:00 2001 From: fadwen <110697945+fadwen@users.noreply.github.com> Date: Thu, 8 Oct 2026 13:31:39 -0700 Subject: [PATCH] feat(win32): default -Architecture to the device's 64-bit host Invoke-IntuneDetectionTest, Invoke-IntuneRequirementTest and Invoke-IntuneWin32AppTest defaulted -Architecture to x64, which Windows on ARM refuses: there is no x64 PowerShell host there, and the agent runs Win32 detection and requirement scripts in the native ARM64 host when the rule's "run as 32-bit" option is off (Findings, "Windows on ARM"). Every example in the help had to be given -Architecture arm64 by hand on such a device. The parameter now has no default. Left out, the commands resolve it with Get-IslHostArchitecture, a new private helper that returns arm64 on an ARM64 device and x64 elsewhere, which is the host the agent would use. Explicit x64 and arm64 keep naming one host each and are still refused on the other CPU; the result's Architecture field reports the host that ran. The three tests that asserted the x64 default now expect the device's own 64-bit host, so the Windows on ARM job exercises the same assertion. --- CHANGELOG.md | 9 ++++++ Private/Get-IslHostArchitecture.ps1 | 27 +++++++++++++++++ Public/Invoke-IntuneDetectionTest.ps1 | 6 +++- Public/Invoke-IntuneRequirementTest.ps1 | 6 +++- Public/Invoke-IntuneWin32AppTest.ps1 | 6 +++- .../Private/Get-IslHostArchitecture.Tests.ps1 | 30 +++++++++++++++++++ .../Invoke-IntuneDetectionTest.Tests.ps1 | 13 ++++++++ .../Invoke-IntuneRequirementTest.Tests.ps1 | 8 +++-- .../Invoke-IntuneWin32AppTest.Tests.ps1 | 8 +++-- .../Invoke-IntuneDetectionTest.md | 9 +++--- .../Invoke-IntuneRequirementTest.md | 9 +++--- .../Invoke-IntuneWin32AppTest.md | 4 +-- en-US/IntuneScriptLab-Help.xml | 16 +++++----- 13 files changed, 125 insertions(+), 26 deletions(-) create mode 100644 Private/Get-IslHostArchitecture.ps1 create mode 100644 Tests/Unit/Private/Get-IslHostArchitecture.Tests.ps1 diff --git a/CHANGELOG.md b/CHANGELOG.md index 9192f2e..42e9425 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,15 @@ release notes. ## [Unreleased] +### Changed + +- `Invoke-IntuneDetectionTest`, `Invoke-IntuneRequirementTest` and `Invoke-IntuneWin32AppTest` no + longer default `-Architecture` to `x64`, which Windows on ARM refuses because there is no x64 + host there. Left out, the architecture is the device's 64-bit host: `x64` on an x64 device, + `arm64` on Windows on ARM, the host the agent uses for Win32 detection and requirement scripts + with "run as 32-bit" off. `-Architecture x64` and `arm64` still name one host each and are + still refused on the other CPU; the result's `Architecture` says which host ran. + ### Fixed - A Win32 install or uninstall command named as a bare batch file (`install.cmd`) failed with diff --git a/Private/Get-IslHostArchitecture.ps1 b/Private/Get-IslHostArchitecture.ps1 new file mode 100644 index 0000000..e2ff1ae --- /dev/null +++ b/Private/Get-IslHostArchitecture.ps1 @@ -0,0 +1,27 @@ +function Get-IslHostArchitecture { + <# + .SYNOPSIS + The architecture of this device's 64-bit Windows PowerShell host: x64, or arm64 on Windows on ARM. + + .DESCRIPTION + The agent runs Win32 detection and requirement scripts in the device's 64-bit host unless + the rule's "run as 32-bit" option is on. On Windows on ARM that host is the native ARM64 + one and there is no x64 PowerShell at all (Findings, "Windows on ARM"), so the harness + commands use this as their default architecture instead of a fixed x64 that an ARM64 + device refuses. The 32-bit host (x86) exists on both and is never the answer here. + + .EXAMPLE + Get-IslHostArchitecture + + x64 on an x64 device, arm64 on a Windows on ARM device. + + .OUTPUTS + System.String. x64 or arm64, as Get-IslHostPath and the -Architecture parameters name them. + #> + [CmdletBinding()] + [OutputType([string])] + param() + + $osArchitecture = "$([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture)" + if ($osArchitecture -eq 'Arm64') { 'arm64' } else { 'x64' } +} diff --git a/Public/Invoke-IntuneDetectionTest.ps1 b/Public/Invoke-IntuneDetectionTest.ps1 index 172adff..7809aae 100644 --- a/Public/Invoke-IntuneDetectionTest.ps1 +++ b/Public/Invoke-IntuneDetectionTest.ps1 @@ -11,8 +11,9 @@ function Invoke-IntuneDetectionTest { [Alias('FullName', 'PSPath')] [string]$Path, + # Left out: the device's 64-bit host (x64, or arm64 on Windows on ARM), the agent's default [ValidateSet('x86', 'x64', 'arm64')] - [string]$Architecture = 'x64', + [string]$Architecture, [ValidateSet('User', 'System')] [string]$Context = 'User', @@ -35,6 +36,9 @@ function Invoke-IntuneDetectionTest { throw '-Credential applies to -Context User; System runs as NT AUTHORITY\SYSTEM' } Write-Verbose "Starting $($MyInvocation.MyCommand.Name) for $($PSBoundParameters.Keys -join ', ')" + # The agent's default host is the device's 64-bit one: arm64 on Windows on ARM, where no x64 + # host exists, and x64 elsewhere + if (-not $Architecture) { $Architecture = Get-IslHostArchitecture } $reasons = [System.Collections.Generic.List[string]]::new() $signatureStatus = '' diff --git a/Public/Invoke-IntuneRequirementTest.ps1 b/Public/Invoke-IntuneRequirementTest.ps1 index ad2b709..c6a7096 100644 --- a/Public/Invoke-IntuneRequirementTest.ps1 +++ b/Public/Invoke-IntuneRequirementTest.ps1 @@ -22,8 +22,9 @@ function Invoke-IntuneRequirementTest { [AllowEmptyString()] [string]$Value, + # Left out: the device's 64-bit host (x64, or arm64 on Windows on ARM), the agent's default [ValidateSet('x86', 'x64', 'arm64')] - [string]$Architecture = 'x64', + [string]$Architecture, [ValidateSet('User', 'System')] [string]$Context = 'User', @@ -41,6 +42,9 @@ function Invoke-IntuneRequirementTest { process { Write-Verbose "Starting $($MyInvocation.MyCommand.Name) for $($PSBoundParameters.Keys -join ', ')" + # The agent's default host is the device's 64-bit one: arm64 on Windows on ARM, where no x64 + # host exists, and x64 elsewhere + if (-not $Architecture) { $Architecture = Get-IslHostArchitecture } $scriptRunSplat = @{ Path = $Path diff --git a/Public/Invoke-IntuneWin32AppTest.ps1 b/Public/Invoke-IntuneWin32AppTest.ps1 index f5fd711..3e9afe9 100644 --- a/Public/Invoke-IntuneWin32AppTest.ps1 +++ b/Public/Invoke-IntuneWin32AppTest.ps1 @@ -33,8 +33,9 @@ function Invoke-IntuneWin32AppTest { # uninstalled before this app installs, an update target stays (W32-SUP-OLD-A, W32-SUP-OLD-B) [hashtable[]]$Supersedes, + # Left out: the device's 64-bit host (x64, or arm64 on Windows on ARM), the agent's default [ValidateSet('x86', 'x64', 'arm64')] - [string]$Architecture = 'x64', + [string]$Architecture, [ValidateSet('User', 'System')] [string]$Context = 'User', @@ -64,6 +65,9 @@ function Invoke-IntuneWin32AppTest { [switch]$EnforceSignatureCheck ) Write-Verbose "Starting $($MyInvocation.MyCommand.Name) for $($PSBoundParameters.Keys -join ', ')" + # The agent's default host is the device's 64-bit one: arm64 on Windows on ARM, where no x64 + # host exists, and x64 elsewhere + if (-not $Architecture) { $Architecture = Get-IslHostArchitecture } if (-not $DetectionPath -and -not $DetectionRule) { throw 'Give a detection script (-DetectionPath), detection rules (-DetectionRule), or both' diff --git a/Tests/Unit/Private/Get-IslHostArchitecture.Tests.ps1 b/Tests/Unit/Private/Get-IslHostArchitecture.Tests.ps1 new file mode 100644 index 0000000..ccdc1c0 --- /dev/null +++ b/Tests/Unit/Private/Get-IslHostArchitecture.Tests.ps1 @@ -0,0 +1,30 @@ +#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '6.2.0' } + +<# + The default architecture of the Win32 harness commands: the 64-bit host this device has, + which Get-IslHostPath resolves to System32 rather than refusing. +#> + +BeforeAll { + $script:ModuleRoot = Split-Path -Parent (Split-Path -Parent (Split-Path -Parent $PSScriptRoot)) + Import-Module (Join-Path $script:ModuleRoot 'IntuneScriptLab.psd1') -Force + $osArchitecture = "$([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture)" + $script:Native = if ($osArchitecture -eq 'Arm64') { 'arm64' } else { 'x64' } +} + +AfterAll { + Remove-Module IntuneScriptLab -Force -ErrorAction SilentlyContinue +} + +Describe 'Get-IslHostArchitecture' -Tag 'Unit', 'Private' { + + It 'names the 64-bit host this device has' { + InModuleScope IntuneScriptLab { Get-IslHostArchitecture } | Should-Be $script:Native + } + + It 'names a host Get-IslHostPath resolves instead of refusing' { + $hostInfo = InModuleScope IntuneScriptLab { Get-IslHostPath -Architecture (Get-IslHostArchitecture) } + $hostInfo.Path | Should-BeLikeString '*\WindowsPowerShell\v1.0\powershell.exe' + $hostInfo.Path | Should-NotBeLikeString '*SysWOW64*' + } +} diff --git a/Tests/Unit/Public/Invoke-IntuneDetectionTest.Tests.ps1 b/Tests/Unit/Public/Invoke-IntuneDetectionTest.Tests.ps1 index 36bd313..28c01f4 100644 --- a/Tests/Unit/Public/Invoke-IntuneDetectionTest.Tests.ps1 +++ b/Tests/Unit/Public/Invoke-IntuneDetectionTest.Tests.ps1 @@ -11,6 +11,8 @@ BeforeAll { Import-Module (Join-Path $script:ModuleRoot 'IntuneScriptLab.psd1') -Force . (Join-Path $script:ModuleRoot 'Tests\TestHelpers\TestHelpers.ps1') $script:Detect = 'C:\lab\detect.ps1' + $osArchitecture = "$([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture)" + $script:Native = if ($osArchitecture -eq 'Arm64') { 'arm64' } else { 'x64' } } AfterAll { @@ -98,6 +100,17 @@ Describe 'Invoke-IntuneDetectionTest' -Tag 'Unit', 'Public' { $result.Architecture | Should-Be 'x86' $result.Context | Should-Be 'User' } + + It 'defaults to the 64-bit host this device has, the one the agent uses for Win32 detection' { + Mock Invoke-IslScriptRun -ModuleName IntuneScriptLab { + [pscustomobject]@{ ExitCode = 0; TimedOut = $false; StdOut = 'x'; StdErr = '' } + } + $result = Invoke-IntuneDetectionTest -Path $script:Detect + Should-Invoke Invoke-IslScriptRun -ModuleName IntuneScriptLab -Exactly -Times 1 -ParameterFilter { + $Architecture -in 'x64', 'arm64' + } + $result.Architecture | Should-Be $script:Native + } } Context 'Enforced signature check (W32-DET-SIGCHECK)' { diff --git a/Tests/Unit/Public/Invoke-IntuneRequirementTest.Tests.ps1 b/Tests/Unit/Public/Invoke-IntuneRequirementTest.Tests.ps1 index 9078290..3fe953d 100644 --- a/Tests/Unit/Public/Invoke-IntuneRequirementTest.Tests.ps1 +++ b/Tests/Unit/Public/Invoke-IntuneRequirementTest.Tests.ps1 @@ -11,6 +11,8 @@ BeforeAll { Import-Module (Join-Path $script:ModuleRoot 'IntuneScriptLab.psd1') -Force . (Join-Path $script:ModuleRoot 'Tests\TestHelpers\TestHelpers.ps1') $script:Script = 'C:\lab\requirement.ps1' + $osArchitecture = "$([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture)" + $script:Native = if ($osArchitecture -eq 'Arm64') { 'arm64' } else { 'x64' } } AfterAll { @@ -40,16 +42,16 @@ Describe 'Invoke-IntuneRequirementTest' -Tag 'Unit', 'Public' { $results.Applicable | Should-All { $_ } } - It 'defaults to the 64-bit host, as the portal does for requirement rules' { + It 'defaults to the 64-bit host this device has, as the portal does for requirement rules' { Mock Invoke-IslScriptRun -ModuleName IntuneScriptLab { [pscustomobject]@{ ExitCode = 0; TimedOut = $false; StdOut = "ok`r`n"; StdErr = '' } } $result = Invoke-IntuneRequirementTest -Path $script:Script -OutputType String -Value 'ok' - $result.Architecture | Should-Be 'x64' + $result.Architecture | Should-Be $script:Native $result.Context | Should-Be 'User' $result.Operator | Should-Be 'Equal' Should-Invoke Invoke-IslScriptRun -ModuleName IntuneScriptLab -Exactly -Times 1 -ParameterFilter { - $Architecture -eq 'x64' -and $Phase -eq 'requirement' + $Architecture -in 'x64', 'arm64' -and $Phase -eq 'requirement' } } } diff --git a/Tests/Unit/Public/Invoke-IntuneWin32AppTest.Tests.ps1 b/Tests/Unit/Public/Invoke-IntuneWin32AppTest.Tests.ps1 index d135eb5..8c2267f 100644 --- a/Tests/Unit/Public/Invoke-IntuneWin32AppTest.Tests.ps1 +++ b/Tests/Unit/Public/Invoke-IntuneWin32AppTest.Tests.ps1 @@ -11,6 +11,8 @@ BeforeAll { $script:ModuleRoot = Split-Path -Parent (Split-Path -Parent (Split-Path -Parent $PSScriptRoot)) Import-Module (Join-Path $script:ModuleRoot 'IntuneScriptLab.psd1') -Force . (Join-Path $script:ModuleRoot 'Tests\TestHelpers\TestHelpers.ps1') + $osArchitecture = "$([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture)" + $script:Native = if ($osArchitecture -eq 'Arm64') { 'arm64' } else { 'x64' } $script:FileRule = @{ Type = 'File'; Path = 'C:\Fixtures'; FileOrFolderName = 'present.txt' OperationType = 'exists' } $script:MissingRule = @{ Type = 'File'; Path = 'C:\Fixtures'; FileOrFolderName = 'missing.txt' @@ -76,16 +78,16 @@ Describe 'Invoke-IntuneWin32AppTest' -Tag 'Unit', 'Public' { Should-Throw -ExceptionMessage '*-UninstallCommand*' } - It 'defaults to the 64-bit host, as the portal does for Win32 detection' { + It 'defaults to the 64-bit host this device has, as the portal does for Win32 detection' { $intuneWin32AppTestSplat = @{ DetectionPath = $script:Fixture.Detection ContentPath = $script:Fixture.Content InstallCommand = 'setup.exe /exit 0' } $result = Invoke-IntuneWin32AppTest @intuneWin32AppTestSplat - $result.Architecture | Should-Be 'x64' + $result.Architecture | Should-Be $script:Native Should-Invoke Invoke-IntuneDetectionTest -ModuleName IntuneScriptLab -ParameterFilter { - $Architecture -eq 'x64' + $Architecture -in 'x64', 'arm64' } } diff --git a/docs/IntuneScriptLab/Invoke-IntuneDetectionTest.md b/docs/IntuneScriptLab/Invoke-IntuneDetectionTest.md index b8f7dc2..e9a83e3 100644 --- a/docs/IntuneScriptLab/Invoke-IntuneDetectionTest.md +++ b/docs/IntuneScriptLab/Invoke-IntuneDetectionTest.md @@ -67,13 +67,14 @@ Inside a Pester test. ### -Architecture -Host to run in: x64 (Intune's default for Win32 detection), x86 (the "run as 32-bit" -option), or arm64 on a Windows on ARM device. A Windows on ARM device has no x64 host, so the -x64 default is refused there: pass arm64, the host the agent uses on ARM64. +Host to run in: x64, x86 (the "run as 32-bit" option), or arm64. Left out, the device's +64-bit host: x64 on an x64 device, arm64 on Windows on ARM, which is the host the agent uses +for Win32 detection. x64 and arm64 each name a host only its own CPU has, so one is refused +on the other. ```yaml Type: System.String -DefaultValue: x64 +DefaultValue: '' SupportsWildcards: false Aliases: [] ParameterSets: diff --git a/docs/IntuneScriptLab/Invoke-IntuneRequirementTest.md b/docs/IntuneScriptLab/Invoke-IntuneRequirementTest.md index 9f9a009..c82d05a 100644 --- a/docs/IntuneScriptLab/Invoke-IntuneRequirementTest.md +++ b/docs/IntuneScriptLab/Invoke-IntuneRequirementTest.md @@ -69,13 +69,14 @@ As SYSTEM from an elevated session, inside a Pester test. ### -Architecture -Host to run in: x64 (the default for requirement rules), x86 (the "run as 32-bit" -option), or arm64 on a Windows on ARM device. A Windows on ARM device has no x64 host, so the -x64 default is refused there: pass arm64, the host the agent uses on ARM64. +Host to run in: x64, x86 (the "run as 32-bit" option), or arm64. Left out, the device's +64-bit host: x64 on an x64 device, arm64 on Windows on ARM, which is the host the agent uses +for requirement rules. x64 and arm64 each name a host only its own CPU has, so one is refused +on the other. ```yaml Type: System.String -DefaultValue: x64 +DefaultValue: '' SupportsWildcards: false Aliases: [] ParameterSets: diff --git a/docs/IntuneScriptLab/Invoke-IntuneWin32AppTest.md b/docs/IntuneScriptLab/Invoke-IntuneWin32AppTest.md index c1574e1..7865bb0 100644 --- a/docs/IntuneScriptLab/Invoke-IntuneWin32AppTest.md +++ b/docs/IntuneScriptLab/Invoke-IntuneWin32AppTest.md @@ -144,11 +144,11 @@ package is uninstalled if it is present, then the app installs. ### -Architecture -Host for the detection script: x64 (Intune default), x86, or arm64. A Windows on ARM device has no x64 host, so a -DetectionPath with the x64 default is refused there: pass arm64. +Host for the detection script: x64, x86, or arm64. Left out, the device's 64-bit host: x64 on an x64 device, arm64 on Windows on ARM, which is the host the agent uses for Win32 detection. x64 and arm64 each name a host only its own CPU has, so one is refused on the other. ```yaml Type: System.String -DefaultValue: x64 +DefaultValue: '' SupportsWildcards: false Aliases: [] ParameterSets: diff --git a/en-US/IntuneScriptLab-Help.xml b/en-US/IntuneScriptLab-Help.xml index f33f4f3..adad4b5 100644 --- a/en-US/IntuneScriptLab-Help.xml +++ b/en-US/IntuneScriptLab-Help.xml @@ -2751,9 +2751,10 @@ Test-IntuneScript's IslContextIssue flags statically. Architecture - Host to run in: x64 (Intune's default for Win32 detection), x86 (the "run as 32-bit" -option), or arm64 on a Windows on ARM device. A Windows on ARM device has no x64 host, so the -x64 default is refused there: pass arm64, the host the agent uses on ARM64. + Host to run in: x64, x86 (the "run as 32-bit" option), or arm64. Left out, the device's +64-bit host: x64 on an x64 device, arm64 on Windows on ARM, which is the host the agent uses +for Win32 detection. x64 and arm64 each name a host only its own CPU has, so one is refused +on the other. System.String @@ -3469,9 +3470,10 @@ Under Intune the requirement runs before the install and after the detection has Architecture - Host to run in: x64 (the default for requirement rules), x86 (the "run as 32-bit" -option), or arm64 on a Windows on ARM device. A Windows on ARM device has no x64 host, so the -x64 default is refused there: pass arm64, the host the agent uses on ARM64. + Host to run in: x64, x86 (the "run as 32-bit" option), or arm64. Left out, the device's +64-bit host: x64 on an x64 device, arm64 on Windows on ARM, which is the host the agent uses +for requirement rules. x64 and arm64 each name a host only its own CPU has, so one is refused +on the other. System.String @@ -3834,7 +3836,7 @@ Soft/hard reboot codes count as success but are reported. Architecture - Host for the detection script: x64 (Intune default), x86, or arm64. A Windows on ARM device has no x64 host, so a -DetectionPath with the x64 default is refused there: pass arm64. + Host for the detection script: x64, x86, or arm64. Left out, the device's 64-bit host: x64 on an x64 device, arm64 on Windows on ARM, which is the host the agent uses for Win32 detection. x64 and arm64 each name a host only its own CPU has, so one is refused on the other. System.String