MobDev.SecurityScan.BundledRuntime.Advisories (mob_dev v0.7.30)

Copy Markdown View Source

Matches upstream advisory data against the OTP and OpenSSL versions fingerprinted from a cached runtime, and the SQLite version compiled into the project's exqlite, producing Findings.

Pure: callers pass the decoded feed bodies (fetched or cached by MobDev.SecurityScan.BundledRuntime.AdvisoryFeed) and the installed versions. Four sources:

  • Erlang/OTP — the GitHub repository security advisories of erlang/otp. OSV.dev is no help here: the EEF records it carries use GIT ranges only, so a pkg:otp/ssl@11.7 query returns nothing.
  • OpenSSL — openssl-library.org/news/vulnerabilities-<branch>/, one page per release branch. OpenSSL retired its JSON feed; the HTML page is the published record.
  • OpenSSL branch EOL — endoflife.date/api/openssl.json.
  • SQLite — the "Status Of Recent SQLite CVEs" table on www.sqlite.org/cves.html: CVE ids, the release that fixed them (or "Not a bug in SQLite", which is skipped) and a comment.

How an OTP advisory is matched

Each advisory lists vulnerabilities[], one entry per package: the umbrella OTP release plus the individual applications (ssl, public_key, erts, ...). vulnerable_version_range is the introduction point (">= 9.5") and patched_versions names one fix per maintained branch, e.g. "11.7.8, 11.6.0.7, 11.2.12.14".

Application entries are matched against the application versions in the tarball's lib/ (what actually gets packaged); the OTP entry is only used when an advisory names no application, because the app entries are the precise statement and an app the runtime doesn't ship can't be vulnerable in it. A package may appear more than once with different introduction points; its entries are merged.

