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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@ release notes.
`'AMD64'` compared with `-eq`, `$PWD.Path`. The `#Requires` finding now sits on its line rather than on
the whole script.

### Changed

- The in-box module table `IslModuleDependency` reports from is held against the Windows client the tests run
on: every listed module must be under the host's system module paths, except the engine module and the seven
a Home edition lacks, and nothing may be under the Windows module folder that the table or the test does not
account for, Hyper-V and the container family being features a host may have turned on. The table
was re-captured as SYSTEM on the lab device on 2026-10-06 and is unchanged.

### Fixed

- **`Test-IntuneDeployedScript` decided whether a group holds devices from its first 20 members.** A mixed group
Expand Down
15 changes: 12 additions & 3 deletions Private/Get-IslInboxModule.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,18 @@ function Get-IslInboxModule {

.DESCRIPTION
Get-Module -ListAvailable as NT AUTHORITY\SYSTEM on an Entra-joined Windows 11 24H2 test
device with nothing installed beyond Windows itself (VM 125, 2026-09-28): the list a script
can import under the agent without installing anything first. Feature-on-demand modules
(RSAT, Hyper-V, containers) are absent on purpose, since a device may or may not have them.
device with nothing installed beyond Windows itself (VM 125, 2026-09-28, the same 90 again
on 2026-10-06 under Windows 11 Enterprise LTSC 24H2): the list a script can import under
the agent without installing anything first. Feature-on-demand modules (RSAT, Hyper-V,
containers) are absent on purpose, since a device may or may not have them.

Microsoft.PowerShell.Core is listed although Get-Module -ListAvailable never shows it: the
engine's own module, whose commands every session has. Import-Module of it by name fails on
both hosts ("no valid module file was found", 2026-10-06), which is a script's mistake of
another kind than a missing module. A Home edition lacks seven of the others
(AppLocker, AppvClient, AssignedAccess, BranchCache, ConfigCI, iSCSI, UEV), and a Windows 11
Home ARM64 host has HostNetworkingService in addition; the unit test holds the table against
the host it runs on with those allowances.

.EXAMPLE
'ActiveDirectory' -in (Get-IslInboxModule)
Expand Down
108 changes: 108 additions & 0 deletions Tests/Unit/Private/Get-IslInboxModule.Tests.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '6.2.0' }

<#
The in-box module table IslModuleDependency reports from, held against a Windows client's own
Windows PowerShell: every listed module is there under the system module paths, except the
engine module and the ones a Home edition does not ship, and nothing is there that the table
does not name or this file does not account for. The host is asked as a child process, so the
test gives the same answer whichever host runs it; a server has a different set and is skipped.
#>

BeforeDiscovery {
$script:ClientHost = $false
if (Get-Command powershell.exe -ErrorAction SilentlyContinue) {
# ProductType 1 is a workstation; the table describes a client, and a server's set differs
$os = Get-CimInstance -ClassName Win32_OperatingSystem -ErrorAction SilentlyContinue
$script:ClientHost = [int]$os.ProductType -eq 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.

windows-latest in CI is Windows Server, whose in-box set has server modules and lacks client ones; the table describes a client, so the host assertions run on windows-11-arm and on a developer's machine, not there.

}
}

