Skip to content

Fail mix hex.audit when a locked package can't be checked - #1250

Open
rubas wants to merge 1 commit into
hexpm:mainfrom
rubas:audit-fail-on-missing-registry-entry-signed
Open

rubas wants to merge 1 commit into
hexpm:mainfrom
rubas:audit-fail-on-missing-registry-entry-signed

Conversation

@rubas

@rubas rubas commented Sep 29, 2026

Copy link
Copy Markdown

Summary

mix hex.audit passes when it can't read a locked package's registry entry. Here is a project that locks mint 1.10.1, which has three advisories, run with the registry unreachable and no cache:

$ HEX_HOME=$(mktemp -d) HEX_MIRROR_URL=http://127.0.0.1:1 mix hex.audit
Failed to fetch record for mint from registry
Failed to check for new Hex version
{:failed_connect, [{:to_address, {~c"127.0.0.1", 1}}, {:inet, [:inet], :econnrefused}]}
{:failed_connect, [{:to_address, {~c"127.0.0.1", 1}}, {:inet, [:inet], :econnrefused}]}
Failed to fetch record for hpax from registry
{:failed_connect, [{:to_address, {~c"127.0.0.1", 1}}, {:inet, [:inet], :econnrefused}]}
No retired or security advisory packages found
$ echo $?
0

With the registry reachable, the same project lists the three advisories and exits with 1. Hex 2.5.1 does the same, except it also says "(using cache instead)".

  • A failed fetch stores nothing, so Registry.advisories/3 and Registry.retired/3 return nil, and Hex.Audit treats nil as "nothing found".
  • A CI job that runs mix hex.audit goes green when the registry is down, or when a proxy, a mirror, or missing credentials block it.
  • --format sarif writes an empty report in the same case.

This PR makes Hex.Audit.run/3 check that the registry has an entry for each locked package version before it looks at findings. verify_dep/3 in Hex.RemoteConverger already does the same check for mix deps.get:

defp registry_entry?(repo, package, version) do
  case Registry.versions(repo, package) do
    {:ok, versions} -> version in Enum.map(versions, &to_string/1)
    :error -> false
  end
end

The same run on this branch:

$ HEX_HOME=$(mktemp -d) HEX_MIRROR_URL=http://127.0.0.1:1 mix hex.audit
Failed to fetch record for mint from registry
Failed to check for new Hex version
{:failed_connect, [{:to_address, {~c"127.0.0.1", 1}}, {:inet, [:inet], :econnrefused}]}
{:failed_connect, [{:to_address, {~c"127.0.0.1", 1}}, {:inet, [:inet], :econnrefused}]}
Failed to fetch record for hpax from registry
{:failed_connect, [{:to_address, {~c"127.0.0.1", 1}}, {:inet, [:inet], :econnrefused}]}
** (Mix) Could not audit mint 1.10.1, hpax 1.1.0, the registry entry for the locked version could not be fetched and is not cached locally
$ echo $?
1

A cached copy still works. With the network cut and a warm cache, the audit says "(using cache instead)", lists the three advisories, and exits with 1.

Testing

  • mix test --exclude integration: 360 tests pass.
  • New test/hex/audit_test.exs, with the registry down. A missing entry raises, a cached entry without the locked version raises, and a cached entry with an advisory reports it. The first two fail without this change.
  • The runs above use an archive built from this branch, on Elixir 1.20.4 and OTP 29.1.1.
Cause and design

Hex.Registry.Server.write_result/4 prints the fetch error and stores nothing. It used to raise "Stopping due to errors" after that when nothing was cached. But cached? = !!:ets.lookup(...) was always true, so the raise never ran. #1247 fixed the check and dropped the raise, since callers raise when data is missing, for example "No package with name X (from: mix.exs) in registry" during resolution. Hex.Audit doesn't. It only reads advisories and retired, and both return nil for a missing entry.

The check is in Hex.Audit.run/3, so mix hex.audit and the policy_enforce_lock check in mix deps.get both get it. In mix deps.get it never fires, because verify_lock/1 has already raised by then. It uses Registry.versions/2 because that call tells a missing entry (:error) apart from an entry with no advisories. When a fetch fails but a cached copy exists, it returns the cached copy.

It also checks the locked version. A stale mirror or cache without that release gives the same silent pass.

Offline mode already raises for an uncached package in prefetch_offline/2. Now the online path fails the same way.

@ericmj

ericmj commented Sep 29, 2026 •

Copy link
Copy Markdown
Member

Should we fail the audit when we fail to update the registry for any package? New advisories can arrive at any time so even if it's cached it can be outdated. Maybe we should have a strict mode?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants