← 記事一覧へ

ARTEXを内側から読む:ガードレールが見落とすもの、残る痕跡、ハントに必要なログ

攻撃ツールARTEXのラボ監査をソースコードで検証し、ツールが残す痕跡を整理。Sentinel・Falcon向けのログ点検表、準備状況を確かめるクエリ、通報先の一覧をまとめました。

事案の発生時期: 各社の報告は2026年10月7日〜11月上旬、コード確認は11月4日出典確認日: 2026/11/4

30秒でわかる要点

要点
公開されたラボ監査は、ARTEXのスコープ制御と承認制御が効くべき場所にないと指摘しました。同じリリースのコードを読み、6つの所見をすべて確認し、検索に使える固定のユーザーエージェントなど3つの詳細を加えました。そのうえで、16種類のログ点検表、SentinelとFalconのクエリ8本、通報先の一覧にまとめました。
対応するチーム
SIEM、EDR、IT運用、法務・プライバシー、委託先リスク
最初のアクション
今日、SentinelでS01とS02を、またはFalconでC01とC02を実行します。MISSINGやSTALEのログ、空のユーザーエージェント欄は、期限を付けて担当に渡します。次に、Microsoftの手順が必要とし、Entraが保持しないGraphのアクティビティとサービスプリンシパルのサインインをエクスポートします。
不確実性・注意点
ARTEXのコードは読みましたが、実行していません。調査環境では第三者のコードを実行できません。実行時の挙動はCTI Academyのラボ結果に基づきます。非公開版やフォークは異なる場合があります。クエリは実環境で実行しておらず、KQLはMicrosoftの構文解析器を通過し、CQLは目視で確認しました。
ヘッダー図版:ARTEXを内側から読む。4つの枠:承認ゲートが既定で対象にするツールは8種。組み込みブロックリストのドメイン末尾は4つで.go.jpはない。自動更新が確認する署名は0。エクスポートしない場合にEntraがサインインログを保持するのは30日(P1/P2)。
実務への示唆

ARTEXが掲げる2つの安全制御は、効くべき場所にありません。スコープのブロックリストはツールが接続するときではなく、エージェントが資産を記録するときに照合されます。承認ゲートは名前で指定した8種のツールだけが対象で、カスタムツールとMCPツールは素通りします。ツール自身には、運用者も攻撃者も範囲内にとどめる仕組みがありません。防御側に役立つのは、コードから分かる固定のユーザーエージェント、既定のポート、特徴的なプロセスツリーです。ただし、ログがなければ何も見えません。MicrosoftがStorm-0940について示した確認手順が使うログの多くは、既定でオフです。ログが届いていることを確かめ、痕跡を探し、誰に知らせていつ外部の支援を呼ぶかを事前に決めておきます。

ARTEX自身の安全制御は誰も範囲内にとどめず、それを見るために必要なログの多くは既定でオフです。 2026年10月、AIエージェントが侵入テストを計画して実行するオープンソースのフレームワークARTEXが、実際の侵入と結び付けられました。CrowdStrikeとZscalerは韓国の金融機関への攻撃に、MicrosoftはStorm-0940によるクラウドテナントへの攻撃に、それぞれ関連付けています。その後CTI Academyは、より狭い問いを立てました。ツール自体はどれほど安全なのか、という問いです。同社のラボ監査では、スコープと承認の制御が、名前から想像されるよりはるかに狭い範囲でしか効いていませんでした。私たちは同じリリースのソースコードを読んでこの監査を独立に検証し、ツールが残す痕跡を探しました。そして両方を、SOCが必要とする形にまとめました。Microsoft SentinelとCrowdStrike Falcon向けのログ点検表、準備状況の確認とハントのためのクエリ8本、そして誰に知らせるかの一覧です。攻撃そのものを探す14のハントはAIハーネスのハンティング・フレームワークに、対応手順はARTEX対応プレイブックにあります。

何が起きたか、何が確認されているか

2026年11月4日時点の状況です。

報告 日付 内容 位置付け
CrowdStrike 10月7日 9月下旬から、正体不明の攻撃者がARTEXを使って韓国の金融機関を攻撃。あるARTEXのホストには、Claude Codeのセッション履歴とARTEXの設定ファイルを置いた公開ディレクトリがあり、攻撃者は9つのプロキシIPを使用。 ベンダーの分析。攻撃者は特定せず
JPCERT/CC 10月8日(23日更新) 国内組織への不正アクセス。API、JSPのWebシェル、AIフレームワークの悪用という別々の事例に分けて説明。 注意喚起。すべての事例がARTEXと結び付くわけではない
Zscaler ThreatLabz 10月14日 金銭目的の中国語話者の攻撃者(確度は中程度)が周辺のWebアプリを悪用し、約6万7,000人分の個人データを窃取。ARTEXの既定のブロックリストは政府と教育機関のドメインが対象で、金融は対象外。 ベンダーの評価
Microsoft 10月21日(26日更新) Storm-0940はConnectWiseの侵害の後、ARTEXを使って探索、パスワードスプレー、OAuthアプリの悪用を行い、複数の事例で3時間未満のうちにクラウドの特権アクセスを得た。8組織の327アカウントへのスプレーでは、ログインの成功はなかった。 Microsoftによる帰属
CTI Academy 11月上旬 公開されているartex-globalフォークのv0.3.15-global.1をラボで監査。危険な既定値と制御の位置について、深刻度「高」3件と「中」3件。 ラボでの調査
本稿 11月4日 同じタグのコードとエージェントSDKを確認。6件の所見はすべてコードと一致し、3つの詳細を追加。 コードの確認。ARTEXは実行せず

「帰属」は各社の評価で、裁判所の判断ではありません。誰でも入手できるツールが使われたという事実だけでは、誰の攻撃かはほとんど分かりません。

ツール自身の制御が防御側にとって重要な理由

  • 自社のレッドチームがARTEXのようなハーネスを使う場合、ツールのスコープ制御が封じ込めの役割を担います。それが効くべき場所になければ、テストが本番環境、クラウドのメタデータサービス、他社のネットワークに届きかねません。
  • 攻撃者が使う場合、フォークで変更されるまでは、既定値がそのまま探索用の指紋になります。
  • 誰かが外部に公開した場合、記録用プロキシは、見つけた人なら誰でも使える中継点になります。

ARTEXの構造と、制御の位置

ARTEX Global Editionは、Webの画面を組み込んだ1つのGoの実行ファイルとPostgreSQLのデータベースで動きます。プランナーのエージェントが目標を「インテント」に分解し、ワーカーのエージェントが実際のツールを実行し、結果を資産と所見のグラフに蓄積します。ツールが送る要求はすべて記録用プロキシが記録します。管理者アカウントは1つだけの、1人で使う製品です。

