😮 You looked at the source!

Dimitris Zorbas

Code spells, smells and sourcery

A Guide to Secure Elixir Package Updates

Keeping your dependencies up-to-date is essential to ensure that your applications stay healthy, secure, and performant. Thankfully, the BEAM ecosystem has its own package manager, Hex, which is fast, mature, and simple to use.

This article explores the available tools and commands to manage Hex dependencies and some tips to make the process more enjoyable.

Let’s dive in!

List Updatable Dependencies in Your Elixir App

You can use the commands below to understand the relationships between dependencies before you attempt to update any of them.

List all your application’s dependencies with:

mix deps --all
* bunt 0.2.0 (Hex package) (mix)
  locked at 0.2.0 (bunt) 7af5c7e0
  ok
* castore 0.1.15 (Hex package) (mix)
  locked at 0.1.15 (castore) c69379b9
  ok
* connection 1.1.0 (Hex package) (mix)
  locked at 1.1.0 (connection) 722c1eb0
  ok
* cowboy 2.9.0 (Hex package) (rebar3)
  locked at 2.9.0 (cowboy) 2c729f93
  ok
* cowboy_telemetry 0.4.0 (Hex package) (rebar3)
  locked at 0.4.0 (cowboy_telemetry) 7d98bac1
  ok

Or you can choose to print them in a tree format with:

mix deps.tree

This produces output like the following:

short
├── credo ~> 1.6 (Hex package)
│   ├── bunt ~> 0.2.0 (Hex package)
│   ├── file_system ~> 0.2.8 (Hex package)
│   └── jason ~> 1.0 (Hex package)
├── ecto_psql_extras ~> 0.6 (Hex package)
│   ├── ecto_sql ~> 3.4 (Hex package)
│   ├── postgrex >= 0.15.7 (Hex package)
│   └── table_rex ~> 3.1.1 (Hex package)

To produce an image output, run:

mix deps.tree --format dot && dot -Tpng deps_tree.dot -o deps_tree.png

Note: This option requires Graphviz. Read these Graphviz instructions to install it on your system.

Then open the created deps_tree.png file with a viewer of your choice. Being able to quickly visualize dependencies can help you decide whether a package is worth keeping in your mix.lock. A package could be pulling too many sub-dependencies or might not even be used in your app. Remember, the fewer dependencies, the easier it is to keep things up-to-date.

When you remove a dependency from mix.exs, it will remain in mix.lock. To remove unused dependencies, run:

mix deps.clean --unlock --unused

Check for Outdated Dependencies with Hex

After making sense of your dependencies tree and performing any necessary cleanups, check for outdated packages with:

mix hex.outdated --all

The output will resemble:

Dependency              Current  Latest  Status
bunt                    0.2.0    0.2.0   Up-to-date
cowlib                  2.11.0   2.11.0  Up-to-date
credo                   1.6.1    1.6.3   Update possible
db_connection           2.4.1    2.4.1   Up-to-date
decimal                 2.0.0    2.0.0   Up-to-date
earmark_parser          1.4.19   1.4.20  Update possible
postgrex                0.15.13  0.16.2  Update not possible

To view the diffs in each available update, visit:
https://hex.pm/l/AsY7q

Note: The --all flag shows all outdated packages, including the children of packages defined in mix.exs.

Notice the link in the output above. Hex prepares a nice page for us to inspect the diffs:

hexdiff

Pro-Tip

Use the --within-requirements flag in your CI to notify you of available updates.

mix hex.outdated --within-requirements 1>/dev/null || echo 'Updates available!'

The output of Elixir’s package management tasks tends to be concise, well-documented and precisely guides you towards actions.

Inspecting Changes with Hex

To see changes between two package versions in the terminal, run:

