cmd/vulnreport: generate data/excluded report for withdrawn issues

- Previously vulnreport create would throw an error that an issue was
  withdrawn.
- Now when vulnreport create encounters a withdrawn issue, it will label
  the issue as "excluded: WITHDRAWN" and create a data/excluded/*.yaml
  report.

Change-Id: Iad5c45d6a1b854e9c55009a21d9df9e64b1e29b3
Reviewed-on: https://go-review.googlesource.com/c/vulndb/+/695155
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Neal Patel <nealpatel@google.com>
Auto-Submit: Ethan Lee <ethanalee@google.com>
diff --git a/cmd/vulnreport/create.go b/cmd/vulnreport/create.go
index 693d105..af37782 100644
--- a/cmd/vulnreport/create.go
+++ b/cmd/vulnreport/create.go
@@ -43,5 +43,5 @@
 
 func (c *create) run(ctx context.Context, input any) error {
 	iss := input.(*issues.Issue)
-	return c.newReportFromIssue(ctx, iss)
+	return c.newReportFromIssue(ctx, iss, c.ic)
 }
diff --git a/cmd/vulnreport/create_excluded.go b/cmd/vulnreport/create_excluded.go
index 17f853f..70d5839 100644
--- a/cmd/vulnreport/create_excluded.go
+++ b/cmd/vulnreport/create_excluded.go
@@ -46,7 +46,7 @@
 
 func (c *createExcluded) run(ctx context.Context, input any) (err error) {
 	iss := input.(*issues.Issue)
-	return c.newReportFromIssue(ctx, iss)
+	return c.newReportFromIssue(ctx, iss, c.ic)
 }
 
 func (c *createExcluded) skip(input any) string {
diff --git a/cmd/vulnreport/creator.go b/cmd/vulnreport/creator.go
index 0018a72..f92c55b 100644
--- a/cmd/vulnreport/creator.go
+++ b/cmd/vulnreport/creator.go
@@ -95,7 +95,7 @@
 	return ""
 }
 
-func (c *creator) newReportFromIssue(ctx context.Context, iss *issues.Issue) error {
+func (c *creator) newReportFromIssue(ctx context.Context, iss *issues.Issue, ic issueClient) error {
 	id := iss.NewGoID()
 	r, err := c.reportFromMeta(ctx, &reportMeta{
 		id:           id,
@@ -108,8 +108,15 @@
 	if err != nil {
 		return err
 	}
-	if r.Withdrawn != nil {
-		return fmt.Errorf("new regular report should not be created for withdrawn vulnerability; %s", withdrawnGuidance(id, iss.Number))
+	if r.Excluded == report.ExcludedWithdrawn {
+		log.Infof("issue #%d: vulnerability withdrawn; labeling as %q", iss.Number, labelWithdrawn)
+		labels := append(iss.Labels, labelWithdrawn)
+		slices.Sort(labels)
+		labels = slices.Compact(labels)
+
+		if err := ic.SetLabels(ctx, iss.Number, labels); err != nil {
+			log.Infof("failed to label issue #%d: %v", iss.Number, err)
+		}
 	}
 	return c.write(ctx, r)
 }
@@ -182,6 +189,10 @@
 	meta.aliases = c.allAliases(ctx, meta.aliases)
 	raw := c.rawReport(ctx, meta)
 
+	if raw.Withdrawn != nil {
+		meta.excluded = report.ExcludedWithdrawn
+	}
+
 	if meta.excluded != "" {
 		raw = &report.Report{
 			ID: meta.id,
@@ -291,6 +302,7 @@
 	labelFirstParty    = "first party"
 	labelPossiblyNotGo = "possibly not Go"
 	labelOutOfScope    = "excluded: OUT_OF_SCOPE"
+	labelWithdrawn     = "excluded: WITHDRAWN"
 )
 
 func excludedReason(iss *issues.Issue) report.ExcludedType {