Add screen-timeout RMM var + sweep power-management to $env: hardening#80
Open
Gumbees wants to merge 3 commits into
Open
Add screen-timeout RMM var + sweep power-management to $env: hardening#80Gumbees wants to merge 3 commits into
Gumbees wants to merge 3 commits into
Conversation
Two changes folded into one commit on msft-windows-power-management-config.ps1: New behavior: - $env:ScreenTimeoutSeconds controls the monitor (display) idle timeout applied AC+DC across every power scheme. SUB_VIDEO/VIDEOIDLE GUIDs (7516b95f-... / 3c0bc021-...). Default 900s (15 min). 0 = never turn off. Range-guarded 0..86400 via [int]::TryParse; invalid values warn and fall back to default. Interactive prompt with re-prompt-on-invalid for non-RMM runs. Header item #14. - Balanced power plan (GUID 381b4222-...) is now explicitly set active after the per-scheme loop, with a [WARN] fallback when the GUID is absent (custom OEM scheme sets). Previously the per-scheme loop's inner powercfg /setactive left the last-enumerated scheme active as a non-deterministic side effect. Header item #15. RMM hardening sweep (follows PR #78 / open PR #79): - All RMM inputs read via $env:* ($env:RMM, $env:Description, $env:RMMScriptPath, $env:ScreenTimeoutSeconds). Bare $RMM / $Description / $RMMScriptPath previously resolved to $null under NinjaRMM's env-var injection and fell through to Read-Host (hang). - [string]::IsNullOrWhiteSpace() guards on env-var reads. - -NonInteractive command-line auto-detect promotes to RMM mode. - Main body wrapped in try/catch/finally with $transcriptStarted + $exitCode tracked; Stop-Transcript runs in finally. Removes the duplicate guarded Stop-Transcript at every exit path. - Inner exit 1 replaced with throw so the catch block centralizes failure handling. $_.ScriptStackTrace logged on unhandled exception. - [SUCCESS] / [FAILURE] markers for at-a-glance RMM result reading. - Explicit exit $exitCode at end of script. Operator-policy changes: - Critical-battery action flattened to shutdown (3) across all chassis, AC and DC, for every scheme. Was: 2 (hibernate) on laptops / 3 (shutdown) on desktops via $IsLaptop chassis guard. - powercfg /hibernate off now runs unconditionally regardless of chassis. Was: only ran on desktops. - Rationale: dental PMS and older line-of-business apps crash on resume from hibernation due to stale network state; Fast Startup's hybrid-shutdown amplifies the same issue. Operator preference overrides the Windows 11 Adaptive Hibernate / Modern Standby default. - Chassis detection (Win32_SystemEnclosure.ChassisTypes against laptop set {8,9,10,14,31,32}) retained as an informational log line only. Header rewrite: - 15 truthful items. Old header was lying about items #1 ("Sets Balanced power plan as active") and #2 ("Disables display timeout (never turn off display)") which the body never implemented. Both are now actually implemented; item count is honest. Encoding: - File converted from UTF-16 LE (the existing committed format) to UTF-8 no-BOM with CRLF, matching the repo norm established by PR #78 (app-putty/*.ps1), PR #79 (msft-windows-rds-logon-report.ps1) and script-template-powershell.ps1. Removes git's "binary file" treatment of the previous UTF-16 blob; future diffs will be readable text. Pre-existing Unicode markers (check / cross / warn glyphs) replaced with ASCII [OK] / [FAIL] / [WARN] to avoid PowerShell 5.1 Windows-1252 mojibake when reading a BOM-less file containing multi-byte UTF-8 sequences. Output content otherwise unchanged. Smoke-tested on Windows 11 (laptop chassis): - Interactive prompts both required Read-Host paths; Enter accepts defaults; non-integer / out-of-range values re-prompt. - RMM mode: env-var passes through; unset = default 900; invalid ('abc' / '-99' / '999999') falls back to 900 with [WARN]; '0' accepted and logged as 'never turn off'; '-NonInteractive' on the command line auto-promotes RMM mode. - powercfg /setacvalueindex calls were not exercised on the host during smoke-testing (no admin elevation); end-to-end verification is at deploy time on a representative laptop and desktop. Lineage: follows PR #78 hardening pattern; addresses the bare-variable drift carryover in this script noted by the open PR #79 review. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Code review is billed via overage credits. To resume reviews, an organization admin can raise the monthly limit at claude.ai/admin-settings/claude-code.
Once credits are available, push a new commit or reopen this pull request to trigger a review.
Round-2 changes on top of 8dac4d1 in response to operator review of the PR against actual laptop behavior. Three new behaviors + two nit fixes from the multi-agent review pass. New chassis-scoped behaviors (laptops only; desktops skip cleanly): - Lid close action: AC = 0 (do nothing), DC = 1 (sleep). Was 1/1 in round 1. AC=do-nothing keeps docked / external-monitor workflows awake when the lid is closed. DC stays sleep to preserve battery. Header item #6 rewritten. - Power button action: AC + DC = 3 (shutdown). New header item #16. PBUTTONACTION GUID 7648EFA3-DD9C-4E3E-B566-50F929386280 under SUB_BUTTONS. Verified via `powercfg /qh SCHEME_BALANCED SUB_BUTTONS` on the dev box; alias is PBUTTONACTION (not POWERBUTTONACTION as the team-lead brief had it). - Energy Saver auto-on at 20% battery, aggressive policy (DC only). ESBATTTHRESHOLD = e69653ca-cf7f-4f05-aa73-cb833fa90ad4 = 20. ESPOLICY = 5c5bb349-ad29-4ee2-9d0b-2b25270f7a81 = 1 (aggressive; `powercfg /qh` confirms 0=User, 1=Aggressive). New header item #17. Chassis-aware scoping note: chassis detection (Win32_SystemEnclosure ChassisTypes vs the laptop set {8,9,10,14,31,32}) is now load-bearing for these three behaviors. The headline policy items (hibernate off, critical battery = shutdown, screen timeout, hybrid sleep off, hard disk never off, etc.) remain non-chassis-aware per round-1 decision. Nit fixes from the reviewer pass: - Hibernation-verification false-positive: `$hibernationStatus -like "*Hibernate*"` matched the literal "Hibernate" entry in the "not available" section of `powercfg /availablesleepstates` output, emitting `[WARN] Hibernation may still be available` even when hibernation IS off. Fixed by splitting the output on the "not available" delimiter and checking for a bare-line "Hibernate" match only in the first (available) chunk. Reviewer suggested the HKLM HibernateEnabled registry alternative; chose the parser path because it's elevation-free and matches what the script is already doing for status display. - Single-scheme host `$($powerSchemes.Count)` rendered empty because PS 5.1 doesn't auto-array-wrap a pipeline yielding exactly one PSCustomObject. Wrapped the assignment in @(...) so single-scheme hosts (Nate's laptop has only Balanced) render "Found 1 power scheme(s):" correctly. Multi-scheme hosts unaffected. Smoke tests (powercfg stubbed; chassis fixture-driven): - LAPTOP path (chassis 9): writes LIDACTION AC=0/DC=1, PBUTTONACTION AC=3/DC=3, ESBATTTHRESHOLD DC=20, ESPOLICY DC=1; setactive Balanced fires; hibernation-verify reports `[OK] properly disabled` against a realistic /availablesleepstates fixture. - DESKTOP path (chassis 3): logs "Skipping lid + power-button settings (desktop)" and "Skipping Energy Saver settings (desktop)"; no writes to LIDACTION, PBUTTONACTION, ESBATTTHRESHOLD, ESPOLICY; setactive Balanced still fires; non-chassis-aware items still apply. - env-var resolution (default 900, 600, 'abc', '-99', '0'): all five cases behave as before; `[WARN]` text and the (never turn off) label unchanged. Coverage gap acknowledged: dev box has only the Balanced scheme. Multi-scheme hosts (Dell OEM + Balanced + Power Saver) get cold-read validation only. Future smoke on a multi-scheme box recommended. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tracks $missingBalanced alongside $exitCode in the main try/catch/finally
envelope. When the Balanced GUID (381b4222-...) isn't found in
powercfg /list output, the script still applies all settings to whatever
schemes are present (no destructive powercfg /restoredefaultschemes) and
flags the host via a dedicated exit code so RMM aggregation can count
how many endpoints don't ship with Balanced.
Exit code semantics now documented in the header doc block:
0 = clean success
1 = real failure (caught exception, not-admin, any [FAIL] path)
2 = otherwise-clean run, Balanced was missing
Priority: 1 wins over 2 wins over 0.
Implementation:
- $missingBalanced = $false at the top of the main try block (next to
$exitCode = 0).
- The not-found branch in Step 4z sets $missingBalanced = $true and
emits [WARN] Balanced power plan not found; active scheme unchanged.
The WARN wording matches the operator brief verbatim.
- In the finally block, just after the catch could have promoted to
$exitCode = 1: if ($exitCode -eq 0 -and $missingBalanced) { $exitCode = 2 }
This preserves the priority ordering (a real failure leaves code 1
even when Balanced was also missing).
- No destructive recovery on missing Balanced. The settings from the
per-scheme loop above stay applied to whatever scheme set the host
has; only the explicit setactive step is skipped.
Smoke tests (powercfg stubbed; chassis fixture-driven):
- Case 1: Balanced present, laptop -> [OK] Balanced plan is now
active -> LASTEXITCODE = 0
- Case 2: Balanced absent (single OEM Custom scheme), laptop ->
[WARN] Balanced power plan not found; active scheme unchanged ->
LASTEXITCODE = 2
- Case 3: Balanced absent + induced throw in Step 7 -> [WARN]
Balanced... + [FAILURE] INDUCED_FAILURE -> LASTEXITCODE = 1
(priority 1 > 2 confirmed)
Header now lists Exit codes section after item #17; PR #80 body gets
the operator-action mapping for each code in a separate edit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Two changes folded into one PR on
msft-windows/msft-windows-power-management-config.ps1:$env:ScreenTimeoutSecondsRMM variable — monitor (display) idle timeout applied AC+DC across every power scheme. Default900(15 min);0= never; range-guarded0..86400via[int]::TryParse. Interactive prompt with re-prompt-on-invalid for non-RMM runs.$env:baseline as PR Add PuTTY MSI install and per-user session logging scripts #78 (app-putty) and open PR Fix RDS logon report to read RMM vars from $env: namespace #79 (rds-logon-report). Without it, bare$RMM/$Description/$RMMScriptPathresolve to$nullunder NinjaRMM's env-var injection model and the script silently falls through to the interactive Read-Host (hang under SYSTEM context).Additionally:
381b4222-...), with a[WARN]fallback when the GUID isn't present (custom OEM scheme sets). Header item Fixed Reboot Logic #15. Resolves the previous non-deterministic 'last-scheme-wins' behavior of the inner per-schemepowercfg /setactive.Operator-policy changes (heads-up for reviewers)
2(hibernate) on laptops /3(shutdown) on desktops via a$IsLaptopchassis guard.powercfg /hibernate offnow runs unconditionally regardless of chassis. Was: only ran on desktops.Win32_SystemEnclosure.ChassisTypesagainst{8,9,10,14,31,32}) retained as an informational log line only.Backward-compatibility notes
$env:ScreenTimeoutSecondsis new. Operators currently consuming this script under RMM had no screen-timeout setting at all (the merge-base never wroteVIDEOIDLE). After this lands, the new default of 900s applies on every run unless the RMM custom variable is set. Surface this to clients before broad rollout. To preserve 'never turn off display' behavior post-merge, set$env:ScreenTimeoutSeconds=0.script-template-powershell.ps1are all UTF-8 no-BOM). GitHub's first diff may render as binary because the pre-image is UTF-16; subsequent diffs will be readable text. Unicodecheck / cross / warnglyphs in the old stdout output are replaced with ASCII[OK]/[FAIL]/[WARN]to avoid PowerShell 5.1 Windows-1252 mojibake when reading a BOM-less UTF-8 file.Test plan
Smoke-tested on Windows 11 24H2 laptop with
powercfgcalls stubbed (no host modification):-99,abc,999999) re-prompts with[WARN]; valid value (450) accepted$env:RMM='1'): unset$env:ScreenTimeoutSeconds→ default 900$env:ScreenTimeoutSeconds='600'→ 600 honored'abc','-5','999999') →[WARN]+ fallback to 900$env:ScreenTimeoutSeconds='0'→ accepted, logged as 'never turn off'powershell.exe -NonInteractive -File ...with$env:RMMunset → auto-promotes to RMM mode[FAILURE]path: forcedthrowinside the maintryis caught,$_.ScriptStackTracelogged,$exitCode = 1,Stop-Transcriptruns infinally, host exit code reflects1$powerSchemes) both resolve correctlyEnd-to-end
powercfg /setacvalueindex/powercfg /hibernate offcalls were not exercised under elevation during smoke. Verification at deploy time on a representative laptop and desktop is required to confirm:powercfg /q SCHEME_GUID 7516b95f-... 3c0bc021-...reports the configured$env:ScreenTimeoutSecondsvalue on AC and DC after the runpowercfg /q SCHEME_GUID E73A048D-... 637EA02F-...reports0x00000003(shutdown) on DC for every schemepowercfg /availablesleepstatesno longer listsHibernatepost-run (or, where Modern Standby is involved, the warning surfaces correctly)Lineage / decisions
$env:sweep withIsNullOrWhiteSpaceguards,try { ... } catch { } finally { Stop-Transcript }with$transcriptStarted,$exitCodetracked, innerexit 1→throw,$_.ScriptStackTraceon catch,[SUCCESS]/[FAILURE]markers, explicitexit $exitCode.msft-windows/scripts.Round 2 — power button + lid AC/DC split + Energy Saver
Commit
f1b1159on top of8dac4d1. Operator review of round 1 against actual laptop behavior surfaced three missing settings and one false-positive WARN. Reviewer pass on round 1 added a second nit. All folded into one round-2 commit on this same branch — no separate PR.New chassis-scoped behaviors (laptops only; desktops skip the block cleanly):
7648EFA3-DD9C-4E3E-B566-50F929386280under SUB_BUTTONS, verified viapowercfg /qh SCHEME_BALANCED SUB_BUTTONSon the dev box. Note: the canonical alias isPBUTTONACTION(notPOWERBUTTONACTIONas the operator brief had it).ESBATTTHRESHOLD = e69653ca-cf7f-4f05-aa73-cb833fa90ad4 = 20,ESPOLICY = 5c5bb349-ad29-4ee2-9d0b-2b25270f7a81 = 1(aggressive —powercfg /qhconfirms 0=User, 1=Aggressive). Energy Saver doesn't apply on AC so no AC writes.Chassis-aware scoping note (delta from round 1): chassis detection (
Win32_SystemEnclosure.ChassisTypesvs the laptop set{8, 9, 10, 14, 31, 32}) is now load-bearing for these three behaviors. The headline policy items (hibernate off, critical battery = shutdown, screen timeout, hybrid sleep off, hard disk never off, etc.) remain non-chassis-aware per the round-1 decision.Nits folded in from the multi-agent review pass:
:422was$hibernationStatus -like "*Hibernate*", which matched the literal "Hibernate" entry under the not-available section ofpowercfg /availablesleepstatesoutput. Fix: split the output on the "not available" delimiter and check for a bare-line "Hibernate" only in the first (available) chunk. Reviewer suggested anHKLM\...\Power\HibernateEnabledregistry alternative; picked the parser path because it's elevation-free and stays consistent with how the script already inspects sleep states.$($powerSchemes.Count)rendered as an empty string inFound power scheme(s):on single-scheme hosts because PS 5.1 does not auto-array-wrap a pipeline that yields exactly one PSCustomObject. Wrapped thepowercfg /listpipeline in@(...). Single-scheme hosts (Nate's laptop has only Balanced) now renderFound 1 power scheme(s):correctly; multi-scheme hosts unaffected.Round-2 smoke tests (powercfg stubbed; chassis fixture-driven):
setactive Balancedfires; hibernation-verify reports[OK] properly disabledagainst a realistic/availablesleepstatesfixture.Skipping lid + power-button settings (desktop)andSkipping Energy Saver settings (desktop); no writes to LIDACTION, PBUTTONACTION, ESBATTTHRESHOLD, ESPOLICY;setactive Balancedstill fires; non-chassis-aware items still apply.600,'abc','-99','0'): unchanged from round 1.Found 1 power scheme(s):renders correctly on the single-scheme fixture (round-1 bug confirmed via cold read of the same fixture pre-fix).Coverage gap (acknowledged, not blocking): dev box has only the Balanced scheme. Multi-scheme hosts (Dell OEM + Balanced + Power Saver) get cold-read validation only for the per-scheme loop. Future smoke on a multi-scheme box recommended; not gating this PR.
Round 2 addendum — exit code 2 for hosts missing Balanced (commit
0af3d5c)Added telemetry signal so NinjaOne / HaloPSA can aggregate "how many of our endpoints don't ship with Balanced" across the fleet without us shipping a destructive recovery (no
powercfg /restoredefaultschemes). When the Balanced GUID (381b4222-...) isn't found inpowercfg /list, the script still applies all settings to whatever schemes the host has, emits[WARN] Balanced power plan not found; active scheme unchanged, and the final$exitCodeis promoted from0to2.Exit codes (operator action mapping)
01[FAIL]path%WINDIR%\logs\msft-windows-power-management-config.log2Priority:
1wins over2wins over0. A real failure on a host that also happens to be missing Balanced still exits1(so the operator-action mapping above doesn't get confused).Smoke tests for the exit-code paths
[OK] Balanced plan ('Balanced') is now active->LASTEXITCODE = 0[WARN] Balanced power plan not found; active scheme unchanged+msft-windows-power-management-config.ps1 completed (exit 2)->LASTEXITCODE = 2throwin Step 7 ->[WARN] Balanced...+[FAILURE] INDUCED_FAILURE+completed (exit 1)->LASTEXITCODE = 1(priority1 > 2confirmed)🤖 Generated with Claude Code