code-of-kai

code-of-kai

Vet - Elixir dependency security scanner

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

Most Liked

code-of-kai

code-of-kai

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.

code-of-kai

code-of-kai

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.

hauleth

hauleth

I have some:

  • :ssh and :ssh_sftp aren’t checked - I can freely open some connection to remote server or open remote shell
  • :ftp is not protected, so I can download or upload any data I want
  • :httpd is not checked, so the second “hidden” HTTP server can be ran
  • I can use :prim_file module - while it isn’t public API, I think that attacker will not give a damn about breaking API (and once upon a time I needed genuinely use that module, because “stable” API do not provide such functionality)
  • I can run any external executable using Mix.shell().cmd(…) (which is by the way an approach that should be used in Mix tasks)
  • :erlang.open_port is free to be used
  • I can use Tesla or hackney or other HTTP client just fine, the same with sockets where I can use procket library to open any unsafe socket
  • I can use defdelegate to circumvent any function call check
    defmodule TestMod do
      defdelegate foo(str), to: String, as: :to_atom
    
      def convert(str) do
        foo(str)
      end
    end
    
  • While I cannot use apply I can still write function like def call(mod), do: mod.connect(~c"i.will.not.to.anything.promise.dev", []) and be able to connect to remote server
  • I can use def "$handle_undefined_function"(name, args) to do more nasty things
  • I can use :inet_res functions to leak data by sending prepared DNS requests containing private data
  • I can use :epp.scan_file to read file content (it will be mangled, but in many cases it may be possible to extract required data from there

I appreciate your effort, the problem I see is that Erlang VM is just unsafe VM and I do not believe that there is a way to do it in a way, that will not create false sense of security. I strongly believe that tool that provide false sense of security is worse option than no tool at all.

What this tool currently provide is more of a sanity check against accidental data leakage than checking against actually malicious party trying to obscure their actions. It may be useful anyway, just not as a defence against malicious library providers (I deliberately do not use term “supply chain”, as open source authors aren’t “supply chain”).

If you want to secure against actively malicious parties, then:

  1. You need to think as malicious attacker. If someone wants to hide what they are doing, they for sure will not fall into trap where you can simply use “grep on steroids” to see that it is doing bonkers stuff
  2. In my opinion if someone want to hide what they are doing, then without higher level sandbox (either Erlang VM level or OS level) you cannot really catch that. This library can be circumvented in super simple way already - just write some Erlang module in your project, and you are safe to go and do anything, as it scans only Elixir code.

Last Post!

hauleth

hauleth

It often does not. I do not have access to sensible AI reviewer, but I think it could hiccup on code that looks innocent to people, but is malicious when used together. For example a Vet (and I think neither will any other AI agent) will see anything particularly wrong in using :ranch_tcp.listen or stuff like that. Especially when you will do that across different libraries.

Obfuscation of the attack do not mean that the code is less readable, it makes code flow harder to comprehend.

One of the possible vectors of the obfuscation I see is that someone will write code, where in mix.exs they will do “innocent” mistake in form of:

defp elixirc_paths(:dev), do: ["lib"]
defp elixirc_paths(_), do: ["lib", "test/support"]

And now they will put malicious code in test/support/file_used_only_in_tests_i_promise.ex. Now it will contain module named for example NoHarmPromise.FooControler while in lib/no_harm_promise/foo_controller.ex they will have NoHarmPromise.FooController. In their router they did another silly mistake and they have written post "/nothing-imporant", FooControler. But now, for both the LLM as well as human, it may look like there is no harm really. Code compiles, tests passes, but the code has slightly “unexpected” behaviour.


It is getting off topic there, but I have tried to assess known and intentionally malicious code from Underhanded C Competition from 2015 (so any LLM should have it in their learning corpus). It failed terribly there in both Gemini as well as ChatGPT (I do not have accounts for other LLMs so I didn’t bother). Both have noticed some small problems (like NaN handling or potential division by 0), but none catches the elephant in the room.

I do not really believe, that with determined enough attacker (who also had access to all these tools) it could be avoided. Especially with Jia Tan-level of dedication to prepare the attack.

Where Next?

Popular in Announcing Top

alisinabh
Hey everyone i’ve developed a library for Jalaali calendar for elixir which supports converting Gregorian dates to Jalaali and vice vers...
New
tmbb
I’ve decided to create this topic to discuss optimization possibilities for something like Phoenix LiveView. I’ve created this topic unde...
144 10789 141
New
RobertDober
Earmark is a pure-Elixir Markdown converter. It is intended to be used as a library (just call Earmark.as_html), but can also be used as...
239 12888 134
New
maltoe
Hello! Came here to announce ChromicPDF, a pet project PDF generator I’ve been working on for the past few months. Why another PDF gener...
New
kevinlang
Hey all, We have made an Ecto3 Adapter for SQLite3, ecto_sqlite3! We have successfully on-boarded the full suite of integration tests (...
New
tfwright
After working on it for a couple of months and using it in production for most of that time, today I’ve released LiveAdmin, a LiveView ba...
New
restlessronin
The repo is at GitHub - cyberchitta/openai_ex: Community maintained Elixir library for OpenAI API · GitHub. Docs are at OpenaiEx User Gu...
152 10719 134
New

Other popular topics Top

KronicDeth
Elixir plugin for JetBrain’s IntelliJ Platform (including Rubymine) This is a plugin that adds support for Elixir to JetBrains IntelliJ...
289 36654 110
New
vonH
In asking this question I am more interested about the expressiveness of the language itself and less concerned about the availability of...
New
axelson
This post is a wiki (feel free to hit the edit button near the bottom right of this post to add your own changes!) This post collects co...
239 49084 226
New
SoCreat
i’m a new one to elixir which editor can i use vs code? or atom? Thanks! :smiley:
New
TunkShif
This post is an instruction guide to help you setup your Neovim for Elixir development from scratch. It includes general information on h...
274 42533 114
New
jason.o
In the code below, if the create action is not set to accept “extra_key” as an input, it errors out with a message shown above. Is there ...
New

We're in Beta

About us Mission Statement