internal/worker: (WIP) add logic to convert from govulncheck.Vuln to bigquerry vuln

Adds the convertGovulncheckVuln func to vulncheck_scan.go (see b/264852628)

Change-Id: Icac4f40fb2394de987f61d6cc053c9e26ee9068e
Reviewed-on: https://go-review.googlesource.com/c/pkgsite-metrics/+/466656
Run-TryBot: Maceo Thompson <maceothompson@google.com>
Reviewed-by: Zvonimir Pavlinovic <zpavlinovic@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
diff --git a/internal/worker/vulncheck_scan.go b/internal/worker/vulncheck_scan.go
index db94733..4d288e6 100644
--- a/internal/worker/vulncheck_scan.go
+++ b/internal/worker/vulncheck_scan.go
@@ -34,6 +34,7 @@
 	"golang.org/x/pkgsite-metrics/internal/scan"
 	"golang.org/x/pkgsite-metrics/internal/version"
 	vulnc "golang.org/x/vuln/client"
+	"golang.org/x/vuln/exp/govulncheck"
 	"golang.org/x/vuln/vulncheck"
 )
 
@@ -612,6 +613,50 @@
 	}
 }
 
+func convertGoVulncheckOutput(v *govulncheck.Vuln) (vulns []*bigquery.Vuln) {
+	for _, module := range v.Modules {
+		for pkgNum, pkg := range module.Packages {
+			addedSymbols := make(map[string]bool)
+			baseVuln := &bigquery.Vuln{
+				ID:          v.OSV.ID,
+				ModulePath:  module.Path,
+				PackagePath: pkg.Path,
+				CallSink:    bigquery.NullInt(0),
+				ImportSink:  bigquery.NullInt(pkgNum + 1),
+				RequireSink: bigquery.NullInt(pkgNum + 1),
+			}
+
+			// For each called symbol, reconstruct sinks and create the corresponding bigquery vuln
+			for symbolNum, cs := range pkg.CallStacks {
+				addedSymbols[cs.Symbol] = true
+				toAdd := *baseVuln
+				toAdd.Symbol = cs.Symbol
+				toAdd.CallSink = bigquery.NullInt(symbolNum + 1)
+				vulns = append(vulns, &toAdd)
+			}
+
+			// Find the rest of the vulnerable imported symbols that haven't been called
+			// and create corresponding bigquery vulns
+			for _, affected := range v.OSV.Affected {
+				if affected.Package.Name == module.Path {
+					for _, imp := range affected.EcosystemSpecific.Imports {
+						if imp.Path == pkg.Path {
+							for _, symbol := range imp.Symbols {
+								if !addedSymbols[symbol] {
+									toAdd := *baseVuln
+									toAdd.Symbol = symbol
+									vulns = append(vulns, &toAdd)
+								}
+							}
+						}
+					}
+				}
+			}
+		}
+	}
+	return vulns
+}
+
 // currHeapUsage computes currently allocate heap bytes.
 func currHeapUsage() uint64 {
 	var stats runtime.MemStats
diff --git a/internal/worker/vulncheck_scan_test.go b/internal/worker/vulncheck_scan_test.go
index ccd488d..794fcc7 100644
--- a/internal/worker/vulncheck_scan_test.go
+++ b/internal/worker/vulncheck_scan_test.go
@@ -14,10 +14,14 @@
 
 	"cloud.google.com/go/storage"
 	"github.com/google/go-cmp/cmp"
+	"github.com/google/go-cmp/cmp/cmpopts"
+	"golang.org/x/pkgsite-metrics/internal/bigquery"
 	"golang.org/x/pkgsite-metrics/internal/config"
 	"golang.org/x/pkgsite-metrics/internal/derrors"
 	"golang.org/x/pkgsite-metrics/internal/proxy"
 	vulnc "golang.org/x/vuln/client"
+	"golang.org/x/vuln/exp/govulncheck"
+	"golang.org/x/vuln/osv"
 	"golang.org/x/vuln/vulncheck"
 )
 
