Alpha · Open source · Apache-2.0

A second reviewer for every dependency update.

Garnet Guard checks every dependency update against the last release you trusted, and tells you when it looks different in ways that matter.

Trust the last release. Check the next.

github-actions bot commented

Garnet Guard: 1 update needs a look

⚠ Warning

lightning 2.6.1 → 2.6.2

  • GRN-C003 2.6.2 adds import-time calls to sensitive APIs: lightning/__init__.py:14 calls subprocess.Popen (in _run_runtime(), reached at import).
  • GRN-T004 2.6.1 has a tag in github.com/Lightning-AI/lightning, but github.com/Lightning-AI/pytorch-lightning has none for 2.6.2.

Garnet Guard · what is this? · GRN-C003 · GRN-T004

The comment Garnet Guard posts on the pull request, rendered the way GitHub shows it. A finding is amber because it is a reason to look, not a verdict.

Tested on real releases

  • 52 of 52malicious releases flagged in replayFrom 41 compromised packages on PyPI and npm, December 2024 to September 2026 and one from 2018, each compared with the release the project trusted before it. Every one flagged, none missed. Every result.
  • 606 of 18,181everyday updates flagged for a lookNormal releases of the 2,000 most downloaded PyPI and npm packages, about 1 in 30. Each flag points at a real change; none makes a false claim.
  • Noneof the package’s code is runGarnet reads each release as files. It never installs or imports it.

Replays

Each replay downloads the release a project trusted and the compromised release that followed, checks both against the hashes PyPI published, and diffs them (for npm, where removed files are gone, it uses the registry facts the write-ups recorded), reading the project’s git tags as they stood on the day of the upload. Nothing is run, and no malicious file is hosted here. One of the 52 results is below; every result, with its sources and the everyday-update test, is on the results page.

A linocut of a pomegranate cut in half. Every chamber of seeds shows, and one seed is amber.

What gave them away

The rules that fired on the 52 releases, read as the tags stood on the day of each upload. 28 of the 52 were flagged by both a content rule and a trust rule.

lightning 2.6.1 → 2.6.2 warn

PyPI · April 30, 2026 · MAL-2026-3201

Uploaded with a stolen token. A thread started in lightning/__init__.py launches a Bun-based credential stealer the moment anything imports the package. Install-script blocking does not help here, because the payload runs at import, not at install.

garnet diff lightning 2.6.1 2.6.2
lightning 2.6.1 → 2.6.2: WARN
  WARN  GRN-C003 new-import-time-exec: 2.6.2 adds import-time calls to sensitive APIs in 1 module(s)
           - lightning/__init__.py:14 calls subprocess.Popen (in _run_runtime(), reached at import)
  WARN  GRN-T004 no-matching-tag: 2.6.1 has a tag in github.com/Lightning-AI/lightning, but github.com/Lightning-AI/pytorch-lightning has none for 2.6.2
           - 2.6.1 → tag 2.6.1 at 655a3a91828694b9bfc241177f474741e483aeaa <https://github.com/Lightning-AI/lightning/tree/2.6.1>
           - 2.6.2: no tag named 2.6.2, v2.6.2, lightning-2.6.2 or lightning-v2.6.2 in github.com/Lightning-AI/pytorch-lightning <https://github.com/Lightning-AI/pytorch-lightning/tags>

verdict: warn

What this replay could not see

GRN-T001 (lost provenance) cannot fire here: lightning did not publish attestations before the incident, so there was no provenance to lose. GRN-T004 reads the tags as they stood on April 30. For lightning that matches today: there is no tag for 2.6.2, and we found no record of one pushed and later removed.

How it works

The attacks that hurt in 2025 and 2026 were not new packages. They were new versions of packages people already trusted, and each one looked different from the last good release in ways a machine can check. Every rule has a page that says what it checks, what its evidence looks like and what to do.

A linocut row of three closed pomegranates and a fourth cut open, one seed amber.

Trust downgrade

The new release has no provenance attestation when the old one did, was built by a different repository or workflow, has no matching git tag, or gained a file days after release that pip would install in place of a reviewed one.

GRN-T001GRN-T002GRN-T003GRN-T004GRN-T006

Risky content change

New files that run code on their own, such as a .pth file, build hook or pytest plugin. New code that starts a process, loads code or uses the network, at import time in Python or in the JavaScript an npm package loads. New binaries, new dependencies, and code that isn’t in the tagged source or the other platforms’ wheels.

GRN-C001GRN-C002GRN-C003GRN-C004GRN-C007GRN-C008GRN-C009GRN-C010GRN-C011GRN-C012

Known bad

The release matches an OSV malicious-package advisory, so a feed has already confirmed it. No earlier release is needed to compare with, and by default the finding fails the pull request’s check instead of asking for a look.

GRN-K001

  • Never runs the code it inspects. Releases are read as bytes and parsed with ast.
  • “Couldn't check” is never “pass”. A fetch or parse that fails is reported as a finding.
  • Quiet on clean updates. Passing packages collapse to one line in the comment.
  • Explainable. Every finding names a rule and its evidence, and garnet explain says what to do.

Get started

Garnet Guard is in alpha. 0.2.0, the first full release, is on PyPI with every rule built so far, and the Action is on the GitHub Marketplace. Rules whose pages say planned aren't built yet.

A linocut of seven pomegranate seeds, one of them amber.

1 From source, with the replays

$ git clone https://github.com/orca-solutions/garnet-guard
$ cd garnet-guard && uv sync
$ uv run garnet diff lightning 2.6.1 2.6.2
$ uv run python -m backtest   # replay every incident

Checks the malicious lightning 2.6.2 against 2.6.1, then replays every release on the results page.

2 One command

uvx garnet-guard check --git-base origin/main

Compares your lockfile with the one on main. Works with uv.lock, poetry.lock, pylock.toml and pinned requirements.txt.

3 On every Dependabot PR

permissions:
  contents: read
  pull-requests: write
steps:
  - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
    with:
      persist-credentials: false
  - uses: orca-solutions/garnet-guard@e6e9f44a24bb11ca1b20d339650104a7e273efc1 # v0.2.0

Pinned to the commit tagged v0.2.0, the release listed on the GitHub Marketplace. The whole workflow file, the inputs and the pattern for pull requests from forks are in docs/action.md.

Open, and staying that way

  • Apache-2.0. The whole engine and every rule.
  • No telemetry. No account, no API key, nothing phones home. This page loads nothing from third parties either.
  • Detection is never paywalled. Free for public and private repositories. See OPEN_CORE.md.
  • Complementary. Keep your cooldown, pip-audit and malware feeds. Garnet answers a different question: is this release built and shaped like the last one you trusted?