見出し画像

MDEの状況をMicrosoft Graph APIで取得してLooker Studioで可視化する

ウイルス対策としてMicrosoft Defender for Endpoint(MDE)をMacにインストールしているのですが、定期的な状態チェックができていませんでした。

放っておいても定義ファイルは自動的にアップデートされていくし基本問題ないとは思うのですが、実は定義が古いデバイスがありました、なんてことが起こっていないとも限らないので、最低限の内容をサクッとチェックできる体制を整えることにしました。



最低限チェックしたい内容を検討する

毎日チェックして隅々まで監視する、とまでは考えていないので、まずは最低限見ておきたいものだけはいつでもすぐ見れる状態を目指します。とりあえずは下記の2点をすぐにチェックできる体制を作ることにしました。

定義ファイルが更新されているか

定義ファイルが最新、もしくは最新に近い状態じゃないと脅威に対して反応してくれない、という危険性が出てくると思うので、バージョンが古すぎるものがないか確認できるのは必須かと。

定期スキャンがちゃんと行われているか

定期スキャンを実行するように組み込んでいるのですが、これがちゃんと動いているかもチェックしておきます。組み込んでいるはずのものが動いていない場合、何らかの不具合が起こっている可能性があるためです。


どうやって確認するか

チェックしたい内容が決まったら、次はどうやってそれらの情報を取得するかを検討します。

定義ファイルをMacで確認してみる

とりあえず現状把握ということで、Macに入っているMDEの状態を見てみます。

MacにインストールしたMDEの状態

定義ファイルと呼んでいましたが、MDEではセキュリティインテリジェンスと呼ぶみたいです。確認時点では、どうやら自動で最新になっている様子でした。ひとまず安心です。

また、MDEがインストールされたデバイスのデータは、Microsoft Defenderポータルの管理画面から確認することが出来ます。ポータルのサイドメニューの「デバイス」メニューを開くとデバイスの一覧が表示されるので、デバイスを選べば、セキュリティインテリジェンスの状態が確認できます。

デバイスを選択して少し下にスクロールしたところにあります

ただ、一つ一つ確認するのは面倒です。そこで、Microsoft Defenderポータルの「高度な追及」の利用を検討することにしました。

高度な追及で状態を追う

「高度な追及」では、Kusto照会言語(KQL)を用いてMDEが収集した情報を探索することができます。上手くKQLを組めば、セキュリティインテリジェンスの状態を一覧で取得できるはず。

ただ、公式ドキュメントを参照してイチから目当ての情報を抜き出すKQLを組もうとしたのですが、お目当てのスキーマがどれなのかがよくわからない。

どうしたものかと思っていたら、「コミュニティクエリ」と呼ばれる共有クエリがある事に気が付きました。高度な追及の「クエリ」から「defender」で検索してみると、まさに欲しかった「Microsoft Defender AV Security Intelligence up to date information」というクエリを発見しました。

試しにそのまま叩いてみると、セキュリティインテリジェンスの情報が確認できました。ただそのままだとセキュリティインテリジェンスのバージョン別でしか見れなかったので、デバイス別で状態を確認できるように改造します。

デバイス別でデータを取れるようにする

クエリを見る感じ「DeviceTvmInfoGathering」の「AdditionalFields」内の「AvSignatureVersion」がセキュリティインテリジェンスのバージョンらしいので、これをデバイス別一覧で出すようにします。

また、「AvScanResults」でスキャンの実行結果も取れるようだったので、これも一緒に見れるようにします。その他、エンジンとプラットフォームのバージョンも取れるようなので、これも取っておくことにしました。

最終的にこんな感じでKQLを作成しました。

