← All articles

ARTEX from the inside: what its guardrails miss, the traces it leaves, and the logs you need to hunt it

We checked a lab audit of the ARTEX attack tool against its source code, found the traces it leaves, and built a Sentinel and Falcon log checklist, readiness queries and an alert matrix.

Incident period: Vendor reports 7 Oct – early Nov 2026; code reviewed 4 NovSources checked: 4 Nov 2026

30-second briefing

Key facts
A published lab audit found ARTEX's scope and approval controls enforced in the wrong place. We read the same release's code, confirm all six findings and add three details, including fixed user agents you can search for. Then we turn it into a 16-source log checklist, eight Sentinel and Falcon queries and an alert matrix.
Who should act
SIEM · EDR · IT Operations · Legal / Privacy · Third-party Risk
First action
Run S01 and S02 in Sentinel, or C01 and C02 in Falcon, today. Send every MISSING or STALE source, and every empty user-agent field, to its owner with a date. Then export Graph activity and service-principal sign-ins: Microsoft's guidance needs them and Entra does not keep them.
Uncertainty to keep in mind
We read ARTEX's code but did not run it; our sandbox blocks running third-party code. Runtime behaviour comes from CTI Academy's lab. Private builds and forks can differ. No query ran in a live tenant: the KQL passed Microsoft's parser, and the CQL had static review only.
Header graphic: ARTEX, from the inside. Four panels: 8 tools inside the approval gate's default scope; 4 domain suffixes in the built-in blocklist, with no .go.jp; 0 signatures checked by the self-updater; 30 days that Entra keeps sign-ins on P1/P2 unless exported.
THE POSTURE QUESTION

ARTEX's two headline safety controls sit in the wrong place. Its scope blocklist is checked when the agent records an asset, not when a tool connects, and its approval gate covers eight named tools while custom and MCP tools skip it. Nothing in the tool keeps an operator, or an attacker, in scope. The useful part for defenders is what the code gives away: fixed user agents, default ports and a recognisable process tree. None of that helps if the logs are missing, and most of the sources Microsoft's Storm-0940 guidance relies on are off by default. Prove the logs arrive, hunt the traces, and agree in advance who is alerted and when to call for help.

ARTEX's own safety controls will not keep anyone in scope, and most of the logs you would need to see it are off by default. In October 2026, three vendors tied real intrusions to ARTEX, an open-source framework in which AI agents plan and run a penetration test. CrowdStrike and Zscaler linked it to attacks on South Korean financial firms, and Microsoft to Storm-0940's campaign against cloud tenants. CTI Academy then asked a narrower question: how safe is the tool itself? Its lab audit found the scope and approval controls enforced far more narrowly than their names suggest. We read the same release's source code to check that audit independently, looked for the traces the tool leaves, and turned both into what a SOC needs: a log checklist for Microsoft Sentinel and CrowdStrike Falcon, eight readiness and hunting queries, and a matrix of who to alert. This article sits beside our AI harness hunting framework, which has fourteen hunts for the attacks themselves, and our ARTEX response playbook.

What happened, and what is confirmed

Status as of 4 November 2026.

Report Date What it says Status
CrowdStrike 7 Oct An unknown actor used ARTEX against South Korean financial firms from late September. One ARTEX host had an open directory holding Claude Code session histories and ARTEX configuration files, and the actor used nine proxy IPs. Vendor intelligence; actor not named
JPCERT/CC 8 Oct, updated 23 Oct Unauthorised access to organisations in Japan, split into API, JSP web-shell and AI-framework cases. Alert; not every case is tied to ARTEX
Zscaler ThreatLabz 14 Oct A financially motivated, Chinese-speaking actor (moderate confidence) exploited peripheral web apps and took personal data on about 67,000 people. ARTEX's default blocklist covered government and education domains, not finance. Vendor assessment
Microsoft 21 Oct, updated 26 Oct Storm-0940 used ARTEX for scanning, password spraying and OAuth app abuse after the ConnectWise compromise, and in several cases reached privileged cloud access in under three hours. A spray against 327 accounts at eight organisations produced no successful sign-in. Microsoft's attribution
CTI Academy Early Nov A lab audit of the public artex-global fork at v0.3.15-global.1: three high and three medium findings about insecure defaults and where controls sit. Lab research
This review 4 Nov We read the same tag's code and its agent SDK. All six findings match the code, and we add three details. Code review; ARTEX not run

