code-of-kai

code-of-kai

Vet is a dependency security scanner for Elixir. It detects supply chain attacks by walking the AST of every dependency in your lock file and flagging patterns that have no legitimate reason to appear in a library.

The Problem

On March 24th, someone compromised the PyPI publishing token for LiteLLM, an open-source AI gateway with 3.4 million daily downloads. They pushed a version with a .pth file — the kind Python executes automatically when the interpreter starts. The payload swept SSH keys, AWS tokens, and Kubernetes secrets, then exfiltrated them. The poisoned package was live for three hours. In that window, hundreds of thousands of systems downloaded it. Mercor, a $10 billion AI startup, lost 4TB of data — candidate records, source code, video interviews.

The entire attack was three lines of work: steal a publishing token, push a package, wait. The ecosystem did the rest.

In Elixir, the same pattern works. A package calls System.get_env("AWS_SECRET_ACCESS_KEY") inside a @before_compile hook and POSTs the result during mix deps.compile. Your application hasn’t started. Your tests haven’t run. The BEAM doesn’t distinguish between your code and your dependency’s code.

What Vet Does

Vet walks the AST of every dependency in your lock file and flags patterns with no legitimate reason to appear in a library:

  • Compile-time system commands (System.cmd, :os.cmd, Port.open)

  • Credential access (environment variables containing SECRET, KEY, TOKEN, AWS_*)

  • Network calls to suspicious endpoints

  • Compile-time hooks (@before_compile, @after_compile)

  • Obfuscated payloads (high-entropy strings, Base64+eval patterns)

  • Atom exhaustion DoS attacks

  • Slopsquatting detection (attackers register names that LLMs commonly hallucinate)

mix vet.check catches these before mix deps.get — no execution, no risk.

Why Elixir is Positioned Well

Elixir has the tools to do this properly. Code.string_to_quoted and Macro.prewalk let you walk dependency code with the same tools the compiler uses. Python can’t do this. That said, AST-level checks are ultimately a compiler concern — the compiler already walks every node, sees macro-expanded code, and can’t be skipped. What the compiler can’t do is check download counts, score dependency depth, or detect slopsquatting. The full solution is both.


https://github.com/code-of-kai/vet

Showing Posts 1 to 10

ryanzidago

ryanzidago

Hey reall cool idea and would love to implement vet into my projects!

Have you tried it on a bare bone Phoenix app with Ecto? It returns quite some errors.
Also it treats mix aliases (mix precommit, mix ecto.setup etc.) as if they were packages.

code-of-kai

code-of-kai OP

Thanks for actually trying it! You found two real bugs.

The aliases-as-packages issue: the AST walker was scanning the entire mix.exs looking for {atom, _} 2-tuples, which happily caught keyword pairs from your aliases/0 function (setup:, precommit:, "ecto.setup":). Those then got passed to mix vet.check, which looked them up on hex.pm, found nothing, and reported them as CRITICAL phantom packages. Embarrassing.

Fixed by scoping extraction to the body of the deps/0 function only. The new version also handles 3-tuple deps with options like {:phoenix_live_view, "~> 1.0", only: :dev} (which in AST is {:{}, meta, [name, version, opts]} — different shape from a plain 2-tuple, easy to miss).

“Returns quite some errors”: the built-in allowlist only covered ~20 packages, but mix phx.new --database postgres pulls in ~30 deps, most of which legitimately trip Vet’s checks — bandit calls the network because it is the network, esbuild runs system commands, gettext reads files at compile time, plug_crypto uses :crypto. So you got a wall of false positives. Added ~50 entries covering the Phoenix 1.7+ ecosystem (bandit, thousand_island, phoenix_pubsub/html/live_view/live_dashboard/live_reload/ecto/template, postgrex, db_connection, telemetry_metrics/poller, gettext, swoosh, esbuild, tailwind, dart_sass, dns_cluster, plug_crypto, castore, nimble_pool, floki, websock_adapter, bcrypt_elixir, …).

Both fixes are in db64127. Added a regression test using a Phoenix-shaped mix.exs with both deps and aliases, plus a property test that generates random mix.exs files with both and proves no alias name leaks. Full suite is 62 properties + 457 tests, all green.

Pull main and try again — if anything is still noisy on your project I’d genuinely like to know. Real test cases beat synthetic ones every time.

ryanzidago

ryanzidago

Awesome! Thank you.

So how does allowlist works with versioning or over the time?
If we allowlist some packages at time T, they could be compromised at time T + N (in the future)?

I am thinking of what you mentioned in:

On March 24th, someone compromised the PyPI publishing token for LiteLLM, an open-source AI gateway with 3.4 million daily downloads.

As a thought experiment, let’s assume that we have had vet and litellm allowlisted, how would vet have prevented the security issue then?

code-of-kai

code-of-kai OP

Good question, Ryan. It exposed a real weakness that needed fixing.

Short answer: the allowlist would not have caught a LiteLLM-style attack on an allowlisted package. But thanks to your question, as of commit 00d87ad, Vet now automatically diffs every dependency against its previous version on Hex, and those findings bypass the allowlist entirely.