# This will display the diff for the package opus
mix hex.package diff opus 0.7.0 0.8.1
diff --git a/var/folders/nc/t881hgn/T/opus-0.7.0-9F0FC44B/mix.exs b/var/folders/nc/t881/T/opus-0.8.1-2BEF6AED/mix.exs
index 0aba420..0c1ad3e 100644
--- a/var/folders/nc/t881/T/opus-0.7.0-9F0FC44B/mix.exs
+++ b/var/folders/nc/t881/T/opus-0.8.1-2BEF6AED/mix.exs
@@ -4,7 +4,7 @@ defmodule Opus.Mixfile do
   def project do
     [
       app: :opus,
-      version: "0.7.0",
+      version: "0.8.1",
       elixir: "~> 1.6",
       elixirc_paths: elixirc_paths(Mix.env()),
       build_embedded: Mix.env() == :prod,
@@ -39,7 +39,7 @@ defmodule Opus.Mixfile do
   defp deps do
     [
       {:retry, "~> 0.8"},
-      {:telemetry, "~> 0.4", optional: true},
+      {:telemetry, "~> 0.4 or ~> 1.0", optional: true},
       {:credo, "~> 0.8.10", only: [:dev, :test], runtime: false},
       {:ex_doc, "~> 0.24.2", only: :dev, runtime: false},
       {:dialyxir, "~> 1.0.0-rc.3", only: [:dev, :test], runtime: false},

You can view the diff in the browser by navigating to:

https://diff.hex.pm/diff/<package_name>/<version1>..<version2>

For example: https://diff.hex.pm/diff/opus/0.7.0..0.8.1

Hex Diff generates a highlighted git diff which you can view in the browser. You can share the link or even highlight a specific row.

Third-party dependencies are essentially somebody’s code downloaded from the internet, which ends up in your application. There is no shortage of examples where packages have been hijacked and malicious versions uploaded.

Ideally, you should inspect the diff of every update. Hex seems to be the only package manager with this built-in feature at the moment.

Browsing Changelogs

Ultimately, an update might be available, but is it safe to apply it? Are there any code or configuration changes required for the update to work without issues? The diff between two package versions may contain thousands of lines of templates, tests, and docs that might not seem relevant to you.

Furthermore, a package might not even follow Semver (semver indicates whether the update is safe in compatibility terms).

Commonly, package maintainers keep a changelog to communicate notable changes and upgrade paths concisely. Read more about the benefits of keeping a changelog.

Now the bad news: not all packages have a changelog. So let’s go changelog hunting!

The following task will fetch information for the credo package:

mix hex.info credo
A static code analysis tool with a focus on code consistency and teaching.

Config: {:credo, "~> 1.6"}
Locked version: 1.6.3
Releases: 1.6.3, 1.6.2, 1.6.1, 1.6.0, 1.6.0-rc.1, 1.6.0-rc.0, 1.5.6, 1.5.5, ...

Licenses: MIT
Links:
  Changelog: https://github.com/rrrene/credo/blob/master/CHANGELOG.md <--- Here
  GitHub: https://github.com/rrrene/credo

As you can see, the maintainer has added a link to the changelog, so that’s nice of them.

There is even a link to the changelog in the HexDocs, which some developers may find really handy:

hexdocs

Tips:

  • Ensure there’s a link to your changelog in mix.exs
  • Include the changelog in the hexdocs

Automated Changelog Fetching

Hunting for changelogs can get tedious after a while, especially if you want to update many packages. Thankfully, there is now an experimental package for that.

You can add it in your dependencies with:

defp deps do
  [
    {:changelog, "~> 0.1", only: [:dev, :test], runtime: false}
  ]
end

Ensure it is fetched:

mix deps.get

Invoke it for all updatable packages:

mix changelog

Or for a number of packages:

mix changelog tailwind jason

Package: tailwind
Current version: 0.1.4
Latest version:  0.1.5
Hexdiff: https://diff.hex.pm/diff/tailwind/0.1.4..0.1.5

## v0.1.5 (2022-01-18)
  * Prune app.js css import to remove required manual step on first install

Note: This will only print the version to update to, a link to the diff, and the changelog when there is a more recent version.

I wrote this open-source task. It uses some heuristics to locate the changelog by retrieving the Hex package metadata from the API, falling back to common locations in the repo.

My wish is that Hex will standardize including a link to a changelog in mix.exs and mix hex.info, and that Hex Diff will be enhanced to include changelog information.

Updating Dependencies in Elixir

To update all dependencies, run:

mix deps.update --all

In the output, you will see version updates in the following format:

Upgraded:
  credo 1.6.1 => 1.6.3
  earmark_parser 1.4.19 => 1.4.20
  ecto_sql 3.7.1 => 3.7.2
  ex_doc 0.27.3 => 0.28.2 (minor)
  makeup 1.0.5 => 1.1.0
  mint 1.4.0 => 1.4.1
  nimble_parsec 1.2.0 => 1.2.2
  nimble_pool 0.2.5 => 0.2.6
  phoenix_live_dashboard 0.6.2 => 0.6.5
  phoenix_live_view 0.17.5 => 0.17.7
  phoenix_view 1.1.0 => 1.1.2
  plug 1.12.1 => 1.13.3
  postgrex 0.15.13 => 0.16.2 (minor)
  tailwind 0.1.4 => 0.1.5
New:
  hpax 0.1.1

This is the same as running:

mix deps.unlock --all && mix deps.get

Keep in mind that this task will try to upgrade to versions that match the specifications in your mix.exs.

For example:

def deps do
  [
    {:some_package, "~> 0.9"}
  ]
end

Even if there is a more recent 1.0 version for some_package, it does not match the specification above, and it won’t be upgraded. You will have to change your mix.exs and try again.

As we saw in the first section of this article, there is a task you can run to check if an update is possible for a package.

mix hex.outdated some_package
Dependency              Current  Latest  Status
some_package            0.9.0    1.0.0   Update not possible

There might be a conflict in some cases where the package resolution cannot find a version to satisfy the dependencies in mix.exs.

A workaround (that you should use with caution) is override.

def deps do
  [
    {:some_package, "~> 1.0"},
    {:other_package, "~> 2.0", override: true}
  ]
end

In this case, the dependency will override any other definitions by other dependencies.

Further Reading: Keep Your Package Updates Safe in Elixir

Elixir comes with a whole arsenal of tools to manage dependencies. Try to master them and stir your mix.exs often. Read changelogs and diffs to ensure your updates are safe.

Here are some resources that can help you dive deeper into this topic:

Happy coding!