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
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,17 @@ release notes.
silence, was already right. Its file `doesNotExist` finding cited `W32-FILE-NOTEXIST-PRESENT`;
the experiment is `W32-FILE-NOTEXIST-FALSE`.

### Changed

- Validation kit: `Invoke-ValidationRound.ps1 -Action Trigger` stops the agent's process and starts
the service, and prints the process start time before and after, since `Restart-Service` reported
the service running and restarted nothing (round 11). `New-IntuneLabVm.ps1` makes its guest calls
through `GuestAgent.ps1` instead of its own synchronous `qm guest exec`, whose results Windows'
process id reuse mixed up. `Invoke-LabGuestScript.ps1`'s help describes the file-write transfer
and the rerun after a lost result, and it and the two probe scripts require PowerShell 7.6 like
the driver. The kit's README no longer points at the monorepo paths the module came from, and the
experiment catalog's header says what it holds.

## [0.33.0] - 2026-10-15

Fixes to the CI pieces, found by running the gate script, the workflow template, the suite template
Expand Down
2 changes: 1 addition & 1 deletion Validation/Experiments.psd1
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Round 1 experiments. Each script body is appended to Probe.ps1 before upload.
# The experiment catalog, rounds 1 to 11. Each script body is appended to Probe.ps1 before upload.
# Names are prefixed with ISL- in Intune so Invoke-ValidationRound can find and remove them.
#
# Keys:
Expand Down
2 changes: 1 addition & 1 deletion Validation/Invoke-AssignmentProbe.ps1
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#Requires -Version 7.4
#Requires -Version 7.6
#Requires -Modules Microsoft.Graph.Authentication

<#
Expand Down
2 changes: 1 addition & 1 deletion Validation/Invoke-FilterProbe.ps1
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#Requires -Version 7.4
#Requires -Version 7.6
#Requires -Modules Microsoft.Graph.Authentication

<#
Expand Down
18 changes: 9 additions & 9 deletions Validation/Invoke-LabGuestScript.ps1
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
#Requires -Version 7.4
#Requires -Version 7.6

<#
.SYNOPSIS
Runs a local PowerShell script inside a Proxmox lab VM through the QEMU guest agent, as SYSTEM.

.DESCRIPTION
The guest agent's command line fails silently above a few KB, so the script is delivered as
base64 in numbered part files (Set-Content, so a chunk retried after a host-side timeout
rewrites the same part rather than appending it twice), joined and decoded on the VM, then
run with Windows PowerShell under a process-scoped execution policy bypass. Talks to the
Proxmox host over key-based SSH; the VM needs the guest agent.
The guest agent's command line fails silently above a few KB, so the script is written to
the VM through the agent's file-write call (Send-GuestFile in GuestAgent.ps1: numbered
base64 part files, joined and hashed on the VM and the hash compared here), then run with
Windows PowerShell under a process-scoped execution policy bypass. Talks to the Proxmox host
over key-based SSH; the VM needs the guest agent.

.PARAMETER VmId
The Proxmox VM id.
Expand Down Expand Up @@ -52,9 +52,9 @@

.NOTES
Author: Jeffrey Stuhr. The guest agent calls are GuestAgent.ps1's: each command is started
once and its result asked for by process id, so a status call that times out is asked
again without running the script a second time. A start call that times out is retried and
can run it twice.
once and its result asked for by process id. A status call that times out on the host can
take the finished result with it, and the command is then run again, up to three times, so
the script has to be safe to run twice.
#>
[CmdletBinding()]
# ProxmoxHost is read inside Invoke-GuestPowerShell (GuestAgent.ps1); the analyzer cannot see that
Expand Down
19 changes: 17 additions & 2 deletions Validation/Invoke-ValidationRound.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -615,8 +615,23 @@ function Invoke-Trigger {
[CmdletBinding(SupportsShouldProcess)]
param()
if ($PSCmdlet.ShouldProcess("VM $VmId", 'Restart IntuneManagementExtension')) {
Invoke-GuestPowerShell -Script ('Restart-Service -Name IntuneManagementExtension -Force; (Get-Service ' +
'IntuneManagementExtension).Status')
# Restart-Service reports Running and restarts nothing: the stop fails and the process keeps
# its start time (round 11, VM 126). Stopping the service's process and then starting the
# service fetched new assignments within ten minutes. Both start times are printed, so a
# restart that did not happen shows as an unchanged Before/After
Invoke-GuestPowerShell -Script @'
$name = 'IntuneManagementExtension'
$service = Get-CimInstance -ClassName Win32_Service -Filter "Name='$name'"
$before = if ($service.ProcessId) { (Get-Process -Id $service.ProcessId).StartTime.ToString('o') }
Stop-Service -Name $name -Force -ErrorAction SilentlyContinue
if ($service.ProcessId -and (Get-Process -Id $service.ProcessId -ErrorAction SilentlyContinue)) {
Stop-Process -Id $service.ProcessId -Force

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.

Stop-Service is tried first and allowed to fail, because on a healthy agent it is the clean path and round 11 only showed it failing on a stuck one. The process id is read before the stop so the kill targets the process that was the service, not whatever Start-Service may have already spawned; the Get-Process check skips the kill when the stop did work.

}
Start-Service -Name $name
$service = Get-CimInstance -ClassName Win32_Service -Filter "Name='$name'"
$after = if ($service.ProcessId) { (Get-Process -Id $service.ProcessId).StartTime.ToString('o') }
"Status=$((Get-Service -Name $name).Status) Before=$before After=$after"
'@
}
}

