Claude Media
Microsoft 365の未使用ライセンスをClaude Codeで洗い出す手順

Microsoft 365の未使用ライセンスをClaude Codeで洗い出す手順

Graph PowerShellで割り当て済みライセンスと最終サインインをCSVに出し、Claude Codeで長期未使用と割り当て超過の候補を抽出する手順です。

Microsoft 365の未使用ライセンスをClaude Codeで洗い出す手順

Microsoft 365の未使用ライセンスは、Graph PowerShellで「SKUごとの購入数と割り当て数」「ユーザーごとの割り当てと最終サインイン」の2つのCSVを作り、Claude Codeに突き合わせさせると洗い出せます。データの取得はPowerShellが担い、Claude Codeは判定ルールに沿った抽出と報告に専念します。この分担にすると、抽出結果をCSVの件数で検算できます。

洗い出しに必要な権限と契約

最終サインインを取るには、Microsoft Entra IDのP1またはP2ライセンスが要ります。サインイン情報を返す signInActivity は、権限だけでなく契約も満たさないと取れない項目です。

必要な権限は次のとおりです。

取るものGraphの権限補足
ユーザー一覧と割り当てGraphの権限User.Read.All補足一覧取得の最小権限の一つ
最終サインインGraphの権限AuditLog.Read.All補足あわせて User.Read.All が必要
SKUの購入数・割り当て数Graphの権限LicenseAssignment.Read.All補足Directory.Read.All などでも可

アクティビティログを読む役割として、Reports Readerが最小権限のロールに挙げられています。いずれも読み取りだけの権限です。ライセンスを外す書き込み権限は、この洗い出しには要りません。

Graph PowerShell SDKはPowerShell 7以降で動き、Windows PowerShell 5.1とも互換です。Windows・macOS・Linuxのどれでも使えます。

SKUごとの購入数と割り当て数を出す

最初に、契約しているライセンスの種類と数を取ります。Get-MgSubscribedSku が購入済みのSKUを返し、ConsumedUnits が割り当て済みの数、PrepaidUnits.Enabled が有効な購入数です。