"Attribution" is each vendor's assessment, not a court finding. A tool that many people can download says little about who used it in a given attack.

Why the tool's own controls matter to defenders

  • If your red team runs ARTEX or a similar harness, the tool's scope controls are your containment. If they sit in the wrong place, a test can reach production, a cloud metadata service or someone else's network.
  • If an attacker runs it, its defaults become fingerprints you can search for, until a fork changes them.
  • If anyone exposes it, its recording proxy is a free pivot for whoever finds it.

How ARTEX is built, and where its controls sit

ARTEX Global Edition is a single Go binary with an embedded web interface and a PostgreSQL database. A planner agent breaks a goal into "intents", worker agents run real tools, and the results build a graph of assets and findings. A recording proxy captures every request the tools make. It is a single-user product with one admin account.

Diagram in three lanes. Planning: the planner agent breaks the goal into intents; a graph write through insert_assets or add_intent is where the asset blocklist is checked, and add_intent checks it only when the intent names asset IDs; the built-in blocklist holds .gov, .gov.cn, .edu and .edu.cn, and not .go.jp, .lg.jp, .ac.jp, .go.kr or finance. Tool calls: the eight built-in tools, Bash, WebFetch, web_search, shell_open, shell_send, Write, Edit and MultiEdit, pass through the approval gate, which runs only for names on its scope list and matches the tool name or the raw input JSON rather than the rendered command; custom command, script and HTTP tools and MCP tools named mcp__server__tool skip the gate; everything ends in bash -c on the ARTEX host, which inherits the full environment including database and model API keys. Network: the recording proxy on port 8788, go-mitmproxy with no proxy password and no certificate checks, bound to 127.0.0.1 by the binary and to all interfaces by the Docker image, forwards through an optional egress proxy to the target, and the asset blocklist is not consulted on this path; if the proxy fails, WebFetch retries directly.

Figure 1. Original graphic from our reading of the v0.3.15-global.1 source. Both controls that matter are checked against graph writes and tool names, not where the request leaves.

How we checked

  • Rawest material. We cloned the public artex-global repository and checked out the tag CTI Academy audited, v0.3.15-global.1 (commit d6964594, 14 October 2026). We also read the agent SDK it is built on, norma v0.4.3, from the Go module proxy. The fork's newest commit, fdfb7a1 from 14 October, has the same code at every line we cite.
  • Method. For each control we traced where it is called, not only where it is defined: every caller of the asset gate, the interceptor's scope list and the paths around it, the proxy's options, the updater's integrity check, and every hard-coded user agent. File and line references below are to the tag.
  • What we did not do. We did not run ARTEX. We wrote a small test that would call ARTEX's own gate functions with no agent and no network, as CTI Academy did, but our research sandbox blocks running third-party code, so it never ran. Runtime behaviour in this article comes from CTI Academy's lab, and the code paths from our own reading. The upstream builds are private, and forks can differ.
  • Independent check. Two separate examinations of the same release: CTI Academy's in a running lab, ours in the source. Where they agree, we write "confirmed".

What the code shows

