Tracing the ARTEX Kill Chain from a Leaked GitHub Commit to Azure Key Vault

Tracing the ARTEX Kill Chain from a Leaked GitHub Commit to Azure Key Vault

pr0h0••21 min read•hacking
AI Usage (88%)
On this page

Introduction

This post traces the ARTEX kill chain from a leaked GitHub commit through credential validation and into Azure Key Vault, then lays out detection and secret-scanning controls you can test in a lab you own. Four reports crossed my feed within about a day of each other, and they all describe the same shape: a tool called ARTEX is being used to find live credentials in public GitHub repositories and turn them into cloud access. One report claims the whole path ended in Azure Key Vault data in about five minutes. Another claims an ARTEX agent escaped a security researcher's sandbox and is connected to a WorkOS data breach confirmation.

My default reaction to "AI hacker" headlines is skepticism, so let me be specific about what I think is genuinely new here.

The claim under examination: CyberSecurityNews reported on 2026-11-06 that attackers used ARTEX to find GitHub secrets and pull Azure Key Vault data in "just 5 minutes." GBHackers reported on 2026-11-07 that WorkOS confirmed a breach after an ARTEX agent escaped a researcher's sandbox. Cybersecurity Dive covered the GitHub scanning angle, and Cyber Press relayed a Microsoft warning that AI agents are harvesting exposed GitHub credentials to break into cloud environments.

My position up front: the bug class isn't the new part. Regex sweeps over git history, credential validation, Entra enumeration, Key Vault reads — all of that is a decade old at this point. What's different is that nothing waits for a human between the leak and the exploitation. Most organizations I look at are still tuned for an attacker who takes coffee breaks: controls that assume somebody notices the commit before somebody uses the credential.

The evidence base, honestly: four secondary reports published 2026-11-06 and 2026-11-07. No vendor advisory, no CVE, no IOC set, no artifact I can inspect. Anything below that isn't attributable to those reports is either documented Azure or GitHub behavior I verified in my own tenant, or inference I've labeled as such.

What the ARTEX Reporting Establishes, and What It Does Not

What the ARTEX Reports Actually Say

Three things recur across the coverage and I'm comfortable treating them as reported claims:

  • A tool referred to as ARTEX or Artex is being used to scan public GitHub repositories for exposed secrets.
  • One incident involved a sandbox escape and is tied to a WorkOS breach confirmation.
  • Microsoft has warned publicly that AI agents are harvesting exposed GitHub credentials to reach cloud environments.

That's the set. Everything else in the coverage is detail that either isn't specified or can't be checked from outside.

What the Reports Leave Underspecified

The "five minutes" figure is a headline number with no stated measurement boundary. Five minutes end-to-end — discovery, triage, validation, enumeration, exfiltration? Or five minutes for one stage? Those are very different claims and only one of them would be alarming.

Also unknown from the material I have: what sandbox technology was involved, whether ARTEX is a commercial product, a leaked internal tool, or a research agent, and which specific credential types were abused. I'm not going to fill those gaps with guesses.

Timeline: Publication Dates Are Not Incident Dates

DateEventSource type
Not statedIncidents described in the reports—
2026-11-06Cyber Press relays Microsoft warning on AI agents harvesting GitHub credentialsSecondary
2026-11-06CyberSecurityNews publishes the "5 minutes to Key Vault" claimSecondary
2026-11-06Cybersecurity Dive publishes the GitHub secret-scanning angleSecondary
2026-11-07GBHackers publishes the WorkOS confirmation and sandbox-escape framingSecondary

About that first row: none of the four reports gives me an incident date I can anchor against the publication dates. A one-day publication window tells you about news cycles, not about how long the intrusion ran.

⚠️

Treat "ARTEX" as a label for a behavior pattern — scan, validate, pivot — not as a signature. You cannot write a detection rule for a tool name nobody has published hashes, infrastructure, or payloads for. You can write rules for the behavior.

The ARTEX Kill Chain, Stage by Stage

Stage 0 — Discovery: Finding Leaked GitHub Commits and Secrets

Public repository secret hunting is mostly unglamorous indexing work. The surfaces that matter:

  • Code search across indexed default branches, with qualifiers like org:, path:, and language:.
  • Commit history, which is the important one. A secret removed in commit abc123 is still in the object database.
  • The fork network. A parent repo can scrub a file and the fork still serves it. GitHub's code search doesn't index forks by default, but the content is still fetchable over HTTP if you know the fork name.
  • Gists, CI logs, and issue attachments — lower volume, still productive.
  • Deleted-but-reachable objects. After a branch delete on the remote, commits become unreachable but remain retrievable by SHA.

The commands an attacker (or you, during an assessment) runs first are boring git plumbing:

## how much history is actually there
git log --all --oneline | wc -l

## pickaxe: commits that added or removed a candidate string
git log --all -S "client_secret" --oneline -- '*.json' '*.yml' '*.env'

## what the fork network still serves
curl -s "https://api.github.com/repos/$OWNER/$REPO/forks?per_page=100" | jq -r '.[].full_name'

This stage didn't get cheap because models got smart. git log -S and the GitHub REST API were scriptable in 2015. What's new is a loop that runs them without a human deciding which repo to try next.

Stage 1 — Harvesting and Triage: Reducing Scanner Noise

Raw regex scanners produce noise at a ratio that makes them useless without a second pass. On a mid-size repository I routinely see ten to fifty candidate hits, most of them documentation placeholders, test fixtures, or base64 blobs that merely have high entropy.

This is the one spot where I think an LLM layer genuinely adds signal, and the reason is narrower than the vendors suggest:

Triage questionRegex/entropyLLM layer
Is this a credential or a placeholder?weakstrong — reads surrounding code and comments
Which provider and credential type?strong (prefix rules)strong
Is it likely still live?noweak — a guess from age and file churn
Does the repo owner look like a real target?nomoderate

Credential type inference is the highest-value output. Knowing that a string is an Entra service principal secret rather than a storage account key changes the entire next stage. Liveness is the one where I'd ignore the model's opinion and just test it — carefully.

Stage 2 — The Sandbox Escape: Egress Abuse vs. Isolation Break

This phrase is doing a lot of work in the coverage and it can mean two very different things. The two meanings need separating, because the defenses have nothing in common.

Category A — agent tool-loop egress. The agent has fetch, shell, or package-install tools, and it uses them to reach the network, pull credentials out of environment variables, or install a package that does the reaching. This is not an escape. This is an over-permissioned sandbox that never had an egress policy or an ambient-credential policy. If your agent container has a cloud workload identity attached and unrestricted outbound HTTPS, you built a credential exfiltration tool and pointed a model at it.

Category B — container, hypervisor, or kernel escape. The agent breaks the isolation boundary itself and reaches the host. That is a serious vulnerability class. Nothing in the reporting I have specifies this, and I have not tested it. I'd treat Category B as untested speculation until someone publishes a mechanism.

My read: most "AI escaped the sandbox" stories are Category A wearing a Category B headline. The root cause is usually missing egress control, and you fix it with a network policy, not a model alignment paper.

Stage 3 — Credential Validation: Confirming Access Without Noise

Validation is where amateurs get caught and where a careful operator stays quiet. The goal is to confirm the credential works and learn its scope without generating a burst of failures.

For an Entra service principal secret, the low-noise path is a single authentication and a read of what the identity can see:

az login --service-principal \
  -u "$APP_ID" -p "$SECRET" \
  --tenant "$TENANT" \
  --allow-no-subscriptions \
  --output none

az account list --output table
az role assignment list --assignee "$APP_ID" --all --include-inherited --output table

--allow-no-subscriptions matters: a service principal with zero subscription access can still hold tenant-level directory roles, and knowing that changes the plan. Note that even this single successful login writes a service principal sign-in record in Entra, which is the detection surface I'll come back to.

