PSScriptAnalyzerでPowerShellスクリプトの品質を守る ── ルール選定とCI導入

· · PowerShell, 静的解析, CI/CD, GitHub Actions, 品質管理, 保守性, 運用改善, スクリプト

更新履歴(初版のみ・2026年07月25日公開)
初版公開
この記事を引用する(DOI: 10.5281/zenodo.21547453)

この記事はZenodoにアーカイブされています。常に最新版へ解決されるDOIと、いま表示している版に固定されたDOIの両方を下に示します。

小村 豪(2026)「PSScriptAnalyzerでPowerShellスクリプトの品質を守る ── ルール選定とCI導入」合同会社小村ソフト. https://doi.org/10.5281/zenodo.21547453

DOI(最新版)
10.5281/zenodo.21547453
DOI(この版)
10.5281/zenodo.21547454

社内のPowerShellスクリプトが増えてくると、必ず「品質のばらつき」の問題が出てきます。エイリアスだらけで読めないスクリプト、平文のパスワードが書かれたスクリプト、タイプミスした変数名が誰にも気づかれないまま残っているスクリプト。書いた本人がいなくなった後で、それを読む人が困るという形で問題が表面化します。

こうした問題の相当部分は、静的解析ツールで機械的に検出できます。PowerShellには公式の静的解析モジュール PSScriptAnalyzer があり、コマンド1つでスクリプト群を解析できます。テストコードを1行も書かずに、初日から効果が出るのが最大の利点です。

この記事では、PSScriptAnalyzerを社内スクリプト資産に導入する実務手順を、「まず有効にすべきルール」「既存資産への段階導入」「CIでの自動チェック」という順にまとめます。テストによる品質確保については「PesterによるPowerShellのテスト整備」を、あわせて参照してください。

1. まず結論

  • PSScriptAnalyzerはPowerShell公式の静的解析モジュールです。Invoke-ScriptAnalyzer でスクリプトやモジュールを解析し、ルール違反を報告します。1
  • 指摘には重大度(Severity)があります。Error / Warning / Information の3段階で、まずErrorだけをゼロにするのが現実的な出発点です。1
  • 設定は PSScriptAnalyzerSettings.psd1 に集約します。Severity IncludeRules ExcludeRules Rules をリポジトリに置き、全員が同じ基準で解析できるようにします。2
  • 個別の抑止は SuppressMessageAttribute + 理由の記述です。ルールごと除外する前に、範囲を絞った抑止で足りないか検討します。3
  • -Fix で自動修正できる指摘もあります。整形は Invoke-Formatter が担当します。14
  • VS CodeのPowerShell拡張機能はPSScriptAnalyzerを内蔵しています。編集中にその場で警告が出るため、CIより先に効きます。5
  • CIの失敗条件は「重大度Error + 名指しした重大ルール」にします。重大度はルールごとに決まっており、平文パスワードの検出(PSAvoidUsingPlainTextForPassword)はWarningです。Errorだけを条件にすると素通りします。6
  • 既存資産には段階導入。「Errorをゼロに」→「変更ファイルのみ厳格に」→「範囲を広げる」の順です。

2. 導入 ── まずは1コマンド

Install-Module -Name PSScriptAnalyzer -Scope CurrentUser

# フォルダー配下をまとめて解析する
Invoke-ScriptAnalyzer -Path 'D:\Scripts' -Recurse |
    Sort-Object Severity, RuleName |
    Format-Table Severity, RuleName, ScriptName, Line, Message -AutoSize

# 重大度別の件数を把握する(棚卸しの第一歩)
Invoke-ScriptAnalyzer -Path 'D:\Scripts' -Recurse |
    Group-Object Severity | Select-Object Name, Count

まずはこの2つを実行して、自社の資産がどういう状態にあるかを数字で把握します。何百件出ても驚く必要はありません。ほとんどの現場で最初はそうなります。

利用可能なルールの一覧と説明は Get-ScriptAnalyzerRule で確認できます。1

Get-ScriptAnalyzerRule | Select-Object Severity, RuleName, CommonName | Sort-Object Severity
Get-ScriptAnalyzerRule -Name PSAvoidUsingWriteHost | Format-List *   # 個別ルールの説明

3. まず効く指摘 ── 実務での優先順位

