アンインストール履歴のCSV化では、固定名を上書きせずrunごとに一意なfull pathを作ります。イベントqueryに失敗した状態と真の0件を分け、export後はCSVを読み戻して件数とRecordId集合が元queryと完全一致した場合だけ成功にします。
run-unique出力とterminating query
Desktopのmsi-removals-{GUID}.csvを使い、開始時に不存在を確認します。Get-WinEventは-ErrorAction Stopで実行し、access・channel・provider errorを0件へ変換しません。真の0件ではCSVを作らずNoEventsとして終了します。
$ErrorActionPreference = 'Stop'
$runId = [guid]::NewGuid().ToString('N')
$desktop = [Environment]::GetFolderPath('Desktop')
$outputPath = Join-Path $desktop "msi-removals-$runId.csv"
if (Test-Path -LiteralPath $outputPath) { throw "出力先が既に存在します: $outputPath" }
$startTime = (Get-Date).AddDays(-30)
try {
$events = @(Get-WinEvent -FilterHashtable @{ LogName='Application'; ProviderName='MsiInstaller'; Id=1034,11724; StartTime=$startTime } -ErrorAction Stop)
} catch {
throw "Application/MsiInstaller query failed; 0件として扱いません: $($_.Exception.Message)"
}
if ($events.Count -eq 0) {
[pscustomobject]@{ Status='NoEvents'; StartTime=$startTime; OutputCreated=$false }
return
}
$expectedRecordIds = @($events.RecordId | ForEach-Object {[long]$_} | Sort-Object -Unique)
if ($expectedRecordIds.Count -ne $events.Count) { throw 'RecordIdが一意ではありません。' }
[pscustomobject]@{ RunId=$runId; Output=$outputPath; EventCount=$events.Count; RecordIds=($expectedRecordIds -join ',') }
export直前にも不存在を再確認する
RecordIdを一意な期待集合として保存し、TimeCreated・EventId・RecordId・MachineName・Messageを出力します。承認後の競合を防ぐため、Export-Csv直前に同じfull pathの不存在を再確認します。
$rows = @($events | Sort-Object TimeCreated,RecordId | ForEach-Object {
[pscustomobject]@{
TimeCreated=$_.TimeCreated.ToUniversalTime().ToString('o')
EventId=[int]$_.Id
RecordId=[long]$_.RecordId
MachineName=[string]$_.MachineName
Message=[string]$_.Message
}
})
if (Test-Path -LiteralPath $outputPath) { throw 'export直前に出力先が出現したため停止します。' }
$rows | Export-Csv -LiteralPath $outputPath -NoTypeInformation -Encoding UTF8 -ErrorAction Stop
if (-not (Test-Path -LiteralPath $outputPath -PathType Leaf)) { throw '新規CSVを確認できません。' }
$created = Get-Item -LiteralPath $outputPath -Force -ErrorAction Stop
if ($created.Attributes -band [IO.FileAttributes]::ReparsePoint) { throw '出力CSVがreparse pointです。' }
新規CSVを読み戻して検証する
Import-Csvのrow数、数値化したRecordIdの件数と集合をquery結果へexact compareします。reparse pointは拒否し、最終path・bytes・SHA-256を結果へ含めます。
$readback = @(Import-Csv -LiteralPath $outputPath -ErrorAction Stop)
$actualRecordIds = @($readback | ForEach-Object {[long]$_.RecordId} | Sort-Object -Unique)
if ($readback.Count -ne $events.Count -or $actualRecordIds.Count -ne $expectedRecordIds.Count) { throw 'CSV件数またはRecordId件数が一致しません。' }
if (($actualRecordIds -join ',') -cne ($expectedRecordIds -join ',')) { throw 'CSVのRecordId集合がquery結果と一致しません。' }
[pscustomobject]@{
Status='VerifiedNewCsv'
FullName=$created.FullName
Length=$created.Length
EventCount=$readback.Count
RecordIds=($actualRecordIds -join ',')
Sha256=(Get-FileHash -LiteralPath $outputPath -Algorithm SHA256 -ErrorAction Stop).Hash
}
受入条件
既存sentinel pathのfixtureではexport前に停止しsentinel bytes/hashが不変です。query errorはfailure、成功した0件queryはNoEvents、1件以上ではreadback countとRecordId setが完全一致します。
ログの意味
MsiInstaller eventはWindows Installerが記録した範囲の証拠です。すべてのinstallerやportable applicationの削除を網羅するとは限らないため、期間・provider・event IDをreportに残します。
保管と個人情報
Messageにはproduct名や利用環境情報が含まれ得ます。新規CSVのACL・保管期間・共有先を確認し、同名reportを上書きして履歴を失わないでください。
CSVを監査証跡として扱うための確認点
RecordIdはApplicationログ内のイベントを追跡する識別子として扱い、製品名だけで同一イベントと決めません。TimeCreatedはUTCのISO 8601へ変換し、EventId・RecordId・MachineNameと同じ行に保存します。これにより、表示言語でMessageの文面が変わっても、どの端末のどの記録を抽出したかを後から照合できます。RecordIdの重複や欠落が見つかった場合はCSVを完成扱いにせず、query条件とログ保持状態を再確認します。
調査期間は実行時刻から30日前までという相対条件なので、証跡には実際のStartTimeと実行時刻も残します。別日に再実行すると対象集合が変わり得るため、前回CSVとの差だけでアンインストール件数の増減を断定しません。比較が必要な場合は、同じ端末・provider・event ID・期間境界で取得した結果を使い、各runのCSVをSHA-256と一緒に保管します。
読み戻しでは行数だけでなく、RecordIdをlongへ変換した一意集合を元queryと比較します。列名が欠ける、数値変換できない、集合が一致しない、出力先がreparse pointである、といった状態はすべて失敗です。また、アクセス拒否やprovider不在などのquery errorを「アンインストール履歴なし」と表示してはいけません。NoEventsはquery自体が成功し0件だった場合だけに限定すると、調査不能と正常な空結果を明確に分けられます。
共有前にはMessage列に含まれる製品名、ユーザー操作の手掛かり、端末情報を確認し、閲覧者と保存期間を必要最小限にします。原本CSVは上書きせず、分析用に加工したコピーとは別に保管してください。加工版で列を削除した場合も、元run ID・元CSVのSHA-256・加工日時を対応付ければ、結論の根拠を原本へ戻って検証できます。

コメント