Version semantics

Version comparison is what makes vulnerability matching and banned_versions correct, and every ecosystem orders versions differently. depaudit implements each ecosystem's rules itself (pep440.py, semver.py, dispatched by versions.py).

Ecosystem Versions Ranges
PyPI PEP 440 PEP 440 specifiers (>=1,<2, ~=2.1)
npm semver 2.0 node-semver ranges (^, ~, x, -, \|\|)
crates.io semver 2.0 Cargo requirements (1.2, >=1, <2)
Go semver + pseudo npm-style ranges on the stripped version

PEP 440

A version is [N!]N(.N)*[{a|b|rc}N][.postN][.devN][+local]. Parsing accepts the normalisation aliases (alpha, c, pre, -1 for post, rev, separators -_., a leading v, upper case).

Ordering key (compared left to right):

  1. epoch
  2. release tuple with trailing zeros stripped (1.0 == 1.0.0)
  3. pre-release phase: dev-only < a < b < rc < final
  4. post-release number (none < .post0)
  5. dev number (a .devN sorts before the same version without it)
  6. local label (none < any local; numeric segments > alphanumeric ones)

So 1.0.dev1 < 1.0a1.dev1 < 1.0a1 < 1.0rc1 < 1.0 < 1.0+abc < 1.0.post1.dev1 < 1.0.post1 < 1.0.1 < 1!0.1.

Specifiers

Operator Rule
==V, !=V public version equality: the candidate's local label is ignored unless V has one. ==1.2.* is a prefix match on the release segment.
<=V, >=V compare the candidate's public version
<V candidate < V, and a pre-release of V itself is not matched unless V is a pre-release (2.0rc1 is not <2.0)
>V candidate > V, but not a post-release of V (unless V is one) and not a local version of V
~=V >=V and ==prefix.* where the prefix is V without its last segment (~=2.1.5 means >=2.1.5, ==2.1.*)
===V case-insensitive string equality

Pre-release candidates are excluded unless some clause names a pre-release (pip's default). Callers can force inclusion; banned_versions does.

Worked example: 2.0rc1 against <2.0 is false (it is a pre-release of the bound), against <2.0rc2 true, against >=1.0 false (pre-releases are not offered by default), against >=2.0rc1 true.

Semver (npm, crates.io, Go)

Precedence: compare major.minor.patch numerically; a version with a pre-release is lower than the same version without; pre-release identifiers are compared left to right, numeric identifiers numerically and lower than alphanumeric ones, and a shorter list is lower when all shared identifiers are equal. Build metadata (+build) is ignored.

1.0.0-alpha < 1.0.0-alpha.1 < 1.0.0-alpha.beta < 1.0.0-beta < 1.0.0-beta.2 < 1.0.0-beta.11 < 1.0.0-rc.1 < 1.0.0

npm ranges

Each comparator set is desugared to primitive comparators:

Range Means
^1.2.3 >=1.2.3 <2.0.0-0
^0.2.3 >=0.2.3 <0.3.0-0
^0.0.3 >=0.0.3 <0.0.4-0
~1.2.3, ~1.2 >=1.2.3 <1.3.0-0, >=1.2.0 <1.3.0-0
~1 / 1.x / 1 >=1.0.0 <2.0.0-0
1.2.3 - 2.3 >=1.2.3 <2.4.0-0
>1.2 >=1.3.0
<=1.2 <1.3.0-0
*, x, empty >=0.0.0
A \|\| B either set

The upper bounds are written as -0 pre-releases so that pre-releases of the excluded version are excluded too (2.0.0-beta is not in ^1.2.3).

Pre-release rule: a pre-release version satisfies a comparator set only if some comparator in that set has a pre-release on the same major.minor.patch. So 1.2.3-beta.2 satisfies ^1.2.3-beta.1 but not ^1.2.0, and 1.2.4-beta.2 does not satisfy ^1.2.3-beta.1.

Cargo requirements

Comma-separated comparators, ANDed. A bare version is a caret requirement (1.5 is ^1.5), then the npm desugaring above applies, including the pre-release rule.

Go

Module versions are semver with a v prefix. +incompatible is stripped. Pseudo-versions (v0.0.0-20190308221718-c2843e01d9a2, v1.2.4-0.20200101000000-abcdefabcdef) are semver pre-releases, so ordering falls out of the rules above: a pseudo-version for a commit after v1.2.3 is > v1.2.3 and < v1.2.4. OSV Go ranges (written without the v) therefore work unchanged:

installed  v0.0.0-20190308221718-c2843e01d9a2   (x/crypto)
fixed      0.0.0-20200220183623-bac4c82f6975
=> installed < fixed, so the advisory applies and the fix suggestion is the pseudo-version

OSV range evaluation

An OSV affected entry has ranges of events (introduced, fixed, last_affected) and/or an explicit versions list. depaudit:

  1. matches the installed version against the explicit list using the ecosystem's equality (1.0 == 1.0.0 on PyPI);
  2. sorts each range's events by version (introduced: "0" is the minimum) and turns them into intervals [introduced, fixed) or [introduced, last_affected]; a trailing introduced is open-ended;
  3. reports the fixed event of the interval that contains the version.

Only ECOSYSTEM and SEMVER range types are evaluated; GIT ranges need commit history and are skipped.

Example, minimist: [0, 0.2.4) and [1.0.0, 1.2.6). 1.2.5 is in the second interval (fixed in 1.2.6); 0.5.0 is in neither interval, so it is not affected.

Minimal upgrade suggestion

For a vulnerable package, the candidate versions are the fixed versions of every advisory in the snapshot for that package that are newer than the installed version (pre-releases only when the installed one is a pre-release). The suggestion is the smallest candidate that no advisory in the snapshot affects, which avoids suggesting a version that fixes one advisory but lands inside the range of another. The report flags the suggestion (major) when the major version changes (for 0.x versions the first segment is compared as-is).