critical Threat analysis

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.

#supply-chain#github-actions#ci-cd#credential-theft#tag-hijack
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-helper tags and 15 actions-cool/maintain-one-comment tags 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.Worker memory and exfiltrate secrets to t[.]m-kosche[.]com StepSecurity.
    • 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

    ClassificationCriteriaRequired evidenceRequired actionClosure condition
    Confirmed compromiseA 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 exposedA 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 exposedRepository 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 exposedNo 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.
    UnknownActions 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-helper imposter-commit window beginning; affected tags were moved within minutes StepSecurity.
    • 2026-05-18T19:30:30Z StepSecurity reports the actions-cool/maintain-one-comment imposter-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

    1. 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.
    2. 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 IOCs
    Defang IOCs
    domain t.m-kosche.com t[.]m-kosche[.]com
    hash 8064d4e0322f069b3dba13e7957ff0ca7dab7984 8064d4e0322f069b3dba13e7957ff0ca7dab7984
    hash 6e79ae622b7ef30f31fdbcc2dc65339e 6e79ae622b7ef30f31fdbcc2dc65339e
    file .github/workflows/*.yml .github/workflows/*.yml