BeforeAll {
$script:ModuleRoot = Split-Path -Parent (Split-Path -Parent (Split-Path -Parent $PSScriptRoot))
Import-Module (Join-Path $script:ModuleRoot 'IntuneScriptLab.psd1') -Force
$script:Table = @(InModuleScope IntuneScriptLab { Get-IslInboxModule })

# Listed, and rightly, but not a folder Get-Module -ListAvailable finds: the engine's own module
$script:EngineModule = 'Microsoft.PowerShell.Core'
# Pro and Enterprise features a Home edition does not ship (Windows 11 Home 24H2, ARM64)
$script:EditionOnly = 'AppLocker', 'AppvClient', 'AssignedAccess', 'BranchCache', 'ConfigCI', 'iSCSI', 'UEV'

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.

Measured on a Windows 11 Home ARM64 24H2 host against the Enterprise x64 device: these seven are the whole difference in that direction. A Home host in CI would otherwise fail the first assertion for features it never had.

# Windows features that put a module under System32 when they are turned on: Hyper-V and the
# container and host-guardian family. HostNetworkingService is there on a Windows 11 Home ARM64
# host without the feature. None is on a plain device, so none is in the table
$script:FeatureModules = 'Hyper-V', 'HgsClient', 'HgsDiagnostics', 'HostComputeService',

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 ARM64 runner has Hyper-V, HgsClient, HgsDiagnostics and HostComputeService under System32: features turned on in the runner image. They are not in the table because a plain device has none of them, which the help of Get-IslInboxModule says of feature modules.

'HostNetworkingService'

# Discovery-time variables do not reach the run, so the lookup is repeated here
$script:ClientHost = $false
if (Get-Command powershell.exe -ErrorAction SilentlyContinue) {
$os = Get-CimInstance -ClassName Win32_OperatingSystem -ErrorAction SilentlyContinue
$script:ClientHost = [int]$os.ProductType -eq 1
}
if ($script:ClientHost) {
# What the host's Windows PowerShell has under the two system module paths, the ones a
# SYSTEM session under the agent reads (REM-PSMODULEPATH), kept apart: System32 is Windows'
# own, Program Files is where installed software and the Gallery's AllUsers scope land
$probe = {
$windows = "$env:SystemRoot\System32\WindowsPowerShell\v1.0\Modules"
$programs = "$env:ProgramFiles\WindowsPowerShell\Modules"
$available = Get-Module -ListAvailable
$under = {
param($Root)
@($available | Where-Object { $_.ModuleBase.StartsWith($Root, 'OrdinalIgnoreCase') } |
Select-Object -ExpandProperty Name -Unique | Sort-Object -Unique)
}
# The engine module has no folder and Import-Module of it fails; its commands are there
$engine = (Get-Command -Name Get-Command).ModuleName

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.

Get-Module -Name Microsoft.PowerShell.Core returns nothing on either host and Import-Module of it fails, so the engine module is proved by the module a core command reports. The first draft asked Get-Module and failed on both hosts.

@{ Windows = & $under $windows; Programs = & $under $programs; Engine = $engine } |
ConvertTo-Json -Compress
}
$encoded = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($probe.ToString()))
$raw = & powershell.exe -NoProfile -NonInteractive -EncodedCommand $encoded 2>&1
$answer = ($raw | Where-Object { "$_".StartsWith('{') } | Select-Object -Last 1) | ConvertFrom-Json
$script:WindowsModules = @($answer.Windows)

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.

Two lists rather than one so that completeness can be judged on the Windows folder alone while existence still accepts Program Files, where PackageManagement, PowerShellGet and Pester 3.4 live on a plain device.

$script:HostModules = @($answer.Windows) + @($answer.Programs)
$script:HostEngineModule = "$($answer.Engine)"
}
}

AfterAll {
Remove-Module IntuneScriptLab -Force -ErrorAction SilentlyContinue
}