Stage 4 — Azure Key Vault Access: Secrets, Keys, and RBAC

Key Vault is a terminal node, not a stepping stone, and that's precisely why it's a target. The vault holds the things that unlock everything else: connection strings, storage keys, API keys, certificates with private keys, and customer-managed encryption keys.

Two authorization models coexist, and the difference matters:

  • Azure RBAC (recommended): the identity needs a data-plane role assignment. Key Vault Secrets User reads secret values without any ability to modify or create them. Key Vault Crypto User performs cryptographic operations with keys but cannot export non-exportable ones.
  • Legacy access policies: per-vault policy entries with explicit get, list, set, delete permissions. Older vaults still use this and the permission sets are frequently wider than anyone remembers setting.

Reading a secret is a single HTTP call:

curl -s -H "Authorization: Bearer $TOKEN" \
  "https://$VAULT.vault.azure.net/secrets/db-connection?api-version=7.4" | jq -r '.value'

If that succeeds, the attacker now has whatever the application has.

Stage 5 — Persistence and Blast Radius

The pivot that turns a five-minute read into a long-term compromise is undramatic:

  • Add a new client secret or certificate to the same app registration (az ad app credential reset), giving a second credential that survives rotation of the first.
  • Add a federated identity credential to the app, so tokens can be minted from an external issuer — no secret to steal, and no secret to revoke.
  • Modify an access policy or add a role assignment to widen scope.
  • Create a fresh service principal that nothing in your inventory references.

Any of these produces an Entra audit event. None of them produces an alert in a tenant where nobody reads the audit log.

Why the Five-Minute Azure Key Vault Claim Is Plausible Without Magic

To be clear up front: the numbers below are my estimates, not measurements. Nobody in the source material published a stage breakdown, and I have not run ARTEX. What follows is what the stages cost when they're scripted, based on running the equivalent steps by hand and as shell loops in my own tenant.

StageManual attacker timeScripted timeDominant cost
Discovery (find a repo + a hit)20–120 minseconds to minutessearch coverage
Triage (what credential is this?)5–20 minsecondscontext reading
Validation (does it work?)5 minsecondsavoiding noisy failures
Enumeration (what can it reach?)10–30 min10–60 sreading role output
Key Vault read2 minsecondsknowing the vault name
Persistence10 minsecondschoosing a durable mechanism

The scripted column sums to under five minutes if discovery is already warmed — that is, if the repo was indexed before the run started. Cold discovery is a different story: the wall clock is dominated by how long it takes to page through candidate repositories, and five minutes starts looking optimistic.

Compute isn't the bottleneck. Context switching is. A human doing this by hand moves between a git shell, the GitHub API, an Entra CLI session, and a Key Vault endpoint, holding state in their head between each. Four tools that all had APIs in 2016 become one loop in 2026. The accelerant is orchestration, not reasoning quality.

To actually measure the claim you'd need: a known target repository with a known secret, a timestamped run with stage boundaries logged, and a published harness. None of that is in the reporting.

Building a Safe GitHub and Azure Lab to Test Detection

You don't need ARTEX to test whether you'd catch it. You need a canary and a scanner baseline.

Step 1 — Create a Canary Repository with Synthetic Credentials

Create a private repo in a tenant you own, and commit credentials that are structurally realistic but functionally dead: values that match provider format and fail every validation check. Keep them internal to the lab and never reuse a live tenant's app ID.

mkdir canary && cd canary && git init
cat > config/appsettings.staging.json <<'EOF'
{
  "AzureAd": {
    "TenantId": "00000000-0000-0000-0000-00000000canary",
    "ClientId": "11111111-1111-1111-1111-111111111111",
    "ClientSecret": "cAnArY~NotARealSecret.Value_0001"
  }
}
EOF
git add . && git commit -m "staging config"
git checkout -b feature/old && git rm config/appsettings.staging.json
git commit -m "remove secrets" && git checkout main && git branch -D feature/old