Scorecard of seven findings, each with CTI Academy's lab result, what the code shows, and what to do. 1, scope blocklist, high, confirmed with more detail: a blocked host was still reached by a custom tool; the code checks the rules only at graph writes, add_intent skips them without asset IDs, and the built-ins cover .gov and .edu only; enforce scope at the network edge. 2, approval gate, high, confirmed: the same DELETE was blocked through Bash and allowed through a custom tool; the scope is eight tool names, rules see the name or input JSON, and custom command tools call the Bash core; gate on capability. 3, open recording proxy, high where deployed with Docker defaults, confirmed: an off-host client proxied through with no credentials; the binary binds 127.0.0.1:8788, the Docker CMD :8788, compose publishes on 127.0.0.1, with no proxy authentication and TLS checks off; never publish 8788 and hunt for it. 4, argument injection, medium, confirmed: values starting with a dash reach the binary as options because single quotes stop shell tricks but not option injection; add a double dash in templates. 5, host secrets, medium, confirmed with more detail: bash -c and Python tools inherit the whole environment including ARTEX_PG_DSN, OPENAI_API_KEY and ANTHROPIC_API_KEY; run in a disposable VM. 6, unsigned self-update, medium, confirmed: hard-coded to Autumn-27/artex, now private, trusting SHA256SUMS from the same release; block the update path. 7, fixed user agents, new: WebFetch sends norma/0.4, the title probe artex-enrich/1.0, and WebFetch retries directly if the proxy fails; search web logs for both strings. A dashed box notes what held up: unauthenticated API routes returned 401, JWT forgery failed, logins are throttled, and the code warns when the UI is exposed before a password is set.

Figure 2. Original graphic. "Confirmed" means the code path matches what CTI Academy saw running.

1. The scope blocklist is checked when the agent records an asset, not when a tool connects

ARTEX's "Asset blocking" rules are evaluated by db.EvaluateAssetGate. In the agent, that function has two callers: insert_assets (agent/tools_insert.go:244) and add_intent (agent/tools.go:1053). Both write to the knowledge graph. No execution tool checks the rules before it opens a connection, which is why CTI Academy's custom tool reached a blocked host.

Two details the audit does not mention. First, add_intent checks the rules only when the intent names asset IDs (len(anchors) > 0); an intent without them is never checked. Second, the rules ARTEX seeds on first start are four domain suffixes: .gov, .gov.cn, .edu and .edu.cn (db/db.go:302–305). Zscaler pointed out that they do not cover financial institutions. They do not cover Japanese or Korean public bodies either: .go.jp, .lg.jp, .ac.jp, .go.kr and .ac.kr are not on the list.

2. The approval gate covers eight tool names

The interception rules run only for tools on defaultEnabledTools: Bash, WebFetch, web_search, shell_open, shell_send, Write, Edit and MultiEdit (intercept/intercept.go:159–163). For any other name, the guard returns before it checks a single rule (guard/guard.go:87). Custom command tools render their template and call the Bash core directly (server/customtool.go:360), and MCP tools are named mcp__<server>__<tool> (mcphttp/client.go:35), so neither is in scope. Even inside the scope, a rule matches the tool name or the raw input JSON (intercept/intercept.go:550–552), not the command a template renders. All 19 built-in deny rules, from rm -rf to HTTP DELETE, match the input.

3. The recording proxy has no password, and the Docker image listens on every interface

The binary's defaults are 127.0.0.1:8787 for the interface and 127.0.0.1:8788 for the proxy (cmd/artex/main.go:55–57). The Docker image overrides both with :8787 and :8788 (Dockerfile:45), which means every interface inside the container. The shipped docker-compose.yml publishes them on 127.0.0.1 only (lines 46–47); a plain docker run -p 8788:8788 does not. The proxy is go-mitmproxy v1.9.2 with no proxy authentication and SslInsecure: true (traffic/traffic.go:206), so it does not check the certificates of the sites it reaches. It can also forward everything through a configured egress proxy (line 221), in which case targets see that proxy's address. CrowdStrike's nine proxy IPs fit that pattern, although its report does not say how they were set up.

4. Quoting stops shell tricks, not option injection

Custom-tool values are wrapped in single quotes (server/customtool.go:508–516). That stops ; or $() injection. A value that starts with - still reaches the target program as an option, as CTI Academy found.

5. Every agent shell inherits the host's secrets

