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人で使う製品です。
図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は動作環境で、私たちはソースコードで、別々に調べました。両者が一致する点を「確認」と書いています。
コードから分かること
図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が残す痕跡
図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に届きません。
図4:オリジナル図版。黄色は誰かが有効にするまでオフのもの。赤はオフでライセンスにも依存し、エクスポートしなければ失われるものです。
保存期間も、収集と同じくらい重要です。Entraはサインインと監査のログを、無料版で7日、P1かP2で30日保持します。Graphのアクティビティログは、どこかに送らなければまったく残りません。Defender XDRのハンティング用データは30日で、Sentinelは分析層の90日分の保持を料金に含めています。
図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
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
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)
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])
ツール自身の痕跡を探す
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
// 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)
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
// 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)
攻撃そのものについては、ハーネス・フレームワークの14のハントを使います。探索の後の受理された書き込み(S1)、探索用ファイル(S2)、パスワードスプレー(S3)、アプリのIDによるディレクトリの変更(S4)、均一な外向き通信(S5)と、それぞれに対応するFalconのハントです。
誰に知らせ、いつ外部の支援を呼ぶか
図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の手法とともに示します。出典自身の分類ではなく、私たちが最も近いものを選んだ場合はそう書いています。
- 能力を手に入れる:公開されたフォークとモデルのAPI(T1588.007、Obtain Capabilities: Artificial Intelligence。CrowdStrikeの分類)。CrowdStrikeは、APIの再販業者経由でDeepSeekが使われたことを確認しています。
- 基盤を用意する:仮想専用サーバーと外向きプロキシの連鎖(T1583.003、Virtual Private Server。T1090、Proxy)。記録用プロキシの外向き設定を使えば、1行の設定で済みます。
- 対象を見つけて探る:タイトル取得とWebFetchが、エージェントが見つけたWeb資産をすべて読みます(T1595.002、Vulnerability Scanning。私たちが選んだ最も近いもの)。
- 侵入する:Zscalerの報告のように周辺のWebアプリを悪用してWebシェルを置く(T1190、Exploit Public-Facing Application。T1505.003、Web Shell)。あるいはMicrosoftの報告のように、パスワードスプレーとOAuthアプリの悪用を行う(T1110.003、Password Spraying。T1078.004、Cloud Accounts)。
- 居座る:アプリに新しい資格情報や高いロールを与える(T1098.001、Additional Cloud Credentials。T1098.003、Additional Cloud Roles)。Microsoftのアラート名がこれを示しています。
- データを持ち出す:Zscalerは、構造化された個人データがHTTPSで外部に送られたことを確認しています(T1041、Exfiltration Over C2 Channel。私たちが選んだ最も近いもの)。
他人のARTEXを悪用する。 Dockerイメージから起動して8788番を公開したインスタンスは、認証のないプロキシです。見つけた人は誰でも、その背後にあるネットワークやクラウドのメタデータサービスに届きます(T1090、Proxy。T1552.005、Cloud Instance Metadata API)。
フォークが何であっても変わらないこと: エージェントはモデルと攻撃対象の両方へのネットワーク経路を必要とし、その影響はID、Web、エンドポイントのログに残ります。実行ファイルの名前やユーザーエージェントを変えても、受理された書き込み、サインイン、新しいファイルは見つけられます。ログがあれば、です。
防御側のプレイブック
- 今日:ログを確かめる。 S01とS02、またはC01とC02を実行します。MISSINGやSTALEのログ、埋まっている割合がおよそ90%を下回る項目は、期限を付けて担当に渡します(通報A01)。
- 今週:Microsoftの手順が必要とするログを有効にする。 サービスプリンシパルとマネージドIDのサインイン、Microsoft Graphのアクティビティ(P1かP2が必要)、Entraの監査、すべてのKey Vaultの診断設定、そしてユーザーエージェントとログインユーザーを残すWebログです。これらのテーブルはSentinelで90日保持にします。
- 毎日:痕跡を探す。 S03とS04、またはC03とC04と、ハーネス・フレームワークの影響を探す14のハントを実行します。承認済みのテストが一致することを前提に記録しておけば、本物が目立ちます。
- 自社でARTEXを使うなら、外側から封じ込める:
- テスト対象以外には本番環境への経路がない、使い捨てのVMで動かします。範囲の制限は外向き通信の許可リストで行い、範囲外なら
169.254.169.254と内部のアドレス帯を遮断します。 - 8787番と8788番はループバックで待ち受け、8788番は決して公開せず、画面は社内のシングルサインオンの背後に置きます。
- 専用のデータベースと、利用上限を設定したモデルの鍵を使い、案件ごとに両方を交換し、それ以外の秘密情報は環境変数に置きません。
- カスタムツールとMCPツールは確認するまで未承認として扱い、コマンドのテンプレートでは引数の前に
--を置きます。 - ホストからGitHubへの通信を遮断し、更新は確認したソースから作り直す方法だけにします。
- ガードの監査ログ、インターセプトの履歴、記録用プロキシの通信索引を案件の記録とともに保存します。テストが何をしたかの証拠になります。
- テスト対象以外には本番環境への経路がない、使い捨てのVMで動かします。範囲の制限は外向き通信の許可リストで行い、範囲外なら
- 今:通報先の一覧に合意する。 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本は目視で確認しました。
- ARTEXのコード:artex-global v0.3.15-global.1(コミットd6964594) ・ norma v0.4.3
- 監査:CTI Academy:ARTEX AI pentest framework, what our security audit found(英語)
- ベンダー:CrowdStrike(2026年10月7日、英語) ・ Zscaler ThreatLabz(2026年10月14日、英語) ・ Microsoft(2026年10月21日、26日更新、英語)
- 国内:JPCERT/CC JPCERT-AT-2026-0030(2026年10月8日、23日更新) ・ JPCERT/CCのインシデント報告フォーム ・ 警察庁 サイバー犯罪相談窓口 ・ 個人情報保護委員会 漏えい等の対応
- Microsoft Learn(英語):Entraのデータ保持 ・ Microsoft Graphアクティビティログ ・ Sentinelのデータ層と保持 ・ テーブルリファレンス:AGWAccessLogs、AppServiceHTTPLogs、W3CIISLog、CommonSecurityLog、DeviceNetworkEvents、MicrosoftGraphActivityLogs、AADServicePrincipalSignInLogs
- 当サイト:AIハーネスのハンティング・フレームワーク ・ ARTEX対応プレイブック ・ 日本を狙ったAI攻撃基盤







