cmd/vulnreport: automatically decide whether to create REVIEWED or UNREVIEWED report

Command vulnreport create now decides whether to generate a REVIEWED
or UNREVIEWED report based on issue's labels.

This can be overridden with flag "-status=<REVIEW_STATUS>". The "unreviewed"
flag is removed.

Change-Id: I8f8b808c6f9bbcaeb0dc176fb6cb875b8f9ccee4
Reviewed-on: https://go-review.googlesource.com/c/vulndb/+/587976
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Damien Neil <dneil@google.com>
diff --git a/cmd/issue/main.go b/cmd/issue/main.go
index b1b9cfb..b05d883 100644
--- a/cmd/issue/main.go
+++ b/cmd/issue/main.go
@@ -108,7 +108,7 @@
 		aliases := []string{arg}
 		packages := []string{"<placeholder>"}
 		bodies := []string{fmt.Sprintf("This is a placeholder issue for %q.", arg)}
-		if err := publishIssue(ctx, c, packages, aliases, bodies, []string{}); err != nil {
+		if err := publishIssue(ctx, c, packages, aliases, bodies, []string{"first party"}); err != nil {
 			return err
 		}
 	}
diff --git a/cmd/vulnreport/create.go b/cmd/vulnreport/create.go
index 9d0ece0..d1dbff4 100644
--- a/cmd/vulnreport/create.go
+++ b/cmd/vulnreport/create.go
@@ -16,7 +16,7 @@
 	useAI           = flag.Bool("ai", false, "for create, use AI to write draft summary and description when creating report")
 	populateSymbols = flag.Bool("symbols", false, "for create, attempt to auto-populate symbols")
 	user            = flag.String("user", "", "for create & create-excluded, only consider issues assigned to the given user")
-	unreviewed      = flag.Bool("unreviewed", false, "for create, create issues in unreviewed state")
+	reviewStatus    = flag.String("status", "", "for create, use this review status (REVIEWED or UNREVIEWED) instead of default based on label")
 )
 
 type create struct {
diff --git a/cmd/vulnreport/creator.go b/cmd/vulnreport/creator.go
index d7901a8..aa2ef88 100644
--- a/cmd/vulnreport/creator.go
+++ b/cmd/vulnreport/creator.go
@@ -22,6 +22,10 @@
 	assignee string
 	created  []*report.Report
 
+	// If non-zero, use this review status
+	// instead of the default for new reports.
+	reviewStatus report.ReviewStatus
+
 	*fixer
 	*xrefer
 	*suggester
@@ -34,6 +38,12 @@
 	}
 	c.assignee = user
 
+	rs, ok := report.ToReviewStatus(*reviewStatus)
+	if !ok {
+		return fmt.Errorf("invalid -status=%s", rs)
+	}
+	c.reviewStatus = rs
+
 	c.fixer = new(fixer)
 	c.xrefer = new(xrefer)
 	if *useAI {
@@ -52,15 +62,6 @@
 		return fmt.Sprintf("assignee = %q, not %q", iss.Assignee, c.assignee)
 	}
 
-	if *unreviewed {
-		if iss.HasLabel(labelDirect) {
-			return "should not create unreviewed report for direct report"
-		}
-		if iss.HasLabel(labelHighPriority) {
-			return "should not create unreviewed report for high priority issue"
-		}
-	}
-
 	return c.xrefer.skipReason(iss)
 }
 
@@ -90,14 +91,36 @@
 
 func (c *creator) reportFromIssue(ctx context.Context, iss *issues.Issue) error {
 	return c.reportFromMeta(ctx, &reportMeta{
-		id:         iss.NewGoID(),
-		excluded:   excludedReason(iss),
-		modulePath: modulePath(iss),
-		aliases:    aliases(iss),
-		unreviewed: *unreviewed,
+		id:           iss.NewGoID(),
+		excluded:     excludedReason(iss),
+		modulePath:   modulePath(iss),
+		aliases:      aliases(iss),
+		reviewStatus: reviewStatusOf(iss, c.reviewStatus),
 	})
 }
 
+func reviewStatusOf(iss *issues.Issue, reviewStatus report.ReviewStatus) report.ReviewStatus {
+	d := defaultReviewStatus(iss)
+	// If a valid review status is provided, it overrides the priority label.
+	if reviewStatus != 0 {
+		if d != reviewStatus {
+			log.Warnf("issue %d should be %s based on label(s) but this was overridden with the -status=%s flag", iss.Number, d, reviewStatus)
+		}
+		return reviewStatus
+	}
+	return d
+}
+
+func defaultReviewStatus(iss *issues.Issue) report.ReviewStatus {
+	if iss.HasLabel(labelHighPriority) ||
+		iss.HasLabel(labelDirect) ||
+		iss.HasLabel(labelFirstParty) {
+		return report.Reviewed
+	}
+
+	return report.Unreviewed
+}
+
 func (c *creator) reportFromMeta(ctx context.Context, meta *reportMeta) error {
 	// Find the underlying module if the "module" provided is actually a package path.
 	if module, err := c.pc.FindModule(meta.modulePath); err == nil { // no error
@@ -113,16 +136,11 @@
 		log.Info("no suitable alias found, creating basic report")
 	}
 
-	status := report.Reviewed
-	if meta.unreviewed {
-		status = report.Unreviewed
-	}
-
 	r := report.New(src, c.pc,
 		report.WithGoID(meta.id),
 		report.WithModulePath(meta.modulePath),
 		report.WithAliases(aliases),
-		report.WithReviewStatus(status),
+		report.WithReviewStatus(meta.reviewStatus),
 	)
 
 	// Find any additional aliases referenced by the source aliases.
@@ -164,7 +182,7 @@
 			CVEs:     r.CVEs,
 			GHSAs:    r.GHSAs,
 		}
-	case meta.unreviewed:
+	case meta.reviewStatus == report.Unreviewed:
 		r.Description = ""
 		addNotes := true
 		if fixed := c.fix(ctx, r, addNotes); fixed {
@@ -197,6 +215,7 @@
 	labelSuggestedEdit     = "Suggested Edit"
 	labelTriaged           = "triaged"
 	labelHighPriority      = "high priority"
+	labelFirstParty        = "first party"
 	labelPossibleDuplicate = "possible duplicate"
 	labelPossiblyNotGo     = "possibly not Go"
 )
@@ -235,11 +254,11 @@
 // Data that can be combined with a source vulnerability
 // to create a new report.
 type reportMeta struct {
-	id         string
-	modulePath string
-	aliases    []string
-	excluded   report.ExcludedReason
-	unreviewed bool // create basic report with no TODOs and no description
+	id           string
+	modulePath   string
+	aliases      []string
+	excluded     report.ExcludedReason
+	reviewStatus report.ReviewStatus
 }
 
 const todo = "TODO: "
diff --git a/cmd/vulnreport/unexclude.go b/cmd/vulnreport/unexclude.go
index 118323c..d0cc6be 100644
--- a/cmd/vulnreport/unexclude.go
+++ b/cmd/vulnreport/unexclude.go
@@ -64,10 +64,10 @@
 	}
 
 	if err := u.reportFromMeta(ctx, &reportMeta{
-		id:         oldR.ID,
-		modulePath: modulePath,
-		aliases:    oldR.Aliases(),
-		unreviewed: true,
+		id:           oldR.ID,
+		modulePath:   modulePath,
+		aliases:      oldR.Aliases(),
+		reviewStatus: report.Unreviewed,
 	}); err != nil {
 		return err
 	}
diff --git a/doc/quickstart.md b/doc/quickstart.md
index 34af6bf..61cd97f 100644
--- a/doc/quickstart.md
+++ b/doc/quickstart.md
@@ -49,18 +49,13 @@
 
 ### Add standard reports
 
-1. Issues marked `high priority` need a REVIEWED report, and issues without a priority label need an UNREVIEWED report.
-   * To create a reviewed report for issue #NNN, run:
+1. Issues marked `high priority` need a REVIEWED report, and issues without a priority label need an UNREVIEWED report. To create a report for issue #NNN, run:
 
-      ```bash
-      $ vulnreport create NNN
-      ```
+   ```bash
+   $ vulnreport create NNN
+   ```
 
-   * To create an unreviewed report for issue #NNN, run:
-
-      ```bash
-      $ vulnreport -unreviewed create NNN
-      ```
+   The command knows whether to create a reviewed or unreviewed report based on the issue's label. (To override this decision, use flag `-status=UNREVIEWED` or `-status=REVIEWED`.)
 
 2. Edit the report if needed. For reviewed reports, this follows the standard process. For unreviewed reports, only edit the report if it has lint/fix errors (which will be populated in the notes section).
 3. Fix the report and add derived files:
diff --git a/internal/report/report.go b/internal/report/report.go
index 0dfbcb9..3d16c22 100644
--- a/internal/report/report.go
+++ b/internal/report/report.go
@@ -259,6 +259,13 @@
 	return osv.ReviewStatus(r).String()
 }
 
+func ToReviewStatus(s string) (ReviewStatus, bool) {
+	if rs, ok := osv.ToReviewStatus(s); ok {
+		return ReviewStatus(rs), true
+	}
+	return 0, false
+}
+
 func (r ReviewStatus) MarshalYAML() (any, error) {
 	or := osv.ReviewStatus(r)
 	if !or.IsValid() {
@@ -270,8 +277,8 @@
 func (r *ReviewStatus) UnmarshalYAML(node *yaml.Node) error {
 	if node.Kind == yaml.ScalarNode {
 		v := node.Value
-		if rs, ok := osv.ToReviewStatus(v); ok {
-			*r = ReviewStatus(rs)
+		if rs, ok := ToReviewStatus(v); ok {
+			*r = rs
 			return nil
 		}
 		return fmt.Errorf("UnmarshalYAML: unrecognized review status: %s", v)