数十あるルールのうち、社内スクリプトの品質に直結するものを優先度順に挙げます。

ルール 何を検出するか なぜ重要か
PSAvoidUsingPlainTextForPassword パラメーターで平文パスワードを受け取っている 資格情報の平文保持は監査でも指摘される(重大度はWarning)6
PSAvoidUsingConvertToSecureStringWithPlainText 平文からSecureStringを作っている 上と同根。暗号化の意味がなくなる
PSUseDeclaredVarsMoreThanAssignments 代入されたが一度も使われていない変数 変数名のタイプミスを検出できる。実質的なバグ検出
PSAvoidUsingInvokeExpression Invoke-Expression の使用 文字列をコードとして実行するため、注入の温床になる
PSUseShouldProcessForStateChangingFunctions 状態変更を伴う関数に -WhatIf がない 危険な操作を事前確認できない設計を検出
PSAvoidUsingCmdletAliases ls % ? などのエイリアス 対話では便利でも、スクリプトでは可読性を損なう
PSUseApprovedVerbs 承認されていない動詞の関数名 Get-/Set- などの規約に乗らないと発見されにくい
PSAvoidGlobalVars グローバル変数の使用 副作用が読めなくなる。テストも書けない
PSUseSingularNouns 複数形の名詞(Get-Users など) PowerShellの命名規約。他人が推測できる名前にする

とくに PSUseDeclaredVarsMoreThanAssignments は費用対効果が高い指摘です。$fileName に代入したつもりが後段で $fileNmae を参照している、といったタイプミスを「代入されたのに使われていない変数」として拾えるため、静的解析でしか見つからないバグを実際に捕まえます(なお、PowerShellの変数名は大文字小文字を区別しないため、$fileName$filename は同一の変数です。この種の検出で拾えるのは綴り自体が違うケースです)。

4. 設定ファイルでチーム基準を固定する

各自が別の基準で解析していると意味がありません。PSScriptAnalyzerSettings.psd1 をリポジトリに置き、全員とCIが同じ設定を使うようにします。2

# PSScriptAnalyzerSettings.psd1
@{
    # 既定のルールセットを使う
    IncludeDefaultRules = $true

    # 段階導入の第1段階では Error と Warning に限定する
    Severity = @('Error', 'Warning')

    # 社内方針として今は見送るルール(理由をコメントで残す)
    ExcludeRules = @(
        'PSAvoidUsingWriteHost'          # 対話ツールが多く、当面は許容する
        'PSUseSingularNouns'             # 既存の関数名を一斉に変えられないため
    )

    # ルールごとの詳細設定
    Rules = @{
        PSUseCompatibleSyntax = @{
            # 5.1と7の両方で動く必要があるスクリプト群を検査する。
            # TargetVersionsに指定できるのはルールが構文定義を持つバージョンだけ
            # (Get-ScriptAnalyzerRule で確認できる)。未対応の値を書くと
            # 設定の読み込み時にエラーになるので注意
            Enable         = $true
            TargetVersions = @('5.1', '7.0')
        }
        PSPlaceOpenBrace = @{
            Enable             = $true
            OnSameLine         = $true
            NewLineAfter       = $true
            IgnoreOneLineBlock = $true
        }
        PSUseConsistentIndentation = @{
            Enable          = $true
            IndentationSize = 4
            Kind            = 'space'
        }
    }
}
Invoke-ScriptAnalyzer -Path . -Recurse -Settings .\PSScriptAnalyzerSettings.psd1

PSUseCompatibleSyntax は、5.1と7が混在する環境で特に有用です。7専用の構文(三項演算子やパイプライン連鎖演算子など)を5.1向けスクリプトに書いてしまう事故を、実行前に検出できます。移行方針そのものは「Windows PowerShell 5.1とPowerShell 7の違い」を参照してください。

5. 例外は「理由付き」で残す

どうしても指摘に従えない箇所は、ルール全体を無効化するのではなく、その場所だけ抑止します。3

function Show-KsBanner {
    # 対話ツールの装飾表示が目的のため、意図的にWrite-Hostを使う
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute(
        'PSAvoidUsingWriteHost', '',
        Justification = '対話実行専用の表示関数。値は返さない設計のため')]
    [CmdletBinding()]
    param([string] $Title)

    Write-Host ('=' * 60) -ForegroundColor Cyan
    Write-Host $Title -ForegroundColor Cyan
}

