非エンジニアにバッチを配って一斉実行してもらうときの落とし穴 — ZIP・ダブルクリック・ログの行方
- 第1回:バッチでパスワードを変更したらOutlookとTeamsの認証が消えた
- 第2回:非エンジニアにバッチを配って一斉実行してもらうときの落とし穴(この記事)
- 第3回:Windows・Microsoft 365・Google・NASで共通して使えるパスワードの文字
- 第4回:LanDiskのCSV一括登録で既存ユーザーのパスワードを更新する
新パスワードを埋め込んだバッチファイルを利用者ごとに作り、ダブルクリックで実行してもらう——。手順としては単純だが、配布先が数十人いて、しかもITに不慣れな人が混ざっていると、技術的には正しく動くはずのバッチが思わぬところで崩れる。
この記事は、その「現場で実際に起きた落とし穴」を時系列ではなくテーマ別にまとめたものだ。バッチ自体のパスワード変更ロジックについては第1回を参照してほしい。
前提:バッチはメールにそのまま添付できない
まず配布方法から。多くのメール環境は .bat の添付を自動でブロックする(受信側のメールソフトやウイルス対策ソフトが弾く)。共有フォルダに置く案は、他の利用者からも見えてしまいパスワードの観点で不採用にした。
結局、バッチをZIP圧縮してメール添付する形に落ち着いた。これで配送はできるが、まさにこのZIPが次の落とし穴を生む。
パスワードを埋め込んだバッチは「管理者に昇格→パスワード変更→自己削除」という、マルウェアに似た動きをする。ウイルス対策ソフトに実行をブロックされる可能性があるので、配布前にテスト機で必ず実行確認しておくこと。
落とし穴①:ZIPを解凍せず実行して、ログが消える
バッチには実行結果(成功/失敗)をテキストファイルに残すログ機能を付けていた。集計のためだ。ところが、ある利用者のログだけ回収できなかった。
その人がやったのは、Outlookで届いたZIP添付をダブルクリックで開き、開いたウィンドウの中のバッチを、解凍せずにそのままダブルクリックする、という操作だった。
ここで何が起きるか。ログの出力先はバッチ自身の置き場所(%~dp0)に設定していた。しかしZIPを解凍せずに実行すると、Windowsはバッチを一時フォルダに自動展開して実行する。つまりログは一時フォルダの中に書かれる。そしてバッチが終了すると、Windowsはその一時フォルダごと中身を削除する。ログも一緒に消える。
さらに、バッチは成功時に自分自身を del "%~f0" で削除する設計だったため、痕跡が二重に消えた。技術的にはすべて正しく動いていて、「解凍する」という一手間が抜けただけで、証跡が残らなかったのだ。
Outlookの添付から直接開いた場合、展開先は通常の %Temp% よりさらに奥のOutlook専用のセキュア一時フォルダになり、エクスプローラーからは一段と見つけにくい。回収を試みるなら %Temp% 配下の Temp1_ で始まるフォルダや、エクスプローラーで LOG_ を検索するのが手がかりになる。ただし削除済みなら戻らない。
対策
- ログの出力先を
%~dp0にしない。 デスクトップなど、消えない固定フォルダ(例:%USERPROFILE%\Desktop)に書き出す。これだけで、解凍し忘れてもログは生き残る。 - 手順書で「必ず『すべて展開』してから実行」を最優先で明記する。文章で書くだけでは読まれないので、太字・枠囲みなど目立つ形にする。
- ログが消えても、バッチは画面に実行結果を表示している。「全て成功しました」の表示を見たか本人に確認できれば、成否は判断できる。とはいえ「出てきた画面を全部OKして何も覚えていない」人もいるので、ログを固定フォルダに残す方が確実だ。
落とし穴②:ダブルクリックすると一瞬で閉じる(PowerShellのPATH問題)
テストでは右クリック →「管理者として実行」でずっと検証していて、何の問題もなかった。ところが本番を想定してダブルクリックで実行したところ、一瞬コマンドプロンプトが出てすぐ閉じる。ログも出ず、バッチも消えない。
非管理者のコマンドプロンプトから手動で実行すると、原因が見えた。
管理者権限で再実行します...
'powershell' は、内部コマンドまたは外部コマンド、
操作可能なプログラムまたはバッチ ファイルとして認識されていません。
バッチは「管理者権限がなければ、PowerShellの Start-Process -Verb RunAs で自分自身を昇格して再起動する」設計だった。そのPowerShellを短縮名(powershell)で呼んでいたため、環境変数 PATH にPowerShellのパスが含まれない環境では、昇格処理そのものが起動できずに即終了していた。
対策:PowerShellはフルパスで呼ぶ
UAC昇格でも、その後のPowerShell呼び出しでも、短縮名ではなくフルパスで指定すればPATHに依存しなくなる。
%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe -Command "Start-Process '%~f0' -Verb RunAs"
配布先のPCは環境がそろっていない。自分のテスト機で動いたからといって、全台で動くとは限らない。外部コマンドはフルパスで呼ぶのが、一斉配布バッチの基本姿勢になる。
「以前はダブルクリックすると昇格ダイアログが出たのに、今は出なくなった」——これはWindowsの仕様変更ではなく、このPATH問題であることが多い。OSのせいにする前に、バッチが昇格処理にたどり着けているかを疑う。
落とし穴③:SmartScreen/確認ダイアログが想定より増える
署名のないバッチや、ダウンロード・メール経由で届いたファイルには、WindowsがSmartScreen(スマートアプリコントロール)の警告を出すことがある。「WindowsによってPCが保護されました」といった青い画面だ。
利用者から見れば、想定していたUACの「このアプリがデバイスに変更を加えることを許可しますか?」だけでなく、別の確認ダイアログが追加で1〜2回出ることになる。手順書に「『はい』を押すダイアログは1回だけ」と書いていると、増えたダイアログで利用者が手を止めてしまう。
対策
- 実行は右クリック →「管理者として実行」を基本線にする。ダブルクリック自動昇格も用意しつつ、迷ったらこちらを案内する。SmartScreenの警告が出た場合は「詳細情報 →(内容を確認のうえ)実行」で進む、と手順書に書いておく。
- 手順書には「確認のダイアログが複数回出ることがあります。内容を読んで進めてください」と、増える前提で書く。出る回数を固定で書かない。
- ただし、これらは本来正規のセキュリティ警告だ。「全部OKしてください」と機械的に案内するのは避け、何のための警告かを一言添える。実際、出てくるダイアログを内容も読まず連打する人ほど、後述の別アカウント紐付けなどの事故を起こしやすい。
落とし穴④:自己削除と「2回実行」の罠
バッチは全処理が成功したら自分を削除する設計にした。成功したファイルが残っていると、誰かが再実行して二重処理になるのを防ぐためだ。ここに2つの注意がある。
1ファイル1ユーザーだと、複数台で実行できない
1人が複数台のPCを使っている場合、ユーザー単位で1ファイルにすると、1台目で成功した瞬間にファイルが消え、2台目で実行できなくなる。ZIPから再展開すれば対処できるが、非技術者には酷だ。バッチはPC単位で作ることで、複数台でも順に実行できるようにした。
あわせて、「正しい人が、間違ったPCで実行する」事故も防ぎたい。バッチの先頭で対象PC名(%COMPUTERNAME%)が想定どおりかをチェックし、違えば中断する。次の数行をバッチの冒頭(管理者昇格の直後)に入れておけばよい。
REM ===== 対象PCのチェック =====
REM TARGET_PC にこのバッチを実行してほしいPC名を入れておく
set "TARGET_PC=PC-EIGYO-01"
if /i not "%COMPUTERNAME%"=="%TARGET_PC%" (
echo エラー: このバッチは別のPC用です。
echo 対象PC : %TARGET_PC%
echo このPC : %COMPUTERNAME%
pause
exit /b 1
)
/i は大文字小文字を区別しない比較。%COMPUTERNAME% はWindowsが常に持っている環境変数なので追加準備はいらない。ついでに、ログオン中のユーザーIDが想定どおりかも次の数行で確認できる(whoami は PC名\ユーザー名 を返すので、\ 区切りの2つ目を取り出す)。
REM ===== ログオンユーザーIDのチェック =====
set "TARGET_USER=user01"
for /f "tokens=2 delims=\" %%a in ('whoami') do set "CURRENT_USER=%%a"
if /i not "%CURRENT_USER%"=="%TARGET_USER%" (
echo エラー: ログイン中のユーザーが対象と異なります。
echo 対象ユーザー : %TARGET_USER%
echo 現在のユーザー : %CURRENT_USER%
pause
exit /b 1
)
2回実行すると「失敗」表示で混乱する
第1回で触れた「変更」方式(NetUserChangePassword)は旧パスワードを必要とする。1回目の実行でパスワードが新しくなった後、同じバッチをもう一度実行すると、バッチに埋まっている旧パスワードはもう一致しないのでパスワード変更が「失敗」と表示される。
これは破壊的ではない(すでに新パスワードになっている)が、画面に「失敗」と出れば利用者は不安になる。実際、「ログが消えた人」のPCも、調べると1回目で変更は成功しており、2回目以降が旧パスワード不一致で失敗していただけだった。新パスワードでリモート接続できたことが、1回目成功の動かぬ証拠になった。
運用上のもう一点。ZIPの中には平文のパスワードを含むバッチが残る。 バッチは成功時に自己削除するが、配ったZIPは利用者の手元に残る。全員の成功を確認したあとで、ZIPの削除を案内するとよい。
そもそも数十個のバッチとログを手作業で扱わない
ここまでの落とし穴は「実行してもらう側」の話だが、配る側・集める側にも限界がある。数十人分のバッチを手書きで作り、戻ってきた数十個のログを目視で確認するのは現実的ではない。ここは仕組みで殴る。
バッチはExcelから自動生成する
利用者ID・PC管理番号・新パスワードなどを一覧にしたExcel(マクロ付き)を用意し、1行=1台ぶんのバッチを自動生成するようにした。あわせて次のことも自動化すると、配布作業がぐっと楽になる。
- 生成と同時にZIPも作る(メール添付用)。バッチ単体とZIPの両方を出力しておくと、中身の確認は素のバッチで、配布はZIPで、と使い分けられる。
- 送付先ごとのフォルダに振り分けて出力する。誰に何を送るかが一目でわかり、添付ミスを防げる。
- 生成前にパスワードの形式チェックを通す(文字数・使用文字・全角混入など)。形式違反のパスワードでバッチを作ってしまうと、実行時に初めて事故が判明する。入口で弾く。形式の考え方は第3回にまとめている。
サンプル:Excel VBAでバッチを自動生成する(形式チェック+ZIP圧縮込み)
「ユーザー一覧」シートに A:PC名 / B:ユーザーID / C:新パスワード / D:旧パスワード、「設定」シートの B1 に出力先フォルダを入れておく前提の例。実運用に合わせて列やパスを調整して使う。
' === バッチ一括生成(送付先フォルダ分け+ZIPまで) ===
Sub GenerateBatchFiles()
Dim wsCfg As Worksheet, wsUser As Worksheet
Set wsCfg = ThisWorkbook.Sheets("設定")
Set wsUser = ThisWorkbook.Sheets("ユーザー一覧")
Dim outRoot As String
outRoot = wsCfg.Range("B1").Value ' 出力先フォルダ
If Right(outRoot, 1) <> "\" Then outRoot = outRoot & "\"
Dim r As Long, made As Long
For r = 2 To wsUser.Cells(wsUser.Rows.Count, 1).End(xlUp).Row
Dim pcName$, uid$, newPw$, oldPw$
pcName = Trim(wsUser.Cells(r, 1).Value)
uid = Trim(wsUser.Cells(r, 2).Value)
newPw = Trim(wsUser.Cells(r, 3).Value)
oldPw = Trim(wsUser.Cells(r, 4).Value)
If uid = "" Then GoTo NextRow
' --- 形式チェック:NGならバッチを作らずに印を付ける ---
If Not IsValidPassword(newPw) Then
wsUser.Cells(r, 5).Value = "NG(形式不正)"
GoTo NextRow
End If
wsUser.Cells(r, 5).Value = "OK"
' --- 送付先フォルダ(ユーザーIDごと)を作成 ---
Dim dstDir$
dstDir = outRoot & uid & "\"
If Dir(dstDir, vbDirectory) = "" Then MkDir dstDir
' --- バッチ本文を組み立て、Shift-JISで書き出し ---
Dim batPath$, body$
batPath = dstDir & "PW_" & pcName & "_" & uid & ".bat"
body = MakeBatch(pcName, uid, newPw, oldPw) ' ←バッチ本文を返す自作関数
WriteShiftJis batPath, body ' 日本語を含むので Shift-JIS
' --- 同じ場所に ZIP も作る(メール添付用) ---
MakeZip batPath, dstDir & "PW_" & pcName & "_" & uid & ".zip"
made = made + 1
NextRow:
Next r
MsgBox made & " 件のバッチ(.bat+.zip)を生成しました。"
End Sub
' テキストを Shift-JIS で保存(バッチは日本語を含むため)
Sub WriteShiftJis(path$, body$)
With CreateObject("ADODB.Stream")
.Type = 2: .Charset = "Shift_JIS": .Open
.WriteText body
.SaveToFile path, 2 ' 2 = 上書き保存
.Close
End With
End Sub
' PowerShell の Compress-Archive で ZIP 化(同期実行で確実に作る)
Sub MakeZip(srcFile$, zipPath$)
Dim ps$
ps = "powershell -NoProfile -Command ""Compress-Archive -Path '" & srcFile & _
"' -DestinationPath '" & zipPath & "' -Force"""
CreateObject("WScript.Shell").Run ps, 0, True ' 第3引数 True = 完了を待つ
End Sub
' パスワード形式チェック:15〜20文字/半角ASCIIのみ/!%" 禁止/4種中3種以上
Function IsValidPassword(pw$) As Boolean
Dim i As Long, c As Long
Dim hasU As Boolean, hasL As Boolean, hasD As Boolean, hasS As Boolean
IsValidPassword = False
If Len(pw) < 15 Or Len(pw) > 20 Then Exit Function
For i = 1 To Len(pw)
c = AscW(Mid(pw, i, 1))
If c < 32 Or c > 126 Then Exit Function ' 全角・全角スペース・制御文字を排除
If InStr("!%" & Chr(34), Mid(pw, i, 1)) > 0 Then Exit Function ' バッチ禁止文字 ! % "
Select Case c
Case 65 To 90: hasU = True
Case 97 To 122: hasL = True
Case 48 To 57: hasD = True
Case Else: hasS = True
End Select
Next i
Dim kinds As Long
kinds = Abs(hasU) + Abs(hasL) + Abs(hasD) + Abs(hasS)
IsValidPassword = (kinds >= 3)
End Function
MakeBatch は、これまで紹介したUAC昇格・PC名チェック・パスワード変更などを組み立てて文字列で返す自作関数に置き換える。ポイントは、NGのパスワードはバッチを作らずスキップし、生成漏れを一覧(E列)で見えるようにしておくこと。
ログは統一フォーマットで出して、一括取り込み
バッチが吐くログをタブ区切りなどの統一フォーマットにしておけば、Excelマクロでフォルダ内のログをまとめて読み込み、PC単位・利用者単位で自動集計できる。
ログは五月雨式に届く。届くたびに差分だけ足そうとすると重複や取りこぼしが起きやすいので、取り込みのたびに「集計シート側」を一度クリアし、フォルダ内の全ログを毎回まるごと読み直す方式が運用は楽だった。
初期化するのは取り込み先のExcelシートであって、ログの入っているフォルダではない。フォルダの中身を消すと集めたログそのものが失われる。「シートをクリア → フォルダの全ログを再読み込み」の順を間違えないこと。
同じPC・同じ利用者のログが複数あるときは(=二重実行など)、ファイル名や中身の日時を比較して最新のものだけ採用すれば自然に整理できる。日時をゼロ埋めの YYYY/MM/DD HH:MM:SS 形式でログに出しておくと、文字列のまま大小比較で新旧を判定できる。
サンプル:Excel VBAでログを一括取り込み(シートを初期化+最新のみ採用)
「設定」シートの B2 にログ格納フォルダ、「ログ集計」シートに結果を出す前提。バッチは PC名 [Tab] ユーザーID [Tab] PW変更 [Tab] … [Tab] 実行日時 の形式でログを書く想定。
' === ログ一括取り込み(集計シートを初期化してから全件再読み込み) ===
Sub ImportLogs()
Dim wsCfg As Worksheet, wsOut As Worksheet
Set wsCfg = ThisWorkbook.Sheets("設定")
Set wsOut = ThisWorkbook.Sheets("ログ集計")
' --- ★初期化するのは「シート」。フォルダには触れない ---
wsOut.Cells.Clear
wsOut.Range("A1:G1").Value = _
Array("PC名", "ユーザーID", "PW変更", "メンテAcct", "PIN", "残留数", "実行日時")
Dim folder$
folder = wsCfg.Range("B2").Value
If Right(folder, 1) <> "\" Then folder = folder & "\"
' 同一PC×IDは「最新の1行」だけ残す
Dim latest As Object
Set latest = CreateObject("Scripting.Dictionary")
Dim f$
f = Dir(folder & "LOG_*.txt")
Do While Len(f) > 0
Dim lines() As String, i As Long
lines = Split(ReadShiftJis(folder & f), vbCrLf)
For i = 0 To UBound(lines)
If InStr(lines(i), vbTab) > 0 Then
Dim c() As String
c = Split(lines(i), vbTab)
If UBound(c) >= 6 Then
Dim key$, dt$
key = c(0) & "|" & c(1) ' PC名|ユーザーID
dt = c(6) ' 実行日時(ゼロ埋め)
' 未登録、または日時が新しければ採用
If Not latest.Exists(key) Then
latest(key) = lines(i)
ElseIf dt > Split(latest(key), vbTab)(6) Then
latest(key) = lines(i)
End If
End If
End If
Next i
f = Dir
Loop
' --- 採用した行をシートへ書き出し ---
Dim r As Long, k As Variant
r = 2
For Each k In latest.Keys
Dim cc() As String, j As Long
cc = Split(latest(k), vbTab)
For j = 0 To UBound(cc)
wsOut.Cells(r, j + 1).Value = cc(j)
Next j
r = r + 1
Next k
MsgBox "ログ取り込み完了:" & latest.Count & " 件"
End Sub
' Shift-JISのテキストファイルを丸ごと読み込む
Function ReadShiftJis(path$) As String
With CreateObject("ADODB.Stream")
.Type = 2: .Charset = "Shift_JIS": .Open
.LoadFromFile path
ReadShiftJis = .ReadText
.Close
End With
End Function
毎回 wsOut.Cells.Clear で集計シートだけを空にしてから読み直すので、二重実行ぶんも最新優先で勝手に整理される。フォルダのログは消さないので、後から検証もできる。
まとめ:詰まるのは技術ではなく「人の手順」
振り返ると、ここで挙げた落とし穴の大半はバッチのロジックの誤りではない。ZIPを解凍しない、フルパスを書いていない、想定外のダイアログ、二重実行——いずれも「正しく動くコードが、現場の操作で空振りする」種類の問題だ。
手作業でやっていた操作を肩代わりするツール(今回ならバッチファイル)を、ITに詳しくない人に渡して各自で実行してもらうときは、コードが正しいことと、現場で正しく実行されることは別物だと割り切る。ログは消えない場所へ、外部コマンドはフルパスで、手順書は「増えるダイアログ」を前提に、配布と集計は仕組みで。これだけで、一斉作業の取りこぼしは大きく減る。