For most of the last decade the security advice given to small engineering teams was simple: add a scanner to your pipeline. Run Trivy on your container images, run an infrastructure-as-code scanner over your Terraform, fail the build on a critical finding. It was cheap, fast, and the highest-leverage control a team without a security engineer could adopt.
In 2026 that advice acquired a footnote. Between late February and May, a threat actor tracked as TeamPCP compromised the distribution channels of two of the industry's most widely deployed scanners, Aqua Security's Trivy and Checkmarx KICS, and used them to harvest credentials from build environments at scale. The tool you added to find vulnerabilities became the thing exfiltrating your cloud keys.
This post is for founders and engineers at pre-seed and seed startups running CI on GitHub Actions or Jenkins without a dedicated platform or security team. It is not an incident recap for its own sake. The structural lesson is that CI runners are the most over-privileged and least-monitored machines most startups own, and what follows are the controls that contain this class of attack. Every claim cites a public advisory or vendor disclosure; opinions are labelled inline.
Quick context: what a scanner compromise actually buys an attacker
A security scanner occupies an unusual position. It runs early in the pipeline, on nearly every commit, in almost every repository, inside an environment already holding the secrets the build needs. It is also a tool nobody watches: its calls to fetch vulnerability databases, its filesystem sweeps, its long runtimes are all expected. If you were designing a payload delivery vehicle for CI, you would design a scanner.
That is the shape of what happened in 2026. The attacker needed no novel exploit, just one privileged token and the ordinary, documented mechanics of git and registry tags, used to retroactively poison versions that thousands of pipelines already referenced by name. Takeaway: this was not a failure of the scanners' code quality. It was a failure of how the ecosystem names and trusts software versions, and that failure lives in your pipeline too.
1. The timeline, in the order it actually happened
The campaign began in late February 2026, when the attacker exploited a misconfiguration in Trivy's own GitHub Actions environment to extract a privileged access token and gain a foothold in the project's release automation. Aqua disclosed an initial incident on March 1 and rotated credentials. According to the project's advisory, that rotation was not atomic: not all credentials were revoked simultaneously, leaving a window in which a still-valid token could capture the newly issued replacements.
On March 19 the attacker used that access. Around 17:43 UTC they force-pushed nearly every existing release tag in aquasecurity/trivy-action to new malicious commits, and replaced the tags in aquasecurity/setup-trivy. A malicious Trivy v0.69.4 went out around 18:22 UTC and was pulled roughly three hours later; the poisoned action tags were cleaned around 05:40 UTC on March 20, about twelve hours of exposure. Malicious Docker Hub images tagged v0.69.5 and v0.69.6 followed on March 22 and 23. The issue is tracked as CVE-2026-33634.
Four days later, on March 23, Checkmarx disclosed unauthorized access to its GitHub repositories. Trend Micro and Arctic Wolf attribute that access to CI/CD secrets stolen in the Trivy compromise, and the campaign widened across ecosystems into April and May. Takeaway: this was a chain, not a single event, and each link was a credential harvested from someone else's pipeline.
2. The blast radius was Checkmarx, and then everyone downstream
The Checkmarx side is the more instructive half, because it shows how far a compromise propagates once the attacker holds a vendor's publishing rights. Per the vendor's incident log, affected artifacts spanned four distribution channels: the ast-github-action and KICS GitHub Actions on March 23; the ast-results and cx-dev-assist IDE extensions on OpenVSX the same day, with Microsoft Marketplace builds hit again on April 22; malicious KICS images pushed to official Docker Hub on April 22 under latest, v2.1.20, alpine, debian and a new v2.1.21; and the Jenkins plugin on May 9.
Socket's analysis notes the KICS Docker repository had accumulated over five million pulls, the relevant number for anyone estimating exposure. The Docker Hub window was short, roughly half an hour, but latest and alpine are exactly the tags a pipeline resolves fresh on every run.
The campaign did not stop at scanners. The Register and Sophos documented compromises across PyPI, npm, and GitHub Container Registry in the same period, including a malicious Bitwarden CLI build on npm in late April; Bitwarden reported no evidence that end-user vault data was accessed. Takeaway: assume any developer tool that auto-updates, IDE extensions included, is an unattended code-execution path into your laptop and your pipeline.
3. Mutable references are the root cause
Strip away the specifics and one mechanism explains the entire incident. In both git and container registries, a version tag is a mutable pointer, not an immutable identity. v0.35.0 is a label someone can move. latest is a label someone will move. When your workflow says uses: aquasecurity/trivy-action@0.28.0, you are not asking for specific code; you are asking for whatever commit that label points at, resolved at execution time, with no verification in between.
That is why a force-push poisoned the entire release history at once. The attacker never had to convince anyone to upgrade. Every pipeline referencing a tag by name upgraded itself, silently, the next time it ran, so pipelines untouched for a year were as exposed as ones updated that morning.
The same property applies to registries. A Docker tag resolves to a manifest digest at pull time, and that mapping is rewritable by whoever holds push rights. Docker's documentation is explicit that the digest, not the tag, is an image's immutable identifier. Practitioner opinion: most teams I look at have never pulled an image by digest and do not know their base images are floating. Takeaway: if a name in your pipeline can be repointed by a third party, it is not a version, it is a subscription to that party's future decisions.
4. Rotation that is not atomic is not rotation
The most transferable lesson in the advisory has nothing to do with scanners. After the March 1 disclosure the project rotated its credentials, the textbook response. The attacker retained access anyway, because rotation happened over days rather than at once, and a token still valid in that window could observe the replacements being provisioned.
This failure mode scales down to a five-person startup perfectly. When you believe CI has been compromised, the instinct is to work through the secret list by importance over an afternoon: cloud keys, then the registry token, then third-party API keys, then webhook signing secrets. If the attacker still holds any execution path during that afternoon, they simply collect the new values as you create them.
The correct sequence is to cut execution first and rotate second. Disable the workflows, revoke the runner's identity provider trust or take the runners offline, and only then rotate. Microsoft's guidance for this compromise similarly emphasises establishing whether the environment executed the malicious versions before and during remediation. Takeaway: write the order of operations into your runbook now, because you will not reason it out correctly at 02:00.
5. What was actually stolen was secrets, not source code
Be precise about the payload, because it determines what you rotate. Per the advisory, the infostealer dumped Runner.Worker process memory through /proc/<pid>/mem to extract secrets never written to disk, and swept more than fifty filesystem paths for SSH keys, AWS, GCP and Azure credentials, Kubernetes tokens, Docker configs, .env files, database credentials, and crypto wallets. Collected data was encrypted with hybrid AES-256 and RSA-4096, then sent to a domain typosquatting the vendor's.
Two details deserve emphasis. Process memory extraction defeats the common assumption that a secret injected as an environment variable and never written to disk is safe from a filesystem-scanning payload: if the runner executes attacker code with sufficient privilege, every secret the job can see is exposed, including ones masked in the logs. And when exfiltration failed, the malware created public repositories named tpcp-docs under victim accounts as a fallback channel, which is an unusually convenient indicator of compromise.
CrowdStrike and Palo Alto Networks agree on scope. Takeaway: rotate every secret the affected jobs could read, not the ones you think the tool needed.
6. Fix one: pin by digest, everywhere, and enforce it
This is the single control that would have blunted the attack for most victims. Pinning a GitHub Action to a full 40-character commit SHA means the force-push never reaches you: your workflow names the commit directly, and moving a tag cannot change which commit that is.
- uses: aquasecurity/trivy-action@57a97c7 # v0.35.0, pinned by SHA
Record the human-readable version in a trailing comment, which keeps the file reviewable and lets Dependabot or Renovate propose updates. GitHub's hardening guide recommends this directly, and since a 2025 platform change organisations can enforce SHA pinning through Actions policy instead of review discipline. StepSecurity's guide covers tooling that rewrites tags to SHAs across a repository.
Apply the same rule to container images. Replace image: checkmarx/kics:latest with a digest reference of the form checkmarx/kics@sha256:<digest>, and do the same for base images and any tool image invoked in CI. Pin language dependencies with a committed lockfile and enable hash checking, so the installer verifies content rather than trusting the index. Takeaway: make the rule "no mutable reference executes in CI," then enforce it with policy, because review will not catch it consistently.
7. Fix two: stop storing long-lived cloud credentials in CI at all
A stolen AWS access key pair is useful to an attacker until somebody revokes it, which in practice is often months. A stolen fifteen-minute OIDC token is useful for fifteen minutes. That difference converts a breach into an inconvenience, and it costs an afternoon to implement.
GitHub Actions can act as an OpenID Connect identity provider, exchanging a short-lived signed token for temporary cloud credentials at job runtime. On AWS you configure the GitHub OIDC provider and assume a role scoped by a trust policy; on GCP the equivalent is Workload Identity Federation, which I have walked through step by step. Either way, no static key exists in repository secrets to steal.
The detail teams get wrong is the trust policy condition: scoping the role to your organisation alone lets any repository in it, including a newly created one, assume the role. Constrain the sub claim to the specific repository and, where warranted, the branch or environment. Then cut scope on what remains: attach secrets to environments with required reviewers rather than exposing them repository-wide, and set the default GITHUB_TOKEN to read-only organisation-wide, with jobs elevating only what they need. Takeaway: the objective is that a fully compromised runner yields nothing that outlives the job.
8. Fix three: constrain what the runner can reach
Every stage of this campaign ended with an outbound connection to attacker infrastructure. In the Trivy case the destination typosquatted the vendor's own domain; Checkmarx's indicators included domains such as checkmarx.zone and checkmarx.cx. A runner that can only reach an allowlist of known endpoints turns successful code execution into failed exfiltration.
The accessible option for a small team is Harden-Runner, an open-source action that runs as a workflow's first step and records every outbound connection, process, and source-file write. Run it in audit mode for a week to learn your real egress profile, then switch to block mode with an allowlist. Free for public repositories.
Two adjacent controls matter as much. Do not run untrusted tooling on self-hosted runners attached to production, since those are persistent machines with reusable state and often an instance role. And treat pull_request_target with suspicion, because it runs with repository secrets in the context of an unreviewed fork. Takeaway: you cannot prevent every compromise, but you can make exfiltration fail loudly.
9. Fix four: verify provenance before you execute
Pinning tells you that you are running the same artifact you ran yesterday. Provenance tells you where it came from. Different questions, and after 2026 both are worth answering.
GitHub artifact attestations let a build generate a signed record binding an artifact to the workflow, repository, and commit that produced it, which consumers verify with gh attestation verify before deployment. For images signed with Sigstore, cosign verify does the equivalent, and both map onto the build levels in the SLSA framework. A pre-seed team needs no formal SLSA programme, but it should be able to say which workflow run produced the image in production.
On the publishing side, adopt the immutable options your registry offers. GitHub supports immutable releases, where marking a release immutable prevents its assets and git tag from being altered or deleted afterwards, removing the exact primitive used against Trivy. Enable rulesets forbidding force-pushes to release tags, and require two maintainers on any workflow touching publishing keys. Takeaway: the controls protecting your consumers from you are the same ones that would have protected your vendors' consumers from this campaign.
10. If you ran these tools: the detection checklist
Concrete steps, in order, if you used Trivy or Checkmarx tooling in the affected windows. First, determine exposure: search your workflow run history for jobs executing trivy-action, setup-trivy, the Checkmarx actions, or the KICS image between March 19 and May 10, 2026. Runs pinned to a full commit SHA are unaffected.
Second, move to safe versions: the advisory lists the Trivy binary at v0.69.3 or earlier, trivy-action at v0.35.0, and setup-trivy at v0.2.6, while Checkmarx directs users to ast-github-action v2.3.33 or later and the May 9 Jenkins builds. Verify against the vendors' current advisories, which were updated repeatedly.
Third, search your GitHub organisation and every member account for a public repository named tpcp-docs, the malware's fallback channel. Fourth, rotate every secret an affected job could reach, treating masked and memory-only values as exposed, then check cloud audit logs for use from unfamiliar addresses; if a cluster was in range, the Kubernetes audit-log patterns that signal compromise are the next place to look, and IAM Access Analyzer will surface what the stolen keys could reach. Fifth, audit IDE extension versions on developer laptops and disable auto-update. Takeaway: exposure is determined by whether a job ran in the window, not by whether you noticed anything unusual, because a working infostealer produces a green build.
Summary: control, effort, and what it stops
| Control | Effort | What it would have stopped |
| Pin actions to full commit SHAs | Half a day | The tag force-push entirely |
| Pin images by digest | Half a day | Poisoned latest and alpine tags |
| OIDC instead of static cloud keys | A day per cloud | Long-lived value of stolen credentials |
Scoped sub claim in the trust policy | An hour | Role reuse from other repos |
Read-only default GITHUB_TOKEN | An hour | Writes from a compromised job |
| Egress audit, then block | A week of audit | The exfiltration step |
| Environment-scoped secrets | Half a day | Blast radius across jobs |
| Artifact attestation | One to two days | Deploying artifacts of unknown origin |
| Immutable release tags | An hour | The same attack aimed at your users |
| Cut-execution-then-rotate runbook | Two hours | Attacker capturing rotated secrets |
What to do at your stage
Pre-seed, one to five engineers. Do three things and stop: pin every third-party action to a SHA, replace static cloud keys with OIDC, and set the default workflow token to read-only. Roughly two days of work, and it removes the mechanism this campaign depended on. Skip attestation and egress blocking for now.
Seed, five to twenty engineers. Add the rest of the pinning surface, base images by digest and lockfiles with hash verification, move secrets into environments with reviewers, and run egress monitoring in audit mode until you know your baseline. Write the runbook, specifically the order in which you cut execution and rotate. Practitioner opinion: this is where the runbook pays for itself, being the first stage at which more than one person is involved.
Series A and beyond. Enforce pinning through organisation policy rather than convention, turn egress blocking on, verify provenance as a deployment gate, and separate the publishing pipeline from the build pipeline so release credentials never appear in an ordinary CI job.
None of this requires a security team. It requires deciding that your build environment is production infrastructure and configuring it that way. If you want a second pair of eyes, MatrixGard runs a free scoped review of a startup's cloud and CI posture. Open the checklist here and work through it yourself, or get in touch and we will go through it together.
About the author
Avinash S is the founder of MatrixGard, a fractional DevSecOps practice that acts as the cloud, infrastructure, and security team for early-stage startups across India, Singapore, the UAE, the US, and the UK. He has roughly a decade of hands-on cloud and security engineering experience, and works with pre-seed and seed teams that need production-grade infrastructure without a full-time platform hire.
Methodology and sources
Every factual claim here is drawn from public primary sources: the Trivy security advisory and its CVE-2026-33634 record, Aqua Security's incident updates, Checkmarx's incident log, and analyses from Microsoft, CrowdStrike, Palo Alto Networks, Trend Micro, Arctic Wolf, Socket, and Sophos, cross-checked against a community-maintained campaign timeline. Remediation guidance references official GitHub, AWS, Google Cloud, Docker, SLSA, and Sigstore documentation.
No client data or private incident information appears in this post, and statements reflecting my own judgement are labelled practitioner opinion inline. Investigations into this campaign were updated repeatedly through mid-2026; verify version numbers and affected windows against the vendors' current advisories before acting on them.