The Bash tool runs bash -c with the full process environment plus the session's proxy settings (norma tool/task.go:140–145 and 566–570), and custom Python tools do the same (server/customtool.go:414). ARTEX reads its database connection string and model keys from that environment: ARTEX_PG_DSN, OPENAI_API_KEY and ANTHROPIC_API_KEY. CTI Academy showed that the shell can read /proc/self/environ. The code shows it does not need to: a plain env prints them. A prompt injection that gets the agent to run one command is enough.

6. Self-update trusts a checksum from the same release

The updater is hard-coded to Autumn-27/artex on GitHub (selfupdate/github.go:15–18). It allows redirects only to five GitHub hosts and checks the download against SHA256SUMS from the same release (selfupdate/stage.go:19 and 66). There is no signature. The upstream repository went private, so the check fails today. Whoever controls that repository in future controls every instance where someone clicks "Update now".

7. Fixed user agents, and a fetch that can bypass the proxy

This one is new. The agent's WebFetch tool sends User-Agent: norma/0.4 (norma tool/webfetch.go:188). ARTEX's enrichment engine, which reads the title of each web asset the agent discovers, sends artex-enrich/1.0 (enrich/enrich.go:233), and the updater sends artex-selfupdate. If a fetch through the proxy fails for a proxy-related reason, WebFetch retries once directly (tool/webfetch.go:190–198). That retry skips the recording proxy and any egress proxy behind it, so it may come from the operator's real address. These strings are easy to change, and most of ARTEX's traffic comes from ordinary tools such as curl, but they cost nothing to search for.

What held up

CTI Academy found no way into the admin API without credentials: protected routes returned 401, JWT forgery failed, and logins are throttled per client. The code also logs a warning at start-up if the interface is bound to a non-loopback address before an admin password is set (server/auth.go:394–408).

The traces ARTEX leaves

Two columns of cards. On a host running ARTEX: a process tree of artex, then bash -c, then curl, nmap or python3, where a simple command replaces bash so its parent is artex, and start.sh restarts artex when it exits; listening ports 127.0.0.1:8787 and 127.0.0.1:8788, or every interface inside the Docker image; files next to the binary, data with _index/index.sqlite, _blobs and _ca, and short-lived .tools scripts; secrets in the environment, ARTEX_PG_DSN, OPENAI_API_KEY and ANTHROPIC_API_KEY, visible to every agent shell; and an update check to api.github.com for Autumn-27/artex with user agent artex-selfupdate, which fails today. At the target: page reads with User-Agent norma/0.4; an asset title probe with User-Agent artex-enrich/1.0 sent through the recording proxy; everything else with ordinary user agents such as curl, python-requests and Python-urllib, which Microsoft saw with Storm-0940 alongside azure-toolkit/1.0; source addresses from egress proxies, with CrowdStrike listing nine for one actor, while a direct WebFetch retry may show the real host; and the effects, web shells and data theft reported by Zscaler, and password spraying and app abuse reported by Microsoft.

Figure 3. Original graphic. Every item is a lead to verify: forks rename things, and your own approved testing will look the same.

On a host running ARTEX, the defaults are distinctive: a binary called artex that starts bash -c, listeners on 8787 and 8788, a data directory holding the captured traffic and a man-in-the-middle certificate authority, and model keys in the environment. That matters in two cases: someone in your organisation runs ARTEX without approval, or an attacker has installed it on a server they compromised.

At the target, ARTEX looks mostly like the tools it drives. The two fixed user agents are cheap leads. Microsoft's user agents for Storm-0940's sign-ins and Graph calls were generic: python-requests, Python-urllib, curl and azure-toolkit/1.0. The stronger evidence is what happened: accepted writes, new web shells, sign-ins and app changes, which the harness framework's hunts cover.

The log checklist

A hunt that returns nothing is only meaningful if the log it reads was there. Microsoft's guidance for the ConnectWise compromise asks responders to review service-principal sign-ins back to 1 October, Microsoft Graph activity, Entra audit records and Key Vault access. In most tenants, none of those reaches Microsoft Sentinel until someone turns it on.

