Repository: google/osv-scanner (go)

Issue:
enable easy usage in CI
I had a CI script that just broke because logs are not sent to stdout when they were not before, I think a change in #3 

```
osv-scanner -r . | tee /tmp/vulns
2022/11/23 18:56:22 Scanning dir .
2022/11/23 18:56:22 Scanning /app/ at commit f187e875250ec11be568d5f00974ebcddba51aa3
2022/11/23 18:56:22 Scanned /app/go.mod file and found 37 packages
! [ -s /tmp/vulns ]
```

This used to exit 0, now it exits 1 because those log lines actually get piped to `/tmp/vulns` so the file is not empty.

How should `osv-scanner` be properly used in CI?

Thanks!


Pull request description:
fix: set error codes differently based on if there are vulnerabilities present
I've not yet added any test cases for the exit code when vulnerabilities are present because that requires having a lockfile with a vulnerability (which'll annoy dependabot) and interacting with the production API (which'll be relatively slow, the OSV could change, etc) - I plan to address that by adding a flag to set the API URL which'll allow leveraging [`httptest`](https://pkg.go.dev/net/http/httptest) like what I do in `osv-detector` [for testing the API-based databases](https://github.com/G-Rath/osv-detector/blob/main/pkg/database/api-check_test.go#L139-L153).

As for the error codes themselves, I've effectively split the 0-255 range in half (rounded down) with the upper half (starting at 127, as "general error") being reserved for "non-results related errors" such as files not found, errors with the API, etc; and the lower half being reserved for errors about vulnerabilities i.e. some tools set the exit code based on the severity of the vulnerabilities found (something that I'm not convinced is useful enough to land right away, but this way we can do so with minimal impact).

Currently the only other exit code the detector is using that I think will eventually make its way into the scanner is 128, meaning "lockfile not found", because that's the only error so far that I've identified as being useful to differentiate for automatic tools because that could be expected (and essentially enables differing figuring out if the detector/scanner can do anything with a file or directory to the tool itself, which is a very good thing)

Resolves #7

Patch:
diff --git a/cmd/osv-scanner/main.go b/cmd/osv-scanner/main.go
index dbea4d2..d04f512 100644
--- a/cmd/osv-scanner/main.go
+++ b/cmd/osv-scanner/main.go
@@ -357,13 +357,13 @@ func run(args []string, stdout, stderr io.Writer) int {
 
 	if err := app.Run(args); err != nil {
 		r.PrintError(fmt.Sprintf("%v", err))
-		return 1
+		return 127
 	}
 
 	resp, err := osv.MakeRequest(query)
 	if err != nil {
 		r.PrintError(fmt.Sprintf("Scan failed: %v", err))
-		return 1
+		return 127
 	}
 
 	filtered := filterResponse(r, query, resp, &configManager)
@@ -374,19 +374,33 @@ func run(args []string, stdout, stderr io.Writer) int {
 	hydratedResp, err := osv.Hydrate(resp)
 	if err != nil {
 		r.PrintError(fmt.Sprintf("Failed to hydrate OSV response: %v", err))
-		return 1
+		return 127
 	}
 
 	err = r.PrintResult(query, hydratedResp)
 
 	if err != nil {
 		r.PrintError(fmt.Sprintf("Failed to write output: %s", err))
+		return 127
+	}
+
+	if hasVulnerabilities(hydratedResp) {
 		return 1
 	}
 
 	return 0
 }
 
+func hasVulnerabilities(resp *osv.HydratedBatchedResponse) bool {
+	for _, result := range resp.Results {
+		if len(result.Vulns) == 1 {
+			return true
+		}
+	}
+
+	return false
+}
+
 // TODO(ochang): Machine readable output format.
 func main() {
 	os.Exit(run(os.Args, os.Stdout, os.Stderr))


Test patch:
diff --git a/cmd/osv-scanner/main_test.go b/cmd/osv-scanner/main_test.go
index 86e11d3..7a888b1 100644
--- a/cmd/osv-scanner/main_test.go
+++ b/cmd/osv-scanner/main_test.go
@@ -103,7 +103,7 @@ func TestRun(t *testing.T) {
 		{
 			name:         "",
 			args:         []string{""},
-			wantExitCode: 1,
+			wantExitCode: 127,
 			wantStdout: `
 				NAME:
 					 osv-scanner - scans various mediums for dependencies and matches it against the OSV database
@@ -141,7 +141,7 @@ func TestRun(t *testing.T) {
 		{
 			name:         "",
 			args:         []string{"", "./fixtures/locks-many/not-a-lockfile.toml"},
-			wantExitCode: 1,
+			wantExitCode: 127,
 			wantStdout: `
 				Scanning dir ./fixtures/locks-many/not-a-lockfile.toml
 				NAME: