Repository navigation
fix(harness): a remediation that writes to stderr is a script error, whatever its exit code #26
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -93,6 +93,53 @@ | |
| } | ||
| } | ||
|
|
||
| if ($type -eq 'Remediation') { | ||
| # The remediation script of a pair. Anything on its stderr makes the agent report a script | ||
| # error and skip the post-detection, with exit 0 or not; the detection script's stderr | ||
| # changes nothing (REM-DETECT-STDERR-EXIT0) | ||
| $remediationEvidence = ('A remediation that wrote a cmdlet error to stderr and exited 0 was reported ' + | ||
| 'as RemediationStatus 3, Graph remediationState scriptError, no post-detection run; the same ' + | ||
| 'script with the error silenced ran the post-detection and was reported Recurred ' + | ||
| '(REM-STDERR-EXIT0, REM-STDERR-SILENT)') | ||
| foreach ($command in (Find-IslCommand -Ast $ast -Name 'Write-Error')) { | ||
| $findingSplat = @{ | ||
| RuleName = $rule | ||
| Severity = 'Warning' | ||
| Context = $Context | ||
| Extent = $command.Extent | ||
| Message = ('Write-Error puts text on stderr: Intune reports the remediation as a script error ' + | ||
| 'and skips the post-detection even when the script exits 0. Exit non-zero to fail on ' + | ||
| 'purpose, or report the problem with Write-Output') | ||
| Evidence = $remediationEvidence | ||
| } | ||
| New-IslFinding @findingSplat | ||
| } | ||
| $probing = 'Get-Item', 'Get-ItemProperty', 'Get-ItemPropertyValue', 'Get-ChildItem', 'Get-Package', | ||
| 'Get-Service', 'Get-Process', 'Get-WmiObject', 'Get-CimInstance', 'Get-AppxPackage', | ||
| 'Set-ItemProperty', 'New-ItemProperty', 'Remove-Item', 'Remove-ItemProperty', 'Copy-Item', 'Move-Item' | ||
| $unguarded = @(Find-IslCommand -Ast $ast -Name $probing | | ||
| Where-Object { -not (Test-IslCommandParameter -Command $_ -ParameterName 'ErrorAction') }) | ||
| $preferenceSet = Find-IslAstNode -Ast $ast -TypeName AssignmentStatementAst -Where { | ||
| param($node) $node.Left.Extent.Text -match '(?i)^\$ErrorActionPreference$' -and | ||
| $node.Right.Extent.Text -match '(?i)SilentlyContinue|Ignore|Stop' | ||
|
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A preference of Stop counts as a guard here, unlike in the Win32 block: a terminating error ends the remediation with exit 1, which is the honest Failed, not a success claim followed by a script error. |
||
| } | ||
| if ($unguarded.Count -gt 0 -and -not $preferenceSet) { | ||
| $findingSplat = @{ | ||
| RuleName = $rule | ||
| Severity = 'Warning' | ||
| Context = $Context | ||
| Extent = $unguarded[0].Extent | ||
| Message = ("$($unguarded[0].GetCommandName()) writes an error record to stderr when its target " + | ||
| 'is missing, and the script carries on to exit 0: Intune then reports a script error, not ' + | ||
| 'the Recurred the post-detection would have given. Use -ErrorAction Stop and let the ' + | ||
| 'failure be one, or check the target first') | ||
| Evidence = $remediationEvidence | ||
| Fix = @{ Replacement = $unguarded[0].Extent.Text + ' -ErrorAction Stop' } | ||
|
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Stop, where the Win32 rule offers SilentlyContinue: on a remediation the error is a script error whichever way, and Stop at least ends the script before it prints success and exits 0. SilentlyContinue would turn the run into a Recurred that hides the cause. |
||
| } | ||
| New-IslFinding @findingSplat | ||
| } | ||
| } | ||
|
|
||
| if ($type -eq 'Win32Detection') { | ||
| $stderrCommands = @(Find-IslCommand -Ast $ast -Name 'Write-Error') | ||
| foreach ($command in $stderrCommands) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -63,8 +63,17 @@ function Invoke-IntuneRemediationTest { | |
| } | ||
| else { | ||
| $remediation = Invoke-IslScriptRun -Path $RemediationPath -Phase 'remediate' @scriptRunSplat | ||
| # Anything on the remediation's stderr is a script error to the agent, whatever the exit | ||
| # code: RemediationStatus 3, Graph scriptError, no post-detection (REM-STDERR-EXIT0) | ||
| $remediationStdErr = -not [string]::IsNullOrWhiteSpace($remediation.StdErr) | ||
|
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Whitespace is not an error: a redirection can leave a stray line ending on stderr, and the agent's capture trims. Anything with text in it was a script error on the device. |
||
| if ($remediation.TimedOut) { $status = 'TimedOut' } | ||
| elseif ($remediation.ExitCode -ne 0) { $status = 'Failed' } | ||
| elseif ($remediationStdErr) { | ||
| $status = 'Failed' | ||
| $warnings.Add('Remediation exited 0 but wrote to stderr: Intune reports the run as a script ' + | ||
| 'error (Failed, Graph scriptError) with the error text attached, and skips the ' + | ||
| 'post-detection. Silence the error or exit non-zero on purpose') | ||
| } | ||
| else { | ||
| $post = Invoke-IslScriptRun -Path $DetectionPath -Phase 'detect' @scriptRunSplat | ||
| $status = if ($post.TimedOut) { 'TimedOut' } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,6 +9,69 @@ | |
| # (hourly, the default when omitted). | ||
| @{ | ||
| Remediations = @( | ||
| # --- Round 11: a remediation that writes to stderr, in user context on the ESP device | ||
| # (ISL-ESP-Devices, -GroupName; the signed-in user must hold an Intune licence, a local | ||
| # account or an unlicensed Entra user gets no user-context policy). Run once each, the | ||
| # date in the past so the agent runs them at its next policy fetch | ||
| @{ | ||
| Name = 'REM-STDERR-EXIT0' | ||
| Question = 'Remediation writes a cmdlet error to stderr and exits 0: Recurred, or script error' | ||
| RunAs32Bit = $true | ||
| RunAsAccount = 'user' | ||
| Schedule = @{ Type = 'RunOnce'; DelayMinutes = -10080 } | ||
|
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A week in the past on purpose: the agent runs a run-once schedule that has passed at its next policy fetch, whatever the device clock says (VM 125 sits hours behind until an Entra user signs in). A time ahead waited for a clock that never arrived. |
||
| Detection = @' | ||
| Write-ProbeRecord REM-STDERR-EXIT0 detection | ||
| $key = 'HKCU:\Software\Microsoft\Siuf\Rules' | ||
| $value = (Get-ItemProperty -Path $key -ErrorAction SilentlyContinue).NumberOfSIUFInPeriod | ||
| if ($value -eq 0) { Write-Output 'Feedback requests are off'; exit 0 } | ||
| Write-Output "Feedback requests are on (value: $value)" | ||
| exit 1 | ||
| '@ | ||
| Remediation = @' | ||
| Write-ProbeRecord REM-STDERR-EXIT0 remediation | ||
| Set-ItemProperty -Path 'HKCU:\Software\Microsoft\Siuf\Rules' -Name NumberOfSIUFInPeriod -Value 0 | ||
| Write-Output 'Feedback requests turned off' | ||
| exit 0 | ||
| '@ | ||
| } | ||
| @{ | ||
| Name = 'REM-STDERR-SILENT' | ||
| Question = 'The same remediation with the error silenced: exit 0, no stderr, key still missing' | ||
| RunAs32Bit = $true | ||
| RunAsAccount = 'user' | ||
| Schedule = @{ Type = 'RunOnce'; DelayMinutes = -10080 } | ||
| Detection = @' | ||
| Write-ProbeRecord REM-STDERR-SILENT detection | ||
| $key = 'HKCU:\Software\Microsoft\Siuf\Rules' | ||
| $value = (Get-ItemProperty -Path $key -ErrorAction SilentlyContinue).NumberOfSIUFInPeriod | ||
| if ($value -eq 0) { Write-Output 'Feedback requests are off'; exit 0 } | ||
| Write-Output "Feedback requests are on (value: $value)" | ||
| exit 1 | ||
| '@ | ||
| Remediation = @' | ||
| Write-ProbeRecord REM-STDERR-SILENT remediation | ||
| $key = 'HKCU:\Software\Microsoft\Siuf\Rules' | ||
| Set-ItemProperty -Path $key -Name NumberOfSIUFInPeriod -Value 0 -ErrorAction SilentlyContinue | ||
| Write-Output 'Feedback requests turned off' | ||
| exit 0 | ||
| '@ | ||
| } | ||
| @{ | ||
| Name = 'REM-DETECT-STDERR-EXIT0' | ||
| Question = 'Detection writes a cmdlet error to stderr and exits 0: without issues, or detect error' | ||
| RunAs32Bit = $true | ||
| RunAsAccount = 'user' | ||
| Schedule = @{ Type = 'RunOnce'; DelayMinutes = -10080 } | ||
| Detection = @' | ||
| Write-ProbeRecord REM-DETECT-STDERR-EXIT0 detection | ||
| Get-Item -Path 'C:\does\not\exist' | ||
| Write-Output 'Feedback requests are off' | ||
| exit 0 | ||
| '@ | ||
| Remediation = @' | ||
| Write-ProbeRecord REM-DETECT-STDERR-EXIT0 remediation; Write-Output 'nothing to do'; exit 0 | ||
| '@ | ||
| } | ||
| # --- Round 8: the Enrollment Status Page. Deployed to ISL-ESP-Devices (ESP-DEV-*) and | ||
| # ISL-ESP-Users (ESP-USR-*) with -GroupName; every script records the ESP's state at run time | ||
| @{ | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Win32 list plus the cmdlets a remediation uses to change things, which are the ones that fail on a missing target: the post's own Set-ItemProperty was the case measured.