Sixteen cards in five columns. Identity: L01 Entra interactive sign-ins, SigninLogs; L02 non-interactive user sign-ins; L03 service-principal and managed-identity sign-ins; L04 directory audit, AuditLogs; all off until the Entra ID connector or a diagnostic setting is configured; and L05 Microsoft Graph activity, shown in red because it needs P1 or P2 and a diagnostic setting and is kept nowhere otherwise. Cloud: L06 Azure control plane, AzureActivity; L07 Key Vault data plane, AZKVAuditLogs, needing a diagnostic setting on every vault; L08 Microsoft 365 workload audit, OfficeActivity and CloudAppEvents. Web: L09 web and API access logs, W3CIISLog, AppServiceHTTPLogs and AGWAccessLogs; L10 WAF decisions. Network: L11 firewall and proxy egress with bytes, CommonSecurityLog. Endpoint: L12 process lineage, DeviceProcessEvents and Falcon ProcessRollup2; L13 network connections and listeners, DeviceNetworkEvents and Falcon NetworkConnectIP4 and NetworkListenIP4; L14 file writes under web roots; L15 DNS by process, DeviceEvents and Falcon DnsRequest; L16 sensor health, DeviceInfo and Falcon SensorHeartbeat. Defender tables stay in Defender for 30 days and reach Sentinel only through the XDR connector. A banner says to prove the logs arrive with S01 and S02 in Sentinel or C01 and C02 in Falcon before trusting a quiet hunt.

Figure 4. Original graphic. Amber: off until someone turns it on. Red: off, licence-dependent, and lost unless exported.

Retention matters as much as collection. Entra keeps sign-in and audit logs for 7 days on the free tier and 30 days on P1 or P2. Graph activity logs are not kept at all unless you send them somewhere. Defender XDR keeps its hunting data for 30 days, and Sentinel includes 90 days of analytics retention in its price.

Timeline from 1 August to 4 November 2026. Microsoft's review window for service-principal sign-ins runs from 1 October, and at least from 15 October, to today. The Entra admin center keeps 7 days on the free tier and 30 days on P1 or P2, so on 4 November 1 to 4 October are already gone. Microsoft Graph activity logs keep nothing unless exported. Defender XDR advanced hunting keeps 30 days. The Sentinel analytics tier keeps 90 days at no extra storage cost, if the connector was on. A note says that on 4 November a tenant relying on the Entra admin center alone can no longer see 1 to 4 October, and a free tenant sees only the last week.

Figure 5. Original graphic. Bars show days of history held on 4 November, not any particular tenant's configuration.

