Skip to content
11 min read

Your Security Scanner Just Got Weaponized: Lessons from the Trivy Supply Chain Attack

The Trivy/TeamPCP compromise is a wake-up call for CI/CD pipeline security. Here is a practical framework for hardening your pipeline tooling against supply chain attacks.

Antonio J. del Águila

Knaisoma

At 17:43:37 UTC on March 19, 2026, attackers pushed a malicious tag to the Aqua Security Trivy repository. Within minutes, automated release pipelines had built and published a compromised binary to GitHub Releases, Docker Hub, GHCR, and Amazon ECR. By the time most security teams knew something was wrong, their CI/CD pipelines had already executed the attacker’s code with access to every secret in their infrastructure.

What made this incident different from the supply chain attacks we have been documenting since SolarWinds was not the scale, though the scale was significant: 141 malicious npm package artifacts across 66 unique packages, poisoned GitHub Actions tags covering virtually every published version of trivy-action, two LiteLLM releases infected on PyPI, Checkmarx GitHub Actions compromised, and OpenVSX extensions backdoored in the same campaign window. What made it different was the weapon chosen. The attackers did not compromise a utility library or a build tool. They compromised the security scanner.

The attack that broke the trust chain

The breach did not begin on March 19. The threat actor known as TeamPCP had been inside Aqua Security’s GitHub infrastructure since late February, when an automated bot called hackerbot-claw exploited a misconfigured pull_request_target GitHub Actions workflow in the Trivy repository. The workflow carried overly broad permissions, and the attacker used it to steal a Personal Access Token belonging to the aqua-bot service account. Aqua Security detected the intrusion, rotated credentials, and considered the incident closed. One credential was missed. That oversight cost the ecosystem three weeks of dormancy and then a detonation.

On March 19, TeamPCP used the retained credential to force-push 75 of 76 version tags in aquasecurity/trivy-action and all seven tags in aquasecurity/setup-trivy. Any CI pipeline referencing these actions by tag name now silently executed attacker-controlled code. There was no pull request, no diff, no approval event in any audit log. The string @v0.28.0 in your workflow YAML still read @v0.28.0. It just resolved to something entirely different.

The malicious code performed a three-stage credential harvest. It scraped secrets from the memory of the GitHub Actions runner worker process by reading directly from /proc/<pid>/mem, bypassing GitHub’s log masking entirely since that mechanism only filters logged output, not memory reads. It swept the filesystem for over fifty categories of credentials: AWS, GCP, and Azure tokens, SSH keys, Kubernetes service account tokens, database passwords for MySQL, PostgreSQL, MongoDB, and Redis, Docker Hub credentials, and npm tokens. It queried the AWS instance metadata service at the standard IMDS address for any role credentials attached to the runner. Everything was encrypted with AES-256-CBC and RSA-4096, archived as tpcp.tar.gz, and exfiltrated to scan.aquasecurtiy[.]org, a typosquatted domain designed to pass a cursory inspection.

Then came CanisterWorm.

How the attack propagated

Using the npm tokens harvested from compromised developer machines and CI runners, TeamPCP began mass-publishing malicious versions of every package those tokens had rights to publish. Twenty-eight packages were poisoned in under sixty seconds. But the more technically significant variant came later, observed in packages including @teale.io/eslint-config versions 1.8.11 and 1.8.12. This generation added a findNpmTokens() function to index.js that executed during the postinstall phase — the moment any developer or CI system ran npm install. It searched the local environment for npm tokens, spawned a fully detached background process, and used the harvested tokens to republish infected versions to every package the new victim had publish rights over.

Every engineer who ran npm install became an unwitting propagation node. The worm spread through normal development workflows, requiring no ongoing attacker intervention.

The command-and-control infrastructure introduced a technique we had not seen used in a production attack before. The malware polled a canister on the Internet Computer Protocol blockchain: tdtqy-oyaaa-aaaae-af2dq-cai.raw.icp0.io. An ICP canister is a smart contract deployed on a distributed, censorship-resistant network. Unlike a traditional C2 server, it cannot be sinkholed, seized, or taken offline by a hosting provider or law enforcement action. The canister exposed an update_link method that let the attackers rotate payload URLs at will without touching any infrastructure that could be tracked or disrupted. This is the first publicly documented use of an ICP canister as a dead-drop C2 resolver in a production attack campaign.

The propagation chain looked like this:

