While investigating a spike in script execution detections across several CrowdStrike Falcon® platform customers, CrowdStrike’s Engineering team traced the activity to a compromised GitHub Action named aquasecurity/trivy-action. This popular open-source vulnerability scanner is frequently used in CI/CD pipelines.
Our investigation found that 76 of the scanner’s 77 release tags had been retroactively poisoned via git tag repointing, replacing the legitimate entry point with a multi-stage credential stealer. The malicious code runs silently before the real scanner, so workflows appear to complete normally.
Aqua Security has officially confirmed the compromise of the Trivy GitHub Action script, setup script, and binary, and has subsequently quickly removed all malicious artifacts from their repositories. Additionally, CrowdStrike coordinated with Aqua Security prior to this publication. In this blog, we discuss how this activity was discovered, how the attack works, what the payload does, and how CrowdStrike helps organizations defend against this threat.
GitHub Actions and Why They Matter
GitHub Actions is the CI/CD platform built into GitHub. When a developer pushes code, opens a pull request, or merges a branch, GitHub Actions can automatically build, test, and deploy that code. GitHub Actions is used by millions of repositories from small open-source projects to enterprise production pipelines.
Workflows are defined in YAML files that live in a repository's .github/workflows/ directory. Each workflow is composed of jobs, and each job runs a series of steps on a runner — a virtual machine or container provisioned to execute the work. These runners have access to the repository's code, its configured secrets (API keys, deploy tokens, cloud credentials), and often broad network access to internal infrastructure.
A key feature of GitHub Actions is the ability to reference reusable actions, which are prepackaged steps published by third parties. Instead of writing custom scripts for common tasks like checking out code, setting up a language runtime, or running a security scanner, teams reference shared actions by name and version:
steps:
- uses: actions/checkout@v4
- uses: aquasecurity/trivy-action@0.24.0
When a workflow runs, the runner downloads the referenced action from GitHub, extracts it, and executes its entry point. This is where the trust model becomes critical: The runner executes whatever code the action contains, with full access to the runner's environment, secrets, and network.
Most teams reference actions by tag (e.g., @0.24.0 or @v2). Tags are human-readable and easy to update. But in Git, tags are mutable — they can be silently repointed to a different commit without any visible change to the release page, the tag name, or the published dates. This is the mechanism exploited in the compromise described below.
aquasecurity/trivy-action is a GitHub Action published by Aqua Security that wraps its open-source vulnerability scanner, Trivy. It's widely adopted for scanning container images, file systems, and infrastructure-as-code for known vulnerabilities as part of CI/CD pipelines.
In summary, if an action's code is modified, whether by its maintainers or by someone who gained write access, every pipeline that references it will trust and execute the new code on its next run, all with full access to that pipeline's secrets, credentials, and infrastructure.
Initial Discovery
An initial spike on Linux platforms linked to runners was observed on March 19, 2026. The detections were concentrated on GitHub Actions runners, and the script content didn't appear to align with benign CI/CD activity.
Digging into the process tree, the full execution chain was traced to one of the affected runners:
runc
└── /bin/bash /home/runner/run.sh
└── /bin/bash /home/runner/run-helper.sh
└── Runner.Listener run
└── Runner.Worker spawnclient 149 152
└── /usr/bin/bash --noprofile --norc -e -o pipefail /home/runner/_work/_temp/395479a1-…ded349.sh
└── /bin/bash /home/runner/_work/_actions/aquasecurity/trivy-action/0.24.0/entrypoint.sh
Reading from top to bottom: runc starts the container, which launches the runner's startup scripts (run.sh → run-helper.sh). These start Runner.Listener, which connects to GitHub and waits for jobs. When a workflow is triggered, the Listener spawns Runner.Worker to execute it. The Worker runs each workflow step as a shell script in a temp directory — the 395479a1-…ded349.sh wrapper. That wrapper calls the action's entrypoint.sh, which is where the malicious code lives.
From here, CrowdStrike began investigating the action itself.
What the Payload Does
The malicious entrypoint.sh prepends approximately 105 lines of attack code before the legitimate Trivy scanner logic. An example of one of the malicious commits can be viewed here.
It executes a multi-stage credential theft operation, then runs the real scanner so the workflow appears to succeed normally.
Stage 1: Runner Process Discovery
Initially, the script enumerates process IDs (PIDs) of GitHub Actions runner processes (Runner.Worker, Runner.Listener, runsvc, run.sh) and reads their /proc/{PID}/environ files, filtering for environment variables containing env or ssh in their names.
Stage 2: Platform-Specific Credential Theft
The attack branches based on the runner environment:
On GitHub-hosted Linux runners, a Base64-encoded Python script is decoded and executed with sudo. It locates the Runner.Worker process, reads its memory maps via /proc/{PID}/maps, and scrapes raw process memory via /proc/{PID}/mem looking for GitHub Actions secrets — specifically JSON structures matching "name":{"value":"...","isSecret":true}.
On self-hosted runners, a different Base64-encoded Python script performs broad filesystem credential harvesting. It targets the following:
- SSH private keys and configs
- Cloud credentials (AWS, Google Cloud, Azure)
- Kubernetes configs and service account tokens
- Docker registry credentials
- Database credentials (MySQL, PostgreSQL, MongoDB, Redis)
- CI/CD configs (GitLab CI, Travis, Jenkins, Drone)
- Infrastructure-as-code secrets (Terraform state/vars, Ansible, Helm)
- TLS/SSL private keys
- .env files and API keys
- Cryptocurrency wallet keys (Bitcoin, Ethereum, Solana validator keypairs, Cardano, and others)
- System files (/etc/passwd, /etc/shadow, shell histories)
Stage 3: Encrypted Exfiltration
Collected data is encrypted with a randomly generated AES-256-CBC session key, and the session key is wrapped with a hardcoded 4096-bit RSA public key. The encrypted bundle (payload.enc + session.key.enc) is packaged as tpcp.tar.gz. This ensures only the adversary server with access to the corresponding RSA private key can decrypt the payload.
Stage 4: Dual Exfiltration Channels
The primary exfiltration channel is an HTTPS POST to scan.aquasecurtiy[.]org — a typosquatted domain mimicking Aqua Security's legitimate aquasecurity.com.
If the endpoint returns a non-2XX response and INPUT_GITHUB_PAT is available, the script falls back to exfiltrating through GitHub itself. It will create a public repository named tpcp-docs under the victim's GitHub account, create a timestamped release, and upload the encrypted bundle as a release asset.
Stage 5: Cleanup and Camouflage
All temporary files are removed. The remainder of the script is a functional copy of the real trivy-action entry point. It downloads and runs Trivy normally, producing expected scanner output. To an operator reviewing workflow logs, the step appears to have completed successfully.
Further Discoveries
Every release tag of aquasecurity/trivy-action from GitHub was downloaded and scanned. This includes 77 tags total, spanning from 0.0.1 to 0.35.0. Each entrypoint.sh was checked against five indicator of compromise (IOC) patterns derived from the malicious payload.
The result of this analysis is as follows:
| Metric | Count |
| Total tags scanned | 77 |
| Compromised | 76 |
| Clean | 1 |
The only clean tag is 0.35.0. Every other release from 0.0.1 through 0.34.2 currently serves a malicious entrypoint.sh.
The setup-trivy action was also compromised to execute the same malicious script.
In addition to the compromised trivy-action GitHub Action, the trivy scanner version 0.69.4 was also compromised. When it executes, a script is dropped to ~/.config/sysmon.py. The script acts as a lightweight stage-1 loader. After an initial five-minute sleep (likely intended to outlast sandbox analysis timeouts), it enters a polling loop that contacts a command-and-control (C2) server approximately every 50 minutes.
The C2 is hosted on the Internet Computer (ICP) blockchain (hxxps[://]tdtqy-oyaaa-aaaae-af2dq-cai[.]raw[.]icp0[.]io), making it resistant to traditional domain takedowns.
On each cycle, the loader fetches a URL from the C2, compares it against a locally cached state file, and if the URL is new, downloads the referenced binary to /tmp/pglog. It then marks the file executable and launches it as a detached process with all output suppressed. The choice of filename mimics PostgreSQL logging, and the state file is dot-prefixed to stay hidden from casual directory listings.
Error handling is deliberately silent throughout. Every operation is wrapped in bare except blocks that discard failures, ensuring the loader never crashes or produces visible output. Combined with the spoofed browser User-Agent and the lack of any logging, the script is designed to operate invisibly on a compromised host while allowing the attacker to rotate payloads at will by simply updating the URL served by the C2.
The binary also exhibits the same credential-stealing behavior as the compromised action. It scans for credentials, bundles them, encrypts them, and then exfiltrates the data via a post request.
How It Was Done
Git tags are mutable references. A lightweight tag is just a pointer from a name to a commit SHA. With write access to a repository, an attacker can force-push a tag to point to a different commit:
git tag -f 0.24.0 <new-commit>
git push -f origin refs/tags/0.24.0
The release page on GitHub doesn't change — same name, same dates, same description. But the source archive that gets downloaded now contains different code.
We confirmed this by comparing what the 0.24.0 tag currently delivers against its parent commit:
Tag 0.24.0 resolves to commit e0198fd2b6e1679e36d32933941182d9afa82f6f:
- entrypoint.sh is 17,592 bytes
- First line after the shebang: _COLLECT_PIDS="$$" (the stealer)
- This commit is not reachable from the master branch — it's a dangling commit that exists only because the tag points to it
The parent of that commit (57a97c7e7821a5776cebc9bb87c984fa69cba8f1) is the same commit that the clean 0.35.0 tag points to:
- entrypoint.sh is 2,855 bytes
- First line after the shebang: TRIVY_CMD="${TRIVY_CMD:-trivy}" (the legitimate scanner)
- This commit is reachable from master
The SHA256 hashes tell the story:
| Source | entrypoint.sh SHA256 | Size |
| Tag 0.24.0 (compromised) | 18a24f83e807479438dcab7a1804c51a00dafc1d526698a66e0640d1e5dd671a | 17,592 bytes |
| Parent commit (legitimate) | 07500e81693c06ef7ac6bf210cff9c882bcc11db5f16b5bded161218353ba4da | 2,855 bytes |
| master branch (legitimate) | 07500e81693c06ef7ac6bf210cff9c882bcc11db5f16b5bded161218353ba4da | 2,855 bytes |
The malicious commit's metadata — author name, commit message ("Upgrade trivy to v0.53.0 (#369)"), and timestamps (2024-07-09) — all mirror a legitimate prior commit. Git allows setting arbitrary author and committer dates via GIT_AUTHOR_DATE and GIT_COMMITTER_DATE environment variables, so these cannot be trusted.
The diagram below shows the full attack chain.