ID Log source Sentinel table Falcon source Must contain How it gets turned on Keep at least Owner
L01 Entra interactive sign-ins SigninLogs Entra ID sign-in data connector Client IP, user agent, user, result Off: Entra ID connector or diagnostic setting 90 days (Entra keeps 30) IAM
L02 Non-interactive user sign-ins AADNonInteractiveUserSignInLogs Entra ID sign-in data connector Client IP, user, app Off: a separate category to tick 90 days IAM
L03 Service-principal and managed-identity sign-ins AADServicePrincipalSignInLogs, AADManagedIdentitySignInLogs Entra ID sign-in data connector Client IP, service principal, app ID, result Off: separate categories to tick 90 days; Microsoft asked for review back to 1 Oct IAM
L04 Directory audit AuditLogs Entra ID audit data connector Operation, initiator, target, result Off: Entra ID connector or diagnostic setting 90 days (Entra keeps 30) IAM
L05 Microsoft Graph activity MicrosoftGraphActivityLogs Event Hubs export to Next-Gen SIEM Client IP, user agent, app ID, request URI Off: needs P1/P2 and a diagnostic setting; kept nowhere otherwise 90 days IAM
L06 Azure control plane AzureActivity Azure data connector Caller, operation, resource, result Off: connector or policy per subscription 90 days Cloud platform
L07 Key Vault data plane AZKVAuditLogs Azure data connector Caller IP, identity, operation, result Off: a diagnostic setting on every vault 90 days Cloud platform
L08 Microsoft 365 workload audit OfficeActivity, CloudAppEvents Microsoft 365 data connector User or app, operation, client IP Off: Microsoft 365 or Defender XDR connector 90 days Collaboration admins
L09 Web and API access logs W3CIISLog, AppServiceHTTPLogs, AGWAccessLogs Web or WAF parser Client IP, host, path, method, status, user agent, signed-in user Off: agent collection rule or diagnostic setting 90 days AppSec / web owner
L10 WAF decisions AGWFirewallLogs, CommonSecurityLog WAF parser Rule, action, client IP, URI Off: diagnostic setting or log forwarding 90 days AppSec / web owner
L11 Firewall and proxy egress with bytes CommonSecurityLog Firewall or proxy parser Source, destination, bytes out, user agent Off: log forwarding (CEF) to be set up 90 days Network
L12 Process lineage on servers DeviceProcessEvents ProcessRollup2 Parent, child, command line, account Defender: 30 days in Defender, Sentinel only via the XDR connector 90 days for internet-facing servers EDR
L13 Network connections and listeners DeviceNetworkEvents NetworkConnectIP4, NetworkListenIP4 Local and remote IP and port, process Defender: 30 days in Defender, Sentinel only via the XDR connector 90 days for internet-facing servers EDR
L14 File writes under web roots DeviceFileEvents File-write events for your platform Path, process, hash Defender: 30 days in Defender, Sentinel only via the XDR connector 90 days for internet-facing servers EDR
L15 DNS queries by process DeviceEvents DnsRequest Query name, process Defender: 30 days in Defender, Sentinel only via the XDR connector 30 days at least EDR
L16 Sensor health against inventory DeviceInfo SensorHeartbeat Device, last seen, health Defender: 30 days in Defender, Sentinel only via the XDR connector Daily snapshot IT Operations

Download the log checklist (CSV), with blank columns for status, who checked and when. The Falcon column names the Next-Gen SIEM parser or connector, or the EDR event. Connectors vary by licence, so check that one exists for each source; check every EDR event name against Falcon's Events Data Dictionary for your sensor platforms.

Prove the Sentinel sources arrive: S01 and S02

S01 compares the tables you expect with what the workspace has ingested. It reads 90 days of the Usage table, which is cheap, and only the last hour of the tables themselves. MISSING means nothing arrived in 90 days; STALE means nothing in the last day.

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

Download S01

S02 checks the fields the hunts join on. A web log without user agents cannot answer S03, and a sign-in log without IP addresses cannot answer anything about where an attack came from.

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

Download S02

Prove the Falcon sources arrive: C01 and C02

C01 counts the EDR event types the hunts need, by platform and by sensor. Compare the sensor counts with your server inventory: a server that sends no NetworkListenIP4 events cannot answer 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)

Download C01

C02 does the same for third-party data in Next-Gen SIEM. Replace the parser names with your identity, web and firewall parsers.

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

Download C02

Hunting the tool's own traces

S03 and C03: ARTEX's fixed user agents

These look for the two user agents in your web logs. Expect your own approved testing to match. A hit with no accepted change is alert A02 below; a hit next to an accepted write is 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

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

Download C03

S04 and C04: a host in your estate running ARTEX

These look for the default binary name starting child processes, listeners on 8787 or 8788, and calls from the ARTEX process to the cloud metadata address or to GitHub's API. Microsoft Defender's ListeningConnectionCreated and Falcon's NetworkListenIP4 events must be arriving from the host for the listener check to work; C01 and S01 tell you whether they are.

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

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

Download C04

For the attacks themselves, use the fourteen hunts in the harness framework: discovery followed by accepted writes (S1), probe files (S2), password spraying (S3), app identities changing the directory (S4), uniform egress (S5) and the Falcon equivalents.

Who to alert, and when to call for help

