Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions __snapshots__/main_test.snap
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,44 @@ You must provide at least one path to either a lockfile or a directory containin

---

[TestRun_APIError/#00 - 1]
Loaded the following OSV databases:
api#http://<localhost>:<port> (using batches of 1000)
RubyGems (%% vulnerabilities, including withdrawn - last updated %%)

<tempdir>/Gemfile.lock: found 1 package
Using config at <tempdir>/.osv-detector.yml (0 ignores)
Using db api#http://<localhost>:<port> (using batches of 1000)
Using db RubyGems (%% vulnerabilities, including withdrawn - last updated %%)

no known vulnerabilities found

---

[TestRun_APIError/#00 - 2]
an api error occurred while trying to check the packages listed in <tempdir>/Gemfile.lock: api returned unexpected status (POST http://<localhost>:<port>/querybatch 400)

---

[TestRun_APIError/#01 - 1]
Loaded the following OSV databases:
api#http://<localhost>:<port> (using batches of 1000)
RubyGems (%% vulnerabilities, including withdrawn - last updated %%)

<tempdir>/Gemfile.lock: found 1 package
Using config at <tempdir>/.osv-detector.yml (0 ignores)
Using db api#http://<localhost>:<port> (using batches of 1000)
Using db RubyGems (%% vulnerabilities, including withdrawn - last updated %%)

no known vulnerabilities found

---

[TestRun_APIError/#01 - 2]
an api error occurred while trying to check the packages listed in <tempdir>/Gemfile.lock: api response could not be parsed as json (POST http://<localhost>:<port>/querybatch): invalid character '<' looking for beginning of value

---

[TestRun_Configs/#00 - 1]
Loaded the following OSV databases:

Expand Down
8 changes: 8 additions & 0 deletions internal/reporter/reporter.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ type Reporter struct {
stderr io.Writer
outputAsJSON bool
results []Result

hasErrored bool
}

func New(stdout io.Writer, stderr io.Writer, outputAsJSON bool) *Reporter {
Expand All @@ -27,9 +29,15 @@ func New(stdout io.Writer, stderr io.Writer, outputAsJSON bool) *Reporter {
}
}

func (r *Reporter) HasErrored() bool {
return r.hasErrored
}

// PrintErrorf writes the given message to stderr, regardless of if the reporter
// is outputting as JSON or not
func (r *Reporter) PrintErrorf(msg string, a ...any) {
r.hasErrored = true

fmt.Fprintf(r.stderr, msg, a...)
}

Expand Down
4 changes: 4 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -733,6 +733,10 @@ This flag can be passed multiple times to ignore different vulnerabilities`)
writeUpdatedConfigs(r, vulnsPerConfig)
}

if r.HasErrored() && exitCode == 0 {
exitCode = 127
}

return exitCode
}

Expand Down
11 changes: 11 additions & 0 deletions main_normalize_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,16 @@ func normalizeDatabaseStats(t *testing.T, str string) string {
return re.ReplaceAllString(str, "$1 (%% vulnerabilities, including withdrawn - last updated %%)")
}

// normalizeLocalhostPort attempts to replace references to 127.0.0.1:<port>
// with a placeholder, to ensure tests pass when using httptest.Server
func normalizeLocalhostPort(t *testing.T, str string) string {
t.Helper()

re := cachedregexp.MustCompile(`127\.0\.0\.1:\d+`)

return re.ReplaceAllString(str, "<localhost>:<port>")
}

// normalizeErrors attempts to replace error messages on alternative OSs with their
// known linux equivalents, to ensure tests pass across different OSs
func normalizeErrors(t *testing.T, str string) string {
Expand All @@ -110,6 +120,7 @@ func normalizeSnapshot(t *testing.T, str string) string {
normalizeTempDirectory,
normalizeUserCacheDirectory,
normalizeDatabaseStats,
normalizeLocalhostPort,
normalizeErrors,
} {
str = normalizer(t, str)
Expand Down
69 changes: 69 additions & 0 deletions main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package main
import (
"bytes"
"fmt"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
Expand Down Expand Up @@ -883,3 +885,70 @@ func TestRun_EndToEnd(t *testing.T) {
})
}
}

func TestRun_APIError(t *testing.T) {
t.Parallel()

tests := []struct{ handler http.HandlerFunc }{
{
handler: func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusBadRequest)
_, _ = w.Write([]byte("{}"))
},
},
{
handler: func(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write([]byte("<html></html>"))
},
},
}
for _, tt := range tests {
t.Run("", func(t *testing.T) {
t.Parallel()

//nolint:usetesting // we need to customize the directory name to replace in snapshots
p, err := os.MkdirTemp("", "osv-detector-test-*")
if err != nil {
t.Fatalf("could not create test directory: %v", err)
}

// ensure the test directory is removed when we're done testing
t.Cleanup(func() {
_ = os.RemoveAll(p)
})

// create a file for scanning
err = os.WriteFile(filepath.Join(p, "Gemfile.lock"), []byte(`
GEM
remote: https://rubygems.org/
specs:
ast (2.4.2)
`), 0600)

if err != nil {
t.Fatal(err)
}

// setup a fake api server
ts := httptest.NewServer(tt.handler)
t.Cleanup(ts.Close)

// create a config file setting up our api server
err = os.WriteFile(filepath.Join(p, ".osv-detector.yml"), []byte(`
extra-databases:
- url: `+ts.URL,
), 0600)

if err != nil {
t.Fatal(err)
}

// run the cli in our tmp directory
testCli(t, cliTestCase{
name: "",
args: []string{p},
exit: 127,
})
})
}
}