flowchart TD
    A["February: pull_request_target\nworkflow misconfiguration\nleaks aqua-bot PAT"] --> B["Incomplete credential\nrotation leaves\none token live"]
    B --> C["March 19: Force-push\n75 trivy-action tags\n+ all setup-trivy tags"]
    C --> D["Downstream CI pipelines\nexecute malicious code\nwith full secret access"]
    D --> E1["Runner memory scrape\n/proc/pid/mem"]
    D --> E2["Filesystem credential\nharvest — 50+ paths"]
    D --> E3["Cloud metadata\nIMDS query"]
    E1 & E2 & E3 --> F["Exfiltrate via encrypted archive\nto typosquatted C2 domain"]
    F --> G["npm tokens harvested\nfrom victims"]
    G --> H["CanisterWorm: mass-publish\n28+ packages in 60 seconds"]
    H --> I["Postinstall hook runs\non npm install"]
    I --> J["New victim tokens harvested\nand used to propagate further"]
    J --> H
    F --> K["Checkmarx, LiteLLM,\nOpenVSX extensions\ncompromised in parallel"]

Why did detection miss this for days? Four failures compounded each other. CI pipelines referenced actions by tag name, so the force-pushed tag produced no visible diff anywhere in any repository. The malware read secrets from process memory rather than environment variables, bypassing GitHub’s log masking, which only filters printed output. The exfiltration domain appeared legitimate to threat intelligence feeds at the time of the attack. And the malicious code arrived signed and published by the official aquasecurity organization, so provenance checks passed. There was nothing obviously wrong until you examined what the code was actually doing at runtime, in memory, on the wire.

Why your pipeline is more exposed than you think

We have conducted pipeline security assessments across regulated industries for a long time. The pattern is consistent: security scanners receive a level of implicit trust that is difficult to justify when you examine it closely. They need read access to source code to scan it. They need network access to download vulnerability databases. They often write reports to shared artifact stores. They run on every commit, on every pull request, in every environment. In the CI/CD systems we assess, security tooling routinely has broader access to sensitive credentials than the application code it is scanning.

This is not a criticism of any particular tool or team. It is a structural problem. Shifting security left means running security tooling earlier, more frequently, and with more access. That is the right direction. But we have moved much faster on adding those tools than on treating those tools as an attack surface in their own right.

The OWASP Top 10 2025 reflected this reality explicitly. Software Supply Chain Failures moved to A03, and for the first time the category was expanded beyond “vulnerable library versions” to encompass the full delivery chain: build system integrity, CI/CD pipeline integrity, artifact signing, package registry compromise, and developer workstation compromise as a supply chain vector. It ranked first in the community survey, with the highest average incidence rate and the highest average exploit and impact scores of any category in the report. The Trivy incident is A03 in production, across every dimension the category describes.

The harder conversation is about privilege. A team at a financial services company we advised last year had implemented comprehensive secret scanning, dependency auditing, and container scanning in their pipeline. Every security control was pointed at their application. None was pointed at the pipeline itself. When we asked what would happen if one of their pipeline tools were compromised, there was a long silence. The tooling ran with the same credentials as their deployment automation. A compromised scanner would have had a direct path to production artifact stores, cloud environments, and every downstream system the pipeline touched.

A practical hardening framework for CI/CD pipelines

The root cause of the Trivy compromise was a single incomplete credential rotation. That is almost always the proximate cause of a supply chain incident. The structural cause is that most pipelines are designed for developer convenience, not for the assumption that the tools themselves might be adversarial.

Here is a GitHub Actions pipeline that reflects how the majority of teams run security scanning today:

# Vulnerable configuration — do not use as-is
name: Security Scan

on:
  push:
    branches: [main]
  pull_request:

permissions:
  contents: read
  id-token: write   # Overly broad — scanner does not need this
  packages: write   # Overly broad — scanner does not need this

jobs:
  scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      # Tag-pinned: if attacker force-pushes v0.28.0, this workflow
      # now silently executes their code on every run
      - name: Run Trivy vulnerability scanner
        uses: aquasecurity/[email protected]
        with:
          image-ref: myrepo/myapp:${{ github.sha }}
          format: sarif
          output: trivy-results.sarif

      # npm install runs postinstall scripts by default
      # This is exactly how CanisterWorm propagated
      - run: npm install
      - run: npm audit

A hardened version addresses each of these vectors specifically:

# Hardened configuration
name: Security Scan

on:
  push:
    branches: [main]
  pull_request:

permissions:
  contents: read           # Minimum required for checkout
  security-events: write  # Only what the scanner actually needs

jobs:
  scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
        # SHA pin: this commit is immutable. Force-pushing any tag
        # cannot change what this resolves to.

      - name: Install and verify Trivy
        run: |
          # Pin to a specific version and verify the release checksum
          curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | \
            sh -s -- -b /usr/local/bin v0.69.3
          # Verify using cosign — Trivy publishes SLSA provenance
          cosign verify-blob \
            --certificate trivy.cert \
            --signature trivy.sig \
            /usr/local/bin/trivy

      - name: Run Trivy in offline mode
        run: |
          # TRIVY_OFFLINE_SCAN prevents any outbound network calls
          # A scanner has no legitimate reason to establish
          # arbitrary outbound connections during a scan
          TRIVY_OFFLINE_SCAN=true trivy image \
            --exit-code 1 \
            --severity HIGH,CRITICAL \
            --format sarif \
            --output trivy-results.sarif \
            myrepo/myapp:${{ github.sha }}

      # --ignore-scripts eliminates postinstall hooks entirely
      # CanisterWorm's self-propagation cannot execute without them
      - run: npm ci --ignore-scripts
      - run: npm audit --audit-level=high