@@ -144,3 +148,126 @@
 		t.Errorf("got %+v, want %+v", got, want)
 	}
 }
+
+func TestConvertGoVulncheckOutput(t *testing.T) {
+	var (
+		osvEntry = &osv.Entry{
+			ID: "GO-YYYY-1234",
+			Affected: []osv.Affected{
+				{
+					Package: osv.Package{
+						Name:      "example.com/repo/module",
+						Ecosystem: "Go",
+					},
+					EcosystemSpecific: osv.EcosystemSpecific{
+						Imports: []osv.EcosystemSpecificImport{
+							{
+								Path: "example.com/repo/module/package",
+								Symbols: []string{
+									"Symbol",
+									"Another",
+								},
+							},
+						},
+					},
+				},
+			},
+		}
+
+		vuln1 = &govulncheck.Vuln{
+			OSV: osvEntry,
+			Modules: []*govulncheck.Module{
+				{
+					Path: "example.com/repo/module",
+					Packages: []*govulncheck.Package{
+						{
+							Path: "example.com/repo/module/package",
+							CallStacks: []govulncheck.CallStack{
+								{
+									Symbol:  "Symbol",
+									Summary: "example.go:1:1 xyz.func calls pkgPath.Symbol",
+									Frames:  []*govulncheck.StackFrame{},
+								},
+							},
+						},
+					},
+				},
+			},
+		}
+
+		vuln2 = &govulncheck.Vuln{
+			OSV: osvEntry,
+			Modules: []*govulncheck.Module{
+				{
+					Path: "example.com/repo/module",
+					Packages: []*govulncheck.Package{
+						{
+							Path: "example.com/repo/module/package",
+						},
+					},
+				},
+			},
+		}
+	)
+	tests := []struct {
+		name      string
+		vuln      *govulncheck.Vuln
+		wantVulns []*bigquery.Vuln
+	}{
+		{
+			name: "Call One Symbol",
+			vuln: vuln1,
+			wantVulns: []*bigquery.Vuln{
+				{
+					ID:          "GO-YYYY-1234",
+					Symbol:      "Symbol",
+					PackagePath: "example.com/repo/module/package",
+					ModulePath:  "example.com/repo/module",
+					CallSink:    bigquery.NullInt(1),
+					ImportSink:  bigquery.NullInt(1),
+					RequireSink: bigquery.NullInt(1),
+				},
+				{
+					ID:          "GO-YYYY-1234",
+					Symbol:      "Another",
+					PackagePath: "example.com/repo/module/package",
+					ModulePath:  "example.com/repo/module",
+					CallSink:    bigquery.NullInt(0),
+					ImportSink:  bigquery.NullInt(1),
+					RequireSink: bigquery.NullInt(1),
+				},
+			},
+		},
+		{
+			name: "Call no symbols",
+			vuln: vuln2,
+			wantVulns: []*bigquery.Vuln{
+				{
+					ID:          "GO-YYYY-1234",
+					PackagePath: "example.com/repo/module/package",
+					ModulePath:  "example.com/repo/module",
+					Symbol:      "Symbol",
+					CallSink:    bigquery.NullInt(0),
+					ImportSink:  bigquery.NullInt(1),
+					RequireSink: bigquery.NullInt(1),
+				},
+				{
+					ID:          "GO-YYYY-1234",
+					PackagePath: "example.com/repo/module/package",
+					ModulePath:  "example.com/repo/module",
+					Symbol:      "Another",
+					CallSink:    bigquery.NullInt(0),
+					ImportSink:  bigquery.NullInt(1),
+					RequireSink: bigquery.NullInt(1),
+				},
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if diff := cmp.Diff(convertGoVulncheckOutput(tt.vuln), tt.wantVulns, cmpopts.EquateEmpty()); diff != "" {
+				t.Errorf("mismatch (-got, +want): %s", diff)
+			}
+		})
+	}
+}