Justification を必ず書くのがポイントです。理由のない抑止は、次に読む人にとって「単に警告を消しただけ」と区別がつきません。これはコード上のADR(意思決定記録)のようなもので、考え方は「ADR(アーキテクチャ決定記録)を小さなチームで使う」に通じます。

6. 既存資産への段階導入

数百件の警告を前にして「全部直してから導入」と考えると、まず頓挫します。段階を分けます。

第1段階: 出血を止める(1日) Severity = 'Error' のみを対象にCIへ入れ、これをゼロにします。ここで注意が必要なのは、重大度はルールごとに決まっており、直感と一致しないことです。たとえば PSAvoidUsingPlainTextForPassword の重大度は Warning で、Errorだけを失敗条件にすると引っかかりません。6 資格情報まわりのように「重大度に関係なく落としたい」ルールは、次のようにルール名で明示して失敗条件に加えます。

# 重大度Error + 個別に指定した重大ルールを、CIの失敗条件にする
$mustFix = @(
    'PSAvoidUsingPlainTextForPassword'
    'PSAvoidUsingConvertToSecureStringWithPlainText'
    'PSAvoidUsingUsernameAndPasswordParams'
)
$blocking = $issues | Where-Object { $_.Severity -eq 'Error' -or $_.RuleName -in $mustFix }

第2段階: 新規・変更分を守る(1週間) 変更されたファイルだけを解析対象にします。既存の負債はそのままでも、新しい問題が増えるのを止められます

CIで実行する場合、比較対象のコミットが取得済みである必要がありますactions/checkout は既定で1コミットしか取得しないため、fetch-depth: 0 を指定するか、ベースブランチを明示的にfetchしてください(指定しないと unknown revision で失敗します)。

もう1点、比較先を main に固定しないでください。git diff A...HEAD は「AとHEADの共通の祖先からの差分」を意味するため、develop やリリースブランチ宛のPRで origin/main...HEAD を使うと、そのPRが触っていない変更まで解析対象に入り、無関係なファイルの既存指摘でCIが落ちます。GitHub ActionsではPRのターゲットブランチが GITHUB_BASE_REF に入るので、これを使います。7

さらに、git の失敗を必ず検知してください。PowerShellは既定では、外部コマンドがゼロ以外の終了コードを返しても終了エラーにしません。8 そのため、ベースrefが取得できていない状態では git diff が失敗して出力が空になるだけで、後続は「変更ファイルなし」と解釈し、1ファイルも解析しないままCIが緑になります。差分チェックで最も危険な壊れ方がこれです。実行直後に $LASTEXITCODE を確認して明示的に落としてください(PowerShell 7.3以降なら $PSNativeCommandUseErrorActionPreference = $true を設定する方法もあります)。8

      - uses: actions/checkout@v4
        with:
          fetch-depth: 0        # 差分を取るには履歴が必要
# 変更されたps1/psm1だけを解析する(CIでの差分チェック)
# 比較先をmainに固定しない。developやリリースブランチ宛のPRでは、
# mainとの差分に無関係な変更まで含まれ、触っていないファイルで落ちる。
# PRのターゲットブランチは GITHUB_BASE_REF から取れる(push時は空)
$base = if ($env:GITHUB_BASE_REF) { "origin/$($env:GITHUB_BASE_REF)" } else { 'origin/main' }

# -c core.quotePath=false を付けないと、日本語を含むパスが
# "scripts/\346..." のように引用符+8進エスケープで返り、拡張子の判定から漏れる
$diff = git -c core.quotePath=false diff --name-only "$base...HEAD"

# ネイティブコマンドの失敗は、既定では終了エラーにならない。ベースrefが未取得だと
# gitが失敗して出力が空になり、「変更なし = 解析対象ゼロ = 合格」で素通りする。
# 直後に $LASTEXITCODE を見て明示的に落とす
if ($LASTEXITCODE -ne 0) {
    throw "git diff に失敗しました (exit $LASTEXITCODE)。ベースブランチ $base を取得できていない可能性があります"
}

