blob: f70799e3a4ec1088f126eba2beb307408891ae99 [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.
// Command cve provides utilities for managing CVE IDs and CVE Records via the
// MITRE CVE Services API.
package main
import (
"errors"
"flag"
"fmt"
"log"
"os"
"regexp"
"time"
"golang.org/x/vulndb/internal/cveclient"
)
var (
apiKey = flag.String("key",
os.Getenv("CVE_API_KEY"), "key for accessing the CVE API (can also be set via env var CVE_API_KEY)")
apiUser = flag.String("user",
os.Getenv("CVE_API_USER"), "username for accessing the CVE API (can also be set via env var CVE_API_USER)")
apiOrg = flag.String("org",
"Go", "organization name for accessing the CVE API")
test = flag.Bool("test", false, "whether to access the CVE API in the test environment")
// flags for the reserve command
reserveN = flag.Int("n", 1, "reserve: the number of new CVE IDs to reserve")
reserveSequential = flag.Bool("seq", true, "reserve: if true, reserve new CVE ID batches in sequence")
// flags for the list command
listState = flag.String("state", "", "list: filter by CVE state (RESERVED, PUBLIC, or REJECT)")
// flags that apply to multiple commands
year = flag.Int("year", 0, "reserve: the CVE ID year for newly reserved CVE IDs (default is current year)\nlist: filter by the year in the CVE ID")
)
func main() {
out := flag.CommandLine.Output()
flag.Usage = func() {
fmt.Fprintln(out, "Command cve provides utilities for managing CVE IDs and CVE Records via the MITRE CVE Services API")
formatCmd := " %s: %s\n"
fmt.Fprintf(out, "usage: cve [-key] [-user] [-org] [-test] <cmd> ...\n commands:\n")
fmt.Fprintf(out, formatCmd, "[-n] [-seq] [-year] reserve", "reserves new CVE IDs")
fmt.Fprintf(out, formatCmd, "quota", "outputs the CVE ID quota of an organization")
fmt.Fprintf(out, formatCmd, "lookup {cve-id}", "outputs details on an assigned CVE ID (CVE-YYYY-NNNN)")
fmt.Fprintf(out, formatCmd, "[-year] [-state] list", "lists all CVE IDs for an organization")
flag.PrintDefaults()
}
flag.Parse()
if flag.NArg() < 1 {
logUsageErr("cve", fmt.Errorf("must provide subcommand"))
}
// The cve tool does not currently support the dev endpoint as there is
// no clear use case for us.
endpoint := cveclient.ProdEndpoint
if *test {
endpoint = cveclient.TestEndpoint
}
if *apiKey == "" {
logUsageErr("cve", errors.New("the CVE API key (flag -key or env var CVE_API_KEY) must be set"))
}
if *apiUser == "" {
logUsageErr("cve", errors.New("the CVE API user (flag -user or env var CVE_API_USER) must be set"))
}
cfg := cveclient.Config{
Endpoint: endpoint,
Key: *apiKey,
Org: *apiOrg,
User: *apiUser,
}
c := cveclient.New(cfg)
cmd := flag.Arg(0)
switch cmd {
case "help":
flag.Usage()
case "reserve":
year := *year
if year == 0 {
year = getCurrentYear()
}
mode := cveclient.SequentialRequest
if !*reserveSequential {
mode = cveclient.NonsequentialRequest
}
if err := reserve(c, cveclient.ReserveOptions{
NumIDs: *reserveN,
Year: year,
Mode: mode,
}); err != nil {
log.Fatalf("cve reserve: could not reserve any new CVEs due to error:\n %v", err)
}
case "quota":
if err := quota(c); err != nil {
log.Fatalf("cve quota: could not retrieve quota info due to error:\n %v", err)
}
case "lookup":
id, err := validateID(flag.Arg(1))
if err != nil {
logUsageErr("cve lookup", err)
}
if err := lookup(c, id); err != nil {
log.Fatalf("cve lookup: could not retrieve CVE IDs due to error:\n %v", err)
}
case "list":
// TODO(http://go.dev/issues/53258): allow time-based filters via flags.
var filters *cveclient.ListOptions
if *listState != "" || *year != 0 {
filters = new(cveclient.ListOptions)
state, err := validateState(*listState)
if err != nil {
logUsageErr("cve list", err)
}
filters.State = state
filters.Year = *year
}
if err := list(c, filters); err != nil {
log.Fatalf("cve list: could not retrieve CVE IDs due to error:\n %v", err)
}
default:
logUsageErr("cve", fmt.Errorf("unsupported command: %q", cmd))
}
}
func logUsageErr(context string, err error) {
log.Printf("%s: %s\n\n", context, err)
flag.Usage()
os.Exit(1)
}
func getCurrentYear() int {
year, _, _ := time.Now().Date()
return year
}
var cveRegex = regexp.MustCompile(`^CVE-\d{4}-\d{4,}$`)
func validateID(id string) (string, error) {
if id == "" {
return "", errors.New("CVE ID must be provided")
}
if !cveRegex.MatchString(id) {
return "", fmt.Errorf("%q is not a valid CVE ID", id)
}
return id, nil
}
var stateRegex = regexp.MustCompile(`^(RESERVED|PUBLIC|REJECT)$`)
func validateState(state string) (string, error) {
if state != "" && !stateRegex.MatchString(state) {
return "", fmt.Errorf("state must match regex %v", stateRegex)
}
return state, nil
}
func reserve(c *cveclient.Client, opts cveclient.ReserveOptions) error {
cves, err := c.ReserveIDs(opts)
if err != nil {
return err
}
cvesReserved := len(cves)
if cvesReserved < opts.NumIDs {
fmt.Printf("warning: only %d of %d requested CVE IDs were reserved\n",
len(cves), opts.NumIDs)
}
fmt.Printf("successfully reserved %d CVE IDs:\n %v\n", cvesReserved, cves.ShortString())
return nil
}
func quota(c *cveclient.Client) error {
quota, err := c.RetrieveQuota()
if err != nil {
return err
}
fmt.Printf("quota info for org %q:\n quota: %d\n total reserved: %d\n available: %d\n", c.Org, quota.Quota, quota.Reserved, quota.Available)
return nil
}
func lookup(c *cveclient.Client, id string) error {
cve, err := c.RetrieveCVE(id)
if err != nil {
return err
}
fmt.Println(cve)
return nil
}
func list(c *cveclient.Client, lf *cveclient.ListOptions) error {
cves, err := c.ListOrgCVEs(lf)
if err != nil {
return err
}
var filterString string
if lf != nil {
filterString = fmt.Sprintf(" with filters %s", lf)
}
if n := len(cves); n > 0 {
fmt.Printf("found %d CVE IDs for org %q%s:\n%v\n", n, c.Org, filterString, cves)
} else {
fmt.Printf("found no CVE IDs for org %q%s\n", c.Org, filterString)
}
return nil
}