PowerShell Script governance
Governance is what stops a collection of useful scripts becoming a liability nobody can safely change.
Documentation: Microsoft Learn
Overview
The problems are consistent across organisations. Scripts live on a share or someone's desktop with no history. The person who wrote them has left. Credentials sit in plaintext because it was quick. Nobody knows which of four similar files is the one that runs nightly. And nothing logs, so when output looks wrong there is no way to tell what happened.
A small standard fixes most of it. Everything in source control, so there is history and a review point. A named owner per script, so there is someone to ask. ShouldProcess support on anything destructive, so a dry run is always possible. No secrets in files, using a vault or a gMSA. Structured output rather than console text, so runs can be compared.
Add a review gate for anything that changes production. PSScriptAnalyzer in CI covers the mechanical part; a human reading the diff covers intent.
Deprecate deliberately. A script that is no longer used should be removed or clearly marked, because the most dangerous script in an estate is the one someone runs because it was still there.
Start with an inventory. Most teams do not know how many scripts they depend on.
Inspect this example’s structure
Read the command and its supplied arguments. This example requires real PowerShell or its module; this diagram does not execute it.
$standard = @(
[pscustomobject]@{ Rule = 'Source control';Detail = 'every script in git, no exceptions' }
[pscustomobject]@{ Rule = 'Named owner';Detail = 'a person or team, not a shared mailbox' }
[pscustomobject]@{ Rule = 'ShouldProcess';Detail = 'anything destructive supports -WhatIf' }
[pscustomobject]@{ Rule = 'No secrets';Detail = 'vault or gMSA, never in the file' }
[pscustomobject]@{ Rule = 'Structured logging';Detail = 'objects to CSV or JSON, not Write-Host' })
$standard | Format-Table Rule, Detail- Stage 1$standard
Positional values or no named parameters - Stage 2Format-Table
Positional values or no named parameters
An example to inspect
Preview the example. Sign in for the full lesson and activities.
$standard = @(
[pscustomobject]@{ Rule = 'Source control';Detail = 'every script in git, no exceptions' }
[pscustomobject]@{ Rule = 'Named owner';Detail = 'a person or team, not a shared mailbox' }
[pscustomobject]@{ Rule = 'ShouldProcess';Detail = 'anything destructive supports -WhatIf' }
[pscustomobject]@{ Rule = 'No secrets';Detail = 'vault or gMSA, never in the file' }
[pscustomobject]@{ Rule = 'Structured logging';Detail = 'objects to CSV or JSON, not Write-Host' })
$standard | Format-Table Rule, DetailWhat happens
This prints five rules as a table. They are deliberately few, because a standard nobody remembers is not a standard. The ShouldProcess line is the one with the highest return: it means every destructive script can be run safely once before it is run for real.
Worth remembering
- Source control gives you history and a review point.
- Every script needs a named owner, not a shared mailbox.
- Destructive scripts support -WhatIf through ShouldProcess.
- Structured output beats console text when you need to compare runs.
Primary documentation
Try it
Illustrative output · values vary by environment
Example reader
Inspect this page's example
Run supported commands against the practice data, or inspect the example without executing it.
Ready to inspect.
Prints a five-rule scripting standard as a table.
Show reference output
Illustrative output · values vary by environment
Rule Detail ---- ------ Source control every script in git, no exceptions Named owner a person or team, not a shared mailbox ShouldProcess anything destructive supports -WhatIf No secrets vault or gMSA, never in the file Structured logging objects to CSV or JSON, not Write-Host