Supply Chain Analysis (SCA)
Supply Chain Analysis (SCA) scans your project's dependency lock file for known vulnerabilities. It checks your pinned dependency versions against a regularly-updated advisory database and reports any packages that have known CVEs or security advisories.
SCA fetches the advisory database fresh from OSV.dev on every run — no local cache, and no offline mode. A network connection is required.
Quick Start
Run safe sca from your project root. SAFE will automatically detect a mix.lock (Elixir) or rebar.lock (Erlang) file in the current directory:
safe sca
If both mix.lock and rebar.lock exist in the same directory, use --lock-file to specify which one to scan.
Usage
safe sca [--lock-file LOCK_FILE] [--ignore-file PATH] [--warnings-as-errors]
Arguments
Optional:
-
--lock-file LOCK_FILEPath to the lock file to scan. Supports bothmix.lock(Elixir) andrebar.lock(Erlang). If omitted, SAFE looks formix.lockorrebar.lockin the current directory. -
--ignore-file PATHPath to an SCA ignore file. Defaults to.safe/sca_ignore.jsonin the same directory as the lock file. See Suppressing SCA Findings for the file format. -
--warnings-as-errorsTreat warnings as errors. Three things normally produce a warning: non-hex/non-pkg dependencies (:git,:path, etc.), hex dependencies whose version SAFE cannot read, and advisory data for packages other than your dependencies that failed to parse (see Advisory Database). This flag causes the scan to exit with code3if any of them occur. -
--allow-unreadable-advisoriesContinue when the advisory database contains a record SAFE cannot read at all. Such a record can't be tied to any package, so SAFE cannot rule out that it covered one of your dependencies — by default the scan fails (exit code1) rather than report "clean" with an unknown gap in the data behind it. Use this flag to keep a pipeline moving while the upstream data is fixed; the gap is still reported on every run. See Advisory Database.
Examples
# Auto-detect lock file in current directory
safe sca
# Scan a specific lock file
safe sca --lock-file /path/to/mix.lock
safe sca --lock-file /path/to/rebar.lock
# Use a custom ignore file
safe sca --ignore-file /path/to/sca_ignore.json
# Fail if non-hex dependencies are present
safe sca --warnings-as-errors
# Don't fail the scan over an advisory record SAFE couldn't read
safe sca --allow-unreadable-advisories
Supported Lock Files
| Lock file | Ecosystem | Checked dependencies |
|---|---|---|
mix.lock | Elixir / Mix | :hex packages only |
rebar.lock | Erlang / Rebar3 | :pkg (Hex) packages only |
Only dependencies fetched from the Hex package registry can be checked for vulnerabilities. Dependencies sourced from Git, local paths, or other non-Hex sources cannot be version-matched against the advisory database and will produce a warning instead.
Understanding the Output
When vulnerabilities are found:
Lock file: /your/project/mix.lock
Found vulnerable dependencies:
- bandit (1.11.0)
CVE: CVE-2026-65623
Severity: high (8.7)
CWE: CWE-407
Title: Quadratic CPU blow-up reassembling fragmented WebSocket messages in Bandit
Affected: >= 1.11.0 and < 1.12.1
More: https://osv.dev/vulnerability/EEF-CVE-2026-65623
Each vulnerable dependency lists:
- The installed version
- CVE identifier (if available)
- Severity and CVSS score, computed from the advisory's CVSS vector when one is published
- CWE identifier(s), when known
- Advisory title
- The affected version range
- A link to the full advisory on osv.dev
When the scan is clean:
Lock file: /your/project/mix.lock
No vulnerable dependencies found.
When advisories have been suppressed via the ignore file:
Ignored: 2 advisory(ies) for: hackney, oidcc (see .safe/sca_ignore.json)
When non-Hex dependencies are present:
3 non-hex dependencies found that cannot be checked for vulnerabilities:
- my_git_dep (git: tag: v1.0.0)
- my_path_dep (path: ../my_path_dep)
- my_branch_dep (git: branch: main)
When a dependency's version cannot be read:
1 dependency has a version SAFE could not read, and so could not be checked for vulnerabilities:
- my_dep (1.2.3.4)
Versions missing a component (1.0, 2.0-rc1) are padded and checked normally. A version that
can't be interpreted at all is reported here rather than treated as clean — SAFE cannot match it
against any advisory range, and that must not look the same as "no vulnerabilities found".
Suppress with ignored_non_hex_packages in .safe/sca_ignore.json.
When advisory data for one of your dependencies could not be parsed:
Could not parse advisory data for: hackney. This shouldn't
happen with the default database — please report it at safe@erlang-solutions.com.
The scan cannot be trusted until this is resolved, since SAFE could not check whether
these dependencies have known vulnerabilities.
This fails the scan outright (exit code 1) rather than reporting a clean result — see
Advisory Database.
When advisory data for a package you don't depend on could not be parsed:
Advisory data for 1 package elsewhere in the database could not be parsed. None of your
dependencies were affected. This shouldn't happen with the default database — please
report it at safe@erlang-solutions.com.
This is a warning, not a failure: exit code 0 normally, or 3 with --warnings-as-errors.
When an advisory record could not be read at all:
1 advisory record in the database could not be read at all, so SAFE cannot tell
which packages they covered — possibly one of yours. This shouldn't happen with the
default database — please report it at safe@erlang-solutions.com.
The scan cannot be trusted until this is resolved. To proceed anyway and accept the
gap, re-run with --allow-unreadable-advisories.
Also exit code 1. Unlike the case above, SAFE has no package name to report here — the record
never parsed far enough to name one — so it can't tell you whether one of your dependencies was
affected. Re-running with --allow-unreadable-advisories proceeds and prints this instead:
Proceeding with 1 unreadable advisory record (--allow-unreadable-advisories).
SAFE cannot tell which packages they covered, so this scan may have missed a
vulnerability. Please report it at safe@erlang-solutions.com.
Exit Codes
| Code | Meaning |
|---|---|
0 | No vulnerabilities found |
1 | An error occurred during the scan, advisory data for one of your dependencies could not be parsed, or an advisory record could not be read at all (without --allow-unreadable-advisories) |
2 | One or more vulnerable dependencies found |
3 | Dependencies that could not be checked, or advisory data unrelated to your dependencies that failed to parse, and --warnings-as-errors is enabled |
Advisory Database
The default advisory source is the OSV.dev Hex ecosystem archive, which
aggregates advisories from GitHub's Advisory Database as well as those published directly by the
Erlang Ecosystem Foundation CNA — including advisories that never make it
into a GitHub Advisory Database mirror.
The archive (~450 KB) is downloaded fresh on every run, so a network connection is required.
If OSV.dev's data for a package you actually depend on fails to parse, SAFE fails the scan
outright (exit code 1) rather than silently reporting that package as clean — "could not check"
must never look the same as "no vulnerabilities found". A parse failure for a package you don't
depend on can't affect your scan's correctness, so it's only a warning (see above) — but it's
still surfaced, since it usually points to a bug in SAFE's parser that could affect a dependency
you add later.
A package counts as affected whenever any of its advisory data was lost, not only when all of it was: an affected version range SAFE can't interpret, a version bound that isn't valid semver, or a record missing required fields all put the package in that category — including when the rest of its ranges parsed fine. Half an advisory matching half its affected versions would look identical to a clean result.
If a record can't be decoded at all, there is no package name to attribute it to, so SAFE can't
narrow the problem down to your dependencies or rule them out. That fails the scan too, unless
you pass --allow-unreadable-advisories to accept the gap and continue.