Skip to content

Commit d66b9fe

Browse files
author
Tatiana Bradley
committed
x/vulndb: add cve publish and record commands
Adds a new command, cve publish, which can be used to publish CVE Records to MITRE from YAML reports or JSON files. Also adds a cve record command to look up existing CVE records by ID. The commands are currently only supported in the test environment as the MITRE API does not yet support the commands in production. To support these commands, this CL also contains logic to convert YAML report files to the new CVE JSON 5.0 format. For golang/go#53256 Change-Id: I024bb18a2ece851724ca97f2f6d77f6aafc956b0 Reviewed-on: https://go-review.googlesource.com/c/vulndb/+/411514 Reviewed-by: Tatiana Bradley <[email protected]> Reviewed-by: Julie Qiu <[email protected]>
1 parent 7c289b4 commit d66b9fe

File tree

10 files changed

+907
-42
lines changed

10 files changed

+907
-42
lines changed

cmd/cve/main.go

Lines changed: 112 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,20 @@
77
package main
88

99
import (
10+
"bufio"
11+
"encoding/json"
1012
"errors"
1113
"flag"
1214
"fmt"
1315
"log"
1416
"os"
1517
"regexp"
18+
"strings"
1619
"time"
1720

1821
"golang.org/x/vulndb/internal/cveclient"
22+
"golang.org/x/vulndb/internal/cveschema5"
23+
"golang.org/x/vulndb/internal/report"
1924
)
2025

