Repository: google/osv-scanner
Pull request #149 (2023-01-17 04:15:30)

golangci checks in github action, and fixes initial linter issues
- Uses a modified .golangci config file from osv-detector. Some harder lints disabled for now.
- Fixes some of the linting errors.
- Add golangci to be an optional prereq in CONTRIBUTING.md
- Adds golangci github action


another-rex: I don't feel too strongly about it either way. I added it back in with a block-size of 2 so it doesn't enforce cases where we have 
```
if err != nil {
    print_error(err)
    return err
}
```

Patch:
diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml
index 805d269..9375d6e 100644
--- a/.github/workflows/lint.yaml
+++ b/.github/workflows/lint.yaml
@@ -25,7 +25,7 @@ permissions: read-all
 
 jobs:
   lint:
-    name: Lint and format
+    name: golangci-lint
     runs-on: ubuntu-latest
     steps:
       - name: Check out code
@@ -35,5 +35,9 @@ jobs:
         with:
           go-version: '1.19'
           check-latest: true
-      - name: Run go vet
-        run: ./run_lints.sh
+      - name: Run golangci-lint
+        uses: golangci/golangci-lint-action@v3.3.1
+        with:
+          # Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version
+          version: v1.50.1
+
diff --git a/.golangci.yaml b/.golangci.yaml
new file mode 100644
index 0000000..adafa34
--- /dev/null
+++ b/.golangci.yaml
@@ -0,0 +1,64 @@
+output:
+  sort-results: true
+linters:
+  enable:
+    - gofmt
+  # prettier-ignore
+  disable:
+    - tagliatelle      # we're parsing data from external sources     (g-rath)
+    - varnamelen       # maybe later  (g-rath)
+    - exhaustivestruct # deprecated
+    - exhaustruct      # overkill (g-rath)
+    - forcetypeassert  # too hard (g-rath)
+    - interfacer       # deprecated
+    - golint           # deprecated
+    - scopelint        # deprecated
+    - maligned         # deprecated
+    - varcheck         # deprecated
+    - structcheck      # deprecated
+    - deadcode         # deprecated
+    - nosnakecase      # deprecated
+    - lll              # line length is hard  (g-rath)
+    - godox            # to-do comments are fine  (g-rath)
+    - godot            # comments are fine without full stops (g-rath)
+    - gomnd            # not every number is magic  (g-rath)
+    - wsl              # disagree with, for now (g-rath)
+    - ireturn          # disagree with, sort of (g-rath)
+    - gochecknoglobals # disagree with, for non changing variables (another-rex)
+    - wrapcheck        # too difficult, will re-add later (another-rex)
+    - testpackage      # will re-add later (another-rex)
+    - goerr113         # will re-add later (another-rex)
+    - nonamedreturns   # disagree with, for now (another-rex)
+  presets:
+    - bugs
+    - comment
+    - error
+    - performance
+    - sql
+    - style
+    - test
+    - unused
+
+linters-settings:
+  gocritic:
+    disabled-checks:
+      - ifElseChain
+  revive:
+    rules:
+      - name: increment-decrement
+        disabled: true
+  nlreturn:
+    # Size of the block (including return statement that is still "OK")
+    # so no return split required.
+    # Default: 1
+    block-size: 2
+
+issues:
+  exclude-rules:
+    - path: _test\.go
+      linters:
+        - goerr113
+        - dupl
+    - path: main.go
+      linters:
+        - gochecknoglobals
\ No newline at end of file
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 5261334..c7a3e0b 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -33,6 +33,7 @@ This project follows
 Install:
 1. [Go](https://go.dev/) 1.18+, use `go version` to check.
 2. [GoReleaser](https://goreleaser.com/) (Optional, only if you want reproducible builds).
+3. [golangci-lint](https://golangci-lint.run/) (Optional, only if you want to run the linters locally)
 
 ### Building
 
diff --git a/cmd/osv-scanner/main.go b/cmd/osv-scanner/main.go
index 5dfbc26..82c7c94 100644
--- a/cmd/osv-scanner/main.go
+++ b/cmd/osv-scanner/main.go
@@ -88,8 +88,9 @@ func run(args []string, stdout, stderr io.Writer) int {
 			}, r)
 
 			if errPrint := r.PrintResult(&vulnResult); errPrint != nil {
-				return fmt.Errorf("failed to write output: %v", errPrint)
+				return fmt.Errorf("failed to write output: %w", errPrint)
 			}
+			//nolint:wrapcheck
 			return err
 		},
 	}
@@ -103,11 +104,12 @@ func run(args []string, stdout, stderr io.Writer) int {
 		}
 
 		if errors.Is(err, osvscanner.NoPackagesFoundErr) {
-			r.PrintError(fmt.Sprintf("No package sources found, --help for usage information.\n"))
+			r.PrintError("No package sources found, --help for usage information.\n")
 			return 128
 		}
 
 		r.PrintError(fmt.Sprintf("%v\n", err))
+
 		return 127
 	}
 
