ARTEX, PentAGI, Cairn, CyberStrikeAI and Hexstrike are open-source wrappers around the same thing: a language model driving commodity offensive tools in a plan–act–observe loop. GTIG/Mandiant (Nov 2) report PRC-nexus UNC7228 and financially motivated groups running them in real intrusions since late August; CrowdStrike (Oct 22, Oct 30) names four actors including VAPOR PANDA; Microsoft ties ARTEX to Storm-0940 and to North Korean operators; CERT-In's "Crafty Falcon" runs all four against banking and crypto targets; WorkOS and Chainalysis disclosed breaches by the same tooling. Since the first issue: Check Point (Nov 6) counted 2,557 hosts that run or ran an ARTEX panel, 1,608 live, and read ten operators' databases — the runs were autonomous after the goal was typed; Microsoft (Nov 5, Nov 6) showed agents going from a GitHub secret search to Key Vault exfiltration in five minutes, and named Cambodia-based Storm-3358 running ARTEX against banks with web shells, curl/netcat download cradles and WISP, vshell and AdaptixC2. Anyone can clone the repositories and rename everything, so this pack keys on what no fork can remove.
Verdict: stop building detections per framework. Name-based indicators (artex-probe-*, ArtexProbe, artex-enrich/1.0) are a cheap first layer that a fork erases in an afternoon. The durable signal is the agent loop itself: machine-speed bursts separated by model "think" pauses, permission probing before action, retry-with-variation on errors, a compressed kill chain from first contact to privilege, the same open-source tool chain under every wrapper, and a dependence on supplied valid credentials rather than novel exploits. Hunt those six properties with the twelve searches below, and flip the script with deception: agents act on every credential they find within seconds, so canary credentials are a near-zero-false-positive tripwire. Thresholds here are starting points set from the two published cadence figures (GTIG: 27 control-plane calls in 23 seconds; Check Point: 400,791 agent messages against 292 domains in four days, about 70 a minute sustained); a scoped lab run that clones the four frameworks will reset them from recordings. Microsoft now states that decoys and "fake wins" cut the traffic reaching real assets in its own testing, so the deception half of this card is no longer a hypothesis.
Six loop properties, each with a hunt below. A1 Tempo: bursts of 2–25 seconds, then a pause, then another burst — a bimodal timing distribution from one principal that neither a person nor a constant-rate scanner produces. A2 Permission probing: DryRun calls, deployment validation, write-read-delete of a test object, identity lookups across every service — the agent asks "what can I do" before doing it. A3 Retry with variation: the same request repeated with one parameter changed, many times in a minute. A4 Kill-chain compression: first sight of a source to a privileged action in minutes, with no dwell. A5 Commodity tool chain: nmap, nuclei, ffuf, sqlmap, impacket and curl-to-callback under every framework. A6 Supplied credentials: success from never-seen infrastructure with a scripted client, immediately followed by activity — count residential-proxy ASNs as never-seen, since Check Point found operators routing through them to beat WAF blocks. A7 is the tripwire: any touch of a canary credential, honey account or decoy endpoint is an incident.
| Category | Source | Status |
|---|---|---|
| Primary research | GTIG / Mandiant | Behavioral guidance published — five generic detection notes (control-plane velocity, egress to unseen webhook/OAST services, DMZ-origin LDAP/Kerberos/SMB, bursts of resets and account creation, AD CS enrollment) rendering live below; they are the seed for A1–A5 |
| Vendor reporting | CrowdStrike · Microsoft | Actor context — four actors using agentic frameworks (Oct 22), VAPOR PANDA at 200+ orgs (Oct 30), Storm-0940 and DPRK operators with ARTEX; name-based artifacts only, no loop-level detections |
| Operator-side census | Check Point Research | Autonomy and cost measured — 2,557 hosts with ARTEX panels (1,608 live; US 54%, Hong Kong 18%, China 6%), ten exposed operator databases read; "most attacks were fully autonomous besides the initial goal setup"; a two-week run cost $700–1,500 in tokens; agents validated every credential they found (OCSP checks on stolen signing certs, live sampling of 190,000 game logins) and believed API "success" responses, some of which were hallucinated. No loop-level detections, but three C2 clusters with ASNs and ranges for an egress hunt, and the evidence behind the deception runbook below |
| Vendor hunting content | Microsoft Nov 5 · Microsoft Nov 6 | Queries and deception guidance published — Defender XDR query for curl downloads unseen in seven days, Sentinel ASIM query over eight C2/ARTEX IPs, Entra service-principal sign-in failure→success pattern; Microsoft recommends honeypots, decoys, tarpits and fake wins outright and says they reduced attack traffic reaching real assets. Rendered live below as the pipeline stores it |
| Framework-specific IoCs | Cutsign ARTEX brief (Nov 5) | Short-lived layer — self-naming files, accounts, roles, comments and user agents; keep as cheap early warning, expect them to age out as forks appear |
| Ecosystem | The Hacker News | Supply-chain hazard — a backdoored ARTEX Docker image gave an unauthenticated root shell on 62 exposed consoles; anyone cloning these tools for a lab must build from pinned source, never pull an image |
| Vendor SIEM content | Splunk ES / Elastic prebuilt rules | Not surveyed — no loop-level prebuilt content was checked for this card; the pack below is the coverage until the lab run validates it |
An agent explores breadth-first and tries every credential it finds, usually within seconds, then validates it and reports success from whatever the API returns. Check Point watched agents OCSP-check stolen certificates and sample stolen logins for liveness; it also saw them record wins that never happened because an endpoint said "success". A human attacker sits on one credential and checks the result. That asymmetry makes canaries the lowest-noise control against this tooling, and makes a decoy that says "yes" a way to end the run. Microsoft's Nov 6 post says the same in its own testing. Run it in this order:
.env/config paths (Check Point: hardcoded admin passwords in SPA JavaScript), the cloud control plane the exposed hosts can reach, and the identity plane behind them. If you have a surface report, use its top row per bucket; if not, the DMZ web tier, the cloud account it runs in and the directory it authenticates to.passwords-backup.xlsx, id_rsa.bak) in the home directory of a server process, carrying a token that calls a URL you control when opened. The agent reads everything in reach./.env, /.git/config, /actuator, /wp-login.php) slowly with a 200 and junk, so A3 retries burn the operator's token budget rather than yours. Microsoft reports agents stopping on a false success; Check Point's Campaign 2 is 400,791 messages that moved no money for the same reason.Every public case so far says where the agent got its first foothold. Put the canary at that spot and the tarpit one step before it, so the spot that was the attacker's first win becomes yours:
| Observed entry | Source | Canary there | Tarpit before it |
|---|---|---|---|
| Admin password hardcoded in a front-end JavaScript bundle; unauthenticated web API behind it | Check Point, certificate-shop campaign | A commented "legacy" API key in a real bundle that only the decoy admin API accepts; A7 decoy-endpoint query | The decoy admin API answers every path under /api/admin slowly with a plausible 200 and a session cookie |
Unauthenticated API leaked AWS keys with write access to a release bucket; agent rewrote manifest.plist | Check Point, supply-chain case | An IAM key with no policy served by a decoy config endpoint; a bucket named like release assets with one canary object and access logging on | The decoy endpoint rate-limits itself to one answer a minute, so the write→read→delete probe (A2) takes the agent an hour |
| Secret found by searching GitHub, used to sign in as the service principal, Key Vault read within five minutes | Microsoft, Nov 5 | An app registration with no API permissions, its client secret in a repository under your organisation; alert on any sign-in by that app id. A honey Key Vault with audit logging and a secret named like a production connection string | None: this path is pure identity; the canary is the whole control. Push-protection and secret scanning on the real repositories |
| Anonymous file upload and SSRF on an internet-facing app, web shell, curl/netcat download of WISP, vshell, AdaptixC2 | Microsoft, Storm-3358 | An internal-looking URL (http://metadata.internal/, a private-range host) planted in the app's JavaScript and robots.txt that resolves only to a logging decoy; any request to it is an SSRF hit | An upload endpoint that accepts anything, quarantines it, and returns a path that 404s slowly; A3 retry-with-variation fires on the agent working the path |
| Legacy internal-tools platform exposed to the internet, predictable local-development credential left in production, no rate limiting | WorkOS | A hostname that looks like a legacy tools portal in public DNS, pointing at a decoy login that accepts the obvious development pair and logs the session | The decoy login never rejects, it just takes four seconds; A3 and A6 both fire on it |
| Adobe Commerce template-injection RCE across 100+ storefronts | Cairn, Oct 21 | A Magento-shaped admin page on the decoy host, with a canary admin account whose only use is an incident | /admin, /rest/V1, /.env, /.git/config on every non-commerce host return slow 200s; scanners walking them show up in A5's user-agent hunt |
| Supplied valid credential, then burst-and-pause enumeration of the cloud control plane | GTIG / Mandiant, Nov 2 | The cloud key in an environment file on a DMZ host (step 2 above); A7 cloud-key query | A deny-all permission boundary on the canary principal, so every enumeration call is a logged AccessDenied and the burst itself becomes the A1/A2 record |
Two rules for the table: every decoy lives on a host of its own with nothing real on it, and every entry in it is also an architecture fix to make on the production side (no secrets in bundles, authenticated APIs, no long-lived cloud keys, upload and egress controls, rate limits, legacy platforms retired). The canary buys you the detection while that fix is in the backlog.
The gap this card still has: no public write-up yet shows a canary hit from one of these agents with its timing. First organisation to publish that gives everyone the A4 number for free.
The four frameworks are open source, which cuts both ways: the defender can record exactly what the loop looks like before it arrives. The scoped run clones them from pinned source into a disposable range the operator owns (a web app with MySQL, a scoped cloud account, a small directory), runs them by hand against it, and captures the control-plane, web-access, process and authentication telemetry. The hunt runner reads only the resulting telemetry drop and derives the numbers this card currently estimates: burst length and gap distribution, distinct actions per window, retry variants per minute, time-to-privilege, tool list, object-naming habits. Noise recordings from the same range — a Terraform apply, a CI deploy, a backup job — are what keep A1 and A2 honest, since those are the legitimate bursty callers. The six templates then validate fire-on-signal, silent-on-noise, and this card is re-issued with measured thresholds. Detonation stays a human act in a range the human owns; nothing in Cutsign or the hunt runner initiates it.
GTIG's published guidance (live above) is the seed; these are the generic-SIEM hunts over the six loop properties plus the canary tripwire. Thresholds are starting points, tune on your baseline until the lab run resets them:
A1 tempo ≥15 calls across ≥8 actions in 30 s, ≥3 such bursts, duty cycle <60% (bursts with gaps) A2 probing denied / dry-run calls across ≥5 services in 5 min; write→read→delete of one object in 2 min A3 retry ≥8 errors on one path in 60 s with ≥6 distinct query strings A4 compression first sight of a source → first privileged action ≤15 min A5 tool chain commodity offensive tools spawned on servers; scanner UAs at the edge; egress to OAST/callback services A6 credentials success from a user/ASN pair unseen in 30 days with a scripted or empty user agent (residential-proxy ASNs count as unseen) A7 tripwire any use of a canary key or honey account; any request to a decoy endpoint
Splunk (index=*) / Elastic ES|QL (FROM logs-*) — Splunk over CIM-shaped fields with <placeholder> sourcetypes; Elastic over the AWS CloudTrail, Azure Logs, web-server, Endpoint and Windows integrations (aws.cloudtrail.*, azure.signinlogs.*, url.*, process.*, event.code). Tune field names to your add-on or integration:
# A1 tempo — control plane: bursts of ≥15 calls across ≥8 distinct actions in 30 s, repeated, with gaps between them
index=* sourcetype=<cloud_audit> earliest=-7d
| rename userIdentity.arn as principal, sourceIPAddress as src, eventName as action
| bin _time span=30s
| stats count as n dc(action) as actions by principal src _time
| where n>=15 AND actions>=8
| stats count as bursts max(n) as peak min(_time) as first max(_time) as last by principal src
| where bursts>=3
| eval duty=round((bursts*30)/(last-first+30), 2)
| where duty<0.6
| table first last principal src bursts peak duty
# A1 tempo — web: one source fans out across ≥40 paths in 30 s, more than once
index=* sourcetype=<web_access> earliest=-7d
| bin _time span=30s
| stats count as n dc(uri_path) as paths by src _time
| where n>=60 AND paths>=40
| stats count as bursts max(paths) as peak_paths min(_time) as first max(_time) as last by src
| where bursts>=2
| table first last src bursts peak_paths
# A2 permission probing — denied or dry-run calls fanning out across ≥5 services in 5 min
index=* sourcetype=<cloud_audit> earliest=-7d (errorCode IN ("AccessDenied","UnauthorizedOperation","Client.UnauthorizedOperation","Client.DryRunOperation","AuthorizationError") OR eventName IN ("GetCallerIdentity","SimulatePrincipalPolicy","ListAttachedUserPolicies","GetAccountAuthorizationDetails"))
| rename userIdentity.arn as principal, sourceIPAddress as src
| bin _time span=5m
| stats count as n dc(eventSource) as services dc(eventName) as actions values(errorCode) as errors by principal src _time
| where services>=5 AND n>=20
| table _time principal src n services actions errors
# A2 permission probing — storage write→read→delete of one object inside 2 minutes (the "can I write here?" test)
index=* sourcetype=<cloud_audit> earliest=-7d eventName IN ("PutObject","GetObject","DeleteObject")
| rename userIdentity.arn as principal, requestParameters.key as object
| transaction principal object maxspan=2m
| where eventcount>=3 AND searchmatch("PutObject") AND searchmatch("DeleteObject")
| table _time principal object duration eventcount
# A3 retry with variation — same source, same path, ≥8 errors in 60 s with ≥6 distinct query strings
index=* sourcetype=<web_access> earliest=-7d status>=400
| bin _time span=60s
| stats count as n dc(uri_query) as variants dc(http_method) as methods values(status) as codes by src uri_path _time
| where n>=8 AND variants>=6
| table _time src uri_path n variants methods codes
# A4 kill-chain compression — a source first seen ≤15 min before its first privileged action (7-day baseline)
index=* earliest=-7d (sourcetype=<cloud_audit> OR sourcetype=<windows_eventlog>)
| eval src=coalesce(sourceIPAddress, src_ip, Source_Network_Address)
| eval priv=if(eventName IN ("CreateUser","CreateAccessKey","AttachUserPolicy","PutUserPolicy","AddUserToGroup","CreateLoginProfile","UpdateAssumeRolePolicy") OR EventCode IN (4720,4724,4728,4732,4756,4662,4794), _time, null())
| stats min(_time) as first_seen min(priv) as first_priv count as events by src
| where isnotnull(first_priv)
| eval ttp_min=round((first_priv-first_seen)/60, 1)
| where ttp_min<=15
| table first_seen first_priv ttp_min src events
# A5 tool chain — the open-source tools every framework wraps, spawned on a server
index=* sourcetype=<endpoint_process> earliest=-7d
| rename process_name as proc, process as cmd, parent_process_name as parent, dest as host
| where match(proc, "(?i)^(nmap|masscan|nuclei|ffuf|gobuster|feroxbuster|dirsearch|sqlmap|nikto|hydra|netexec|nxc|crackmapexec|evil-winrm|responder|certipy|bloodhound-python|kerbrute|chisel|ligolo|ncat|netcat)") OR match(cmd, "(?i)(secretsdump|GetUserSPNs|GetNPUsers|psexec\.py|wmiexec|smbexec|ntlmrelayx|mimikatz|rubeus|sharphound)")
| stats count values(proc) as tools values(parent) as parents min(_time) as first by host user
| table first host user count tools parents
# A5 tool chain — scanner and script user agents at the edge, fanning out
index=* sourcetype=<web_access> earliest=-7d
| regex http_user_agent="(?i)(nuclei|sqlmap|ffuf|gobuster|feroxbuster|dirsearch|nikto|masscan|nmap|wpscan|python-requests|python-httpx|go-http-client|aiohttp|axios|okhttp|curl/|wget/|headlesschrome|norma/)"
| stats count dc(uri_path) as paths values(http_user_agent) as uas by src
| where count>=50 AND paths>=20
# A5 tool chain — server egress to out-of-band callback services (the agent's cheapest exfil and liveness check)
index=* (sourcetype=<dns> OR sourcetype=<proxy>) earliest=-7d
| eval dest=coalesce(query, dest_host, url_domain)
| regex dest="(?i)(webhook\.site|oast\.(fun|live|me|online|pro|site)|oastify\.com|interact\.sh|burpcollaborator\.net|requestbin|pipedream\.net|ngrok\.io|ngrok-free\.app|trycloudflare\.com|dnslog\.cn)"
| stats count min(_time) as first values(dest) as dests by src
# A6 supplied credentials — a success from a user/ASN pair unseen in 30 days, with a scripted or empty client
index=* sourcetype=<entra_signin> earliest=-30d category=SignInLogs properties.status.errorCode=0
| rename properties.userPrincipalName as user, properties.autonomousSystemNumber as asn, properties.userAgent as ua
| eval ua=coalesce(ua, ""), scripted=if(match(ua, "(?i)^(python|axios|node|go-http|curl|okhttp|aiohttp|java/|libwww)") OR ua="", 1, 0)
| stats min(_time) as first_seen max(_time) as last_seen count as n max(scripted) as scripted values(ua) as uas by user asn
| where first_seen>=relative_time(now(), "-1d@d") AND scripted=1
| table first_seen last_seen user asn n uas
# A7 tripwire — any use of a canary cloud key is an incident; there is no benign case
index=* sourcetype=<cloud_audit> earliest=-7d userIdentity.accessKeyId IN (<canary_key_ids>)
| rename userIdentity.accessKeyId as key, sourceIPAddress as src
| stats min(_time) as first count values(eventName) as actions values(src) as srcs by key
# A7 tripwire — any authentication touching a honey account, including a Kerberoast of its SPN
index=* sourcetype=<windows_eventlog> earliest=-7d EventCode IN (4624,4625,4768,4769,4771,4776) user IN (<honey_accounts>)
| stats min(_time) as first count values(EventCode) as codes values(src) as srcs values(dest) as targets by user
# A7 tripwire — any request to the decoy host or decoy paths; the fake win logs what was tried
index=* sourcetype=<web_access> earliest=-7d (host IN (<decoy_hosts>) OR uri_path IN (<decoy_paths>))
| stats min(_time) as first max(_time) as last count dc(uri_path) as paths values(http_user_agent) as uas values(status) as statuses by src
| eval seconds=last-first
// A1 tempo — control plane: bursts of ≥15 calls across ≥8 distinct actions in 30 s, repeated, with gaps between them
FROM logs-*
| WHERE @timestamp > NOW() - 7 days AND event.dataset == "aws.cloudtrail"
| EVAL w = DATE_TRUNC(30 seconds, @timestamp)
| STATS n = COUNT(*), actions = COUNT_DISTINCT(event.action) BY user.name, source.ip, w
| WHERE n >= 15 AND actions >= 8
| STATS bursts = COUNT(*), peak = MAX(n), first = MIN(w), last = MAX(w) BY user.name, source.ip
| WHERE bursts >= 3
| EVAL duty = ROUND(TO_DOUBLE(bursts * 30) / TO_DOUBLE(DATE_DIFF("seconds", first, last) + 30), 2)
| WHERE duty < 0.6
| KEEP first, last, user.name, source.ip, bursts, peak, duty
// A1 tempo — web: one source fans out across ≥40 paths in 30 s, more than once
FROM logs-*
| WHERE @timestamp > NOW() - 7 days AND event.dataset IN ("nginx.access", "apache.access", "iis.access")
| EVAL w = DATE_TRUNC(30 seconds, @timestamp)
| STATS n = COUNT(*), paths = COUNT_DISTINCT(url.path) BY source.ip, w
| WHERE n >= 60 AND paths >= 40
| STATS bursts = COUNT(*), peak_paths = MAX(paths), first = MIN(w), last = MAX(w) BY source.ip
| WHERE bursts >= 2
| KEEP first, last, source.ip, bursts, peak_paths
// A2 permission probing — denied or dry-run calls fanning out across ≥5 services in 5 min
FROM logs-*
| WHERE @timestamp > NOW() - 7 days AND event.dataset == "aws.cloudtrail"
AND (aws.cloudtrail.error_code IN ("AccessDenied", "UnauthorizedOperation", "Client.UnauthorizedOperation", "Client.DryRunOperation", "AuthorizationError")
OR event.action IN ("GetCallerIdentity", "SimulatePrincipalPolicy", "ListAttachedUserPolicies", "GetAccountAuthorizationDetails"))
| EVAL w = DATE_TRUNC(5 minutes, @timestamp)
| STATS n = COUNT(*), services = COUNT_DISTINCT(event.provider), actions = COUNT_DISTINCT(event.action), errors = VALUES(aws.cloudtrail.error_code) BY user.name, source.ip, w
| WHERE services >= 5 AND n >= 20
| KEEP w, user.name, source.ip, n, services, actions, errors
// A2 permission probing — storage write→read→delete of one object inside 2 minutes (fixed 2-minute buckets stand in for Splunk's transaction window)
FROM logs-*
| WHERE @timestamp > NOW() - 7 days AND event.dataset == "aws.cloudtrail" AND event.action IN ("PutObject", "GetObject", "DeleteObject")
| EVAL w = DATE_TRUNC(2 minutes, @timestamp)
| STATS ops = COUNT_DISTINCT(event.action), first = MIN(@timestamp), last = MAX(@timestamp) BY user.name, aws.cloudtrail.request_parameters, w
| WHERE ops == 3
| KEEP first, last, user.name, aws.cloudtrail.request_parameters
// A3 retry with variation — same source, same path, ≥8 errors in 60 s with ≥6 distinct query strings
FROM logs-*
| WHERE @timestamp > NOW() - 7 days AND event.dataset IN ("nginx.access", "apache.access", "iis.access") AND http.response.status_code >= 400
| EVAL w = DATE_TRUNC(60 seconds, @timestamp)
| STATS n = COUNT(*), variants = COUNT_DISTINCT(url.query), methods = COUNT_DISTINCT(http.request.method), codes = VALUES(http.response.status_code) BY source.ip, url.path, w
| WHERE n >= 8 AND variants >= 6
| KEEP w, source.ip, url.path, n, variants, methods, codes
// A4 kill-chain compression — a source first seen ≤15 min before its first privileged action (7-day baseline)
FROM logs-*
| WHERE @timestamp > NOW() - 7 days AND event.dataset IN ("aws.cloudtrail", "system.security")
| EVAL priv = CASE(event.action IN ("CreateUser", "CreateAccessKey", "AttachUserPolicy", "PutUserPolicy", "AddUserToGroup", "CreateLoginProfile", "UpdateAssumeRolePolicy")
OR event.code IN ("4720", "4724", "4728", "4732", "4756", "4662", "4794"), @timestamp)
| STATS first_seen = MIN(@timestamp), first_priv = MIN(priv), events = COUNT(*) BY source.ip
| WHERE first_priv IS NOT NULL
| EVAL ttp_min = DATE_DIFF("minutes", first_seen, first_priv)
| WHERE ttp_min <= 15
| KEEP first_seen, first_priv, ttp_min, source.ip, events
// A5 tool chain — the open-source tools every framework wraps, spawned on a server
FROM logs-*
| WHERE @timestamp > NOW() - 7 days AND event.category == "process" AND event.type == "start"
| EVAL proc = TO_LOWER(process.name), cmd = TO_LOWER(process.command_line)
| WHERE proc RLIKE "(nmap|masscan|nuclei|ffuf|gobuster|feroxbuster|dirsearch|sqlmap|nikto|hydra|netexec|nxc|crackmapexec|evil-winrm|responder|certipy|bloodhound-python|kerbrute|chisel|ligolo|ncat|netcat).*"
OR cmd RLIKE ".*(secretsdump|getuserspns|getnpusers|psexec\\.py|wmiexec|smbexec|ntlmrelayx|mimikatz|rubeus|sharphound).*"
| STATS count = COUNT(*), tools = VALUES(process.name), parents = VALUES(process.parent.name), first = MIN(@timestamp) BY host.name, user.name
| KEEP first, host.name, user.name, count, tools, parents
// A5 tool chain — scanner and script user agents at the edge, fanning out
FROM logs-*
| WHERE @timestamp > NOW() - 7 days AND event.dataset IN ("nginx.access", "apache.access", "iis.access")
| EVAL ua = TO_LOWER(user_agent.original)
| WHERE ua RLIKE ".*(nuclei|sqlmap|ffuf|gobuster|feroxbuster|dirsearch|nikto|masscan|nmap|wpscan|python-requests|python-httpx|go-http-client|aiohttp|axios|okhttp|curl/|wget/|headlesschrome|norma/).*"
| STATS count = COUNT(*), paths = COUNT_DISTINCT(url.path), uas = VALUES(user_agent.original) BY source.ip
| WHERE count >= 50 AND paths >= 20
// A5 tool chain — server egress to out-of-band callback services (the agent's cheapest exfil and liveness check)
FROM logs-*
| WHERE @timestamp > NOW() - 7 days AND (event.category == "network" OR event.dataset RLIKE ".*(dns|proxy).*")
| EVAL dest = TO_LOWER(COALESCE(dns.question.name, url.domain, destination.domain))
| WHERE dest RLIKE ".*(webhook\\.site|oast\\.(fun|live|me|online|pro|site)|oastify\\.com|interact\\.sh|burpcollaborator\\.net|requestbin|pipedream\\.net|ngrok\\.io|ngrok-free\\.app|trycloudflare\\.com|dnslog\\.cn).*"
| STATS count = COUNT(*), first = MIN(@timestamp), dests = VALUES(dest) BY source.ip
// A6 supplied credentials — a success from a user/ASN pair unseen in 30 days, with a scripted or empty client
FROM logs-*
| WHERE @timestamp > NOW() - 30 days AND event.dataset == "azure.signinlogs" AND azure.signinlogs.properties.status.error_code == 0
| EVAL ua = TO_LOWER(COALESCE(user_agent.original, "")), scripted = CASE(ua RLIKE "(python|axios|node|go-http|curl|okhttp|aiohttp|java/|libwww).*" OR ua == "", 1, 0)
| STATS first_seen = MIN(@timestamp), last_seen = MAX(@timestamp), n = COUNT(*), scripted = MAX(scripted), uas = VALUES(user_agent.original) BY user.name, source.as.number
| WHERE first_seen >= NOW() - 1 day AND scripted == 1
| KEEP first_seen, last_seen, user.name, source.as.number, n, uas
// A7 tripwire — any use of a canary cloud key is an incident; there is no benign case
FROM logs-*
| WHERE @timestamp > NOW() - 7 days AND event.dataset == "aws.cloudtrail" AND aws.cloudtrail.user_identity.access_key_id IN (<canary_key_ids>)
| STATS first = MIN(@timestamp), count = COUNT(*), actions = VALUES(event.action), srcs = VALUES(source.ip) BY aws.cloudtrail.user_identity.access_key_id
// A7 tripwire — any authentication touching a honey account, including a Kerberoast of its SPN
FROM logs-*
| WHERE @timestamp > NOW() - 7 days AND event.dataset == "system.security" AND event.code IN ("4624", "4625", "4768", "4769", "4771", "4776") AND user.name IN (<honey_accounts>)
| STATS first = MIN(@timestamp), count = COUNT(*), codes = VALUES(event.code), srcs = VALUES(source.ip), targets = VALUES(host.name) BY user.name
// A7 tripwire — any request to the decoy host or decoy paths; the fake win logs what was tried
FROM logs-*
| WHERE @timestamp > NOW() - 7 days AND event.dataset IN ("nginx.access", "apache.access") AND (host.name IN (<decoy_hosts>) OR url.path IN (<decoy_paths>))
| STATS first = MIN(@timestamp), last = MAX(@timestamp), count = COUNT(*), paths = COUNT_DISTINCT(url.path), uas = VALUES(user_agent.original), statuses = VALUES(http.response.status_code) BY source.ip
| EVAL seconds = DATE_DIFF("seconds", first, last)
artex-enrich/1.0 and norma/0.4 publishedGTIG / Mandiant — agentic offensive frameworks (primary) · CrowdStrike — VAPOR PANDA · Microsoft — AI adoption in DPRK tradecraft · BleepingComputer — WorkOS · BleepingComputer — Chainalysis · The Hacker News — backdoored ARTEX image, CERT-In · Check Point Research — Hacking on Autopilot: inside ARTEX cybercrime campaigns · Microsoft — agentic tooling finds and abuses exposed secrets · Microsoft — ARTEX abuse expands to the financial sector (Storm-3358) · Cutsign — Cairn (Oct 21) · Cutsign — agentic frameworks (all stories)