internal/worker: add report template to GHSA issues
Add a partially populated report to an issue filed
for a GitHub Security Advisory.
Change-Id: I6d16a02fe318604f2f74f5555609fd8fec1b611d
Reviewed-on: https://go-review.googlesource.com/c/vulndb/+/389275
Trust: Jonathan Amsterdam <jba@google.com>
Run-TryBot: Jonathan Amsterdam <jba@google.com>
Reviewed-by: Damien Neil <dneil@google.com>
diff --git a/internal/worker/worker.go b/internal/worker/worker.go
index e3c0b63..02fc986 100644
--- a/internal/worker/worker.go
+++ b/internal/worker/worker.go
@@ -22,6 +22,7 @@
"golang.org/x/sync/errgroup"
"golang.org/x/time/rate"
vulnc "golang.org/x/vuln/client"
+ "golang.org/x/vulndb/internal"
"golang.org/x/vulndb/internal/cvelistrepo"
"golang.org/x/vulndb/internal/cveschema"
"golang.org/x/vulndb/internal/derrors"
@@ -359,19 +360,110 @@
func newGHSABody(sr storeRecord) (string, error) {
sa := sr.(*store.GHSARecord).GHSA
+ r := ghsaToReport(sa)
+ rs, err := r.ToString()
+ if err != nil {
+ return "", err
+ }
+
var b strings.Builder
intro := fmt.Sprintf(
"In GitHub Security Advisory [%s](%s), there is a vulnerability in the following Go packages or modules:",
sr.GetPrettyID(), sa.Permalink)
intro += "\n\n" + vulnTable(sa.Vulns)
if err := issueTemplate.Execute(&b, issueTemplateData{
- Intro: intro,
+ Intro: intro,
+ Report: rs,
+ Pre: "```",
}); err != nil {
return "", err
}
return b.String(), nil
}
+func ghsaToReport(sa *ghsa.SecurityAdvisory) *report.Report {
+ u := sa.UpdatedAt
+ r := &report.Report{
+ GHSAs: []string{sa.PrettyID()},
+ Description: sa.Description,
+ Published: sa.PublishedAt,
+ LastModified: &u,
+ }
+ if len(sa.Vulns) == 0 {
+ return r
+ }
+ r.Package = sa.Vulns[0].Package
+ r.Versions = versions(sa.Vulns[0].EarliestFixedVersion, sa.Vulns[0].VulnerableVersionRange)
+ for _, v := range sa.Vulns[1:] {
+ var a report.Additional
+ a.Package = v.Package
+ a.Versions = versions(v.EarliestFixedVersion, v.VulnerableVersionRange)
+ r.AdditionalPackages = append(r.AdditionalPackages, a)
+ }
+ return r
+}
+
+func versions(earliestFixed, vulnRange string) []report.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 []report.VersionRange{{
+ 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 = "v0.0.0"
+ fixed = "v" + 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 = "v" + items[0].version
+ fixed = "v" + earliestFixed
+ }
+
+ // A single "<=" item with no fixed version.
+ if len(items) == 1 && items[0].op == "<=" && earliestFixed == "" {
+ intro = "v0.0.0"
+ }
+
+ if intro == "" {
+ intro = fmt.Sprintf("TODO (earliest fixed %q, vuln range %q)", earliestFixed, vulnRange)
+ }
+ return []report.VersionRange{{Introduced: intro, Fixed: 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 := internal.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
+}
+
func vulnTable(vs []*ghsa.Vuln) string {
var b strings.Builder
fmt.Fprintf(&b, "| Unit | Fixed | Vulnerable Ranges |\n")
diff --git a/internal/worker/worker_test.go b/internal/worker/worker_test.go
index 4391c95..73ceb72 100644
--- a/internal/worker/worker_test.go
+++ b/internal/worker/worker_test.go
@@ -24,6 +24,7 @@
"golang.org/x/vulndb/internal/ghsa"
"golang.org/x/vulndb/internal/gitrepo"
"golang.org/x/vulndb/internal/issues"
+ "golang.org/x/vulndb/internal/report"
"golang.org/x/vulndb/internal/worker/log"
"golang.org/x/vulndb/internal/worker/store"
)
@@ -269,7 +270,17 @@
See [doc/triage.md](https://github.com/golang/vulndb/blob/master/doc/triage.md) for instructions on how to triage this report.
-`
+` + "```" + `
+package: aPackage
+versions:
+ - introduced: v0.0.0
+ fixed: v1.2.3
+description: a description
+ghsas:
+ - G1
+
+` + "```"
+
if diff := cmp.Diff(unindent(want), got); diff != "" {
t.Errorf("mismatch (-want, +got):\n%s", diff)
}
@@ -410,3 +421,51 @@
}
}
}
+
+func TestParseVulnRange(t *testing.T) {
+ for _, test := range []struct {
+ in string
+ want []vulnRangeItem
+ }{
+ {"", nil},
+ {"< 1.2.3", []vulnRangeItem{{"<", "1.2.3"}}},
+ {"< 4.3.2, >= 1.2.3", []vulnRangeItem{
+ {"<", "4.3.2"},
+ {">=", "1.2.3"},
+ }},
+ } {
+ got, err := parseVulnRange(test.in)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !cmp.Equal(got, test.want, cmp.AllowUnexported(vulnRangeItem{})) {
+ t.Errorf("%q:\ngot %+v\nwant %+v", test.in, got, test.want)
+ }
+ }
+}
+
+func TestVersions(t *testing.T) {
+ for _, test := range []struct {
+ earliestFixed string
+ vulnRange string
+ intro, fixed string
+ }{
+ {"1.0.0", "< 1.0.0", "v0.0.0", "v1.0.0"},
+ {"", "<= 1.4.2", "v0.0.0", ""},
+ {"1.1.3", ">= 1.1.0, < 1.1.3", "v1.1.0", "v1.1.3"},
+ {
+ "1.2.3", "<= 2.3.4",
+ `TODO (earliest fixed "1.2.3", vuln range "<= 2.3.4")`, "",
+ },
+ } {
+ got := versions(test.earliestFixed, test.vulnRange)
+ want := []report.VersionRange{{
+ Introduced: test.intro,
+ Fixed: test.fixed,
+ }}
+ if !cmp.Equal(got, want) {
+ t.Errorf("%q, %q:\ngot %+v\nwant %+v",
+ test.earliestFixed, test.vulnRange, got, want)
+ }
+ }
+}