diff --git a/go.mod b/go.mod
index 086b3ad..e247354 100644
--- a/go.mod
+++ b/go.mod
@@ -5,7 +5,6 @@ go 1.19
 require (
 	github.com/BurntSushi/toml v1.2.1
 	github.com/CycloneDX/cyclonedx-go v0.7.0
-	github.com/g-rath/osv-detector v0.9.0
 	github.com/google/go-cmp v0.5.9
 	github.com/jedib0t/go-pretty/v6 v6.4.4
 	github.com/package-url/packageurl-go v0.1.0
diff --git a/go.sum b/go.sum
index 41ad12d..830465e 100644
--- a/go.sum
+++ b/go.sum
@@ -8,8 +8,6 @@ github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/g-rath/osv-detector v0.9.0 h1:8xNRdRf8Av2AgzeWqgdkj3/UIn9yfTTfjNKRevIo1Tw=
-github.com/g-rath/osv-detector v0.9.0/go.mod h1:P0CI4ohMPoDDXfI4ir8NqrPRiHtwOiyi7SGZR0VGj4U=
 github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
 github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/jedib0t/go-pretty/v6 v6.4.4 h1:N+gz6UngBPF4M288kiMURPHELDMIhF/Em35aYuKrsSc=
diff --git a/internal/osv/osv.go b/internal/osv/osv.go
index 5affae2..a29cc83 100644
--- a/internal/osv/osv.go
+++ b/internal/osv/osv.go
@@ -4,7 +4,7 @@ import (
 	"bytes"
 	"encoding/json"
 	"fmt"
-	"io/ioutil"
+	"io"
 	"net/http"
 	"time"
 
@@ -99,11 +99,12 @@ func MakePkgRequest(pkgDetails lockfile.PackageDetails) *Query {
 
 // From: https://stackoverflow.com/a/72408490
 func chunkBy[T any](items []T, chunkSize int) [][]T {
-	_chunks := make([][]T, 0, (len(items)/chunkSize)+1)
+	chunks := make([][]T, 0, (len(items)/chunkSize)+1)
 	for chunkSize < len(items) {
-		items, _chunks = items[chunkSize:], append(_chunks, items[0:chunkSize:chunkSize])
+		items, chunks = items[chunkSize:], append(chunks, items[0:chunkSize:chunkSize])
 	}
-	return append(_chunks, items)
+
+	return append(chunks, items)
 }
 
 // checkResponseError checks if the response has an error.
@@ -112,7 +113,7 @@ func checkResponseError(resp *http.Response) error {
 		return nil
 	}
 
-	respBuf, err := ioutil.ReadAll(resp.Body)
+	respBuf, err := io.ReadAll(resp.Body)
 	if err != nil {
 		return fmt.Errorf("failed to read error response from server: %w", err)
 	}
@@ -120,24 +121,10 @@ func checkResponseError(resp *http.Response) error {
 	return fmt.Errorf("server response error: %s", string(respBuf))
 }
 
-func makeRetryRequest(action func() (*http.Response, error)) (*http.Response, error) {
-	var resp *http.Response
-	var err error
-	retries := 3
-	for i := 0; i < retries; i++ {
-		resp, err = action()
-		if err == nil {
-			break
-		}
-		time.Sleep(time.Second)
-	}
-	return resp, err
-}
 func MakeRequest(request BatchedQuery) (*BatchedResponse, error) {
 	// API has a limit of 1000 bulk query per request
 	queryChunks := chunkBy(request.Queries, MaxQueriesPerRequest)
 	var totalOsvResp BatchedResponse
-
 	for _, queries := range queryChunks {
 		requestBytes, err := json.Marshal(BatchedQuery{Queries: queries})
 		if err != nil {
@@ -146,6 +133,8 @@ func MakeRequest(request BatchedQuery) (*BatchedResponse, error) {
 		requestBuf := bytes.NewBuffer(requestBytes)
 
 		resp, err := makeRetryRequest(func() (*http.Response, error) {
+			// We do not need a specific context
+			//nolint:noctx
 			return http.Post(QueryEndpoint, "application/json", requestBuf)
 		})
 		if err != nil {
@@ -170,9 +159,10 @@ func MakeRequest(request BatchedQuery) (*BatchedResponse, error) {
 	return &totalOsvResp, nil
 }
 
-// Get a Vulnerabiltiy for the given ID.
+// Get a Vulnerability for the given ID.
 func Get(id string) (*models.Vulnerability, error) {
 	resp, err := makeRetryRequest(func() (*http.Response, error) {
+		//nolint:noctx
 		return http.Get(GetEndpoint + "/" + id)
 	})
 	if err != nil {
@@ -190,6 +180,7 @@ func Get(id string) (*models.Vulnerability, error) {
 	if err != nil {
 		return nil, err
 	}
+
 	return &vuln, nil
 }
 
@@ -211,5 +202,21 @@ func Hydrate(resp *BatchedResponse) (*HydratedBatchedResponse, error) {
 		}
 		hydrated.Results = append(hydrated.Results, result)
 	}
+
 	return &hydrated, nil
 }
+
+func makeRetryRequest(action func() (*http.Response, error)) (*http.Response, error) {
+	var resp *http.Response
+	var err error
+	retries := 3
+	for i := 0; i < retries; i++ {
+		resp, err = action()
+		if err == nil {
+			break
+		}
+		time.Sleep(time.Second)
+	}
+
+	return resp, err
+}
diff --git a/internal/output/machinejson.go b/internal/output/machinejson.go
index c118fbe..2d7c72c 100644
--- a/internal/output/machinejson.go
+++ b/internal/output/machinejson.go
@@ -11,5 +11,6 @@ import (
 func PrintJSONResults(vulnResult *models.VulnerabilityResults, outputWriter io.Writer) error {
 	encoder := json.NewEncoder(outputWriter)
 	encoder.SetIndent("", "  ")
+
 	return encoder.Encode(vulnResult)
 }
diff --git a/internal/output/reporter.go b/internal/output/reporter.go
index b135f0b..10a9918 100644
--- a/internal/output/reporter.go
+++ b/internal/output/reporter.go
@@ -26,6 +26,7 @@ func NewReporter(stdout io.Writer, stderr io.Writer, outputAsJSON bool) *Reporte
 func NewVoidReporter() *Reporter {
 	stdout := new(strings.Builder)
 	stderr := new(strings.Builder)
+
 	return NewReporter(stdout, stderr, false)
 }
 
diff --git a/internal/output/table.go b/internal/output/table.go
index 3e342f9..b5ac7d5 100644
--- a/internal/output/table.go
+++ b/internal/output/table.go
@@ -46,11 +46,9 @@ func PrintTableResults(vulnResult *models.VulnerabilityResults, outputWriter io.
 				outputRow := table.Row{}
 				shouldMerge := false
 
-				var ids []string
 				var links []string
 
 				for _, vuln := range group.IDs {
-					ids = append(ids, vuln)
 					if isTerminal {
 						links = append(links, osv.BaseVulnerabilityURL+text.Bold.EscapeSeq()+vuln+text.Reset.EscapeSeq())
 					} else {
diff --git a/internal/sbom/cyclonedx.go b/internal/sbom/cyclonedx.go
index 4927463..85f4205 100644
--- a/internal/sbom/cyclonedx.go
+++ b/internal/sbom/cyclonedx.go
@@ -1,6 +1,7 @@
 package sbom
 
 import (
+	"fmt"
 	"io"
 	"strings"
 
@@ -31,19 +32,24 @@ func (c *CycloneDX) enumeratePackages(bom *cyclonedx.BOM, callback func(Identifi
 			}
 		}
 	}
+
 	return nil
 }
 
 func (c *CycloneDX) GetPackages(r io.ReadSeeker, callback func(Identifier) error) error {
 	var bom cyclonedx.BOM
-	
+
 	for _, formatType := range cycloneDXTypes {
-		r.Seek(0, io.SeekStart)
+		_, err := r.Seek(0, io.SeekStart)
+		if err != nil {
+			return fmt.Errorf("failed to seek to start of file: %w", err)
+		}
 		decoder := cyclonedx.NewBOMDecoder(r, formatType)
-		err := decoder.Decode(&bom)
+		err = decoder.Decode(&bom)
 		if err == nil && (bom.BOMFormat == "CycloneDX" || strings.HasPrefix(bom.XMLNS, "http://cyclonedx.org/schema/bom")) {
 			return c.enumeratePackages(&bom, callback)
 		}
 	}
-	return InvalidFormat
+
+	return ErrInvalidFormat
 }
diff --git a/internal/sbom/sbom.go b/internal/sbom/sbom.go
index b0adb75..ff263c0 100644
--- a/internal/sbom/sbom.go
+++ b/internal/sbom/sbom.go
@@ -17,7 +17,7 @@ type SBOMReader interface {
 }
 
 var (
-	InvalidFormat = errors.New("invalid format")
+	ErrInvalidFormat = errors.New("invalid format")
 )
 
 var (
diff --git a/internal/sbom/spdx.go b/internal/sbom/spdx.go
index eaa650c..560a662 100644
--- a/internal/sbom/spdx.go
+++ b/internal/sbom/spdx.go
@@ -1,6 +1,8 @@
+//nolint:nosnakecase
 package sbom
 
 import (
+	"fmt"
 	"io"
 
 	"github.com/spdx/tools-golang/jsonloader"
@@ -37,16 +39,21 @@ func (s *SPDX) enumeratePackages(doc *spdx.Document2_2, callback func(Identifier
 			}
 		}
 	}
+
 	return nil
 }
 
 func (s *SPDX) GetPackages(r io.ReadSeeker, callback func(Identifier) error) error {
 	for _, loader := range spdxLoaders {
-		r.Seek(0, io.SeekStart)
+		_, err := r.Seek(0, io.SeekStart)
+		if err != nil {
+			return fmt.Errorf("failed to seek to start of file: %w", err)
+		}
 		doc, err := loader(r)
 		if err == nil {
 			return s.enumeratePackages(doc, callback)
 		}
 	}
-	return InvalidFormat
+
+	return ErrInvalidFormat
 }
diff --git a/internal/semantic/parse.go b/internal/semantic/parse.go
index 871fc03..78f2f83 100644
--- a/internal/semantic/parse.go
+++ b/internal/semantic/parse.go
@@ -18,6 +18,7 @@ func MustParse(str string, ecosystem Ecosystem) Version {
 }
 
 func Parse(str string, ecosystem Ecosystem) (Version, error) {
+	//nolint:exhaustive // Using strings to specify ecosystem instead of lockfile types
 	switch ecosystem {
 	case "npm":
 		return parseSemverVersion(str), nil
diff --git a/pkg/config/config.go b/pkg/config/config.go
index eda293b..85f9e25 100644
--- a/pkg/config/config.go
+++ b/pkg/config/config.go
@@ -47,7 +47,6 @@ func (c *Config) ShouldIgnore(vulnID string) (bool, IgnoreEntry) {
 	// Should ignore if IgnoreUntil is still after current time
 	// Takes timezone offsets into account if it is specified. otherwise it's using local time
 	return ignoredLine.IgnoreUntil.After(time.Now()), ignoredLine
-
 }
 
 // Sets the override config by reading the config file at configPath.
@@ -60,6 +59,7 @@ func (c *ConfigManager) UseOverride(configPath string) error {
 	}
 	config.LoadPath = configPath
 	c.OverrideConfig = &config
+
 	return nil
 }
 
@@ -82,7 +82,7 @@ func (c *ConfigManager) Get(r *output.Reporter, targetPath string) Config {
 		return config
 	}
 
-	config, configErr := tryLoadConfig(r, configPath)
+	config, configErr := tryLoadConfig(configPath)
 	if configErr == nil {
 		r.PrintText(fmt.Sprintf("Loaded filter from: %s\n", config.LoadPath))
 	} else {
@@ -108,12 +108,13 @@ func normalizeConfigLoadPath(target string) (string, error) {
 		containingFolder = target
 	}
 	configPath := filepath.Join(containingFolder, osvScannerConfigName)
+
 	return configPath, nil
 }
 
 // tryLoadConfig tries to load config in `target` (or it's containing directory)
 // `target` will be the key for the entry in configMap
-func tryLoadConfig(r *output.Reporter, configPath string) (Config, error) {
+func tryLoadConfig(configPath string) (Config, error) {
 	file, err := os.Open(configPath)
 	var config Config
 	if err == nil { // File exists, and we have permission to read
@@ -121,9 +122,10 @@ func tryLoadConfig(r *output.Reporter, configPath string) (Config, error) {
 
 		_, err := toml.NewDecoder(file).Decode(&config)
 		if err != nil {
-			return Config{}, fmt.Errorf("failed to parse config file: %w\n", err)
+			return Config{}, fmt.Errorf("failed to parse config file: %w", err)
 		}
 		config.LoadPath = configPath
+
 		return config, nil
 	}
 
diff --git a/pkg/grouper/grouper.go b/pkg/grouper/grouper.go
index e28f05f..942331f 100644
--- a/pkg/grouper/grouper.go
+++ b/pkg/grouper/grouper.go
@@ -13,6 +13,7 @@ func min(a, b int) int {
 	if a < b {
 		return a
 	}
+
 	return b
 }
 
@@ -58,9 +59,10 @@ func Group(vulns []IDAliases) []models.GroupInfo {
 	sortedKeys := maps.Keys(extractedGroups)
 	sort.Ints(sortedKeys)
 
-	var result []models.GroupInfo
+	result := make([]models.GroupInfo, 0, len(sortedKeys))
 	for _, key := range sortedKeys {
 		result = append(result, models.GroupInfo{IDs: extractedGroups[key]})
 	}
+
 	return result
 }
diff --git a/pkg/grouper/grouper_models.go b/pkg/grouper/grouper_models.go
index d919ad4..bbd9fa6 100644
--- a/pkg/grouper/grouper_models.go
+++ b/pkg/grouper/grouper_models.go
@@ -16,5 +16,6 @@ func ConvertVulnerabilityToIDAliases(c []models.Vulnerability) []IDAliases {
 		}
 		output = append(output, idAliases)
 	}
+
 	return output
 }
diff --git a/pkg/lockfile/apk-installed.go b/pkg/lockfile/apk-installed.go
index 1ee431b..4c6a45e 100644
--- a/pkg/lockfile/apk-installed.go
+++ b/pkg/lockfile/apk-installed.go
@@ -21,6 +21,7 @@ func groupApkPackageLines(scanner *bufio.Scanner) [][]string {
 				groups = append(groups, group)
 			}
 			group = make([]string, 0)
+
 			continue
 		}
 		group = append(group, line)
@@ -52,7 +53,7 @@ func parseApkPackageGroup(group []string, pathToLockfile string) PackageDetails
 	}
 
 	if pkg.Version == "" {
-		var pkgPrintName string = pkg.Name
+		pkgPrintName := pkg.Name
 		if pkgPrintName == "" {
 			pkgPrintName = "<unknown>"
 		}
@@ -90,6 +91,7 @@ func ParseApkInstalled(pathToLockfile string) ([]PackageDetails, error) {
 				"warning: malformed APK installed file. Found no package name in record. File: %s\n",
 				pathToLockfile,
 			)
+
 			continue
 		}
 
diff --git a/pkg/lockfile/parse-conan-lock.go b/pkg/lockfile/parse-conan-lock.go
index c10fcba..cbd4724 100644
--- a/pkg/lockfile/parse-conan-lock.go
+++ b/pkg/lockfile/parse-conan-lock.go
@@ -22,7 +22,7 @@ type ConanGraphNode struct {
 	Pref      string `json:"pref"`
 	Ref       string `json:"ref"`
 	Options   string `json:"options"`
-	PackageId string `json:"package_id"`
+	PackageID string `json:"package_id"`
 	Prev      string `json:"prev"`
 	Path      string `json:"path"`
 	Context   string `json:"context"`
@@ -94,6 +94,7 @@ func parseConanRenference(ref string) ConanReference {
 		reference.Name = ""
 		reference.Version = ref
 	}
+
 	return reference
 }
 
diff --git a/pkg/lockfile/parse-gradle-lock.go b/pkg/lockfile/parse-gradle-lock.go
index de43b3e..94846a9 100644
--- a/pkg/lockfile/parse-gradle-lock.go
+++ b/pkg/lockfile/parse-gradle-lock.go
@@ -43,7 +43,7 @@ func ParseGradleLock(pathToLockfile string) ([]PackageDetails, error) {
 	}
 	defer file.Close()
 
-	pkgs := make([]PackageDetails, 0, 0)
+	pkgs := make([]PackageDetails, 0)
 	scanner := bufio.NewScanner(file)
 
 	for scanner.Scan() {
diff --git a/pkg/lockfile/parse-requirements-txt.go b/pkg/lockfile/parse-requirements-txt.go
index 40c853f..2cbe5d2 100644
--- a/pkg/lockfile/parse-requirements-txt.go
+++ b/pkg/lockfile/parse-requirements-txt.go
@@ -11,7 +11,8 @@ import (
 const PipEcosystem Ecosystem = "PyPI"
 
 // todo: expand this to support more things, e.g.
-//   https://pip.pypa.io/en/stable/reference/requirements-file-format/#example
+//
+//	https://pip.pypa.io/en/stable/reference/requirements-file-format/#example
 func parseLine(line string) PackageDetails {
 	var constraint string
 	name := line
diff --git a/pkg/lockfile/parse.go b/pkg/lockfile/parse.go
index 0fe9da4..8ca8d83 100644
--- a/pkg/lockfile/parse.go
+++ b/pkg/lockfile/parse.go
@@ -16,7 +16,7 @@ func FindParser(pathToLockfile string, parseAs string) (PackageDetailsParser, st
 	return parsers[parseAs], parseAs
 }
 
-//nolint:gochecknoglobals // this is an optimisation and read-only
+// this is an optimisation and read-only
 var parsers = map[string]PackageDetailsParser{
 	"buildscript-gradle.lockfile": ParseGradleLock,
 	"Cargo.lock":                  ParseCargoLock,
diff --git a/pkg/lockfile/types.go b/pkg/lockfile/types.go
index 64d44bb..3c60378 100644
--- a/pkg/lockfile/types.go
+++ b/pkg/lockfile/types.go
@@ -1,11 +1,11 @@
 package lockfile
 
 type PackageDetails struct {
-		Name      string    `json:"name"`
-		Version   string    `json:"version"`
-		Commit    string    `json:"commit,omitempty"`
-		Ecosystem Ecosystem `json:"ecosystem,omitempty"`
-		CompareAs Ecosystem `json:"compareAs,omitempty"`
+	Name      string    `json:"name"`
+	Version   string    `json:"version"`
+	Commit    string    `json:"commit,omitempty"`
+	Ecosystem Ecosystem `json:"ecosystem,omitempty"`
+	CompareAs Ecosystem `json:"compareAs,omitempty"`
 }
 
 type Ecosystem string
diff --git a/pkg/models/results.go b/pkg/models/results.go
index 16354b1..457cf07 100644
--- a/pkg/models/results.go
+++ b/pkg/models/results.go
@@ -21,6 +21,7 @@ func (vulns *VulnerabilityResults) Flatten() []VulnerabilityFlattened {
 			}
 		}
 	}
+
 	return results
 }
 
diff --git a/pkg/osvscanner/osvscanner.go b/pkg/osvscanner/osvscanner.go
index fa13c0d..aeceefe 100644
--- a/pkg/osvscanner/osvscanner.go
+++ b/pkg/osvscanner/osvscanner.go
@@ -29,8 +29,12 @@ type ScannerActions struct {
 	ConfigOverridePath   string
 }
 
-// Error for when no packages is found during a scan.
+// NoPackagesFoundErr for when no packages is found during a scan.
+//
+//nolint:errname,stylecheck // Would require version bump to change
 var NoPackagesFoundErr = errors.New("no packages found in scan")
+
+//nolint:errname,stylecheck // Would require version bump to change
 var VulnerabilitiesFoundErr = errors.New("vulnerabilities found")
 
 // scanDir walks through the given directory to try to find any relevant files
@@ -57,6 +61,7 @@ func scanDir(r *output.Reporter, query *osv.BatchedQuery, dir string, skipGit bo
 				r.PrintText(fmt.Sprintf("scan failed for git repository, %s: %v\n", path, err))
 				// Not fatal, so don't return and continue scanning other files
 			}
+
 			return filepath.SkipDir
 		}
 
@@ -99,6 +104,7 @@ func scanLockfile(r *output.Reporter, query *osv.BatchedQuery, path string) erro
 		}
 		query.Queries = append(query.Queries, pkgDetailQuery)
 	}
+
 	return nil
 }
 
@@ -126,6 +132,7 @@ func scanSBOMFile(r *output.Reporter, query *osv.BatchedQuery, path string) erro
 				Type: "sbom",
 			}
 			query.Queries = append(query.Queries, purlQuery)
+
 			return nil
 		})
 		if err == nil {
@@ -134,7 +141,7 @@ func scanSBOMFile(r *output.Reporter, query *osv.BatchedQuery, path string) erro
 			return nil
 		}
 
-		if errors.Is(err, sbom.InvalidFormat) {
+		if errors.Is(err, sbom.ErrInvalidFormat) {
 			continue
 		}
 
@@ -152,11 +159,12 @@ func getCommitSHA(repoDir string) (string, error) {
 	err := cmd.Run()
 
 	if err != nil {
-		if exiterr, ok := err.(*exec.ExitError); ok && exiterr.ExitCode() == 128 {
-			return "", fmt.Errorf("Failed to get commit hash, no commits exist? %w", exiterr)
-		} else {
-			return "", fmt.Errorf("failed to get commit hash: %w", err)
+		var exitErr *exec.ExitError
+		if errors.As(err, &exitErr) && exitErr.ExitCode() == 128 {
+			return "", fmt.Errorf("failed to get commit hash, no commits exist? %w", exitErr)
 		}
+
+		return "", fmt.Errorf("failed to get commit hash: %w", err)
 	}
 
 	return strings.TrimSpace(out.String()), nil
@@ -170,16 +178,17 @@ func scanGit(r *output.Reporter, query *osv.BatchedQuery, repoDir string) error
 	}
 	r.PrintText(fmt.Sprintf("Scanning %s at commit %s\n", repoDir, commit))
 
-	return scanGitCommit(r, query, commit, repoDir)
+	return scanGitCommit(query, commit, repoDir)
 }
 
-func scanGitCommit(r *output.Reporter, query *osv.BatchedQuery, commit string, source string) error {
+func scanGitCommit(query *osv.BatchedQuery, commit string, source string) error {
 	gitQuery := osv.MakeCommitRequest(commit)
 	gitQuery.Source = models.SourceInfo{
 		Path: source,
 		Type: "git",
 	}
 	query.Queries = append(query.Queries, gitQuery)
+
 	return nil
 }
 
@@ -196,11 +205,9 @@ func scanDebianDocker(r *output.Reporter, query *osv.BatchedQuery, dockerImageNa
 		r.PrintError(fmt.Sprintf("Failed to start docker image: %s\n", err))
 		return err
 	}
+	// TODO: Do error checking here
+	//nolint:errcheck
 	defer cmd.Wait()
-	if err != nil {
-		r.PrintError(fmt.Sprintf("Failed to run docker: %s\n", err))
-		return err
-	}
 	scanner := bufio.NewScanner(stdout)
 	packages := 0
 	for scanner.Scan() {
@@ -281,7 +288,7 @@ func DoScan(actions ScannerActions, r *output.Reporter) (models.VulnerabilityRes
 	for _, container := range actions.DockerContainerNames {
 		// TODO: Automatically figure out what docker base image
 		// and scan appropriately.
-		scanDebianDocker(r, &query, container)
+		_ = scanDebianDocker(r, &query, container)
 	}
 
 	for _, lockfileElem := range actions.LockfilePaths {
@@ -299,7 +306,7 @@ func DoScan(actions ScannerActions, r *output.Reporter) (models.VulnerabilityRes
 	for _, sbomElem := range actions.SBOMPaths {
 		sbomElem, err := filepath.Abs(sbomElem)
 		if err != nil {
-			return models.VulnerabilityResults{}, fmt.Errorf("failed to resolved path with error %s\n", err)
+			return models.VulnerabilityResults{}, fmt.Errorf("failed to resolved path with error %w", err)
 		}
 		err = scanSBOMFile(r, &query, sbomElem)
 		if err != nil {
@@ -308,7 +315,7 @@ func DoScan(actions ScannerActions, r *output.Reporter) (models.VulnerabilityRes
 	}
 
 	for _, commit := range actions.GitCommits {
-		err := scanGitCommit(r, &query, commit, "HASH")
+		err := scanGitCommit(&query, commit, "HASH")
 		if err != nil {
 			return models.VulnerabilityResults{}, err
 		}
@@ -328,7 +335,7 @@ func DoScan(actions ScannerActions, r *output.Reporter) (models.VulnerabilityRes
 
 	resp, err := osv.MakeRequest(query)
 	if err != nil {
-		return models.VulnerabilityResults{}, fmt.Errorf("scan failed %v", err)
+		return models.VulnerabilityResults{}, fmt.Errorf("scan failed %w", err)
 	}
 
 	filtered := filterResponse(r, query, resp, &configManager)
@@ -338,7 +345,7 @@ func DoScan(actions ScannerActions, r *output.Reporter) (models.VulnerabilityRes
 
 	hydratedResp, err := osv.Hydrate(resp)
 	if err != nil {
-		return models.VulnerabilityResults{}, fmt.Errorf("failed to hydrate OSV response: %v", err)
+		return models.VulnerabilityResults{}, fmt.Errorf("failed to hydrate OSV response: %w", err)
 	}
 
 	vulnerabilityResults := groupResponseBySource(r, query, hydratedResp)
diff --git a/pkg/osvscanner/purl_to_package.go b/pkg/osvscanner/purl_to_package.go
index 5ee813c..a9fe98f 100644
--- a/pkg/osvscanner/purl_to_package.go
+++ b/pkg/osvscanner/purl_to_package.go
@@ -28,6 +28,7 @@ func PURLToPackage(purl string) (models.PackageInfo, error) {
 	if ecosystem == "" {
 		ecosystem = parsedPURL.Type
 	}
+
 	return models.PackageInfo{
 		Name:      parsedPURL.Name,
 		Ecosystem: ecosystem,
diff --git a/pkg/osvscanner/vulnerability_result.go b/pkg/osvscanner/vulnerability_result.go
index 2dd0c38..1c27573 100644
--- a/pkg/osvscanner/vulnerability_result.go
+++ b/pkg/osvscanner/vulnerability_result.go
@@ -32,6 +32,7 @@ func groupResponseBySource(r *output.Reporter, query osv.BatchedQuery, resp *osv
 			if err != nil {
 				r.PrintError(fmt.Sprintf("Failed to parse purl: %s, with error: %s",
 					query.Package.PURL, err))
+
 				continue
 			}
 		} else {
diff --git a/run_lints.sh b/run_lints.sh
index 5e11a4f..ac170cd 100755
--- a/run_lints.sh
+++ b/run_lints.sh
@@ -2,4 +2,4 @@
 
 set -ex
 
-go vet ./...
+go run github.com/golangci/golangci-lint/cmd/golangci-lint@v1.50.1 run ./... --max-same-issues 0


Test patch:
diff --git a/cmd/osv-scanner/main_test.go b/cmd/osv-scanner/main_test.go
index 0fb47de..de837b4 100644
--- a/cmd/osv-scanner/main_test.go
+++ b/cmd/osv-scanner/main_test.go
@@ -1,4 +1,4 @@
-//nolint:testpackage // main cannot be accessed directly, so cannot use main_test
+// main cannot be accessed directly, so cannot use main_test
 package main
 
 import (
@@ -113,7 +113,7 @@ func TestRun(t *testing.T) {
 			name:         "",
 			args:         []string{"", "--version"},
 			wantExitCode: 0,
-			wantStdout:   `
+			wantStdout: `
 				osv-scanner version: dev
 				commit: n/a
 				built at: n/a
diff --git a/pkg/config/config_test.go b/pkg/config/config_internal_test.go
similarity index 89%
rename from pkg/config/config_test.go
rename to pkg/config/config_internal_test.go
index 3b43997..95e3bee 100644
--- a/pkg/config/config_test.go
+++ b/pkg/config/config_internal_test.go
@@ -2,11 +2,9 @@ package config
 
 import (
 	"path/filepath"
-	"strings"
 	"testing"
 
 	"github.com/google/go-cmp/cmp"
-	"github.com/google/osv-scanner/internal/output"
 )
 
 type testStruct struct {
@@ -16,6 +14,7 @@ type testStruct struct {
 }
 
 func TestTryLoadConfig(t *testing.T) {
+	t.Parallel()
 
 	expectedConfig := Config{
 		IgnoredVulns: []IgnoreEntry{
@@ -64,7 +63,7 @@ func TestTryLoadConfig(t *testing.T) {
 		if err != nil {
 			t.Errorf("%s", err)
 		}
-		config, configErr := tryLoadConfig(output.NewReporter(new(strings.Builder), new(strings.Builder), false), configPath)
+		config, configErr := tryLoadConfig(configPath)
 		if !cmp.Equal(config.IgnoredVulns, testData.config.IgnoredVulns) {
 			t.Errorf("Configs not equal: %+v != %+v", config, testData.config)
 		}
diff --git a/pkg/grouper/grouper_test.go b/pkg/grouper/grouper_test.go
index 82ce0d2..f852c8b 100644
--- a/pkg/grouper/grouper_test.go
+++ b/pkg/grouper/grouper_test.go
@@ -8,6 +8,8 @@ import (
 )
 
 func TestGroup(t *testing.T) {
+	t.Parallel()
+
 	// Should be grouped by IDs appearing in alias.
 	v1 := IDAliases{
 		ID: "CVE-1",
@@ -69,7 +71,6 @@ func TestGroup(t *testing.T) {
 	v10 := IDAliases{
 		ID: "UNRELATED-4",
 	}
-
 	for _, tc := range []struct {
 		vulns []IDAliases
 		want  []models.GroupInfo
@@ -136,6 +137,5 @@ func TestGroup(t *testing.T) {
 		if diff := cmp.Diff(tc.want, grouped); diff != "" {
 			t.Errorf("GroupedVulns() returned an unexpected result (-want, +got):\n%s", diff)
 		}
-
 	}
 }
diff --git a/pkg/lockfile/parse-go-lock_test.go b/pkg/lockfile/parse-go-lock_test.go
index d2dfe8a..ee71c5c 100644
--- a/pkg/lockfile/parse-go-lock_test.go
+++ b/pkg/lockfile/parse-go-lock_test.go
@@ -1,8 +1,9 @@
 package lockfile_test
 
 import (
-	"github.com/google/osv-scanner/pkg/lockfile"
 	"testing"
+
+	"github.com/google/osv-scanner/pkg/lockfile"
 )
 
 func TestParseGoLock_FileDoesNotExist(t *testing.T) {
@@ -241,7 +242,6 @@ func TestParseGoLock_Replacements_NotRequired(t *testing.T) {
 	})
 }
 
-
 func TestParseGoLock_Replacements_NoVersion(t *testing.T) {
 	t.Parallel()
 
diff --git a/pkg/lockfile/parse-pipenv-lock_test.go b/pkg/lockfile/parse-pipenv-lock_test.go
index 012b857..acbeae3 100644
--- a/pkg/lockfile/parse-pipenv-lock_test.go
+++ b/pkg/lockfile/parse-pipenv-lock_test.go
@@ -1,8 +1,9 @@
 package lockfile_test
 
 import (
-	"github.com/google/osv-scanner/pkg/lockfile"
 	"testing"
+
+	"github.com/google/osv-scanner/pkg/lockfile"
 )
 
 func TestParsePipenvLock_FileDoesNotExist(t *testing.T) {
@@ -133,4 +134,4 @@ func TestParsePipenvLock_PackageWithoutVersion(t *testing.T) {
 	}
 
 	expectPackages(t, packages, []lockfile.PackageDetails{})
-}
\ No newline at end of file
+}
diff --git a/pkg/lockfile/parse_test.go b/pkg/lockfile/parse_test.go
index 1d74be8..40f3b52 100644
--- a/pkg/lockfile/parse_test.go
+++ b/pkg/lockfile/parse_test.go
@@ -122,7 +122,7 @@ func TestParse_FindsExpectedParsers(t *testing.T) {
 	}
 
 	// gradle.lockfile and buildscript-gradle.lockfile use the same parser
-	count = count - 1
+	count -= 1
 
 	expectNumberOfParsersCalled(t, count)
 }
@@ -147,6 +147,7 @@ func TestListParsers(t *testing.T) {
 	parsers := lockfile.ListParsers()
 
 	firstExpected := "buildscript-gradle.lockfile"
+	//nolint:ifshort
 	lastExpected := "yarn.lock"
 
 	if first := parsers[0]; first != firstExpected {
diff --git a/pkg/osvscanner/vulnerability_result_test.go b/pkg/osvscanner/vulnerability_result_internal_test.go
similarity index 75%
rename from pkg/osvscanner/vulnerability_result_test.go
rename to pkg/osvscanner/vulnerability_result_internal_test.go
index 224e374..81f96af 100644
--- a/pkg/osvscanner/vulnerability_result_test.go
+++ b/pkg/osvscanner/vulnerability_result_internal_test.go
@@ -6,16 +6,15 @@ import (
 
 	"github.com/google/osv-scanner/internal/osv"
 	"github.com/google/osv-scanner/internal/output"
-	"github.com/google/osv-scanner/pkg/config"
 	"github.com/google/osv-scanner/pkg/models"
 )
 
 func Test_groupResponseBySource(t *testing.T) {
+	t.Parallel()
 	type args struct {
-		r             *output.Reporter
-		query         osv.BatchedQuery
-		resp          *osv.HydratedBatchedResponse
-		configManager *config.ConfigManager
+		r     *output.Reporter
+		query osv.BatchedQuery
+		resp  *osv.HydratedBatchedResponse
 	}
 	tests := []struct {
 		name string
@@ -23,7 +22,9 @@ func Test_groupResponseBySource(t *testing.T) {
 		want models.VulnerabilityResults
 	}{}
 	for _, tt := range tests {
+		tt := tt // Reinitialize for t.Parallel()
 		t.Run(tt.name, func(t *testing.T) {
+			t.Parallel()
 			if got := groupResponseBySource(tt.args.r, tt.args.query, tt.args.resp); !reflect.DeepEqual(got, tt.want) {
 				t.Errorf("groupResponse() = %v, want %v", got, tt.want)
 			}