internal/ghsa: a package for getting GitHub security advisories
Change-Id: Ie8448aa59d09534fc3cde570590b5f9a609ae93e
Reviewed-on: https://go-review.googlesource.com/c/vulndb/+/383894
Trust: Jonathan Amsterdam <jba@google.com>
Run-TryBot: Jonathan Amsterdam <jba@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Damien Neil <dneil@google.com>
Trust: Damien Neil <dneil@google.com>
diff --git a/go.mod b/go.mod
index e7c8d54..344be19 100644
--- a/go.mod
+++ b/go.mod
@@ -19,6 +19,7 @@
github.com/google/go-github/v41 v41.0.0
github.com/google/safehtml v0.0.2
github.com/jba/templatecheck v0.6.0
+ github.com/shurcooL/githubv4 v0.0.0-20220115235240-a14260e6f8a2
go.opentelemetry.io/otel v1.3.0
go.opentelemetry.io/otel/sdk v1.3.0
go.opentelemetry.io/otel/trace v1.3.0
@@ -62,6 +63,7 @@
github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/sergi/go-diff v1.1.0 // indirect
+ github.com/shurcooL/graphql v0.0.0-20200928012149-18c5c3165e3a // indirect
github.com/xanzy/ssh-agent v0.3.0 // indirect
go.opencensus.io v0.23.0 // indirect
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect
diff --git a/go.sum b/go.sum
index 944f4b8..455445d 100644
--- a/go.sum
+++ b/go.sum
@@ -435,6 +435,10 @@
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
+github.com/shurcooL/githubv4 v0.0.0-20220115235240-a14260e6f8a2 h1:82EIpiGB79OIPgSGa63Oj4Ipf+YAX1c6A9qjmEYoRXc=
+github.com/shurcooL/githubv4 v0.0.0-20220115235240-a14260e6f8a2/go.mod h1:hAF0iLZy4td2EX+/8Tw+4nodhlMrwN3HupfaXj3zkGo=
+github.com/shurcooL/graphql v0.0.0-20200928012149-18c5c3165e3a h1:KikTa6HtAK8cS1qjvUvvq4QO21QnwC+EfvB+OAuZ/ZU=
+github.com/shurcooL/graphql v0.0.0-20200928012149-18c5c3165e3a/go.mod h1:AuYgA5Kyo4c7HfUmvRGs/6rGlMMV/6B1bVnB9JxJEEg=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
diff --git a/internal/ghsa/ghsa.go b/internal/ghsa/ghsa.go
new file mode 100644
index 0000000..55775fe
--- /dev/null
+++ b/internal/ghsa/ghsa.go
@@ -0,0 +1,163 @@
+// Copyright 2022 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 supports GitHub security advisories.
+package ghsa
+
+import (
+ "context"
+ "fmt"
+ "time"
+
+ "github.com/shurcooL/githubv4"
+ "golang.org/x/oauth2"
+)
+
+// A SecurityAdvisory represents a GitHub security advisory.
+type SecurityAdvisory struct {
+ // The GitHub Security Advisory identifier
+ ID string
+ // A complete list of identifiers, e.g. CVE numbers.
+ Identifiers []Identifier
+ // A short description of the advisory.
+ Summary string
+ // A full description of the advisory.
+ Description string
+ // Where the advisory came from.
+ Origin string
+ // A link to a page for the advisory.
+ Permalink string
+ // When the advisory was first published.
+ PublishedAt time.Time
+ // When the advisory was last updated; should always be >= PublishedAt.
+ UpdatedAt time.Time
+ // The vulnerabilities associated with this advisory.
+ Vulns []*Vuln
+}
+
+// An Identifier identifies an advisory according to some scheme or
+// organization, given by the Type field. Examples are GitHub and CVE.
+type Identifier struct {
+ Type string
+ Value string
+}
+
+// A Vuln represents a vulnerability.
+type Vuln struct {
+ // The vulnerable Go package or module.
+ Package string
+ // The severity of the vulnerability.
+ Severity githubv4.SecurityAdvisorySeverity
+ // The earliest fixed version.
+ EarliestFixedVersion string
+ // A string representing the range of vulnerable versions.
+ // E.g. ">= 1.0.3"
+ VulnerableVersionRange string
+ // When the vulnerability was last updated.
+ UpdatedAt time.Time
+}
+
+// List returns all SecurityAdvisories that are not CVEs and that affect Go,
+// published or updated since the given time.
+func List(ctx context.Context, accessToken string, since time.Time) ([]*SecurityAdvisory, error) {
+ ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: accessToken})
+ tc := oauth2.NewClient(context.Background(), ts)
+ client := githubv4.NewClient(tc)
+
+ var query struct { // the GraphQL query
+ SAs struct {
+ Nodes []struct {
+ ID string
+ Identifiers []Identifier
+ Summary string
+ Description string
+ Origin string
+ Permalink githubv4.URI
+ PublishedAt time.Time
+ UpdatedAt time.Time
+ Vulnerabilities struct {
+ Nodes []struct {
+ Package struct {
+ Name string
+ Ecosystem string
+ }
+ FirstPatchedVersion struct{ Identifier string }
+ Severity githubv4.SecurityAdvisorySeverity
+ UpdatedAt time.Time
+ VulnerableVersionRange string
+ }
+ PageInfo struct {
+ HasNextPage bool
+ }
+ } `graphql:"vulnerabilities(first: 100, ecosystem: $go)"` // include only Go vulns
+ }
+ PageInfo struct {
+ EndCursor githubv4.String
+ HasNextPage bool
+ }
+ } `graphql:"securityAdvisories(updatedSince: $since, first: 100, after: $cursor)"`
+ }
+ vars := map[string]interface{}{
+ "cursor": (*githubv4.String)(nil),
+ "go": githubv4.SecurityAdvisoryEcosystemGo,
+ "since": githubv4.DateTime{Time: since},
+ }
+
+ var sas []*SecurityAdvisory
+ // We need a loop to page through the list. The GitHub API limits us to 100
+ // values per call.
+ for {
+ if err := client.Query(ctx, &query, vars); err != nil {
+ return nil, err
+ }
+ for _, sa := range query.SAs.Nodes {
+ if sa.PublishedAt.After(sa.UpdatedAt) {
+ return nil, fmt.Errorf("%s: published at %s, after updated at %s", sa.ID, sa.PublishedAt, sa.UpdatedAt)
+ }
+ if isCVE(sa.Identifiers) {
+ continue
+ }
+ if len(sa.Vulnerabilities.Nodes) == 0 {
+ continue
+ }
+ if sa.Vulnerabilities.PageInfo.HasNextPage {
+ return nil, fmt.Errorf("%s has more than 100 vulns", sa.ID)
+ }
+ s := &SecurityAdvisory{
+ ID: sa.ID,
+ Identifiers: sa.Identifiers,
+ Summary: sa.Summary,
+ Description: sa.Description,
+ Origin: sa.Origin,
+ Permalink: sa.Permalink.URL.String(),
+ PublishedAt: sa.PublishedAt,
+ UpdatedAt: sa.UpdatedAt,
+ }
+ for _, v := range sa.Vulnerabilities.Nodes {
+ s.Vulns = append(s.Vulns, &Vuln{
+ Package: v.Package.Name,
+ Severity: v.Severity,
+ EarliestFixedVersion: v.FirstPatchedVersion.Identifier,
+ VulnerableVersionRange: v.VulnerableVersionRange,
+ UpdatedAt: v.UpdatedAt,
+ })
+ }
+ sas = append(sas, s)
+ }
+ if !query.SAs.PageInfo.HasNextPage {
+ break
+ }
+ vars["cursor"] = githubv4.NewString(query.SAs.PageInfo.EndCursor)
+ }
+ return sas, nil
+}
+
+func isCVE(ids []Identifier) bool {
+ for _, id := range ids {
+ if id.Type == "CVE" {
+ return true
+ }
+ }
+ return false
+}
diff --git a/internal/ghsa/ghsa_test.go b/internal/ghsa/ghsa_test.go
new file mode 100644
index 0000000..664b33e
--- /dev/null
+++ b/internal/ghsa/ghsa_test.go
@@ -0,0 +1,37 @@
+// Copyright 2022 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"
+ "flag"
+ "os"
+ "strings"
+ "testing"
+ "time"
+)
+
+var githubTokenFile = flag.String("ghtokenfile", "",
+ "path to file containing GitHub access token")
+
+func TestList(t *testing.T) {
+ if *githubTokenFile == "" {
+ t.Skip("-ghtokenfile not provided")
+ }
+ bytes, err := os.ReadFile(*githubTokenFile)
+ if err != nil {
+ t.Fatal(err)
+ }
+ accessToken := strings.TrimSpace(string(bytes))
+ // There were three relevant SAs From Jan 1 to Feb 7 2022.
+ got, err := List(context.Background(), accessToken, time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC))
+ if err != nil {
+ t.Fatal(err)
+ }
+ want := 3
+ if len(got) < want {
+ t.Errorf("got %d, want at least %d", len(got), want)
+ }
+}