3段の図。計画:プランナーが目標をインテントに分解し、insert_assetsかadd_intentによるグラフ書き込みの時点で資産ブロックリストを照合する。add_intentは資産IDがある場合だけ照合する。組み込みのブロックリストは.gov、.gov.cn、.edu、.edu.cnで、.go.jp、.lg.jp、.ac.jp、.go.kr、金融は含まない。ツールの呼び出し:Bash、WebFetch、web_search、shell_open、shell_send、Write、Edit、MultiEditの組み込み8種は承認ゲートを通る。承認ゲートは対象リストにある名前のときだけ動き、照合するのはツール名か入力JSONで、組み立てられたコマンドではない。カスタムのcommand・script・HTTPツールとmcp__サーバー名__ツール名という名前のMCPツールはゲートを通らない。最後はARTEXホスト上のbash -cになり、DBやモデルのAPIキーを含む環境変数をすべて引き継ぐ。ネットワーク:8788番の記録用プロキシはgo-mitmproxyで、認証も証明書の検証もない。実行ファイルは127.0.0.1、Dockerイメージは全インターフェースで待ち受け、任意の外向きプロキシを経て攻撃対象に届く。この経路では資産ブロックリストを照合しない。プロキシで失敗するとWebFetchは直接再試行する。

図1:v0.3.15-global.1のソースコードを読んで作成したオリジナル図版。重要な2つの制御は、要求が外に出る場所ではなく、グラフへの書き込みとツール名に対して効いています。

どのように確かめたか

  • 一次資料。 公開されているartex-globalのリポジトリを取得し、CTI Academyが監査したタグv0.3.15-global.1(コミットd6964594、2026年10月14日)を確認しました。ARTEXが使うエージェントSDKのnorma v0.4.3もGoのモジュールプロキシから取得して読みました。フォークの最新コミット(10月14日のfdfb7a1)でも、本稿で引用する箇所はすべて同じです。
  • 方法。 制御ごとに、定義された場所だけでなく呼び出される場所をたどりました。資産ゲートのすべての呼び出し元、インターセプターの対象リストとそれを迂回する経路、プロキシの設定、更新機能の完全性確認、そしてコードに埋め込まれたユーザーエージェントです。以下のファイル名と行番号はタグ時点のものです。
  • 行わなかったこと。 ARTEXは実行していません。CTI Academyと同様に、エージェントもネットワークも使わずARTEX自身のゲート関数を呼ぶ小さなテストを書きましたが、調査環境では第三者のコードを実行できないため、動かしていません。実行時の挙動はCTI Academyのラボ結果、コードの経路は私たちの確認に基づきます。上流の版は非公開で、フォークによって異なることがあります。
  • 独立した確認。 同じリリースを、CTI Academyは動作環境で、私たちはソースコードで、別々に調べました。両者が一致する点を「確認」と書いています。

コードから分かること

7つの所見のスコアカード。各所見について、CTI Academyのラボ結果、コードから分かること、対応を示す。1、スコープのブロックリスト、深刻度は高、確認と追加の詳細:ブロック対象のホストにカスタムツールで到達できた。照合はグラフ書き込み時だけで、資産IDのないadd_intentは素通り、組み込みは.govと.eduのみ。範囲の制限はネットワーク側で行う。2、承認ゲート、高、確認:同じDELETEがBashでは拒否、カスタムツールでは許可された。対象はツール名8種で、ルールは名前か入力JSONを見るだけ、カスタムcommandはBashの中核を直接呼ぶ。能力で制御する。3、認証のない記録用プロキシ、Dockerの既定値で導入した場合は高、確認:別ホストから認証なしで中継できた。実行ファイルは127.0.0.1:8788、Docker CMDは:8788、composeは127.0.0.1に公開し、認証もTLS検証もない。8788番を公開せず、社内で探す。4、引数インジェクション、中、確認:-で始まる値がオプションとして渡る。単一引用符はシェルの悪用は防ぐがオプション注入は防がない。テンプレートに--を置く。5、ホストの秘密情報、中、確認と追加の詳細:bash -cとPythonツールがARTEX_PG_DSN、OPENAI_API_KEY、ANTHROPIC_API_KEYを含む環境変数をすべて継承する。使い捨てのVMで動かす。6、署名のない自動更新、中、確認:現在は非公開のAutumn-27/artexに固定され、同じリリースのSHA256SUMSを信頼する。更新経路を止める。7、固定のユーザーエージェント、新規:WebFetchはnorma/0.4、タイトル取得はartex-enrich/1.0を送り、プロキシで失敗するとWebFetchは直接再試行する。Webログで両方の文字列を探す。点線の枠には、持ちこたえた点として、認証なしのAPIが401を返し、JWTの偽造が失敗し、ログインが回数制限され、パスワード設定前にUIが公開されると警告が出ることを記す。

図2:オリジナル図版。「確認」は、CTI Academyが動作環境で見た結果とコードの経路が一致したことを示します。

1. スコープのブロックリストは、ツールの接続時ではなく資産の記録時に照合される

ARTEXの「資産ブロック」のルールはdb.EvaluateAssetGateで評価されます。エージェントの中でこの関数を呼ぶのは、insert_assets(agent/tools_insert.go:244)とadd_intent(agent/tools.go:1053)の2か所だけで、どちらもナレッジグラフへの書き込みです。接続を開く前にルールを確認する実行ツールはありません。だからこそ、CTI Academyのカスタムツールはブロック対象のホストに届きました。

監査には書かれていない点が2つあります。1つ目に、add_intentがルールを照合するのは、インテントに資産IDが指定されている場合だけです(len(anchors) > 0)。指定のないインテントは照合されません。2つ目に、ARTEXが初回起動時に登録するルールは、.gov、.gov.cn、.edu、.edu.cnの4つのドメイン末尾です(db/db.go:302–305)。Zscalerは金融機関が対象外だと指摘しましたが、日本と韓国の公的機関も対象外です。.go.jp、.lg.jp、.ac.jp、.go.kr、.ac.krはリストにありません。

2. 承認ゲートの対象は8つのツール名

インターセプトのルールが動くのは、defaultEnabledToolsにあるBash、WebFetch、web_search、shell_open、shell_send、Write、Edit、MultiEditだけです(intercept/intercept.go:159–163)。それ以外の名前では、ガードはルールを1つも見ずに処理を返します(guard/guard.go:87)。カスタムのcommandツールはテンプレートを組み立ててBashの中核を直接呼び(server/customtool.go:360)、MCPツールはmcp__<サーバー名>__<ツール名>という名前になるため(mcphttp/client.go:35)、どちらも対象外です。対象のツールであっても、ルールが照合するのはツール名か入力そのもののJSONで(intercept/intercept.go:550–552)、テンプレートから組み立てられたコマンドではありません。rm -rfからHTTPのDELETEまで、組み込みの拒否ルール19本はすべて入力を照合します。

3. 記録用プロキシには認証がなく、Dockerイメージは全インターフェースで待ち受ける

実行ファイルの既定値は、画面が127.0.0.1:8787、プロキシが127.0.0.1:8788です(cmd/artex/main.go:55–57)。Dockerイメージはこれを:8787と:8788で上書きし(Dockerfile:45)、コンテナ内の全インターフェースで待ち受けます。同梱のdocker-compose.ymlは127.0.0.1だけに公開しますが(46〜47行目)、docker run -p 8788:8788のように起動すると外部に公開されます。プロキシはgo-mitmproxy v1.9.2で、プロキシの認証はなく、SslInsecure: true(traffic/traffic.go:206)のため、接続先の証明書も検証しません。設定した外向きプロキシを経由させることもでき(221行目)、その場合、攻撃先からはそのプロキシのアドレスが見えます。CrowdStrikeが公表した9つのプロキシIPはこの使い方と矛盾しませんが、報告書はその設定方法を書いていません。

4. 引用符はシェルの悪用を防ぐが、オプションの注入は防がない

カスタムツールに渡す値は単一引用符で囲まれます(server/customtool.go:508–516)。これで;や$()による注入は防げます。しかし、-で始まる値は、CTI Academyが確かめたとおり、対象のプログラムにオプションとして渡ります。

5. エージェントのシェルはすべて、ホストの秘密情報を引き継ぐ

Bashツールは、プロセスの環境変数すべてにセッションのプロキシ設定を加えてbash -cを実行します(normaのtool/task.go:140–145と566–570)。カスタムのPythonツールも同様です(server/customtool.go:414)。ARTEXは、データベースの接続文字列とモデルの鍵をこの環境変数から読みます。ARTEX_PG_DSN、OPENAI_API_KEY、ANTHROPIC_API_KEYです。CTI Academyはシェルから/proc/self/environを読めることを示しましたが、コードを見るとその必要すらありません。単にenvを実行すれば表示されます。エージェントに1つコマンドを実行させるプロンプトインジェクションで十分です。

6. 自動更新は、同じリリースのチェックサムを信頼する

更新機能はGitHubのAutumn-27/artexに固定されています(selfupdate/github.go:15–18)。リダイレクト先をGitHubの5つのホストに限り、ダウンロードしたファイルを同じリリースのSHA256SUMSと照合します(selfupdate/stage.go:19と66)。署名の確認はありません。上流のリポジトリは非公開になったため、現時点では確認に失敗します。今後このリポジトリを支配した者は、「今すぐ更新」を押したすべての環境を支配できます。

7. 固定のユーザーエージェントと、プロキシを迂回する取得

これは今回の新しい所見です。エージェントのWebFetchツールはUser-Agent: norma/0.4を送ります(normaのtool/webfetch.go:188)。エージェントが見つけたWeb資産ごとにタイトルを読むARTEXの補完エンジンはartex-enrich/1.0を(enrich/enrich.go:233)、更新機能はartex-selfupdateを送ります。プロキシ経由の取得がプロキシ側の理由で失敗すると、WebFetchは一度だけ直接再試行します(tool/webfetch.go:190–198)。この再試行は記録用プロキシとその先の外向きプロキシを通らないため、運用者の実際のアドレスから届く可能性があります。これらの文字列は簡単に変えられますし、ARTEXの通信の大半はcurlなど普通のツールから出ますが、探す手間はほとんどかかりません。

持ちこたえた点

CTI Academyは、認証情報なしで管理APIに入る方法を見つけられませんでした。保護された経路は401を返し、JWTの偽造は失敗し、ログインは接続元ごとに回数制限されます。コードは、管理者パスワードを設定する前に画面がループバック以外のアドレスで待ち受けていると、起動時に警告を出します(server/auth.go:394–408)。

ARTEXが残す痕跡

2列のカード。ARTEXを動かしているホスト:artexからbash -c、さらにcurl、nmap、python3へと続くプロセスツリー。単純なコマンドはbashを置き換えるため親はartexになり、start.shは終了したartexを再起動する。待ち受けポートは127.0.0.1:8787と127.0.0.1:8788、Dockerイメージではコンテナ内の全インターフェース。実行ファイルの隣には、_index/index.sqlite、_blobs、_caを含むdataと、短命の.toolsのスクリプト。環境変数にはARTEX_PG_DSN、OPENAI_API_KEY、ANTHROPIC_API_KEYがあり、エージェントのすべてのシェルから見える。api.github.comのAutumn-27/artexへの更新確認はユーザーエージェントartex-selfupdateで、現時点では失敗する。攻撃先:ユーザーエージェントnorma/0.4によるページ取得。記録用プロキシを経由するartex-enrich/1.0によるタイトル取得。それ以外はcurl、python-requests、Python-urllibなど普通のユーザーエージェントで、MicrosoftはStorm-0940でこれらとazure-toolkit/1.0を確認した。送信元は外向きプロキシのIPで、CrowdStrikeは1つの攻撃者について9つを公表したが、WebFetchの直接再試行では実際のホストが見えることがある。攻撃先で起きることとして、ZscalerはWebシェルとデータ窃取を、Microsoftはパスワードスプレーとアプリの悪用を報告した。

図3:オリジナル図版。どれも確認すべき手がかりです。フォークは名前を変えますし、承認済みの自社テストも同じに見えます。

ARTEXを動かしているホストには、既定値の特徴がはっきり出ます。bash -cを起動するartexという実行ファイル、8787番と8788番での待ち受け、記録した通信と中間者用の証明書局を置いたデータディレクトリ、そして環境変数にあるモデルの鍵です。これが意味を持つのは2つの場面です。社内の誰かが承認なしにARTEXを動かしている場合と、攻撃者が侵害したサーバーにARTEXを入れた場合です。

攻撃先から見ると、ARTEXは自分が動かすツールとほとんど同じに見えます。2つの固定のユーザーエージェントは、手間のかからない手がかりです。MicrosoftがStorm-0940のサインインとGraphの呼び出しで確認したユーザーエージェントは、python-requests、Python-urllib、curl、azure-toolkit/1.0というありふれたものでした。より強い証拠は実際に起きたこと、つまり受理された書き込み、新しいWebシェル、サインイン、アプリの変更で、これらはハーネス・フレームワークのハントで探せます。

ログの点検表

結果が空のハントに意味があるのは、そのハントが読むログが存在した場合だけです。MicrosoftはConnectWiseの侵害について、サービスプリンシパルのサインインを10月1日まで遡って確認し、Microsoft Graphのアクティビティ、Entraの監査記録、Key Vaultへのアクセスも見るよう求めました。多くのテナントでは、誰かが有効にするまで、これらはどれもMicrosoft Sentinelに届きません。

5列・16枚のカード。ID:L01 Entraの対話型サインイン(SigninLogs)、L02 非対話型のユーザーサインイン、L03 サービスプリンシパルとマネージドIDのサインイン、L04 ディレクトリ監査(AuditLogs)。いずれもEntra IDのコネクタか診断設定を構成するまでオフ。L05 Microsoft Graphのアクティビティは、P1かP2と診断設定が必要で、それ以外ではどこにも残らないため赤で示す。クラウド:L06 Azureのコントロールプレーン(AzureActivity)、L07 Key Vaultのデータプレーン(AZKVAuditLogs、Key Vaultごとに診断設定が必要)、L08 Microsoft 365のワークロード監査(OfficeActivityとCloudAppEvents)。Web:L09 WebとAPIのアクセスログ(W3CIISLog、AppServiceHTTPLogs、AGWAccessLogs)、L10 WAFの判定。ネットワーク:L11 送信バイト数付きのファイアウォール・プロキシログ(CommonSecurityLog)。エンドポイント:L12 プロセスの親子関係(DeviceProcessEventsとFalconのProcessRollup2)、L13 通信と待ち受けポート(DeviceNetworkEventsとFalconのNetworkConnectIP4、NetworkListenIP4)、L14 Web公開ディレクトリへの書き込み、L15 プロセスごとのDNS(DeviceEventsとFalconのDnsRequest)、L16 センサーの稼働状況(DeviceInfoとFalconのSensorHeartbeat)。DefenderのテーブルはDefender内に30日残り、SentinelにはXDRコネクタ経由でのみ届く。下部の帯は、結果が静かなハントを信用する前に、S01とS02(Sentinel)またはC01とC02(Falcon)でログが届いていることを確かめるよう促す。