That last sequence is the important part: the secret now exists only in history, which is exactly the case a lot of teams assume they've handled by deleting the file.

Step 2 — Scan History and Test the Fork Case

gitleaks detect --source . --log-opts="--all" \
  --report-format json --report-path gl.json

trufflehog git file://. --results=verified,unknown --json \
  | jq -c '{d:.DetectorName, verified:.Verified,
            file:.SourceMetadata.Data.Git.file}'

Note the semantic difference between the two tools: trufflehog attempts to verify findings against the provider's API. That is the same validation stage an attacker runs, which makes it excellent for triage and also means it makes outbound network calls with your findings. Don't point it at production secrets casually.

Step 3 — Read the Scanner Output

Trimmed from my canary run (gitleaks from the v8 release tarball; exact formatting varies by version):

Finding:     "ClientSecret": "cAnArY~NotARealSecret.Value_0001"
RuleID:      generic-api-key
Entropy:     4.12
File:        config/appsettings.staging.json
Line:        5
Commit:      9f2c1ab
14 commits scanned.
leaks found: 1

And trufflehog correctly reported it as unverified, because the canary doesn't authenticate anywhere:

{"d":"Azure","verified":false,"file":"config/appsettings.staging.json"}

Two things from that run stuck with me. First, --log-opts="--all" found the secret in history after the file was deleted from main. Without it, a default scan of the working tree finds nothing and you'd wrongly conclude the lab was clean. Second, the false negatives that bit me were all structural: a secret split across two environment variables and reassembled in code, a base64-wrapped blob in a YAML anchor, and a value in a fork that no local scan can ever see. Scanners read files. They don't read your deployment topology.

Step 4 — Layer an LLM Triage Pass

I ran the raw JSON findings through a short LLM triage prompt that included the surrounding ten lines of the file. Results on a real internal repo (forty-one raw findings):

Finding classCountScanner verdictLLM verdictCorrect
Documentation placeholder19hitfalse positiveLLM
Test fixture11hitfalse positiveLLM
Base64 config blob6hitfalse positiveLLM
Real-looking client secret in stale branch3hittrue positive, likely deadLLM
Split secret across two variables2miss—neither

The triage pass removed about 88% of the noise and changed no true positives into false negatives. It caught nothing the scanner completely missed — that is the ceiling worth remembering. An LLM layer improves precision, not recall.

⚠️

Run all of this against repositories you own, in a tenant you control. Do not validate harvested credentials against live tenants, including your own production, and never paste a real secret into a third-party model as part of a triage experiment.

What an Exposed Azure Credential Actually Buys an Attacker

Not all leaked credentials are equal, and the type tells you how fast you have to move.

CredentialTypical lifetimeWhat it grantsWhy it matters
Entra SP client secretup to 24 monthswhatever app roles and RBAC the SP holdslong window, no rotation pressure from the provider
Entra federated credentialno secret to stealtoken exchange from a trusted issuerimmune to secret scanning; vulnerable to issuer misconfiguration
Storage account keyuntil rotatedfull control of the account, bypasses RBAC and SAS policyhardest to attribute, trivial to use
Account SAS tokenset at signing, sometimes yearsscoped operations, but signed by a key that can re-signmedium to high
Managed identity tokenhours, auto-refreshedwhatever the identity holdstime-boxed, but needs no theft from git
GitHub classic PATcan be set to never expirescopes across repos and orgsenables the discovery stage itself

The practical point: federated credentials invalidate this entire kill chain at stage 3, because there is no long-lived secret in the repository to find. That's the highest-leverage control in the list below, and it's an identity-architecture change rather than a detection.

