KQL(SecurityResources)でAzure SQL Databaseの最終スキャン日時を取得する方法:Defender for CloudのUIとズレるScanTimeをlastReportedScanTimeで解決

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)
)

フォールバックの優先順位は、次の考え方がわかりやすいです。

優先フィールド狙い注意点
1lastReportedScanTimeDB レベルの “最終スキャン日時” に寄せるレコードによっては欠損する可能性
2scanTime次点の “スキャン時刻” を拾うホスト/評価側に寄ることがある
3scanDate日付だけでも確保する粒度が粗く一致しにくい
4timeGenerated最終手段として “何かしらの時刻” を埋めるスキャン時刻ではなく生成/更新時刻の可能性

運用の現場では「欠損してレポートが落ちる」より「精度が落ちても値がある」ほうが助かる場面も多いので、まずはフォールバック有りを推奨します(監査用途などで厳密一致が必要なら、フォールバックを外して欠損を別途扱うのが安全です)。

“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 生成部分から置き換えてみてください。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次