blob: 55775fea4de49703e100c2530981b315ff754af3 [file] [log] [blame]
// 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
}