BitLocker回復キーが未保管のWindows端末をGraph APIとClaude Codeで洗い出す
Microsoft Graphの端末一覧と回復キー一覧を、キー値を読まずにdeviceIdだけで突き合わせ、Entraにキーが預けられていないWindows端末の候補をClaude Codeで出す手順です。
BitLockerの回復キーがEntra IDに預けられていない端末は、ロックされたときにヘルプデスクが助けられません。その候補を洗い出すには、端末の一覧と回復キーの一覧をMicrosoft Graphで取り、deviceIdで突き合わせます。この記事では、その突き合わせをClaude Codeに任せつつ、回復キーの値そのものはClaudeに一切渡さない構成を組みます。
渡すのはIDと端末IDだけです。キー値は取得もしません。それでも「どの端末のキーが無いか」は十分に分かります。
全体像: 2つの一覧を突き合わせるだけ
使うAPIは2本です。
照合の流れ
- 1
端末一覧を取る
GET /devicesでWindows端末だけを取り、deviceIdと表示名を残します。 - 2
回復キー一覧を取る
GET /informationProtection/bitlocker/recoveryKeysで、キーのID・deviceId・volumeTypeだけを取ります。 - 3
deviceIdで突き合わせる
キーが1本も無い端末と、OSボリュームのキーが無い端末を書き出します。
- 4
Claude Codeに結果だけ読ませる
出力ファイルには端末名とIDしか入りません。Claudeはそれを整形し、確認の優先順位を付けます。
Claude Codeに任せる範囲は、スクリプトを書くことと、出力を読んで報告を整えることです。トークンを使ってAPIを叩く実行は、人間が自分の端末で行います。
回復キー一覧APIが返すもの・返さないもの
回復キーのリソース(bitlockerRecoveryKey)には、次のプロパティがあります。
| プロパティ | 内容 |
|---|---|
| id | 内容キーの一意なID |
| deviceId | 内容キーの預託元の端末ID。$filter で eq が使える |
| createdDateTime | 内容キーがEntra IDに最初にバックアップされた日時 |
| volumeType | 内容1 がOSボリューム、2 が固定データボリューム、3 が取り外し可能ボリューム、4 が unknownFutureValue |
| key | 内容回復キーそのもの。取得には $select が必要 |
一覧の取得(List)は key プロパティを返しません。値を読むには別に、キーを1本指定する取得(Get)で $select=key を付ける必要があります。つまり、一覧を取るだけなら、キー値が手元に来ることはありません。
この性質が、今回の設計の土台です。スクリプトに $select=key を書かない限り、Claudeはもちろん、ログにもファイルにもキー値は残りません。
一覧のAPIには、扱いの癖が3つあります。
$filterで絞れるのはdeviceIdで、$topは使えません- 続きがあると
@odata.nextLinkが返るので、辿って全件を取ります - リクエストには
User-Agentヘッダーが必須です
$top が使えないので、件数を絞ってから試すことはできません。最初の1回から全件を取る前提で書きます。
必要な権限とロール
アプリケーション権限でも委任権限でも、最小権限として挙げられているのは BitlockerKey.ReadBasic.All です。上位の権限は BitlockerKey.Read.All です。端末側の GET /devices には Device.Read.All が必要です。
委任権限で呼ぶ場合、呼び出すユーザーは対象端末の登録済み所有者か、次のいずれかのロールを持つ必要があります。
- Cloud device administrator
- Helpdesk administrator
- Intune service administrator
- Security administrator
- Security reader
- Global reader
棚卸しだけが目的なら、キー値を読む必要はありません。ReadBasic を最小権限として選び、Read.All は付与しない運用が素直です。
ただし、ReadBasic のトークンで $select=key が通るかどうかは、Getのページの権限表からは読み取れません。表には最小権限として同じ ReadBasic が載っているだけです。権限を絞ったから安全、と決めつけず、スクリプトにキー取得の呼び出しを入れないことを第一の防御にしてください。
照合スクリプトをClaude Codeに書かせる
Claude Codeには、仕様を渡して書かせます。依頼文は、次のように「やらないこと」を先に書くのがコツです。
Microsoft Graph v1.0 で Windows 端末と BitLocker 回復キーの一覧を取り、
deviceId で突き合わせる Python スクリプト audit.py を書いて。
- トークンは環境変数 GRAPH_TOKEN から読む(ファイルに書かない)
- 回復キー側は $select を使わず、id / deviceId / volumeType / createdDateTime
だけを保持する。key というフィールドは捨てる
- 一覧は @odata.nextLink を辿って全件取る。$top は使わない
- User-Agent ヘッダーを付ける
- 出力は out/missing.json のみ。端末名・deviceId・不足の種類だけ例えば次のような形のスクリプトになります(公式のAPI仕様に沿った一例です)。
import json, os, urllib.request
from urllib.parse import quote
BASE = "https://graph.microsoft.com/v1.0"
HEAD = {
"Authorization": "Bearer " + os.environ["GRAPH_TOKEN"],
"User-Agent": "bitlocker-audit/0.1",
}
def pages(path):
url = BASE + path
while url:
req = urllib.request.Request(url, headers=HEAD)
with urllib.request.urlopen(req) as res:
body = json.load(res)
yield from body["value"]
url = body.get("@odata.nextLink")
flt = quote("operatingSystem eq 'Windows'")
sel = "id,deviceId,displayName,trustType,isManaged"
devices = list(pages(f"/devices?$filter={flt}&$select={sel}"))
keys = {}
for k in pages("/informationProtection/bitlocker/recoveryKeys"):
keys.setdefault(k["deviceId"], set()).add(str(k["volumeType"]))続きは突き合わせです。
missing = []
for d in devices:
vols = keys.get(d["deviceId"], set())
if not vols:
kind = "no-key"
elif "1" not in vols:
kind = "no-os-volume-key"
else:
continue
missing.append({
"displayName": d["displayName"],
"deviceId": d["deviceId"],
"trustType": d.get("trustType"),
"isManaged": d.get("isManaged"),
"kind": kind,
})
os.makedirs("out", exist_ok=True)
with open("out/missing.json", "w") as f:
json.dump(missing, f, ensure_ascii=False, indent=2)実行は自分のターミナルで行います。トークンはその場の環境変数に入れ、Claudeのセッションには流しません。
export GRAPH_TOKEN="<トークン>"
python3 audit.py
python3 -c "import json; print(len(json.load(open('out/missing.json'))))"端末の $select に並べた項目は、端末リソースのプロパティ名です。trustType は Workplace(個人所有端末の持ち込み)、AzureAd(クラウド専用の参加)、ServerAd(オンプレミスのドメイン参加をEntra IDに参加させたもの)の値を取ります。後で優先順位を付けるときの材料になります。
deviceIdで突き合わせるときの注意
端末リソースには id と deviceId の2つがあります。deviceId は端末登録サービスが登録時に付ける識別子で、端末オブジェクトを参照する代替キーとして使えます。回復キー側の deviceId も同じ名前ですが、2つが同じ値を指すとは、リソースの説明に書かれていません。
だから、全件を信用する前に、既知の端末で1回確かめてください。
curl -s -H "Authorization: Bearer $GRAPH_TOKEN" \
-H "User-Agent: bitlocker-audit/0.1" \
"https://graph.microsoft.com/v1.0/informationProtection/bitlocker/recoveryKeys?\$filter=deviceId%20eq%20'<端末のdeviceId>'"キーが既にあると分かっている端末で1件以上返れば、突き合わせの軸は合っています。0件なら、軸が違うか、その端末のキーが預けられていません。後者を疑う前に、端末の deviceId を取り違えていないか見直します。
また、一覧API側のフィルターの説明は「キーが最も最近バックアップされた端末のdeviceId」、リソース側は「キーの預託元の端末」と書かれています。どちらの定義で突き合わされるかで、再登録した端末の扱いが変わる余地があります。件数が合わない端末は、個別に $filter=deviceId eq で引き直してください。
出力をClaudeに読ませ、確認の順番を付ける
out/missing.json には、端末名・deviceId・不足の種類・trustType・isManagedしか入っていません。ここからはClaude Codeに読ませてよい範囲です。
out/missing.json を読んで、次の順に整理して。
1. kind が no-key の端末を trustType 別に件数で
2. isManaged が false の端末を別枠で
3. 調査の優先順位のたたき台(理由つき)ここでの「優先順位」は、Claudeの提案にすぎません。たとえば次のような観点が候補に挙がります。
no-keyでisManagedがtrue: 管理下にあるのに預託されていないので、ポリシーの適用漏れを疑うno-keyでisManagedがfalse: 管理外の端末なので、そもそも預託の仕組みの対象かno-os-volume-key: データボリュームのキーだけがある。OSドライブが暗号化されていない可能性も含めて見る
どれも「疑う」止まりです。Entraにキーが無い端末が暗号化されていないとは限りませんし、暗号化されていてもバックアップが走っていない端末もあります。この一覧は結論ではなく、確認すべき端末の絞り込みです。
Claude Codeの権限を絞って事故を防ぐ
キー値を取らない設計でも、別の経路で値が漏れる余地は潰しておきます。作業ディレクトリの .claude/settings.json に、トークンを置きがちなファイルを読めなくする Read の拒否ルールを入れます。
{
"permissions": {
"deny": [
"Read(./.env)",
"Read(./secrets/**)"
]
}
}Claude Codeの権限は、拒否、確認、許可の順に評価され、最初に当たったルールで結果が決まります。拒否ルールに当たる呼び出しは、より狭い許可ルールがあっても通りません。Read の拒否ルールは、Claudeの組み込みのファイル系ツールに加え、Bashで認識される cat や head などにも効きます。
一方で、次の限界も押さえておく必要があります。grep -r のように対象ファイルを名指ししないコマンドや、PythonやNodeのスクリプトが自分でファイルを開く場合には、Read の拒否は効きません。OSレベルで確実に止めたいなら、サンドボックスを使います。
そのためこの記事の構成では、そもそもキー値をどこにも書かないことを主軸にしています。拒否ルールは、その外側の保険です。
見つけた端末をどう扱うか
洗い出した端末への対応は、Microsoftのヘルプデスク向け手順が前提になります。回復キーのEntra IDへのバックアップは、自動で行われない場合があり、端末側にバックアップを有効にするポリシー設定が必要です。
回復キーのローテーションについては、自動ローテーションを構成していれば、Entra参加またはハイブリッド参加の端末が新しい回復パスワードを作り、Entra IDに保存するとされています。管理者は、IntuneやConfiguration Managerから手動でローテーションを起動することもできます。
回復キーはロック解除と管理操作ができる機微な情報です。セルフサービスでの回復を許すのは信頼できる環境に限るか、ヘルプデスク経由にするよう、公式の手順でも勧められています。この棚卸しで集めたのはキーの有無だけです。それでも、結果の共有範囲は、通常の端末台帳と同じ扱いにするのが無難です。
なお、Intuneで管理しているiPhoneにClaudeを配る手順はClaude for Intuneの設定手順に、Entra側のログを調べる流れはEntra IDのサインインログをKQLで調べるにあります。端末の棚卸しとサインインの点検は、同じテナントの別の面です。
よくあるつまずき
つまずきは、ほぼ権限と取得範囲に集約されます。
結果が空・少ないときの原因
403が返る
BitlockerKey.ReadBasic.AllとDevice.Read.Allのどちらかが同意されていない可能性があります。委任権限なら、ユーザーが上記のロールを持つかも見ます。件数が少ない
@odata.nextLinkを辿れていないと、1ページ目しか取れません。$topは使えないため、ページ送りで全件を取る必要があります。全端末が未保管になる
突き合わせの軸が
idになっていないか確認します。deviceId同士で比べます。
まとめ
回復キーの未保管端末は、端末一覧と回復キー一覧を deviceId で突き合わせれば絞れます。Claude Codeに任せるのはスクリプト作成と結果の整理で、キーの値は一覧APIが返さず、スクリプトも取りに行きません。出てきた一覧は「Entraにキーが無い端末の候補」です。暗号化の有無は別の情報源で確かめて、初めて対応が決まります。