actions-cool GitHub Actions Tag Hijack Credential Theft
GitHub Action tags for actions-cool/issues-helper and actions-cool/maintain-one-comment were moved to imposter commits that scraped GitHub Actions runner memory and exfiltrated CI/CD secrets. StepSecurity's incident center now preserves the two-action scope and shared C2 linkage.
On this page 0% read
Executive Summary
StepSecurity reported that actions-cool/issues-helper and actions-cool/maintain-one-comment had all reviewed tags moved to imposter commits on 2026-05-18. Workflows referencing those tags could execute attacker-controlled action code while the workflow file still appeared to use a familiar third-party action StepSecurity.
The payload used Bun/JavaScript and Python to inspect the GitHub Actions Runner.Worker process memory, extract decrypted secrets, and exfiltrate them to t[.]m-kosche[.]com. Any repository that ran affected action tags during the exposure window should rotate GitHub, cloud, package-registry, deployment, and OIDC-related credentials reachable by those workflows StepSecurity.
Freshness check on 2026-05-27: StepSecurity’s incident center now records the incident as affecting both actions-cool/issues-helper and actions-cool/maintain-one-comment, with all tags across both actions poisoned simultaneously and shared t[.]m-kosche[.]com infrastructure tying the incident to the Mini Shai-Hulud wave StepSecurity Incident Center.
Key Facts
threat_type: "GitHub Action tag hijack and CI credential theft"
ecosystem: "GitHub Actions"
registry: "GitHub repositories and action tags"
affected_packages:
- "actions-cool/issues-helper"
- "actions-cool/maintain-one-comment"
malicious_versions:
- "actions-cool/issues-helper@v1"
- "actions-cool/[email protected]"
- "actions-cool/[email protected]"
- "actions-cool/[email protected]"
- "actions-cool/[email protected]"
- "actions-cool/[email protected]"
- "actions-cool/[email protected]"
- "actions-cool/[email protected]"
- "actions-cool/[email protected]"
- "actions-cool/[email protected]"
- "actions-cool/[email protected]"
- "actions-cool/[email protected]"
- "actions-cool/[email protected]"
- "actions-cool/[email protected]"
- "actions-cool/[email protected]"
- "actions-cool/[email protected]"
- "actions-cool/[email protected]"
- "actions-cool/[email protected]"
- "actions-cool/[email protected]"
- "actions-cool/[email protected]"
- "actions-cool/[email protected]"
- "actions-cool/[email protected]"
- "actions-cool/[email protected]"
- "actions-cool/[email protected]"
- "actions-cool/[email protected]"
- "actions-cool/[email protected]"
- "actions-cool/[email protected]"
- "actions-cool/[email protected]"
- "actions-cool/[email protected]"
- "actions-cool/[email protected]"
- "actions-cool/[email protected]"
- "actions-cool/[email protected]"
- "actions-cool/[email protected]"
- "actions-cool/[email protected]"
- "actions-cool/[email protected]"
- "actions-cool/[email protected]"
- "actions-cool/[email protected]"
- "actions-cool/[email protected]"
- "actions-cool/[email protected]"
- "actions-cool/issues-helper@v2"
- "actions-cool/[email protected]"
- "actions-cool/[email protected]"
- "actions-cool/[email protected]"
- "actions-cool/[email protected]"
- "actions-cool/[email protected]"
- "actions-cool/[email protected]"
- "actions-cool/issues-helper@v3"
- "actions-cool/[email protected]"
- "actions-cool/[email protected]"
- "actions-cool/[email protected]"
- "actions-cool/[email protected]"
- "actions-cool/maintain-one-comment@v1"
- "actions-cool/[email protected]"
- "actions-cool/[email protected]"
- "actions-cool/[email protected]"
- "actions-cool/[email protected]"
- "actions-cool/maintain-one-comment@v2"
- "actions-cool/[email protected]"
- "actions-cool/[email protected]"
- "actions-cool/[email protected]"
- "actions-cool/[email protected]"
- "actions-cool/maintain-one-comment@v3"
- "actions-cool/[email protected]"
- "actions-cool/[email protected]"
- "actions-cool/[email protected]"
- "actions-cool/[email protected]"
known_good_versions: []
fixed_or_safe_versions: []
execution_trigger: "GitHub Actions workflow referencing a hijacked action tag"
primary_impact: "GitHub Actions secret theft from runner process memory"
campaign_context: "Part of the May 2026 wave targeting CI/CD trust anchors and mutable tags."
confidence: "high"
canonical_source: "https://www.stepsecurity.io/blog/actions-cool-issues-helper-github-action-compromised-all-tags-point-to-imposter-commit-that-exfiltrates-ci-cd-credentials"
last_verified: "2026-05-24"
Source Confidence & Evidence Mapping
- confirmed: StepSecurity reports 53
actions-cool/issues-helpertags and 15actions-cool/maintain-one-commenttags pointing to imposter commits StepSecurity. - confirmed: The imposter commits were not reachable from the default branch, making tag target reachability a useful detection signal StepSecurity.
- confirmed: The payload attempted to read
Runner.Workermemory and exfiltrate secrets tot[.]m-kosche[.]comStepSecurity. - confirmed: StepSecurity’s incident center preserves the second affected action and states that all tags across both actions were poisoned simultaneously StepSecurity Incident Center.
- unclear: Public reporting does not establish which downstream organizations had successful secret exfiltration.
Impact Determination
| Classification | Criteria | Required evidence | Required action | Closure condition |
|---|---|---|---|---|
| Confirmed compromise | A workflow run resolved one of the affected action tags during the reported window and runner telemetry shows the exfiltration domain, memory scraping pattern, or malicious commit SHA. | Workflow run metadata, resolved action SHA, runner process or network telemetry, and preserved job logs. | Disable the workflow path, isolate self-hosted runners, and rotate every secret available to the run from a clean environment. | No affected tag references remain, all exposed credentials are replaced, and runner telemetry is preserved. |
| Presumed exposed | A workflow used the affected actions by mutable tag during the window and the run had secrets, GITHUB_TOKEN, id-token: write, or deployment credentials available. | Workflow YAML, run start time, job permissions, environment assignment, and secret or OIDC availability. | Rotate reachable GitHub, cloud, registry, and deployment credentials even without confirmed egress. | Credential owners confirm replacement and downstream audit modules show no follow-on abuse. |
| Potentially exposed | Repository workflow history contains the affected actions but execution timing, resolved SHA, or secret availability is incomplete. | Current and historical workflow files, Actions run list, commit history, and organization audit log extracts. | Collect missing run evidence and temporarily block the action tags while scoping. | The missing run and permission evidence resolves to presumed exposed, confirmed compromise, or not exposed. |
| Not exposed | No workflow referenced the affected actions during the window, or every run was pinned to a verified full commit SHA outside the imposter commits and had no secret-bearing execution. | Repository-wide workflow search, run history, resolved SHAs, and commit verification. | Document the negative result and keep action SHA-pinning controls in place. | A reproducible repository search and run export are attached to the incident record. |
| Unknown | Actions history, resolved SHA data, or runner telemetry is unavailable. | Gap statement naming the missing repositories, runs, or log sources. | Treat credential exposure as unresolved until repository owners or platform logs answer the gap. | Evidence is recovered or risk owners accept residual uncertainty after rotation decisions. |
Minimum Evidence To Collect
minimum_evidence:
- "Repository list and workflow files containing actions-cool/issues-helper or actions-cool/maintain-one-comment."
- "GitHub Actions run metadata from 2026-05-18T19:00:00Z through confirmed cleanup."
- "Resolved action SHA for each affected run and whether the reference was a mutable tag."
- "Job permissions, environment, secret availability, and id-token setting for each run."
- "Runner DNS, proxy, firewall, or EDR telemetry for t[.]m-kosche[.]com."
Timeline
- 2026-05-18T19:10:24Z StepSecurity reports the
actions-cool/issues-helperimposter-commit window beginning; affected tags were moved within minutes StepSecurity. - 2026-05-18T19:30:30Z StepSecurity reports the
actions-cool/maintain-one-commentimposter-commit window beginning StepSecurity. - 2026-05-19 StepSecurity publishes the public technical report StepSecurity.
- 2026-05-24 This local feed split creates a standalone actions-cool article instead of grouping it into a weekly roundup.
What Happened
The attacker moved GitHub Action tags to imposter commits. Workflows using tag references such as @vX or other mutable tags could execute malicious action code without changing the victim repository’s workflow file. This is the same class of trust failure as package tag rewrites, but the execution environment is CI/CD.
StepSecurity’s analysis showed that the imposter action code tried to scrape secrets from the runner process itself. That is significant because GitHub Actions masks secrets in logs, but the runner must still hold usable values in memory while jobs execute.
Technical Analysis
sequenceDiagram
autonumber
actor Attacker
participant GH as GitHub Action Registry
participant Runner as GitHub Runner (Runner.Worker)
participant C2 as Attacker C2 (t.m-kosche.com)
Attacker->>GH: Hijack/force-push mutable action tag (e.g., @v3) to imposter commit
Note over GH: Tag resolved by victim workflow runs during compromise window
Runner->>GH: Trigger job & fetch action: actions-cool/issues-helper@v3
GH-->>Runner: Return tampered action code (imposter commit payload)
Note over Runner: Execute tampered action (Bun/JavaScript + Python)
Runner->>Runner: Scrape process memory (/proc/*/mem) of Runner.Worker for secrets
Runner->>C2: Exfiltrate harvested GitHub/OIDC tokens and secrets
Initial Access
The public report proves tag movement and imposter commits but does not establish the exact credential or account takeover path that allowed tag manipulation.
Package or Artifact Tampering
The tampered artifacts were Git refs: all reported action tags pointed to commits that were not part of the legitimate default-branch history. Compare action tag targets against known-good commit SHAs and default-branch reachability StepSecurity.
Execution Trigger
Execution occurs when a GitHub Actions workflow uses the affected action by tag. No package install is required beyond normal action resolution.
Payload Behavior
The payload used Bun/JavaScript and Python to identify the Runner.Worker process and read process memory. It searched for secret material available to the job and prepared it for exfiltration StepSecurity.
Exfiltration / C2
The reported exfiltration domain is t[.]m-kosche[.]com. Any outbound traffic from GitHub Actions runners to this domain during affected workflow runs should be treated as a credential-loss event.
Propagation
No autonomous propagation is reported. The blast radius is every repository and workflow that referenced the hijacked tags during the compromise window.
Obfuscation or Evasion
The primary evasion is trust indirection. The workflow file still names the expected action, but the tag target changed underneath it. The malicious commits not being reachable from the default branch provides a strong detection heuristic.
Affected Assets and Blast Radius
affected_assets:
ecosystems:
- "GitHub Actions"
packages:
- "actions-cool/issues-helper"
- "actions-cool/maintain-one-comment"
versions:
- "53 issues-helper tags reported by StepSecurity"
- "15 maintain-one-comment tags reported by StepSecurity"
repositories:
- "actions-cool/issues-helper"
- "actions-cool/maintain-one-comment"
ci_cd_systems:
- "GitHub Actions"
container_images: []
developer_tools:
- "GitHub Actions workflows"
credentials_at_risk:
- "GitHub Actions secrets"
- "GitHub tokens"
- "OIDC tokens"
- "cloud credentials"
- "package registry credentials"
- "deployment credentials"
not_currently_known_to_affect:
- "Workflows pinned to verified full commit SHAs outside the imposter commits."
Indicators of Compromise
package_versions:
- "actions-cool/issues-helper affected tags"
- "actions-cool/maintain-one-comment affected tags"
files:
- ".github/workflows/*.yml"
hashes:
- "8064d4e0322f069b3dba13e7957ff0ca7dab7984"
- "6e79ae622b7ef30f31fdbcc2dc65339e"
domains:
- "t[.]m-kosche[.]com"
urls: []
ips: []
process_patterns:
- "python3 reading /proc/<Runner.Worker PID>/mem"
- "bun executing unexpected action code"
network_patterns:
- "POST or HTTPS traffic from GitHub Actions runner to t[.]m-kosche[.]com"
provenance_signals:
- "GitHub Action tag target not reachable from default branch"
- "actions-cool tag target changed around 2026-05-18T19:10:24Z or 2026-05-18T19:30:30Z"
Detection and Hunting
Script: local repository and exported telemetry scope
#!/usr/bin/env python3
import os
import sys
import json
import subprocess
from pathlib import Path
ROOT = sys.argv[1] if len(sys.argv) > 1 else "."
LOG_ROOT = os.environ.get("LOG_ROOT", "")
OUT = Path(os.environ.get("OUT", "hp-actions-cool-github-actions-tag-hijack-scope"))
SINCE = "2026-05-18T19:00:00Z"
UNTIL = "2026-05-24T23:59:59Z"
PACKAGES = [
"actions-cool/issues-helper",
"actions-cool/maintain-one-comment",
]
VERSIONS = [
"actions-cool/issues-helper@v1",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/issues-helper@v2",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/issues-helper@v3",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/maintain-one-comment@v1",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/maintain-one-comment@v2",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/maintain-one-comment@v3",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/issues-helper affected tags",
"actions-cool/maintain-one-comment affected tags",
]
FILES = [
".github/workflows/*.yml",
]
DOMAINS = [
"t.m-kosche.com",
]
URLS = [
]
IPS = [
]
HASHES = [
"8064d4e0322f069b3dba13e7957ff0ca7dab7984",
"6e79ae622b7ef30f31fdbcc2dc65339e",
]
PROCESS_PATTERNS = [
"python3 reading /proc//mem",
"bun executing unexpected action code",
]
NETWORK_PATTERNS = [
"POST or HTTPS traffic from GitHub Actions runner to t.m-kosche.com",
]
# Positive signal: repository, lockfile, artifact, process, or network telemetry contains one of the exact incident selectors above.
# Escalation: any match tied to a production build, CI run, deployed asset, or secret-bearing host moves the asset to presumed exposed.
OUT.mkdir(parents=True, exist_ok=True)
indicators_file = OUT / "indicators.txt"
# Collect unique indicators
indicators = set()
for group in [PACKAGES, VERSIONS, FILES, DOMAINS, URLS, IPS, HASHES, PROCESS_PATTERNS, NETWORK_PATTERNS]:
for val in group:
if val:
indicators.add(val)
with open(indicators_file, "w") as f:
for ind in sorted(indicators):
f.write(ind + "\n")
print(f"[+] Written unique selectors to {indicators_file}")
# Walk local directory
print(f"[+] Scanning directory: {ROOT} for selectors...")
matches = []
exclude_dirs = {"node_modules", "vendor", "dist", ".git"}
for root, dirs, filenames in os.walk(ROOT):
dirs[:] = [d for d in dirs if d not in exclude_dirs]
for filename in filenames:
filepath = Path(root) / filename
try:
content = filepath.read_text(errors="ignore")
for ind in indicators:
if ind in content:
matches.append(f"{filepath}: found '{ind}'")
except Exception:
pass
if matches:
(OUT / "repository-indicator-matches.txt").write_text("\n".join(matches) + "\n")
print(f"[!] Found {len(matches)} matches in codebase!")
# Optional Log Scanning
if LOG_ROOT and os.path.exists(LOG_ROOT):
print(f"[+] Scanning telemetry log directory: {LOG_ROOT}...")
log_matches = []
for root, _, filenames in os.walk(LOG_ROOT):
for filename in filenames:
filepath = Path(root) / filename
try:
content = filepath.read_text(errors="ignore")
for ind in indicators:
if ind in content:
log_matches.append(f"{filepath}: found '{ind}'")
except Exception:
pass
if log_matches:
(OUT / "exported-telemetry-indicator-matches.txt").write_text("\n".join(log_matches) + "\n")
print(f"[!] Found {len(log_matches)} matches in logs!")
if PACKAGES:
registry_dir = OUT / "registry"
registry_dir.mkdir(exist_ok=True)
print(f"[+] Wrote scope artifacts under {OUT}")
Downstream Abuse Audits
Script: GitHub organization run, release, secret, and workflow audit
#!/usr/bin/env python3
import os
import sys
import json
import subprocess
from pathlib import Path
if "ORG" not in os.environ:
print("ERROR: Set ORG environment variable to the GitHub organization to audit", file=sys.stderr)
sys.exit(1)
ORG = os.environ["ORG"]
SINCE = "2026-05-18T19:00:00Z"
UNTIL = "2026-05-24T23:59:59Z"
OUT = Path(os.environ.get("OUT", "hp-actions-cool-github-actions-tag-hijack-github-audit"))
SELECTORS = [
"actions-cool/issues-helper",
"actions-cool/maintain-one-comment",
"actions-cool/issues-helper@v1",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/issues-helper@v2",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/issues-helper@v3",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/maintain-one-comment@v1",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/maintain-one-comment@v2",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/maintain-one-comment@v3",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/issues-helper affected tags",
"actions-cool/maintain-one-comment affected tags",
".github/workflows/*.yml",
"t.m-kosche.com",
"8064d4e0322f069b3dba13e7957ff0ca7dab7984",
"6e79ae622b7ef30f31fdbcc2dc65339e",
]
# Positive signal: a workflow run, release, secret, key, package, or workflow change overlaps the exposure window and references an incident selector.
# Remediation trigger: unauthorized post-exposure write activity or a secret-bearing run matching an incident selector requires token revocation and downstream cloud/registry review.
OUT.mkdir(parents=True, exist_ok=True)
(OUT / "runs").mkdir(exist_ok=True)
(OUT / "logs").mkdir(exist_ok=True)
(OUT / "repos").mkdir(exist_ok=True)
# 1. Write incident-selectors file
selectors_file = OUT / "incident-selectors.txt"
with open(selectors_file, "w") as sf:
for s in SELECTORS:
if s:
sf.write(s + "\n")
# 2. Get list of repos
print(f"[+] Fetching repositories for organization: {ORG}")
repo_res = subprocess.run(["gh", "repo", "list", ORG, "--limit", "1000", "--json", "nameWithOwner"], capture_output=True, text=True)
if repo_res.returncode != 0:
print(f"[-] Failed to fetch repos: {repo_res.stderr}", file=sys.stderr)
sys.exit(1)
repos = [r["nameWithOwner"] for r in json.loads(repo_res.stdout)]
for repo in repos:
safe_repo = repo.replace("/", "__")
print(f"[+] Auditing repository: {repo}")
# Check runs in the window
runs_res = subprocess.run([
"gh", "api", f"/repos/{repo}/actions/runs",
"-f", "per_page=100",
"-f", f"created=>={SINCE}",
"--paginate"
], capture_output=True, text=True)
if runs_res.returncode == 0:
try:
all_runs = json.loads(runs_res.stdout).get("workflow_runs", [])
filtered_runs = [r for r in all_runs if r["created_at"] <= UNTIL]
if filtered_runs:
with open(OUT / "runs" / f"{safe_repo}-runs.jsonl", "w") as rf:
for run in filtered_runs:
rf.write(json.dumps(run) + "\n")
# Fetch log dynamically
run_id = str(run["id"])
log_res = subprocess.run(["gh", "run", "view", run_id, "--repo", repo, "--log"], capture_output=True, text=True)
if log_res.returncode == 0:
(OUT / "logs" / f"{safe_repo}-{run_id}.log").write_text(log_res.stdout)
# Fetch details
view_res = subprocess.run(["gh", "run", "view", run_id, "--repo", repo, "--json", "databaseId,workflowName,headSha,event,createdAt,jobs"], capture_output=True, text=True)
if view_res.returncode == 0:
(OUT / "runs" / f"{safe_repo}-{run_id}.json").write_text(view_res.stdout)
except Exception as e:
print(f"[-] Error parsing runs for {repo}: {e}")
# Check releases in window
subprocess.run(["gh", "api", f"/repos/{repo}/releases", "-f", "per_page=100", "--paginate"], capture_output=True)
# Check repo secrets updated in window
subprocess.run(["gh", "api", f"/repos/{repo}/actions/secrets", "-f", "per_page=100", "--paginate"], capture_output=True)
# Check deploy keys
subprocess.run(["gh", "api", f"/repos/{repo}/keys", "-f", "per_page=100", "--paginate"], capture_output=True)
# Scan output directory for any indicator selector matches
print("[+] Scanning gathered telemetry for indicator matches...")
subprocess.run(["rg", "-n", "--hidden", "--fixed-strings", "-f", str(selectors_file), str(OUT)], capture_output=False)
print(f"[+] Wrote GitHub audit artifacts under {OUT}")
Script: cloud OIDC and deployment credential follow-on audit
#!/usr/bin/env python3
import os
import json
import subprocess
from pathlib import Path
SINCE = "2026-05-18T19:00:00Z"
UNTIL = "2026-05-24T23:59:59Z"
OUT = Path(os.environ.get("OUT", "hp-actions-cool-github-actions-tag-hijack-cloud-audit"))
AWS_REGIONS = os.environ.get("AWS_REGIONS", "us-east-1").split(",")
# Positive signal: token exchange or privileged write activity occurs in the exposure window from GitHub, CI/CD, package registry, or deployment automation identity.
# Remediation trigger: unexpected write, deploy, IAM, secret, or registry activity tied to an exposed CI/CD path requires trust-policy disablement and credential rotation.
OUT.mkdir(parents=True, exist_ok=True)
# 1. AWS CloudTrail Audit
print("[+] Querying AWS CloudTrail for Web Identity token exchanges...")
aws_events = []
for region in AWS_REGIONS:
res = subprocess.run([
"aws", "cloudtrail", "lookup-events",
"--region", region,
"--start-time", SINCE,
"--end-time", UNTIL,
"--lookup-attributes", "AttributeKey=EventName,AttributeValue=AssumeRoleWithWebIdentity",
"--output", "json"
], capture_output=True, text=True)
if res.returncode == 0:
try:
events = json.loads(res.stdout).get("Events", [])
for event in events:
ct = json.loads(event.get("CloudTrailEvent", "{}"))
ct["region"] = region
aws_events.append(ct)
except Exception as e:
print(f"[-] Error parsing AWS CloudTrail events: {e}")
if aws_events:
with open(OUT / "aws-assume-role-with-web-identity.jsonl", "w") as f:
for ev in aws_events:
f.write(json.dumps(ev) + "\n")
# Audit follow-on events for returned access keys
for ev in aws_events:
access_key = ev.get("responseElements", {}).get("credentials", {}).get("accessKeyId")
region = ev.get("region", "us-east-1")
if access_key:
print(f"[+] Enumerating AWS events for AccessKey: {access_key}")
f_res = subprocess.run([
"aws", "cloudtrail", "lookup-events",
"--region", region,
"--start-time", SINCE,
"--end-time", UNTIL,
"--lookup-attributes", f"AttributeKey=AccessKeyId,AttributeValue={access_key}",
"--output", "json"
], capture_output=True, text=True)
if f_res.returncode == 0:
try:
f_events = json.loads(f_res.stdout).get("Events", [])
with open(OUT / "aws-follow-on-api-calls.jsonl", "a") as ff:
for fe in f_events:
ff.write(fe.get("CloudTrailEvent", "{}") + "\n")
except Exception as e:
print(f"[-] Error writing follow-on events: {e}")
# 2. Azure Activity Log Audit
print("[+] Querying Azure activity logs...")
az_res = subprocess.run([
"az", "monitor", "activity-log", "list",
"--start-time", SINCE,
"--end-time", UNTIL,
"--query", "[?contains(operationName.value, 'write') || contains(operationName.value, 'delete') || contains(operationName.value, 'Microsoft.ManagedIdentity')]",
"-o", "json"
], capture_output=True, text=True)
if az_res.returncode == 0:
(OUT / "azure-write-delete-activity.json").write_text(az_res.stdout)
# 3. GCP Logging Audit
print("[+] Querying GCP Cloud Logging...")
gcp_filter = f'timestamp>="{SINCE}" AND timestamp<="{UNTIL}" AND (protoPayload.methodName="google.sts.v1.SecurityTokenService.ExchangeToken" OR protoPayload.methodName:"GenerateAccessToken" OR protoPayload.methodName:"CreateServiceAccountKey" OR protoPayload.methodName:"SetIamPolicy")'
gcp_res = subprocess.run([
"gcloud", "logging", "read", gcp_filter,
"--format", "json"
], capture_output=True, text=True)
if gcp_res.returncode == 0:
(OUT / "gcp-token-and-iam-activity.json").write_text(gcp_res.stdout)
print(f"[+] Wrote cloud audit artifacts under {OUT}")
Script: registry metadata and artifact audit
#!/usr/bin/env python3
import os
import json
import subprocess
from pathlib import Path
SINCE = "2026-05-18T19:00:00Z"
OUT = Path(os.environ.get("OUT", "hp-actions-cool-github-actions-tag-hijack-registry-audit"))
PACKAGES = [
"actions-cool/issues-helper",
"actions-cool/maintain-one-comment",
]
VERSIONS = [
"actions-cool/issues-helper@v1",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/issues-helper@v2",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/issues-helper@v3",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/maintain-one-comment@v1",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/maintain-one-comment@v2",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/maintain-one-comment@v3",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/[email protected]",
"actions-cool/issues-helper affected tags",
"actions-cool/maintain-one-comment affected tags",
]
# Positive signal: workflow files or extensions reference the affected action/extension names or versions.
# Remediation trigger: exposed secrets or OIDC federation policies must be immediately rotated.
OUT.mkdir(parents=True, exist_ok=True)
with open(OUT / "affected-versions.txt", "w") as av:
for version in VERSIONS:
if version:
av.write(version + "\n")
# 1. Search local workspace files for the affected actions/extensions
print("[+] Scanning workspace workflows for selectors...")
for file in Path(".").glob(".github/workflows/**/*.yml"):
subprocess.run(["rg", "-n", "--hidden", "--fixed-strings", "-f", str(OUT / "affected-versions.txt"), str(file)])
# 2. HOW TO ROTATE EXPOSED GITHUB ACTIONS SECRETS:
# Overwrite compromised secrets with newly generated credentials:
# subprocess.run(["gh", "secret", "set", "COMPROMISED_SECRET_NAME", "--body", "my-new-secret-value", "--repo", "my-org/my-repo"])
# For organization-level secrets:
# subprocess.run(["gh", "secret", "set", "COMPROMISED_SECRET_NAME", "--org", "my-org", "--visibility", "private"])
# Revoke compromised OIDC federated trust credentials in AWS/GCP and redeploy the IAM trust policy:
# subprocess.run(["aws", "iam", "update-assume-role-policy", "--role-name", "my-role-name", "--policy-document", "file://new-clean-trust-policy.json"])
print(f"[+] Wrote registry audit artifacts under {OUT}")
Sources
- StepSecurity: actions-cool/issues-helper GitHub Action Compromised - Role: PRIMARY_RESEARCH - Impact: Documents affected actions, tag hijack timing, imposter commits, runner memory scraping, exfiltration domain, and detections.
- StepSecurity Incident Center: CI/CD Incidents - Role: PRIMARY_RESEARCH - Impact: Confirms incident-center scope for both affected actions, simultaneous all-tag poisoning, and shared TeamPCP/Mini Shai-Hulud infrastructure.
IOC Clipboard
4 IOCst.m-kosche.com t[.]m-kosche[.]com 8064d4e0322f069b3dba13e7957ff0ca7dab7984 8064d4e0322f069b3dba13e7957ff0ca7dab7984 6e79ae622b7ef30f31fdbcc2dc65339e 6e79ae622b7ef30f31fdbcc2dc65339e .github/workflows/*.yml .github/workflows/*.yml