図4:オリジナル図版。黄色は誰かが有効にするまでオフのもの。赤はオフでライセンスにも依存し、エクスポートしなければ失われるものです。

保存期間も、収集と同じくらい重要です。Entraはサインインと監査のログを、無料版で7日、P1かP2で30日保持します。Graphのアクティビティログは、どこかに送らなければまったく残りません。Defender XDRのハンティング用データは30日で、Sentinelは分析層の90日分の保持を料金に含めています。

2026年8月1日から11月4日までの時系列。サービスプリンシパルのサインインについてMicrosoftが示した確認期間は、10月1日から(少なくとも10月15日から)現在まで。Entra管理センターは無料版で7日、P1かP2で30日を保持するため、11月4日の時点で10月1日〜4日はすでに消えている。Microsoft Graphのアクティビティログはエクスポートしなければ何も残らない。Defender XDRの高度な追求は30日。Sentinelの分析層は、コネクタが有効なら90日分を追加の保存費用なしで保持する。注記には、11月4日の時点でEntra管理センターだけに頼るテナントは10月1日〜4日をもう確認できず、無料版では直近1週間しか見えないとある。

図5:オリジナル図版。棒は11月4日時点で保持される日数で、特定のテナントの設定を示すものではありません。

ID ログの種類 Sentinelのテーブル Falconのデータ 必ず埋まっているべき項目 有効にする方法 最低保存期間 担当
L01 Entra IDの対話型サインイン SigninLogs Entra IDのサインイン用データコネクタ クライアントIP、ユーザーエージェント、ユーザー、結果 既定でオフ:Entra IDコネクタか診断設定 90日(Entra側は30日) ID管理
L02 非対話型のユーザーサインイン AADNonInteractiveUserSignInLogs Entra IDのサインイン用データコネクタ クライアントIP、ユーザー、アプリ 既定でオフ:別カテゴリとして選択 90日 ID管理
L03 サービスプリンシパルとマネージドIDのサインイン AADServicePrincipalSignInLogs, AADManagedIdentitySignInLogs Entra IDのサインイン用データコネクタ クライアントIP、サービスプリンシパル、アプリID、結果 既定でオフ:別カテゴリとして選択 90日(Microsoftは10月1日まで遡る確認を求めた) ID管理
L04 ディレクトリ監査 AuditLogs Entra IDの監査用データコネクタ 操作、実行者、対象、結果 既定でオフ:Entra IDコネクタか診断設定 90日(Entra側は30日) ID管理
L05 Microsoft Graphのアクティビティ MicrosoftGraphActivityLogs Event Hubs経由でNext-Gen SIEMへ クライアントIP、ユーザーエージェント、アプリID、要求URI 既定でオフ:P1/P2と診断設定が必要。設定しなければどこにも残らない 90日 ID管理
L06 Azureのコントロールプレーン AzureActivity Azure用データコネクタ 呼び出し元、操作、リソース、結果 既定でオフ:サブスクリプションごとのコネクタかポリシー 90日 クラウド基盤
L07 Key Vaultのデータプレーン AZKVAuditLogs Azure用データコネクタ 呼び出し元IP、ID、操作、結果 既定でオフ:Key Vaultごとに診断設定 90日 クラウド基盤
L08 Microsoft 365のワークロード監査 OfficeActivity, CloudAppEvents Microsoft 365用データコネクタ ユーザーかアプリ、操作、クライアントIP 既定でオフ:Microsoft 365かDefender XDRのコネクタ 90日 コラボレーション基盤の管理者
L09 WebとAPIのアクセスログ W3CIISLog, AppServiceHTTPLogs, AGWAccessLogs WebまたはWAFのパーサー クライアントIP、ホスト、パス、メソッド、ステータス、ユーザーエージェント、ログインユーザー 既定でオフ:エージェントの収集ルールか診断設定 90日 AppSec・Web担当
L10 WAFの判定 AGWFirewallLogs, CommonSecurityLog WAFのパーサー ルール、アクション、クライアントIP、URI 既定でオフ:診断設定かログ転送 90日 AppSec・Web担当
L11 送信バイト数付きのファイアウォール・プロキシログ CommonSecurityLog ファイアウォールかプロキシのパーサー 送信元、宛先、送信バイト数、ユーザーエージェント 既定でオフ:ログ転送(CEF)の設定が必要 90日 ネットワーク担当
L12 サーバーのプロセスの親子関係 DeviceProcessEvents ProcessRollup2 親プロセス、子プロセス、コマンドライン、アカウント Defender:Defender内に30日。SentinelにはXDRコネクタ経由のみ インターネット公開サーバーは90日 EDR担当
L13 通信と待ち受けポート DeviceNetworkEvents NetworkConnectIP4、NetworkListenIP4 ローカルとリモートのIP・ポート、プロセス Defender:Defender内に30日。SentinelにはXDRコネクタ経由のみ インターネット公開サーバーは90日 EDR担当
L14 Web公開ディレクトリへの書き込み DeviceFileEvents 対象OSのファイル書き込みイベント パス、プロセス、ハッシュ Defender:Defender内に30日。SentinelにはXDRコネクタ経由のみ インターネット公開サーバーは90日 EDR担当
L15 プロセスごとのDNS問い合わせ DeviceEvents DnsRequest 問い合わせ名、プロセス Defender:Defender内に30日。SentinelにはXDRコネクタ経由のみ 30日以上 EDR担当
L16 資産台帳と照合したセンサーの稼働状況 DeviceInfo SensorHeartbeat 端末、最終確認日時、状態 Defender:Defender内に30日。SentinelにはXDRコネクタ経由のみ 毎日のスナップショット IT運用

ログ点検表をダウンロード(CSV)。状態、確認者、確認日時を書き込む空欄があります。Falconの列には、Next-Gen SIEMのパーサーかコネクタ、またはEDRのイベントを示しています。コネクタはライセンスによって異なるため、ログごとに利用できるかを確認し、EDRのイベント名は、自社のセンサーのOSについてFalconのEvents Data Dictionaryで確かめてください。

Sentinelのログが届いているか確かめる:S01とS02

S01は、期待するテーブルと、ワークスペースに実際に取り込まれたものを比べます。負荷の小さいUsageテーブルを90日分読み、テーブル本体は直近1時間分だけ読みます。MISSINGは90日間何も届いていないこと、STALEは直近1日に何も届いていないことを示します。