Connect-MgGraph -Scopes "User.Read.All","AuditLog.Read.All",`
  "LicenseAssignment.Read.All"
 
Get-MgSubscribedSku |
  Select-Object SkuPartNumber, SkuId, ConsumedUnits,
    @{n='Enabled'; e={$_.PrepaidUnits.Enabled}},
    @{n='Suspended'; e={$_.PrepaidUnits.Suspended}} |
  Export-Csv skus.csv -NoTypeInformation -Encoding utf8

PrepaidUnits には enabled のほか suspended warning lockedOut があります。更新期限を過ぎた猶予期間の数が warning、解約後に再有効化できる数が suspended です。未使用の検討対象は、基本的に enabled から ConsumedUnits を引いた余りと、割り当て済みだが使われていないライセンスの2種類です。

管理センターに表示される製品名と、Graphの skuPartNumber は別の文字列です。対応表は、Microsoftが公開している「Product names and service plan identifiers for licensing」にあります。Claude Codeに報告させるときは、skuPartNumber のままでも構いません。

ユーザーごとの割り当てと最終サインインをCSVにする

次に、ライセンスを1つ以上持つユーザーだけを、最終サインインつきで書き出します。

Get-MgUser -All `
  -Property Id,UserPrincipalName,DisplayName,AccountEnabled,`
AssignedLicenses,SignInActivity |
  Where-Object { $_.AssignedLicenses.Count -gt 0 } |
  ForEach-Object {
    [pscustomobject]@{
      upn           = $_.UserPrincipalName
      enabled       = $_.AccountEnabled
      skuIds        = ($_.AssignedLicenses.SkuId -join ';')
      lastSignIn    = $_.SignInActivity.LastSignInDateTime
      lastSuccess   = $_.SignInActivity.LastSuccessfulSignInDateTime
      lastNonInter  = $_.SignInActivity.LastNonInteractiveSignInDateTime
    }
  } | Export-Csv users.csv -NoTypeInformation -Encoding utf8

例えば次のような行が並ぶCSVになります。列名は上のスクリプトで決めたもので、テナントの運用に合わせて変えて構いません。

upn,enabled,skuIds,lastSignIn,lastSuccess,lastNonInter
a.tanaka@contoso.example,True,c7df2760-...,2026-10-10T01:12:44Z,2026-10-10T01:12:44Z,2026-10-12T22:03:10Z
b.suzuki@contoso.example,True,c7df2760-...,,,

2行目のように3つの日時がすべて空のユーザーが、後で効いてきます。

ユーザー一覧に signInActivity を指定すると、1ページの上限は500件に下がります。通常は999件です。-All を付けておけば、ページの繰り返しはSDKが処理します。

どの「最終サインイン」で判定するか

signInActivity には3つの日時があり、判定にはそれぞれ癖があります。

項目記録されるもの使いどころ
lastSignInDateTime記録されるもの対話型サインインの試行(失敗も含む)使いどころ試行の有無を見る
lastNonInteractiveSignInDateTime記録されるもの非対話型サインインの試行使いどころクライアント経由の利用を拾う
lastSuccessfulSignInDateTime記録されるもの対話型・非対話型を問わず最後に成功した時刻使いどころ実際に使われたかの判定

lastSignInDateTime は失敗した試行でも更新されるため、実際の利用を示すとは限りません。実利用の判定には lastSuccessfulSignInDateTime を使うのが筋です。このプロパティは2023年12月1日以降に提供が始まり、過去分の補完はされていません。それ以前の成功履歴はこの値に入っていないので、導入直後のテナントでは空欄が多く出ます。

空欄の意味は2通りあります。サインイン試行そのものが一度もないか、最後の試行が2020年4月より前かのどちらかです。CSVの空欄だけでは、「一度も使っていない」と「データが残っていない」の区別がつきません。ここは次の節で、Claude Codeに分けて報告させます。

Claude Codeに抽出させる

2つのCSVを置いたフォルダーに、判定ルールを CLAUDE.md として書きます。ルールを会話ごとに口頭で伝えると、実行のたびに基準がぶれるためです。

# ライセンス棚卸し
 
- 読むのは skus.csv と users.csv だけ。Graph への接続やライセンス変更はしない
- 長期未使用: lastSuccess が基準日の90日より前
- lastSuccess が空欄のユーザーは「記録なし」として別枠に出す
- enabled が False のアカウントに割り当てが残っていれば別枠に出す
- 出力は upn, skuIds, lastSuccess, 判定理由 の表。件数も併記する

90日は例として置いた基準です。契約の更新サイクルや繁忙期に合わせて変えてください。

実行は非対話モードで行います。claude -p にプロンプトを渡せば結果が標準出力に返り、--allowedTools で許可するツールを絞れます。ここでは読み取りだけを許可します。

claude -p "users.csv と skus.csv を CLAUDE.md の基準で判定して。\
基準日は 2026-10-13" \
  --allowedTools "Read" > report.md

依頼文には基準日を書き込みます。「90日前」とだけ書くと、Claude Codeが日付をどう解釈したかが出力から読み取れません。

行数が多いCSVでは、読んで数えさせるより、集計用のスクリプトを書かせて実行させるほうが結果を再現できます。その場合は許可ツールにBashを足しますが、書き込み先と実行できるコマンドは settings.json の権限で絞っておきます。

結果を件数で検算する

出力された表は、そのまま信じず、次の2点をPowerShell側の数字と照らします。

  1. users.csvのSKU別の割り当て件数が、skus.csvの ConsumedUnits と一致するか
  2. 報告された「長期未使用」の件数が、Import-Csv と Where-Object で数えた件数と一致するか
$cut = [datetime]'2026-07-15'
Import-Csv users.csv |
  Where-Object { $_.lastSuccess -and ([datetime]$_.lastSuccess -lt $cut) } |
  Measure-Object

ConsumedUnits は「割り当てられたライセンスの数」です。1人が複数のSKUを持っていればSKUごとに数えるので、skuIds を ; で分けてから集計した値と比べます。一致しなければ、-All で取り切れていないか、割り当てのあるユーザーを絞り込む条件が漏れています。

大きなテナントでは取得側で絞り込む

ユーザーが数万人規模になると、全員分を取ってからClaude Codeに渡すのは重くなります。GraphのRESTでは、最終サインインの条件を取得側に持たせられます。

GET https://graph.microsoft.com/v1.0/users
  ?$filter=signInActivity/lastSuccessfulSignInDateTime le 2026-07-15T00:00:00Z

signInActivity の $filter は eq ne not ge le に対応します。ただし、他の絞り込み対象のプロパティとは組み合わせられません。「ライセンスを持ち、かつ最終サインインが古い」を1回のリクエストで取ることはできず、サインインの条件で絞った結果を、手元で assignedLicenses の有無で選び直すことになります。

この形で取ると、最終サインインが空欄のユーザーは le の条件に入りません。空欄は別に取り、「記録なし」の枠として追加する前提で組みます。

ライセンスの種類側から攻める方法もあります。assignedLicenses/any(u:u/skuId eq <SKUのGUID>) で、特定のSKUを持つユーザーだけに絞れます。高額なSKUから順に見たいときに使えます。

数十人規模なら管理センターでも足りる

件数が少なければ、スクリプトを書かなくても済みます。Microsoft Entra管理センターのユーザー一覧に「Last interactive sign-in time」の列を足し、演算子を「以下」にして日付を指定すると、その日より前が最後のサインインのユーザーに絞れます。サインインしている管理者は、少なくともReports Readerの役割が必要です。

この画面の値は対話型サインインが基準です。成功だけを見たい場合や、割り当てているSKUとの突合まで行うなら、前節までのCSVを使うほうが筋が通ります。1人のユーザーの直近の動きを近い時刻で見たいときは、ユーザー詳細のサインイン欄にある「See all sign-ins」から全件を開けます。概要のタイルの日時は反映に最大24時間かかります。

実行前に押さえたいつまずき

  • 最終サインインは更新が遅れる。対話型サインインはサインインレポートに通常6時間以内に反映され、ユーザーオブジェクトの値は最大24時間かかることがあります。昨日使った人が「未使用」に見えることがあります。
  • サービスアカウントや共有用のアカウント。人がサインインしない用途のアカウントは、対話型の履歴が薄くなります。lastNonInteractiveSignInDateTime も合わせて見ないと、利用中のものを候補に入れかねません。
  • ゲストアカウント。非アクティブなゲストの扱いには、Microsoftが専用の手順を用意しています。ここでの洗い出しは社内ユーザー向けです。
  • CSVに含まれる個人情報。UPNは個人を特定できる値です。社内規程でAPIへの送信が制限されている場合は、UPNを連番に置き換えた版を作って渡します。

候補から割り当て解除までの距離

抽出された一覧は、解除してよいユーザーの一覧ではなく、確認先の一覧です。非アクティブなアカウントの扱いについて、Microsoftのドキュメントは次の問いから始めるよう案内しています。

  • そのユーザーはまだ在籍しているか
  • いま持っているリソースへのアクセスが、まだ必要か
  • アカウントは他の目的でまだ必要か

洗い出しの出力にこの3問を足した確認シートを作り、本人や上長に回すと、解除の判断が揃います。ライセンスの割り当て解除自体は user: assignLicense で行いますが、書き込み権限をこの調査と同じ接続に持たせる理由はありません。調査用と変更用でサインインを分けておくと、Claude Codeに渡したフォルダーから変更が起きる経路が残りません。

SaaS全体の請求とSSOログを突き合わせる棚卸しはSaaSの棚卸しをClaudeで進める手順で扱っています。ClaudeとMicrosoft 365のプラン別の違いを確かめるならClaude Microsoft 365のプラン別の違いが行き先です。外部のAPIから取ったデータをCSVにしてClaudeに読ませる別の例はEDINET APIの有価証券報告書の手順にあります。

運用に乗せるときの判断

月次の棚卸しにするなら、スクリプトの出力先を日付つきのフォルダーに固定し、CLAUDE.md の基準日だけを書き換える形にすると、前月との差分を見られます。長期未使用に入った人が翌月も残っているかを追えば、「一時的な不在」と「使われなくなったもの」が分かれます。割り当て超過の確認は、ConsumedUnits が Enabled を上回っていないかをSKUごとに見るだけで済みます。

この記事を共有:XはてブLinkedIn