$changed = $diff |
    Where-Object { $_ -match '\.ps(m|d)?1$' } |   # .ps1 / .psm1 / .psd1 を対象にする
    Where-Object { Test-Path $_ }

# -Pathは単一のパスを受け取るパラメーターなので、配列をそのまま渡すと
# パラメーターバインドで失敗する。1ファイルずつ解析して結果を集約する
$issues = foreach ($file in $changed) {
    Invoke-ScriptAnalyzer -Path $file -Settings .\PSScriptAnalyzerSettings.psd1
}

第3段階: 範囲を広げる(継続) ExcludeRules を1つずつ外し、対応した分だけ厳格にしていきます。リファクタリングの機会に既存ファイルを直し、負債を減らしていきます。

自動修正が効く指摘は -Fix で一括処理できます(適用前に必ず差分を確認してください)。1 整形だけなら Invoke-Formatter が使えます。4

Invoke-ScriptAnalyzer -Path .\Scripts -Recurse -Fix -Settings .\PSScriptAnalyzerSettings.psd1
git diff        # 何が変わったかを必ず目視する

7. CIで自動化する

GitHub Actionsなら、Windowsランナーで数行です。ポイントはErrorで失敗させ、Warningは表示にとどめることです。

name: powershell-lint

on:
  pull_request:
    paths: ['**/*.ps1', '**/*.psm1', '**/*.psd1']