// Log-source readiness for hunting ARTEX-style intrusions (The Tokyo Nomad, 4 Nov 2026).
// Read-only. Not executed in a live tenant; checked with Microsoft's Kusto parser against Microsoft Learn schemas.
// Usage gives 90 days of ingestion history per table cheaply; a one-hour scan adds freshness and delay.
// MISSING = no ingestion in 90 days. STALE = nothing in the last 24 hours. Edit Expected to match your estate.
// Defender XDR tables reach Sentinel only through the XDR connector; without it they stay in Defender for 30 days.
let Expected = datatable(Layer:string, DataType:string, UsedFor:string)[
    "Identity", "SigninLogs", "Password spray and sign-in sources",
    "Identity", "AADNonInteractiveUserSignInLogs", "Token use outside interactive sign-ins",
    "Identity", "AADServicePrincipalSignInLogs", "App and service-principal sign-ins",
    "Identity", "AuditLogs", "Credential, role, owner and consent changes",
    "Identity", "MicrosoftGraphActivityLogs", "Graph calls by apps; not kept unless exported",
    "Cloud", "AzureActivity", "Subscription control-plane changes",
    "Cloud", "AZKVAuditLogs", "Key Vault secret reads and key operations",
    "Cloud", "OfficeActivity", "Mailbox, SharePoint and Teams access",
    "Cloud", "CloudAppEvents", "OAuth app activity",
    "Web", "W3CIISLog", "IIS requests with user agent",
    "Web", "AppServiceHTTPLogs", "App Service requests with user agent",
    "Web", "AGWAccessLogs", "Application Gateway requests with user agent",
    "Web", "AGWFirewallLogs", "Application Gateway WAF matches",
    "Network", "CommonSecurityLog", "Firewall, proxy or WAF events (CEF)",
    "Endpoint", "DeviceProcessEvents", "Process lineage on servers",
    "Endpoint", "DeviceNetworkEvents", "Listening ports and outbound connections",
    "Endpoint", "DeviceFileEvents", "Web shells and dropped tools",
    "Endpoint", "DeviceEvents", "DNS answers and other endpoint events",
    "Endpoint", "DeviceInfo", "Sensor health against inventory"
];
let History = Usage
    | where TimeGenerated > ago(90d)
    | summarize FirstIngest=min(TimeGenerated), LastIngest=max(TimeGenerated),
        DaysWithData=dcount(bin(TimeGenerated, 1d)), TotalMB=sum(Quantity) by DataType
    | extend AvgDailyMB=round(TotalMB / DaysWithData, 2);
let Fresh = union withsource=DataType isfuzzy=true
        SigninLogs, AADNonInteractiveUserSignInLogs, AADServicePrincipalSignInLogs, AuditLogs,
        MicrosoftGraphActivityLogs, AzureActivity, AZKVAuditLogs, OfficeActivity, CloudAppEvents,
        W3CIISLog, AppServiceHTTPLogs, AGWAccessLogs, AGWFirewallLogs, CommonSecurityLog,
        DeviceProcessEvents, DeviceNetworkEvents, DeviceFileEvents, DeviceEvents, DeviceInfo
    | where TimeGenerated > ago(1h)
    | summarize Newest=max(TimeGenerated),
        MedianDelayMin=round(percentile(datetime_diff("second", ingestion_time(), TimeGenerated) / 60.0, 50), 1) by DataType;
Expected
| join kind=leftouter History on DataType
| join kind=leftouter Fresh on DataType
| extend Status=case(isnull(LastIngest), "MISSING", LastIngest < ago(1d), "STALE", "FLOWING")
| extend Rank=case(Status == "MISSING", 0, Status == "STALE", 1, 2)
| order by Rank asc, Layer asc, DataType asc
| project Layer, DataType, Status, FirstIngest, LastIngest, DaysWithData, AvgDailyMB, Newest, MedianDelayMin, UsedFor
| take 100

S01をダウンロード

S02は、ハントが照合に使う項目が埋まっているかを確かめます。ユーザーエージェントのないWebログではS03に答えられず、IPアドレスのないサインインログでは攻撃の出どころについて何も分かりません。

// Field readiness: are the fields the hunts join on actually filled? (The Tokyo Nomad, 4 Nov 2026)
// Read-only; last hour. Not executed in a live tenant; checked with Microsoft's Kusto parser against Microsoft Learn schemas.
// An empty field is a coverage gap, not a clean result. A blank percentage means the table has no such field.
// Tables you do not collect are skipped (isfuzzy); compare the output with the log checklist.
let Window = 1h;
union isfuzzy=true
    (SigninLogs | where TimeGenerated > ago(Window)
        | summarize Rows=count(), ClientIP=countif(isnotempty(IPAddress)), UserAgent=countif(isnotempty(UserAgent)), Principal=countif(isnotempty(UserPrincipalName))
        | extend Source="SigninLogs"),
    (AADServicePrincipalSignInLogs | where TimeGenerated > ago(Window)
        | summarize Rows=count(), ClientIP=countif(isnotempty(IPAddress)), Principal=countif(isnotempty(ServicePrincipalId))
        | extend Source="AADServicePrincipalSignInLogs", UserAgent=long(null)),
    (MicrosoftGraphActivityLogs | where TimeGenerated > ago(Window)
        | summarize Rows=count(), ClientIP=countif(isnotempty(IPAddress)), UserAgent=countif(isnotempty(UserAgent)), Principal=countif(isnotempty(AppId))
        | extend Source="MicrosoftGraphActivityLogs"),
    (W3CIISLog | where TimeGenerated > ago(Window)
        | summarize Rows=count(), ClientIP=countif(isnotempty(cIP)), UserAgent=countif(isnotempty(csUserAgent)), Principal=countif(isnotempty(csUserName) and csUserName != "-")
        | extend Source="W3CIISLog"),
    (AppServiceHTTPLogs | where TimeGenerated > ago(Window)
        | summarize Rows=count(), ClientIP=countif(isnotempty(CIp)), UserAgent=countif(isnotempty(UserAgent)), Principal=countif(isnotempty(CsUsername) and CsUsername != "-")
        | extend Source="AppServiceHTTPLogs"),
    (AGWAccessLogs | where TimeGenerated > ago(Window)
        | summarize Rows=count(), ClientIP=countif(isnotempty(ClientIp)), UserAgent=countif(isnotempty(UserAgent))
        | extend Source="AGWAccessLogs", Principal=long(null)),
    (CommonSecurityLog | where TimeGenerated > ago(Window)
        | summarize Rows=count(), ClientIP=countif(isnotempty(SourceIP)), UserAgent=countif(isnotempty(RequestClientApplication)), Principal=countif(isnotempty(SourceUserName))
        | extend Source="CommonSecurityLog")
| where Rows > 0
| extend ClientIPPct=round(100.0 * ClientIP / Rows, 1), UserAgentPct=round(100.0 * UserAgent / Rows, 1), PrincipalPct=round(100.0 * Principal / Rows, 1)
| project Source, Rows, ClientIPPct, UserAgentPct, PrincipalPct
| order by Source asc

