Skip to content

Add screen-timeout RMM var + sweep power-management to $env: hardening#80

Open
Gumbees wants to merge 3 commits into
mainfrom
enhancement/power-config-screen-timeout-and-env-sweep
Open

Add screen-timeout RMM var + sweep power-management to $env: hardening#80
Gumbees wants to merge 3 commits into
mainfrom
enhancement/power-config-screen-timeout-and-env-sweep

Conversation

@Gumbees

@Gumbees Gumbees commented May 13, 2026

Copy link
Copy Markdown
Contributor

Summary

Two changes folded into one PR on msft-windows/msft-windows-power-management-config.ps1:

  1. New $env:ScreenTimeoutSeconds RMM variable — monitor (display) idle timeout applied AC+DC across every power scheme. Default 900 (15 min); 0 = never; range-guarded 0..86400 via [int]::TryParse. Interactive prompt with re-prompt-on-invalid for non-RMM runs.
  2. RMM hardening sweep — brings this script to the same $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 / $RMMScriptPath resolve to $null under NinjaRMM's env-var injection model and the script silently falls through to the interactive Read-Host (hang under SYSTEM context).

Additionally:

  • Balanced power plan now explicitly set as active after the per-scheme loop (GUID 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-scheme powercfg /setactive.
  • Header rewrite to 15 truthful items. Old header was lying about items Nfs veeam patch1 #1 'Sets Balanced power plan as active' and Local Account Token Filter Policy #2 'Disables display timeout (never turn off display)' — the body never implemented either. Both are now actually implemented; the item count is honest.

Operator-policy changes (heads-up for reviewers)

  • Critical-battery action flattened to shutdown (3) across all chassis, AC and DC, every scheme. Was: 2 (hibernate) on laptops / 3 (shutdown) on desktops via a $IsLaptop chassis guard.
  • powercfg /hibernate off now runs unconditionally regardless of chassis. Was: only ran on desktops.
  • Rationale: dental PMS and older LOB apps crash on resume from hibernation due to stale network state; Fast Startup's hybrid-shutdown amplifies the same issue. Operator preference (Nate, after researching the failure modes documented in Windows 11 24H2/25H2 hibernation-regression threads) overrides Microsoft's documented critical-battery default and the Windows 11 Adaptive Hibernate / Modern Standby behavior.
  • Chassis detection (Win32_SystemEnclosure.ChassisTypes against {8,9,10,14,31,32}) retained as an informational log line only.

Backward-compatibility notes

  • $env:ScreenTimeoutSeconds is new. Operators currently consuming this script under RMM had no screen-timeout setting at all (the merge-base never wrote VIDEOIDLE). 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.
  • Critical-battery / hibernate change is a policy reversal on laptops. Pre-merge: laptop critical battery = hibernate, hibernation kept enabled. Post-merge: laptop critical battery = shutdown, hibernation disabled. Field experience with PMS / LOB apps drove the call. Any client running mixed-criticality workloads on the same fleet that depends on resume-from-hibernate (e.g. long-running calculation tasks) should opt out of this script.
  • File encoding changed from UTF-16 LE to UTF-8 no-BOM with CRLF. This is a one-time normalization to match repo canon (PR Add PuTTY MSI install and per-user session logging scripts #78, PR Fix RDS logon report to read RMM vars from $env: namespace #79, script-template-powershell.ps1 are 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. Unicode check / cross / warn glyphs 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 powercfg calls stubbed (no host modification):

  • Interactive: blank Description prompt re-prompts on empty; blank monitor-timeout prompt accepts default 900
  • Interactive: invalid timeout (-99, abc, 999999) re-prompts with [WARN]; valid value (450) accepted
  • RMM mode ($env:RMM='1'): unset $env:ScreenTimeoutSeconds → default 900
  • RMM mode: $env:ScreenTimeoutSeconds='600' → 600 honored
  • RMM mode: invalid env-var ('abc', '-5', '999999') → [WARN] + fallback to 900
  • RMM mode: $env:ScreenTimeoutSeconds='0' → accepted, logged as 'never turn off'
  • Auto-detect: powershell.exe -NonInteractive -File ... with $env:RMM unset → auto-promotes to RMM mode
  • [FAILURE] path: forced throw inside the main try is caught, $_.ScriptStackTrace logged, $exitCode = 1, Stop-Transcript runs in finally, host exit code reflects 1
  • Balanced-detection: present case + absent case (mocked $powerSchemes) both resolve correctly

End-to-end powercfg /setacvalueindex / powercfg /hibernate off calls 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:ScreenTimeoutSeconds value on AC and DC after the run
  • powercfg /q SCHEME_GUID E73A048D-... 637EA02F-... reports 0x00000003 (shutdown) on DC for every scheme
  • powercfg /availablesleepstates no longer lists Hibernate post-run (or, where Modern Standby is involved, the warning surfaces correctly)
  • All 15 stated behaviors land on both a laptop chassis and a desktop chassis

Lineage / decisions

  • Follows the PR Add PuTTY MSI install and per-user session logging scripts #78 hardening pattern: $env: sweep with IsNullOrWhiteSpace guards, try { ... } catch { } finally { Stop-Transcript } with $transcriptStarted, $exitCode tracked, inner exit 1throw, $_.ScriptStackTrace on catch, [SUCCESS] / [FAILURE] markers, explicit exit $exitCode.
  • Addresses the bare-variable drift carryover in this script that the open PR Fix RDS logon report to read RMM vars from $env: namespace #79 review flagged as a class-of-bug across legacy msft-windows/ scripts.
  • Critical-battery action flattened to shutdown=3 and hibernation disabled across all chassis per operator preference: dental PMS + older LOB apps crash on resume from hibernation due to stale network state. Field experience overrides the Windows 11 Adaptive Hibernate default.
  • Hiberfile-as-encrypted-RAM-dump is a CMMC compliance question the team has not validated. Disabling hibernation globally sidesteps the question for this PR. If a follow-up enables hibernation for any client segment, Mike Schepers (CISO) needs to validate hiberfile encryption posture against the FIPS / CMMC scope before that change ships.
  • Balanced-as-active chosen over dropping the header claim — implementing it is the lower-surprise option for operators expecting a deterministic active plan post-run.

Round 2 — power button + lid AC/DC split + Energy Saver

Commit f1b1159 on top of 8dac4d1. 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):

  • Lid close action AC/DC split. AC = 0 (do nothing) so docked / external-monitor workflows keep the laptop awake when the lid closes; DC = 1 (sleep) so closing the lid on battery still saves power. Round 1 set both to 1. Header item Ticket #702806 - Reboot Specific Time Range #6 rewritten.
  • Power button action = shutdown (3) on AC + DC. New header item Done (#15) #16. PBUTTONACTION GUID 7648EFA3-DD9C-4E3E-B566-50F929386280 under SUB_BUTTONS, verified via powercfg /qh SCHEME_BALANCED SUB_BUTTONS on the dev box. Note: the canonical alias is PBUTTONACTION (not POWERBUTTONACTION as the operator brief had it).
  • Energy Saver auto-on at 20% battery, aggressive policy (DC only). New header item Added "pending reboot" detection. #17. ESBATTTHRESHOLD = e69653ca-cf7f-4f05-aa73-cb833fa90ad4 = 20, ESPOLICY = 5c5bb349-ad29-4ee2-9d0b-2b25270f7a81 = 1 (aggressive — powercfg /qh confirms 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.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 the round-1 decision.

Nits folded in from the multi-agent review pass:

  • Hibernation-verification false-positive at :422 was $hibernationStatus -like "*Hibernate*", which matched the literal "Hibernate" entry under the not-available section of powercfg /availablesleepstates output. Fix: split the output on the "not available" delimiter and check for a bare-line "Hibernate" only in the first (available) chunk. Reviewer suggested an HKLM\...\Power\HibernateEnabled registry 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 in Found power scheme(s): on single-scheme hosts because PS 5.1 does not auto-array-wrap a pipeline that yields exactly one PSCustomObject. Wrapped the powercfg /list pipeline in @(...). Single-scheme hosts (Nate's laptop has only Balanced) now render Found 1 power scheme(s): correctly; multi-scheme hosts unaffected.

Round-2 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'): 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 in powercfg /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 $exitCode is promoted from 0 to 2.

Exit codes (operator action mapping)

Code Meaning Operator action
0 Normal — all settings applied, Balanced present and set active None
1 Failure — caught exception, not-admin, or any [FAIL] path Investigate transcript at %WINDIR%\logs\msft-windows-power-management-config.log
2 Balanced power plan missing — host has only custom/OEM schemes Flag for inventory; no immediate action. Settings still applied to whatever schemes exist

Priority: 1 wins over 2 wins over 0. A real failure on a host that also happens to be missing Balanced still exits 1 (so the operator-action mapping above doesn't get confused).

Smoke tests for the exit-code paths

  • Case 1: Balanced present, laptop fixture -> [OK] Balanced plan ('Balanced') is now active -> LASTEXITCODE = 0
  • Case 2: Balanced absent (single OEM Custom scheme), laptop fixture -> [WARN] Balanced power plan not found; active scheme unchanged + msft-windows-power-management-config.ps1 completed (exit 2) -> LASTEXITCODE = 2
  • Case 3: Balanced absent + induced throw in Step 7 -> [WARN] Balanced... + [FAILURE] INDUCED_FAILURE + completed (exit 1) -> LASTEXITCODE = 1 (priority 1 > 2 confirmed)

🤖 Generated with Claude Code

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>

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Code review skipped — your organization's overage spend limit has been reached.

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.

Gumbees and others added 2 commits May 13, 2026 14:10
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant