Garnet Guard / Limits
Limits
Garnet Guard compares a new release with the one your project already trusts, using the release's files, its git tags and the registry's records. It never runs the package. These are the ways a malicious release can still get past it today, with what Garnet sees of each and, where a check is planned, the issue that tracks it. When a change closes a gap, it comes off this list.
What the replay and everyday-update numbers don't show is on the results page. The full map, including how Garnet protects itself, is in the threat model.
In the package's code
- A payload that runs on use, from code that was committed. Code that is committed to the repository, built normally and run when a feature is used rather than at import isn't reported by any rule. GRN-C007 catches it only when the build added code that isn't at the release's git tag, as in ultralytics 8.3.42. A package-level signal was measured and not built: it added no catch on the replays and missed ultralytics 8.3.42. Reporting a new host, URL or command that reaches a call the package already makes is planned for v2 (#65). This is the clearest open gap.
- A sensitive call made under another name. GRN-C003 recognizes a sensitive call by the name it is called under. A listed function handed to another call that runs it later (
Thread(target=os.system)), or given a harmless-looking name first (json.dumps = subprocess.Popen), isn't reported yet (#91). Neither is a name built at run time (#54). - A call that isn't on the sensitive list. The list covers starting processes, loading code and reaching the network, and was widened for launch. An API still off it, such as
pickle.load,multiprocessing.PoolorProcessPoolExecutor, is missed until it has been measured on everyday updates and added. - An import hook. Code that installs a hook at import (
sys.meta_path,sys.settrace, a change tobuiltins, a metaclass) is reported only when it also makes a listed call (#55). - A payload in a model or data file. A pickle or other file that a loader runs when the package opens it isn't read (#59).
- Native code that changed but isn't new. GRN-C004 reports native code where the trusted release had none, or under a name that hides it. A rebuilt binary, a new library in a folder that already held native code, or a new extension module in a release that already shipped some isn't reported, and what a binary does isn't read.
- A wheel or file that isn't compared. The content rules read the wheel pip would install, and GRN-C012 compares the Python files in one wheel for each other platform family. A payload in a second wheel of a family already compared, such as Linux aarch64 beside Linux x86-64, or in a file that isn't Python, isn't seen. Neither is a file in a wheel with no counterpart in the source archive (#60).
- The code of a newly added dependency. GRN-C008 reports a dependency the trusted version didn't declare, and warns when it is new to the registry, missing from it or installed from outside it. The added package's own code isn't read; a first look at it is planned (#58).
- npm code is listed, not warned. On npm, GRN-C011 lists a new process, network or
evaluse in the JavaScript a package loads atinfo, which doesn't change the verdict. A name built at run time isn't resolved, and native binaries in npm packages aren't reported.
In the trust signals
- The attacker runs the project's own release pipeline. When the project's real workflow builds and publishes the release, its provenance, publisher and tag all look normal, and only the content rules can report it, as for elementary-data 0.23.3 and ultralytics 8.3.42. A release published from a different branch, ref or trigger isn't compared yet (#56), and neither is a change to the release workflow between the two attested commits (#57).
- The attacker also pushes a matching tag. GRN-T004 finds the tag it looks for. Checking that the tagged commit is on the default branch and that the tag predates the upload is planned (#61).
Beyond the one update
- A bad release between the trusted version and the new one. The trust rules compare the two ends of the update, not the releases skipped in between (#62).
- A risky change made slowly, one small step per release. Each step is compared only with the release before it. Per-package history is planned for v2 (#64).
- An update that never reaches a lockfile. Garnet checks the dependency changes in a lockfile, so a package installed by hand or built into an image is never compared. A scheduled check of a deployed environment is planned (#63).
What a result means
- A flag is not proof. Garnet reports what changed and why it is worth a look. It doesn't say the release is malicious, or that a payload would work: in telnyx 4.87.1 a typo stopped the payload, and GRN-C003 still reported the call. On everyday updates of popular packages, 366 of 18,181 were flagged for a look (results); none is known to be malicious, so each is a real change a reviewer has to look at and dismiss.
- No flag is not a verdict. A release can pass every rule and still carry a payload through any gap above. Garnet is a second reviewer for dependency updates; it doesn't replace code review, pinning or waiting a few days before taking a new release.
- A file Garnet can't read is reported, not passed. A file over the parse limit, code that doesn't parse as Python 3, or an archive Garnet refuses to open gets a couldn't-check notice (GRN-E002, GRN-E003) that names what wasn't checked, and the rest of the release is still compared.