blob: ac0fd52055827e5876f8989e866b6ffb444cb15f [file] [log] [blame]
// Copyright 2021 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package ghsa
import (
"context"
"fmt"
"strings"
"golang.org/x/vulndb/internal/proxy"
"golang.org/x/vulndb/internal/report"
)
var _ report.Source = &SecurityAdvisory{}
func (sa *SecurityAdvisory) ToReport(_ *proxy.Client, modulePath string) *report.Report {
return ghsaToReport(sa, modulePath)
}
func (sa *SecurityAdvisory) SourceID() string {
return sa.ID
}
var _ report.Fetcher = &Client{}
func (c *Client) Fetch(ctx context.Context, id string) (report.Source, error) {
return c.FetchGHSA(ctx, id)
}
// ghsaToReport creates a Report struct from a given GHSA SecurityAdvisory and modulePath.
func ghsaToReport(sa *SecurityAdvisory, modulePath string) *report.Report {
r := &report.Report{
Summary: report.Summary(sa.Summary),
Description: report.Description(sa.Description),
}
var cves, ghsas []string
for _, id := range sa.Identifiers {
switch id.Type {
case "CVE":
cves = append(cves, id.Value)
case "GHSA":
ghsas = append(ghsas, id.Value)
}
}
for _, ref := range sa.References {
r.References = append(r.References, report.ReferenceFromUrl(ref.URL))
}
r.CVEs = cves
r.GHSAs = ghsas
for _, v := range sa.Vulns {
if modulePath == "" {
modulePath = v.Package
}
m := &report.Module{
Module: modulePath,
Versions: versions(v.EarliestFixedVersion, v.VulnerableVersionRange),
Packages: []*report.Package{{
Package: v.Package,
}},
}
r.Modules = append(r.Modules, m)
}
return r
}
// versions extracts the versions in which a vulnerability was introduced and
// fixed from a Github Security Advisory's EarliestFixedVersion and
// VulnerableVersionRange fields, and wraps them in a []VersionRange.
//
// If the vulnRange cannot be parsed, or the earliestFixed and vulnRange are
// incompatible, populate the relevant fields with a TODO for a human to handle.
func versions(earliestFixed, vulnRange string) report.Versions {
// Don't try to be fully general here. Handle the common cases (which, as of
// March 2022, are the only cases), and let a person handle the others.
items, err := parseVulnRange(vulnRange)
if err != nil {
return report.Versions{
report.Introduced(fmt.Sprintf("TODO (got error %q)", err)),
}
}
var intro, fixed string
// Most common case: a single "<" item with a version that matches earliestFixed.
if len(items) == 1 && items[0].op == "<" && items[0].version == earliestFixed {
intro = "0.0.0"
fixed = earliestFixed
}
// Two items, one >= and one <, with the latter matching earliestFixed.
if len(items) == 2 && items[0].op == ">=" && items[1].op == "<" && items[1].version == earliestFixed {
intro = items[0].version
fixed = earliestFixed
}
// A single "<=" item with no fixed version.
if len(items) == 1 && items[0].op == "<=" && earliestFixed == "" {
intro = "0.0.0"
}
if intro == "" {
intro = fmt.Sprintf("TODO (earliest fixed %q, vuln range %q)", earliestFixed, vulnRange)
}
// Unset intro if vuln was always present.
if intro == "0.0.0" {
intro = ""
}
var result report.Versions
if intro != "" {
result = append(result, report.Introduced(intro))
}
if fixed != "" {
result = append(result, report.Fixed(fixed))
}
return result
}
type vulnRangeItem struct {
op, version string
}
// parseVulnRange splits the contents of a GitHub Security Advisory's
// VulnerableVersionRange field into separate items.
func parseVulnRange(s string) ([]vulnRangeItem, error) {
// A GHSA vuln range is a comma-separated list of items of the form "OP VERSION"
// where OP is one of "<", ">", "<=" or ">=" and VERSION is a semantic
// version.
var items []vulnRangeItem
parts := strings.Split(s, ",")
for _, p := range parts {
p = strings.TrimSpace(p)
if p == "" {
continue
}
before, after, found := strings.Cut(p, " ")
if !found {
return nil, fmt.Errorf("invalid vuln range item %q", p)
}
items = append(items, vulnRangeItem{strings.TrimSpace(before), strings.TrimSpace(after)})
}
return items, nil
}