Matrix of eight triggers against ten teams, with a deadline and outside help for each. A01, a log source missing or stale or a key field mostly empty: SIEM engineering first, then the SOC, the same business day or within an hour during an incident. A02, an ARTEX user agent or published proxy IP at the web edge with no accepted effect: the SOC first, then AppSec, within an hour; share indicators with an ISAC. A03, an accepted effect such as a write then read-back, a web shell or a web worker starting a shell: page the incident lead, EDR and DFIR, and AppSec within 15 minutes; call an IR retainer if needed and JPCERT/CC for coordination. A04, identity changes such as a sprayed account signing in or an app gaining a credential, role or consent: IAM and the incident lead within 15 minutes, then the cloud team; Microsoft and the app's vendor. A05, personal data may have left: legal and privacy with the incident lead immediately; report to the PPC and the police. A06, a host in your estate runs ARTEX: EDR and IT operations within an hour, then the platform owner; if unapproved, handle as a compromise. A07, your approved ARTEX reachable from other hosts: the platform owner the same day. A08, your ARTEX checking for updates or holding keys in its environment: the platform owner the same day, then IAM. A note lists Japan's contacts: JPCERT/CC, the police cybercrime consultation desk, and the PPC for personal-data leaks with a prompt report in about 3 to 5 days.

Figure 6. Original graphic. "Platform owner" means whoever runs ARTEX or similar tools in your organisation, often the red team.

ID Trigger Seen in Alert first Within Then involve Outside help
A01 A log source is missing or stale, or a key field is mostly empty S01, S02, C01, C02 SIEM engineering + the source's owner Same business day; 1 hour during an incident SOC lead, if a hunt or rule depends on it None
A02 An ARTEX user agent or a published ARTEX proxy IP reaches your web edge, with no accepted effect yet S03, C03 SOC 1 hour AppSec / web owner Share indicators with your ISAC or peers
A03 An accepted effect: a write then read-back, a new web-shell file, or a web worker starting a shell Harness S1, S2, C1, C2 Incident lead (page) + EDR / DFIR + AppSec 15 minutes Owner of the affected data IR retainer if you lack DFIR capacity; JPCERT/CC for coordination
A04 Identity: a sprayed account signs in, an app gains a credential or role, or consent is granted Harness S3, S4; Microsoft alert titles IAM + incident lead 15 minutes Cloud platform; third-party risk if a vendor's app is involved Microsoft support or IR; the app's vendor
A05 Personal or customer data may have left Harness S5; DLP; application logs Legal / Privacy + incident lead Immediately Executive owner and communications PPC report (prompt report in about 3–5 days); police
A06 A host in your estate runs ARTEX: a listener on 8787/8788, the artex binary, or its metadata calls S04, C04 EDR + IT Operations 1 hour Red-team owner to confirm approval; insider-risk process if unapproved If unapproved, handle as a compromise
A07 Your approved ARTEX can be reached from other hosts, or its proxy listens on all interfaces S04, C04; your own exposure inventory Platform owner + network Same day Security architecture None
A08 Your ARTEX checks for updates, or keeps model or database keys in its environment S04; configuration review Platform owner Same day IAM, to rotate the keys None

Download the alert matrix (CSV), with blank columns for the named owner, contact and the date you last tested the route. The deadlines are starting points for a team with an on-call rota; agree them with each owner. The response playbook covers what happens after the first call.

When to call for help in Japan:

  • JPCERT/CC takes incident reports and helps coordinate with the other organisations involved (report form, Japanese). Its alert of 8 October covers the recent intrusions in Japan.
  • The police take crime reports through the cybercrime consultation desks (Japanese).
  • The Personal Information Protection Commission (PPC) must be told about qualifying personal-data leaks: a prompt report within about 3–5 days of learning of the incident, and a final report within 30 days, or 60 days when the cause is a wrongful act such as an attack (PPC, Japanese).
  • Vendors and retainers: Microsoft's and CrowdStrike's incident response services, your IR retainer or cyber insurer, and the vendor of any third-party app involved, as with ConnectWise.

The attacker's playbook