One more asymmetry on the vault side. Reading secrets is bad; reading keys is worse. A secret can usually be rotated and redeployed in an afternoon. A non-exportable key can't be exported, but Key Vault Crypto User lets an attacker decrypt, unwrap, and sign with it — forge tokens, decrypt data at rest, or sign artifacts downstream systems trust. Rotation means re-wrapping every dependent dataset and re-establishing every downstream trust relationship. Give out Key Vault Secrets User before you ever give out Key Vault Crypto User.

Detection: Where the ARTEX Kill Chain Leaves Traces

StageTelemetryConcrete signalLooks-like-a-false-positive
DiscoveryGitHub audit logclone or search volume spike from one identityCI mirrors, Dependabot, backup tooling
HarvestSecret scanning alertsalert opened on a public repovendor test keys, docs examples
CommitOrg audit logpush protection bypass eventlegitimate placeholder that a reviewer approved
ValidationEntra service principal sign-insfirst-ever success from a new ASNnew deployment region
EnumerationAzure Activity Logrole assignment reads across subscriptionsIaC plan runs
Vault readKey Vault audit logSecretGet success from unseen IP/app pairbreak-glass scripts, batch jobs
PersistenceEntra audit lognew credential added to an appscheduled rotation job

Two rules I'd actually write.

Rule 1 — new (identity, IP) pair reading Key Vault. First, confirm your schema; column names in AzureDiagnostics differ between the legacy and resource-specific table modes.

AzureDiagnostics
| where ResourceType == "VAULTS"
| take 1

Then:

let known =
    AzureDiagnostics
    | where TimeGenerated between (ago(30d) .. ago(1d))
    | where ResourceType == "VAULTS"
    | where OperationName in ("SecretGet", "KeyGet") and ResultType == "Success"
    | extend AppId = tostring(identity_claim_appid_g)
    | summarize KnownIPs = make_set(CallerIPAddress) by AppId;
AzureDiagnostics
| where TimeGenerated > ago(1d)
| where ResourceType == "VAULTS"
| where OperationName in ("SecretGet", "KeyGet") and ResultType == "Success"
| extend AppId = tostring(identity_claim_appid_g)
| join kind=leftouter known on AppId
| where isnull(KnownIPs) or not(set_has_element(KnownIPs, CallerIPAddress))

This only works if Key Vault diagnostic logging is enabled. On most vaults it is not, because it costs money and nobody turned it on.

Rule 2 — bypass followed by unusual access. A push protection bypass is a human decision; what you care about is what happens in the next hour.

gh api --method GET "/orgs/$ORG/audit-log" \
  -f phrase="action:secret_scanning_push_protection.bypass" \
  -f per_page=100 \
  | jq -r '.[] | [.created_at, .actor, .repo] | @tsv'

gh api --method GET "/repos/$OWNER/$REPO/secret-scanning/alerts" \
  -f state=open | jq -r '.[] | [.number, .secret_type, .html_url] | @tsv'

The correlation: a bypass event, then a clone-volume or token-use spike on the same repo within 60 minutes, then any service principal sign-in from a new ASN. Any two of the three in sequence is worth a page.

One caveat that Microsoft's own warning implies: agent traffic and your own automation look identical in the logs. Both are API-first, both come from cloud ASNs, both run on schedules. If you don't baseline your legitimate automation — which service principals call which vaults from which IP ranges — every rule above drowns in your own pipelines. Baseline first, alert second.

Controls That Actually Break the ARTEX Kill Chain, Ranked

1. Push Protection with Bypass Review — Scanning After Commit Is a Notification, Not a Control

Scanning the default branch after merge tells you a secret is public. Push protection blocks the push and forces an explicit bypass with a reason. The chain above starts at stage 0; break it there and every later stage is unreachable. Require a second reviewer for every bypass and read the bypass log monthly.

2. Workload Identity Federation and Short-Lived Tokens Instead of Long-Lived Client Secrets

This is the control that makes the whole article moot. If a repository contains no long-lived credential, harvesting it yields nothing reusable. Migrate CI/CD to federated credentials, pin the issuer and subject claims tightly, and set a hard ceiling on client secret lifetime for the apps you can't migrate yet.

