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