Exchange Online の Start-HistoricalSearch(ヒストリカルサーチ)で作成したメッセージ追跡レポートを、GUIと同じCSVで取り出したい。PowerShellだけで完結できるのか、現実的な半自動化のやり方まで、運用目線で整理します。
やりたいこと:Start-HistoricalSearch の結果を「CSVとして手元に残す」
Exchange Online のメッセージ追跡は、通常のメッセージトレース(比較的短期間の追跡)だけでなく、より過去の期間や大量の結果を対象にした「ヒストリカルサーチ(Historical Search)」を実行できるケースがあります。その代表的なフローが Start-HistoricalSearch で、検索ジョブを開始し、完了後に管理ポータル(ブラウザ)から CSVレポートをダウンロードする形です。
現場でよく出る要望は、次の2つです。
- GUI(管理センター等)で「Download the report」を押して取得できるのと同等の CSVファイル を、PowerShell側で取得したい
- 可能なら PowerShellや.batでダウンロードまで自動化 して、運用の手間を減らしたい
結論から言うと、「CSVを生成するジョブの管理」 と 「CSVファイルのダウンロード」 は別物で、後者が壁になります。まず、全体像を整理しましょう。
| やりたいこと | できる/できない | 現実的な手段 |
|---|---|---|
| 履歴検索ジョブの作成・実行(Start-HistoricalSearch) | できる | PowerShell(Exchange Online)で実行 |
| ジョブの状態確認、対象ジョブの抽出(Get-HistoricalSearch) | できる | PowerShellで一覧化・絞り込み |
| GUIと同じCSVのダウンロード(FileUrlを直接取得) | 難しい | ブラウザ(GUI)を前提にするのが安全 |
| ダウンロード操作の完全自動化 | 非現実的になりやすい | 半自動化(ブラウザでまとめて開く)を落としどころにする |
Start-HistoricalSearch の基本:ジョブ作成と進捗確認
まずは「どのジョブが、いつ、どんな条件で作られたか」を運用として追えるようにしておくと、あとでダウンロードや整理が楽になります。スクリプトを組む前に、手元の環境でパラメータや戻り値を確認しましょう。
# 使えるパラメータや説明を確認(環境差・更新差を吸収するために最初にやる)
Get-Help Start-HistoricalSearch -Detailed
Get-Help Get-HistoricalSearch -Detailed
実行例(代表的な形)は次のイメージです。実際に使えるパラメータ名は Get-Help の出力に合わせて調整してください。
# 例:過去期間のメッセージ追跡レポートを作成し、完了通知を受け取る
Start-HistoricalSearch `
-ReportTitle "MessageTrace_Incident_202512" `
-StartDate (Get-Date).AddDays(-30) `
-EndDate (Get-Date) `
-NotifyAddress "[email protected]"
# 例:ジョブ一覧を確認
Get-HistoricalSearch | Sort-Object CreatedTime -Descending | Select-Object -First 10
ジョブが完了するまでは FileUrl が空だったり、ダウンロードできない状態だったりします。ダウンロードに進む前に、まずは Status が Completed(完了)になっているか をチェックするのが鉄則です。
# 例:特定ジョブの詳細を確認(JobIdは実際の値に置き換え)
Get-HistoricalSearch -JobId <ジョブID> | Format-List
結論:PowerShellだけで「GUIと同じCSV」を自動ダウンロードするのはほぼ不可能
一見すると、Get-HistoricalSearch で取得できる FileUrl を使って、PowerShell の Invoke-WebRequest(または curl)で落とせそうに見えます。例えば、次のような発想です。
# 例:ジョブIDからFileUrlを取り出し、HTTPで取得したい
$url = (Get-HistoricalSearch -JobId <ジョブID>).FileUrl
Invoke-WebRequest -Uri $url -OutFile "C:\\Reports\\HistoricalSearch.csv"
しかし実際には、多くの環境で次の理由により失敗します。
- FileUrl は「ログイン済みのブラウザ前提」のURL になりやすく、アクセスに 有効な認証状態(トークン/クッキー) が必要
- その認証状態を PowerShell だけで再現するのが難しい(特に MFA(多要素認証) 利用時)
- 仮に一時的に成功しても、仕様変更や期限切れ に弱く、運用で事故りやすい(再現性が低い)
つまり、実務的には「PowerShellだけでCSVを直接落とす」のではなく、GUIダウンロードを前提に、周辺作業をPowerShellで効率化する のが現実解になります。
なぜ Invoke-WebRequest で落とせないのか
ここを理解しておくと、「どこまで自動化できるか」の線引きがしやすくなります。
ブラウザのダウンロードは、裏で認証が成立している
管理ポータル(Exchange 管理センター等)でダウンロードできるのは、ブラウザがすでにサインイン済みで、必要な認証情報を保持しているからです。ボタンを押した瞬間に、ブラウザは以下のような動きをします(概念図)。
- サインイン状態に基づいて、ダウンロード先へアクセスするための認証(トークン/クッキー)を利用
- 認証が通った場合のみ、CSV本体のダウンロードが開始される
- URL自体が短時間で失効する、またはサインイン状態が必須な作りになっていることが多い
この「ブラウザが持っている認証状態」を、PowerShellのHTTPリクエストで完全に再現しようとすると、手間が跳ね上がります。
MFA環境では「トークンを自動で用意する」が特に難しい
MFAを有効にしているテナントでは、ユーザーのサインインが対話型(認証アプリ、SMS、FIDO2など)になりやすく、PowerShellスクリプト単体で「常に同じ手順でトークンを得る」ことが難しくなります。
さらに、セキュリティ上の理由で、トークンやクッキーの扱いを自作実装するのはおすすめできません。仮に動いても、担当者交代や端末変更、条件変更(条件付きアクセス)で壊れやすく、“毎月どこかで落ちる自動化” になりがちです。
| よくある現象 | PowerShell側で起きること | 原因の典型 |
|---|---|---|
| ダウンロードできずエラーになる | 401/403、またはHTML(サインイン画面)が保存される | 認証が通っていない(トークン/クッキー不足) |
| リダイレクトが繰り返される | 302の連続、最終的にログインURLへ | ブラウザ前提のフローで、PowerShellのHTTPだけでは追従しきれない |
| 昨日は取れたのに今日は取れない | 同じスクリプトで不安定 | URLの有効期限、条件付きアクセス、セッション切れ |
現実解:PowerShellで「対象ジョブを抽出」し、ブラウザで一気にダウンロードする
完全自動が難しいなら、「人がやるべきクリック操作」だけを残し、そこまでの準備をPowerShellで固める のが一番効きます。要は、次の作戦です。
- PowerShellで Get-HistoricalSearch を使い、欲しいジョブだけを抽出する
- 抽出したジョブの FileUrl を、既定ブラウザやEdgeで まとめて開く
- ブラウザ上で「Download the report」を順番に押して保存する(この部分だけ手動)
「検索画面を開く→探す→開く」を省けるので、件数が多いほど効きます。
事前準備(ここが地味に重要)
- ダウンロードに使うブラウザで、あらかじめ管理ポータルにサインインしておく(同一セッションを使い回せる)
- 可能なら、管理操作専用のブラウザプロファイル(Edge/Chromeのプロファイル)を作っておく
- ダウンロード先フォルダを決め、ファイル名のルール(例:レポート名+日付)を決めておく
まずはプロパティ確認:環境差を吸収する
Get-HistoricalSearch が返すプロパティは、管理センターの仕様や更新で微妙に見え方が変わることがあります。スクリプトを固める前に、まず自分の環境で何が取れるか確認しておくと失敗しにくいです。
# 1件だけ取り出して、どんなプロパティがあるか確認
Get-HistoricalSearch | Select-Object -First 1 | Get-Member
# よく見る項目だけ一覧(例)
Get-HistoricalSearch | Select-Object -First 5 JobId,ReportTitle,Status,CreatedTime,StartDate,EndDate,NotifyAddress,FileUrl
対象ジョブを絞り込む(例:タイトル/通知先/完了ステータス)
「とりあえず全部開く」とタブ地獄になります。業務で使う絞り込み条件を決めておくのがポイントです。
# 例:完了済みのジョブだけを対象にし、タイトルや通知先で絞り込む
$jobs = Get-HistoricalSearch -ResultSize Unlimited | Where-Object {
$_.Status -eq "Completed" -and
(
$_.ReportTitle -like "*インシデント*" -or
$_.NotifyAddress -contains "[email protected]"
)
}
# 目視で確認しやすい形に整形
$jobs | Sort-Object CreatedTime -Descending |
Select-Object JobId, ReportTitle, Status, CreatedTime, NotifyAddress
上記の ReportTitle や NotifyAddress は、運用の実態に合わせて調整してください。例えば「依頼番号」「案件名」「チケット番号」をタイトルに含める運用にすると、あとから抽出が簡単になります。
FileUrl をブラウザでまとめて開く(Edge 例)
絞り込んだ結果を、ブラウザで順番に開きます。Edgeの実行ファイル名は環境により異なることがありますが、一般的には msedge.exe で起動できます。
# Edgeでまとめて開く(開きすぎ防止のため少し待つ)
$jobs | ForEach-Object {
if ([string]::IsNullOrWhiteSpace($_.FileUrl)) { return }
Start-Process "msedge.exe" -ArgumentList $_.FileUrl
Start-Sleep -Milliseconds 300
}
ポイントは、InPrivate(シークレット)を安易に使わないことです。InPrivateはセッションが分離されるため、ポータルにログイン済みでも別セッション扱いになり、毎回サインインが要求されることがあります。運用で「サインインし直しが多発」する場合は、通常ウィンドウ+専用プロファイル運用の方が楽です。
選択式にして開く件数をコントロールする(Out-GridView)
GUI操作の残る運用では「必要な分だけ開く」方が安全です。Windows PowerShell なら Out-GridView を使って選択式にもできます。
# 表で見て、必要な行だけ選んで開く
$selected = $jobs |
Select-Object JobId, ReportTitle, Status, CreatedTime, NotifyAddress, FileUrl |
Out-GridView -Title "開きたいHistoricalSearchを選択してOK" -PassThru
$selected | ForEach-Object {
Start-Process "msedge.exe" -ArgumentList $_.FileUrl
}
サーバー上で実行する、あるいは PowerShell 7 で Out-GridView が使えない場合は、後述の「HTMLリンク集を生成」がおすすめです。
作業をさらに短縮する:リンク集(CSV/HTML)を自動生成する
「開くだけ」の自動化でも効果はありますが、チーム運用では「誰がどれをダウンロードしたか」が曖昧になりやすいです。そこで、ジョブ一覧をエビデンスとして残しつつ、クリック導線も用意する と運用が安定します。
ジョブ一覧をCSVにエクスポートする
$timestamp = Get-Date -Format "yyyyMMdd_HHmmss"
$outCsv = "C:\\Reports\\HistoricalSearch_Jobs_$timestamp.csv"
$jobs |
Select-Object JobId, ReportTitle, Status, CreatedTime, StartDate, EndDate, NotifyAddress, FileUrl |
Export-Csv -Path $outCsv -NoTypeInformation -Encoding UTF8
$outCsv
このCSVを残しておくと、「いつ、どの条件で、どのジョブを対象にしたか」が後から追えます。監査対応や引き継ぎにも強くなります。
クリックしやすいHTML(ローカルのリンク集)を生成する
大量のURLをタブで開くより、ローカルにリンク集HTMLを作り、そこから順に開く方が事故りにくいことがあります(誤って関係ないURLを開く、タブがどれかわからない、など)。
$timestamp = Get-Date -Format "yyyyMMdd_HHmmss"
$outHtml = "C:\\Reports\\HistoricalSearch_Links_$timestamp.html"
$rows = $jobs | Sort-Object CreatedTime -Descending | ForEach-Object {
$title = [System.Net.WebUtility]::HtmlEncode([string]$_.ReportTitle)
$jobid = [System.Net.WebUtility]::HtmlEncode([string]$_.JobId)
$ctime = [System.Net.WebUtility]::HtmlEncode([string]$_.CreatedTime)
$url = [System.Net.WebUtility]::HtmlEncode([string]$_.FileUrl)
"<tr><td>$ctime</td><td>$jobid</td><td>$title</td><td><a href='$url' target='_blank'>開く</a></td></tr>"
}
@"
<!doctype html>
<html lang='ja'>
<meta charset='utf-8'>
<title>HistoricalSearch Links</title>
<body>
<h1>HistoricalSearch レポートリンク集</h1>
<table border='1' cellpadding='6' cellspacing='0'>
<thead><tr><th>CreatedTime</th><th>JobId</th><th>ReportTitle</th><th>FileUrl</th></tr></thead>
<tbody>
$($rows -join "`n")
</tbody>
</table>
</body>
</html>
"@ | Set-Content -Path $outHtml -Encoding UTF8
Start-Process $outHtml
このHTMLはローカルファイルなので、ポータルの認証状態を保ったまま、必要なリンクだけを順に開けます。リンク(FileUrl)自体は機密扱いにし、共有フォルダに無制限に置かないなどの運用ルールも合わせて決めてください。
.bat で開きたい場合の例(PowerShellが苦手な現場向け)
PowerShellで「URL一覧テキスト」を作り、batで順に開く運用もできます。例えば、PowerShell側でURLだけのテキストを作ります。
# FileUrlだけを抽出してテキスト化
$outTxt = "C:\\Reports\\HistoricalSearch_Urls.txt"
$jobs | Where-Object { $_.FileUrl } | Select-Object -ExpandProperty FileUrl | Set-Content -Path $outTxt -Encoding UTF8
次に、batで1行ずつEdgeで開きます。
@echo off
setlocal enabledelayedexpansion
set "LIST=C:\\Reports\\HistoricalSearch_Urls.txt"
for /f "usebackq delims=" %%U in ("%LIST%") do (
if not "%%U"=="" (
start "" msedge.exe "%%U"
timeout /t 1 > nul
)
)
endlocal
この方法の注意点は、URLの失効や認証状態に依存する点は変わらないことです。batは「開く」までの段取りを助けるだけ、と割り切るのが安全です。
運用でハマりやすいポイントと対策
半自動化をうまく回すコツは、「リンクを開く」以外の運用設計にあります。特に以下はよくハマります。
| ハマりどころ | 症状 | 対策 |
|---|---|---|
| ジョブが完了していない | FileUrlが空、または開いてもレポートがない | StatusがCompletedのものだけに絞る。通知メール(NotifyAddress)で完了をトリガーにする |
| URLが期限切れ | 昨日のリンクを開くとエラー/ログインに戻る | 「作成→完了→ダウンロード」までのリードタイムを短くする。期限切れ前提で再実行手順を用意 |
| InPrivate/別プロファイルで認証が分離 | タブごとにサインインを求められる | 管理者専用プロファイルを作り通常ウィンドウで運用。多アカウント環境はプロファイルを分ける |
| ファイル名が分からなくなる | download.csv が大量に増える | 保存後にリネームする運用(例:ReportTitle_日付.csv)。ダウンロードフォルダを案件別に分ける |
| 誰がどれを取得したか追跡できない | 証跡不足で後から困る | ジョブ一覧CSVを残す。取得後にチケットへ添付、または保存先を一元化してログを残す |
ファイル整理を少しだけ自動化する小技(保存後リネームのシンプル版)
「ダウンロードしたCSVがどのジョブのものか」を後から追えるように、最低限ジョブIDを含めるだけでも効果があります。ダウンロード直後に、JobIdを入力して最新CSVをリネームする例です。
$downloadDir = "$env:USERPROFILE\\Downloads"
$latestCsv = Get-ChildItem $downloadDir -Filter "*.csv" |
Sort-Object LastWriteTime -Descending |
Select-Object -First 1
$jobId = Read-Host "リネームに使うJobIdを入力"
$newName = "HistoricalSearch_{0}_{1}.csv" -f $jobId, (Get-Date -Format "yyyyMMdd_HHmmss")
Rename-Item -Path $latestCsv.FullName -NewName $newName
件数が多い場合は「リンク集HTMLを開く→上から順にダウンロード→都度リネーム」の流れにすると、取り違えが減ります。
どうしても“完全自動化”に寄せたい場合の考え方
「人がクリックするのが許されない」「定期的に必ず取得して保管したい」といった要件がある場合、無理に PowerShell だけで FileUrl を叩くより、要件を分解した方が安全です。
- 監査・証跡が目的なら:メッセージ追跡以外の監査ログ/レポートで代替できないか(保持期間、検索性、証跡の完全性)を検討
- 定期取得が目的なら:取得対象・期間・頻度を見直し、別の公式手段(定期レポートや監査の仕組み)に寄せる
- 操作の自動化が目的なら:ブラウザ操作を前提にしたRPA(UI操作)で「ダウンロードボタンを押す」までを自動化する選択肢はあるが、UI変更に弱い点と、認証・セキュリティの取り扱いが課題
多くの現場では、「GUIダウンロードは残すが、対象抽出と導線作りを自動化する」 が最もコスト対効果が高く、運用トラブルも少ない落としどころになります。
まとめ
- Start-HistoricalSearch のレポートは、PowerShellでジョブ管理はできても、GUI同様のCSVを“PowerShellだけで直接ダウンロード”するのは難しい
- 現実的には、Get-HistoricalSearchで対象ジョブを抽出し、FileUrlをブラウザでまとめて開くことで、手作業を大幅に短縮できる
- 運用を安定させるには、ジョブ一覧のCSV保存やリンク集HTMLの生成など、証跡と導線を同時に整えるのが効果的
「完全自動」を狙うより、確実に回る半自動化を作る方が、結果として早く・安全に目的を達成できます。まずは、日々の運用で一番時間がかかっている工程(探す、開く、整理する)から潰していきましょう。

コメント