DeviceTvmInfoGathering
| extend AvMode = AdditionalFields.AvMode
| extend AvEngineVersion = AdditionalFields.AvEngineVersion
| extend AvSignatureVersion = AdditionalFields.AvSignatureVersion
| extend AvPlatformVersion = AdditionalFields.AvPlatformVersion
| extend AVScanResults = AdditionalFields.AvScanResults
| extend QuickScan_result = extractjson("$.Quick.ScanStatus", tostring(AVScanResults))
| extend QuickScan_Timestamp = extractjson("$.Quick.Timestamp", tostring(AVScanResults))
| extend FullScan_result = extractjson("$.Full.ScanStatus", tostring(AVScanResults))
| extend FullScan_Timestamp = extractjson("$.Full.Timestamp", tostring(AVScanResults))
| join kind = leftouter (DeviceInfo | distinct DeviceId, LoggedOnUsers ) on DeviceId
| extend UserName = extract_json("$.[0].UserName", LoggedOnUsers)
| project DeviceId, DeviceName, UserName, OSPlatform, AvMode, AvEngineVersion, AvSignatureVersion, AvPlatformVersion, QuickScan_result, QuickScan_Timestamp, FullScan_result, FullScan_Timestamp


「extend」はAdditionalFieldsの中身をキーを指定して別名で取り出しています。「extractjson」は、jsonから指定の要素を取り出しています。

また、ユーザー情報も紐づけたかったのですが「DeviceTvmInfoGathering」テーブルではユーザー情報を持っていなかったので、「DeviceInfo」テーブルと結合してユーザー情報を引っ張ってきました。

あとはprojectで結果に表示したい項目を指定して並べれば、デバイス別のMDEのバージョンやスキャンの実行状態一覧の出来上がり。

ただ「DeviceInfo」に同一のデバイスが重複して存在しているようで、実際のデバイス数よりも少しデータ数が多く出てしまいました。ざっと見たところ利用ユーザーが変わったデバイスが重複して表示されているようだったので、利用者の切替時にMDEのオフボードしていないせいなのかもしれません。あまり影響はないので一旦無視します。


定期的な収集・チェック体制を検討する

KQLで欲しい情報が取得できるのはわかったのですが、このままでもやはり俯瞰して見づらいのは変わりありません。そこで、Microsoft GraphのAPIで上手いこと取得できないか検証することにしました。

Microsoft Graphを触る

Microsoft Graphもあまり触ったことがないので、まずMicrosoft Graph Explorerで感覚を掴みます。

とりあえずクエリを発行してみるだけなら、左側のメニューからサンプルクエリを選んで右上の「Run Query」をクリックするだけで実行可能です。クエリが実行されると画面下部にレスポンスが表示されました。

発行するクエリによっては権限が不足することがあります。権限が無くて怒られた場合は、「Modify permissions」をクリックすると必要な権限が表示されるので、「consent」を押して許可してやるとクエリが実行できるようになります。

API経由でデータを取得してスプレッドシートに落とす

Microsoft Graphについてある程度感覚を掴んだら、GASでMicrosoft Graph APIを叩いてMDEの状態を取得し、スプレッドシートに出力する方法を検討します。

少し調べたらクラスメソッドさんの下記の記事を見つけました。これを参考にMicrosoft Graph の「クライアントID」、「クライアントシークレット」、「テナントID」を取得します。

必要なID類が準備ができたら、下記を参考にしつつGASを組みました。

// GraphAPI設定
const baseApi = 'https://graph.microsoft.com/v1.0'
const authApi = 'https://login.microsoftonline.com'
const properties = PropertiesService.getScriptProperties().getProperties()

// 書き込み先SS
const ss = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('MDE');

// データ一時格納用配列
const dataValues = [];

// メイン関数
function main(){
  const token = getToken();
  getDevice(token);
}

// トークン取得
function getToken(){
  // 認証情報
  const clientId = properties.CLIENT_ID;
  const clientSecret = properties.CLIENT_SECRET;
  const tennantId = properties.TENNANT_ID;

  // トークン取得
  try {
    const res = UrlFetchApp.fetch(`${authApi}/${tennantId}/oauth2/v2.0/token`,{
      method: 'post',
      payload: {
        'client_id': clientId,
        'scope': 'https://graph.microsoft.com/.default',
        'client_secret': clientSecret,
        'grant_type': 'client_credentials'
      }
    })
    const json = JSON.parse(res.getContentText());
    return json.access_token;
  }catch(err){
    throw new Error('トークン取得エラー');
  }
}

// 情報取得
function getDevice(token){

  // 実行したいKQL
  const payload = {
    'query': `DeviceTvmInfoGathering
    | extend AvMode = AdditionalFields.AvMode
    | extend AvEngineVersion = AdditionalFields.AvEngineVersion
    | extend AvSignatureVersion = AdditionalFields.AvSignatureVersion
    | extend AvPlatformVersion = AdditionalFields.AvPlatformVersion
    | extend AVScanResults = AdditionalFields.AvScanResults
    | extend QuickScan_result = extractjson("$.Quick.ScanStatus", tostring(AVScanResults))
    | extend QuickScan_Timestamp = extractjson("$.Quick.Timestamp", tostring(AVScanResults))
    | extend FullScan_result = extractjson("$.Full.ScanStatus", tostring(AVScanResults))
    | extend FullScan_Timestamp = extractjson("$.Full.Timestamp", tostring(AVScanResults))
    | join kind = leftouter  (DeviceInfo 
    | distinct DeviceId, LoggedOnUsers
    ) on DeviceId
    | extend UserName = extract_json("$.[0].UserName", LoggedOnUsers)
    | project DeviceId, DeviceName, UserName, OSPlatform, AvMode, AvEngineVersion, AvSignatureVersion, AvPlatformVersion,
    QuickScan_result, QuickScan_Timestamp, FullScan_result, FullScan_Timestamp`
  };

  // 高度な追及を実行する
  const res = UrlFetchApp.fetch(`${baseApi}/security/runHuntingQuery`, {
    method: 'get',
    headers: {
      'Authorization': `Bearer ${token}`,
      'Content-Type': 'application/json',
    },
    payload : JSON.stringify(payload)
  });

  const json = JSON.parse(res.getContentText());

  // データ取得件数分回す
  for (let i = 0; i < json.results.length; i++) {
    // 情報を配列に格納
    let row = [json.results[i].DeviceId, json.results[i].DeviceName, json.results[i].UserName, json.results[i].OSPlatform, json.results[i].AvMode, 
              json.results[i].AvEngineVersion, json.results[i].AvSignatureVersion, json.results[i].AvPlatformVersion, json.results[i].QuickScan_result, 
              json.results[i].QuickScan_Timestamp, json.results[i].FullScan_result,json.results[i].FullScan_Timestamp];
    dataValues.push(row);
  }

  // 出力用ヘッダ
  const heading = ["DeviceId", "DeviceName", "UserName", "OSPlatform", "AvMode", "AvEngineVersion", "AvSignatureVersion", "AvPlatformVersion", "QuickScan_result", "QuickScan_Timestamp", "FullScan_result", "FullScan_Timestamp"];

  // 配列のデータをスプレッドシートに書き込む
  ss.getRange(1, 1, 1, heading.length).setValues([heading]);
  ss.getRange(2, 1, dataValues.length, dataValues[0].length).setValues(dataValues);


}

「runHuntingQuery」のエンドポイントを利用すれば先に組んでいたKQLをそのまま投げることができました。これならKQLで検証→GASにそのまま組み込みというフローで継続的に改修ができそうです。


Looker Studioで可視化する

スプレッドシートに出力したことである程度俯瞰して見ることができるようになりましたが、せっかくなのでLooker Studioにで可視化します。

あわせてLooker Studio側で、

  • 最後のクイックスキャンから10日以上経っていないか確認する

  • 最後のクイックスキャンからの経過日数を計算する

カラムを組み込みました。グラフも作って見える化しておきました。

とりあえずグラフを入れてそれっぽくする


可視化の次のステップも忘れずに

GASの定期実行の組み込みや表示方法などをもう少し調整する予定ですが、ここまでやればMDEの状況を確認したいときにサッと確認できる土台が整ったかと思います。

ただ、現段階では状況が見れるようになっただけなので、今後実際にセキュリティインテリジェンスのバージョンが古いデバイスなどが見つかった場合のオペレーションも引き続き検討したいと思います。

この記事が気に入ったらサポートをしてみませんか?