internal/worker: add triageResult

TriageCVE now returns a triageResult instead of just the module path.

Change-Id: Ie7359ab97d4505e05ebf8b26fcbad80b09ff96f9
Reviewed-on: https://go-review.googlesource.com/c/vuln/+/369874
Reviewed-by: Jonathan Amsterdam <jba@google.com>
Trust: Julie Qiu <julie@golang.org>
Run-TryBot: Julie Qiu <julie@golang.org>
TryBot-Result: Gopher Robot <gobot@golang.org>
diff --git a/internal/worker/triage.go b/internal/worker/triage.go
index 1d9a0d0..e4f67bf 100644
--- a/internal/worker/triage.go
+++ b/internal/worker/triage.go
@@ -31,50 +31,59 @@
 }
 
 // TriageCVE reports whether the CVE refers to a Go module.
-func TriageCVE(ctx context.Context, c *cveschema.CVE, pkgsiteURL string) (module string, err error) {
+func TriageCVE(ctx context.Context, c *cveschema.CVE, pkgsiteURL string) (_ *triageResult, err error) {
 	defer derrors.Wrap(&err, "triageCVE(%q)", c.ID)
 	switch c.DataVersion {
 	case "4.0":
-		mp, err := cveModulePath(ctx, c, pkgsiteURL)
-		if err != nil {
-			return "", err
-		}
-		return mp, nil
+		return triageV4CVE(ctx, c, pkgsiteURL)
 	default:
 		// TODO(https://golang.org/issue/49289): Add support for v5.0.
-		return "", fmt.Errorf("CVE %q has DataVersion %q: %w", c.ID, c.DataVersion, errCVEVersionUnsupported)
+		return nil, fmt.Errorf("CVE %q has DataVersion %q: %w", c.ID, c.DataVersion, errCVEVersionUnsupported)
 	}
 }
 
-// cveModulePath returns a Go module path for a CVE, if we can determine what
-// it is.
-func cveModulePath(ctx context.Context, c *cveschema.CVE, pkgsiteURL string) (_ string, err error) {
-	defer derrors.Wrap(&err, "cveModulePath(%q)", c.ID)
+type triageResult struct {
+	modulePath string
+	stdlib     bool
+	reason     string
+}
+
+// triageV4CVE triages a CVE following schema v4.0 and returns the result.
+func triageV4CVE(ctx context.Context, c *cveschema.CVE, pkgsiteURL string) (_ *triageResult, err error) {
+	defer derrors.Wrap(&err, "triageV4CVE(ctx, %q, %q)", c.ID, pkgsiteURL)
 	for _, r := range c.References.Data {
 		if r.URL == "" {
 			continue
 		}
 		for k := range stdlibKeywords {
 			if strings.Contains(r.URL, k) && !strings.Contains(r.URL, "golang.org/x/") {
-				return "Go Standard Library", nil
+				return &triageResult{
+					modulePath: "Go Standard Library",
+					stdlib:     true,
+					reason:     fmt.Sprintf("Reference data URL %q contains %q", r.URL, k),
+				}, nil
 			}
 		}
 		refURL, err := url.Parse(r.URL)
 		if err != nil {
-			return "", fmt.Errorf("url.Parse(%q): %v", r.URL, err)
+			return nil, fmt.Errorf("url.Parse(%q): %v", r.URL, err)
 		}
 		modpaths := candidateModulePaths(refURL.Host + refURL.Path)
 		for _, mp := range modpaths {
 			known, err := knownToPkgsite(ctx, pkgsiteURL, mp)
 			if err != nil {
-				return "", err
+				return nil, err
 			}
 			if known {
-				return mp, nil
+				u := pkgsiteURL + "/" + mp
+				return &triageResult{
+					modulePath: mp,
+					reason:     fmt.Sprintf("Reference data URL %q contains path %q; %q returned a status 200", r.URL, mp, u),
+				}, nil
 			}
 		}
 	}
-	return "", nil
+	return nil, nil
 }
 
 // Limit pkgsite calls to 2 qps (once every 500ms).
diff --git a/internal/worker/triage_test.go b/internal/worker/triage_test.go
index 8d5a367..43a8cb5 100644
--- a/internal/worker/triage_test.go
+++ b/internal/worker/triage_test.go
@@ -18,7 +18,7 @@
 
 var usePkgsite = flag.Bool("pkgsite", false, "use pkg.go.dev for tests")
 
-func TestCVEModulePath(t *testing.T) {
+func TestTriageV4CVE(t *testing.T) {
 	ctx := log.WithLineLogger(context.Background())
 	url := pkgsiteURL(t)
 
@@ -84,12 +84,19 @@
 		},
 	} {
 		t.Run(test.name, func(t *testing.T) {
-			got, err := cveModulePath(ctx, test.in, url)
+			test.in.DataVersion = "4.0"
+			got, err := TriageCVE(ctx, test.in, url)
 			if err != nil {
 				t.Fatal(err)
 			}
-			if got != test.want {
-				t.Errorf("got %q, want %q", got, test.want)
+			if got == nil {
+				if test.want != "" {
+					t.Fatalf("got empty string, want %q", test.want)
+				}
+				return
+			}
+			if got.modulePath != test.want {
+				t.Errorf("got %q, want %q", got.modulePath, test.want)
 			}
 		})
 	}
diff --git a/internal/worker/update.go b/internal/worker/update.go
index c844978..2c9739f 100644
--- a/internal/worker/update.go
+++ b/internal/worker/update.go
@@ -29,7 +29,7 @@
 // A triageFunc triages a CVE: it decides whether an issue needs to be filed.
 // If so, it returns a non-empty string indicating the possibly
 // affected module.
-type triageFunc func(*cveschema.CVE) (string, error)
+type triageFunc func(*cveschema.CVE) (*triageResult, error)
 
 // An updater performs an update operation on the DB.
 type updater struct {
@@ -256,9 +256,9 @@
 	if err := json.NewDecoder(r).Decode(cve); err != nil {
 		return false, err
 	}
-	module := ""
+	var result *triageResult
 	if cve.State == cveschema.StatePublic && !u.knownIDs[cve.ID] {
-		module, err = u.affectedModule(cve)
+		result, err = u.affectedModule(cve)
 		if err != nil {
 			return false, err
 		}
@@ -268,9 +268,9 @@
 	if old == nil {
 		cr := store.NewCVERecord(cve, pathname, f.blobHash.String())
 		cr.CommitHash = u.commitHash.String()
-		if module != "" {
+		if result != nil {
 			cr.TriageState = store.TriageStateNeedsIssue
-			cr.Module = module
+			cr.Module = result.modulePath
 			cr.CVE = cve
 		} else {
 			cr.TriageState = store.TriageStateNoActionNeeded
@@ -291,15 +291,15 @@
 	mod.CommitHash = u.commitHash.String()
 	switch old.TriageState {
 	case store.TriageStateNoActionNeeded:
-		if module != "" {
+		if result != nil {
 			// Didn't need an issue before, does now.
 			mod.TriageState = store.TriageStateNeedsIssue
-			mod.Module = module
+			mod.Module = result.modulePath
 		}
 		// Else don't change the triage state, but we still want
 		// to update the other changed fields.
 	case store.TriageStateNeedsIssue:
-		if module == "" {
+		if result == nil {
 			// Needed an issue, no longer does.
 			mod.TriageState = store.TriageStateNoActionNeeded
 			mod.Module = ""
@@ -310,7 +310,11 @@
 	case store.TriageStateIssueCreated, store.TriageStateUpdatedSinceIssueCreation:
 		// An issue was filed, so a person should revisit this CVE.
 		mod.TriageState = store.TriageStateUpdatedSinceIssueCreation
-		mod.TriageStateReason = fmt.Sprintf("CVE changed; affected module = %q", module)
+		var mp string
+		if result != nil {
+			mp = result.modulePath
+		}
+		mod.TriageStateReason = fmt.Sprintf("CVE changed; affected module = %q", mp)
 		// TODO(golang/go#49733): keep a history of the previous states and their commits.
 	default:
 		return false, fmt.Errorf("unknown TriageState: %q", old.TriageState)
diff --git a/internal/worker/update_test.go b/internal/worker/update_test.go
index 5d50f31..b394241 100644
--- a/internal/worker/update_test.go
+++ b/internal/worker/update_test.go
@@ -58,7 +58,7 @@
 	}
 
 	purl := pkgsiteURL(t)
-	needsIssue := func(cve *cveschema.CVE) (string, error) {
+	needsIssue := func(cve *cveschema.CVE) (*triageResult, error) {
 		return TriageCVE(ctx, cve, purl)
 	}
 
diff --git a/internal/worker/worker.go b/internal/worker/worker.go
index 2535e32..c6e0899 100644
--- a/internal/worker/worker.go
+++ b/internal/worker/worker.go
@@ -46,7 +46,7 @@
 	if err != nil {
 		return err
 	}
-	u := newUpdater(repo, ch, st, knownVulnIDs, func(cve *cveschema.CVE) (string, error) {
+	u := newUpdater(repo, ch, st, knownVulnIDs, func(cve *cveschema.CVE) (*triageResult, error) {
 		return TriageCVE(ctx, cve, pkgsiteURL)
 	})
 	_, err = u.update(ctx)