Skip to content

Commit

Permalink
feat(cmd): add source and image subcommands to scan (#1519)
Browse files Browse the repository at this point in the history
**This change does not break any existing behaviour.**
- Creates `source` and `image` subcommands for `scan`.
- Inserts `source` as the default subcommand if none is provided.
- Removes the `experimental-oci-image` flag and its tests.
- Adds a feedback link to HTML output

For project scanning, users can use the following commands:
- `osv-scanner <file_name>`
- `osv-scanner scan <file_name>`
- `osv-scanner scan source <file_name>`

For docker scanning, users can use the following commands:
- `osv-scanner scan image <docker_image>`
- `osv-scanner scan image --archive <docker_image.tar>`

Help command:
```
NAME:
  osv-scanner - scans various mediums for dependencies and checks them against the OSV database

USAGE:
  osv-scanner [global options] command [command options]

EXAMPLES:
  # Scan a source directory
  $ osv-scanner scan source -r <source_directory>

  # Scan a container image
  $ osv-scanner scan image <image_name>

  # Scan a local image archive (e.g. a tar file) and generate HTML output
  $ osv-scanner scan image --serve --archive <image_name.tar>

  # Fix vulnerabilities in a manifest file and lockfile (non-interactive mode)
  $ osv-scanner fix --non-interactive -M <manifest_file> -L <lockfile>

  For full usage details, please refer to the help command of each subcommand (e.g. osv-scanner scan --help).

VERSION:
  1.9.1

COMMANDS:
  scan     scans projects and container images for dependencies, and checks them against the OSV database.
  fix      [EXPERIMENTAL] scans a manifest and/or lockfile for vulnerabilities and suggests changes for remediating them
  help, h  Shows a list of commands or help for one command


GLOBAL OPTIONS:
    --help, -h  show help  --version, -v  print the version

```
  • Loading branch information
hogo6002 authored Jan 24, 2025
1 parent 0b7a102 commit 0e88d4f
Show file tree
Hide file tree
Showing 19 changed files with 687 additions and 760 deletions.
298 changes: 33 additions & 265 deletions cmd/osv-scanner/__snapshots__/main_test.snap

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
package scan
package helper

var stableCallAnalysisStates = map[string]bool{
"go": true,
"rust": false,
}

// Creates a map to record if languages are enabled or disabled for call analysis.
func createCallAnalysisStates(enabledCallAnalysis []string, disabledCallAnalysis []string) map[string]bool {
func CreateCallAnalysisStates(enabledCallAnalysis []string, disabledCallAnalysis []string) map[string]bool {
callAnalysisStates := make(map[string]bool)

for _, language := range enabledCallAnalysis {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package scan
package helper

import (
"reflect"
Expand Down Expand Up @@ -55,7 +55,7 @@ func TestCreateCallAnalysisStates(t *testing.T) {
}

for _, testCase := range testCases {
actualCallAnalysisStates := createCallAnalysisStates(testCase.enabledCallAnalysis, testCase.disabledCallAnalysis)
actualCallAnalysisStates := CreateCallAnalysisStates(testCase.enabledCallAnalysis, testCase.disabledCallAnalysis)

if !reflect.DeepEqual(actualCallAnalysisStates, testCase.expectedCallAnalysisStates) {
t.Errorf("expected call analysis states to be %v, but got %v", testCase.expectedCallAnalysisStates, actualCallAnalysisStates)
Expand Down
145 changes: 145 additions & 0 deletions cmd/osv-scanner/internal/helper/helper.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package helper

import (
"fmt"
"net/http"
"os/exec"
"runtime"
"slices"
"strings"
"time"

"github.com/google/osv-scanner/pkg/reporter"
"github.com/urfave/cli/v2"
)

// flags that require network access and values to disable them.
var OfflineFlags = map[string]string{
"skip-git": "true",
"experimental-offline-vulnerabilities": "true",
"experimental-no-resolve": "true",
"experimental-licenses-summary": "false",
// "experimental-licenses": "", // StringSliceFlag has to be manually cleared.
}

var GlobalScanFlags = []cli.Flag{
&cli.StringFlag{
Name: "config",
Usage: "set/override config file",
TakesFile: true,
},
&cli.StringFlag{
Name: "format",
Aliases: []string{"f"},
Usage: "sets the output format; value can be: " + strings.Join(reporter.Format(), ", "),
Value: "table",
Action: func(_ *cli.Context, s string) error {
if slices.Contains(reporter.Format(), s) {
return nil
}

return fmt.Errorf("unsupported output format \"%s\" - must be one of: %s", s, strings.Join(reporter.Format(), ", "))
},
},
&cli.BoolFlag{
Name: "serve",
Usage: "output as HTML result and serve it locally",
},
&cli.StringFlag{
Name: "output",
Usage: "saves the result to the given file path",
TakesFile: true,
},
&cli.StringFlag{
Name: "verbosity",
Usage: "specify the level of information that should be provided during runtime; value can be: " + strings.Join(reporter.VerbosityLevels(), ", "),
Value: "info",
},
&cli.BoolFlag{
Name: "experimental-offline",
Usage: "run in offline mode, disabling any features requiring network access",
Action: func(ctx *cli.Context, b bool) error {
if !b {
return nil
}
// Disable the features requiring network access.
for flag, value := range OfflineFlags {
// TODO(michaelkedar): do something if the flag was already explicitly set.
if err := ctx.Set(flag, value); err != nil {
panic(fmt.Sprintf("failed setting offline flag %s to %s: %v", flag, value, err))
}
}

return nil
},
},
&cli.BoolFlag{
Name: "experimental-offline-vulnerabilities",
Usage: "checks for vulnerabilities using local databases that are already cached",
},
&cli.BoolFlag{
Name: "experimental-download-offline-databases",
Usage: "downloads vulnerability databases for offline comparison",
},
&cli.BoolFlag{
Name: "experimental-no-resolve",
Usage: "disable transitive dependency resolution of manifest files",
},
&cli.StringFlag{
Name: "experimental-local-db-path",
Usage: "sets the path that local databases should be stored",
Hidden: true,
},
&cli.BoolFlag{
Name: "experimental-all-packages",
Usage: "when json output is selected, prints all packages",
},
&cli.BoolFlag{
Name: "experimental-licenses-summary",
Usage: "report a license summary, implying the --experimental-all-packages flag",
},
&cli.StringSliceFlag{
Name: "experimental-licenses",
Usage: "report on licenses based on an allowlist",
},
}

// openHTML opens the outputted HTML file.
func OpenHTML(r reporter.Reporter, outputPath string) {
// Open the outputted HTML file in the default browser.
r.Infof("Opening %s...\n", outputPath)
var err error
switch runtime.GOOS {
case "linux":
err = exec.Command("xdg-open", outputPath).Start()
case "windows":
err = exec.Command("start", "", outputPath).Start()
case "darwin": // macOS
err = exec.Command("open", outputPath).Start()
default:
r.Infof("Unsupported OS.\n")
}

if err != nil {
r.Errorf("Failed to open: %s.\n Please manually open the outputted HTML file: %s\n", err, outputPath)
}
}

// ServeHTML serves the single HTML file for remote accessing.
// The program will keep running to serve the HTML report on localhost
// until the user manually terminates it (e.g. using Ctrl+C).
func ServeHTML(r reporter.Reporter, outputPath string) {
servePort := "8000"
localhostURL := fmt.Sprintf("http://localhost:%s/", servePort)
r.Infof("Serving HTML report at %s.\nIf you are accessing remotely, use the following SSH command:\n`ssh -L local_port:destination_server_ip:%s ssh_server_hostname`\n", localhostURL, servePort)
server := &http.Server{
Addr: ":" + servePort,
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, outputPath)
}),
ReadHeaderTimeout: 3 * time.Second,
}
if err := server.ListenAndServe(); err != nil {
r.Errorf("Failed to start server: %v\n", err)
}
}
84 changes: 78 additions & 6 deletions cmd/osv-scanner/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ func run(args []string, stdout, stderr io.Writer) int {
fix.Command(stdout, stderr, &r),
update.Command(stdout, stderr, &r),
},
CustomAppHelpTemplate: getCustomHelpTemplate(),
}

// If ExitErrHandler is not set, cli will use the default cli.HandleExitCoder.
Expand Down Expand Up @@ -84,6 +85,41 @@ func run(args []string, stdout, stderr io.Writer) int {
return 0
}

func getCustomHelpTemplate() string {
return `
NAME:
{{.Name}} - {{.Usage}}
USAGE:
{{.Name}} {{if .VisibleFlags}}[global options]{{end}}{{if .Commands}} command [command options]{{end}}
EXAMPLES:
# Scan a source directory
$ {{.Name}} scan source -r <source_directory>
# Scan a container image
$ {{.Name}} scan image <image_name>
# Scan a local image archive (e.g. a tar file) and generate HTML output
$ {{.Name}} scan image --serve --archive <image_name.tar>
# Fix vulnerabilities in a manifest file and lockfile (non-interactive mode)
$ {{.Name}} fix --non-interactive -M <manifest_file> -L <lockfile>
For full usage details, please refer to the help command of each subcommand (e.g. {{.Name}} scan --help).
VERSION:
{{.Version}}
COMMANDS:
{{range .Commands}}{{if and (not .HideHelp) (not .Hidden)}} {{join .Names ", "}}{{ "\t"}}{{.Usage}}{{ "\n" }}{{end}}{{end}}
{{if .VisibleFlags}}
GLOBAL OPTIONS:
{{range .VisibleFlags}} {{.}}{{end}}
{{end}}
`
}

// Gets all valid commands and global options for OSV-Scanner.
func getAllCommands(commands []*cli.Command) []string {
// Adding all subcommands
Expand All @@ -108,24 +144,60 @@ func getAllCommands(commands []*cli.Command) []string {
return allCommands
}

// warnIfCommandAmbiguous warns the user if the command they are trying to run
// exists as both a subcommand and as a file on the filesystem.
// If this is the case, the command is assumed to be a subcommand.
func warnIfCommandAmbiguous(command string, stdout, stderr io.Writer) {
if _, err := os.Stat(command); err == nil {
r := reporter.NewJSONReporter(stdout, stderr, reporter.InfoLevel)
r.Warnf("Warning: `%[1]s` exists as both a subcommand of OSV-Scanner and as a file on the filesystem. `%[1]s` is assumed to be a subcommand here. If you intended for `%[1]s` to be an argument to `%[1]s`, you must specify `%[1]s %[1]s` in your command line.\n", command)
}
}

// Inserts the default command to args if no command is specified.
func insertDefaultCommand(args []string, commands []*cli.Command, defaultCommand string, stdout, stderr io.Writer) []string {
// Do nothing if no command or file name is provided.
if len(args) < 2 {
return args
}

allCommands := getAllCommands(commands)
if !slices.Contains(allCommands, args[1]) {
command := args[1]
// If no command is provided, use the default command and subcommand.
if !slices.Contains(allCommands, command) {
// Avoids modifying args in-place, as some unit tests rely on its original value for multiple calls.
argsTmp := make([]string, len(args)+1)
copy(argsTmp[2:], args[1:])
argsTmp := make([]string, len(args)+2)
copy(argsTmp[3:], args[1:])
argsTmp[1] = defaultCommand
// Set the default subCommand of Scan
argsTmp[2] = scan.DefaultSubcommand

// Executes the cli app with the new args.
return argsTmp
} else if _, err := os.Stat(args[1]); err == nil {
r := reporter.NewJSONReporter(stdout, stderr, reporter.InfoLevel)
r.Warnf("Warning: `%[1]s` exists as both a subcommand of OSV-Scanner and as a file on the filesystem. `%[1]s` is assumed to be a subcommand here. If you intended for `%[1]s` to be an argument to `%[1]s`, you must specify `%[1]s %[1]s` in your command line.\n", args[1])
}

warnIfCommandAmbiguous(command, stdout, stderr)

// If only the default command is provided without its subcommand, append the subcommand.
if command == defaultCommand {
if len(args) < 3 {
// Indicates that only "osv-scanner scan" was provided, without a subcommand or filename
return args
}

subcommand := args[2]
// Default to the "source" subcommand if none is provided.
if !slices.Contains(scan.Subcommands, subcommand) {
argsTmp := make([]string, len(args)+1)
copy(argsTmp[3:], args[2:])
argsTmp[1] = defaultCommand
argsTmp[2] = scan.DefaultSubcommand

return argsTmp
}

// Print a warning message if subcommand exist on the filesystem.
warnIfCommandAmbiguous(subcommand, stdout, stderr)
}

return args
Expand Down
Loading

0 comments on commit 0e88d4f

Please sign in to comment.