blob: c52c7c76afb379bbb062578e3ed8d6139c3d341b [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 report
import (
"fmt"
"strings"
"golang.org/x/vulndb/internal/ghsa"
)
// GHSAToReport creates a Report struct from a given GHSA SecurityAdvisory and modulePath.
func GHSAToReport(sa *ghsa.SecurityAdvisory, modulePath string) *Report {
u := sa.UpdatedAt
r := &Report{
Description: sa.Description,
Published: sa.PublishedAt,
LastModified: &u,
Links: Links{Context: []string{sa.Permalink}},
}
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)
}
}
r.CVEs = cves
r.GHSAs = ghsas
for i, v := range sa.Vulns {
p := Package{
Package: v.Package,
Versions: versions(v.EarliestFixedVersion, v.VulnerableVersionRange),
}
if i == 0 {
p.Module = modulePath
}
r.Packages = append(r.Packages, p)
}
r.Fix()
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) []VersionRange {
// 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 []VersionRange{{
Introduced: Version(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 = ""
}
return []VersionRange{{Introduced: Version(intro), Fixed: Version(fixed)}}
}
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
}