osv

package
v0.8.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 7, 2026 License: MIT Imports: 17 Imported by: 0

Documentation

Overview

Package osv resolves vulnerability advisories for a package version from the OSV database (https://osv.dev).

An advisory is keyed by every identifier it is known by (its own OSV id plus every alias, such as CVE- and GHSA- ids), and the record that actually carries package-level detail wins when the same key is contributed by more than one source record.

The Go database is the only one that publishes vulnerable import paths, so Advisory.Pkgs is populated for Go refs and left empty for everything else -- see advisoryFor.

Index

Constants

View Source
const DefaultBaseURL = "https://api.osv.dev/v1"

DefaultBaseURL is the OSV v1 API root. Endpoints are derived from it.

View Source
const GoEcosystem = "Go"

GoEcosystem is the OSV ecosystem name for Go modules and the standard library.

Variables

View Source
var ErrUnknownDistro = errors.New("no OSV ecosystem is known for this distribution")

ErrUnknownDistro is returned by Release.Ecosystem when the distribution has no entry in the mapping table. It is deliberately an error and never an empty string: a missing ecosystem must stop the scan, because an OSV query with no ecosystem finds nothing and reads exactly like a clean image.

Functions

func Families

func Families() []string

Families lists the OSV ecosystem families Ecosystem can produce, without their version suffixes. It is what --ecosystem accepts for OS packages, and it lives beside the mapping so the two cannot drift apart.

func KnownDistroIDs

func KnownDistroIDs() []string

KnownDistroIDs lists the os-release ID values Ecosystem can map, for error messages.

func MatchesProductRelease

func MatchesProductRelease(eco, release string) bool

MatchesProductRelease reports whether an OSV affected-entry ecosystem string names a product of the given release.

eco is the full affected-entry spelling ("SUSE:Linux Enterprise Module for Basesystem 15 SP7"); release is a ProductRelease token ("15 SP7"). The support-phase suffix is dropped before comparing, because it distinguishes subscriptions rather than releases: an image running 15 SP4 is described by the "15 SP4-LTSS" records whether or not its owner holds that subscription.

Types

type Advisory

type Advisory struct {
	// ID is the canonical OSV identifier (e.g. GO-2024-1234, DSA-5678-1).
	ID string
	// Aliases are all other identifiers this advisory is known by.
	Aliases []string
	// Upstream is the vulnerabilities this record addresses, which for a distro
	// advisory is the CVEs its patch fixes.
	//
	// It is deliberately not merged into Aliases, because an alias is a claim
	// of identity and this is not one. Every distro database uses this field
	// and none uses aliases: SUSE-SU-2026:0312-1 addresses eight unrelated
	// CVEs, RHSA-2024:2447 seven. Treating those as eight names for one thing
	// would let buildMap file the record under eight keys and borrowSeverity
	// copy one CVE's vector onto the other seven.
	//
	// Consumers that join on CVE read it anyway, because a bundle still has to
	// be findable by what it fixes -- see advisoryResolver.cveSets.
	Upstream []string
	// Summary and Details are the advisory prose. They are the input to
	// advisory-text mining for ecosystems that publish no package-level data.
	Summary string
	Details string
	// Pkgs is the set of vulnerable import paths declared for a Go module.
	// Empty when OSV publishes no import paths (e.g. GitHub-only GHSA
	// records), in which case callers should fall back to module granularity,
	// and always empty for non-Go ecosystems.
	Pkgs []string

	// Fixed maps an affected package name to every version the record says its
	// patch landed in, read from the affected ranges in the order OSV published
	// them. It is the single most actionable field a report can show -- "this is
	// what to upgrade to" -- and unlike Pkgs it is populated for every
	// ecosystem.
	//
	// A package is absent when the record publishes no fixed version for it,
	// which is a real and common state: the flaw is acknowledged and no patch
	// has shipped. Callers must show that as "no fix" rather than as blank,
	// because the two mean opposite things.
	//
	// The list is a list because one affected entry routinely carries several
	// fixed events, one per branch the vendor still maintains: 22 of the 110
	// records for github.com/hashicorp/vault do, and GO-2022-0623 names 1.5.9,
	// 1.6.5 and 1.7.2 together. Those are alternatives, not a progression, so
	// there is no "latest" to collapse to without knowing what is installed --
	// telling a 1.5.x user to jump to 1.7.2 is a major upgrade they did not need
	// and did not ask for. Picking among them is the caller's job, because only
	// the caller knows the installed version. Distro records are single-branch,
	// so on Debian and Ubuntu the list is almost always one element.
	Fixed map[string][]string

	// Ecosystem is the OSV ecosystem this advisory was resolved for, copied from
	// the query ref. It is what tells a caller which comparator orders the Fixed
	// versions -- dpkg rules for "Debian:12", semver for "Go" -- and picking the
	// wrong one is worse than declining to order them at all.
	Ecosystem string

	// CVSSVector is the CVSS:3.0 or CVSS:3.1 base vector the record publishes,
	// empty when it publishes none or publishes only a version this tool does
	// not score. It is kept as the string rather than only as a number so a
	// report can show the metrics behind a rating someone disputes.
	CVSSVector string
	// PublisherSeverity is the qualitative rating the database itself assigned,
	// verbatim. Empty when the record carries no label.
	//
	// It is kept separate from CVSSVector because the two are independent
	// claims that disagree more often than one would expect. See Severity.
	PublisherSeverity string
}

Advisory is the resolved information for a single vulnerability id.

func (*Advisory) CVSSScore added in v0.2.0

func (a *Advisory) CVSSScore() (float64, bool)

CVSSScore returns the base score for the advisory's vector. The bool is false when there is no vector, or it is a version this tool does not score, and callers must not read that as a score of zero -- 0.0 is a real CVSS answer.

func (*Advisory) Severity added in v0.2.0

func (a *Advisory) Severity() string

Severity is the rating to display for this advisory: the more severe of what the publisher said and what its CVSS v3 vector computes to.

Taking the maximum is not indecision about a conflict. It is the only rule available here that never demotes a finding on a metadata technicality. The two sources disagree in both directions -- measured across 442 GHSA records, the v3 vector is milder than GitHub's own label 27 times and harsher 20 times -- so neither "always trust the vector" nor "always trust the label" avoids quietly lowering the severity of some real findings.

Neither source is wrong. GitHub rates the advisory, increasingly against the CVSS 4.0 vector it also publishes and this tool deliberately does not score, while the v3 vector is a separate and older statement about the same flaw. The computed score cannot simply be dropped in the label's favour either: a Debian record carries a vector and no label at all, and scoring it is what makes it comparable with a GHSA one in the same table.

Erring upward costs a reader time on a finding milder than billed. Erring downward costs them the finding.

type Client

type Client struct {
	HTTP *http.Client
	// BaseURL is the API root; DefaultBaseURL when empty.
	BaseURL string
	// Concurrency bounds the parallel per-id fetches QueryBatch makes;
	// defaultConcurrency when zero.
	Concurrency int
	// OnCorrection is called once for every advisory a query set aside because
	// the record's own precise ranges exclude the version asked about; see
	// customranges.go.
	//
	// It is a callback rather than a return value because the corrections are
	// not the answer to any one caller's question -- they are something the
	// report has to say about the scan as a whole, and threading them back
	// through every resolver signature to reach it would be noise. Calls are
	// sequential: matching happens after hydration, on one goroutine.
	OnCorrection func(Correction)
}

Client queries the OSV API.

func NewClient

func NewClient() *Client

NewClient returns a Client with sane defaults.

func (*Client) Query

func (c *Client) Query(ctx context.Context, ref Ref) (map[string]*Advisory, error)

Query returns the map of advisory-id -> Advisory for ref. Every alias identifier is a key in the returned map, so a caller may look up a CVE, GHSA or GO id interchangeably.

func (*Client) QueryBatch

func (c *Client) QueryBatch(ctx context.Context, refs []Ref) ([]map[string]*Advisory, error)

QueryBatch resolves many refs at once. result[i] is what Query(refs[i]) would have returned, so the answer is always the same length as refs.

A whole-image scan is thousands of lookups; one /v1/query round trip each is not viable. /v1/querybatch takes 1000 refs per request but answers with ids only, so every distinct id is then fetched once through /v1/vulns/{id} -- distinct being the point, since an OS advisory typically covers many of the packages in one image.

func (*Client) Vuln

func (c *Client) Vuln(ctx context.Context, id string) (*Advisory, error)

Vuln fetches one advisory record by its OSV id.

type Correction added in v0.8.0

type Correction struct {
	// Advisory is the OSV id that was set aside.
	Advisory string
	// Package and Version are what was queried.
	Package string
	Version string
	// Ranges renders the record's own precise affected ranges, so a reader can
	// check the arithmetic that excluded them.
	Ranges string
}

Correction is an advisory OSV matched against a version that the advisory's own precise ranges exclude.

It exists so the drop can be counted and shown. A scan that quietly returned 27 fewer findings than the database offered would be indistinguishable from a cleaner image, and that is the one reading this tool must never invite.

func (Correction) String added in v0.8.0

func (c Correction) String() string

type Ref

type Ref struct {
	// Ecosystem is an OSV ecosystem string, e.g. "Go", "Debian:12",
	// "Alpine:v3.19". See Release.Ecosystem for how these are derived for OS
	// distributions.
	Ecosystem string
	Name      string
	Version   string

	// Release narrows a bare-family ecosystem to a single product release. It
	// is empty for every ecosystem whose query already names its release, and
	// when set an advisory survives only if one of its affected entries names
	// a product of that release. See Release.ProductRelease for why SUSE
	// cannot be handled in the query itself.
	Release string
}

Ref is an OSV package coordinate: an ecosystem name, a package name as that ecosystem's database spells it, and a version. Version may be empty to ask for every advisory against the package regardless of version.

func (Ref) String

func (r Ref) String() string

type Release

type Release struct {
	ID              string   // ID=
	IDLike          []string // ID_LIKE=
	Version         string   // VERSION=
	VersionID       string   // VERSION_ID=
	VersionCodename string   // VERSION_CODENAME=
	PrettyName      string   // PRETTY_NAME=
	CPEName         string   // CPE_NAME=
}

Release is the subset of /etc/os-release needed to name an OSV ecosystem.

func ParseOSRelease

func ParseOSRelease(r io.Reader) (Release, error)

ParseOSRelease reads the os-release(5) key=value format.

func ReleaseFromDistro added in v0.7.0

func ReleaseFromDistro(id, version string) Release

ReleaseFromDistro builds a Release from a distribution id and version, for callers whose whole account of the operating system is those two strings -- an SBOM's "distro=debian-12" qualifier, and nothing else.

The gap it has to close is LTS. Ecosystem reads that off VERSION and PRETTY_NAME, which an SBOM does not carry, and the suffix is not cosmetic: "Ubuntu:22.04" and "Ubuntu:22.04:LTS" are different ecosystems, only one of them has any records in it, and the empty one answers HTTP 200. Guessing wrong there reads as a clean image.

Both distributions that need the suffix publish on a fixed schedule, so the version alone settles it -- Ubuntu ships LTS every April of an even year, openEuler every March -- and the inference is written into VERSION, where isLTS already looks, rather than bolted onto the mapping table. Every other distribution ignores the field.

func (Release) Ecosystem

func (rel Release) Ecosystem() (string, error)

Ecosystem returns the OSV ecosystem string for this release.

The strings below were verified against the live api.osv.dev rather than read off the schema, because the API validates them asymmetrically: the family name is checked -- a misspelled "Debain:12" is rejected with HTTP 400 and {"code":3,"message":"invalid ecosystem"} -- but the version suffix is not. "Debian:99" answers HTTP 200 with an empty result, indistinguishable from a clean image. That asymmetry is why this is a table with tests rather than a format string, and why an unrecognized distribution is an error.

The suffix rules are not uniform and none of them are guessable:

Debian          major only            Debian:12          (bare "Debian" over-matches every release)
Ubuntu          ":LTS" only when LTS  Ubuntu:24.04:LTS   ("Ubuntu:24.10" for a non-LTS release)
Alpine          "v" prefix, no patch  Alpine:v3.19       ("Alpine:3.19" finds nothing)
Azure Linux     major only            Azure Linux:3      ("Azure Linux:3.0" finds nothing)
openEuler       "-LTS" when LTS       openEuler:24.03-LTS
openSUSE        product name from PRETTY_NAME
Red Hat         bare -- see below
SLE             bare + ProductRelease narrowing -- see below

func (Release) ProductRelease

func (rel Release) ProductRelease() string

ProductRelease is the release token an affected entry must carry for this image, or "" when no narrowing applies.

It exists for the bare-family ecosystems, where a query matches records from every product the vendor ships. That over-matching is not benign for SUSE: gzip is fixed at 1.10-150200.13.1 on SLE 15 and at 1.13-160000.3.1 on SLE 16, so a *fully patched* SLES 15 SP7 image still matches the SLE 16 record, and no amount of patching will ever clear it. Version comparison cannot sort this out, because the two products' version lines never converge.

The token is the trailing version part of the product name -- "15 SP7" for SLES 15 SP7, "5.5" for SLE Micro 5.5 -- which is the one component every spelling of a product shares, module and support-phase suffix included.

type StatusError

type StatusError struct {
	Status int
	URL    string
	Body   string
}

StatusError is a non-200 answer from the OSV API.

func (*StatusError) Error

func (e *StatusError) Error() string

func (*StatusError) Retryable

func (e *StatusError) Retryable() bool

Retryable reports whether repeating the request could plausibly succeed.

A 4xx other than 429 is a defect in the request, not a transient fault. The one that matters here is an unrecognized ecosystem name: OSV answers {"code":3,"message":"invalid ecosystem"} with HTTP 400. Retrying that three times and then reporting "unexpected status 400" buries the one message that says what is wrong.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL