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 ¶
const DefaultBaseURL = "https://api.osv.dev/v1"
DefaultBaseURL is the OSV v1 API root. Endpoints are derived from it.
const GoEcosystem = "Go"
GoEcosystem is the OSV ecosystem name for Go modules and the standard library.
Variables ¶
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 ¶
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 the version its patch lands in,
// read from the record's affected ranges. 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. When a package has several fixed
// events the latest is kept, which is the upgrade target for anything
// still on an older version.
Fixed map[string]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
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
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
}
Client queries the OSV API.
func (*Client) Query ¶
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 ¶
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.
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.
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 ¶
ParseOSRelease reads the os-release(5) key=value format.
func (Release) Ecosystem ¶
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 ¶
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 ¶
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.