Skip to content

Commit bbf7cc7

Browse files
tatianabTatiana Bradley
authored and
Tatiana Bradley
committed
cmd/cve, internal/cveclient: check if a record is already published in cve publish
Instead of asking the user to provide a flag -update indicating whether a record is already published, use the CVE Services API to determine this automatically. Change-Id: I6e5bf7d6e095360335043424eb3330aeaf23b297 Reviewed-on: https://go-review.googlesource.com/c/vulndb/+/446218 Reviewed-by: Jonathan Amsterdam <[email protected]> Reviewed-by: Tatiana Bradley <[email protected]> TryBot-Result: Gopher Robot <[email protected]> Run-TryBot: Tatiana Bradley <[email protected]>
1 parent 4b2e401 commit bbf7cc7

File tree

5 files changed

+72
-48
lines changed

5 files changed

+72
-48
lines changed

cmd/cve/main.go

Lines changed: 54 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"strings"
1919
"time"
2020

21+
"github.com/google/go-cmp/cmp"
2122
"golang.org/x/vulndb/internal/cveclient"
2223
"golang.org/x/vulndb/internal/cveschema5"
2324
"golang.org/x/vulndb/internal/report"
@@ -43,10 +44,7 @@ var (
4344
reserveSequential = flag.Bool("seq", true, "reserve: if true, reserve new CVE ID batches in sequence")
4445

4546
// flags for the list command
46-
listState = flag.String("state", "", "list: filter by CVE state (RESERVED, PUBLIC, or REJECT)")
47-
48-
// flags for the publish command
49-
publishUpdate = flag.Bool("update", false, "publish: if true, update an existing CVE Record")
47+
listState = flag.String("state", "", "list: filter by CVE state (RESERVED, PUBLISHED, or REJECTED)")
5048

5149
// flags that apply to multiple commands
5250
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")
@@ -62,7 +60,7 @@ func main() {
6260
fmt.Fprintf(out, formatCmd, "quota", "outputs the CVE ID quota of the authenticated organization")
6361
fmt.Fprintf(out, formatCmd, "id {cve-id}", "outputs details on an assigned CVE ID (CVE-YYYY-NNNN)")
6462
fmt.Fprintf(out, formatCmd, "record {cve-id}", "outputs the record associated with a CVE ID (CVE-YYYY-NNNN)")
65-
fmt.Fprintf(out, formatCmd, "[-update] publish {filename}", "publishes a CVE Record from a YAML or JSON file")
63+
fmt.Fprintf(out, formatCmd, "publish {filename}", "publishes or updates a CVE Record from a YAML or JSON file")
6664
fmt.Fprintf(out, formatCmd, "org", "outputs details on the authenticated organization")
6765
fmt.Fprintf(out, formatCmd, "[-year] [-state] list", "lists all CVE IDs for an organization")
6866
flag.PrintDefaults()
@@ -123,7 +121,7 @@ func main() {
123121
if !strings.HasSuffix(filename, ".json") && !strings.HasSuffix(filename, ".yaml") {
124122
logFatalUsageErr("cve publish", errors.New("filename must end in '.json' or '.yaml'"))
125123
}
126-
if err := publish(c, filename, *publishUpdate); err != nil {
124+
if err := publish(c, filename); err != nil {
127125
log.Fatalf("cve publish: could not publish CVE record due to error:\n %v", err)
128126
}
129127
case "org":
@@ -203,7 +201,7 @@ func validateID(id string) (string, error) {
203201
return id, nil
204202
}
205203

206-
var stateRegex = regexp.MustCompile(`^(RESERVED|PUBLIC|REJECT)$`)
204+
var stateRegex = regexp.MustCompile(`^(RESERVED|PUBLISHED|REJECTED)$`)
207205

208206
func validateState(state string) (string, error) {
209207
if state != "" && !stateRegex.MatchString(state) {
@@ -254,10 +252,12 @@ func lookupID(c *cveclient.Client, id string) error {
254252
return nil
255253
}
256254

257-
func recordToString(r *cveschema5.CVERecord) string {
258-
s, err := json.MarshalIndent(r, "", " ")
255+
// toJSON converts a struct into a JSON string.
256+
// If JSON marshal fails, it falls back to fmt.Sprint.
257+
func toJSON(v any) string {
258+
s, err := json.Marshal(v)
259259
if err != nil {
260-
s = []byte(fmt.Sprint(r))
260+
return fmt.Sprint(v)
261261
}
262262
return string(s)
263263
}
@@ -268,53 +268,67 @@ func lookupRecord(c *cveclient.Client, id string) error {
268268
return err
269269
}
270270
// Display the retrieved CVE record.
271-
fmt.Println(recordToString(record))
271+
fmt.Println(toJSON(record))
272272
return nil
273273
}
274274

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)
275+
func publish(c *cveclient.Client, filename string) (err error) {
276+
if !strings.HasSuffix(filename, ".json") {
277+
return errors.New("filename must end in '.json'")
278+
}
279+
280+
cveID, toPublish, err := cveschema5.ReadForPublish(filename)
281+
if err != nil {
282+
return err
283+
}
284+
285+
// Determine if the record should be created or updated.
286+
assigned, err := c.RetrieveID(cveID)
287+
if err != nil {
288+
return err
289+
}
290+
291+
var (
292+
publish func(string, *cveschema5.Containers) (*cveschema5.CVERecord, error)
293+
action string
294+
)
295+
switch state := assigned.State; state {
296+
case cveschema5.StatePublished:
297+
existing, err := c.RetrieveRecord(cveID)
280298
if err != nil {
281299
return err
282300
}
283-
case strings.HasSuffix(filename, ".json"):
284-
toPublish, err = cveschema5.Read(filename)
285-
if err != nil {
286-
return err
301+
if diff := cmp.Diff(existing.Containers, *toPublish); diff != "" {
302+
fmt.Printf("publish would update record for %s (-existing, +new):\n%s\n", cveID, diff)
303+
} else {
304+
fmt.Println("updating record would have no effect, exiting")
305+
return nil
287306
}
307+
publish = c.UpdateRecord
308+
action = "update"
309+
case cveschema5.StateReserved:
310+
fmt.Printf("publish would create new record for %s\n", cveID)
311+
publish = c.CreateRecord
312+
action = "create"
288313
default:
289-
return errors.New("filename must end in '.json' or '.yaml'")
314+
return fmt.Errorf("publishing a %s record is not supported", state)
290315
}
291316

292317
reader := bufio.NewReader(os.Stdin)
293-
fmt.Printf("ready to publish:\n%s\ncontinue? (y/N)\n", recordToString(toPublish))
318+
fmt.Printf("%s record for %s? (y/N)\n", action, cveID)
294319
text, _ := reader.ReadString('\n')
295320
if text != "y\n" {
296-
fmt.Println("exiting")
321+
fmt.Printf("exiting without %sing record\n", strings.TrimSuffix(action, "e"))
297322
return nil
298323
}
299324

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"
325+
published, err := publish(cveID, toPublish)
326+
if err != nil {
327+
return err
316328
}
317-
fmt.Printf("successfully %sd record for %s:\n%v\nlink: %s%s\n", action, published.Metadata.ID, recordToString(published), report.NISTPrefix, published.Metadata.ID)
329+
330+
fmt.Printf("successfully %sd record for %s:\n\n%v\n\nlink: %s%s\n", action, cveID, toJSON(published), report.MITREPrefix, cveID)
331+
318332
return nil
319333
}
320334

internal/cveclient/cveclient.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,12 +55,12 @@ func New(cfg Config) *Client {
5555

5656
// AssignedCVE contains information about an assigned CVE.
5757
type AssignedCVE struct {
58-
ID string `json:"cve_id"`
59-
Year string `json:"cve_year"`
60-
State string `json:"state"`
61-
CNA string `json:"owning_cna"`
62-
Reserved time.Time `json:"reserved"`
63-
RequestedBy RequestedBy `json:"requested_by"`
58+
ID string `json:"cve_id"`
59+
Year string `json:"cve_year"`
60+
State cveschema5.State `json:"state"`
61+
CNA string `json:"owning_cna"`
62+
Reserved time.Time `json:"reserved"`
63+
RequestedBy RequestedBy `json:"requested_by"`
6464
}
6565

6666
// RequestedBy indicates the requesting user and organization for a CVE.

internal/cveclient/cveclient_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ var (
6565
testTime1992 = time.Date(1992, 1, 1, 0, 0, 0, 0, time.UTC)
6666
)
6767

68-
func newTestCVE(id, state, year string) AssignedCVE {
68+
func newTestCVE(id string, state cveschema5.State, year string) AssignedCVE {
6969
return AssignedCVE{
7070
ID: id,
7171
Year: year,

internal/cveschema5/cveschema5.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,3 +122,13 @@ func Read(filename string) (*CVERecord, error) {
122122
}
123123
return &record, nil
124124
}
125+
126+
// ReadForPublish reads the portion of a CVE record that can be published
127+
// via the CVE Services API from filename.
128+
func ReadForPublish(filename string) (cveID string, toPublish *Containers, err error) {
129+
record, err := Read(filename)
130+
if err != nil {
131+
return "", nil, err
132+
}
133+
return record.Metadata.ID, &record.Containers, nil
134+
}

internal/report/report.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,7 @@ func (r *Report) GetAliases() []string {
214214

215215
const (
216216
NISTPrefix = "https://nvd.nist.gov/vuln/detail/"
217-
mitrePrefix = "https://cve.mitre.org/cgi-bin/cvename.cgi?name="
217+
MITREPrefix = "https://cve.org/CVERecord?id="
218218
ghsaURLPrefix = "https://github.com/advisories/"
219219
goURLPrefix = "https://pkg.go.dev/vuln/"
220220
)

0 commit comments

Comments
 (0)