Microsoft Defender for Cloud の脆弱性評価を KQL(SecurityResources)で集計していると、「Azure SQL Database の最終スキャン日時」が Azure ポータル(DB 画面)と合わないことがあります。本記事では、なぜズレるのかを整理したうえで、UI に近い “DB レベルの最終スキャン日時” を返すための KQL(lastReportedScanTime 活用)を具体例つきで解説します。
起きている問題:KQL の ScanTime が DB 画面の「最終スキャン日時」と一致しない
Azure Resource Graph の SecurityResources を使って Defender for Cloud(脆弱性評価)の情報を引き、ScanTime を表示すると、Azure SQL Database(DB)画面に出る「最終スキャン日時」とズレるケースがあります。
- 例:KQL は 8/2 を返す
- しかし DB の UI 上は 7/30 や 8/1 など別の日付になっている
このズレが困るのは、運用面で「DB のスキャンが止まっていないか」「何日以上スキャンされていないか」を正しく判定したいときです。KQL 側の値が UI と一致しないと、監視やレポートの信頼性が下がってしまいます。
なぜズレるのか:SecurityResources のタイムスタンプは “同じ意味” ではない
結論から言うと、SecurityResources 内に存在する日時フィールドが、必ずしも “DB 自体の最終スキャン” を表していないことが原因です。Defender for Cloud の脆弱性評価は「評価レコード(recommendation/assessment/subassessment など)」として蓄積されるため、日時が次のどれかに寄ってしまうことがあります。
- ホスト(または評価実行基盤)側のスキャン時刻
- 評価結果(レコード)が生成・更新された時刻
- 集計や報告(report)として最後に記録された時刻
とくに Azure SQL Database の場合、同じ “ScanTime っぽい名前” でも、どの粒度の情報を指すかで意味が変わりやすく、結果として UI とズレます。
よく使われがちなフィールドと、ズレやすい理由
| フィールド | 見え方 | ズレが起きる典型理由 |
|---|---|---|
properties.additionalData.scanTime | スキャン時刻っぽい | DB の最終スキャンではなく、評価(ホスト/実行側)に寄ることがある |
properties.additionalData.scanDate | 日付としてある | 粒度が粗く、別レコード由来・日付丸めで一致しにくい |
properties.timeGenerated | それっぽいが実態は生成/更新 | 評価レコードの生成・更新タイミングであって、スキャン実行とは別 |
つまり、「UI に出る DB レベルの最終スキャン日時」を取りたいのに、別のタイムスタンプを拾ってしまっている状態です。
解決策:DB レベルの最終スキャン日時は lastReportedScanTime を参照する
このケースでは、properties.additionalData.lastReportedScanTime を参照することで、UI に近い “最終スキャン日時” になりやすくなります。
ポイントは、ScanTime を作るときの参照先を差し替えるだけです。
差し替え(最重要ポイント)
以下の 1 行で、KQL が返す ScanTime を UI 寄りにできます。
| extend ScanTime = todatetime(properties.additionalData.lastReportedScanTime)
これにより、「scanTime/scanDate/timeGenerated を見ていたせいで、ホスト側や評価レコード生成時刻を拾っていた」状況から抜け出せます。
まずは現状確認:どの日時が入っているか横並びで見る
いきなり置換する前に、まずは “同じ DB” に対して各フィールドがどんな値を返しているか、横並びで確認すると原因の切り分けが早いです。
SecurityResources
| where type =~ "microsoft.security/assessments/subassessments"
| extend TargetId = tostring(properties.resourceDetails.id)
| where TargetId has "/providers/Microsoft.Sql/servers/" and TargetId has "/databases/"
| extend lastReportedScanTime = todatetime(properties.additionalData.lastReportedScanTime)
| extend scanTime = todatetime(properties.additionalData.scanTime)
| extend scanDate = todatetime(properties.additionalData.scanDate)
| extend timeGenerated = todatetime(properties.timeGenerated)
| project TargetId, name, lastReportedScanTime, scanTime, scanDate, timeGenerated
| order by lastReportedScanTime desc
この結果を見て、次のような傾向があれば “まさに今回の症状” です。
scanTimeは新しい日付を返すが、UI とは合わないtimeGeneratedはさらに新しい(または妙にバラつく)lastReportedScanTimeが UI の「最終スキャン日時」と一致、または最も近い
実装パターン:ScanTime だけ置換(最小変更)
既存のクエリがすでにあり、集計や結合が多い場合は、“ScanTime を作っている行だけ” を差し替えるのが安全です。
// 既存クエリの途中にある ScanTime 生成だけを差し替え
| extend ScanTime = todatetime(properties.additionalData.lastReportedScanTime)
これだけで UI とズレていた値が解消されるケースが多いです。
欠損に強くする:フォールバック(coalesce)で運用事故を防ぐ
環境やレコード種別によっては、lastReportedScanTime が入っていない場合があります。その場合でも “日時が空でレポートが壊れる” のを防ぐため、フォールバック付きの ScanTime を作っておくと運用品質が上がります。
| extend ScanTime = coalesce(
todatetime(properties.additionalData.lastReportedScanTime),
todatetime(properties.additionalData.scanTime),
todatetime(properties.additionalData.scanDate),
todatetime(properties.timeGenerated)
)
フォールバックの優先順位は、次の考え方がわかりやすいです。
| 優先 | フィールド | 狙い | 注意点 |
|---|---|---|---|
| 1 | lastReportedScanTime | DB レベルの “最終スキャン日時” に寄せる | レコードによっては欠損する可能性 |
| 2 | scanTime | 次点の “スキャン時刻” を拾う | ホスト/評価側に寄ることがある |
| 3 | scanDate | 日付だけでも確保する | 粒度が粗く一致しにくい |
| 4 | timeGenerated | 最終手段として “何かしらの時刻” を埋める | スキャン時刻ではなく生成/更新時刻の可能性 |
運用の現場では「欠損してレポートが落ちる」より「精度が落ちても値がある」ほうが助かる場面も多いので、まずはフォールバック有りを推奨します(監査用途などで厳密一致が必要なら、フォールバックを外して欠損を別途扱うのが安全です)。
“DB ごとの最終スキャン日時” にする:同一 DB の複数行を集約する
SecurityResources は、同一の DB に対して複数の評価レコード(subassessment 等)を持つことがあります。行ごとに ScanTime を出すだけだと、「同じ DB が何行も出る」「どれが最終なのか分かりづらい」状態になります。
DB ごとに 1 行へまとめたい場合は、DB ID(resourceDetails.id 等)で group by して max(ScanTime)を取ります。
SecurityResources
| where type =~ "microsoft.security/assessments/subassessments"
| extend DbId = tostring(properties.resourceDetails.id)
| where DbId has "/providers/Microsoft.Sql/servers/" and DbId has "/databases/"
| extend ScanTime = coalesce(
todatetime(properties.additionalData.lastReportedScanTime),
todatetime(properties.additionalData.scanTime),
todatetime(properties.additionalData.scanDate),
todatetime(properties.timeGenerated)
)
| summarize LastScanTime = max(ScanTime) by DbId
| order by LastScanTime desc
「DB 一覧を作って、最終スキャンが古いものを抽出する」用途では、この形が最も扱いやすいです。
最終スキャンが “古い DB” を洗い出す(運用に直結)
レポートでありがちなのが、「最終スキャンが 7 日より前の DB を抽出して、アラート対象にする」運用です。以下は概念例です(利用環境の KQL 方言により関数が異なる場合があるため、エラーが出る場合は ago() 相当を datetime_add() などに置き換えてください)。
SecurityResources
| where type =~ "microsoft.security/assessments/subassessments"
| extend DbId = tostring(properties.resourceDetails.id)
| where DbId has "/providers/Microsoft.Sql/servers/" and DbId has "/databases/"
| extend ScanTime = todatetime(properties.additionalData.lastReportedScanTime)
| summarize LastScanTime = max(ScanTime) by DbId
| where isnotempty(LastScanTime)
| where LastScanTime < ago(7d)
| order by LastScanTime asc
この抽出結果を定期的に確認できるようにしておくと、「Defender for Cloud の設定変更でスキャンが止まった」「権限や接続の問題で評価が失敗していた」といった運用トラブルを早期に見つけやすくなります。
“UI と一致しない” をさらに悪化させる落とし穴
lastReportedScanTime へ差し替えても、まだ「数時間ずれる」「日付が 1 日ズレに見える」ことがあります。これはバグではなく、見え方の落とし穴であることが多いです。
タイムゾーン(UTC / ローカル)の見え方
Azure の各種ログ・リソース情報は UTC 基準で保存されることが多く、UI 表示は環境や表示側の設定でローカル換算されることがあります。たとえば、UTC の 23:30 は日本時間だと翌日の 08:30 なので、「日付が 1 日ズレた」ように見えます。
対策としては、まず UTC と理解したうえで比較するか、表示用にフォーマットを整えます(表示の整形は環境依存なので、まずは “同じ基準で比較しているか” を確認してください)。
“別のレコード種別” を見ている
SecurityResources には assessments / subassessments など複数種があり、同じ DB でも別粒度のレコードが混在する場合があります。DB 単位の最終スキャンを取りたいのに、サブ評価(ルール単位)の行をそのまま日時として見てしまうと、揺れが出ます。
その場合は、次のチェックが効きます。
| チェック項目 | 見るべき列 | 狙い |
|---|---|---|
| DB を正しく指しているか | properties.resourceDetails.id | /Microsoft.Sql/servers/…/databases/… を含むか |
| DB ごとに 1 行になっているか | summarize max(ScanTime) by DbId | 同一 DB の複数行を集約しているか |
| 欠損を想定しているか | coalesce() | lastReportedScanTime 欠損時の耐性 |
ポータル反映のタイムラグ
UI とクエリの値を “完全一致” させたい場合、データ反映タイミングの差も考慮が必要です。ポータル表示と集計基盤への反映にはタイムラグが出ることがあり、「同日に見ても片方が先に更新される」ことがあります。
この場合は、“最新の 1 回だけ見て一致するか” ではなく、数回(例:数時間〜翌日)見て継続的に一致しているかで判断すると、無駄な深掘りを減らせます。
実務向け:最終スキャン日時に加えて “根拠列” も一緒に出す
運用レポートでは、後から「なぜその日時になったの?」と聞かれることが多いです。そこで、最終スキャン日時(LastScanTime)だけでなく、参照元の候補も一緒に出しておくと説明が楽になります。
SecurityResources
| where type =~ "microsoft.security/assessments/subassessments"
| extend DbId = tostring(properties.resourceDetails.id)
| where DbId has "/providers/Microsoft.Sql/servers/" and DbId has "/databases/"
| extend lastReported = todatetime(properties.additionalData.lastReportedScanTime)
| extend scanTime = todatetime(properties.additionalData.scanTime)
| extend scanDate = todatetime(properties.additionalData.scanDate)
| extend generated = todatetime(properties.timeGenerated)
| extend ScanTime = coalesce(lastReported, scanTime, scanDate, generated)
| summarize
LastScanTime = max(ScanTime),
AnyLastReported = max(lastReported),
AnyScanTime = max(scanTime),
AnyGenerated = max(generated)
by DbId
| order by LastScanTime desc
この出力なら、LastScanTime の根拠(lastReported が入っていたか、scanTime へ落ちたか等)を後追いしやすく、チーム内の問い合わせ対応コストが下がります。
結論:DB 画面の「最終スキャン日時」に寄せたいなら lastReportedScanTime を第一候補にする
SecurityResources で Defender for Cloud の脆弱性評価を扱うとき、scanTime や timeGenerated は “それっぽい” 反面、DB レベルの最終スキャン日時と一致しないことがあります。UI と同じ感覚で “DB の最終スキャン” を取りたいなら、まずは properties.additionalData.lastReportedScanTime を参照し、必要に応じて coalesce() で欠損にも備えるのが実務的です。
レポートの精度が上がるだけでなく、「スキャン停止の検知」「スキャンが古い DB の抽出」「運用監視の自動化」まで一気に進めやすくなるので、ぜひ既存クエリの ScanTime 生成部分から置き換えてみてください。

コメント