OTP and application versions form a tree, not a line: 11.6.0.7 is a maintenance branch off 11.6, and 11.7 neither contains nor precedes it. otp_compare/2 implements the documented comparison (OTP's versions:compare/2, "Version Scheme" in the OTP system principles), which answers :same, :ancestor, :descendant or :undefined; the advisories use >= in that sense. An installed version is:

  • affected when it is the same as, or a descendant of, some entry's introduction point;
  • fixed when it is the same as, or a descendant of, some patched version;
  • vulnerable when affected and not fixed. The fix reported is the patched version descending from it that shares the longest prefix with it (11.7 → 11.7.8, 11.2 → 11.2.12.14), or none when no patched version descends from it (an unmaintained branch).

A release candidate (29.0-rc3) is treated as an ancestor of its release.

Summary

Types

Outcome of matching a branch against endoflife.date data.

Versions fingerprinted from one runtime tarball. :otp is the full OTP release version (releases/*/OTP_VERSION); :apps lists {name, version} for each application directory, including erts.

Functions

The OpenSSL release branch a version belongs to, which names its advisory page: 3.4.0 → 3.4, 1.1.1w → 1.1.1 (pre-3.0 branches were three components wide).

Compare two OpenSSL versions, which are linearly ordered: dotted numbers with an optional letter suffix (1.1.1zj). Missing components count as zero; letter suffixes order by length, then alphabetically (z < za). A pre-release tail (3.4.0-beta1) compares as its release, so a beta is reported against the release's CVEs rather than none.

Whether the OpenSSL branch of version is past end of life on today according to endoflife.date data. A branch is treated as EOL from its EOL date onward: on that day it stops receiving fixes. eol: true (end of life, date not given) is EOL with a nil date.

Findings for every CVE on a branch page whose affected range on the installed version's branch contains it.

URL of the advisory page for an OpenSSL branch.

Human-facing index of the Erlang/OTP advisories.

Compare two OTP or OTP-application versions per the OTP version scheme: how v1 relates to v2 in the version tree. :error when either is not a version.

Findings for every published, non-withdrawn OTP advisory that affects the installed versions.

Verdict for one installed version against the merged advisory entries for its package. See the moduledoc for the rules.

Parse an openssl-library.org/news/vulnerabilities-<branch>/ page into %{cve, severity, title, affected: [{from, before | nil}]} entries.

Parse the "Status Of Recent SQLite CVEs" table of sqlite.org/cves.html into one %{cves, fixed_in, severity, comment} entry per row.

Map an upstream severity label (GitHub critical/high/medium/low, OpenSSL Critical/High/Moderate/Low) to a Finding severity.

URL of SQLite's CVE status page.

Findings for every CVE on the SQLite page whose fixing release is newer than version. Disputed rows are skipped. Severity is the page's, or :medium when the page gives none.

Whether version is a dotted version the SQLite matcher can compare.

Types

eol_status()

@type eol_status() ::
  {:eol, Date.t() | nil, MobDev.SecurityScan.Finding.t()}
  | {:supported, Date.t() | nil}
  | :unknown_branch

Outcome of matching a branch against endoflife.date data.

installed()

@type installed() :: %{otp: String.t() | nil, apps: [{String.t(), String.t()}]}

Versions fingerprinted from one runtime tarball. :otp is the full OTP release version (releases/*/OTP_VERSION); :apps lists {name, version} for each application directory, including erts.

Functions

openssl_branch(version)

@spec openssl_branch(String.t()) :: {:ok, String.t()} | :error

The OpenSSL release branch a version belongs to, which names its advisory page: 3.4.0 → 3.4, 1.1.1w → 1.1.1 (pre-3.0 branches were three components wide).

openssl_compare(a, b)

@spec openssl_compare(String.t(), String.t()) :: :lt | :eq | :gt | :error

Compare two OpenSSL versions, which are linearly ordered: dotted numbers with an optional letter suffix (1.1.1zj). Missing components count as zero; letter suffixes order by length, then alphabetically (z < za). A pre-release tail (3.4.0-beta1) compares as its release, so a beta is reported against the release's CVEs rather than none.

openssl_eol(cycles, version, today)

@spec openssl_eol([map()], String.t(), Date.t()) :: eol_status()

Whether the OpenSSL branch of version is past end of life on today according to endoflife.date data. A branch is treated as EOL from its EOL date onward: on that day it stops receiving fixes. eol: true (end of life, date not given) is EOL with a nil date.

openssl_findings(entries, version, branch)

@spec openssl_findings([map()], String.t(), String.t()) :: [
  MobDev.SecurityScan.Finding.t()
]

Findings for every CVE on a branch page whose affected range on the installed version's branch contains it.

openssl_page_url(branch)

@spec openssl_page_url(String.t()) :: String.t()

URL of the advisory page for an OpenSSL branch.

otp_advisories_url()

@spec otp_advisories_url() :: String.t()

Human-facing index of the Erlang/OTP advisories.

otp_compare(v1, v2)

@spec otp_compare(String.t(), String.t()) ::
  :same | :ancestor | :descendant | :undefined | :error

Compare two OTP or OTP-application versions per the OTP version scheme: how v1 relates to v2 in the version tree. :error when either is not a version.

otp_findings(advisories, installed)

@spec otp_findings([map()], installed()) :: [MobDev.SecurityScan.Finding.t()]

Findings for every published, non-withdrawn OTP advisory that affects the installed versions.

otp_verdict(version, entries)

@spec otp_verdict(String.t(), [map()]) ::
  {:vulnerable, String.t() | nil} | :not_affected

Verdict for one installed version against the merged advisory entries for its package. See the moduledoc for the rules.

parse_openssl_page(html, branch)

@spec parse_openssl_page(String.t(), String.t()) ::
  {:ok, [map()]} | {:error, :unrecognised_page}

Parse an openssl-library.org/news/vulnerabilities-<branch>/ page into %{cve, severity, title, affected: [{from, before | nil}]} entries.

Returns {:error, :unrecognised_page} when the page doesn't parse the way the published layout does, so a redesigned or wrong page fails loudly instead of reading as "no CVEs": it must carry the Vulnerabilities <branch> heading, every CVE block must yield a known severity and at least one affected range, and a page that mentions a CVE must yield at least one block.

parse_sqlite_cves_page(html)

@spec parse_sqlite_cves_page(String.t()) ::
  {:ok, [map()]} | {:error, :unrecognised_page}

Parse the "Status Of Recent SQLite CVEs" table of sqlite.org/cves.html into one %{cves, fixed_in, severity, comment} entry per row.

fixed_in is the release named in the Fix column, or nil for rows SQLite disputes ("Not a bug in SQLite", "unreproducible"); those are kept so callers can count them, never matched. severity is the page's Severity column when it has one (it has none today), else nil.

Returns {:error, :unrecognised_page} unless the page carries the table heading and its CVE Number / Fix / Comments columns, every row names a CVE and has a Fix cell that is a version or a recognised dispute, and at least one row names a fix. A redesigned page then fails loudly instead of reading as "no CVEs".

severity(label)

@spec severity(String.t() | nil) :: MobDev.SecurityScan.Finding.severity()

Map an upstream severity label (GitHub critical/high/medium/low, OpenSSL Critical/High/Moderate/Low) to a Finding severity.

sqlite_cves_url()

@spec sqlite_cves_url() :: String.t()

URL of SQLite's CVE status page.

sqlite_findings(entries, version)

@spec sqlite_findings([map()], String.t()) :: [MobDev.SecurityScan.Finding.t()]

Findings for every CVE on the SQLite page whose fixing release is newer than version. Disputed rows are skipped. Severity is the page's, or :medium when the page gives none.

sqlite_version?(version)

@spec sqlite_version?(String.t()) :: boolean()

Whether version is a dotted version the SQLite matcher can compare.