How an operator uses ARTEX, with MITRE ATT&CK techniques. Where we chose the nearest match rather than the source's own label, we say so.

  1. Obtain the capability: a public fork and a model API (T1588.007, Obtain Capabilities: Artificial Intelligence, as CrowdStrike labels it). CrowdStrike saw DeepSeek reached through an API reseller.
  2. Stand up infrastructure: virtual private servers and a chain of egress proxies (T1583.003, Virtual Private Server; T1090, Proxy). The recording proxy's egress setting makes this one line of configuration.
  3. Find and probe targets: the title probe and WebFetch read every web asset the agent finds (T1595.002, Vulnerability Scanning, our nearest match).
  4. Get in: exploit peripheral web applications and plant web shells, as Zscaler reports (T1190, Exploit Public-Facing Application; T1505.003, Web Shell); or spray passwords and abuse OAuth apps, as Microsoft reports (T1110.003, Password Spraying; T1078.004, Cloud Accounts).
  5. Stay: give an app new credentials or a higher role (T1098.001, Additional Cloud Credentials; T1098.003, Additional Cloud Roles), which Microsoft's alert titles describe.
  6. Take the data: Zscaler saw structured personal data sent out over HTTPS (T1041, Exfiltration Over C2 Channel, our nearest match).

Abusing someone else's ARTEX. An instance started from the Docker image with a published 8788 is an unauthenticated proxy: anyone who finds it can reach the networks behind it, including a cloud metadata service (T1090, Proxy; T1552.005, Cloud Instance Metadata API).

What does not change, whatever the fork: the agent needs a network path to its model and to its target, and its effects land in your identity, web and endpoint logs. Rename the binary and change the user agents, and the accepted writes, sign-ins and new files are still there to find, if the logs are.

The defender's playbook

  1. Today: prove the logs. Run S01 and S02, or C01 and C02. Send every MISSING or STALE source, and every field below about 90%, to its owner with a date (alert A01).
  2. This week: turn on what Microsoft's guidance needs. Service-principal and managed-identity sign-ins, Microsoft Graph activity (P1 or P2), Entra audit, Key Vault diagnostics on every vault, and web logs that keep the user agent and the signed-in user. Set those tables to 90 days in Sentinel.
  3. Every day: hunt the traces. Run S03 and S04, or C03 and C04, and the harness framework's fourteen hunts for effects. Expect matches from approved testing and record them, so a real one stands out.
  4. If you run ARTEX yourselves, contain it from outside:
    • Run it in a disposable VM with no route to production except the engagement's targets. Enforce scope with an egress allow-list, and block 169.254.169.254 and internal ranges unless they are in scope.
    • Bind 8787 and 8788 to loopback, never publish 8788, and put the interface behind your single sign-on.
    • Give it its own database and a model key with a spending cap, rotate both after each engagement, and keep every other secret out of its environment.
    • Treat custom and MCP tools as unapproved until reviewed, and put -- before arguments in command templates.
    • Block the host's access to GitHub and update only by rebuilding from source you have checked.
    • Keep the guard's audit log, the interception history and the recording proxy's traffic index with the engagement record. They are your evidence of what the test did.
  5. Now: agree the alert matrix. Name an owner and a contact for A01 to A08, and test each route once before you need it.

What remains unknown

  • How the private upstream builds differ from the fork. CrowdStrike and Microsoft saw the tool in the wild; we reviewed the public fork.
  • Whether attackers changed the user agents. No vendor report lists norma/0.4 or artex-enrich/1.0; their absence from your logs proves nothing.
  • How CrowdStrike's proxy IPs were set up, whether through ARTEX's egress setting or outside it.
  • Whether CTI Academy's findings will be fixed. The fork's newest commit, from 14 October, has the same code.

Sources and updates

Published 4 November 2026 (JST). We read the Microsoft, CrowdStrike and Zscaler reports directly on 4 November. CTI Academy's page is rendered by script and returned only a cookie notice to our fetcher, so we worked from the copy the site owner supplied and confirmed its address and title through a search listing. The JPCERT/CC alert is cited from our earlier review. We read ARTEX's code at the commit below and did not run it. No query was run in a live tenant; the four KQL files passed Microsoft's Kusto parser against table definitions from Microsoft Learn, and the four CQL files had static review only.

All articles →日本語で読む ↗