Expand Down
13 changes: 7 additions & 6 deletions Validation/New-IntuneLabVm.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,15 @@ function Invoke-Qm {
$out
}

# The guest agent calls are GuestAgent.ps1's; Invoke-GuestPowerShell reads $ProxmoxHost and $VmId
# from this scope
. (Join-Path -Path $PSScriptRoot -ChildPath 'GuestAgent.ps1')

function Invoke-Guest {
# Runs PowerShell inside the VM via the guest agent; returns stdout or $null if the agent isn't up.
# Runs PowerShell inside the VM; $null while the agent is not up, which the OOBE wait polls on
param([Parameter(Mandatory)][string]$Script)
$encoded = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($Script))
$raw = ssh -o BatchMode=yes $ProxmoxHost ("qm guest exec $VmId --timeout 60 -- powershell -NoProfile " +
"-NonInteractive -EncodedCommand $encoded") 2>$null
if ($LASTEXITCODE -ne 0 -or -not $raw) { return $null }
(($raw -join "`n") | ConvertFrom-Json).'out-data'
try { Invoke-GuestPowerShell -Script $Script -TimeoutSeconds 60 -WarningAction SilentlyContinue }
catch { $null }

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.

Kept as a wrapper rather than calling Invoke-GuestPowerShell directly: the OOBE wait polls a VM whose agent is not running yet, and GuestAgent.ps1 throws after three failed exec attempts where the old helper returned $null. The wrapper keeps that contract so Wait-GuestAtOobe's loop is unchanged; warnings are silenced for the same reason.

}

function Wait-GuestAtOobe {
Expand Down
5 changes: 2 additions & 3 deletions Validation/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,7 @@ with the documented behaviour next to the observed one.
- An app registration with certificate auth and these application permissions:
`DeviceManagementScripts.ReadWrite.All`, `DeviceManagementConfiguration.ReadWrite.All`,
`DeviceManagementApps.ReadWrite.All`, `DeviceManagementManagedDevices.Read.All`,
`Group.ReadWrite.All`, `Device.Read.All`.
`Scripts\Entra\Grant-ClaudeDevTenantPermissions.ps1` grants them.
`Group.ReadWrite.All`, `Device.Read.All`, granted as application permissions with admin consent.

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 script this line named lived in the monorepo's Scripts\Entra folder and was not carried over, so the sentence now says what has to be true of the app registration rather than how it was once done.

- A Proxmox host reachable over key-based SSH, a sysprepped Windows 11 Enterprise/Pro template
with the QEMU guest agent installed, and a licensed test user (Windows E3/E5 for remediations).
- `Tools\IntuneWinAppUtil.exe` (only for the Win32 experiments). `Deploy` downloads it from
Expand All @@ -50,7 +49,7 @@ remediations are assigned hourly and will keep running on whatever is in the gro

```powershell
$conn = @{ TenantId = '<tenant>'; ClientId = '<app>'; CertificateThumbprint = '<thumbprint>' }
cd Scripts\Intune\IntuneScriptLab\Validation
cd Validation

# 1. Test VM. Ends at the OOBE account screen with a clean-oobe snapshot.
.\New-IntuneLabVm.ps1 -TemplateId 123 -VmId 125 -Name INTUNE-TEST-02
Expand Down
Loading