Skip to main content
Version: 1.5.1

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.

info

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
tip

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_FILE Path to the lock file to scan. Supports both mix.lock (Elixir) and rebar.lock (Erlang). If omitted, SAFE looks for mix.lock or rebar.lock in the current directory.

  • --ignore-file PATH Path to an SCA ignore file. Defaults to .safe/sca_ignore.json in the same directory as the lock file. See Suppressing SCA Findings for the file format.

  • --warnings-as-errors Treat 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 code 3 if any of them occur.

  • --allow-unreadable-advisories Continue 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 code 1) 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 fileEcosystemChecked dependencies
mix.lockElixir / Mix:hex packages only
rebar.lockErlang / Rebar3:pkg (Hex) packages only
info

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

CodeMeaning
0No vulnerabilities found
1An 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)
2One or more vulnerable dependencies found
3Dependencies 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.