The framework behind this configuration:

  • Pin all GitHub Actions and pipeline tools to full commit SHAs. Tags are mutable references. A SHA is not. Use Dependabot with allow: [{dependency-type: github-actions}] to keep SHA pins current without manual overhead.
  • Run npm ci --ignore-scripts in all CI pipelines. Postinstall scripts have no legitimate purpose in a controlled build environment. Disable them unconditionally.
  • Apply least-privilege token scopes to every job. Enumerate exactly what each job requires and remove everything else. A scanner writing SARIF output needs security-events: write. It does not need id-token: write or packages: write.
  • Restrict outbound network access from CI runners. A vulnerability scanner does not need arbitrary egress. Run in offline mode, enforce an egress allowlist, or use a network-restricted execution environment.
  • Replace long-lived PATs with OIDC short-lived tokens wherever possible. A stolen OIDC token expires in minutes. The retained Aqua Security credential that enabled the March attack was valid for weeks after the February intrusion.
  • Verify artifact signatures before execution. Trivy, along with most mature open-source security tools, publishes SLSA provenance and cosign signatures. Verifying them takes two lines of shell. Skipping them takes zero lines.
  • Audit every pull_request_target workflow in your organization immediately. This was the initial intrusion vector. Any workflow using pull_request_target with write-scoped secrets or privileged tokens is a candidate for this attack class.
  • Generate and maintain an SBOM for your pipeline, not just your application. You cannot track what you have not inventoried. Treat your CI/CD toolchain as a dependency graph with the same rigor you apply to your application’s dependencies.

What to do this week

If your pipeline ran trivy-action or setup-trivy between March 19 and March 24, 2026, treat your entire secret surface as compromised until you can prove otherwise. Search your GitHub organizations for repositories named tpcp-docs or docs-tpcp: their existence confirms exfiltration occurred. Review GitHub Actions runner logs for that window for references to tpcp.tar.gz. Check developer machines for ~/.config/systemd/user/sysmon.py or a systemd user service named pgmon. Rotate every credential that could have been in scope: GitHub PATs, cloud IAM credentials, npm publish tokens, Docker Hub credentials, SSH keys, database passwords, and any TLS private keys accessible from your CI environment.

For teams not directly affected: the pull_request_target audit and SHA-pinning your GitHub Actions are both afternoon-sized tasks that close entire classes of attack surface. Start there.

Who watches the watchmen

The strategic implication of the Trivy incident is not simply that supply chain attacks are increasing in sophistication, though they are. It is that the tools organizations have built to defend against such attacks are now themselves primary targets. SolarWinds, the Shai-Hulud npm worm in 2025, and CanisterWorm form a clear progression: attackers have systematically worked up the trust hierarchy from application code, to open-source dependencies, to build infrastructure, and now to the security tooling layer itself.

This matters because of the implicit trust we extend to security vendors. Aqua Security is a legitimate, security-focused company with a real product and a real customer base. That did not prevent the incident. In a meaningful sense, it enabled it: the more trusted the source, the more pipelines execute its code without scrutiny, and the more valuable the compromise. The attacker’s choice of Trivy was not incidental. It was the point.

We have worked with organizations that have invested significantly in shifting security left over the past several years. Every one of them treats the security tooling as trusted infrastructure, sitting above the threat model rather than inside it. The pipeline is where secrets live, where credentials flow, and where every application in the organization passes through on its way to production. Treating it as anything other than a first-class attack surface is a gap that TeamPCP just demonstrated how to exploit at scale.

Building a genuine pipeline security posture means applying the same controls to your CI/CD toolchain that you apply to the systems it protects: inventory, privilege minimization, integrity verification, and runtime behavioral monitoring. Falco and Sysdig can detect the specific behaviors CanisterWorm exhibited: reads from /proc/mem, unexpected outbound curl POST requests, IMDS queries from processes that have no business making them. These are not exotic capabilities. They are standard runtime security practice applied to a part of the stack that most teams have simply never thought to cover.

The next campaign will not target Trivy. It will target whatever your pipelines trust most, precisely because that trust is the vulnerability. The organizations that come out of this period in reasonable shape will be the ones that started treating their pipeline as an adversarial environment before the next incident forced the question.

If your team is working through the aftermath of this incident, or if you are looking at your pipeline security posture and realising there are gaps you are not sure how to close, we are happy to talk it through. This is the kind of problem where an outside perspective and some battle-tested patterns can save you weeks of guesswork.

Security DevOps Supply Chain
Share:

Stay updated

Get insights on engineering transformation delivered to your inbox.

Newsletter coming soon.