Elixir and Phoenix have a reputation for reliability, but reliability isn't the same as security. A vulnerable dependency sitting in your mix.lock file or an unsanitized Phoenix.HTML.raw/1 call in a template can slip past code review just as easily as it would in any other stack. This mix_audit Sobelow tutorial walks through setting up both tools in a real Phoenix project, running them locally and in CI, and interpreting the findings so you're not just generating noise you'll learn to ignore.
By the end, you'll have mix_audit checking your dependency tree against the Elixir Security Advisories database, Sobelow performing elixir static analysis security checks against your controllers and templates, and a repeatable workflow that catches issues before they reach production. We'll also cover the configuration tweaks that keep both tools useful instead of muted, and where a platform like Safeguard fits once manual scans stop scaling.
Step 1: Set Up Your mix_audit Sobelow Tutorial Environment
Both tools install as Mix dependencies, so there's no separate binary to manage outside your project. Add them to mix.exs as dev-only, non-runtime dependencies:
defp deps do
[
{:mix_audit, "~> 2.1", only: [:dev, :test], runtime: false},
{:sobelow, "~> 0.13", only: [:dev, :test], runtime: false}
]
end
Fetch and compile them:
mix deps.get
mix deps.compile mix_audit sobelow
Confirm both tasks are registered:
mix help | grep -E "audit|sobelow"
You should see mix deps.audit and mix sobelow listed. If either is missing, double-check the dependency was pulled in for the environment you're running (MIX_ENV=dev is the default, so this usually just works).
Step 2: Run an Elixir Dependency Audit with mix_audit
mix_audit cross-references every package in mix.lock against a local mirror of the Elixir Security Advisories repository, flagging known CVEs and unmaintained packages. Run your first elixir dependency audit with:
mix deps.audit
On first run, it clones the advisory database to ~/.cache/mix_audit (or %LOCALAPPDATA% on Windows) and checks it against your lockfile. A clean project prints "No vulnerabilities found." A flagged one looks like this:
mix_audit found a vulnerability!
Package: plug
Version: 1.13.6
CVE: CVE-2023-XXXXX
Advisory: https://github.com/dependabot/advisory-database/...
Solution: Upgrade to >= 1.14.0
Update the advisory database periodically so you're not auditing against stale data:
mix deps.audit --update-advisories
Because mix_audit only reads mix.lock, it's fast enough to run on every commit. Treat any hit as a blocking finding, not a suggestion — upgrade the package or, if a patch isn't available yet, document the accepted risk with an expiry date.
Step 3: Run Sobelow as Your Phoenix Security Scanner
Where mix_audit looks at what you depend on, Sobelow looks at what you wrote. It's a phoenix security scanner purpose-built to understand routers, controllers, channels, and EEx/HEEx templates, and it checks for the vulnerability classes that actually show up in Phoenix apps: XSS via unescaped output, SQL injection through raw Ecto fragments, insecure config (CSRF disabled, misconfigured CORS), command injection, and traversal issues in Plug.Static or file uploads.
Run a scan from your project root:
mix sobelow
Point it explicitly at your web app's router if you have an umbrella project:
mix sobelow --router lib/my_app_web/router.ex
For a quick severity-only view during code review:
mix sobelow --format compact
Sobelow groups findings by category (Config, XSS, SQL.Query, Traversal, RCE, Vuln.Vulnerable, etc.) and assigns a confidence level. High-confidence hits on SQL.Query or RCE deserve immediate attention; low-confidence Config warnings are worth a look but rarely block a merge.
Step 4: Tune Findings with a .sobelow-conf File
Out of the box, Sobelow will flag things your team has already decided are acceptable — maybe you intentionally allow raw HTML in a CMS field with a separate sanitizer, or you've reviewed a specific Config.HTTPS warning in staging. Rather than silencing checks ad hoc on the command line, commit a .sobelow-conf file so the exceptions are visible and reviewable:
[
verbose: false,
private: true,
skip: false,
ignore: ["Config.HTTPS", "XSS.Raw"],
router: "lib/my_app_web/router.ex",
exit: "high"
]
The exit key is the important one for automation: set it to high, medium, or low to control the severity threshold at which Sobelow returns a non-zero exit code. Anything below that threshold still prints but won't fail your build.
Step 5: Wire Both Tools into CI
A tool that only runs on a developer's laptop is a tool that gets skipped under deadline pressure. Add a dedicated job to your GitHub Actions workflow so every pull request gets both checks automatically:
name: security-audit
on: [pull_request]
jobs:
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: erlef/setup-beam@v1
with:
otp-version: "26.x"
elixir-version: "1.16.x"
- run: mix deps.get
- name: Elixir dependency audit
run: mix deps.audit
- name: Phoenix static analysis
run: mix sobelow --exit low
Cache the deps and _build directories in a real pipeline to keep this fast, and consider running mix_audit on a nightly schedule too, since a dependency can become vulnerable after a new CVE is published even if your code hasn't changed.
Step 6: Triage Findings Instead of Drowning in Them
The fastest way to kill adoption of any elixir static analysis security tooling is to let it produce a wall of unreviewed warnings. After your first full scan:
- Fix or upgrade anything mix_audit flags first — dependency CVEs are usually a one-line version bump.
- Sort Sobelow output by confidence, and clear every
highfinding before touchingmediumorlow. - For accepted-risk findings, add them to
.sobelow-confwith a comment explaining why, and set a calendar reminder to revisit. - Re-run both tools after every dependency upgrade, not just on a schedule —
mix deps.updatecan introduce new lockfile entries that need auditing too.
Troubleshooting and Verification
"mix sobelow" command not found. The task is registered at compile time. Run mix deps.compile sobelow explicitly, and confirm you're not running under MIX_ENV=prod, where dev-only dependencies aren't compiled.
mix_audit reports nothing even though you expect a hit. Check that the advisory database actually downloaded — look for a populated ~/.cache/mix_audit directory. Corporate proxies or offline CI runners sometimes block the initial git clone; you can vendor the advisory database or set MIX_AUDIT_ADVISORY_PATH to a pre-cloned checkout.
Sobelow flags a false positive in generated code. Phoenix-generated boilerplate (from mix phx.gen.auth, for example) occasionally trips Config or XSS.Raw checks depending on your version pairing. Verify the generator version matches the Sobelow release notes before assuming it's a real bug, then add a scoped ignore rather than disabling the whole category.
CI passes locally but fails in the pipeline. This is almost always a lockfile drift issue — someone updated mix.lock locally without committing it, or the CI runner resolved different dependency versions. Run mix deps.get --check-locked to confirm the lockfile is authoritative before debugging the scanners themselves.
Verify your setup end to end by intentionally introducing a known-bad pattern, such as Plug.Conn.send_resp(conn, 200, raw_user_input) in a scratch branch, and confirming Sobelow catches it, then reverting. Do the same for mix_audit by temporarily pinning a package version with a known CVE in mix.lock and confirming mix deps.audit reports it. Both checks should fail loudly; if they don't, your configuration is too permissive.
How Safeguard Helps
mix_audit and Sobelow are the right starting point for any Elixir or Phoenix codebase, but they're point-in-time, per-repo checks — someone still has to run them, read the output, and remember to re-scan after every dependency bump. Safeguard extends that same coverage across your entire software supply chain: continuous dependency monitoring that catches newly disclosed CVEs the moment they're published (not just at your next CI run), aggregated findings from Sobelow-style static analysis alongside SBOM and provenance data, and policy gates that block a release when a high-confidence finding lands, instead of relying on someone reviewing a log.
For teams running Elixir and Phoenix services alongside everything else in a polyglot stack, Safeguard gives security and platform teams one place to see dependency risk, code-level findings, and build provenance together — so the mix_audit Sobelow tutorial you just ran locally becomes an organization-wide guardrail instead of a one-off checklist.