パッチ適用までの時間指標:QualysとElasticを用いた生存分析アプローチ
はじめに
さまざまな環境やチームにわたって脆弱性がどれだけ早く修復されるかを理解することは、強力なセキュリティ体制を維持するために重要です。この記事では、 Elastic Stackを使用して、 Qualys VMDRの脆弱性管理 (VM) データに生存分析を適用する方法について説明します。これにより、チームの速度 (チームがどれだけ早く作業を完了するか) と修復能力 (チームがどれだけの修正を行えるか) に関する一般的な仮定を確認できるだけでなく、測定可能な洞察を得ることもできました。セキュリティデータのほとんどは Elastic Stack にあるため、このプロセスは他のセキュリティデータソースでも簡単に再現できるはずです。
なぜそれをしたのか
私たちの主な動機は、次のような点について、一般的な仮定からデータに裏付けられた洞察へと移行することでした。
- さまざまなチームや環境が脆弱性をパッチする速度
- パッチ適用のパフォーマンスが内部サービスレベル目標 (SLO) を満たしているかどうか
- ボトルネックや遅延が頻繁に発生する場所
- パッチ適用のパフォーマンスに影響を与える他の要因
なぜ生存分析を行うのか?平均修復時間よりも優れた代替手段
脆弱性がどのくらい速く修正されたかを追跡するために、平均修復時間 (MTTR) がよく使用されますが、平均値と中央値はどちらも重大な制限を受けます (この記事の後半で例を示します)。平均値は外れ値[^1]の影響を非常に受けやすく、修復時間は平均修復時間を中心に均等にバランスが取れていると想定されますが、実際にはそうなることはほとんどありません。中央値は極端な値にはあまり敏感ではありませんが、分布の形状に関する情報を無視し、パッチ適用が遅い脆弱性のロングテールについては何も言及しません。どちらも、未解決のケース、つまり観測ウィンドウを超えて未解決のまま残っている脆弱性を考慮しておらず、多くの場合、完全に除外されています。実際には、最も長く放置された脆弱性こそが、私たちが最も懸念すべきものなのです。
生存分析はこれらの制限に対処します。医療と保険数理の文脈で生まれたこの手法は、打ち切られた観測を明示的に組み込みながらイベント発生までの時間をモデル化します。つまり、この文脈では脆弱性が未解決のまま残ることを意味します。(脆弱性管理への適用の詳細については、「The Metrics Manifesto」を強くお勧めします)。修復行動を単一の数値にまとめるのではなく、生存分析では、脆弱性が時間の経過とともにパッチ適用されないままになる確率を推定します(例:脆弱性の 90% は 30 日以内に修正されます。これにより、SLO 内(たとえば、30 日、90 日、または 180 日以内)に修正された脆弱性の割合など、より有意義な評価が可能になります。
生存分析は、脆弱性が時間の経過とともに修正されないままになる確率を推定する生存関数を提供します。
::: この方法により、修復パフォーマンスをより適切に把握できるため、脆弱性が存続する期間だけでなく、システム、チーム、重大度レベルによって修復動作がどのように異なるかを評価することができます。これは、多くの場合不完全で、偏っていて、正規性の仮定に抵抗するセキュリティ データに特に適しています。:::
コンテクスト
私たちはさまざまな環境、チーム、組織に生存分析を適用してきましたが、このブログでは Elastic Cloud の実稼働環境の結果に焦点を当てています。
脆弱性年齢の計算
脆弱性年齢を計算する方法はいくつかあります。
脆弱性遵守 SLOなどの社内指標では、脆弱性の経過時間を、脆弱性が最後に発見されたときと最初に検出されたとき(通常は公開後数日)の差として定義しています。このアプローチは、古いベース イメージから再導入された脆弱性をペナルティの対象とすることを目的としています。これまで、ベースイメージは満足できるほど頻繁に更新されていませんでした。新しいインスタンスが作成されると、脆弱性は発見された日からかなりの期間(例: 100 日)経過することがあります。
この分析では、最後に発見された日付と最初に発見された日付の間の日数に基づいて年齢を計算する方が適切であることがわかりました。この場合、年齢はシステムが実際に公開された日数を表します。
「すべてにパッチを当てる」戦略
当社のクラウド環境では、すべてにパッチを適用するポリシーを維持しています。これは、すべてのインスタンスでほぼ同じベースイメージを独占的に使用するためです。Elastic Cloud は完全にコンテナー上で動作するため、特定のアプリケーション パッケージ (Elasticsearch など) がシステムに直接インストールされることはありません。その結果、私たちの艦隊は均質性を保っています。
データパイプライン
Elastic Stack へのデータの取り込みとマッピングは面倒な場合があります。幸いなことに、これらをネイティブに処理するセキュリティ統合は数多くあり、 Qualys VMDRもその 1 つです。
この統合は 3 カスタム取り込み方法(例:スクリプト、ビート、…):
- Qualys ナレッジ ベースから脆弱性データをネイティブに強化し、CVE ID、脅威インテリジェンス情報などを追加します。強化パイプラインを構成する必要はありません。
- Qualysデータは、Elastic Common Schemaにすでにマッピングされています。これは、データがどのソースから来たかに関係なく、データを表す標準化された方法です。たとえば、CVEは常にフィールドvulnerability.idに保存されます。ソースに依存しません。
- 最新の脆弱性を含むトランスフォームがすでに設定されています。このインデックスを照会すると、最新の脆弱性ステータスを取得できます。
Qualysエージェント統合構成
生存分析を行うには、アクティブな脆弱性とパッチが適用された脆弱性の両方を取り込む必要があります。特定の期間を分析するには、フィールドmax_days_since_detection_updatedに日数を設定する必要があります。私たちの環境では、Qualys データを毎日取り込んでいるので、固定データの長い履歴を取り込む必要はありません。これはすでに実行済みです。
Qualys VMDR エラスティック エージェント統合は次のように構成されています。
| 財産 | Value | コメント |
|---|---|---|
| (設定セクション)ユーザー名 | ||
| (設定セクション)パスワード | Qualys には使用できる API キーがないため、基本認証でのみ認証できます。このアカウントでSSOが無効になっていることを確認してください | |
| URL | https://qualysapi.qg2.apps.qualys.com (US2 の場合) | https://www.qualys.com/プラットフォーム識別/ |
| 間隔 | 4時間 | 取り込まれたイベントの数に基づいて調整します。 |
| 入力パラメータ | show_asset_id=1& include_vuln_type=confirmed& show_results=1& max_days_since_detection_updated=3& status=New, Active, Re-Opened, Fixed& filter_superseded_qids=1& use_tags=1& tag_set_by=name& tag_include_selector=all& tag_exclude_selector=any& tag_set_include=status:running& tag_set_exclude=status:terminated,status:stopped,status:stale& show_tags=1& show_cloud_tags=1 | show_asset_id=1: アセットIDを取得します。 show_results=1: 現在インストールされているパッケージとインストールする必要があるバージョンの詳細を表示します。 max_days_since_detection_updated=3: 過去 3 日間更新されていない脆弱性を除外します(例:パッチ適用日数が 3 日を超えるもの)ステータス=新規、アクティブ、再オープン、修正済み: すべての脆弱性ステータスが取り込まれます。filter_superseded_qids=1: 置き換えられた脆弱性を無視します。タグ: タグでフィルタリングします。show_tags=1: Qualysタグを取得します。show_cloud_tags=1: クラウドタグを取得します。 |
データが完全に取り込まれると、Kibana Discover (logs-* データ ビュー -> data_stream.dataset : "qualys_vmdr.asset_host_detection" ) または Kibana セキュリティ アプリ (検出結果 -> 脆弱性) で確認できます。
elasticsearchクライアントを使用してPythonにデータをロードする
生存分析の計算は Python で行われるため、Elastic から Python データフレームにデータを抽出する必要があります。これを実現するにはいくつかの方法がありますが、この記事ではそのうちの 2 つに焦点を当てます。
ES|QL付き
最も簡単で便利な方法は、矢印形式で ES|QL を活用することです。Python データフレーム (行と列) が自動的に入力されます。詳細については、ブログ記事「ES|QL から Python のネイティブ Pandas データフレームへ」をお読みになることをお勧めします。
from elasticsearch import Elasticsearch
import pandas as pd
client = Elasticsearch(
"https://[host].elastic-cloud.com",
api_key="...",
)
response = client.esql.query(
query="""
FROM logs-qualys_vmdr.asset_host_detection-default
| WHERE elastic.owner.team == "platform-security" AND elastic.environment == "production"
| WHERE qualys_vmdr.asset_host_detection.vulnerability.is_ignored == FALSE
| EVAL vulnerability_age = DATE_DIFF("day", qualys_vmdr.asset_host_detection.vulnerability.first_found_datetime, qualys_vmdr.asset_host_detection.vulnerability.last_found_datetime)
| STATS
mean=AVG(vulnerability_age),
median=MEDIAN(vulnerability_age)
""",
format="arrow",
)
df = response.to_pandas(types_mapper=pd.ArrowDtype)
print(df)
現在、ESQL には制限があり、結果をページ分割することはできません。したがって、出力ドキュメントは 10K に制限されます (サーバー構成が変更された場合は 100K)。進捗状況は、この機能強化リクエストを通じて確認できます。
DSLの場合
elasticsearch python クライアントには、透過的なページ区切りを使用してクエリからすべてのデータを抽出するネイティブ機能があります。難しいのは、DSL クエリを作成することです。Discover でクエリを作成し、「検査」をクリックしてから「リクエスト」タブをクリックして DSL クエリを取得することをお勧めします。
query = {
"track_total_hits": True,
"query": {
"bool": {
"filter": [
{
"match": {
"elastic.owner.team": "awesome-sre-team"
}
},
{
"match": {
"elastic.environment": "production"
}
},
{
"match": {
"qualys_vmdr.asset_host_detection.vulnerability.is_ignored": False
}
}
]
}
},
"fields": [
"@timestamp",
"qualys_vmdr.asset_host_detection.vulnerability.unique_vuln_id",
"qualys_vmdr.asset_host_detection.vulnerability.first_found_datetime",
"qualys_vmdr.asset_host_detection.vulnerability.last_found_datetime",
"elastic.vulnerability.age",
"qualys_vmdr.asset_host_detection.vulnerability.status",
"vulnerability.severity",
"qualys_vmdr.asset_host_detection.vulnerability.is_ignored"
],
"_source": False
}
results = list(scan(
client=es,
query=query,
scroll='30m',
index=source_index,
size=10000,
raise_on_error=True,
preserve_order=False,
clear_scroll=True
))
生存分析
コードを参照して理解したり、データセットで再現したりすることができます。
学んだこと
Cyentia Instituteの調査を参考に、平均値、中央値、生存曲線を使用して脆弱性の修復にかかる時間を測定するいくつかの方法を検討しました。それぞれの方法により、パッチ適用までの時間データを理解するための異なるレンズが提供され、どの方法を使用するかに応じて、脆弱性がどの程度適切に対処されているかについて非常に異なる結論を導き出すことができるため、比較が重要です。
最初の方法は、すでに解決された脆弱性にのみ焦点を当てます。パッチ適用に要した時間の中央値と平均を計算します。これは直感的でシンプルですが、潜在的に大きな重要なデータ部分(まだ残っている脆弱性)が省略されます。その結果、特に一部の脆弱性が他の脆弱性よりも長期間にわたって未解決である場合、修復にかかる実際の時間を過小評価する傾向があります。
2 番目の方法は、脆弱性がこれまでにオープンになってから経過した時間を使用して、クローズされた脆弱性とオープンな脆弱性の両方を含めようとします。未解決の脆弱性に対するパッチ適用までの時間を概算する方法は多数ありますが、ここでは簡単にするために、報告時点でパッチが適用されている(適用される予定?)と仮定しましたが、これは正しくないことはわかっています。しかし、それはそれらの存在を考慮に入れる方法を提供します。
3 番目の方法では、生存分析を使用します。具体的には、Kaplan-Meier 推定量を使用して、特定の時点で脆弱性がまだ開いている可能性をモデル化しました。この方法は、未修正の脆弱性を適切に処理します。つまり、パッチが適用されているかのように見せかけるのではなく、「検閲された」データとして扱います。生成される生存曲線は時間の経過とともに低下し、数日または数週間が経過してもまだ残っている脆弱性の割合を示します。
脆弱性はどれくらい持続しますか?
現在の6か月間のスナップショット[^2]では、クローズドのみのパッチ適用時間は中央値が約33日、平均値が約35日となっています。表面的には妥当に見えますが、カプラン・マイヤー曲線を見ると、これらの数字に隠れているものが見えてきます。 33 日の時点で、約 54% がまだ開いており、 35 日の時点で、約 46% がまだ開いています。つまり、「典型的な」1 か月が経過しても、問題の約半分は未解決のままです。
また、これまでに観測された統計も計算しました(測定期間の終了時に未解決の脆弱性が修正されたものとして扱います)。この期間では、今日の未処理項目の経過期間が 1 か月近くに集まっているため、ほぼ同じになります (中央値約 33 日、平均値約 35 日)。この偶然の一致により平均値が安心できるように見えますが、これは偶発的で不安定です。スナップショットを月次パッチ プッシュの直前にシフトすると、基礎となるプロセスに何の変化もないのに、同じ統計が急激に減少します (観測された中央値は約 19 日、平均値は約 15 日です)。
生存曲線は、「30/60/90日後にまだ開いている割合」という質問に答え、1か月をはるかに超えて開いているロングテールを可視化するため、この罠を回避します。
あらゆる場所ですべてを同じようにパッチしますか?
層別生存分析は、生存曲線の考え方をさらに一歩進めたものです。すべての脆弱性を 1 つの大きなプールでまとめて調べるのではなく、意味のある特性に基づいて脆弱性をグループ (または「層」) に分けます。私たちの分析では、脆弱性を重大度、資産の重要度、環境、クラウド プロバイダー、チーム/部門/組織別に分類しました。各グループには独自の生存曲線があり、このサンプル グラフでは、さまざまな脆弱性の重大度が時間の経過とともにどれだけ速く修復されるかを比較します。
このアプローチの利点は、集計では隠れてしまう差異を明らかにすることです。全体的な生存曲線だけを見た場合、全体的な修復パフォーマンスについてのみ結論を導き出すことができます。しかし、階層化により、異なるチーム、環境、または重大度の問題が他のものよりも速く対処されているかどうかが明らかになり、私たちのケースでは、すべてにパッチを適用する戦略が実際に一貫していることがわかります。このレベルの詳細は、対象を絞った改善を行うために重要であり、修復に一般的にどのくらいの時間がかかるかだけでなく、実際のボトルネックが存在するかどうか、またどこに存在するかを把握するのに役立ちます。
チームはどのくらい速く行動しますか?
生存曲線は脆弱性がどれだけ長く残っているかを強調しますが、代わりに累積分布関数 (CDF) を使用することで視点を反転することができます。CDF は、脆弱性がどれだけ速く修正されるかに焦点を当て、特定の時点までに修復された脆弱性の割合を示します。
CDF をプロットするという選択により、修復速度が明確に示されますが、このバージョンには、観測された時間枠内に修正された脆弱性のみが含まれていることに注意することが重要です。ライフサイクル全体を捉えるために6か月間のコホートにわたって計算する生存曲線とは異なり、CDFは、その月に完了した項目に基づいて月ごとに計算されます[^3]。
したがって、これはチームが脆弱性を修復した後、どれだけ早く修復したかを示しており、未解決の脆弱性がどれだけ長く未解決のままであるかを反映するものではありません。たとえば、今月クローズされた脆弱性の 83.2% が最初の検出から 30 日以内に解決されたことがわかります。これは、最近の成功したパッチのパッチ適用速度を強調していますが、未解決のままであり、パッチ適用までの期間が長くなる可能性のある、長期間存在する脆弱性は考慮されていません。したがって、短期的な対応行動を理解するために CDF を使用しますが、ライフサイクル全体のダイナミクスは CDF と生存分析の組み合わせによって提供されます。CDF はチームがパッチを適用した後どのくらい速く行動するかを示しますが、生存曲線は脆弱性が実際にどのくらい長く続くかを示します。
生存分析と平均値・中央値の違い
待ってください。外れ値の影響を避けるために、パッチ適用までの時間を分析するには生存分析の方が良いと言いました。しかし、この例では、平均/中央値と生存分析で同様の結果が得られます。付加価値とは何でしょうか?理由は簡単です。パッチ適用プロセスは完全に自動化され、効率的であるため、実稼働環境に異常値が発生しないからです。
異種データへの影響を示すために、自動パッチ適用が行われていない非本番環境の古い例を使用します。
ESQL クエリ:
FROM qualys_vmdr.vulnerability_6months
| WHERE elastic.environment == "my-outdated-non-production-environment"
| WHERE qualys_vmdr.asset_host_detection.vulnerability.is_ignored == FALSE
| EVAL vulnerability_age = DATE_DIFF("day", qualys_vmdr.asset_host_detection.vulnerability.first_found_datetime, qualys_vmdr.asset_host_detection.vulnerability.last_found_datetime)
| STATS
count=COUNT(*),
count_closed_only=COUNT(*) WHERE qualys_vmdr.asset_host_detection.vulnerability.status == "Fixed",
mean_observed_so_far=MEDIAN(vulnerability_age),
mean_closed_only=MEDIAN(vulnerability_age) WHERE qualys_vmdr.asset_host_detection.vulnerability.status == "Fixed",
median_observed_so_far=MEDIAN(vulnerability_age),
median_closed_only=MEDIAN(vulnerability_age) WHERE qualys_vmdr.asset_host_detection.vulnerability.status == "Fixed"
| これまでの観察 | 閉店のみ | |
|---|---|---|
| カウント | 833 | 322 |
| 平均値 | 178.7(日) | 163.8(日) |
| 中央 | 61(日) | 5(日) |
| 生存期間中央値 | 527(日) | N/A |
この例では、平均値と中央値を使用すると、非常に異なる結果が得られます。単一の代表的な指標を選択するのは困難であり、誤解を招く可能性があります。生存分析グラフは、この環境内の脆弱性に対処する当社の有効性を正確に表しています。
最後に
生存分析を使用する利点は、より正確な測定だけでなく、パッチ適用動作のダイナミクスに関する洞察から得られ、ボトルネックが発生する場所、パッチ適用速度に影響を与える要因、およびそれが SLO と一致しているかどうかがわかります。技術的な統合の観点から見ると、運用ワークフローとレポートの一部としての生存分析の使用は、現在の Elastic Stack 設定に最小限の追加変更を加えるだけで実現できます。生存分析はパッチ適用サイクルと同じリズムで実行でき、結果は Kibana にプッシュバックされて視覚化されます。決定的な利点は、既存の運用メトリックと生存分析を組み合わせて、長期的な傾向と短期的なパフォーマンスの追跡の両方を実現できることです。
今後、私たちは、脆弱性が実際にどのように処理されるかをより動的に理解するための手段となる、 Arrival Rate 、 Burndown Rate 、 Escape Rateなどの新しい指標を追加で実験しています。
到着率は、新しい脆弱性が環境に侵入する速度を測る指標です。たとえば、毎月 50 件の新しい CVE が出現することを知っていれば、パッチの測定を開始する前に、ワークロードに何が予想されるかがわかります。したがって、到着率は、必ずしもバックログに関する情報ではなく、システムにかかるプレッシャーに関する情報を示す指標です。
バーンダウン レート(傾向) は、方程式のもう半分、つまり脆弱性がどのくらい速く修復されているかを示します。
エスケープ レートは、本来封じ込めるべきポイントをすり抜けてしまう脆弱性に焦点を当てることで、さらに別の次元を追加します。ここでのエスケープとは、パッチ適用ウィンドウを逃したり、SLO しきい値を超えたりした CVE に関するものです。エスケープ率の上昇は、脆弱性が存在することを示すだけでなく、パッチ適用サイクルが遅すぎる、自動化プロセスが欠如している、または補償制御が意図したとおりに機能していないなどの理由で、脆弱性を制御するために設計されたプロセスが失敗していることも示しています。
これらの指標を組み合わせることで、より正確な状況把握が可能になります。つまり、到着率は、どの程度の新しいリスクが導入されているかを示し、バーンダウンの傾向は、私たちがそのプレッシャーに対応できているか、それともそれに圧倒されているかを示し、脱出率は、計画された制御にもかかわらず脆弱性が残っている場所を明らかにします。
[1]:統計における外れ値とは、中心傾向から非常に離れた(またはデータセット内の残りの値から離れた)データポイントのことです。たとえば、ほとんどの脆弱性が 30 日以内に修正されるが、1 つの脆弱性が 600 日かかる場合、その 600 日のケースは外れ値となります。外れ値は、「典型的な」経験を反映しない形で平均値を引き上げたり引き下げたりする可能性があります。パッチ適用の観点から見ると、これらはパッチ適用が特に遅く、通常よりもはるかに長い間未解決のままとなる脆弱性です。これらは、簡単に更新できないシステムや、広範囲にわたるテストを必要とするパッチなど、まれではあるが重要な状況を表している可能性があります。
[2]: 注: 現在の6か月間のデータセットには、観測期間の終了時に未解決のままであるすべての脆弱性(未解決/最初に確認された時期に関係なく)と、6か月の期間中に解決されたすべての脆弱性の両方が含まれています。この混合コホートアプローチにもかかわらず、以前の観察期間からの生存曲線は、特に曲線の初期部分で一貫した傾向を示しています。最初の 30 ~ 60 日間の形状と傾きはスナップショット全体で非常に安定していることが証明されており、パッチ適用までの時間の平均や初期段階の修復動作などの指標は、短い観察期間によるものではないことが示唆されています。長期的な見積もり(例:短いスナップショットでは、パッチ適用の精度 (90 パーセンタイルなど) は不完全なままですが、これらのコホートから導き出された結論は、依然として永続的で信頼性の高いパッチ適用のダイナミクスを反映しています。
[3]:CDFは運用報告(当月に完了した作業のスループットとSLO遵守)のために月次サイクルを維持しましたが、カプランマイヤーは6か月の期間を使用して適切に打ち切りを処理し、より広範なコホート全体のテールリスクを明らかにしました。