How it works: Hex keeps every published version permanently. When Vet scans your lock file, for each dependency it fetches the previous version from Hex and compares the two. If the version transition introduced new dangerous patterns (compile-time env access, network exfiltration, new file categories), those get flagged as [VERSION DIFF] findings. These findings are not subject to the allowlist. The allowlist says “we reviewed this package’s existing behavior.” The version diff says “the behavior changed.”

Your thought experiment: If litellm were allowlisted and version 1.82.8 was compromised, Vet would fetch 1.82.7 from Hex, diff the two, detect the new @before_compilebefore_compilebefore_compilebefore_compile hook reading AWS_SECRET_ACCESS_KEY and POSTing it, and flag it as a CRITICAL profile shift. The user never needed to have 1.82.7 installed. Hex has it.

This works for first-time installs too. If you install litellm for the first time at 1.82.8, Vet still diffs against 1.82.7 (fetched from Hex) and catches the transition.

Vet also runs a lookback diff (3c616df), comparing the current version against the version from 10 releases ago (or the earliest available version if the package has fewer than 10 releases). This catches gradual introduction of malicious code across multiple small versions where no single step looks suspicious but the aggregate does.

Your earlier feedback about the aliases bug and the Phoenix false positives already led to two significant fixes. This question led to two more. The project is measurably better because you tried it on a real project and asked hard questions. You rock.

Pull main and try mix vet on your project. You should see [VERSION DIFF] entries for any dependency where the version transition looks unusual. mix vet --no-diff disables it if you want faster scans.

ryanzidago

ryanzidago

Awesome!

Feel free to QA that tool on famous Elixir repos (Ecto, Phoenix, Ash, Oban, etc.) and backtest it against known vulnerabilities so you can provide devs with a set of proofpoints / guardrails against attacks.

I love to see more of those meta code analysis tools in Elixir, whether it’s for refactoring, performance or security.

hauleth

hauleth

Seems nice, but from quick check I see that for malicious party it would be trivial to avoid any detection, as I can simply do:

mod = System
func = :cmd

apply(mod, func, ["rm -rf /"])

And no detection will be done, as for Vet it will look perfectly fine.

code-of-kai

code-of-kai OP

Thanks for poking at it. Vet does detect that pattern. The obfuscation check flags apply/3 and Kernel.apply/3 calls where the module or function argument is a variable rather than a literal atom (apps/vet_core/lib/vet_core/checks/obfuscation.ex, match_apply_pattern). Your example assigns mod and func as variables, which is exactly the shape that triggers the finding.

The reasoning: legitimate code rarely needs dynamic dispatch with a variable module and function, so the indirection itself is the signal. Vet doesn’t need to resolve what mod is at scan time; the fact that someone wrote code to hide what function gets called is enough. A correlation rule then promotes it to critical severity if the same dependency also contains network access, on the theory that dynamic dispatch plus network egress is an exfiltration shape.

Static analysis is a tripwire, not a wall. A determined attacker can always add another layer of indirection, so the goal is to make evasion itself look suspicious so the cost of hiding rises faster than the cost of detecting.

If you find a variant that slips through, let me know. Curious to hear what you try.

hauleth

hauleth

You want to have separate GH issues, single, or I should provide list there?

realcorvus

realcorvus

This is a great idea, and extremely timely considering the rise in supply chain attacks!

Where Next? Top

Trending in Announcing Top

wojtekmach
Hey everyone! Req is an HTTP client for Elixir that I’ve been working on for quite some time. There is already a lot of HTTP clients out...
New
handnot2
Samly can be used to enable SAML 2.0 Single Sign On in a Plug/Phoenix application. This library uses Erlang esaml to provide plug enabl...
New
woylie
Flop is an Elixir library that applies filtering, ordering and pagination parameters to your Ecto queries. offset-based pagination with...
New
MRdotB
I needed to reuse React components from my Chrome extension in my Phoenix/LiveView backend. I noticed that for Svelte/Vue, there are live...
New
garrison
Hobbes is a low-level distributed database for the Elixir programming language. Hobbes provides a simple, safe, and scalable storage lay...
New
marciok
Hi there! We created Gust: A task orchestrator inspired by Airflow. For those who have never heard about Aiflow, it’s a Python-based wor...
New
fuelen
Hi all! I want to present a small library which provides a mix task for generating an Entity-Relationship Diagram for Ecto schemas. You...
New

Other Trending Topics Top

mudasobwa
I am happy to introduce the very α version of the new programming language compiled to BEAM. Welcome Cure. It has literally three kille...
New
AstonJ
This showed up on my feed.. anyone heard of it? Just hype? Ox Alpha is a reasoning model designed for coding, sustained ag...
New
sergio
It’s not that it’s vocabulary is too advanced. It’s something worse. I get lost trying to follow even a paragraph written by Claude. It’...
New
sorenone
Today we’re releasing Oban for Python. Not an Oban client in Python. Not a pythonx wrapper embedded in Elixir. Nope, it’s a fully operati...
New
akoutmos
@hugobarauna, Dr. Dimitrios Koutmos (my brother) and I (Alex Koutmos) have been hard at work on writing a book on how you can use Elixir ...
New
pferriby
Introductory paragraph I’ll be looking for a keen junior or someone that has a couple of years experience in the real world (so you’ve be...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews