Azure SQL Database の Defender for SQL 画面では「最終スキャン日時」が見えるのに、Azure Resource Graph の KQL で取得すると古い日時や 0001/01/01 が返ることがあります。本記事では原因の整理と、現場で迷わない切り分け・対処手順をまとめます。
現象:ポータルの「最終スキャン日時」と KQL が噛み合わない
Microsoft Defender for Cloud(Defender for SQL)を運用していると、Azure ポータル上では「最終スキャン日時」が最新になっているのに、Azure Resource Graph(ARG)などで KQL を使って同じ値を引こうとしても一致しない、という事象に遭遇することがあります。
| 確認場所 | 見えている/返ってくる値の例 | 現場で起きがちな困りごと |
|---|---|---|
| Azure ポータル(Defender for SQL の画面) | 最終スキャン日時:2025/8/3 | 運用担当は「最新スキャン済み」と判断できる |
| KQL(SecurityResources など) | properties.timeGenerated:想定と違う(=スキャン実行日時ではなさそう) | どの時刻が「スキャン」なのか判断できない |
| KQL(SecurityResources など) | properties.additionalData.lastReportedScanTime:2024/3/23 または 0001/01/01 | 古い/初期値になり、ポータルと整合しない |
この相談で最も重要なのは、「このフィールドを見れば必ずポータルと同じ最終スキャン日時が取れる」という前提が、現場では崩れることがある点です。特定の SQL サーバー/DB に限って lastReportedScanTime が欠落(初期値扱い)し、KQL だけで完結した棚卸しや監視が難しくなります。
まず押さえるべき前提:Defender for SQL の「最終スキャン日時」は何を指すのか
ポータルに表示される「最終スキャン日時」は、一般に SQL Vulnerability Assessment(脆弱性評価、VA) のスキャン実行に紐づく日時として扱われます。
一方で Defender for SQL という名称の中には複数の要素が混在します。ここを混同すると「KQL で見ているもの」と「ポータルが見せているもの」がズレる原因になります。
| 機能/概念 | 何をするものか | 「最終スキャン日時」との関係 |
|---|---|---|
| Vulnerability Assessment(VA) | 構成や設定から脆弱性/推奨事項をスキャンし、レポートを生成する | 主にここ(スキャンの実行・結果の生成に紐づく) |
| セキュリティ推奨事項(Assessments) | リソース状態を評価し「対応すべき推奨事項」を算出する | 評価の更新時刻(timeGenerated)と混同しやすい |
| 脅威検知/アラート | 不審な挙動を検知しアラートを出す | 通常「最終スキャン日時」とは別の時系列で動く |
つまり、ポータルの「最終スキャン日時」は VA のスキャン履歴 を元に表示される一方、KQL で参照しがちな SecurityResources(assessments) は「評価レコード」です。ここを同一視すると、ズレが“障害”なのか“見ているものが違う”だけなのかを見誤ります。
補足:同じ KQL でも「どこで実行しているか」で意味が変わる
「KQL」と一口に言っても、Azure ではデータソースが複数あります。今回の話題(SecurityResources)は主に Azure Resource Graph 側の KQL で、Log Analytics の KQL とはデータの性質が違います。
| KQL を実行する場所 | 代表例 | 得意なこと | 今回のポイント |
|---|---|---|---|
| Azure Resource Graph | Resource Graph Explorer、ワークブックなど | リソース全体の棚卸し、状態の俯瞰 | SecurityResources を見る場合はこちら |
| Log Analytics | Log Analytics workspace、Azure Monitor | ログ/テレメトリの時系列分析、詳細な相関 | VA の「スキャン実行ログ」が常に入るとは限らない |
混乱しやすい点ですが、どちらも見た目が KQL なので「同じはず」と思いがちです。まずは どのデータを KQL で見ているかを明確にしてから切り分けるのが安全です。
ズレの本質:スキャン実行時刻・レポート時刻・評価生成時刻は一致しない
この問題を整理するため、運用上よく出てくる「3つの時刻」を分けて考えます。
| 時刻の種類 | 意味 | どこに出やすいか | ズレが起きる理由 |
|---|---|---|---|
| スキャン実行時刻 | VA のスキャン処理を走らせた(走り始めた/終わった)時刻 | ポータルの VA 画面、スキャン履歴 | スキャンは DB 側で実行され、評価生成とは別処理 |
| スキャン結果の報告時刻 | スキャン結果が Defender 側へ報告された時刻 | KQL の additionalData(例:lastReportedScanTime) | 報告経路の遅延/欠落で、空や初期値になり得る |
| 評価レコード生成時刻 | Defender for Cloud が評価(推奨事項)レコードを生成・更新した時刻 | KQL の timeGenerated | 評価はスキャン以外の要因でも更新される |
ここで重要なのが、ポータルに見えている「最終スキャン日時」と、KQL の timeGenerated が一致しないこと自体は“あり得る”という点です。timeGenerated は「スキャン時刻」ではなく「評価レコードの生成/更新時刻」として動くことがあります。
ただし本件の争点は、想定どおりならスキャン報告時刻として利用できそうな lastReportedScanTime が古い/0001/01/01 になるという部分です。ここは単なる“仕様上のズレ”では片付けづらく、値が格納されていない(欠落している)可能性が出てきます。
結論:KQL 側の値が一部リソースで正しく入らないケースがあり、KQL だけで完結しにくい
運用の結論としては次のとおりです。
- lastReportedScanTime が初期値(0001/01/01)や古い値のままになり、ポータルと一致しないケースがある
- その場合、「このフィールドを見れば必ず取れる」という形で KQL のみで完結させるのは難しい
- 切り分けは 手動スキャン → KQL の再確認 → サポート調査 の順が現実的
KQL で見える代表フィールドと、期待値とのギャップ
Azure Resource Graph で Defender 関連を追う際、よく触るのが SecurityResources テーブルです。ここで「最終スキャン日時」に見える候補は主に次の2つです。
| フィールド | 見え方の例 | 読み取り上の注意 | ポータルの「最終スキャン日時」と一致する可能性 |
|---|---|---|---|
properties.timeGenerated | 2025-08-03T02:15:44Z など | 評価レコードの生成/更新時刻として解釈されやすい | 一致しないことがある(ズレても不思議ではない) |
properties.additionalData.lastReportedScanTime | 2024-03-23T00:00:00Z / 0001-01-01T00:00:00Z など | 値が欠落すると “初期値” が入っているように見える | 本来は一致してほしいが、一致しない/初期値になる例がある |
確認用 KQL(Azure Resource Graph)例
まずは「どのリソースでズレているか」を可視化します。以下は Azure Resource Graph Explorer で実行することを想定した例です(環境や評価項目により、フィルタ条件やプロパティの場所は調整してください)。
SecurityResources
| where type =~ "microsoft.security/assessments"
| extend rd = todynamic(properties.resourceDetails)
| extend ad = todynamic(properties.additionalData)
| extend targetId = tostring(rd.id)
| where targetId contains "/providers/Microsoft.Sql/servers/" and targetId contains "/databases/"
| extend timeGenerated = todatetime(properties.timeGenerated)
| extend lastReportedScanTime = todatetime(ad.lastReportedScanTime)
| extend lastReportedScanTimeIsDefault = lastReportedScanTime == datetime(0001-01-01)
| project
subscriptionId,
targetId,
assessmentId = name,
assessmentName = tostring(properties.displayName),
status = tostring(properties.status.code),
timeGenerated,
lastReportedScanTime,
lastReportedScanTimeIsDefault
| order by timeGenerated desc
この結果で、lastReportedScanTimeIsDefault が true になる DB が混じっている場合、「値が入っていない」ことが疑われます。ポータル上の最終スキャン日時が更新されているのに KQL 側が初期値のままなら、ユーザー側だけで整合させるのが難しい可能性が高まります。
“初期値” を運用上の null として扱う(表示崩れを防ぐ)
ダッシュボードや棚卸しで 0001/01/01 をそのまま出すと誤解を招くため、運用上は「不明(null)」に置き換えるのが安全です。
SecurityResources
| where type =~ "microsoft.security/assessments"
| extend rd = todynamic(properties.resourceDetails)
| extend ad = todynamic(properties.additionalData)
| extend targetId = tostring(rd.id)
| where targetId contains "/providers/Microsoft.Sql/servers/" and targetId contains "/databases/"
| extend lastReportedScanTimeRaw = todatetime(ad.lastReportedScanTime)
| extend lastReportedScanTime =
iff(lastReportedScanTimeRaw == datetime(0001-01-01), todatetime(null), lastReportedScanTimeRaw)
| project targetId, lastReportedScanTime
ただしこれは「表示上の応急処置」であり、ポータルと同じ最終スキャン日時を再現できる保証はありません。
なぜ 0001/01/01 や古い日時になるのか:考えられる原因と切り分け
0001/01/01 は DateTime の最小値(初期値)として扱われることが多く、「値が未設定/欠落」でも返ってきてしまう典型例です。Defender 関連の評価データでも、何らかの理由でスキャン時刻が記録されないと、結果としてこのような値に見えることがあります。
原因は1つに限りません。運用で役立つように、「現場でよく当たる原因」と「確認ポイント」を表にまとめます。
| 原因候補 | どう見えるか | 確認ポイント | 次のアクション |
|---|---|---|---|
| そもそも VA スキャンが一度も実行されていない | 0001/01/01、または null 相当 | VA が有効か、スキャン履歴が存在するか | 手動スキャンを実行して履歴を作る |
| スキャンは実行されたが、Defender 側への報告が欠落/遅延 | ポータルは更新されるのに KQL では古い/初期値 | 同じ DB で「評価レコード」が更新されているか | 再現条件を揃えてサポート調査へ |
| timeGenerated を「スキャン実行時刻」と誤認している | timeGenerated がポータル表示と一致しない | timeGenerated は評価レコードの更新時刻である可能性 | lastReportedScanTime を優先し、timeGenerated は補助と割り切る |
| UTC/ローカル表示の差、表示丸め | 日付が1日ズレたように見える | ポータル表示がローカル時刻か、KQL が UTC か | UTC で統一して比較する(変換して確認) |
| リソース種別や評価項目の取り違え | 見ている assessment が想定外 | 対象 DB の resourceId と assessment の対応 | resourceDetails.id で DB を特定して絞り込む |
この中で、最も厄介なのが 「手動スキャンを実行しても lastReportedScanTime が初期値のまま」というパターンです。これはユーザー側設定だけで直せない可能性があり、次章のフローで対応するのが現実的です。
推奨の対処フロー:手動スキャンで切り分け → サポートへ
「KQL で取れる/取れない」を議論する前に、まずはデータが正しく生成・報告されているかを切り分けます。運用での再現性を上げるため、順番に実施するのがポイントです。
事前確認(設定・権限)
- 対象の Azure SQL Database(またはサーバー)で Defender for SQL が有効になっている
- Vulnerability Assessment(VA)が有効になっており、スキャンを実行できる状態になっている
- 手動スキャンを実行できる権限(例:SQL セキュリティ管理相当/SQL への管理権限)を持つ担当者がいる
- Defender for Cloud の評価データを参照できる権限(例:Security Reader 以上)がある
手動スキャンの実行(再現性のある切り分け)
バックエンドログにアクセスできる管理者(または該当権限を持つ担当者)に依頼し、対象 DB に対して 手動スキャンを実行します。ここでの狙いは次の2点です。
- ポータル上の「最終スキャン日時」が確実に更新される状態を作る
- その直後の KQL で
lastReportedScanTimeが更新されるかを確認する
KQL 側での再確認
手動スキャン後、前述の KQL を同じ条件で実行し、以下を確認します。
lastReportedScanTimeが手動スキャンの実施時刻に追従して更新されるか- 更新されない場合、対象が「特定のサーバー/DB に偏る」のか「全体的に発生する」のか
- 同じ DB でも assessment によって値の入り方が異なるか(displayName/assessmentId で差がないか)
手動スキャン後も初期値/不正な値なら、Microsoft サポートへ
手動スキャン後も 0001/01/01 が返る、あるいは明らかに古い日時のまま変化しない場合、KQL 側(Defender の評価データ)の不整合として、Microsoft サポートでの調査・修正依頼が必要になることがあります。
サポートに渡す情報を揃えると、調査がスムーズになります。
| 用意すると良い情報 | 具体例 | 意図 |
|---|---|---|
| 対象リソースの Resource ID | /subscriptions/…/resourceGroups/…/providers/Microsoft.Sql/servers/…/databases/… | どの DB で発生しているかを一意に特定 |
| ポータル表示の最終スキャン日時 | 2025/8/3(スクリーンショット含む) | 期待値(正しいと考える値)を明確化 |
| KQL の実行結果 | assessmentId / lastReportedScanTime / timeGenerated など | 「KQL 側の欠落」を客観化 |
| 手動スキャン実施の記録 | 実施日時、実行者、対象 DB | 再現条件を固定し、因果関係を示す |
| 影響範囲 | 特定サーバーのみ/複数サブスクリプション/Managed Instance も含む 等 | 障害か設定かを切り分けやすくする |
運用で困らないための回避策:KQL に依存しすぎない
運用の理想は KQL だけで「最終スキャン日時」を棚卸し・監視できることです。しかし、lastReportedScanTime が安定しないケースがある以上、KQL を唯一の正としない設計が安全です。
回避策1:KQL は “異常検知” に寄せる
最終スキャン日時を厳密に揃えるのではなく、次のような使い方に寄せると、欠落しているリソースを早期にあぶり出せます。
lastReportedScanTimeが0001/01/01の DB を「要確認」として抽出する- 一定期間更新がない DB を抽出し、手動スキャンや設定確認につなげる
SecurityResources
| where type =~ "microsoft.security/assessments"
| extend rd = todynamic(properties.resourceDetails)
| extend ad = todynamic(properties.additionalData)
| extend targetId = tostring(rd.id)
| where targetId contains "/providers/Microsoft.Sql/servers/" and targetId contains "/databases/"
| extend lastReported = todatetime(ad.lastReportedScanTime)
| extend isMissing = (isnull(lastReported) or lastReported == datetime(0001-01-01))
| where isMissing
| project targetId, assessmentId = name, assessmentName = tostring(properties.displayName)
回避策2:正の「スキャン履歴」を別経路で取得し、KQL で参照できる場所へ集約する
ポータルが参照しているスキャン履歴に近い情報は、評価レコード(SecurityResources)とは別経路で取得できる場合があります。運用としては、
- スキャン履歴から「最新のスキャン日時」を取得する
- その値を Log Analytics(カスタムログ)や Storage に蓄積する
- 蓄積先を KQL で参照してダッシュボード化する
という構成にすると、「Defender 側の評価データに欠落がある」状況でも、監査対応や月次レポートが回ります。特に 日時の正確性が求められる用途では有効です。
比較時のチェックリスト:見落としを減らす
最後に、ポータルと KQL を比較するときの見落としを防ぐチェックリストです。
| チェック項目 | 見落とすとどうなるか | おすすめの確認方法 |
|---|---|---|
| 対象 DB の Resource ID を揃えているか | 別 DB の assessment を見て「値が違う」と誤判定 | properties.resourceDetails.id で一致確認 |
| ポータル表示のタイムゾーン | 1日ズレて見える | UTC に換算して比較(日時の表示形式も統一) |
| どの assessment を見ているか | 関係ない評価項目の値を拾う | displayName/assessmentId を記録し、対象項目を固定 |
| 0001/01/01 を「スキャン済み」と誤認していないか | 実際は未報告なのに正常扱いしてしまう | 初期値は null と同等に扱い、アラート対象にする |
まとめ
Azure ポータルで見える Defender for SQL の「最終スキャン日時」を、KQL(SecurityResources など)から同じ値として取得しようとすると、timeGenerated が別の意味になっていたり、lastReportedScanTime が初期値/古い値のままだったりして一致しないことがあります。
- timeGenerated は“スキャン時刻”とは限らない(評価レコードの生成・更新時刻として動く)
- lastReportedScanTime が初期値になるのは、値が正しく反映されていない可能性がある
- 手動スキャンで切り分け、再現するなら Microsoft サポートへ不整合として調査依頼するのが近道
KQL だけで完結しない状況でも、切り分けの順番を押さえておけば、運用を止めずに原因の特定と改善に進めます。

コメント