internal/derrors, internal/worker: add local import and vendor error classification

Adds logic to classify errors that occur when a go.mod file replaces a
dependency with a local directory, and errors that occur when
go mod vendor was ran and the package has the resulting vendor/ directory.

Change-Id: I34dfa5369aad8629f275aa093d489cfdbef9b992
Reviewed-on: https://go-review.googlesource.com/c/pkgsite-metrics/+/480517
Run-TryBot: Maceo Thompson <maceothompson@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Zvonimir Pavlinovic <zpavlinovic@google.com>
diff --git a/internal/derrors/derrors.go b/internal/derrors/derrors.go
index eea4d74..aa718dd 100644
--- a/internal/derrors/derrors.go
+++ b/internal/derrors/derrors.go
@@ -84,6 +84,13 @@
 	// with govulncheck and is likely happening due to outdated go.sum file.
 	LoadPackagesMissingGoSumEntryError = errors.New("scan module load packages error: missing go.sum entry")
 
+	// VendorError occurs when loading a package fails because of a vendor directory.
+	VendorError = errors.New("scan module load packages error: -mod=vendor mode")
+
+	// LoadPackagesImportedLocalError occurs when packages use a replace directive
+	// with a local directory in their go.mod file. This is not an error with govulncheck.
+	LoadPackagesImportedLocalError = errors.New("scan module load packages error: package replaces an import with a local file/directory")
+
 	// ScanModuleGovulncheckDBConnectionError is used to capture a specific
 	// govulncheck scan error where a connection to vuln db failed.
 	ScanModuleGovulncheckDBConnectionError = errors.New("scan module govulncheck error: communication with vuln db failed")
@@ -192,6 +199,10 @@
 		return "LOAD - NO REQUIRED MODULE"
 	case errors.Is(err, LoadPackagesMissingGoSumEntryError):
 		return "LOAD - NO GO.SUM ENTRY"
+	case errors.Is(err, LoadPackagesImportedLocalError):
+		return "LOAD - GO.MOD REPLACES WITH A LOCAL PATH"
+	case errors.Is(err, VendorError):
+		return "VENDOR"
 	case errors.Is(err, ScanModuleOSError):
 		return "OS"
 	case errors.Is(err, ScanModulePanicError):
diff --git a/internal/worker/govulncheck_scan.go b/internal/worker/govulncheck_scan.go
index 1181b9b..0b37fa0 100644
--- a/internal/worker/govulncheck_scan.go
+++ b/internal/worker/govulncheck_scan.go
@@ -12,6 +12,7 @@
 	"os"
 	"os/exec"
 	"path/filepath"
+	"regexp"
 	"strings"
 	"syscall"
 	"time"
@@ -212,6 +213,8 @@
 			err = fmt.Errorf("%v: %w", err, derrors.ScanModuleTooManyOpenFiles)
 		case isMissingGoSumEntry(err):
 			err = fmt.Errorf("%v: %w", err, derrors.LoadPackagesMissingGoSumEntryError)
+		case isReplacingWithLocalPath(err):
+			err = fmt.Errorf("%v: %w", err, derrors.LoadPackagesImportedLocalError)
 		case errors.Is(err, derrors.LoadPackagesError):
 			// general load packages error
 		case isVulnDBConnection(err):
@@ -463,6 +466,16 @@
 	return strings.Contains(err.Error(), "no go.mod file")
 }
 
+func isModVendor(err error) bool {
+	return strings.Contains(err.Error(), "-mod=vendor")
+}
+
+func isReplacingWithLocalPath(err error) bool {
+	errStr := err.Error()
+	matched, err := regexp.MatchString(`replaced by .{0,2}/`, errStr)
+	return err == nil && matched && strings.Contains(errStr, "go.mod: no such file")
+}
+
 func isVulnDBConnection(err error) bool {
 	s := err.Error()
 	return strings.Contains(s, "https://vuln.go.dev") &&