Repository: google/osv-scanner
Pull request #160 (2023-01-22 22:11:28)

test: add cases for `semantic.MustParse`
Port of https://github.com/G-Rath/osv-detector/pull/167


Patch:
diff --git a/README.md b/README.md
index 7c9e9c9..157df22 100644
--- a/README.md
+++ b/README.md
@@ -212,12 +212,30 @@ id = "GO-2022-1059"
 reason = "No external http servers are written in Go lang."
 ```
 
-## JSON output
-By default osv-scanner outputs a human readable table. To have osv-scanner output JSON instead, pass the `--json` flag when calling osv-scanner.
+## Output formats
 
-When using the --json flag, only the JSON output will be printed to stdout, with all other outputs being directed to stderr. So to save only the json output to file, you can redirect the output with `osv-scanner --json ... > /path/to/file.json`
+You can control the format used by the scanner to output results with the `--format` flag. The different formats supported by the scanner are:
 
-### Output Format
+### `table` format
+
+The default format, which outputs the results as a human-readable table.
+
+Sample output:
+
+```
+╭─────────────────────────────────────┬───────────┬──────────────────────────┬─────────┬────────────────────╮
+│ OSV URL (ID IN BOLD)                │ ECOSYSTEM │ PACKAGE                  │ VERSION │ SOURCE             │
+├─────────────────────────────────────┼───────────┼──────────────────────────┼─────────┼────────────────────┤
+│ https://osv.dev/GHSA-c3h9-896r-86jm │ Go        │ github.com/gogo/protobuf │ 1.3.1   │ path/to/go.mod     │
+│ https://osv.dev/GHSA-m5pq-gvj9-9vr8 │ crates.io │ regex                    │ 1.3.1   │ path/to/Cargo.lock │
+╰─────────────────────────────────────┴───────────┴──────────────────────────┴─────────┴────────────────────╯
+```
+
+### `json` format
+
+Outputs the results as a JSON object to stdout, with all other output being directed to stderr - this makes it safe to redirect the output to a file with `osv-scanner --format json ... > /path/to/file.json`.
+
+Sample output:
 
 ```json5
 {
diff --git a/cmd/osv-scanner/main.go b/cmd/osv-scanner/main.go
index 82c7c94..53cd51e 100644
--- a/cmd/osv-scanner/main.go
+++ b/cmd/osv-scanner/main.go
@@ -22,7 +22,7 @@ func run(args []string, stdout, stderr io.Writer) int {
 	var r *output.Reporter
 
 	cli.VersionPrinter = func(ctx *cli.Context) {
-		r = output.NewReporter(stdout, stderr, false)
+		r = output.NewReporter(stdout, stderr, "")
 		r.PrintText(fmt.Sprintf("osv-scanner version: %s\ncommit: %s\nbuilt at: %s\n", ctx.App.Version, commit, date))
 	}
 
@@ -57,9 +57,22 @@ func run(args []string, stdout, stderr io.Writer) int {
 				Usage:     "set/override config file",
 				TakesFile: true,
 			},
+			&cli.StringFlag{
+				Name:    "format",
+				Aliases: []string{"f"},
+				Usage:   "sets the output format",
+				Value:   "table",
+				Action: func(context *cli.Context, s string) error {
+					if s != "table" && s != "json" {
+						return fmt.Errorf("unsupported output format \"%s\" - must be either \"table\" or \"json\"", s)
+					}
+
+					return nil
+				},
+			},
 			&cli.BoolFlag{
 				Name:  "json",
-				Usage: "sets output to json (WIP)",
+				Usage: "sets output to json (deprecated, use --format json instead)",
 			},
 			&cli.BoolFlag{
 				Name:  "skip-git",
@@ -75,7 +88,13 @@ func run(args []string, stdout, stderr io.Writer) int {
 		},
 		ArgsUsage: "[directory1 directory2...]",
 		Action: func(context *cli.Context) error {
-			r = output.NewReporter(stdout, stderr, context.Bool("json"))
+			format := context.String("format")
+
+			if context.Bool("json") {
+				format = "json"
+			}
+
+			r = output.NewReporter(stdout, stderr, format)
 
 			vulnResult, err := osvscanner.DoScan(osvscanner.ScannerActions{
 				LockfilePaths:        context.StringSlice("lockfile"),
@@ -97,7 +116,7 @@ func run(args []string, stdout, stderr io.Writer) int {
 
 	if err := app.Run(args); err != nil {
 		if r == nil {
-			r = output.NewReporter(stdout, stderr, false)
+			r = output.NewReporter(stdout, stderr, "")
 		}
 		if errors.Is(err, osvscanner.VulnerabilitiesFoundErr) {
 			return 1
diff --git a/internal/output/reporter.go b/internal/output/reporter.go
index 10a9918..50679a8 100644
--- a/internal/output/reporter.go
+++ b/internal/output/reporter.go
@@ -9,16 +9,16 @@ import (
 )
 
 type Reporter struct {
-	stdout       io.Writer
-	stderr       io.Writer
-	outputAsJSON bool
+	stdout io.Writer
+	stderr io.Writer
+	format string
 }
 
-func NewReporter(stdout io.Writer, stderr io.Writer, outputAsJSON bool) *Reporter {
+func NewReporter(stdout io.Writer, stderr io.Writer, format string) *Reporter {
 	return &Reporter{
-		stdout:       stdout,
-		stderr:       stderr,
-		outputAsJSON: outputAsJSON,
+		stdout: stdout,
+		stderr: stderr,
+		format: format,
 	}
 }
 
@@ -27,7 +27,7 @@ func NewVoidReporter() *Reporter {
 	stdout := new(strings.Builder)
 	stderr := new(strings.Builder)
 
-	return NewReporter(stdout, stderr, false)
+	return NewReporter(stdout, stderr, "")
 }
 
 // PrintError writes the given message to stderr, regardless of if the reporter
@@ -44,7 +44,7 @@ func (r *Reporter) PrintError(msg string) {
 func (r *Reporter) PrintText(msg string) {
 	target := r.stdout
 
-	if r.outputAsJSON {
+	if r.format == "json" {
 		target = r.stderr
 	}
 
@@ -52,11 +52,12 @@ func (r *Reporter) PrintText(msg string) {
 }
 
 func (r *Reporter) PrintResult(vulnResult *models.VulnerabilityResults) error {
-	if r.outputAsJSON {
+	switch r.format {
+	case "json":
 		return PrintJSONResults(vulnResult, r.stdout)
+	case "table":
+		PrintTableResults(vulnResult, r.stdout)
 	}
 
-	PrintTableResults(vulnResult, r.stdout)
-
 	return nil
 }


Test patch:
diff --git a/cmd/osv-scanner/main_test.go b/cmd/osv-scanner/main_test.go
index de837b4..26e4ff3 100644
--- a/cmd/osv-scanner/main_test.go
+++ b/cmd/osv-scanner/main_test.go
@@ -208,6 +208,20 @@ func TestRun(t *testing.T) {
 				Scanned %%/fixtures/locks-many/composer.lock file and found 1 packages
 			`,
 		},
+		{
+			name:         "",
+			args:         []string{"", "--format", "json", "./fixtures/locks-many/composer.lock"},
+			wantExitCode: 0,
+			wantStdout: `
+				{
+					"results": []
+				}
+			`,
+			wantStderr: `
+				Scanning dir ./fixtures/locks-many/composer.lock
+				Scanned %%/fixtures/locks-many/composer.lock file and found 1 packages
+			`,
+		},
 	}
 	for _, tt := range tests {
 		tt := tt
diff --git a/internal/semantic/parse_test.go b/internal/semantic/parse_test.go
index 7e759be..0ed847d 100644
--- a/internal/semantic/parse_test.go
+++ b/internal/semantic/parse_test.go
@@ -20,3 +20,30 @@ func TestParse(t *testing.T) {
 		}
 	}
 }
+
+func TestMustParse(t *testing.T) {
+	t.Parallel()
+
+	defer func() {
+		if r := recover(); r != nil {
+			t.Errorf("unexpected panic - '%s'", r)
+		}
+	}()
+
+	ecosystems := lockfile.KnownEcosystems()
+
+	for _, ecosystem := range ecosystems {
+		semantic.MustParse("", ecosystem)
+	}
+}
+
+func TestMustParse_Panic(t *testing.T) {
+	t.Parallel()
+
+	defer func() { _ = recover() }()
+
+	semantic.MustParse("", "<unknown>")
+
+	// if we reached here, then we can't have panicked
+	t.Errorf("function did not panic when given an unknown ecosystem")
+}