S02をダウンロード

Falconのログが届いているか確かめる:C01とC02

C01は、ハントに必要なEDRのイベントの種類を、OSごと・センサーごとに数えます。センサーの数をサーバーの資産台帳と比べてください。NetworkListenIP4を送っていないサーバーについては、C04で答えが出ません。

// Falcon EDR readiness: are the event types the ARTEX hunts need arriving? (The Tokyo Nomad, 4 Nov 2026)
// Read-only. REQUIRED time picker: last 24 hours. Static review only; not executed in a live Falcon tenant.
// Compare Sensors with your server inventory per platform. A missing event type is a coverage gap, not a clean result.
// Check each event name against Falcon's Events Data Dictionary for your sensor versions and platforms.
#event_simpleName=/^(ProcessRollup2|NetworkConnectIP4|NetworkListenIP4|DnsRequest|SensorHeartbeat)$/
| groupBy([#event_simpleName, event_platform], function=[count(as=Events), count(aid, distinct=true, as=Sensors), max(@timestamp, as=LastSeen)], limit=100)
| LastSeenUTC := formatTime("%Y-%m-%d %H:%M", field=LastSeen, timezone="UTC")
| select([#event_simpleName, event_platform, Events, Sensors, LastSeenUTC])
| sort(#event_simpleName, order=asc, limit=100)

C01をダウンロード

C02は、Next-Gen SIEMの外部データについて同じことを確かめます。パーサー名は、自社のID、Web、ファイアウォールのものに置き換えます。

// Falcon Next-Gen SIEM readiness: do third-party sources arrive with the fields the hunts need? (The Tokyo Nomad, 4 Nov 2026)
// Read-only. REQUIRED time picker: last 1 hour. Static review only; not executed in a live Falcon tenant.
// Replace the parser names with yours (identity, web/WAF, firewall/proxy). Fields follow the parsers' ECS-style names.
// A parser with no rows, or with few rows carrying a user agent or source IP, is a coverage gap.
in(field="#type", values=["REPLACE_WITH_IDENTITY_PARSER", "REPLACE_WITH_WEB_PARSER", "REPLACE_WITH_FIREWALL_PARSER"])
| case { user_agent.original=* | HasUA := 1 ; * | HasUA := 0 ; }
| case { source.ip=* | HasIP := 1 ; * | HasIP := 0 ; }
| case { user.name=* | HasUser := 1 ; * | HasUser := 0 ; }
| groupBy([#type], function=[count(as=Events), sum(HasUA, as=WithUserAgent), sum(HasIP, as=WithSourceIP), sum(HasUser, as=WithUser), max(@timestamp, as=LastSeen)], limit=100)
| LastSeenUTC := formatTime("%Y-%m-%d %H:%M", field=LastSeen, timezone="UTC")
| select([#type, Events, WithUserAgent, WithSourceIP, WithUser, LastSeenUTC])

C02をダウンロード

ツール自身の痕跡を探す

S03とC03:ARTEXの固定のユーザーエージェント

Webログの中から2つのユーザーエージェントを探します。承認済みの自社テストも一致する前提で見てください。受理された変更がない一致は後述の通報A02、受理された書き込みと並ぶ一致はA03にあたります。

// ARTEX's fixed user agents in web logs (The Tokyo Nomad, 4 Nov 2026). Read-only; last 24 hours.
// Not executed in a live tenant; checked with Microsoft's Kusto parser against Microsoft Learn schemas.
// Strings from the public artex-global source at v0.3.15-global.1 (d6964594) and its norma v0.4.3 SDK:
//   norma/0.4 = agent WebFetch; artex-enrich/1.0 = asset title probe; artex-selfupdate = updater (GitHub only).
// A match is a lead: a fork can change these strings, and your own approved testing will match too.
// Most ARTEX tools are curl, python or scanners with their own user agents; absence proves nothing.
let Window = 24h;
let ArtexAgents = @"(?i)^(?:norma/0\.|artex-enrich/|artex-selfupdate)";
union isfuzzy=true
    (W3CIISLog | where TimeGenerated > ago(Window)
        | project TimeGenerated, Source="W3CIISLog", ClientIP=cIP, Host=csHost, Path=csUriStem, Status=tostring(scStatus), UserAgent=csUserAgent),
    (AppServiceHTTPLogs | where TimeGenerated > ago(Window)
        | project TimeGenerated, Source="AppServiceHTTPLogs", ClientIP=CIp, Host=CsHost, Path=CsUriStem, Status=tostring(ScStatus), UserAgent),
    (AGWAccessLogs | where TimeGenerated > ago(Window)
        | project TimeGenerated, Source="AGWAccessLogs", ClientIP=ClientIp, Host, Path=RequestUri, Status=tostring(HttpStatus), UserAgent),
    (CommonSecurityLog | where TimeGenerated > ago(Window)
        | project TimeGenerated, Source="CommonSecurityLog", ClientIP=SourceIP, Host=DestinationHostName, Path=RequestURL, Status="", UserAgent=RequestClientApplication)
| where UserAgent matches regex ArtexAgents
| summarize FirstSeen=min(TimeGenerated), LastSeen=max(TimeGenerated), Requests=count(),
    Statuses=make_set(Status, 10), Paths=make_set(Path, 10) by Source, ClientIP, Host, UserAgent
| order by LastSeen desc
| take 100

S03をダウンロード

// ARTEX's fixed user agents in web logs, Falcon Next-Gen SIEM (The Tokyo Nomad, 4 Nov 2026).
// Read-only. REQUIRED time picker: last 24 hours. Static review only; not executed in a live Falcon tenant.
// norma/0.4 = agent WebFetch; artex-enrich/1.0 = asset title probe; artex-selfupdate = updater (GitHub only).
// A match is a lead: forks can change these strings and approved testing will match. Absence proves nothing.
#type="REPLACE_WITH_WEB_PARSER"
| user_agent.original=/^(?:norma\/0\.|artex-enrich\/|artex-selfupdate)/i
| groupBy([source.ip, url.domain, user_agent.original], function=[count(as=Requests), min(@timestamp, as=FirstSeen), max(@timestamp, as=LastSeen), collect([url.path, http.response.status_code], limit=10)], limit=100)
| head(limit=100)

C03をダウンロード

S04とC04:ARTEXを動かしている社内のホスト

既定の実行ファイル名が子プロセスを起動していないか、8787番か8788番で待ち受けていないか、ARTEXのプロセスがクラウドのメタデータのアドレスやGitHubのAPIに通信していないかを探します。待ち受けの確認には、ホストからMicrosoft DefenderのListeningConnectionCreatedやFalconのNetworkListenIP4のイベントが届いている必要があります。届いているかどうかはS01とC01で分かります。

// Hosts in your estate that look like they run ARTEX (The Tokyo Nomad, 4 Nov 2026). Read-only; last 24 hours.
// Not executed in a live tenant; checked with Microsoft's Kusto parser against Microsoft Learn schemas.
// Defaults from the public artex-global source at v0.3.15-global.1: binary "artex", UI on 8787, recording proxy on 8788,
// tools started with "bash -c" (a simple command replaces bash, so its parent is artex), updater calls api.github.com.
// Leads, not verdicts: a renamed binary or new -addr/-proxy flags defeats these checks, and an approved
// red-team host will match. Confirm the owner and authorisation before containment.
let Window = 24h;
let ArtexNames = dynamic(["artex", "artex.exe"]);
let Listeners = DeviceNetworkEvents
    | where TimeGenerated > ago(Window)
    | where ActionType == "ListeningConnectionCreated" and LocalPort in (8787, 8788)
    | project TimeGenerated, DeviceId, DeviceName, Evidence=strcat("listening on ", LocalIP, ":", tostring(LocalPort), " by ", InitiatingProcessFileName), Detail=InitiatingProcessCommandLine;
let Children = DeviceProcessEvents
    | where TimeGenerated > ago(Window)
    | where InitiatingProcessFileName in~ (ArtexNames)
    | project TimeGenerated, DeviceId, DeviceName, Evidence=strcat("artex started ", FileName), Detail=ProcessCommandLine;
let Outbound = DeviceNetworkEvents
    | where TimeGenerated > ago(Window)
    | where InitiatingProcessFileName in~ (ArtexNames) or InitiatingProcessParentFileName in~ (ArtexNames)
    | where RemoteIP == "169.254.169.254" or RemoteUrl has "api.github.com"
    | project TimeGenerated, DeviceId, DeviceName, Evidence=strcat(InitiatingProcessFileName, " connected to ", iff(isempty(RemoteUrl), RemoteIP, RemoteUrl)), Detail=InitiatingProcessCommandLine;
union Listeners, Children, Outbound
| summarize FirstSeen=min(TimeGenerated), LastSeen=max(TimeGenerated), Events=count(),
    Evidence=make_set(Evidence, 20), Details=make_set(Detail, 5) by DeviceId, DeviceName
| order by LastSeen desc
| take 100

S04をダウンロード

// Hosts that look like they run ARTEX, Falcon EDR (The Tokyo Nomad, 4 Nov 2026).
// Read-only. REQUIRED time picker: last 24 hours. Static review only; not executed in a live Falcon tenant.
// Defaults from the public artex-global source at v0.3.15-global.1: binary "artex", UI on 8787, recording proxy on 8788.
// Leads, not verdicts: renamed binaries and changed ports defeat this, and approved red-team hosts match.
// Pivot a listener's ContextProcessId to ProcessRollup2 TargetProcessId to name the listening process.
#event_simpleName=/^(ProcessRollup2|NetworkListenIP4)$/
| case {
    #event_simpleName=ProcessRollup2 ParentBaseFileName=/^artex(\.exe)?$/i | Evidence := format("artex started %s", field=[FileName]) ;
    #event_simpleName=NetworkListenIP4 LocalPort=/^878[78]$/ | Evidence := format("listening on port %s", field=[LocalPort]) ;
  }
| groupBy([aid, ComputerName, Evidence], function=[count(as=Events), min(@timestamp, as=FirstSeen), max(@timestamp, as=LastSeen), collect([CommandLine], limit=5)], limit=100)
| head(limit=100)

C04をダウンロード

攻撃そのものについては、ハーネス・フレームワークの14のハントを使います。探索の後の受理された書き込み(S1)、探索用ファイル(S2)、パスワードスプレー(S3)、アプリのIDによるディレクトリの変更(S4)、均一な外向き通信(S5)と、それぞれに対応するFalconのハントです。

誰に知らせ、いつ外部の支援を呼ぶか

8つのきっかけと10のチームの対応表。期限と外部の支援も示す。A01、ログが届いていない・古い、または重要な項目がほぼ空:まずSIEM担当、次にSOC。当日中、インシデント対応中は1時間以内。A02、ARTEXのユーザーエージェントや公表済みのプロキシIPがWeb入口に届いたが受理された変更はない:まずSOC、次にAppSec。1時間以内。ISACと指標を共有。A03、書き込み後の読み戻し、Webシェル、Webワーカーからのシェル起動など受理された変更:インシデント責任者、EDR・DFIR、AppSecを15分以内に呼び出す。必要ならIR契約先、調整はJPCERT/CC。A04、スプレー対象のアカウントでのログイン成功や、アプリへの資格情報・ロール・同意の付与などIDの変化:ID管理とインシデント責任者に15分以内、次にクラウド担当。Microsoftと当該アプリのベンダー。A05、個人データが外部に出たおそれ:法務・個人情報担当とインシデント責任者に直ちに。個人情報保護委員会と警察に報告。A06、社内のホストでARTEXが動いている:EDRとIT運用に1時間以内、次に基盤担当。未承認なら侵害として扱う。A07、承認済みのARTEXに他のホストから到達できる:基盤担当に当日中。A08、自社のARTEXが更新を確認している、または鍵を環境変数に持っている:基盤担当に当日中、次にID管理。注記には国内の連絡先として、JPCERT/CC、警察のサイバー犯罪相談窓口、個人データの漏えいについては速報を概ね3〜5日以内とする個人情報保護委員会を挙げる。

図6:オリジナル図版。「基盤担当」は、社内でARTEXなどのツールを運用する担当(多くはレッドチーム)です。

ID きっかけ 見える場所 最初に知らせる相手 期限 次に巻き込む相手 外部の支援
A01 必要なログが届いていない・古い、または重要な項目がほぼ空 S01, S02, C01, C02 SIEM担当とログ元の担当 当日中。インシデント対応中は1時間以内 それに依存する検知・ハントがあればSOCリーダー なし
A02 ARTEXのユーザーエージェントや公表済みのARTEXプロキシIPがWeb入口に届いたが、受理された変更はまだない S03, C03 SOC 1時間以内 AppSec・Web担当 ISACや同業者と指標を共有
A03 受理された変更:書き込み後の読み戻し、新しいWebシェル、Webワーカーからのシェル起動 Harness S1, S2, C1, C2 インシデント責任者(呼び出し)、EDR・DFIR、AppSec 15分以内 影響を受けたデータの担当 DFIRの体制がなければIR契約先。調整はJPCERT/CC
A04 ID:スプレー対象のアカウントでログイン成功、アプリへの資格情報・ロール追加、同意の付与 Harness S3, S4; Microsoft alert titles ID管理とインシデント責任者 15分以内 クラウド基盤。取引先のアプリが関わればサードパーティリスク担当 Microsoftのサポート・IR、当該アプリのベンダー
A05 個人データや顧客データが外部に出たおそれ Harness S5; DLP; application logs 法務・個人情報担当とインシデント責任者 直ちに 担当役員と広報 個人情報保護委員会への報告(速報は概ね3〜5日以内)、警察
A06 社内のホストでARTEXが動いている:8787/8788番の待ち受け、artexの実行、メタデータへの通信 S04, C04 EDR担当とIT運用 1時間以内 承認の有無をレッドチームの責任者に確認。未承認なら内部不正の手順へ 未承認なら侵害として扱う
A07 承認済みのARTEXに他のホストから到達できる、またはプロキシが全インターフェースで待ち受けている S04, C04; your own exposure inventory 基盤担当とネットワーク担当 当日中 セキュリティアーキテクチャ担当 なし
A08 自社のARTEXが更新を確認している、またはモデル・DBの鍵を環境変数に持っている S04; configuration review 基盤担当 当日中 鍵を交換するためID管理 なし

通報先の一覧をダウンロード(CSV)。担当者名、連絡先、最後に連絡経路を試した日を書き込む空欄があります。期限は当番体制のあるチームを想定した出発点です。担当ごとに合意して決めてください。最初の連絡の後の手順は対応プレイブックにあります。

国内で外部の支援を呼ぶとき:

  • JPCERT/CCはインシデントの報告を受け付け、関係する組織との調整を支援します(報告フォーム)。最近の国内の侵入については、10月8日の注意喚起があります。
  • 警察はサイバー犯罪相談窓口で被害の相談を受け付けます。
  • 個人情報保護委員会には、対象となる個人データの漏えいを報告する必要があります。速報は事態を知ってから概ね3〜5日以内、確報は30日以内(不正の目的による攻撃などが原因の場合は60日以内)です(個人情報保護委員会)。
  • ベンダーと契約先: MicrosoftとCrowdStrikeのインシデント対応サービス、IRの契約先やサイバー保険会社、そしてConnectWiseのように関係する外部アプリがあればそのベンダーです。

攻撃者のプレイブック

運用者がARTEXをどう使うかを、MITRE ATT&CKの手法とともに示します。出典自身の分類ではなく、私たちが最も近いものを選んだ場合はそう書いています。

  1. 能力を手に入れる:公開されたフォークとモデルのAPI(T1588.007、Obtain Capabilities: Artificial Intelligence。CrowdStrikeの分類)。CrowdStrikeは、APIの再販業者経由でDeepSeekが使われたことを確認しています。
  2. 基盤を用意する:仮想専用サーバーと外向きプロキシの連鎖(T1583.003、Virtual Private Server。T1090、Proxy)。記録用プロキシの外向き設定を使えば、1行の設定で済みます。
  3. 対象を見つけて探る:タイトル取得とWebFetchが、エージェントが見つけたWeb資産をすべて読みます(T1595.002、Vulnerability Scanning。私たちが選んだ最も近いもの)。
  4. 侵入する:Zscalerの報告のように周辺のWebアプリを悪用してWebシェルを置く(T1190、Exploit Public-Facing Application。T1505.003、Web Shell)。あるいはMicrosoftの報告のように、パスワードスプレーとOAuthアプリの悪用を行う(T1110.003、Password Spraying。T1078.004、Cloud Accounts)。
  5. 居座る:アプリに新しい資格情報や高いロールを与える(T1098.001、Additional Cloud Credentials。T1098.003、Additional Cloud Roles)。Microsoftのアラート名がこれを示しています。
  6. データを持ち出す:Zscalerは、構造化された個人データがHTTPSで外部に送られたことを確認しています(T1041、Exfiltration Over C2 Channel。私たちが選んだ最も近いもの)。

他人のARTEXを悪用する。 Dockerイメージから起動して8788番を公開したインスタンスは、認証のないプロキシです。見つけた人は誰でも、その背後にあるネットワークやクラウドのメタデータサービスに届きます(T1090、Proxy。T1552.005、Cloud Instance Metadata API)。

フォークが何であっても変わらないこと: エージェントはモデルと攻撃対象の両方へのネットワーク経路を必要とし、その影響はID、Web、エンドポイントのログに残ります。実行ファイルの名前やユーザーエージェントを変えても、受理された書き込み、サインイン、新しいファイルは見つけられます。ログがあれば、です。

防御側のプレイブック

  1. 今日:ログを確かめる。 S01とS02、またはC01とC02を実行します。MISSINGやSTALEのログ、埋まっている割合がおよそ90%を下回る項目は、期限を付けて担当に渡します(通報A01)。
  2. 今週:Microsoftの手順が必要とするログを有効にする。 サービスプリンシパルとマネージドIDのサインイン、Microsoft Graphのアクティビティ(P1かP2が必要)、Entraの監査、すべてのKey Vaultの診断設定、そしてユーザーエージェントとログインユーザーを残すWebログです。これらのテーブルはSentinelで90日保持にします。
  3. 毎日:痕跡を探す。 S03とS04、またはC03とC04と、ハーネス・フレームワークの影響を探す14のハントを実行します。承認済みのテストが一致することを前提に記録しておけば、本物が目立ちます。
  4. 自社でARTEXを使うなら、外側から封じ込める:
    • テスト対象以外には本番環境への経路がない、使い捨てのVMで動かします。範囲の制限は外向き通信の許可リストで行い、範囲外なら169.254.169.254と内部のアドレス帯を遮断します。
    • 8787番と8788番はループバックで待ち受け、8788番は決して公開せず、画面は社内のシングルサインオンの背後に置きます。
    • 専用のデータベースと、利用上限を設定したモデルの鍵を使い、案件ごとに両方を交換し、それ以外の秘密情報は環境変数に置きません。
    • カスタムツールとMCPツールは確認するまで未承認として扱い、コマンドのテンプレートでは引数の前に--を置きます。
    • ホストからGitHubへの通信を遮断し、更新は確認したソースから作り直す方法だけにします。
    • ガードの監査ログ、インターセプトの履歴、記録用プロキシの通信索引を案件の記録とともに保存します。テストが何をしたかの証拠になります。
  5. 今:通報先の一覧に合意する。 A01からA08までの担当者と連絡先を決め、必要になる前に一度ずつ連絡経路を試します。

まだ分かっていないこと

  • 非公開の上流版とフォークの違い。 CrowdStrikeとMicrosoftは実際に使われたツールを見ていますが、私たちが確認したのは公開フォークです。
  • 攻撃者がユーザーエージェントを変えたかどうか。 norma/0.4やartex-enrich/1.0を挙げたベンダーの報告はなく、ログに見当たらなくても何も証明しません。
  • CrowdStrikeが公表したプロキシIPの設定方法。 ARTEXの外向き設定によるものか、その外側の仕組みかは分かっていません。
  • CTI Academyの所見が修正されるかどうか。 10月14日のフォークの最新コミットも同じコードです。

出典と更新履歴

2026年11月4日(日本時間)公開。 Microsoft、CrowdStrike、Zscalerの報告は11月4日に直接読みました。CTI Academyのページはスクリプトで描画されるため、私たちの取得ツールにはCookieの案内しか返らず、サイト運営者が提供した写しを使い、アドレスと題名は検索結果の一覧で確かめました。JPCERT/CCの注意喚起は、以前の確認の記録から引用しています。ARTEXのコードは以下のコミットで読み、実行はしていません。クエリは実環境で実行しておらず、KQLの4本はMicrosoft Learnのテーブル定義を使ってMicrosoftのKusto構文解析器を通過し、CQLの4本は目視で確認しました。

記事一覧へ →Read in English ↗