jobs:
  analyze:
    runs-on: windows-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0        # 差分解析に切り替える場合に必要(6章)

      - name: Install PSScriptAnalyzer
        shell: pwsh
        run: |
          Set-PSRepository -Name PSGallery -InstallationPolicy Trusted
          Install-Module PSScriptAnalyzer -Scope CurrentUser -Force

      - name: Analyze
        shell: pwsh
        run: |
          $issues = Invoke-ScriptAnalyzer -Path . -Recurse `
                    -Settings ./PSScriptAnalyzerSettings.psd1

          # 全件をログに出す(警告も見えるようにする)
          $issues | Sort-Object Severity, ScriptName, Line |
              Format-Table Severity, RuleName, ScriptName, Line, Message -AutoSize |
              Out-String -Width 200 | Write-Host

          # 失敗条件 = 重大度Error + 重大度に関係なく許容しないルール
          $mustFix = @(
              'PSAvoidUsingPlainTextForPassword'
              'PSAvoidUsingConvertToSecureStringWithPlainText'
              'PSAvoidUsingUsernameAndPasswordParams'
          )
          $blocking = @($issues | Where-Object { $_.Severity -eq 'Error' -or $_.RuleName -in $mustFix })
          $warns    = @($issues | Where-Object Severity -eq 'Warning')
          Write-Host "Blocking: $($blocking.Count) / Warning: $($warns.Count)"

          # 段階導入が進んだらWarningも条件に加える
          if ($blocking.Count -gt 0) {
              throw "$($blocking.Count) 件の要修正指摘があります"
          }

Pesterのテストと同じワークフローにまとめると、「lintが通る → テストが通る → マージできる」という流れが作れます。WindowsアプリのCI/CD全般の組み方は「WinForms / WPFアプリのCI/CD実践」を参照してください。

CIがない環境でも、月次で Invoke-ScriptAnalyzer を回して結果をCSVに残すだけで、資産の状態は十分に可視化できます。

Invoke-ScriptAnalyzer -Path '\\fileserver\scripts' -Recurse |
    Select-Object Severity, RuleName, ScriptName, Line, Message |
    Export-Csv "D:\棚卸\lint_$(Get-Date -f yyyyMM).csv" -Encoding utf8BOM -NoTypeInformation

8. 実務の定石(判断表)

論点 選択肢 判断の目安
導入順序 Pesterから / PSScriptAnalyzerから 静的解析はテストを書かずに初日から効く
最初の対象 全ルール / Severity=Error + 資格情報系ルールを名指し 全部を条件にすると誰も通らない。重大度はルールごとに決まっている点に注意6
既存の大量警告 全部直す / 変更ファイルのみ厳格に 増加を止めてから、機会を見て減らす
個別の例外 ExcludeRules / SuppressMessageAttribute + Justification 影響範囲を最小に。理由を必ず残す3
設定の共有 各自の設定 / リポジトリの .psd1 CIと開発者で基準を一致させる2
5.1と7の混在 実行して確認 / PSUseCompatibleSyntax 構文レベルの非互換を実行前に検出
自動修正 手作業 / -Fix + 差分確認 適用後に必ず git diff を見る1
編集時のフィードバック CIのみ / VS Code拡張機能 その場で直せるのが最も安い5

9. まとめ

  • PSScriptAnalyzerは公式の静的解析モジュールで、テストを書かずに導入でき、初日から効果が出ます。
  • まずはErrorをゼロにし、次に変更ファイルのみを厳格にチェックする、という段階導入が現実的です。重大度はルールごとに決まっているため、平文パスワード(Warning)のように落としたいルールはルール名で失敗条件に加えます。
  • PSUseDeclaredVarsMoreThanAssignments のように、変数名のタイプミスという実バグを拾えるルールがあります。
  • 設定は PSScriptAnalyzerSettings.psd1 に集約してリポジトリに置き、開発者とCIで基準を揃えます。
  • 例外は SuppressMessageAttribute に理由を書いて残します。ルールごと除外するのは最後の手段です。
  • CIではErrorで失敗、Warningは可視化。Pesterと同じワークフローに載せると、品質の門番が1か所にまとまります。

サンプルコードのダウンロード

この記事で扱ったコードは、そのまま動かせる形にまとめて配布しています。設定ファイル・CI合否判定スクリプト・GitHub Actions の例が入っています。

サンプルコードをダウンロード(zip)

この記事のサンプルは、PowerShell 7.6 で実際に実行して検証しています(Pester 14件)。zipに含まれる Invoke-SampleTests.ps1 を実行すれば、お手元でも同じ検証を再現できます。

# 構文解析 + 静的解析 + Pesterテスト
./Invoke-SampleTests.ps1

設定値(パス、サーバー名、テナントIDなど)は例です。そのまま本番環境で実行せず、自社の環境に合わせて読み替えてください。

関連記事

関連する相談領域

合同会社小村ソフトでは、社内スクリプト資産の棚卸しと品質基準の策定、静的解析・テストのCI導入、属人化した運用スクリプトの保守性改善を扱っています。

参考リンク

  1. Microsoft Learn, PSScriptAnalyzer モジュールの概要. PSScriptAnalyzerがPowerShellスクリプト・モジュール向けの静的解析ツールであること、Invoke-ScriptAnalyzerによる解析と-Path / -Recurse / -Settings / -Fix / -ExcludeRule などのパラメーター、Get-ScriptAnalyzerRuleによるルール一覧の取得、診断結果が重大度(Error / Warning / Information)を持つことについて。  2 3 4 5 6

  2. Microsoft Learn, PSScriptAnalyzer の設定ファイル. 設定ファイル(.psd1)でSeverity・IncludeRules・ExcludeRules・IncludeDefaultRules・Rulesなどを指定できること、-Settingsパラメーターで設定ファイルを渡せること、ルールごとの詳細設定(PSUseCompatibleSyntaxのTargetVersionsや整形系ルールのオプション)について。  2 3

  3. Microsoft Learn, PSScriptAnalyzer のルール抑制. System.Diagnostics.CodeAnalysis.SuppressMessageAttributeによりルール単位・対象単位で診断を抑止できること、RuleName・Target・Justificationの各引数について。  2 3

  4. Microsoft Learn, Invoke-Formatter. 設定に基づいてスクリプトテキストを整形すること、整形ルール(インデント、開き波かっこの位置、空白の扱いなど)を設定ファイルで指定できることについて。  2

  5. Microsoft Learn, Visual Studio Code で PowerShell を使用する. PowerShell拡張機能がPSScriptAnalyzerを利用して編集中に警告を表示すること、書式設定機能を提供することについて。  2

  6. Microsoft Learn, AvoidUsingPlainTextForPassword. パスワードや秘密情報を平文の文字列型パラメーターで受け取るべきではなくSecureStringまたはPSCredentialを使うべきであること、およびこのルールの重大度(Severity Level)がWarningであり常に有効であることについて。関連ルールとしてAvoidUsingConvertToSecureStringWithPlainText(平文からSecureStringを生成すると秘密が保護されないこと)も参照。  2 3 4

  7. GitHub Docs, Variables reference ─ Default environment variables. pull_requestイベントでGITHUB_BASE_REFにPRのターゲットブランチ名が入ること(それ以外のイベントでは空であること)について。三点リーダー記法の意味(明示した2つのrefのマージベースからの差分)はGit公式のgit diffを参照。 

  8. Microsoft Learn, about_Preference_Variables ─ $PSNativeCommandUseErrorActionPreference. ネイティブコマンドのゼロ以外の終了コードが既定では終了エラーにならないこと、PowerShell 7.3で導入されたこの設定を$trueにすると$ErrorActionPreferenceに従って終了エラーになること、直近の外部コマンドの終了コードが$LASTEXITCODEで取得できることについて。  2

同じタグを共有する最新の記事です。さらに近い話題で知識を深められます。

このテーマと近いトピックページです。記事を起点に、関連するサービスや他の記事へ進めます。

よくある質問

この記事のテーマについて、相談時によくある質問をまとめています。

PSScriptAnalyzerを既存スクリプトに掛けたら、警告が数百件出ました。どこから手を付ければよいですか?
いきなり全部を直そうとしないでください。実務的な進め方は、まず重大度Errorのものだけを対象にして、それをゼロにすることです。なお、重大度はルールごとに決まっていて、たとえば平文パスワードを検出するPSAvoidUsingPlainTextForPasswordはWarningです。credentialまわりのように重大度に関係なく落としたいルールがある場合は、CIの失敗条件にルール名を明示して加えてください。次に、これから変更するファイルだけを解析対象にするルールをCIに入れ、新しく増える問題を止めます。既存の警告は「今は許容する」と決めて設定ファイルで除外し、リファクタリングのついでに1つずつ減らしていくのが現実的です。
特定の箇所だけ警告を抑止したいのですが、どうすればよいですか?
その関数やスクリプトにSuppressMessageAttributeを付けます。System.Diagnostics.CodeAnalysis.SuppressMessageAttributeにルール名を指定し、Justificationに理由を書いてください。理由を書くことが重要で、後から読んだ人が「なぜ例外なのか」を判断できます。ルール全体を無効にしたいなら設定ファイルのExcludeRulesに書きますが、こちらは影響範囲が広いので、まず個別抑止で足りないかを検討してください。
Write-Hostを使うと警告が出ます。使ってはいけないのですか?
PSAvoidUsingWriteHostは、値を返すべき場面でWrite-Hostを使うと出力を取り出せなくなる、という設計上の指摘です。対話的なツールで装飾表示を行う目的なら、SuppressMessageAttributeに理由を書いて抑止するのが妥当です。一方、無人実行のスクリプトでWrite-Hostしか使っていない場合は、警告のとおり見直す価値があります。ルールに機械的に従うのではなく、指摘の意図を理解して判断してください。
PesterとPSScriptAnalyzerは、どちらを先に導入すべきですか?
PSScriptAnalyzerを先に入れるほうが、導入コストに対する効果が大きいです。テストコードを一切書かずに、コマンド1つで全スクリプトを解析でき、初日から効果が出ます。Pesterはテストを書く作業が必要なぶん立ち上がりに時間がかかりますが、ロジックの正しさを守れるのはテストだけです。順序としては、まず静的解析をCIに入れて明らかな問題を止め、次に壊れると困る処理からPesterのテストを足していく流れをおすすめします。
CIサーバーがない小さなチームでも導入する意味はありますか?
あります。GitやCIがなくても、共有フォルダーのスクリプト一式に対してInvoke-ScriptAnalyzer -Path . -Recurseを実行するだけで棚卸しになります。結果をCSVに出して「重大度Errorが何件あるか」を月次で見るだけでも、資産の状態が可視化されます。加えて、VS CodeのPowerShell拡張機能はPSScriptAnalyzerを内蔵しているため、編集中にその場で警告が出ます。これだけでも書き方の癖は着実に改善します。

著者プロフィール

記事の著者プロフィールページです。

小村 豪

合同会社小村ソフト 代表

Windows ソフト開発、技術相談、不具合調査を中心に、既存資産が残る案件や原因が見えにくい障害調査に強みがあります。

ブログ一覧に戻る