blob: fb335be3056c1eb6e8ae0aa25a28f3e1ddfc8e80 [file] [log] [blame]
// Copyright 2023 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 ghsarepo provides a client and utilities for reading
// GitHub security advisories directly from the Git repo
// https://github.com/github/advisory-database.
//
// This allows us to read GHSAs in OSV format instead of
// the SecurityAdvisory format output by the GraphQL API.
package ghsarepo
import (
"context"
"encoding/json"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/storage/memory"
"golang.org/x/exp/maps"
"golang.org/x/vulndb/internal/genericosv"
"golang.org/x/vulndb/internal/gitrepo"
)
type Client struct {
byID map[string]*genericosv.Entry
byAlias map[string][]*genericosv.Entry
}
const URL = "https://github.com/github/advisory-database"
const DirectURLPrefix = "https://raw.githubusercontent.com/github/advisory-database/main/advisories/github-reviewed"
// NewDefaultClient returns a client to read from the GHSA database.
// It clones the Git repo at https://github.com/github/advisory-database,
// which can take around ~20 seconds.
func NewDefaultClient() (*Client, error) {
repo, err := git.Clone(memory.NewStorage(), nil, &git.CloneOptions{
URL: URL,
ReferenceName: "refs/heads/main",
SingleBranch: true,
Depth: 1,
Tags: git.NoTags,
NoCheckout: true,
})
if err != nil {
return nil, err
}
return NewClient(repo)
}
func NewLocalClient(ctx context.Context, path string) (*Client, error) {
repo, err := gitrepo.Open(ctx, path)
if err != nil {
return nil, err
}
return NewClient(repo)
}
// NewClient returns a client that reads from the GHSA database
// in the given repo, which must follow the structure of
// https://github.com/github/advisory-database.
func NewClient(repo *git.Repository) (*Client, error) {
hc, err := gitrepo.HeadCommit(repo)
if err != nil {
return nil, err
}
files, err := Files(repo, hc)
if err != nil {
return nil, err
}
c := &Client{
byID: make(map[string]*genericosv.Entry),
byAlias: make(map[string][]*genericosv.Entry),
}
for _, f := range files {
var advisory genericosv.Entry
b, err := f.ReadAll(repo)
if err != nil {
return nil, err
}
if err := json.Unmarshal(b, &advisory); err != nil {
return nil, err
}
if !advisory.AffectsGo() {
continue
}
if advisory.IsWithdrawn() {
continue
}
c.byID[advisory.ID] = &advisory
for _, alias := range advisory.Aliases {
c.byAlias[alias] = append(c.byAlias[alias], &advisory)
}
}
return c, nil
}
// IDs returns all the GHSA IDs in the GHSA database
// that affect Go and are not withdrawn.
func (c *Client) IDs() []string {
return maps.Keys(c.byID)
}
// List returns all the genericosv.Entry entries in the GHSA database
// that affect Go and are not withdrawn.
func (c *Client) List() []*genericosv.Entry {
return maps.Values(c.byID)
}
// ByGHSA returns the genericosv.Entry entry for the given GHSA, or nil if none
// exists.
func (c *Client) ByGHSA(ghsa string) *genericosv.Entry {
return c.byID[ghsa]
}
// ByCVE returns the genericosv.Entry entries for the given CVE, or nil if none
// exist.
func (c *Client) ByCVE(cve string) []*genericosv.Entry {
return c.byAlias[cve]
}