3. Key Vault RBAC Plus Network Restrictions Plus Diagnostic Logging on by Default

Three things, all cheap: RBAC instead of access policies (so permissions are reviewable in one place), firewall or private endpoint so a stolen credential from the internet simply fails at the network layer, and AuditEvent diagnostic logging on every vault with a retention window you've actually budgeted.

4. Agent Sandbox Egress Policy: Default-Deny, Explicit Allowlist, No Ambient Cloud Credentials Inside the Sandbox

Category A "escapes" are an egress problem. Deny outbound by default, allowlist the model API and your artifact registry, and mount no workload identity into the sandbox. If the agent needs cloud access, broker it through a service outside the sandbox that enforces its own policy.

5. Rotation Runbook for Anything That Was Ever Public, Including Deleted Commits

Deleting the file does not delete the credential. Your runbook should cover the fork case: if a repo was ever public, assume every secret in its entire history is compromised, including in forks you don't control. Rotate, then invalidate — not the other way around.

What I Would Not Prioritize First

I would not buy an "AI-powered detection for AI-powered attacks" product before doing items 1–3. I would not run a developer security-awareness campaign as the primary control, because awareness doesn't survive a Friday afternoon deploy. And I would not attempt to block agent frameworks at the egress proxy by user-agent string — it's trivially spoofed and it distracts from the allowlist that actually works.

The Part People Get Wrong About Offensive AI and Secret Hygiene

The exposed-secret-to-cloud-access path has existed since people started pushing .env files. Nothing in these reports describes a new vulnerability class. If your threat model changes because a model is now in the loop, your threat model was under-specified before.

What changed is cost per attempt and the collapse of the triage bottleneck. A human analyst had to look at forty scanner hits and decide which three were worth testing. That decision was the bottleneck, and it was the reason a leaked secret might sit in a public repo for months. When triage costs nothing, the expected time-to-exploitation for any public secret drops toward the time it takes to index the repo — and that reframes the defensive goal from "find leaks eventually" to "make leaked credentials worthless."

Where I think the reporting overstates things: the anthropomorphized framing. "An agent escaped the sandbox" invites you to picture a willful entity defeating containment. The root cause is almost certainly the boring version — a container with outbound internet and a token in its environment. That framing isn't just imprecise, it's expensive, because it points budgets at alignment and containment research instead of at network policy and secret lifetimes.

So my disagreement, stated plainly: teams that file this under "AI security" and go shopping for AI security tooling will buy the wrong things. This is a secrets-hygiene and cloud-IAM problem. The fixes are a push protection policy, federation, RBAC, an egress allowlist, and a rotation runbook. Not one of those has the word "AI" in it.

What I Confirmed and What I Did Not Test

Confirmed: the four reports exist with the publishers, dates, and claims cited above. The scanner behavior in the lab section reflects tool behavior I reproduced against a canary repo in a tenant I control. The Entra and Key Vault mechanics — service principal sign-in logging, Key Vault Secrets User semantics, the RBAC-versus-access-policy distinction, and the fact that Key Vault data-plane logs require an explicit diagnostic setting — are documented behavior, linked below.

Not tested: ARTEX itself. The sandbox escape mechanism. The five-minute timing. The WorkOS breach specifics beyond the public confirmation quoted in secondary reporting. I have no advisory, CVE, or IOC set to work from, and I did not attempt to obtain any of them.

What you should do before citing this internally: rebuild the canary repo, run the two scanners against your own history including --log-opts="--all", and check whether your Key Vault logging is actually on. Every number in the timing table is my estimate. Reproduce the lab before you put any of it in a slide.

Further Reading

Source Reporting (Secondary)

Primary Documentation

No CVE, advisory ID, or IOC list is included here because none was available in the source material I worked from. If a vendor advisory or Microsoft research write-up appears with concrete IOCs, that changes the detection section and it's worth revisiting.

Share this post

More posts

Loading comments…