Skip to content

feat(elixir): gate on Hex's live advisory feed, not just mix_audit's mirror - #67

Merged
nkg merged 1 commit into
mainfrom
fix/hex-advisory-gate
Aug 24, 2026
Merged

feat(elixir): gate on Hex's live advisory feed, not just mix_audit's mirror#67
nkg merged 1 commit into
mainfrom
fix/hex-advisory-gate

Conversation

@nkg

@nkg nkg commented Aug 24, 2026

Copy link
Copy Markdown
Owner

Fixes the detector half of HordiaLabs/scraper-control#69.

The problem

run-deps-audit is treated as the dependency security gate by consumers. It can't carry that alone.

mix_audit resolves advisories from exactly one source — mirego/elixir-security-advisories, itself a sync of the GitHub Advisory Database. An advisory that hasn't reached that mirror isn't reported at all.

Observed on scraper-control today, same mix.lock, same machine:

$ mix deps.audit
No vulnerabilities found.

$ mix deps.get
  bandit 1.12.4 VULNERABLE!
    EEF-CVE-2026-75484 (MEDIUM)  HTTP/2 header values with CR/LF/NUL passed through unvalidated
    EEF-CVE-2026-74836 (HIGH)    HTTP/2 connection-window starvation pins Plug processes
  decimal 2.4.1 VULNERABLE!
    EEF-CVE-2026-32686 (MEDIUM)

This is not the stale-cache story it looks like. The issue guessed that mix_audit vendors its DB at build time and goes stale in the CI cache. It doesn't — MixAudit.Repo git-clones/pulls the mirror into ~/.local/share/elixir-security-advisories-mirego on every run. The local clone was already at upstream HEAD, and neither bandit advisory exists anywhere in that repo. Busting the build cache or rebuilding mix_audit, the issue's option 1, would have changed nothing.

A gate that reports green off data it never had is worse than no gate, because CI is trusted.

The fix

run-hex-advisory-check parses the VULNERABLE! blocks Hex prints during resolution. Hex queries the live feed on every deps.get, so it's current by construction.

  • hex-advisory-ignore takes comma-separated ids, matched against the primary id or any aka: alias — so the GHSA ids consumers already pass to mix_audit's --ignore-advisory-ids work verbatim, and the two lists stay in one dialect. (scraper-control needs this for GHSA-rhv4-8758-jx7v: decimal 3.0 has the fix, but ecto pins decimal ~> 2.0.)
  • Ignored advisories still print as ::notice:: — they stay visible rather than vanishing.
  • It refuses to fail open. If Hex reports advisories and the parser extracts none, the step errors on suspected format drift. That's the specific bug being replaced; the replacement shouldn't be able to repeat it.

Defaults

Off by default, and run-deps-audit stays. Turning this on can red-line a repo whose deps carry an untriaged advisory, so adoption should be per-repo and deliberate rather than arriving unannounced with a @v2 bump. Worth flagging: every other Elixir repo on @v2 has the same blind spot until it opts in.

The two sources are complementary — neither is a superset — so the recommendation is both. run-deps-audit's input description now says so explicitly, so the next reader doesn't assume it's sufficient.

Testing

Parser exercised against real captured Hex output, all paths verified:

case result
3 advisories, no ignores exit 1, all 3 reported
multiple advisories on one package both parsed separately
ignore decimal by GHSA alias exit 1, bandit still blocks, decimal → notice
all ignored exit 0
clean output exit 0
VULNERABLE! present but unparseable exit 1, drift error

actionlint + shellcheck clean.

🤖 Generated with Claude Code

…mirror

`run-deps-audit` was being trusted as *the* dependency security gate, and it
cannot carry that weight on its own. mix_audit resolves advisories from a
single source — mirego/elixir-security-advisories, a sync of the GitHub
Advisory Database — so an advisory that has not landed in that mirror is not
reported at all.

Seen on a consumer repo today: `mix deps.audit` printed "No vulnerabilities
found." while `mix deps.get`, against the same mix.lock, flagged three
advisories including a HIGH (bandit EEF-CVE-2026-74836, HTTP/2 connection-
window starvation). The local mirror clone was at upstream HEAD, so this is
not the stale-cache story it first looks like — a cache-bust or a fresh
mix_audit build would have changed nothing. The data was never there.

That is the dangerous shape of failure: not a gate that breaks, but a gate
that returns green off data it did not read, on a signal people rely on.

Adds `run-hex-advisory-check`, which parses the `VULNERABLE!` blocks Hex emits
during resolution — a live feed, current by construction — and fails on any
advisory not named in the new `hex-advisory-ignore` input. Ignores match the
primary id or any `aka:` alias, so the GHSA ids consumers already pass to
mix_audit's --ignore-advisory-ids work unchanged; ignored advisories are still
printed as notices so they stay visible rather than vanishing.

Learning from the bug it replaces, the parser refuses to fail open: if Hex
reports advisories and the parser extracts none, the step errors on suspected
output-format drift instead of passing quietly.

Off by default — switching it on can red-line a repo with an untriaged
advisory, so adoption stays deliberate and per-repo. run-deps-audit stays; the
two sources are complementary and neither subsumes the other. Its input
description now says so, so the next reader doesn't assume it is sufficient.

Verified against captured Hex output: multiple advisories on one package,
ignore-by-GHSA-alias, all-ignored, clean, and format-drift all behave.
actionlint + shellcheck clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YEMvdSY4vBpcbE2cP1MP7A
@nkg
nkg merged commit 21697db into main Aug 24, 2026
11 checks passed
@nkg
nkg deleted the fix/hex-advisory-gate branch August 24, 2026 10:26
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.

1 participant