diff --git a/CHANGELOG.md b/CHANGELOG.md index 427d480..f497bb6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,19 @@ release notes. ## [Unreleased] -Nothing yet. +### Fixed + +- **`Invoke-IntuneRemediationTest` reported Recurred for a remediation that writes to stderr and exits 0.** On + the device that run is a script error: `RemediationStatus` 3, Graph `remediationState` `scriptError`, the + error text attached, no post-detection. The harness now reports Failed, skips the post-detection and warns + that the exit code was 0. A detection that writes to stderr and exits 0 is still Without issues, as before. + Measured in user context on the lab device with five one-off remediations (round 11, `REM-STDERR-*`). + +### Added + +- `IslOutputIssue` warns about `Write-Error` and an unguarded cmdlet in a remediation script, the way it did + for Win32 detection scripts, since either makes the agent report a script error instead of running the + post-detection; the unguarded cmdlet carries `-ErrorAction Stop` as its fix. ## [0.28.0] - 2026-10-06 diff --git a/Private/Rules/Find-IslOutputIssue.ps1 b/Private/Rules/Find-IslOutputIssue.ps1 index e17acaf..ba38def 100644 --- a/Private/Rules/Find-IslOutputIssue.ps1 +++ b/Private/Rules/Find-IslOutputIssue.ps1 @@ -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' + } + 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' } + } + New-IslFinding @findingSplat + } + } + if ($type -eq 'Win32Detection') { $stderrCommands = @(Find-IslCommand -Ast $ast -Name 'Write-Error') foreach ($command in $stderrCommands) { diff --git a/Public/Invoke-IntuneRemediationTest.ps1 b/Public/Invoke-IntuneRemediationTest.ps1 index af6bf0a..4a1db30 100644 --- a/Public/Invoke-IntuneRemediationTest.ps1 +++ b/Public/Invoke-IntuneRemediationTest.ps1 @@ -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) 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' } diff --git a/Tests/Unit/Private/Rules/Find-IslOutputIssue.Tests.ps1 b/Tests/Unit/Private/Rules/Find-IslOutputIssue.Tests.ps1 index 7b47a22..ac9182a 100644 --- a/Tests/Unit/Private/Rules/Find-IslOutputIssue.Tests.ps1 +++ b/Tests/Unit/Private/Rules/Find-IslOutputIssue.Tests.ps1 @@ -39,6 +39,38 @@ Describe 'Find-IslOutputIssue' -Tag 'Unit', 'Private', 'Rule' { } } + Context 'Remediation script' { + It 'warns on Write-Error, which makes the run a script error even with exit 0' { + $path = New-TestScript 'Remediate-E.ps1' "Write-Error 'could not'`nWrite-Output 'done'`nexit 0" + $findings = @(Get-RuleFinding $path IslOutputIssue | Where-Object Message -like '*Write-Error*') + $findings.Count | Should-Be 1 + $findings[0].Severity | Should-Be 'Warning' + $findings[0].Message | Should-BeLikeString '*script error*skips the post-detection*' + $findings[0].Evidence | Should-BeLikeString '*REM-STDERR-EXIT0*' + } + + It 'warns on an unguarded cmdlet and offers -ErrorAction Stop, as the error is a script error anyway' { + $body = "Set-ItemProperty -Path HKCU:\Software\X -Name V -Value 0`nWrite-Output 'done'`nexit 0" + $path = New-TestScript 'Remediate-U.ps1' $body + $findings = @(Get-RuleFinding $path IslOutputIssue | Where-Object Message -like '*Set-ItemProperty*') + $findings.Count | Should-Be 1 + $findings[0].Severity | Should-Be 'Warning' + $findings[0].Fix.Replacement | + Should-Be 'Set-ItemProperty -Path HKCU:\Software\X -Name V -Value 0 -ErrorAction Stop' + } + + It 'is silent for a guarded cmdlet, a Stop or SilentlyContinue preference, and in a detection' { + $guarded = New-TestScript 'Remediate-G.ps1' "Set-ItemProperty HKCU:\X V 0 -ErrorAction Stop`nexit 0" + @(Get-RuleFinding $guarded IslOutputIssue | Where-Object Severity -eq 'Warning').Count | Should-Be 0 + $body = "`$ErrorActionPreference = 'Stop'`nSet-ItemProperty HKCU:\X V 0`nexit 0" + $preference = New-TestScript 'Remediate-P.ps1' $body + @(Get-RuleFinding $preference IslOutputIssue | Where-Object Severity -eq 'Warning').Count | Should-Be 0 + # A detection's stderr changes nothing on the device (REM-DETECT-STDERR-EXIT0) + $detect = New-TestScript 'Detect-U.ps1' "Get-Item C:\x`nWrite-Output 'ok'`nexit 0" + @(Get-RuleFinding $detect IslOutputIssue | Where-Object Severity -eq 'Warning').Count | Should-Be 0 + } + } + Context 'Win32 detection' { It 'errors on exit 0 with no stdout' { $path = New-TestScript 'app.ps1' ("# IntuneScriptLab: ScriptType=Win32Detection`n" + diff --git a/Tests/Unit/Public/Invoke-IntuneRemediationTest.Tests.ps1 b/Tests/Unit/Public/Invoke-IntuneRemediationTest.Tests.ps1 index 72d5b76..b3f74ec 100644 --- a/Tests/Unit/Public/Invoke-IntuneRemediationTest.Tests.ps1 +++ b/Tests/Unit/Public/Invoke-IntuneRemediationTest.Tests.ps1 @@ -106,6 +106,31 @@ Describe 'Invoke-IntuneRemediationTest' -Tag 'Unit', 'Public' { $result.PostDetection.ExitCode | Should-Be 1 } + It 'reports Failed, skips the post-detection and warns when the remediation exits 0 but wrote to stderr' { + # On the device a cmdlet error on the remediation's stderr is a script error whatever the + # exit code: RemediationStatus 3, Graph scriptError, no post-detection (REM-STDERR-EXIT0) + Mock Invoke-IslScriptRun -ModuleName IntuneScriptLab -ParameterFilter { $Phase -eq 'remediate' } { + $null = New-Item -ItemType File -Path (Join-Path (Split-Path $Path -Parent) 'fixed.marker') -Force + [pscustomobject]@{ ExitCode = 0; TimedOut = $false; StdOut = 'turned off' + StdErr = "Set-ItemProperty : Cannot find path 'HKCU:\x' because it does not exist." } + } + $result = Invoke-IntuneRemediationTest -DetectionPath $script:Detect -RemediationPath $script:Remediate + $result.Status | Should-Be 'Failed' + $result.PostDetection | Should-BeNull + $result.Remediation.ExitCode | Should-Be 0 + $result.RemediationOutput | Should-Be 'turned off' + $result.Warnings -join ' ' | Should-BeLikeString '*exited 0 but wrote to stderr*script error*' + } + + It 'does not take whitespace on the remediation stderr for an error' { + Mock Invoke-IslScriptRun -ModuleName IntuneScriptLab -ParameterFilter { $Phase -eq 'remediate' } { + [pscustomobject]@{ ExitCode = 0; TimedOut = $false; StdOut = 'did nothing'; StdErr = "`r`n" } + } + $result = Invoke-IntuneRemediationTest -DetectionPath $script:Detect -RemediationPath $script:Remediate + $result.Status | Should-Be 'Recurred' + $result.Warnings | Should-BeCollection @() + } + It 'reports Failed and skips the post-detection when the remediation exits non-zero' { Mock Invoke-IslScriptRun -ModuleName IntuneScriptLab -ParameterFilter { $Phase -eq 'remediate' } { [pscustomobject]@{ ExitCode = 1; TimedOut = $false; StdOut = ''; StdErr = 'nope' } diff --git a/Validation/Experiments.psd1 b/Validation/Experiments.psd1 index 4830dbd..cede8a9 100644 --- a/Validation/Experiments.psd1 +++ b/Validation/Experiments.psd1 @@ -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 } + 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 @{ diff --git a/Validation/Findings.md b/Validation/Findings.md index 32db546..951e91a 100644 --- a/Validation/Findings.md +++ b/Validation/Findings.md @@ -89,8 +89,8 @@ IME log evidence (registered device): `IsDeviceWPJ()` throws from `NetGetAadJoin | Script decoding | UTF-8; no BOM when signature check is on (REM) | With BOM: literal `Grüße — ✓` correct. **Without BOM: decoded as ANSI** → `Grüße â€" ✓` | ❌ trap | | Non-ASCII in captured output | Not documented | Output goes through the OEM console code page (437): even with a BOM, `Grüße — ✓` is reported as `Grüße - √` (best-fit, lossy) | ⚠️ | | Execution order | Not documented | Sequential, ~15 s per detection/remediation pair; 17 policies took ~6 minutes | ⚠️ | -| Status mapping (device registry `RemediationStatus`) | Portal: Without issues / Fixed / Recurred / Failed | `4` = without issues (detect exit 0) · `1` = fixed (remediate, then detect exit 0) · `2` = recurred (post-detect still non-zero, including when remediation exited 0) · `3` = remediation failed (remediation exit non-zero; post-detect skipped) | ⚠️ | -| Status mapping (Graph `deviceRunStates`) | `detectionState` / `remediationState` enums (GRAPH-HS) | detect exit 0 → `detectionState=success`, `remediationState=skipped` · fixed → `fail` / `success` · post-detect still failing (exit 2, -1, throw, parse error, or remediation exited 0 but didn't fix) → `fail` / **`remediationFailed`** · remediation script exit non-zero → `fail` / **`scriptError`**. So `remediationFailed` means "recurred", not "the remediation script failed" | ⚠️ | +| Status mapping (device registry `RemediationStatus`) | Portal: Without issues / Fixed / Recurred / Failed | `4` = without issues (detect exit 0) · `1` = fixed (remediate, then detect exit 0) · `2` = recurred (post-detect still non-zero, including when remediation exited 0) · `3` = remediation failed (remediation exit non-zero, **or exit 0 with anything on stderr**; post-detect skipped either way, round 11) | ⚠️ | +| Status mapping (Graph `deviceRunStates`) | `detectionState` / `remediationState` enums (GRAPH-HS) | detect exit 0 → `detectionState=success`, `remediationState=skipped` · fixed → `fail` / `success` · post-detect still failing (exit 2, -1, throw, parse error, or remediation exited 0 but didn't fix) → `fail` / **`remediationFailed`** · remediation script exit non-zero, or exit 0 with anything on stderr (round 11) → `fail` / **`scriptError`**. So `remediationFailed` means "recurred", not "the remediation script failed" | ⚠️ | | Reporting latency to Graph | Recurring scripts report on change only (REM) | `lastStateUpdateDateTime` = 14:19:17, i.e. ~40 s after the last of the 17 policies finished (reported as one batch). The run states were still empty when queried at 14:23 and populated by 15:23 | ⚠️ | | Result cache | Not documented | `HKLM\SOFTWARE\Microsoft\IntuneManagementExtension\SideCarPolicies\Scripts\Reports\\_\Result` (JSON with pre/post output, error and exit codes). Platform scripts: `...\IntuneManagementExtension\Policies\\` (`Result`, `ErrorCode`, `DownloadCount`) | ⚠️ | | First run after new assignment | Not documented; policy retrieval on IME start / sign-in / 8h (REM) | Policies received ~8 min after assignment (13:50). After each fetch the HS scheduler queues a run **5 minutes later** (`Job is queued and will be scheduled to run at ...`). Restarting IME before then discards the queued run | ⚠️ | @@ -549,6 +549,39 @@ prompt and can go. `IslContextIssue` no longer calls every drive letter from `D:` to `Z:` an unmapped drive: the letter cannot say whether it is a local volume, so the finding is a note that says which case fails. +## A remediation that writes to stderr + +Round 11, 2026-10-07, VM 126 (Windows 11 Enterprise LTSC 24H2, Entra joined through Autopilot, +agent 1.105.152.0), user context, the licensed ESP user signed in at the console. Five one-off +remediations for a per-user registry setting that is absent until written +(`HKCU:\Software\Microsoft\Siuf\Rules`, `NumberOfSIUFInPeriod`), deployed outside the kit with the +same scripts the `REM-STDERR-*` experiments carry; the device registry result, the agent logs and +the Graph run states were read for each. + +| Remediation script | Exit codes (detect / remediate / post) | Device `RemediationStatus` | Graph `detectionState` / `remediationState` | Notes | +|---|---|---|---|---| +| `Set-ItemProperty` on the missing key, no `-ErrorAction`, `exit 0` | 1 / 0 / none | **3** | `fail` / **`scriptError`** | AgentExecutor logged `Powershell exit code is 0`; the cmdlet error is in `RemediationScriptErrorDetails`; `RemediationScriptOutputDetails` still says "turned off". No post-detection (REM-STDERR-EXIT0) | +| Same with `-ErrorAction Stop` (`exit 1`) | 1 / 1 / none | 3 | `fail` / `scriptError` | The same report, with exit 1 | +| Same with `-ErrorAction SilentlyContinue` (`exit 0`, nothing on stderr) | 1 / 0 / 1 | **2** | `fail` / `remediationFailed` | The post-detection ran and found the key still missing: Recurred (REM-STDERR-SILENT) | +| Detection writes `Get-Item` of a missing path to stderr, `exit 0` | 0 / none / none | 4 | `success` / `skipped` | Without issues; the error text in `PreRemediationDetectScriptError` (REM-DETECT-STDERR-EXIT0) | +| Key created first (`New-Item`), then set | 1 / 0 / 0 | 1 | `fail` / `success` | Fixed | + +**For the tool:** `Invoke-IntuneRemediationTest` reported the first row as Recurred up to 0.28.0, +on the round-1 rule that a remediation exit of 0 runs the post-detection. It reports Failed, skips +the post-detection and warns. `IslOutputIssue` warns about `Write-Error` and an unguarded cmdlet +in a remediation script, as it did for Win32 detection, with `-ErrorAction Stop` as the fix. + +Two things about getting user-context policies to run at all, learnt on the way: + +- A local account at the console is not a user to the agent: with `isl-user` signed in on VM 125 + the runner logged `needs user context, but no user logged on now, skip it`. With the Entra test + user signed in instead, the runner processed the session and got `0 script policies` for it; that + user has no Intune licence. Only the licensed ESP user on VM 126 received the policies. +- `Restart-Service IntuneManagementExtension` reports the service running and restarts nothing: + the stop fails, the process keeps its start time, and the next runner cycle is an hour away. + `sc stop`, `Stop-Process` on the service's process and `Start-Service` fetched the new + assignments within ten minutes. + ## Win32 custom detection scripts Round 2: ten Win32 apps sharing one `.intunewin` package (an install script that only writes a probe diff --git a/docs/IntuneScriptLab/Invoke-IntuneRemediationTest.md b/docs/IntuneScriptLab/Invoke-IntuneRemediationTest.md index 24f7b9d..5724856 100644 --- a/docs/IntuneScriptLab/Invoke-IntuneRemediationTest.md +++ b/docs/IntuneScriptLab/Invoke-IntuneRemediationTest.md @@ -1,4 +1,4 @@ ---- +--- document type: cmdlet external help file: IntuneScriptLab-Help.xml HelpUri: https://github.com/fadwen/IntuneScriptLab/blob/main/docs/IntuneScriptLab/Invoke-IntuneRemediationTest.md @@ -39,10 +39,17 @@ The status follows what the device recorded: detection non-zero with no -RemediationPath, as a detect-only remediation records it Fixed detection non-zero, remediation 0, post-detection 0 - Recurred detection non-zero, remediation 0, post-detection still non-zero - Failed remediation exited non-zero (post-detection skipped) + Recurred detection non-zero, remediation 0 with nothing on stderr, post-detection + still non-zero + Failed remediation exited non-zero, or exited 0 having written anything to + stderr; the post-detection is skipped either way, as the agent skips it TimedOut a script hit the timeout +A remediation's stderr decides the verdict and a detection's does not: on the device, a +remediation that wrote a cmdlet error and exited 0 was reported as a script error with the error +text attached and no post-detection, while a detection that wrote one and exited 0 was reported +Without issues. The result carries a warning for the first case. + Each script is launched as the agent launches it: Windows PowerShell 5.1 in the chosen host, -NoProfile -ExecutionPolicy Bypass -File, no -NonInteractive, working directory C:\WINDOWS\system32, run from a copy of the script, output through the OEM code page. diff --git a/docs/Rules.md b/docs/Rules.md index 99c27f3..807012b 100644 --- a/docs/Rules.md +++ b/docs/Rules.md @@ -153,6 +153,8 @@ Remediations report only the *last* line of the console output, capped at its la | Warning | is the last thing written, so its text is what Intune reports instead of your summary. Write the summary line last, with Write-Output | A trailing Write-Host, Write-Warning or Write-Verbose -Verbose was reported verbatim: "host-last", "WARNING: warn-last", "VERBOSE: verbose-last" (REM-OUT-HOSTLAST/WARNLAST/VERBLAST); otherwise the last Write-Output line wins (REM-OUT-STREAMS) | | Information | text lands in the same console output Intune reads; it is fine as logging as long as the summary Write-Output stays last | A trailing Write-Host, Write-Warning or Write-Verbose -Verbose was reported verbatim: "host-last", "WARNING: warn-last", "VERBOSE: verbose-last" (REM-OUT-HOSTLAST/WARNLAST/VERBLAST); otherwise the last Write-Output line wins (REM-OUT-STREAMS) | | Information | Write-Output calls: Intune reports only the last line (and only its last 2,048 characters). Put the summary last | Last line only (REM-OUT-STREAMS); a 6,000-character line was reported as its final 2,048 characters (REM-OUT-LONG) | +| Warning | 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 | 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) | +| Warning | 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 | 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) | | Error | Write-Error puts text on stderr: the app is reported as not detected even with exit 0 and stdout | exit 0 + "installed" + Write-Error: AgentExecutor reported exitCode -1, applicationDetected False (W32-DET-STDERR) | | Error | Writing to the error stream makes the app "not detected" regardless of exit code and stdout | Any stderr output → not detected (W32-DET-STDERR) | | Error | Nothing is ever written to stdout: exit 0 alone means "not detected". Write-Output a line before exit 0 on the installed path | exit 0 with no stdout → NotDetected → install ran → 0x87D1041C (W32-DET-NOOUT) | @@ -164,7 +166,7 @@ Remediations report only the *last* line of the console output, capped at its la | Information | A non-zero exit fails the rule without looking at the output; fine as a deliberate "not applicable", surprising if the output was meant to decide | "ok" with exit 1 against string equal ok: not applicable (W32-REQ-EXIT1) | | Warning | writes an error record to stderr when its target is missing, which fails the rule. Use -ErrorAction SilentlyContinue or Test-Path first | Any stderr fails the rule (W32-REQ-STDERR) | -Experiments: REM-EXIT-ERRNOEXIT, REM-OUT-HOSTLAST, REM-OUT-LONG, REM-OUT-STREAMS, W32-DET-NOOUT, W32-DET-STDERR, W32-REQ-EXIT1, W32-REQ-FIRSTLINE, W32-REQ-HOST, W32-REQ-LASTLINE, W32-REQ-NOOUT, W32-REQ-STDERR, W32-REQ-TRAIL +Experiments: REM-EXIT-ERRNOEXIT, REM-OUT-HOSTLAST, REM-OUT-LONG, REM-OUT-STREAMS, REM-STDERR-EXIT0, REM-STDERR-SILENT, W32-DET-NOOUT, W32-DET-STDERR, W32-REQ-EXIT1, W32-REQ-FIRSTLINE, W32-REQ-HOST, W32-REQ-LASTLINE, W32-REQ-NOOUT, W32-REQ-STDERR, W32-REQ-TRAIL ## IslPowerShell7Syntax diff --git a/en-US/IntuneScriptLab-Help.xml b/en-US/IntuneScriptLab-Help.xml index 6d1bb74..203ea69 100644 --- a/en-US/IntuneScriptLab-Help.xml +++ b/en-US/IntuneScriptLab-Help.xml @@ -3138,10 +3138,17 @@ The status follows what the device recorded: detection non-zero with no -RemediationPath, as a detect-only remediation records it Fixed detection non-zero, remediation 0, post-detection 0 - Recurred detection non-zero, remediation 0, post-detection still non-zero - Failed remediation exited non-zero (post-detection skipped) + Recurred detection non-zero, remediation 0 with nothing on stderr, post-detection + still non-zero + Failed remediation exited non-zero, or exited 0 having written anything to + stderr; the post-detection is skipped either way, as the agent skips it TimedOut a script hit the timeout +A remediation's stderr decides the verdict and a detection's does not: on the device, a +remediation that wrote a cmdlet error and exited 0 was reported as a script error with the error +text attached and no post-detection, while a detection that wrote one and exited 0 was reported +Without issues. The result carries a warning for the first case. + Each script is launched as the agent launches it: Windows PowerShell 5.1 in the chosen host, -NoProfile -ExecutionPolicy Bypass -File, no -NonInteractive, working directory C:\WINDOWS\system32, run from a copy of the script, output through the OEM code page.