2126
var (
@@ -34,6 +39,9 @@ var (
3439
// flags for the list command
3540
listState = flag.String("state", "", "list: filter by CVE state (RESERVED, PUBLIC, or REJECT)")
3641

42+
// flags for the publish command
43+
publishUpdate = flag.Bool("update", false, "publish: if true, update an existing CVE Record")
44+
3745
// flags that apply to multiple commands
3846
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")
3947
)
@@ -47,6 +55,8 @@ func main() {
4755
fmt.Fprintf(out, formatCmd, "[-n] [-seq] [-year] reserve", "reserves new CVE IDs")
4856
fmt.Fprintf(out, formatCmd, "quota", "outputs the CVE ID quota of the authenticated organization")
4957
fmt.Fprintf(out, formatCmd, "id {cve-id}", "outputs details on an assigned CVE ID (CVE-YYYY-NNNN)")
58+
fmt.Fprintf(out, formatCmd, "record {cve-id}", "outputs the record associated with a CVE ID (CVE-YYYY-NNNN)")
59+
fmt.Fprintf(out, formatCmd, "[-update] publish {filename}", "publishes a CVE Record from a YAML or JSON file")
5060
fmt.Fprintf(out, formatCmd, "org", "outputs details on the authenticated organization")
5161
fmt.Fprintf(out, formatCmd, "[-year] [-state] list", "lists all CVE IDs for an organization")
5262
flag.PrintDefaults()
@@ -111,6 +121,35 @@ func main() {
111121
if err := lookupID(c, id); err != nil {
112122
log.Fatalf("cve id: could not retrieve CVE IDs due to error:\n %v", err)
113123
}
124+
case "record":
125+
id, err := validateID(flag.Arg(1))
126+
if err != nil {
127+
logUsageErr("cve record", err)
128+
}
129+
// TODO(https://go.dev/issue/53256): Remove when record lookup is
130+
// supported by CVE Services API.
131+
if !*test {
132+
logUnsupportedErr("cve record")
133+
}
134+
if err := lookupRecord(c, id); err != nil {
135+
log.Fatalf("cve record: could not retrieve CVE record due to error:\n %v", err)
136+
}
137+
case "publish":
138+
filename := flag.Arg(1)
139+
if filename == "" {
140+
logUsageErr("cve publish", errors.New("filename must be provided"))
141+
}
142+
if !strings.HasSuffix(filename, ".json") && !strings.HasSuffix(filename, ".yaml") {
143+
logUsageErr("cve publish", errors.New("filename must end in '.json' or '.yaml'"))
144+
}
145+
// TODO(https://go.dev/issue/53256): Remove when record publish is
146+
// supported by CVE Services API.
147+
if !*test {
148+
logUnsupportedErr("cve publish")
149+
}
150+
if err := publish(c, filename, *publishUpdate); err != nil {
151+
log.Fatalf("cve publish: could not publish CVE record due to error:\n %v", err)
152+
}
114153
case "org":
115154
if err := lookupOrg(c); err != nil {
116155
log.Fatalf("cve org: could not retrieve org info due to error:\n %v", err)
@@ -141,6 +180,10 @@ func logUsageErr(context string, err error) {
141180
os.Exit(1)
142181
}
143182

183+
func logUnsupportedErr(context string) {
184+
log.Fatalf("%s: command not yet supported by MITRE CVE Services API", context)
185+
}
186+
144187
func getCurrentYear() int {
145188
year, _, _ := time.Now().Date()
146189
return year
@@ -200,11 +243,78 @@ func lookupOrg(c *cveclient.Client) error {
200243
}
201244

202245
func lookupID(c *cveclient.Client, id string) error {
203-
cve, err := c.RetrieveID(id)
246+
assigned, err := c.RetrieveID(id)
204247
if err != nil {
205248
return err
206249
}
207-
fmt.Println(cve)
250+
// Display the retrieved CVE ID metadata.
251+
fmt.Println(assigned)
252+
return nil
253+
}
254+
255+
const cveRecordLink = "https://cve.mitre.org/cgi-bin/cvename.cgi?name="
256+
257+
func recordToString(r *cveschema5.CVERecord) string {
258+
s, err := json.MarshalIndent(r, "", " ")
259+
if err != nil {
260+
s = []byte(fmt.Sprint(r))
261+
}
262+
return string(s)
263+
}
264+
265+
func lookupRecord(c *cveclient.Client, id string) error {
266+
record, err := c.RetrieveRecord(id)
267+
if err != nil {
268+
return err
269+
}
270+
// Display the retrieved CVE record.
271+
fmt.Println(recordToString(record))
272+
return nil
273+
}
274+
275+
func publish(c *cveclient.Client, filename string, update bool) (err error) {
276+
var toPublish *cveschema5.CVERecord
277+
switch {
278+
case strings.HasSuffix(filename, ".yaml"):
279+
toPublish, err = report.ToCVE5(filename)
280+
if err != nil {
281+
return err
282+
}
283+
case strings.HasSuffix(filename, ".json"):
284+
toPublish, err = cveschema5.Read(filename)
285+
if err != nil {
286+
return err
287+
}
288+
default:
289+
return errors.New("filename must end in '.json' or '.yaml'")
290+
}
291+
292+
reader := bufio.NewReader(os.Stdin)
293+
fmt.Printf("ready to publish:\n%s\ncontinue? (y/N)\n", recordToString(toPublish))
294+
text, _ := reader.ReadString('\n')
295+
if text != "y\n" {
296+
fmt.Println("exiting")
297+
return nil
298+
}
299+
300+
var (
301+
published *cveschema5.CVERecord
302+
action string
303+
)
304+
if update {
305+
published, err = c.UpdateRecord(toPublish.Metadata.ID, &toPublish.Containers)
306+
if err != nil {
307+
return err
308+
}
309+
action = "update"
310+
} else {
311+
published, err = c.CreateRecord(toPublish.Metadata.ID, &toPublish.Containers)
312+
if err != nil {
313+
return err
314+
}
315+
action = "create"
316+
}
317+
fmt.Printf("successfully %sd record for %s:\n%v\nlink: %s%s\n", action, published.Metadata.ID, recordToString(published), cveRecordLink, published.Metadata.ID)
208318
return nil
209319
}
210320

internal/cveclient/cveclient.go

Lines changed: 91 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
package cveclient
88

99
import (
10+
"bytes"
1011
"encoding/json"
1112
"fmt"
1213
"io"
@@ -15,6 +16,8 @@ import (
1516
"strconv"
1617
"strings"
1718
"time"
19+
20+
"golang.org/x/vulndb/internal/cveschema5"
1821
)
1922

2023
const (
@@ -128,12 +131,13 @@ func (o *ReserveOptions) getURLParams(org string) url.Values {
128131
}
129132

130133
func (c *Client) createReserveIDsRequest(opts ReserveOptions) (*http.Request, error) {
131-
req, err := c.createRequest(http.MethodPost, c.getURL(cveIDTarget))
134+
req, err := c.createRequest(http.MethodPost,
135+
c.getURL(cveIDTarget), nil)
132136
if err != nil {
133137
return nil, err
134138
}
135139
req.URL.RawQuery = opts.getURLParams(c.Org).Encode()
136-
return req, nil
140+
return req, err
137141
}
138142

139143
type reserveIDsResponse struct {
@@ -169,16 +173,61 @@ type Quota struct {
169173

170174
// RetrieveQuota queries the API for the organizations reservation quota.
171175
func (c *Client) RetrieveQuota() (q *Quota, err error) {
172-
err = c.queryAPI(http.MethodGet, c.getURL(orgTarget, c.Org, quotaTarget), &q)
176+
err = c.queryAPI(http.MethodGet, c.getURL(orgTarget, c.Org, quotaTarget), nil, &q)
173177
return
174178
}
175179

176180
// RetrieveID requests information about an assigned CVE ID.
177181
func (c *Client) RetrieveID(id string) (cve *AssignedCVE, err error) {
178-
err = c.queryAPI(http.MethodGet, c.getURL(cveIDTarget, id), &cve)
182+
err = c.queryAPI(http.MethodGet, c.getURL(cveIDTarget, id), nil, &cve)
183+
return
184+
}
185+
186+
// RetrieveRecord requests a CVE record.
187+
func (c *Client) RetrieveRecord(id string) (cve *cveschema5.CVERecord, err error) {
188+
err = c.queryAPI(http.MethodGet, c.getURL(cveTarget, id, cnaTarget), nil, &cve)
179189
return
180190
}
181191

192+
func (c *Client) getCVERecordEndpoint(cveID string) string {
193+
return c.getURL(cveTarget, cveID, cnaTarget)
194+
}
195+
196+
type recordRequestBody struct {
197+
CNAContainer cveschema5.CNAPublishedContainer `json:"cnaContainer"`
198+
}
199+
type createResponse struct {
200+
Created cveschema5.CVERecord `json:"created"`
201+
}
202+
203+
func (c *Client) CreateRecord(id string, record *cveschema5.Containers) (*cveschema5.CVERecord, error) {
204+
requestBody := recordRequestBody{
205+
CNAContainer: record.CNAContainer,
206+
}
207+
var response createResponse
208+
err := c.queryAPI(http.MethodPost, c.getCVERecordEndpoint(id), requestBody, &response)
209+
if err != nil {
210+
return nil, err
211+
}
212+
return &response.Created, nil
213+
}
214+
215+
type updateResponse struct {
216+
Updated cveschema5.CVERecord `json:"updated"`
217+
}
218+
219+
func (c *Client) UpdateRecord(id string, record *cveschema5.Containers) (*cveschema5.CVERecord, error) {
220+
requestBody := recordRequestBody{
221+
CNAContainer: record.CNAContainer,
222+
}
223+
var response updateResponse
224+
err := c.queryAPI(http.MethodPut, c.getCVERecordEndpoint(id), requestBody, &response)
225+
if err != nil {
226+
return nil, err
227+
}
228+
return &response.Updated, nil
229+
}
230+
182231
type Org struct {
183232
Name string `json:"name"`
184233
ShortName string `json:"short_name"`
@@ -187,7 +236,7 @@ type Org struct {
187236

188237
// RetrieveOrg requests information about an organization.
189238
func (c *Client) RetrieveOrg() (org *Org, err error) {
190-
err = c.queryAPI(http.MethodGet, c.getURL(orgTarget, c.Org), &org)
239+
err = c.queryAPI(http.MethodGet, c.getURL(orgTarget, c.Org), nil, &org)
191240
return
192241
}
193242

@@ -257,8 +306,8 @@ type listOrgCVEsResponse struct {
257306
CVEs AssignedCVEList `json:"cve_ids"`
258307
}
259308

260-
func (c Client) createListOrgCVEsRequest(opts *ListOptions, page int) (*http.Request, error) {
261-
req, err := c.createRequest(http.MethodGet, c.getURL(cveIDTarget))
309+
func (c Client) createListOrgCVEsRequest(opts *ListOptions, page int) (req *http.Request, err error) {
310+
req, err = c.createRequest(http.MethodGet, c.getURL(cveIDTarget), nil)
262311
if err != nil {
263312
return nil, err
264313
}
@@ -267,7 +316,7 @@ func (c Client) createListOrgCVEsRequest(opts *ListOptions, page int) (*http.Req
267316
params.Set("page", fmt.Sprint(page))
268317
}
269318
req.URL.RawQuery = params.Encode()
270-
return req, nil
319+
return
271320
}
272321

273322
// ListOrgCVEs requests information about the CVEs the organization has been
@@ -294,8 +343,8 @@ func (c *Client) ListOrgCVEs(opts *ListOptions) (AssignedCVEList, error) {
294343
return cves, nil
295344
}
296345

297-
func (c *Client) queryAPI(method, url string, response any) error {
298-
req, err := c.createRequest(method, url)
346+
func (c *Client) queryAPI(method, url string, requestBody any, response any) error {
347+
req, err := c.createRequest(method, url, requestBody)
299348
if err != nil {
300349
return err
301350
}
@@ -313,14 +362,23 @@ var (
313362
)
314363

315364
// createRequest creates a new HTTP request and sets the header fields.
316-
func (c *Client) createRequest(method, url string) (*http.Request, error) {
317-
req, err := http.NewRequest(method, url, nil)
365+
func (c *Client) createRequest(method, url string, body any) (*http.Request, error) {
366+
var r io.Reader
367+
if body != nil {
368+
b, err := json.Marshal(body)
369+
if err != nil {
370+
return nil, err
371+
}
372+
r = bytes.NewReader(b)
373+
}
374+
req, err := http.NewRequest(method, url, r)
318375
if err != nil {
319376
return nil, err
320377
}
321378
req.Header.Set(headerApiUser, c.User)
322379
req.Header.Set(headerApiOrg, c.Org)
323380
req.Header.Set(headerApiKey, c.Key)
381+
req.Header.Set("Content-Type", "application/json")
324382
return req, nil
325383
}
326384

@@ -352,18 +410,30 @@ func (c *Client) sendRequest(req *http.Request, checkStatus func(int) bool, resu
352410
}
353411

354412
var (
413+
cveTarget = "cve"
355414
cveIDTarget = "cve-id"
356415
orgTarget = "org"
357416
quotaTarget = "id_quota"
417+
cnaTarget = "cna"
358418
)
359419

360420
func (c *Client) getURL(targets ...string) string {
361421
return fmt.Sprintf("%s/api/%s", c.Endpoint, strings.Join(targets, "/"))
362422
}
363423

364424
type apiError struct {
365-
Error string `json:"error"`
366-
Message string `json:"message"`
425+
Error string `json:"error"`
426+
Message string `json:"message"`
427+
Detail apiErrorDetail `json:"details"`
428+
}
429+
430+
type apiErrorDetail struct {
431+
Errors []apiErrorInner `json:"errors"`
432+
}
433+
434+
type apiErrorInner struct {
435+
InstancePath string `json:"instancePath"`
436+
Message string `json:"message"`
367437
}
368438

369439
// extractError extracts additional error messages from the HTTP response
@@ -372,13 +442,11 @@ func extractError(resp *http.Response) error {
372442
errMsg := resp.Status
373443
body, err := io.ReadAll(resp.Body)
374444
if err != nil {
375-
// Discard the read error and return the HTTP status.
376-
return fmt.Errorf(errMsg)
445+
return fmt.Errorf("%s: could not read error data: %s", errMsg, err)
377446
}
378447
var apiErr apiError
379448
if err := json.Unmarshal(body, &apiErr); err != nil {
380-
// Discard the unmarshal error and return the HTTP status.
381-
return fmt.Errorf(errMsg)
449+
return fmt.Errorf("%s: could not unmarshal error: %s", errMsg, err)
382450
}
383451

384452
// Append the error and message text if they add extra information
@@ -389,5 +457,10 @@ func extractError(resp *http.Response) error {
389457
errMsg = fmt.Sprintf("%s: %s", errMsg, errText)
390458
}
391459
}
460+
461+
for _, detail := range apiErr.Detail.Errors {
462+
errMsg = fmt.Sprintf("%s\n %s: %s", errMsg, detail.InstancePath, detail.Message)
463+
}
464+
392465
return fmt.Errorf(errMsg)
393466
}

0 commit comments

Comments
 (0)