Describe 'Get-IslInboxModule' -Tag 'Unit', 'Private' {

It 'names each module once' {
$script:Table.Count | Should-BeGreaterThan 80
@($script:Table | Sort-Object -Unique).Count | Should-Be $script:Table.Count
$script:Table | Should-ContainCollection 'Microsoft.PowerShell.Utility'
$script:Table | Should-ContainCollection 'ScheduledTasks'
$script:Table | Should-NotContainCollection 'ActiveDirectory'
}

Context 'Against this Windows client' -Skip:(-not $script:ClientHost) {
It 'has every listed module under the system module paths, or is a Home edition without the feature' {
$script:HostModules.Count | Should-BeGreaterThan 50
$missing = @($script:Table | Where-Object {
$_ -ne $script:EngineModule -and $_ -notin $script:EditionOnly -and
$_ -notin $script:HostModules
})
$missing | Should-BeCollection @()
}

It 'has the engine module''s commands without a module folder for it' {
$script:HostModules | Should-NotContainCollection $script:EngineModule
$script:HostEngineModule | Should-Be $script:EngineModule
}

It 'has nothing under the Windows module folder that the table or this file does not account for' {
# System32 only: a GitHub runner has fifty installed modules under Program Files, and so
# may any managed device. A module here that is in neither list is either new to
# Windows, in which case the table is behind, or a feature of this host, in which case
# it goes in FeatureModules
$unlisted = @($script:WindowsModules | Where-Object {
$_ -notin $script:Table -and $_ -notin $script:FeatureModules
})
$unlisted | Should-BeCollection @()
}
}
}
2 changes: 1 addition & 1 deletion Validation/Findings.md
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,7 @@ device, the agent restarted once to fetch them.
| Rule | Documented | Observed | |
|---|---|---|---|
| Execution policy | Scripts run regardless of the device's execution policy (PS) | AgentExecutor launches every script as `powershell.exe -NoProfile -executionPolicy bypass -file <script>`; inside a remediation `Get-ExecutionPolicy -List` read `MachinePolicy=Undefined;UserPolicy=Undefined;Process=Bypass;CurrentUser=Undefined;LocalMachine=Undefined` (REM-EXECPOLICY). A `Set-ExecutionPolicy` in a script changes nothing for that run; any scope but Process changes the device's policy as SYSTEM | ✅ |
| Module path under SYSTEM | Not documented | `$env:PSModulePath` in a SYSTEM remediation is `WindowsPowerShell\Modules;C:\Program Files\WindowsPowerShell\Modules;C:\WINDOWS\system32\WindowsPowerShell\v1.0\Modules`: the first entry is a **relative** path, because SYSTEM has no Documents folder to resolve, so a module installed with `-Scope CurrentUser` as SYSTEM lands nowhere useful. 90 modules were available on a plain Windows 11 device, the same list as `Get-Module -ListAvailable` under SYSTEM outside the agent (REM-PSMODULEPATH; the list is Get-IslInboxModule) | ⚠️ |
| Module path under SYSTEM | Not documented | `$env:PSModulePath` in a SYSTEM remediation is `WindowsPowerShell\Modules;C:\Program Files\WindowsPowerShell\Modules;C:\WINDOWS\system32\WindowsPowerShell\v1.0\Modules`: the first entry is a **relative** path, because SYSTEM has no Documents folder to resolve, so a module installed with `-Scope CurrentUser` as SYSTEM lands nowhere useful. 90 modules were available on a plain Windows 11 device, the same list as `Get-Module -ListAvailable` under SYSTEM outside the agent (REM-PSMODULEPATH; the list is Get-IslInboxModule). Re-captured as SYSTEM on VM 125 on 2026-10-06 (Windows 11 Enterprise LTSC 24H2, build 26100): the same 90. A Windows 11 Home ARM64 24H2 host has 84 of them, lacking AppLocker, AppvClient, AssignedAccess, BranchCache, ConfigCI, iSCSI and UEV, and has HostNetworkingService besides; a unit test holds the table against the host it runs on with those allowances | ⚠️ |
| `Install-Module` from a remediation | Not documented | **Hangs the queue.** `Find-Module` and `Install-Module -Scope CurrentUser -Force` in a SYSTEM detection script never returned: no NuGet provider on the device, no network connection from the process, 12 s of CPU in 20 minutes, 14 threads, stuck on the provider-bootstrap prompt that a non-interactive session cannot answer. Because remediations run one at a time, every remediation queued behind it waited too; the agent killed it at exactly 60 minutes (AgentExecutor.log `Error:Powershell script execution timed out. timeout = 3600 seconds`, agent log `exitCode = 2147483647`), Graph reports `detectionState scriptError`, and the queue moved on; the two remediations behind it ran an hour late (REM-INSTALL-MODULE). It did the same every hourly cycle afterwards and held round 10's seven detections for 70 minutes, so its assignment was removed on 2026-10-06; the policy stays in the tenant, unassigned | ⚠️ |
| `#Requires -Modules` for a module the device lacks | The script does not run (PS docs on #Requires) | As documented, and what Intune makes of it: the detection exits 1 without running, stderr `The script 'detect.ps1' cannot be run because the following modules that are specified by the "#requires" statements of the script are missing: IslNoSuchModule` (`ScriptRequiresMissingModules`), so the remediation script **runs** (exit 0), the post-detection fails the same way, and Graph reports `detectionState fail, remediationState remediationFailed` with the error text in both detection outputs (REM-REQUIRES-MODULE) | ✅ |
| Script size | "Scripts must be less than 200 KB" (REM, PS) | Not enforced at 200 KB. Through the Graph API a remediation of 504 KB and a platform script of 660 KB were accepted; 512 KB and 680 KB were refused with a generic "An error has occurred" (no size in the message). On the device a 500 KB platform script ran (PS-SIZE-500KB, probe record at 00:31:49) and a 250 KB remediation ran and reported `big script ran` with `detectionState success` (REM-SIZE-250KB) | ❌ |
Expand Down
Loading