diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 6c3274f6832..1d9552aa867 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -9,6 +9,7 @@ env: builds: - id: linux-build + dir: ./cmd/grype binary: grype goos: - linux @@ -29,6 +30,7 @@ builds: -X github.com/anchore/grype/internal/version.gitDescription={{.Summary}} - id: darwin-build + dir: ./cmd/grype binary: grype goos: - darwin @@ -44,6 +46,7 @@ builds: - QUILL_LOG_FILE=/tmp/quill-{{ .Target }}.log - id: windows-build + dir: ./cmd/grype binary: grype goos: - windows diff --git a/DEVELOPING.md b/DEVELOPING.md index 5357f21caff..3c1993ecbb0 100644 --- a/DEVELOPING.md +++ b/DEVELOPING.md @@ -4,11 +4,9 @@ There are a few useful things to know before diving into the codebase. This proj ## Getting started -### Native Development - After cloning do the following: -1. run `go build main.go` to get a binary named `main` from the source (use `-o ` to get a differently named binary), or optionally `go run main.go` to run from source. +1. run `go build ./cmd/grype` to get a binary named `main` from the source (use `-o ` to get a differently named binary), or optionally `go run ./cmd/grype` to run from source. In order to run tests and build all artifacts: @@ -19,14 +17,6 @@ The main make tasks for common static analysis and testing are `lint`, `format`, See `make help` for all the current make tasks. -### Docker Development - -This depends on Docker and Docker Compose - -1. run `docker-compose build grype` to build the local development container -2. run `docker-compose run --rm grype bash` to enter into the container with all the bootstrapped dependencies installed. -3. run `make` to verify everything is installed and working properly - ## Relationship to Syft Grype uses Syft as a library for all-things related to obtaining and parsing the given scan target (pulling container @@ -41,7 +31,7 @@ to a released version (e.g. `go get github.com/anchore/syft@v` The currently supported database format is Sqlite3. Install `sqlite3` in your system and ensure that the `sqlite3` executable is available in your path. Ask `grype` about the location of the database, which will be different depending on the operating system: ``` -$ go run main.go db status +$ go run ./cmd/grype db status Location: /Users/alfredo/Library/Caches/grype/db Built: 2020-07-31 08:18:29 +0000 UTC Current DB Version: 1 diff --git a/Makefile b/Makefile index 6e920388879..9b7ced0d690 100644 --- a/Makefile +++ b/Makefile @@ -14,7 +14,7 @@ GOLANGCILINT_VERSION := v1.53.3 GOSIMPORTS_VERSION := v0.3.8 BOUNCER_VERSION := v0.4.0 CHRONICLE_VERSION := v0.6.0 -GORELEASER_VERSION := v1.18.2 +GORELEASER_VERSION := v1.19.2 YAJSV_VERSION := v1.4.1 QUILL_VERSION := v0.2.0 GLOW_VERSION := v1.5.1 diff --git a/README.md b/README.md index 9c37ed988e7..a59345e47a7 100644 --- a/README.md +++ b/README.md @@ -446,7 +446,7 @@ Find complete information on Grype's database commands by running `grype db --he Grype supplies shell completion through its CLI implementation ([cobra](https://github.com/spf13/cobra/blob/master/shell_completions.md)). Generate the completion code for your shell by running one of the following commands: - `grype completion ` -- `go run main.go completion ` +- `go run ./cmd/grype completion ` This will output a shell script to STDOUT, which can then be used as a completion script for Grype. Running one of the above commands with the `-h` or `--help` flags will provide instructions on how to do that for your chosen shell. diff --git a/cmd/cmd.go b/cmd/grype/cli/legacy/cmd.go similarity index 99% rename from cmd/cmd.go rename to cmd/grype/cli/legacy/cmd.go index 20b4c14ebb1..9c5520c2db5 100644 --- a/cmd/cmd.go +++ b/cmd/grype/cli/legacy/cmd.go @@ -1,4 +1,4 @@ -package cmd +package legacy import ( "encoding/json" diff --git a/cmd/completion.go b/cmd/grype/cli/legacy/completion.go similarity index 99% rename from cmd/completion.go rename to cmd/grype/cli/legacy/completion.go index 1ba1bb0fb34..ae7dd3e0906 100644 --- a/cmd/completion.go +++ b/cmd/grype/cli/legacy/completion.go @@ -1,4 +1,4 @@ -package cmd +package legacy import ( "context" diff --git a/cmd/db.go b/cmd/grype/cli/legacy/db.go similarity index 91% rename from cmd/db.go rename to cmd/grype/cli/legacy/db.go index 9a56d61726a..247ed7c5ca5 100644 --- a/cmd/db.go +++ b/cmd/grype/cli/legacy/db.go @@ -1,4 +1,4 @@ -package cmd +package legacy import ( "github.com/spf13/cobra" diff --git a/cmd/db_check.go b/cmd/grype/cli/legacy/db_check.go similarity index 98% rename from cmd/db_check.go rename to cmd/grype/cli/legacy/db_check.go index 94f9bcec313..0c1eec9822f 100644 --- a/cmd/db_check.go +++ b/cmd/grype/cli/legacy/db_check.go @@ -1,4 +1,4 @@ -package cmd +package legacy import ( "fmt" diff --git a/cmd/db_delete.go b/cmd/grype/cli/legacy/db_delete.go similarity index 97% rename from cmd/db_delete.go rename to cmd/grype/cli/legacy/db_delete.go index ff404ab7000..4ca36db32a0 100644 --- a/cmd/db_delete.go +++ b/cmd/grype/cli/legacy/db_delete.go @@ -1,4 +1,4 @@ -package cmd +package legacy import ( "fmt" diff --git a/cmd/db_diff.go b/cmd/grype/cli/legacy/db_diff.go similarity index 82% rename from cmd/db_diff.go rename to cmd/grype/cli/legacy/db_diff.go index 6f81455e95c..180ec99ed58 100644 --- a/cmd/db_diff.go +++ b/cmd/grype/cli/legacy/db_diff.go @@ -1,18 +1,15 @@ -package cmd +package legacy import ( - "fmt" - "os" + "strings" "github.com/spf13/cobra" - "github.com/wagoodman/go-partybus" + "github.com/anchore/grype/cmd/grype/internal/ui" "github.com/anchore/grype/grype/db" "github.com/anchore/grype/grype/differ" - "github.com/anchore/grype/grype/event" "github.com/anchore/grype/internal/bus" "github.com/anchore/grype/internal/log" - "github.com/anchore/grype/internal/ui" "github.com/anchore/stereoscope" ) @@ -38,6 +35,7 @@ func startDBDiffCmd(base string, target string, deleteDatabases bool) <-chan err errs := make(chan error) go func() { defer close(errs) + defer bus.Exit() d, err := differ.NewDiffer(appConfig.DB.ToCuratorConfig()) if err != nil { errs <- err @@ -60,38 +58,27 @@ func startDBDiffCmd(base string, target string, deleteDatabases bool) <-chan err return } + sb := &strings.Builder{} + if len(*diff) == 0 { - fmt.Println("Databases are identical!") + sb.WriteString("Databases are identical!\n") } else { - err := d.Present(dbDiffOutputFormat, diff, os.Stdout) + err := d.Present(dbDiffOutputFormat, diff, sb) if err != nil { errs <- err } } + bus.Report(sb.String()) + if deleteDatabases { errs <- d.DeleteDatabases() } - - bus.Publish(partybus.Event{ - Type: event.NonRootCommandFinished, - Value: "", - }) }() return errs } func runDBDiffCmd(cmd *cobra.Command, args []string) error { - reporter, closer, err := reportWriter() - defer func() { - if err := closer(); err != nil { - log.Warnf("unable to write to report destination: %+v", err) - } - }() - if err != nil { - return err - } - deleteDatabases, err := cmd.Flags().GetBool(deleteFlag) if err != nil { return err @@ -123,7 +110,7 @@ func runDBDiffCmd(cmd *cobra.Command, args []string) error { setupSignals(), eventSubscription, stereoscope.Cleanup, - ui.Select(isVerbose(), appConfig.Quiet, reporter)..., + ui.Select(isVerbose(), appConfig.Quiet)..., ) } diff --git a/cmd/db_import.go b/cmd/grype/cli/legacy/db_import.go similarity index 98% rename from cmd/db_import.go rename to cmd/grype/cli/legacy/db_import.go index fc00261da4b..400d19d15a9 100644 --- a/cmd/db_import.go +++ b/cmd/grype/cli/legacy/db_import.go @@ -1,4 +1,4 @@ -package cmd +package legacy import ( "fmt" diff --git a/cmd/db_list.go b/cmd/grype/cli/legacy/db_list.go similarity index 99% rename from cmd/db_list.go rename to cmd/grype/cli/legacy/db_list.go index 7fcc2880d4c..e07240b6595 100644 --- a/cmd/db_list.go +++ b/cmd/grype/cli/legacy/db_list.go @@ -1,4 +1,4 @@ -package cmd +package legacy import ( "encoding/json" diff --git a/cmd/db_status.go b/cmd/grype/cli/legacy/db_status.go similarity index 98% rename from cmd/db_status.go rename to cmd/grype/cli/legacy/db_status.go index 891c6c08e27..4ed655f67e9 100644 --- a/cmd/db_status.go +++ b/cmd/grype/cli/legacy/db_status.go @@ -1,4 +1,4 @@ -package cmd +package legacy import ( "fmt" diff --git a/cmd/db_update.go b/cmd/grype/cli/legacy/db_update.go similarity index 66% rename from cmd/db_update.go rename to cmd/grype/cli/legacy/db_update.go index d697d68f6e5..1a72ce6dacf 100644 --- a/cmd/db_update.go +++ b/cmd/grype/cli/legacy/db_update.go @@ -1,16 +1,13 @@ -package cmd +package legacy import ( "fmt" "github.com/spf13/cobra" - "github.com/wagoodman/go-partybus" + "github.com/anchore/grype/cmd/grype/internal/ui" "github.com/anchore/grype/grype/db" - "github.com/anchore/grype/grype/event" "github.com/anchore/grype/internal/bus" - "github.com/anchore/grype/internal/log" - "github.com/anchore/grype/internal/ui" "github.com/anchore/stereoscope" ) @@ -29,6 +26,8 @@ func startDBUpdateCmd() <-chan error { errs := make(chan error) go func() { defer close(errs) + defer bus.Exit() + dbCurator, err := db.NewCurator(appConfig.DB.ToCuratorConfig()) if err != nil { errs <- err @@ -44,29 +43,17 @@ func startDBUpdateCmd() <-chan error { result = "Vulnerability database updated to latest version!\n" } - bus.Publish(partybus.Event{ - Type: event.NonRootCommandFinished, - Value: result, - }) + bus.Report(result) }() return errs } func runDBUpdateCmd(_ *cobra.Command, _ []string) error { - reporter, closer, err := reportWriter() - defer func() { - if err := closer(); err != nil { - log.Warnf("unable to write to report destination: %+v", err) - } - }() - if err != nil { - return err - } return eventLoop( startDBUpdateCmd(), setupSignals(), eventSubscription, stereoscope.Cleanup, - ui.Select(isVerbose(), appConfig.Quiet, reporter)..., + ui.Select(isVerbose(), appConfig.Quiet)..., ) } diff --git a/cmd/event_loop.go b/cmd/grype/cli/legacy/event_loop.go similarity index 75% rename from cmd/event_loop.go rename to cmd/grype/cli/legacy/event_loop.go index 596f8b52c2a..ce24d2519a6 100644 --- a/cmd/event_loop.go +++ b/cmd/grype/cli/legacy/event_loop.go @@ -1,4 +1,4 @@ -package cmd +package legacy import ( "errors" @@ -8,20 +8,21 @@ import ( "github.com/hashicorp/go-multierror" "github.com/wagoodman/go-partybus" + "github.com/anchore/clio" + "github.com/anchore/grype/grype/grypeerr" "github.com/anchore/grype/internal/log" - "github.com/anchore/grype/internal/ui" ) // eventLoop listens to worker errors (from execution path), worker events (from a partybus subscription), and // signal interrupts. Is responsible for handling each event relative to a given UI an to coordinate eventing until // an eventual graceful exit. -func eventLoop(workerErrs <-chan error, signals <-chan os.Signal, subscription *partybus.Subscription, cleanupFn func(), uxs ...ui.UI) error { +func eventLoop(workerErrs <-chan error, signals <-chan os.Signal, subscription *partybus.Subscription, cleanupFn func(), uxs ...clio.UI) error { //nolint:gocognit defer cleanupFn() events := subscription.Events() var err error - var ux ui.UI + var ux clio.UI - if ux, err = setupUI(subscription.Unsubscribe, uxs...); err != nil { + if ux, err = setupUI(subscription, uxs...); err != nil { return err } @@ -39,12 +40,15 @@ func eventLoop(workerErrs <-chan error, signals <-chan os.Signal, subscription * continue } if err != nil { - // capture the error from the worker and unsubscribe to complete a graceful shutdown + // if the error is not a severity threshold error, then it is unexpected and we should start to tear down the UI + if !errors.Is(err, grypeerr.ErrAboveSeverityThreshold) { + // capture the error from the worker and unsubscribe to complete a graceful shutdown + _ = subscription.Unsubscribe() + // the worker has exited, we may have been mid-handling events for the UI which should now be + // ignored, in which case forcing a teardown of the UI irregardless of the state is required. + forceTeardown = true + } retErr = multierror.Append(retErr, err) - _ = subscription.Unsubscribe() - // the worker has exited, we may have been mid-handling events for the UI which should now be - // ignored, in which case forcing a teardown of the UI irregardless of the state is required. - forceTeardown = true } case e, isOpen := <-events: if !isOpen { @@ -85,9 +89,9 @@ func eventLoop(workerErrs <-chan error, signals <-chan os.Signal, subscription * // during teardown. With the given UIs, the first UI which the ui.Setup() function does not return an error // will be utilized in execution. Providing a set of UIs allows for the caller to provide graceful fallbacks // when there are environmental problem (e.g. unable to setup a TUI with the current TTY). -func setupUI(unsubscribe func() error, uis ...ui.UI) (ui.UI, error) { +func setupUI(subscription *partybus.Subscription, uis ...clio.UI) (clio.UI, error) { for _, ux := range uis { - if err := ux.Setup(unsubscribe); err != nil { + if err := ux.Setup(subscription); err != nil { log.Warnf("unable to setup given UI, falling back to alternative UI: %+v", err) continue } diff --git a/cmd/event_loop_test.go b/cmd/grype/cli/legacy/event_loop_test.go similarity index 91% rename from cmd/event_loop_test.go rename to cmd/grype/cli/legacy/event_loop_test.go index c860e433ae0..489065d3761 100644 --- a/cmd/event_loop_test.go +++ b/cmd/grype/cli/legacy/event_loop_test.go @@ -1,4 +1,4 @@ -package cmd +package legacy import ( "fmt" @@ -11,39 +11,42 @@ import ( "github.com/stretchr/testify/mock" "github.com/wagoodman/go-partybus" + "github.com/anchore/clio" "github.com/anchore/grype/grype/event" - "github.com/anchore/grype/internal/ui" ) -var _ ui.UI = (*uiMock)(nil) +var _ clio.UI = (*uiMock)(nil) type uiMock struct { - t *testing.T - finalEvent partybus.Event - unsubscribe func() error + t *testing.T + finalEvent partybus.Event + subscription partybus.Unsubscribable mock.Mock } -func (u *uiMock) Setup(unsubscribe func() error) error { +func (u *uiMock) Setup(unsubscribe partybus.Unsubscribable) error { + u.t.Helper() u.t.Logf("UI Setup called") - u.unsubscribe = unsubscribe - return u.Called(unsubscribe).Error(0) + u.subscription = unsubscribe + return u.Called(unsubscribe.Unsubscribe).Error(0) } func (u *uiMock) Handle(event partybus.Event) error { + u.t.Helper() u.t.Logf("UI Handle called: %+v", event.Type) if event == u.finalEvent { - assert.NoError(u.t, u.unsubscribe()) + assert.NoError(u.t, u.subscription.Unsubscribe()) } return u.Called(event).Error(0) } func (u *uiMock) Teardown(_ bool) error { + u.t.Helper() u.t.Logf("UI Teardown called") return u.Called().Error(0) } -func Test_eventLoop_gracefulExit(t *testing.T) { +func Test_EventLoop_gracefulExit(t *testing.T) { test := func(t *testing.T) { testBus := partybus.NewBus() @@ -51,7 +54,7 @@ func Test_eventLoop_gracefulExit(t *testing.T) { t.Cleanup(testBus.Close) finalEvent := partybus.Event{ - Type: event.VulnerabilityScanningFinished, + Type: event.CLIExit, } worker := func() <-chan error { @@ -110,7 +113,7 @@ func Test_eventLoop_gracefulExit(t *testing.T) { testWithTimeout(t, 5*time.Second, test) } -func Test_eventLoop_workerError(t *testing.T) { +func Test_EventLoop_workerError(t *testing.T) { test := func(t *testing.T) { testBus := partybus.NewBus() @@ -175,7 +178,7 @@ func Test_eventLoop_workerError(t *testing.T) { testWithTimeout(t, 5*time.Second, test) } -func Test_eventLoop_unsubscribeError(t *testing.T) { +func Test_EventLoop_unsubscribeError(t *testing.T) { test := func(t *testing.T) { testBus := partybus.NewBus() @@ -183,7 +186,7 @@ func Test_eventLoop_unsubscribeError(t *testing.T) { t.Cleanup(testBus.Close) finalEvent := partybus.Event{ - Type: event.VulnerabilityScanningFinished, + Type: event.CLIExit, } worker := func() <-chan error { @@ -244,7 +247,7 @@ func Test_eventLoop_unsubscribeError(t *testing.T) { testWithTimeout(t, 5*time.Second, test) } -func Test_eventLoop_handlerError(t *testing.T) { +func Test_EventLoop_handlerError(t *testing.T) { test := func(t *testing.T) { testBus := partybus.NewBus() @@ -252,8 +255,8 @@ func Test_eventLoop_handlerError(t *testing.T) { t.Cleanup(testBus.Close) finalEvent := partybus.Event{ - Type: event.VulnerabilityScanningFinished, - Error: fmt.Errorf("unable to create presenter"), + Type: event.CLIExit, + Error: fmt.Errorf("an exit error occured"), } worker := func() <-chan error { @@ -316,7 +319,7 @@ func Test_eventLoop_handlerError(t *testing.T) { testWithTimeout(t, 5*time.Second, test) } -func Test_eventLoop_signalsStopExecution(t *testing.T) { +func Test_EventLoop_signalsStopExecution(t *testing.T) { test := func(t *testing.T) { testBus := partybus.NewBus() @@ -369,7 +372,7 @@ func Test_eventLoop_signalsStopExecution(t *testing.T) { testWithTimeout(t, 5*time.Second, test) } -func Test_eventLoop_uiTeardownError(t *testing.T) { +func Test_EventLoop_uiTeardownError(t *testing.T) { test := func(t *testing.T) { testBus := partybus.NewBus() @@ -377,7 +380,7 @@ func Test_eventLoop_uiTeardownError(t *testing.T) { t.Cleanup(testBus.Close) finalEvent := partybus.Event{ - Type: event.VulnerabilityScanningFinished, + Type: event.CLIExit, } worker := func() <-chan error { diff --git a/cmd/root.go b/cmd/grype/cli/legacy/root.go similarity index 93% rename from cmd/root.go rename to cmd/grype/cli/legacy/root.go index c93dba75d60..0fb0b6a18e5 100644 --- a/cmd/root.go +++ b/cmd/grype/cli/legacy/root.go @@ -1,4 +1,4 @@ -package cmd +package legacy import ( "errors" @@ -13,6 +13,7 @@ import ( "github.com/spf13/viper" "github.com/wagoodman/go-partybus" + "github.com/anchore/grype/cmd/grype/internal/ui" "github.com/anchore/grype/grype" "github.com/anchore/grype/grype/db" grypeDb "github.com/anchore/grype/grype/db/v5" @@ -28,7 +29,6 @@ import ( "github.com/anchore/grype/grype/matcher/ruby" "github.com/anchore/grype/grype/matcher/stock" "github.com/anchore/grype/grype/pkg" - "github.com/anchore/grype/grype/presenter" "github.com/anchore/grype/grype/presenter/models" "github.com/anchore/grype/grype/store" "github.com/anchore/grype/grype/vulnerability" @@ -37,7 +37,7 @@ import ( "github.com/anchore/grype/internal/config" "github.com/anchore/grype/internal/format" "github.com/anchore/grype/internal/log" - "github.com/anchore/grype/internal/ui" + "github.com/anchore/grype/internal/stringutil" "github.com/anchore/grype/internal/version" "github.com/anchore/stereoscope" "github.com/anchore/syft/syft/linux" @@ -62,7 +62,7 @@ var ( rootCmd = &cobra.Command{ Use: fmt.Sprintf("%s [IMAGE]", internal.ApplicationName), Short: "A vulnerability scanner for container images, filesystems, and SBOMs", - Long: format.Tprintf(`A vulnerability scanner for container images, filesystems, and SBOMs. + Long: stringutil.Tprintf(`A vulnerability scanner for container images, filesystems, and SBOMs. Supports the following image sources: {{.appName}} yourrepo/yourimage:tag defaults to using images from a Docker daemon @@ -130,14 +130,14 @@ func setRootFlags(flags *pflag.FlagSet) { fmt.Sprintf("selection of layers to analyze, options=%v", source.AllScopes), ) - flags.StringP( - "output", "o", "", - fmt.Sprintf("report output formatter, formats=%v, deprecated formats=%v", presenter.AvailableFormats, presenter.DeprecatedFormats), + flags.StringArrayP( + "output", "o", nil, + fmt.Sprintf("report output formatter, formats=%v, deprecated formats=%v", format.AvailableFormats, format.DeprecatedFormats), ) flags.StringP( "file", "", "", - "file to write the report output to (default is STDOUT)", + "file to write the default report output to (default is STDOUT)", ) flags.StringP( @@ -262,23 +262,12 @@ func rootExec(_ *cobra.Command, args []string) error { userInput = args[0] } - reporter, closer, err := reportWriter() - defer func() { - if err := closer(); err != nil { - log.Warnf("unable to write to report destination: %+v", err) - } - }() - - if err != nil { - return err - } - return eventLoop( startWorker(userInput, appConfig.FailOnSeverity), setupSignals(), eventSubscription, stereoscope.Cleanup, - ui.Select(isVerbose(), appConfig.Quiet, reporter)..., + ui.Select(isVerbose(), appConfig.Quiet)..., ) } @@ -298,8 +287,12 @@ func startWorker(userInput string, failOnSeverity *vulnerability.Severity) <-cha errs := make(chan error) go func() { defer close(errs) + defer bus.Exit() - presenterConfig, err := presenter.ValidatedConfig(appConfig.Output, appConfig.OutputTemplateFile, appConfig.ShowSuppressed) + writer, err := format.MakeScanResultWriter(appConfig.Outputs, appConfig.File, format.PresentationConfig{ + TemplateFilePath: appConfig.OutputTemplateFile, + ShowSuppressed: appConfig.ShowSuppressed, + }) if err != nil { errs <- err return @@ -332,7 +325,7 @@ func startWorker(userInput string, failOnSeverity *vulnerability.Severity) <-cha go func() { defer wg.Done() log.Debugf("gathering packages") - // packages are grype.Pacakge, not syft.Package + // packages are grype.Package, not syft.Package // the SBOM is returned for downstream formatting concerns // grype uses the SBOM in combination with syft formatters to produce cycloneDX // with vulnerability information appended @@ -379,7 +372,7 @@ func startWorker(userInput string, failOnSeverity *vulnerability.Severity) <-cha } } - pb := models.PresenterConfig{ + if err := writer.Write(models.PresenterConfig{ Matches: *remainingMatches, IgnoredMatches: ignoredMatches, Packages: packages, @@ -388,12 +381,9 @@ func startWorker(userInput string, failOnSeverity *vulnerability.Severity) <-cha SBOM: sbom, AppConfig: appConfig, DBStatus: status, + }); err != nil { + errs <- err } - - bus.Publish(partybus.Event{ - Type: event.VulnerabilityScanningFinished, - Value: presenter.GetPresenter(presenterConfig, pb), - }) }() return errs } @@ -447,7 +437,7 @@ func checkForAppUpdate() { log.Infof("new version of %s is available: %s (currently running: %s)", internal.ApplicationName, newVersion, version.FromBuild().Version) bus.Publish(partybus.Event{ - Type: event.AppUpdateAvailable, + Type: event.CLIAppUpdateAvailable, Value: newVersion, }) } else { diff --git a/cmd/root_test.go b/cmd/grype/cli/legacy/root_test.go similarity index 98% rename from cmd/root_test.go rename to cmd/grype/cli/legacy/root_test.go index 70f85c159a6..75b0433fa72 100644 --- a/cmd/root_test.go +++ b/cmd/grype/cli/legacy/root_test.go @@ -1,4 +1,4 @@ -package cmd +package legacy import ( "testing" diff --git a/cmd/signals.go b/cmd/grype/cli/legacy/signals.go similarity index 95% rename from cmd/signals.go rename to cmd/grype/cli/legacy/signals.go index f20379d1eeb..e70133231c8 100644 --- a/cmd/signals.go +++ b/cmd/grype/cli/legacy/signals.go @@ -1,4 +1,4 @@ -package cmd +package legacy import ( "os" diff --git a/cmd/util.go b/cmd/grype/cli/legacy/util.go similarity index 93% rename from cmd/util.go rename to cmd/grype/cli/legacy/util.go index e9fa6bc3ff8..da07f95a858 100644 --- a/cmd/util.go +++ b/cmd/grype/cli/legacy/util.go @@ -1,4 +1,4 @@ -package cmd +package legacy import ( "fmt" diff --git a/cmd/version.go b/cmd/grype/cli/legacy/version.go similarity index 99% rename from cmd/version.go rename to cmd/grype/cli/legacy/version.go index fcdd3379b3a..ce58d1618a0 100644 --- a/cmd/version.go +++ b/cmd/grype/cli/legacy/version.go @@ -1,4 +1,4 @@ -package cmd +package legacy import ( "encoding/json" diff --git a/cmd/grype/cli/ui/__snapshots__/handle_database_diff_started_test.snap b/cmd/grype/cli/ui/__snapshots__/handle_database_diff_started_test.snap new file mode 100755 index 00000000000..9386bb14a93 --- /dev/null +++ b/cmd/grype/cli/ui/__snapshots__/handle_database_diff_started_test.snap @@ -0,0 +1,8 @@ + +[TestHandler_handleDatabaseDiffStarted/DB_diff_started - 1] + ⠋ Comparing Vulnerability DBs ━━━━━━━━━━━━━━━━━━━━ [current] +--- + +[TestHandler_handleDatabaseDiffStarted/DB_diff_complete - 1] + ✔ Compared Vulnerability DBs [20 differences found] +--- diff --git a/cmd/grype/cli/ui/__snapshots__/handle_update_vulnerability_database_test.snap b/cmd/grype/cli/ui/__snapshots__/handle_update_vulnerability_database_test.snap new file mode 100755 index 00000000000..04810b863cc --- /dev/null +++ b/cmd/grype/cli/ui/__snapshots__/handle_update_vulnerability_database_test.snap @@ -0,0 +1,8 @@ + +[TestHandler_handleUpdateVulnerabilityDatabase/downloading_DB - 1] + ⠋ Vulnerability DB ━━━━━━━━━━━━━━━━━━━━ [current] +--- + +[TestHandler_handleUpdateVulnerabilityDatabase/DB_download_complete - 1] + ✔ Vulnerability DB [current] +--- diff --git a/cmd/grype/cli/ui/__snapshots__/handle_vulnerability_scanning_started_test.snap b/cmd/grype/cli/ui/__snapshots__/handle_vulnerability_scanning_started_test.snap new file mode 100755 index 00000000000..6a0244c4192 --- /dev/null +++ b/cmd/grype/cli/ui/__snapshots__/handle_vulnerability_scanning_started_test.snap @@ -0,0 +1,18 @@ + +[TestHandler_handleVulnerabilityScanningStarted/vulnerability_scanning_in_progress/task_line - 1] + ⠋ Scanning for vulnerabilities [20 vulnerabilities] +--- + +[TestHandler_handleVulnerabilityScanningStarted/vulnerability_scanning_in_progress/tree - 1] + ├── 1 critical, 2 high, 3 medium, 4 low, 5 negligible (6 unknown) + └── 30 fixed +--- + +[TestHandler_handleVulnerabilityScanningStarted/vulnerability_scanning_complete/task_line - 1] + ✔ Scanned for vulnerabilities [25 vulnerabilities] +--- + +[TestHandler_handleVulnerabilityScanningStarted/vulnerability_scanning_complete/tree - 1] + ├── 1 critical, 2 high, 3 medium, 4 low, 5 negligible (6 unknown) + └── 35 fixed +--- diff --git a/cmd/grype/cli/ui/handle_database_diff_started.go b/cmd/grype/cli/ui/handle_database_diff_started.go new file mode 100644 index 00000000000..c8dbecd4a3e --- /dev/null +++ b/cmd/grype/cli/ui/handle_database_diff_started.go @@ -0,0 +1,58 @@ +package ui + +import ( + "fmt" + + tea "github.com/charmbracelet/bubbletea" + "github.com/wagoodman/go-partybus" + "github.com/wagoodman/go-progress" + + "github.com/anchore/bubbly/bubbles/taskprogress" + "github.com/anchore/grype/grype/event/monitor" + "github.com/anchore/grype/grype/event/parsers" + "github.com/anchore/grype/internal/log" +) + +type dbDiffProgressStager struct { + monitor *monitor.DBDiff +} + +func (p dbDiffProgressStager) Stage() string { + if progress.IsErrCompleted(p.monitor.StageProgress.Error()) { + return fmt.Sprintf("%d differences found", p.monitor.DifferencesDiscovered.Current()) + } + return p.monitor.Stager.Stage() +} + +func (p dbDiffProgressStager) Current() int64 { + return p.monitor.StageProgress.Current() +} + +func (p dbDiffProgressStager) Error() error { + return p.monitor.StageProgress.Error() +} + +func (p dbDiffProgressStager) Size() int64 { + return p.monitor.StageProgress.Size() +} + +func (m *Handler) handleDatabaseDiffStarted(e partybus.Event) []tea.Model { + mon, err := parsers.ParseDatabaseDiffingStarted(e) + if err != nil { + log.WithFields("error", err).Warn("unable to parse event") + return nil + } + + tsk := m.newTaskProgress( + taskprogress.Title{ + Default: "Compare Vulnerability DBs", + Running: "Comparing Vulnerability DBs", + Success: "Compared Vulnerability DBs", + }, + taskprogress.WithStagedProgressable(dbDiffProgressStager{monitor: mon}), + ) + + tsk.HideStageOnSuccess = false + + return []tea.Model{tsk} +} diff --git a/cmd/grype/cli/ui/handle_database_diff_started_test.go b/cmd/grype/cli/ui/handle_database_diff_started_test.go new file mode 100644 index 00000000000..b890f791ead --- /dev/null +++ b/cmd/grype/cli/ui/handle_database_diff_started_test.go @@ -0,0 +1,96 @@ +package ui + +import ( + "testing" + "time" + + tea "github.com/charmbracelet/bubbletea" + "github.com/gkampitakis/go-snaps/snaps" + "github.com/stretchr/testify/require" + "github.com/wagoodman/go-partybus" + "github.com/wagoodman/go-progress" + + "github.com/anchore/bubbly/bubbles/taskprogress" + "github.com/anchore/grype/grype/event" + "github.com/anchore/grype/grype/event/monitor" +) + +func TestHandler_handleDatabaseDiffStarted(t *testing.T) { + + tests := []struct { + name string + eventFn func(*testing.T) partybus.Event + iterations int + }{ + { + name: "DB diff started", + eventFn: func(t *testing.T) partybus.Event { + prog := &progress.Manual{} + prog.SetTotal(100) + prog.Set(50) + + diffs := &progress.Manual{} + diffs.Set(20) + + mon := monitor.DBDiff{ + Stager: &progress.Stage{Current: "current"}, + StageProgress: prog, + DifferencesDiscovered: diffs, + } + + return partybus.Event{ + Type: event.DatabaseDiffingStarted, + Value: mon, + } + }, + }, + { + name: "DB diff complete", + eventFn: func(t *testing.T) partybus.Event { + prog := &progress.Manual{} + prog.SetTotal(100) + prog.Set(100) + prog.SetCompleted() + + diffs := &progress.Manual{} + diffs.Set(20) + + mon := monitor.DBDiff{ + Stager: &progress.Stage{Current: "current"}, + StageProgress: prog, + DifferencesDiscovered: diffs, + } + + return partybus.Event{ + Type: event.DatabaseDiffingStarted, + Value: mon, + } + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + e := tt.eventFn(t) + handler := New(DefaultHandlerConfig()) + handler.WindowSize = tea.WindowSizeMsg{ + Width: 100, + Height: 80, + } + + models := handler.Handle(e) + require.Len(t, models, 1) + model := models[0] + + tsk, ok := model.(taskprogress.Model) + require.True(t, ok) + + got := runModel(t, tsk, tt.iterations, taskprogress.TickMsg{ + Time: time.Now(), + Sequence: tsk.Sequence(), + ID: tsk.ID(), + }) + t.Log(got) + snaps.MatchSnapshot(t, got) + }) + } +} diff --git a/cmd/grype/cli/ui/handle_update_vulnerability_database.go b/cmd/grype/cli/ui/handle_update_vulnerability_database.go new file mode 100644 index 00000000000..938ae9e1c07 --- /dev/null +++ b/cmd/grype/cli/ui/handle_update_vulnerability_database.go @@ -0,0 +1,53 @@ +package ui + +import ( + "fmt" + + tea "github.com/charmbracelet/bubbletea" + "github.com/dustin/go-humanize" + "github.com/wagoodman/go-partybus" + "github.com/wagoodman/go-progress" + + "github.com/anchore/bubbly/bubbles/taskprogress" + "github.com/anchore/grype/grype/event/parsers" + "github.com/anchore/grype/internal/log" +) + +type dbDownloadProgressStager struct { + prog progress.StagedProgressable +} + +func (s dbDownloadProgressStager) Stage() string { + stage := s.prog.Stage() + if stage == "downloading" { + // note: since validation is baked into the download progress there is no visibility into this stage. + // for that reason we report "validating" on the last byte being downloaded (which tends to be the longest + // since go-downloader is doing this work). + if s.prog.Current() >= s.prog.Size()-1 { + return "validating" + } + // show intermediate progress of the download + return fmt.Sprintf("%s / %s", humanize.Bytes(uint64(s.prog.Current())), humanize.Bytes(uint64(s.prog.Size()))) + } + return stage +} + +func (m *Handler) handleUpdateVulnerabilityDatabase(e partybus.Event) []tea.Model { + prog, err := parsers.ParseUpdateVulnerabilityDatabase(e) + if err != nil { + log.WithFields("error", err).Warn("unable to parse event") + return nil + } + + tsk := m.newTaskProgress( + taskprogress.Title{ + Default: "Vulnerability DB", + }, + taskprogress.WithStagedProgressable(prog), // ignore the static stage provided by the event + taskprogress.WithStager(dbDownloadProgressStager{prog: prog}), + ) + + tsk.HideStageOnSuccess = false + + return []tea.Model{tsk} +} diff --git a/cmd/grype/cli/ui/handle_update_vulnerability_database_test.go b/cmd/grype/cli/ui/handle_update_vulnerability_database_test.go new file mode 100644 index 00000000000..c3b64e1585f --- /dev/null +++ b/cmd/grype/cli/ui/handle_update_vulnerability_database_test.go @@ -0,0 +1,97 @@ +package ui + +import ( + "testing" + "time" + + tea "github.com/charmbracelet/bubbletea" + "github.com/gkampitakis/go-snaps/snaps" + "github.com/stretchr/testify/require" + "github.com/wagoodman/go-partybus" + "github.com/wagoodman/go-progress" + + "github.com/anchore/bubbly/bubbles/taskprogress" + "github.com/anchore/grype/grype/event" +) + +func TestHandler_handleUpdateVulnerabilityDatabase(t *testing.T) { + + tests := []struct { + name string + eventFn func(*testing.T) partybus.Event + iterations int + }{ + { + name: "downloading DB", + eventFn: func(t *testing.T) partybus.Event { + prog := &progress.Manual{} + prog.SetTotal(100) + prog.Set(50) + + mon := struct { + progress.Progressable + progress.Stager + }{ + Progressable: prog, + Stager: &progress.Stage{ + Current: "current", + }, + } + + return partybus.Event{ + Type: event.UpdateVulnerabilityDatabase, + Value: mon, + } + }, + }, + { + name: "DB download complete", + eventFn: func(t *testing.T) partybus.Event { + prog := &progress.Manual{} + prog.SetTotal(100) + prog.Set(100) + prog.SetCompleted() + + mon := struct { + progress.Progressable + progress.Stager + }{ + Progressable: prog, + Stager: &progress.Stage{ + Current: "current", + }, + } + + return partybus.Event{ + Type: event.UpdateVulnerabilityDatabase, + Value: mon, + } + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + e := tt.eventFn(t) + handler := New(DefaultHandlerConfig()) + handler.WindowSize = tea.WindowSizeMsg{ + Width: 100, + Height: 80, + } + + models := handler.Handle(e) + require.Len(t, models, 1) + model := models[0] + + tsk, ok := model.(taskprogress.Model) + require.True(t, ok) + + got := runModel(t, tsk, tt.iterations, taskprogress.TickMsg{ + Time: time.Now(), + Sequence: tsk.Sequence(), + ID: tsk.ID(), + }) + t.Log(got) + snaps.MatchSnapshot(t, got) + }) + } +} diff --git a/cmd/grype/cli/ui/handle_vulnerability_scanning_started.go b/cmd/grype/cli/ui/handle_vulnerability_scanning_started.go new file mode 100644 index 00000000000..54ef042bcb8 --- /dev/null +++ b/cmd/grype/cli/ui/handle_vulnerability_scanning_started.go @@ -0,0 +1,204 @@ +package ui + +import ( + "fmt" + "sort" + "strings" + "time" + + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/wagoodman/go-partybus" + "github.com/wagoodman/go-progress" + + "github.com/anchore/bubbly/bubbles/taskprogress" + "github.com/anchore/grype/grype/event/monitor" + "github.com/anchore/grype/grype/event/parsers" + "github.com/anchore/grype/grype/vulnerability" + "github.com/anchore/grype/internal/log" +) + +const ( + branch = "├──" + end = "└──" +) + +var _ progress.StagedProgressable = (*vulnerabilityScanningAdapter)(nil) + +type vulnerabilityProgressTree struct { + mon *monitor.Matching + windowSize tea.WindowSizeMsg + + countBySeverity map[vulnerability.Severity]int64 + unknownCount int64 + fixedCount int64 + severities []vulnerability.Severity + + id uint32 + sequence int + + updateDuration time.Duration + textStyle lipgloss.Style +} + +func newVulnerabilityProgressTree(monitor *monitor.Matching, textStyle lipgloss.Style) vulnerabilityProgressTree { + allSeverities := vulnerability.AllSeverities() + sort.Sort(sort.Reverse(vulnerability.Severities(allSeverities))) + + return vulnerabilityProgressTree{ + mon: monitor, + countBySeverity: make(map[vulnerability.Severity]int64), + severities: allSeverities, + textStyle: textStyle, + } +} + +// vulnerabilityProgressTreeTickMsg indicates that the timer has ticked and we should render a frame. +type vulnerabilityProgressTreeTickMsg struct { + Time time.Time + Sequence int + ID uint32 +} + +type vulnerabilityScanningAdapter struct { + mon *monitor.Matching +} + +func (p vulnerabilityScanningAdapter) Current() int64 { + return p.mon.VulnerabilitiesDiscovered.Current() +} + +func (p vulnerabilityScanningAdapter) Error() error { + return p.mon.VulnerabilitiesDiscovered.Error() +} + +func (p vulnerabilityScanningAdapter) Size() int64 { + return -1 +} + +func (p vulnerabilityScanningAdapter) Stage() string { + return fmt.Sprintf("%d vulnerabilities", p.mon.VulnerabilitiesDiscovered.Current()) +} + +func (m *Handler) handleVulnerabilityScanningStarted(e partybus.Event) []tea.Model { + mon, err := parsers.ParseVulnerabilityScanningStarted(e) + if err != nil { + log.WithFields("error", err).Warn("unable to parse event") + return nil + } + + tsk := m.newTaskProgress( + taskprogress.Title{ + Default: "Scan for vulnerabilities", + Running: "Scanning for vulnerabilities", + Success: "Scanned for vulnerabilities", + }, + taskprogress.WithStagedProgressable(vulnerabilityScanningAdapter{mon: mon}), + ) + + tsk.HideStageOnSuccess = false + + textStyle := tsk.HintStyle + + return []tea.Model{ + tsk, + newVulnerabilityProgressTree(mon, textStyle), + } +} + +func (l vulnerabilityProgressTree) Init() tea.Cmd { + // this is the periodic update of state information + return func() tea.Msg { + return vulnerabilityProgressTreeTickMsg{ + // The time at which the tick occurred. + Time: time.Now(), + + // The ID of the log frame that this message belongs to. This can be + // helpful when routing messages, however bear in mind that log frames + // will ignore messages that don't contain ID by default. + ID: l.id, + + Sequence: l.sequence, + } + } +} + +func (l vulnerabilityProgressTree) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.WindowSizeMsg: + l.windowSize = msg + return l, nil + + case vulnerabilityProgressTreeTickMsg: + // update the model + l.fixedCount = l.mon.Fixed.Current() + l.unknownCount = l.mon.BySeverity[vulnerability.UnknownSeverity].Current() + for _, sev := range l.severities { + l.countBySeverity[sev] = l.mon.BySeverity[sev].Current() + } + + // kick off the next tick + tickCmd := l.handleTick(msg) + + return l, tickCmd + } + + return l, nil +} + +func (l vulnerabilityProgressTree) View() string { + sb := strings.Builder{} + + for idx, sev := range l.severities { + count := l.countBySeverity[sev] + sb.WriteString(fmt.Sprintf("%d %s", count, sev)) + if idx < len(l.severities)-1 { + sb.WriteString(", ") + } + } + if l.unknownCount > 0 { + unknownStr := fmt.Sprintf(" (%d unknown)", l.unknownCount) + sb.WriteString(unknownStr) + } + + status := sb.String() + sb.Reset() + + sevStr := l.textStyle.Render(fmt.Sprintf(" %s %s", branch, status)) + fixedStr := l.textStyle.Render(fmt.Sprintf(" %s %d fixed", end, l.fixedCount)) + + sb.WriteString(sevStr) + sb.WriteString("\n") + sb.WriteString(fixedStr) + + return sb.String() +} + +func (l vulnerabilityProgressTree) queueNextTick() tea.Cmd { + return tea.Tick(l.updateDuration, func(t time.Time) tea.Msg { + return vulnerabilityProgressTreeTickMsg{ + Time: t, + ID: l.id, + Sequence: l.sequence, + } + }) +} + +func (l *vulnerabilityProgressTree) handleTick(msg vulnerabilityProgressTreeTickMsg) tea.Cmd { + // If an ID is set, and the ID doesn't belong to this log frame, reject the message. + if msg.ID > 0 && msg.ID != l.id { + return nil + } + + // If a sequence is set, and it's not the one we expect, reject the message. + // This prevents the log frame from receiving too many messages and + // thus updating too frequently. + if msg.Sequence > 0 && msg.Sequence != l.sequence { + return nil + } + + l.sequence++ + + // note: even if the log is completed we should still respond to stage changes and window size events + return l.queueNextTick() +} diff --git a/cmd/grype/cli/ui/handle_vulnerability_scanning_started_test.go b/cmd/grype/cli/ui/handle_vulnerability_scanning_started_test.go new file mode 100644 index 00000000000..ab23fd7d4fd --- /dev/null +++ b/cmd/grype/cli/ui/handle_vulnerability_scanning_started_test.go @@ -0,0 +1,145 @@ +package ui + +import ( + "sort" + "testing" + "time" + + tea "github.com/charmbracelet/bubbletea" + "github.com/gkampitakis/go-snaps/snaps" + "github.com/stretchr/testify/require" + "github.com/wagoodman/go-partybus" + "github.com/wagoodman/go-progress" + + "github.com/anchore/bubbly/bubbles/taskprogress" + "github.com/anchore/grype/grype/event" + "github.com/anchore/grype/grype/event/monitor" + "github.com/anchore/grype/grype/vulnerability" +) + +func TestHandler_handleVulnerabilityScanningStarted(t *testing.T) { + tests := []struct { + name string + eventFn func(*testing.T) partybus.Event + iterations int + }{ + { + name: "vulnerability scanning in progress", + eventFn: func(t *testing.T) partybus.Event { + return partybus.Event{ + Type: event.VulnerabilityScanningStarted, + Value: getVulnerabilityMonitor(false), + } + }, + }, + { + name: "vulnerability scanning complete", + eventFn: func(t *testing.T) partybus.Event { + return partybus.Event{ + Type: event.VulnerabilityScanningStarted, + Value: getVulnerabilityMonitor(true), + } + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + e := tt.eventFn(t) + handler := New(DefaultHandlerConfig()) + handler.WindowSize = tea.WindowSizeMsg{ + Width: 100, + Height: 80, + } + + models := handler.Handle(e) + require.Len(t, models, 2) + + t.Run("task line", func(t *testing.T) { + tsk, ok := models[0].(taskprogress.Model) + require.True(t, ok) + + got := runModel(t, tsk, tt.iterations, taskprogress.TickMsg{ + Time: time.Now(), + Sequence: tsk.Sequence(), + ID: tsk.ID(), + }) + t.Log(got) + snaps.MatchSnapshot(t, got) + }) + + t.Run("tree", func(t *testing.T) { + log, ok := models[1].(vulnerabilityProgressTree) + require.True(t, ok) + got := runModel(t, log, tt.iterations, vulnerabilityProgressTreeTickMsg{ + Time: time.Now(), + Sequence: log.sequence, + ID: log.id, + }) + t.Log(got) + snaps.MatchSnapshot(t, got) + }) + + }) + } +} + +func getVulnerabilityMonitor(completed bool) monitor.Matching { + pkgs := &progress.Manual{} + pkgs.SetTotal(-1) + if completed { + pkgs.Set(2000) + pkgs.SetCompleted() + } else { + pkgs.Set(300) + } + + vulns := &progress.Manual{} + vulns.SetTotal(-1) + if completed { + vulns.Set(25) + vulns.SetCompleted() + } else { + vulns.Set(20) + } + + fixed := &progress.Manual{} + fixed.SetTotal(-1) + if completed { + fixed.Set(35) + fixed.SetCompleted() + } else { + fixed.Set(30) + } + + bySeverityWriter := map[vulnerability.Severity]*progress.Manual{ + vulnerability.CriticalSeverity: {}, + vulnerability.HighSeverity: {}, + vulnerability.MediumSeverity: {}, + vulnerability.LowSeverity: {}, + vulnerability.NegligibleSeverity: {}, + vulnerability.UnknownSeverity: {}, + } + + allSeverities := vulnerability.AllSeverities() + sort.Sort(sort.Reverse(vulnerability.Severities(allSeverities))) + + var count int64 = 1 + for _, sev := range allSeverities { + bySeverityWriter[sev].Add(count) + count++ + } + bySeverityWriter[vulnerability.UnknownSeverity].Add(count) + + bySeverity := map[vulnerability.Severity]progress.Monitorable{} + + for k, v := range bySeverityWriter { + bySeverity[k] = v + } + + return monitor.Matching{ + PackagesProcessed: pkgs, + VulnerabilitiesDiscovered: vulns, + Fixed: fixed, + BySeverity: bySeverity, + } +} diff --git a/cmd/grype/cli/ui/handler.go b/cmd/grype/cli/ui/handler.go new file mode 100644 index 00000000000..4232191862a --- /dev/null +++ b/cmd/grype/cli/ui/handler.go @@ -0,0 +1,66 @@ +package ui + +import ( + "sync" + + tea "github.com/charmbracelet/bubbletea" + "github.com/wagoodman/go-partybus" + + "github.com/anchore/bubbly" + "github.com/anchore/bubbly/bubbles/taskprogress" + "github.com/anchore/grype/grype/event" +) + +var _ interface { + bubbly.EventHandler + bubbly.MessageListener + bubbly.HandleWaiter +} = (*Handler)(nil) + +type HandlerConfig struct { + TitleWidth int + AdjustDefaultTask func(taskprogress.Model) taskprogress.Model +} + +type Handler struct { + WindowSize tea.WindowSizeMsg + Running *sync.WaitGroup + Config HandlerConfig + + bubbly.EventHandler +} + +func DefaultHandlerConfig() HandlerConfig { + return HandlerConfig{ + TitleWidth: 30, + } +} + +func New(cfg HandlerConfig) *Handler { + d := bubbly.NewEventDispatcher() + + h := &Handler{ + EventHandler: d, + Running: &sync.WaitGroup{}, + Config: cfg, + } + + // register all supported event types with the respective handler functions + d.AddHandlers(map[partybus.EventType]bubbly.EventHandlerFn{ + event.UpdateVulnerabilityDatabase: h.handleUpdateVulnerabilityDatabase, + event.VulnerabilityScanningStarted: h.handleVulnerabilityScanningStarted, + event.DatabaseDiffingStarted: h.handleDatabaseDiffStarted, + }) + + return h +} + +func (m *Handler) OnMessage(msg tea.Msg) { + if msg, ok := msg.(tea.WindowSizeMsg); ok { + m.WindowSize = msg + } +} + +func (m *Handler) Wait() { + m.Running.Wait() +} diff --git a/cmd/grype/cli/ui/new_task_progress.go b/cmd/grype/cli/ui/new_task_progress.go new file mode 100644 index 00000000000..036f7b37de9 --- /dev/null +++ b/cmd/grype/cli/ui/new_task_progress.go @@ -0,0 +1,19 @@ +package ui + +import "github.com/anchore/bubbly/bubbles/taskprogress" + +func (m Handler) newTaskProgress(title taskprogress.Title, opts ...taskprogress.Option) taskprogress.Model { + tsk := taskprogress.New(m.Running, opts...) + + tsk.HideProgressOnSuccess = true + tsk.HideStageOnSuccess = true + tsk.WindowSize = m.WindowSize + tsk.TitleWidth = m.Config.TitleWidth + tsk.TitleOptions = title + + if m.Config.AdjustDefaultTask != nil { + tsk = m.Config.AdjustDefaultTask(tsk) + } + + return tsk +} diff --git a/cmd/grype/cli/ui/util_test.go b/cmd/grype/cli/ui/util_test.go new file mode 100644 index 00000000000..81c8525dc71 --- /dev/null +++ b/cmd/grype/cli/ui/util_test.go @@ -0,0 +1,69 @@ +package ui + +import ( + "reflect" + "sync" + "testing" + "unsafe" + + tea "github.com/charmbracelet/bubbletea" +) + +func runModel(t testing.TB, m tea.Model, iterations int, message tea.Msg, wgs ...*sync.WaitGroup) string { + t.Helper() + if iterations == 0 { + iterations = 1 + } + m.Init() + var cmd tea.Cmd = func() tea.Msg { + return message + } + + for _, wg := range wgs { + if wg != nil { + wg.Wait() + } + } + + for i := 0; cmd != nil && i < iterations; i++ { + msgs := flatten(cmd()) + var nextCmds []tea.Cmd + var next tea.Cmd + for _, msg := range msgs { + t.Logf("Message: %+v %+v\n", reflect.TypeOf(msg), msg) + m, next = m.Update(msg) + nextCmds = append(nextCmds, next) + } + cmd = tea.Batch(nextCmds...) + } + return m.View() +} + +func flatten(p tea.Msg) (msgs []tea.Msg) { + if reflect.TypeOf(p).Name() == "batchMsg" { + partials := extractBatchMessages(p) + for _, m := range partials { + msgs = append(msgs, flatten(m)...) + } + } else { + msgs = []tea.Msg{p} + } + return msgs +} + +func extractBatchMessages(m tea.Msg) (ret []tea.Msg) { + sliceMsgType := reflect.SliceOf(reflect.TypeOf(tea.Cmd(nil))) + value := reflect.ValueOf(m) // note: this is technically unaddressable + + // make our own instance that is addressable + valueCopy := reflect.New(value.Type()).Elem() + valueCopy.Set(value) + + cmds := reflect.NewAt(sliceMsgType, unsafe.Pointer(valueCopy.UnsafeAddr())).Elem() + for i := 0; i < cmds.Len(); i++ { + item := cmds.Index(i) + r := item.Call(nil) + ret = append(ret, r[0].Interface().(tea.Msg)) + } + return ret +} diff --git a/cmd/grype/internal/ui/__snapshots__/post_ui_event_writer_test.snap b/cmd/grype/internal/ui/__snapshots__/post_ui_event_writer_test.snap new file mode 100755 index 00000000000..05ee3a5a291 --- /dev/null +++ b/cmd/grype/internal/ui/__snapshots__/post_ui_event_writer_test.snap @@ -0,0 +1,46 @@ + +[Test_postUIEventWriter_write/no_events/stdout - 1] + +--- + +[Test_postUIEventWriter_write/no_events/stderr - 1] + +--- + +[Test_postUIEventWriter_write/all_events/stdout - 1] + + + + + +--- + +[Test_postUIEventWriter_write/all_events/stderr - 1] + + + + + + + + + + + + + +--- + +[Test_postUIEventWriter_write/quiet_only_shows_report/stdout - 1] + + +--- + +[Test_postUIEventWriter_write/quiet_only_shows_report/stderr - 1] + +--- \ No newline at end of file diff --git a/cmd/grype/internal/ui/no_ui.go b/cmd/grype/internal/ui/no_ui.go new file mode 100644 index 00000000000..e78489fcf78 --- /dev/null +++ b/cmd/grype/internal/ui/no_ui.go @@ -0,0 +1,44 @@ +package ui + +import ( + "os" + + "github.com/wagoodman/go-partybus" + + "github.com/anchore/clio" + "github.com/anchore/grype/grype/event" +) + +var _ clio.UI = (*NoUI)(nil) + +type NoUI struct { + finalizeEvents []partybus.Event + subscription partybus.Unsubscribable + quiet bool +} + +func None(quiet bool) *NoUI { + return &NoUI{ + quiet: quiet, + } +} + +func (n *NoUI) Setup(subscription partybus.Unsubscribable) error { + n.subscription = subscription + return nil +} + +func (n *NoUI) Handle(e partybus.Event) error { + switch e.Type { + case event.CLIReport, event.CLINotification: + // keep these for when the UI is terminated to show to the screen (or perform other events) + n.finalizeEvents = append(n.finalizeEvents, e) + case event.CLIExit: + return n.subscription.Unsubscribe() + } + return nil +} + +func (n NoUI) Teardown(_ bool) error { + return newPostUIEventWriter(os.Stdout, os.Stderr).write(n.quiet, n.finalizeEvents...) +} diff --git a/cmd/grype/internal/ui/post_ui_event_writer.go b/cmd/grype/internal/ui/post_ui_event_writer.go new file mode 100644 index 00000000000..c008195e1fc --- /dev/null +++ b/cmd/grype/internal/ui/post_ui_event_writer.go @@ -0,0 +1,133 @@ +package ui + +import ( + "fmt" + "io" + "strings" + + "github.com/charmbracelet/lipgloss" + "github.com/hashicorp/go-multierror" + "github.com/wagoodman/go-partybus" + + "github.com/anchore/grype/grype/event" + "github.com/anchore/grype/grype/event/parsers" + "github.com/anchore/grype/internal/log" +) + +type postUIEventWriter struct { + handles []postUIHandle +} + +type postUIHandle struct { + respectQuiet bool + event partybus.EventType + writer io.Writer + dispatch eventWriter +} + +type eventWriter func(io.Writer, ...partybus.Event) error + +func newPostUIEventWriter(stdout, stderr io.Writer) *postUIEventWriter { + return &postUIEventWriter{ + handles: []postUIHandle{ + { + event: event.CLIReport, + respectQuiet: false, + writer: stdout, + dispatch: writeReports, + }, + { + event: event.CLINotification, + respectQuiet: true, + writer: stderr, + dispatch: writeNotifications, + }, + { + event: event.CLIAppUpdateAvailable, + respectQuiet: true, + writer: stderr, + dispatch: writeAppUpdate, + }, + }, + } +} + +func (w postUIEventWriter) write(quiet bool, events ...partybus.Event) error { + var errs error + for _, h := range w.handles { + if quiet && h.respectQuiet { + continue + } + + for _, e := range events { + if e.Type != h.event { + continue + } + + if err := h.dispatch(h.writer, e); err != nil { + errs = multierror.Append(errs, err) + } + } + } + return errs +} + +func writeReports(writer io.Writer, events ...partybus.Event) error { + var reports []string + for _, e := range events { + _, report, err := parsers.ParseCLIReport(e) + if err != nil { + log.WithFields("error", err).Warn("failed to gather final report") + continue + } + + // remove all whitespace padding from the end of the report + reports = append(reports, strings.TrimRight(report, "\n ")+"\n") + } + + // prevent the double new-line at the end of the report + report := strings.Join(reports, "\n") + + if _, err := fmt.Fprint(writer, report); err != nil { + return fmt.Errorf("failed to write final report to stdout: %w", err) + } + return nil +} + +func writeNotifications(writer io.Writer, events ...partybus.Event) error { + // 13 = high intensity magenta (ANSI 16 bit code) + style := lipgloss.NewStyle().Foreground(lipgloss.Color("13")) + + for _, e := range events { + _, notification, err := parsers.ParseCLINotification(e) + if err != nil { + log.WithFields("error", err).Warn("failed to parse notification") + continue + } + + if _, err := fmt.Fprintln(writer, style.Render(notification)); err != nil { + // don't let this be fatal + log.WithFields("error", err).Warn("failed to write final notifications") + } + } + return nil +} + +func writeAppUpdate(writer io.Writer, events ...partybus.Event) error { + // 13 = high intensity magenta (ANSI 16 bit code) + italics + style := lipgloss.NewStyle().Foreground(lipgloss.Color("13")).Italic(true) + + for _, e := range events { + notice, err := parsers.ParseCLIAppUpdateAvailable(e) + if err != nil { + log.WithFields("error", err).Warn("failed to parse app update notification") + continue + } + + if _, err := fmt.Fprintln(writer, style.Render(notice)); err != nil { + // don't let this be fatal + log.WithFields("error", err).Warn("failed to write app update notification") + } + } + return nil +} diff --git a/cmd/grype/internal/ui/post_ui_event_writer_test.go b/cmd/grype/internal/ui/post_ui_event_writer_test.go new file mode 100644 index 00000000000..1d8509de6cc --- /dev/null +++ b/cmd/grype/internal/ui/post_ui_event_writer_test.go @@ -0,0 +1,95 @@ +package ui + +import ( + "bytes" + "testing" + + "github.com/gkampitakis/go-snaps/snaps" + "github.com/stretchr/testify/require" + "github.com/wagoodman/go-partybus" + + "github.com/anchore/grype/grype/event" +) + +func Test_postUIEventWriter_write(t *testing.T) { + + tests := []struct { + name string + quiet bool + events []partybus.Event + wantErr require.ErrorAssertionFunc + }{ + { + name: "no events", + }, + { + name: "all events", + events: []partybus.Event{ + { + Type: event.CLINotification, + Value: "\n\n\n\n", + }, + { + Type: event.CLINotification, + Value: "", + }, + { + Type: event.CLIAppUpdateAvailable, + Value: "\n\n\n\n", + }, + { + Type: event.CLINotification, + Value: "", + }, + { + Type: event.CLIReport, + Value: "\n\n\n\n", + }, + { + Type: event.CLIReport, + Value: "", + }, + }, + }, + { + name: "quiet only shows report", + quiet: true, + events: []partybus.Event{ + + { + Type: event.CLINotification, + Value: "", + }, + { + Type: event.CLIAppUpdateAvailable, + Value: "", + }, + { + Type: event.CLIReport, + Value: "", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.wantErr == nil { + tt.wantErr = require.NoError + } + + stdout := &bytes.Buffer{} + stderr := &bytes.Buffer{} + w := newPostUIEventWriter(stdout, stderr) + + tt.wantErr(t, w.write(tt.quiet, tt.events...)) + + t.Run("stdout", func(t *testing.T) { + snaps.MatchSnapshot(t, stdout.String()) + }) + + t.Run("stderr", func(t *testing.T) { + snaps.MatchSnapshot(t, stderr.String()) + }) + }) + } +} diff --git a/internal/ui/select.go b/cmd/grype/internal/ui/select.go similarity index 55% rename from internal/ui/select.go rename to cmd/grype/internal/ui/select.go index 2ecd90cfdfb..5628907ea36 100644 --- a/internal/ui/select.go +++ b/cmd/grype/internal/ui/select.go @@ -1,32 +1,39 @@ -//go:build linux || darwin -// +build linux darwin - package ui import ( - "io" "os" + "runtime" "golang.org/x/term" -) -// TODO: build tags to exclude options from windows + "github.com/anchore/clio" + grypeHandler "github.com/anchore/grype/cmd/grype/cli/ui" + syftHandler "github.com/anchore/syft/cmd/syft/cli/ui" +) // Select is responsible for determining the specific UI function given select user option, the current platform // config values, and environment status (such as a TTY being present). The first UI in the returned slice of UIs // is intended to be used and the UIs that follow are meant to be attempted only in a fallback posture when there // are environmental problems (e.g. cannot write to the terminal). A writer is provided to capture the output of // the final SBOM report. -func Select(verbose, quiet bool, reportWriter io.Writer) (uis []UI) { +func Select(verbose, quiet bool) (uis []clio.UI) { isStdoutATty := term.IsTerminal(int(os.Stdout.Fd())) isStderrATty := term.IsTerminal(int(os.Stderr.Fd())) notATerminal := !isStderrATty && !isStdoutATty switch { - case verbose || quiet || notATerminal || !isStderrATty: - uis = append(uis, NewLoggerUI(reportWriter)) + case runtime.GOOS == "windows" || verbose || quiet || notATerminal || !isStderrATty: + uis = append(uis, None(quiet)) default: - uis = append(uis, NewEphemeralTerminalUI(reportWriter), NewLoggerUI(reportWriter)) + // TODO: it may make sense in the future to pass handler options into select + + uis = append(uis, + New( + verbose, quiet, + grypeHandler.New(grypeHandler.DefaultHandlerConfig()), + syftHandler.New(syftHandler.DefaultHandlerConfig()), + ), + ) } return uis diff --git a/cmd/grype/internal/ui/ui.go b/cmd/grype/internal/ui/ui.go new file mode 100644 index 00000000000..5814e8033a4 --- /dev/null +++ b/cmd/grype/internal/ui/ui.go @@ -0,0 +1,163 @@ +package ui + +import ( + "os" + "sync" + + tea "github.com/charmbracelet/bubbletea" + "github.com/wagoodman/go-partybus" + + "github.com/anchore/bubbly" + "github.com/anchore/bubbly/bubbles/frame" + "github.com/anchore/clio" + "github.com/anchore/go-logger" + "github.com/anchore/grype/grype/event" + "github.com/anchore/grype/internal/bus" + "github.com/anchore/grype/internal/log" +) + +var _ interface { + tea.Model + partybus.Responder + clio.UI +} = (*UI)(nil) + +type UI struct { + program *tea.Program + running *sync.WaitGroup + quiet bool + subscription partybus.Unsubscribable + finalizeEvents []partybus.Event + + handler *bubbly.HandlerCollection + frame tea.Model +} + +func New(_, quiet bool, hs ...bubbly.EventHandler) *UI { + return &UI{ + handler: bubbly.NewHandlerCollection(hs...), + frame: frame.New(), + running: &sync.WaitGroup{}, + quiet: quiet, + } +} + +func (m *UI) Setup(subscription partybus.Unsubscribable) error { + // we still want to collect log messages, however, we also the logger shouldn't write to the screen directly + if logWrapper, ok := log.Get().(logger.Controller); ok { + logWrapper.SetOutput(m.frame.(*frame.Frame).Footer()) + } + + m.subscription = subscription + m.program = tea.NewProgram(m, tea.WithOutput(os.Stderr), tea.WithInput(os.Stdin)) + m.running.Add(1) + + go func() { + defer m.running.Done() + if _, err := m.program.Run(); err != nil { + log.Errorf("unable to start UI: %+v", err) + m.exit() + } + }() + + return nil +} + +func (m *UI) exit() { + // stop the event loop + bus.Exit() +} + +func (m *UI) Handle(e partybus.Event) error { + if m.program != nil { + m.program.Send(e) + if e.Type == event.CLIExit { + return m.subscription.Unsubscribe() + } + } + return nil +} + +func (m *UI) Teardown(force bool) error { + if !force { + m.handler.Wait() + m.program.Quit() + } else { + m.program.Kill() + } + + m.running.Wait() + + // TODO: allow for writing out the full log output to the screen (only a partial log is shown currently) + // this needs coordination to know what the last frame event is to change the state accordingly (which isn't possible now) + + return newPostUIEventWriter(os.Stdout, os.Stderr).write(m.quiet, m.finalizeEvents...) +} + +// bubbletea.Model functions + +func (m UI) Init() tea.Cmd { + return m.frame.Init() +} + +func (m UI) RespondsTo() []partybus.EventType { + return append([]partybus.EventType{ + event.CLIReport, + event.CLINotification, + event.CLIExit, + event.CLIAppUpdateAvailable, + }, m.handler.RespondsTo()...) +} + +func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + // note: we need a pointer receiver such that the same instance of UI used in Teardown is referenced (to keep finalize events) + + var cmds []tea.Cmd + + // allow for non-partybus UI updates (such as window size events). Note: these must not affect existing models, + // that is the responsibility of the frame object on this UI object. The handler is a factory of models + // which the frame is responsible for the lifecycle of. This update allows for injecting the initial state + // of the world when creating those models. + m.handler.OnMessage(msg) + + switch msg := msg.(type) { + case tea.KeyMsg: + switch msg.String() { + case "esc", "ctrl+c": + m.exit() + return m, tea.Quit + } + + case partybus.Event: + log.WithFields("component", "ui").Tracef("event: %q", msg.Type) + + switch msg.Type { + case event.CLIReport, event.CLINotification, event.CLIExit, event.CLIAppUpdateAvailable: + // keep these for when the UI is terminated to show to the screen (or perform other events) + m.finalizeEvents = append(m.finalizeEvents, msg) + + // why not return tea.Quit here for exit events? because there may be UI components that still need the update-render loop. + // for this reason we'll let the syft event loop call Teardown() which will explicitly wait for these components + return m, nil + } + + for _, newModel := range m.handler.Handle(msg) { + if newModel == nil { + continue + } + cmds = append(cmds, newModel.Init()) + m.frame.(*frame.Frame).AppendModel(newModel) + } + // intentionally fallthrough to update the frame model + } + + frameModel, cmd := m.frame.Update(msg) + m.frame = frameModel + cmds = append(cmds, cmd) + + return m, tea.Batch(cmds...) +} + +func (m UI) View() string { + return m.frame.View() +} diff --git a/cmd/grype/main.go b/cmd/grype/main.go new file mode 100644 index 00000000000..e4cb2d5f579 --- /dev/null +++ b/cmd/grype/main.go @@ -0,0 +1,9 @@ +package main + +import ( + "github.com/anchore/grype/cmd/grype/cli/legacy" +) + +func main() { + legacy.Execute() +} diff --git a/cmd/report_writer.go b/cmd/report_writer.go deleted file mode 100644 index 99cdbfcde00..00000000000 --- a/cmd/report_writer.go +++ /dev/null @@ -1,33 +0,0 @@ -package cmd - -import ( - "fmt" - "io" - "os" - "strings" -) - -func reportWriter() (io.Writer, func() error, error) { - nop := func() error { return nil } - path := strings.TrimSpace(appConfig.File) - - switch len(path) { - case 0: - return os.Stdout, nop, nil - - default: - reportFile, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644) - - if err != nil { - return nil, nop, fmt.Errorf("unable to create report file: %w", err) - } - - return reportFile, func() error { - if !appConfig.Quiet { - fmt.Printf("Report written to %q\n", path) - } - - return reportFile.Close() - }, nil - } -} diff --git a/go.mod b/go.mod index 9cbe642f23f..946fe252c85 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/anchore/packageurl-go v0.1.1-0.20230104203445-02e0a6721501 github.com/anchore/stereoscope v0.0.0-20230627195312-cd49355d934e github.com/bmatcuk/doublestar/v2 v2.0.4 - github.com/docker/docker v24.0.2+incompatible + github.com/docker/docker v24.0.4+incompatible github.com/dustin/go-humanize v1.0.1 github.com/facebookincubator/nvdtools v0.1.5 github.com/gabriel-vasile/mimetype v1.4.2 @@ -41,21 +41,26 @@ require ( github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.16.0 github.com/stretchr/testify v1.8.4 - github.com/wagoodman/go-partybus v0.0.0-20210627031916-db1f5573bbc5 + github.com/wagoodman/go-partybus v0.0.0-20230516145632-8ccac152c651 github.com/wagoodman/go-progress v0.0.0-20230301185719-21920a456ad5 - github.com/wagoodman/jotframe v0.0.0-20211129225309-56b0d0a4aebb github.com/x-cray/logrus-prefixed-formatter v0.5.2 - golang.org/x/term v0.9.0 + golang.org/x/term v0.10.0 gopkg.in/yaml.v2 v2.4.0 gorm.io/gorm v1.23.10 ) require ( - github.com/anchore/go-logger v0.0.0-20220728155337-03b66a5207d8 + github.com/anchore/bubbly v0.0.0-20230712165553-812110ab0a10 + github.com/anchore/clio v0.0.0-20230630162005-9535e9dc2817 + github.com/anchore/go-logger v0.0.0-20230531193951-db5ae83e7dbe github.com/anchore/sqlite v1.4.6-0.20220607210448-bcc6ee5c4963 - github.com/anchore/syft v0.84.1 + github.com/anchore/syft v0.85.0 + github.com/charmbracelet/bubbletea v0.24.2 + github.com/charmbracelet/lipgloss v0.7.1 + github.com/gkampitakis/go-snaps v0.4.8 github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b github.com/mitchellh/mapstructure v1.5.0 + github.com/wagoodman/go-presenter v0.0.0-20211015174752-f9c01afc824b ) require ( @@ -72,14 +77,19 @@ require ( github.com/ProtonMail/go-crypto v0.0.0-20230518184743-7afd39499903 // indirect github.com/acobaugh/osrelease v0.1.0 // indirect github.com/acomagu/bufpipe v1.0.4 // indirect + github.com/anchore/fangs v0.0.0-20230628163043-a51c5a39b097 // indirect github.com/anchore/go-macholibre v0.0.0-20220308212642-53e6d0aaf6fb // indirect github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092 // indirect github.com/andybalholm/brotli v1.0.4 // indirect github.com/aws/aws-sdk-go v1.44.180 // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/becheran/wildmatch-go v1.0.0 // indirect github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect github.com/bmatcuk/doublestar/v4 v4.6.0 // indirect + github.com/charmbracelet/bubbles v0.16.1 // indirect + github.com/charmbracelet/harmonica v0.2.0 // indirect github.com/cloudflare/circl v1.3.3 // indirect + github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect github.com/containerd/containerd v1.7.0 // indirect github.com/containerd/stargz-snapshotter/estargz v0.14.3 // indirect github.com/davecgh/go-spew v1.1.1 // indirect @@ -94,6 +104,8 @@ require ( github.com/felixge/fgprof v0.9.3 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/github/go-spdx/v2 v2.1.2 // indirect + github.com/gkampitakis/ciinfo v0.2.4 // indirect + github.com/gkampitakis/go-diff v1.3.2 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/go-billy/v5 v5.4.1 // indirect github.com/go-git/go-git/v5 v5.7.0 // indirect @@ -124,21 +136,30 @@ require ( github.com/klauspost/compress v1.16.5 // indirect github.com/klauspost/pgzip v1.2.5 // indirect github.com/knqyf263/go-rpmdb v0.0.0-20230301153543-ba94b245509b // indirect + github.com/kr/pretty v0.3.1 // indirect + github.com/kr/text v0.2.0 // indirect github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.18 // indirect - github.com/mattn/go-runewidth v0.0.13 // indirect + github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75 // indirect + github.com/mattn/go-runewidth v0.0.14 // indirect github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect github.com/microsoft/go-rustaudit v0.0.0-20220730194248-4b17361d90a5 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/go-testing-interface v1.14.1 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/muesli/reflow v0.3.0 // indirect + github.com/muesli/termenv v0.15.1 // indirect github.com/nwaples/rardecode v1.1.0 // indirect github.com/onsi/ginkgo v1.16.5 // indirect github.com/onsi/gomega v1.19.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0-rc3 // indirect + github.com/pborman/indent v1.2.1 // indirect github.com/pelletier/go-toml v1.9.5 // indirect github.com/pelletier/go-toml/v2 v2.0.8 // indirect github.com/pierrec/lz4/v4 v4.1.15 // indirect @@ -147,6 +168,7 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/rivo/uniseg v0.2.0 // indirect + github.com/rogpeppe/go-internal v1.9.0 // indirect github.com/sassoftware/go-rpmutils v0.2.0 // indirect github.com/shopspring/decimal v1.2.0 // indirect github.com/skeema/knownhosts v1.1.1 // indirect @@ -158,6 +180,10 @@ require ( github.com/sylabs/sif/v2 v2.8.1 // indirect github.com/sylabs/squashfs v0.6.1 // indirect github.com/therootcompany/xz v1.0.1 // indirect + github.com/tidwall/gjson v1.14.4 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.1 // indirect + github.com/tidwall/sjson v1.2.5 // indirect github.com/ulikunitz/xz v0.5.10 // indirect github.com/vbatts/go-mtree v0.5.3 // indirect github.com/vbatts/tar-split v0.11.3 // indirect @@ -166,16 +192,17 @@ require ( github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect github.com/zclconf/go-cty v1.10.0 // indirect + github.com/zyedidia/generic v1.2.2-0.20230320175451-4410d2372cb1 // indirect go.opencensus.io v0.24.0 // indirect go.uber.org/goleak v1.2.0 // indirect - golang.org/x/crypto v0.10.0 // indirect + golang.org/x/crypto v0.11.0 // indirect golang.org/x/exp v0.0.0-20230202163644-54bba9f4231b // indirect - golang.org/x/mod v0.11.0 // indirect - golang.org/x/net v0.11.0 // indirect + golang.org/x/mod v0.12.0 // indirect + golang.org/x/net v0.12.0 // indirect golang.org/x/oauth2 v0.7.0 // indirect golang.org/x/sync v0.1.0 // indirect - golang.org/x/sys v0.9.0 // indirect - golang.org/x/text v0.10.0 // indirect + golang.org/x/sys v0.10.0 // indirect + golang.org/x/text v0.11.0 // indirect golang.org/x/time v0.2.0 // indirect golang.org/x/tools v0.8.0 // indirect golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect @@ -194,7 +221,7 @@ require ( modernc.org/mathutil v1.5.0 // indirect modernc.org/memory v1.5.0 // indirect modernc.org/opt v0.1.3 // indirect - modernc.org/sqlite v1.23.1 // indirect + modernc.org/sqlite v1.24.0 // indirect modernc.org/strutil v1.1.3 // indirect modernc.org/token v1.1.0 // indirect ) diff --git a/go.sum b/go.sum index 5b85d70446c..02fdecbf320 100644 --- a/go.sum +++ b/go.sum @@ -227,8 +227,14 @@ github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuy github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/anchore/go-logger v0.0.0-20220728155337-03b66a5207d8 h1:imgMA0gN0TZx7PSa/pdWqXadBvrz8WsN6zySzCe4XX0= -github.com/anchore/go-logger v0.0.0-20220728155337-03b66a5207d8/go.mod h1:+gPap4jha079qzRTUaehv+UZ6sSdaNwkH0D3b6zhTuk= +github.com/anchore/bubbly v0.0.0-20230712165553-812110ab0a10 h1:Wrqt9fd8ygEMyFtxncZU7RgW2qBu5CL1x876xIyUlPU= +github.com/anchore/bubbly v0.0.0-20230712165553-812110ab0a10/go.mod h1:Ger02eh5NpPm2IqkPAy396HU1KlK3BhOeCljDYXySSk= +github.com/anchore/clio v0.0.0-20230630162005-9535e9dc2817 h1:YsE91GT81FQOAOKByAnJVeJY2q8AunJ1eNf1bDC/o8g= +github.com/anchore/clio v0.0.0-20230630162005-9535e9dc2817/go.mod h1:H5f7dtqPQ6kbL0OHcLrc5N0zkIxLZPBL2oKUE03fLgA= +github.com/anchore/fangs v0.0.0-20230628163043-a51c5a39b097 h1:79jSyWO6WOV8HPEpOQBOr7WsC2DnBRpyl7zsdaahCcg= +github.com/anchore/fangs v0.0.0-20230628163043-a51c5a39b097/go.mod h1:E3zNHEz7mizIFGJhuX+Ga7AbCmEN5TfzVDxmOfj7XZw= +github.com/anchore/go-logger v0.0.0-20230531193951-db5ae83e7dbe h1:Df867YMmymdMG6z5IW8pR0/2CRpLIjYnaTXLp6j+s0k= +github.com/anchore/go-logger v0.0.0-20230531193951-db5ae83e7dbe/go.mod h1:ubLFmlsv8/DFUQrZwY5syT5/8Er3ugSr4rDFwHsE3hg= github.com/anchore/go-macholibre v0.0.0-20220308212642-53e6d0aaf6fb h1:iDMnx6LIjtjZ46C0akqveX83WFzhpTD3eqOthawb5vU= github.com/anchore/go-macholibre v0.0.0-20220308212642-53e6d0aaf6fb/go.mod h1:DmTY2Mfcv38hsHbG78xMiTDdxFtkHpgYNVDPsF2TgHk= github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092 h1:aM1rlcoLz8y5B2r4tTLMiVTrMtpfY0O8EScKJxaSaEc= @@ -243,8 +249,8 @@ github.com/anchore/sqlite v1.4.6-0.20220607210448-bcc6ee5c4963 h1:vrf2PYH77vqVJo github.com/anchore/sqlite v1.4.6-0.20220607210448-bcc6ee5c4963/go.mod h1:AVRyXOUP0hTz9Cb8OlD1XnwA8t4lBPfTuwPHmEUuiLc= github.com/anchore/stereoscope v0.0.0-20230627195312-cd49355d934e h1:zhk3ZLtomMJ750nNCE+c24PonMzoO/SeL/4uTr1L9kM= github.com/anchore/stereoscope v0.0.0-20230627195312-cd49355d934e/go.mod h1:0LsgHgXO4QFnk2hsYwtqd3fR18PIZXlFLIl2qb9tu3g= -github.com/anchore/syft v0.84.1 h1:O6V1gCSHTVbyfQq6M1qB86ui64qobZRC3h7lvKpVNWw= -github.com/anchore/syft v0.84.1/go.mod h1:dozEWcwhRawdB3ArPM2BGfZWLslZ+bDNwW+wWUwKySY= +github.com/anchore/syft v0.85.0 h1:JShy/YIqffcIR3cvssABGr/yNDRCgZwpcQPcRLO2nHc= +github.com/anchore/syft v0.85.0/go.mod h1:nCMEh98C1BEfkH49HXKeJNPcUEfDM4B6xmptGT5Lv3Q= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY= @@ -261,6 +267,8 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPd github.com/aws/aws-sdk-go v1.44.122/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= github.com/aws/aws-sdk-go v1.44.180 h1:VLZuAHI9fa/3WME5JjpVjcPCNfpGHVMiHx8sLHWhMgI= github.com/aws/aws-sdk-go v1.44.180/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/becheran/wildmatch-go v1.0.0 h1:mE3dGGkTmpKtT4Z+88t8RStG40yN9T+kFEGj2PZFSzA= github.com/becheran/wildmatch-go v1.0.0/go.mod h1:gbMvj0NtVdJ15Mg/mH9uxk2R1QCistMyU7d9KFzroX4= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= @@ -280,6 +288,14 @@ github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/charmbracelet/bubbles v0.16.1 h1:6uzpAAaT9ZqKssntbvZMlksWHruQLNxg49H5WdeuYSY= +github.com/charmbracelet/bubbles v0.16.1/go.mod h1:2QCp9LFlEsBQMvIYERr7Ww2H2bA7xen1idUDIzm/+Xc= +github.com/charmbracelet/bubbletea v0.24.2 h1:uaQIKx9Ai6Gdh5zpTbGiWpytMU+CfsPp06RaW2cx/SY= +github.com/charmbracelet/bubbletea v0.24.2/go.mod h1:XdrNrV4J8GiyshTtx3DNuYkR1FDaJmO3l2nejekbsgg= +github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ= +github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao= +github.com/charmbracelet/lipgloss v0.7.1 h1:17WMwi7N1b1rVWOjMT+rCh7sQkvDU75B2hbZpc5Kc1E= +github.com/charmbracelet/lipgloss v0.7.1/go.mod h1:yG0k3giv8Qj8edTCbbg6AlQ5e8KNWpFujkNawKNhE2c= github.com/cheggaaa/pb v1.0.27/go.mod h1:pQciLPpbU0oxA0h+VJYYLxO+XeDQb5pZijXscXHm81s= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= @@ -300,6 +316,8 @@ github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWH github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211130200136-a8f946100490/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY= +github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= github.com/containerd/containerd v1.7.0 h1:G/ZQr3gMZs6ZT0qPUZ15znx5QSdQdASW11nXTLTM2Pg= github.com/containerd/containerd v1.7.0/go.mod h1:QfR7Efgb/6X2BDpTPJRvPTYDE9rsF0FsXX9J8sIs/sc= github.com/containerd/stargz-snapshotter/estargz v0.14.3 h1:OqlDCK3ZVUO6C3B/5FSkDwbkEETK84kQgEeFwDC+62k= @@ -319,8 +337,8 @@ github.com/docker/cli v23.0.5+incompatible h1:ufWmAOuD3Vmr7JP2G5K3cyuNC4YZWiAsuD github.com/docker/cli v23.0.5+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8= github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= -github.com/docker/docker v24.0.2+incompatible h1:eATx+oLz9WdNVkQrr0qjQ8HvRJ4bOOxfzEo8R+dA3cg= -github.com/docker/docker v24.0.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v24.0.4+incompatible h1:s/LVDftw9hjblvqIeTiGYXBCD95nOEEl7qRsRrIOuQI= +github.com/docker/docker v24.0.4+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker-credential-helpers v0.7.0 h1:xtCHsjxogADNZcdv1pKUHXryefjlVRqWqIhk/uXJp0A= github.com/docker/docker-credential-helpers v0.7.0/go.mod h1:rETQfLdHNT3foU5kuNkFR1R1V12OJRRO5lzt2D1b5X0= github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= @@ -369,6 +387,12 @@ github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9 github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/github/go-spdx/v2 v2.1.2 h1:p+Tv0yMgcuO0/vnMe9Qh4tmUgYhI6AsLVlakZ/Sx+DM= github.com/github/go-spdx/v2 v2.1.2/go.mod h1:hMCrsFgT0QnCwn7G8gxy/MxMpy67WgZrwFeISTn0o6w= +github.com/gkampitakis/ciinfo v0.2.4 h1:Ip1hf4K7ISRuVlDrheuhaeffg1VOhlyeFGaQ/vTxrtE= +github.com/gkampitakis/ciinfo v0.2.4/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo= +github.com/gkampitakis/go-diff v1.3.2 h1:Qyn0J9XJSDTgnsgHRdz9Zp24RaJeKMUHg2+PDZZdC4M= +github.com/gkampitakis/go-diff v1.3.2/go.mod h1:LLgOrpqleQe26cte8s36HTWcTmMEur6OPYerdAAS9tk= +github.com/gkampitakis/go-snaps v0.4.8 h1:B/CJswqJ9LwQMI0tiU7ztWK8qlnz6HxOqZm+XIFuEDU= +github.com/gkampitakis/go-snaps v0.4.8/go.mod h1:8HW4KX3JKV8M0GSw69CvT+Jqhd1AlBPMPpBfjBI3bdY= github.com/glebarez/go-sqlite v1.20.3 h1:89BkqGOXR9oRmG58ZrzgoY/Fhy5x0M+/WV48U5zVrZ4= github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= @@ -587,7 +611,6 @@ github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHm github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= @@ -616,12 +639,15 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381 h1:bqDmpDG49ZRnB5PcgP0RXtQvnMSgIF14M7CBd2shtXs= github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/lyft/protoc-gen-star v0.5.3/go.mod h1:V0xaHgaf5oCCqmcxYcWiDfTiKsZsRc87/1qhoTACD8w= github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= @@ -637,7 +663,6 @@ github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxec github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-isatty v0.0.6/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= @@ -646,10 +671,13 @@ github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27k github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98= github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75 h1:P8UmIzZMYDR+NGImiFvErt6VWfIRPuGM+vyjiEdkmIw= +github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= -github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= +github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= +github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-sqlite3 v1.14.12/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= @@ -688,6 +716,14 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70 h1:kMlmsLSbjkikxQJ1IPwaM+7LJ9ltFu/fi8CRzvSnQmA= +github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= +github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= +github.com/muesli/termenv v0.15.1 h1:UzuTb/+hhlBugQz28rpzey4ZuKcZ03MeKsoG7IJZIxs= +github.com/muesli/termenv v0.15.1/go.mod h1:HeAQPTzpfs016yGtA4g00CsdYnVLJvxsS4ANqrZs2sQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nwaples/rardecode v1.1.0 h1:vSxaY8vQhOcVr4mm5e8XllHWTiM4JF507A0Katqw7MQ= @@ -713,6 +749,8 @@ github.com/owenrumney/go-sarif v1.1.1 h1:QNObu6YX1igyFKhdzd7vgzmw7XsWN3/6NMGuDzB github.com/owenrumney/go-sarif v1.1.1/go.mod h1:dNDiPlF04ESR/6fHlPyq7gHKmrM0sHUvAGjsoh8ZH0U= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pborman/indent v1.2.1 h1:lFiviAbISHv3Rf0jcuh489bi06hj98JsVMtIDZQb9yM= +github.com/pborman/indent v1.2.1/go.mod h1:FitS+t35kIYtB5xWTZAPhnmrxcciEEOdbyrrpz5K6Vw= github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= @@ -723,6 +761,7 @@ github.com/pierrec/lz4/v4 v4.1.15 h1:MO0/ucJhngq7299dKLwIMtgTfbkoSPF6AoMYDd8Q4q0 github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -750,11 +789,13 @@ github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+Gx github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/sagikazarmark/crypt v0.3.0/go.mod h1:uD/D+6UF4SrIR1uGEv7bBNkNqLGqUr43MRiaGWX1Nig= @@ -826,6 +867,16 @@ github.com/sylabs/squashfs v0.6.1 h1:4hgvHnD9JGlYWwT0bPYNt9zaz23mAV3Js+VEgQoRGYQ github.com/sylabs/squashfs v0.6.1/go.mod h1:ZwpbPCj0ocIvMy2br6KZmix6Gzh6fsGQcCnydMF+Kx8= github.com/therootcompany/xz v1.0.1 h1:CmOtsn1CbtmyYiusbfmhmkpAAETj0wBIH6kCYaX+xzw= github.com/therootcompany/xz v1.0.1/go.mod h1:3K3UH1yCKgBneZYhuQUvJ9HPD19UEXEI0BWbMn8qNMY= +github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM= +github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= +github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/ulikunitz/xz v0.5.9/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= @@ -840,12 +891,12 @@ github.com/vifraa/gopom v0.2.1 h1:MYVMAMyiGzXPPy10EwojzKIL670kl5Zbae+o3fFvQEM= github.com/vifraa/gopom v0.2.1/go.mod h1:oPa1dcrGrtlO37WPDBm5SqHAT+wTgF8An1Q71Z6Vv4o= github.com/vmihailenco/msgpack/v4 v4.3.12/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4= github.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI= -github.com/wagoodman/go-partybus v0.0.0-20210627031916-db1f5573bbc5 h1:phTLPgMRDYTizrBSKsNSOa2zthoC2KsJsaY/8sg3rD8= -github.com/wagoodman/go-partybus v0.0.0-20210627031916-db1f5573bbc5/go.mod h1:JPirS5jde/CF5qIjcK4WX+eQmKXdPc6vcZkJ/P0hfPw= +github.com/wagoodman/go-partybus v0.0.0-20230516145632-8ccac152c651 h1:jIVmlAFIqV3d+DOxazTR9v+zgj8+VYuQBzPgBZvWBHA= +github.com/wagoodman/go-partybus v0.0.0-20230516145632-8ccac152c651/go.mod h1:b26F2tHLqaoRQf8DywqzVaV1MQ9yvjb0OMcNl7Nxu20= +github.com/wagoodman/go-presenter v0.0.0-20211015174752-f9c01afc824b h1:uWNQ0khA6RdFzODOMwKo9XXu7fuewnnkHykUtuKru8s= +github.com/wagoodman/go-presenter v0.0.0-20211015174752-f9c01afc824b/go.mod h1:ewlIKbKV8l+jCj8rkdXIs361ocR5x3qGyoCSca47Gx8= github.com/wagoodman/go-progress v0.0.0-20230301185719-21920a456ad5 h1:lwgTsTy18nYqASnH58qyfRW/ldj7Gt2zzBvgYPzdA4s= github.com/wagoodman/go-progress v0.0.0-20230301185719-21920a456ad5/go.mod h1:jLXFoL31zFaHKAAyZUh+sxiTDFe1L1ZHrcK2T1itVKA= -github.com/wagoodman/jotframe v0.0.0-20211129225309-56b0d0a4aebb h1:Yz6VVOcLuWLAHYlJzTw7JKnWxdV/WXpug2X0quEzRnY= -github.com/wagoodman/jotframe v0.0.0-20211129225309-56b0d0a4aebb/go.mod h1:nDi3BAC5nEbVbg+WSJDHLbjHv0ZToq8nMPA97XMxF3E= github.com/x-cray/logrus-prefixed-formatter v0.5.2 h1:00txxvfBM9muc0jiLIEAkAcIMJzfthRT6usrui8uGmg= github.com/x-cray/logrus-prefixed-formatter v0.5.2/go.mod h1:2duySbKsL6M18s5GU7VPsoEPHyzalCE06qoARUCeBBE= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= @@ -862,6 +913,8 @@ github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zclconf/go-cty v1.10.0 h1:mp9ZXQeIcN8kAwuqorjH+Q+njbJKjLrvB2yIh4q7U+0= github.com/zclconf/go-cty v1.10.0/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUAzyuvAk= +github.com/zyedidia/generic v1.2.2-0.20230320175451-4410d2372cb1 h1:V+UsotZpAVvfj3X/LMoEytoLzSiP6Lg0F7wdVyu9gGg= +github.com/zyedidia/generic v1.2.2-0.20230320175451-4410d2372cb1/go.mod h1:ly2RBz4mnz1yeuVbQA/VFwGjK3mnHGRj1JuoG336Bis= go.etcd.io/etcd/api/v3 v3.5.1/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= go.etcd.io/etcd/client/pkg/v3 v3.5.1/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= go.etcd.io/etcd/client/v2 v2.305.1/go.mod h1:pMEacxZW7o8pg4CrFE7pquyCJJzZvkvdD2RibOCCCGs= @@ -883,7 +936,6 @@ go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9i go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -900,8 +952,8 @@ golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0 golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= -golang.org/x/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM= -golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I= +golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA= +golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -942,8 +994,8 @@ golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU= -golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -1004,8 +1056,8 @@ golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= -golang.org/x/net v0.11.0 h1:Gi2tvZIJyBtO9SDr1q9h5hEQCp/4L2RQ+ar0qjx2oNU= -golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ= +golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50= +golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1149,18 +1201,17 @@ golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s= -golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= +golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= -golang.org/x/term v0.9.0 h1:GRRCnKYhdQrD8kfRAdQ6Zcw1P0OcELxGLKJvtjVMZ28= -golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo= +golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c= +golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1174,8 +1225,8 @@ golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58= -golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= +golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -1551,8 +1602,8 @@ modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= modernc.org/sqlite v1.17.3/go.mod h1:10hPVYar9C0kfXuTWGz8s0XtB8uAGymUy51ZzStYe3k= -modernc.org/sqlite v1.23.1 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM= -modernc.org/sqlite v1.23.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk= +modernc.org/sqlite v1.24.0 h1:EsClRIWHGhLTCX44p+Ri/JLD+vFGo0QGjasg2/F9TlI= +modernc.org/sqlite v1.24.0/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk= modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw= modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY= modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw= diff --git a/grype/db/curator_test.go b/grype/db/curator_test.go index acb20216bf3..bb3ee22ac09 100644 --- a/grype/db/curator_test.go +++ b/grype/db/curator_test.go @@ -21,14 +21,14 @@ import ( "github.com/stretchr/testify/require" "github.com/wagoodman/go-progress" - "github.com/anchore/grype/internal" "github.com/anchore/grype/internal/file" + "github.com/anchore/grype/internal/stringutil" ) type testGetter struct { file map[string]string dir map[string]string - calls internal.StringSet + calls stringutil.StringSet fs afero.Fs } @@ -36,7 +36,7 @@ func newTestGetter(fs afero.Fs, f, d map[string]string) *testGetter { return &testGetter{ file: f, dir: d, - calls: internal.NewStringSet(), + calls: stringutil.NewStringSet(), fs: fs, } } diff --git a/grype/db/test-fixtures/tls/Makefile b/grype/db/test-fixtures/tls/Makefile index f0ca3949475..be149c85410 100644 --- a/grype/db/test-fixtures/tls/Makefile +++ b/grype/db/test-fixtures/tls/Makefile @@ -9,14 +9,14 @@ serve: www/listing.json www/db.tar.gz server.crt grype-test-fail: clean-dbdir dbdir GRYPE_DB_CACHE_DIR=$(shell pwd)/dbdir \ GRYPE_DB_UPDATE_URL=https://$(shell hostname).local/listing.json \ - go run ../../../../main.go -vv alpine:latest + go run ../../../../cmd/grype -vv alpine:latest .PHONY: grype-test-pass grype-test-pass: clean-dbdir dbdir GRYPE_DB_CA_CERT=$(shell pwd)/server.crt \ GRYPE_DB_CACHE_DIR=$(shell pwd)/dbdir \ GRYPE_DB_UPDATE_URL=https://$(shell hostname).local/listing.json \ - go run ../../../../main.go -vv alpine:latest + go run ../../../../cmd/grype -vv alpine:latest dbdir: mkdir -p dbdir diff --git a/grype/db/v1/store/store.go b/grype/db/v1/store/store.go index d6ffab4816d..02656bd9317 100644 --- a/grype/db/v1/store/store.go +++ b/grype/db/v1/store/store.go @@ -10,7 +10,7 @@ import ( "github.com/anchore/grype/grype/db/internal/gormadapter" v1 "github.com/anchore/grype/grype/db/v1" "github.com/anchore/grype/grype/db/v1/store/model" - "github.com/anchore/grype/internal" + "github.com/anchore/grype/internal/stringutil" _ "github.com/anchore/sqlite" // provide the sqlite dialect to gorm via import ) @@ -172,7 +172,7 @@ func (s *store) AddVulnerabilityMetadata(metadata ...v1.VulnerabilityMetadata) e existing.CvssV3 = m.CvssV3 } - links := internal.NewStringSetFromSlice(existing.Links) + links := stringutil.NewStringSetFromSlice(existing.Links) for _, l := range m.Links { links.Add(l) } diff --git a/grype/db/v2/store/store.go b/grype/db/v2/store/store.go index 073cbaf01ff..b0d7907f636 100644 --- a/grype/db/v2/store/store.go +++ b/grype/db/v2/store/store.go @@ -10,7 +10,7 @@ import ( "github.com/anchore/grype/grype/db/internal/gormadapter" v2 "github.com/anchore/grype/grype/db/v2" "github.com/anchore/grype/grype/db/v2/store/model" - "github.com/anchore/grype/internal" + "github.com/anchore/grype/internal/stringutil" _ "github.com/anchore/sqlite" // provide the sqlite dialect to gorm via import ) @@ -171,7 +171,7 @@ func (s *store) AddVulnerabilityMetadata(metadata ...v2.VulnerabilityMetadata) e existing.CvssV3 = m.CvssV3 } - links := internal.NewStringSetFromSlice(existing.Links) + links := stringutil.NewStringSetFromSlice(existing.Links) for _, l := range m.Links { links.Add(l) } diff --git a/grype/db/v3/namespace.go b/grype/db/v3/namespace.go index 386c89c793c..ab43539ac64 100644 --- a/grype/db/v3/namespace.go +++ b/grype/db/v3/namespace.go @@ -6,8 +6,8 @@ import ( "github.com/anchore/grype/grype/distro" "github.com/anchore/grype/grype/pkg" - "github.com/anchore/grype/internal" "github.com/anchore/grype/internal/log" + "github.com/anchore/grype/internal/stringutil" packageurl "github.com/anchore/packageurl-go" syftPkg "github.com/anchore/syft/syft/pkg" ) @@ -110,7 +110,7 @@ func defaultPackageNamer(p pkg.Package) []string { } func githubJavaPackageNamer(p pkg.Package) []string { - names := internal.NewStringSet() + names := stringutil.NewStringSet() // all github advisories are stored by ":" if metadata, ok := p.Metadata.(pkg.JavaMetadata); ok { diff --git a/grype/db/v3/store/diff.go b/grype/db/v3/store/diff.go index 24a473a46cc..ff32486818b 100644 --- a/grype/db/v3/store/diff.go +++ b/grype/db/v3/store/diff.go @@ -5,8 +5,8 @@ import ( "github.com/wagoodman/go-progress" v3 "github.com/anchore/grype/grype/db/v3" - diffEvents "github.com/anchore/grype/grype/differ/events" "github.com/anchore/grype/grype/event" + "github.com/anchore/grype/grype/event/monitor" "github.com/anchore/grype/internal/bus" ) @@ -32,18 +32,21 @@ type storeMetadata struct { } // create manual progress bars for tracking the database diff's progress -func trackDiff() (*progress.Manual, *progress.Manual) { - rowsProcessed := progress.Manual{} - differencesDiscovered := progress.Manual{} +func trackDiff(total int64) (*progress.Manual, *progress.Manual, *progress.Stage) { + stageProgress := &progress.Manual{} + stageProgress.SetTotal(total) + differencesDiscovered := &progress.Manual{} + stager := &progress.Stage{} bus.Publish(partybus.Event{ Type: event.DatabaseDiffingStarted, - Value: diffEvents.Monitor{ - RowsProcessed: progress.Monitorable(&rowsProcessed), - DifferencesDiscovered: progress.Monitorable(&differencesDiscovered), + Value: monitor.DBDiff{ + Stager: stager, + StageProgress: progress.Progressable(stageProgress), + DifferencesDiscovered: progress.Monitorable(differencesDiscovered), }, }) - return &rowsProcessed, &differencesDiscovered + return stageProgress, differencesDiscovered, stager } // creates a map from an unpackaged key to a list of all packages associated with it diff --git a/grype/db/v3/store/store.go b/grype/db/v3/store/store.go index 5dce10e5647..212c0e9449d 100644 --- a/grype/db/v3/store/store.go +++ b/grype/db/v3/store/store.go @@ -10,7 +10,7 @@ import ( "github.com/anchore/grype/grype/db/internal/gormadapter" v3 "github.com/anchore/grype/grype/db/v3" "github.com/anchore/grype/grype/db/v3/store/model" - "github.com/anchore/grype/internal" + "github.com/anchore/grype/internal/stringutil" _ "github.com/anchore/sqlite" // provide the sqlite dialect to gorm via import ) @@ -179,7 +179,7 @@ func (s *store) AddVulnerabilityMetadata(metadata ...v3.VulnerabilityMetadata) e existing.Cvss = append(existing.Cvss, incomingCvss) } - links := internal.NewStringSetFromSlice(existing.URLs) + links := stringutil.NewStringSetFromSlice(existing.URLs) for _, l := range m.URLs { links.Add(l) } @@ -249,37 +249,45 @@ func (s *store) GetAllVulnerabilityMetadata() (*[]v3.VulnerabilityMetadata, erro // DiffStore creates a diff between the current sql database and the given store func (s *store) DiffStore(targetStore v3.StoreReader) (*[]v3.Diff, error) { - rowsProgress, diffItems := trackDiff() + // 7 stages, one for each step of the diff process (stages) + rowsProgress, diffItems, stager := trackDiff(7) + stager.Current = "reading target vulnerabilities" targetVulns, err := targetStore.GetAllVulnerabilities() rowsProgress.Increment() if err != nil { return nil, err } + stager.Current = "reading base vulnerabilities" baseVulns, err := s.GetAllVulnerabilities() rowsProgress.Increment() if err != nil { return nil, err } + stager.Current = "preparing" baseVulnPkgMap := buildVulnerabilityPkgsMap(baseVulns) targetVulnPkgMap := buildVulnerabilityPkgsMap(targetVulns) + stager.Current = "comparing vulnerabilities" allDiffsMap := diffVulnerabilities(baseVulns, targetVulns, baseVulnPkgMap, targetVulnPkgMap, diffItems) + stager.Current = "reading base metadata" baseMetadata, err := s.GetAllVulnerabilityMetadata() if err != nil { return nil, err } rowsProgress.Increment() + stager.Current = "reading target metadata" targetMetadata, err := targetStore.GetAllVulnerabilityMetadata() if err != nil { return nil, err } rowsProgress.Increment() + stager.Current = "comparing metadata" metaDiffsMap := diffVulnerabilityMetadata(baseMetadata, targetMetadata, baseVulnPkgMap, targetVulnPkgMap, diffItems) for k, diff := range *metaDiffsMap { (*allDiffsMap)[k] = diff diff --git a/grype/db/v4/pkg/resolver/java/resolver.go b/grype/db/v4/pkg/resolver/java/resolver.go index b9741f57150..c5933d78ef6 100644 --- a/grype/db/v4/pkg/resolver/java/resolver.go +++ b/grype/db/v4/pkg/resolver/java/resolver.go @@ -5,8 +5,8 @@ import ( "strings" grypePkg "github.com/anchore/grype/grype/pkg" - "github.com/anchore/grype/internal" "github.com/anchore/grype/internal/log" + "github.com/anchore/grype/internal/stringutil" "github.com/anchore/packageurl-go" ) @@ -18,7 +18,7 @@ func (r *Resolver) Normalize(name string) string { } func (r *Resolver) Resolve(p grypePkg.Package) []string { - names := internal.NewStringSet() + names := stringutil.NewStringSet() // The current default for the Java ecosystem is to use a Maven-like identifier of the form // ":" diff --git a/grype/db/v4/store/diff.go b/grype/db/v4/store/diff.go index 4cff4b9327a..88ed92f5b5c 100644 --- a/grype/db/v4/store/diff.go +++ b/grype/db/v4/store/diff.go @@ -5,8 +5,8 @@ import ( "github.com/wagoodman/go-progress" v4 "github.com/anchore/grype/grype/db/v4" - diffEvents "github.com/anchore/grype/grype/differ/events" "github.com/anchore/grype/grype/event" + "github.com/anchore/grype/grype/event/monitor" "github.com/anchore/grype/internal/bus" ) @@ -32,18 +32,21 @@ type storeMetadata struct { } // create manual progress bars for tracking the database diff's progress -func trackDiff() (*progress.Manual, *progress.Manual) { - rowsProcessed := progress.Manual{} - differencesDiscovered := progress.Manual{} +func trackDiff(total int64) (*progress.Manual, *progress.Manual, *progress.Stage) { + stageProgress := &progress.Manual{} + stageProgress.SetTotal(total) + differencesDiscovered := &progress.Manual{} + stager := &progress.Stage{} bus.Publish(partybus.Event{ Type: event.DatabaseDiffingStarted, - Value: diffEvents.Monitor{ - RowsProcessed: progress.Monitorable(&rowsProcessed), - DifferencesDiscovered: progress.Monitorable(&differencesDiscovered), + Value: monitor.DBDiff{ + Stager: stager, + StageProgress: progress.Progressable(stageProgress), + DifferencesDiscovered: progress.Monitorable(differencesDiscovered), }, }) - return &rowsProcessed, &differencesDiscovered + return stageProgress, differencesDiscovered, stager } // creates a map from an unpackaged key to a list of all packages associated with it diff --git a/grype/db/v4/store/store.go b/grype/db/v4/store/store.go index 018a19b7909..01b005590f3 100644 --- a/grype/db/v4/store/store.go +++ b/grype/db/v4/store/store.go @@ -10,7 +10,7 @@ import ( "github.com/anchore/grype/grype/db/internal/gormadapter" v4 "github.com/anchore/grype/grype/db/v4" "github.com/anchore/grype/grype/db/v4/store/model" - "github.com/anchore/grype/internal" + "github.com/anchore/grype/internal/stringutil" _ "github.com/anchore/sqlite" // provide the sqlite dialect to gorm via import ) @@ -189,7 +189,7 @@ func (s *store) AddVulnerabilityMetadata(metadata ...v4.VulnerabilityMetadata) e existing.Cvss = append(existing.Cvss, incomingCvss) } - links := internal.NewStringSetFromSlice(existing.URLs) + links := stringutil.NewStringSetFromSlice(existing.URLs) for _, l := range m.URLs { links.Add(l) } @@ -307,37 +307,45 @@ func (s *store) GetAllVulnerabilityMetadata() (*[]v4.VulnerabilityMetadata, erro // DiffStore creates a diff between the current sql database and the given store func (s *store) DiffStore(targetStore v4.StoreReader) (*[]v4.Diff, error) { - rowsProgress, diffItems := trackDiff() + // 7 stages, one for each step of the diff process (stages) + rowsProgress, diffItems, stager := trackDiff(7) + stager.Current = "reading target vulnerabilities" targetVulns, err := targetStore.GetAllVulnerabilities() rowsProgress.Increment() if err != nil { return nil, err } + stager.Current = "reading base vulnerabilities" baseVulns, err := s.GetAllVulnerabilities() rowsProgress.Increment() if err != nil { return nil, err } + stager.Current = "preparing" baseVulnPkgMap := buildVulnerabilityPkgsMap(baseVulns) targetVulnPkgMap := buildVulnerabilityPkgsMap(targetVulns) + stager.Current = "comparing vulnerabilities" allDiffsMap := diffVulnerabilities(baseVulns, targetVulns, baseVulnPkgMap, targetVulnPkgMap, diffItems) + stager.Current = "reading base metadata" baseMetadata, err := s.GetAllVulnerabilityMetadata() if err != nil { return nil, err } rowsProgress.Increment() + stager.Current = "reading target metadata" targetMetadata, err := targetStore.GetAllVulnerabilityMetadata() if err != nil { return nil, err } rowsProgress.Increment() + stager.Current = "comparing metadata" metaDiffsMap := diffVulnerabilityMetadata(baseMetadata, targetMetadata, baseVulnPkgMap, targetVulnPkgMap, diffItems) for k, diff := range *metaDiffsMap { (*allDiffsMap)[k] = diff diff --git a/grype/db/v5/pkg/resolver/java/resolver.go b/grype/db/v5/pkg/resolver/java/resolver.go index b9741f57150..c5933d78ef6 100644 --- a/grype/db/v5/pkg/resolver/java/resolver.go +++ b/grype/db/v5/pkg/resolver/java/resolver.go @@ -5,8 +5,8 @@ import ( "strings" grypePkg "github.com/anchore/grype/grype/pkg" - "github.com/anchore/grype/internal" "github.com/anchore/grype/internal/log" + "github.com/anchore/grype/internal/stringutil" "github.com/anchore/packageurl-go" ) @@ -18,7 +18,7 @@ func (r *Resolver) Normalize(name string) string { } func (r *Resolver) Resolve(p grypePkg.Package) []string { - names := internal.NewStringSet() + names := stringutil.NewStringSet() // The current default for the Java ecosystem is to use a Maven-like identifier of the form // ":" diff --git a/grype/db/v5/store/diff.go b/grype/db/v5/store/diff.go index d1a2780027b..3aaddd9ada2 100644 --- a/grype/db/v5/store/diff.go +++ b/grype/db/v5/store/diff.go @@ -5,8 +5,8 @@ import ( "github.com/wagoodman/go-progress" v5 "github.com/anchore/grype/grype/db/v5" - diffEvents "github.com/anchore/grype/grype/differ/events" "github.com/anchore/grype/grype/event" + "github.com/anchore/grype/grype/event/monitor" "github.com/anchore/grype/internal/bus" ) @@ -32,18 +32,21 @@ type storeMetadata struct { } // create manual progress bars for tracking the database diff's progress -func trackDiff() (*progress.Manual, *progress.Manual) { - rowsProcessed := progress.Manual{} - differencesDiscovered := progress.Manual{} +func trackDiff(total int64) (*progress.Manual, *progress.Manual, *progress.Stage) { + stageProgress := &progress.Manual{} + stageProgress.SetTotal(total) + differencesDiscovered := &progress.Manual{} + stager := &progress.Stage{} bus.Publish(partybus.Event{ Type: event.DatabaseDiffingStarted, - Value: diffEvents.Monitor{ - RowsProcessed: progress.Monitorable(&rowsProcessed), - DifferencesDiscovered: progress.Monitorable(&differencesDiscovered), + Value: monitor.DBDiff{ + Stager: stager, + StageProgress: progress.Progressable(stageProgress), + DifferencesDiscovered: progress.Monitorable(differencesDiscovered), }, }) - return &rowsProcessed, &differencesDiscovered + return stageProgress, differencesDiscovered, stager } // creates a map from an unpackaged key to a list of all packages associated with it diff --git a/grype/db/v5/store/store.go b/grype/db/v5/store/store.go index 725fa54ba26..e1505ce3e1d 100644 --- a/grype/db/v5/store/store.go +++ b/grype/db/v5/store/store.go @@ -10,7 +10,7 @@ import ( "github.com/anchore/grype/grype/db/internal/gormadapter" v5 "github.com/anchore/grype/grype/db/v5" "github.com/anchore/grype/grype/db/v5/store/model" - "github.com/anchore/grype/internal" + "github.com/anchore/grype/internal/stringutil" _ "github.com/anchore/sqlite" // provide the sqlite dialect to gorm via import ) @@ -207,7 +207,7 @@ func (s *store) AddVulnerabilityMetadata(metadata ...v5.VulnerabilityMetadata) e existing.Cvss = append(existing.Cvss, incomingCvss) } - links := internal.NewStringSetFromSlice(existing.URLs) + links := stringutil.NewStringSetFromSlice(existing.URLs) for _, l := range m.URLs { links.Add(l) } @@ -325,37 +325,45 @@ func (s *store) GetAllVulnerabilityMetadata() (*[]v5.VulnerabilityMetadata, erro // DiffStore creates a diff between the current sql database and the given store func (s *store) DiffStore(targetStore v5.StoreReader) (*[]v5.Diff, error) { - rowsProgress, diffItems := trackDiff() + // 7 stages, one for each step of the diff process (stages) + rowsProgress, diffItems, stager := trackDiff(7) + stager.Current = "reading target vulnerabilities" targetVulns, err := targetStore.GetAllVulnerabilities() rowsProgress.Increment() if err != nil { return nil, err } + stager.Current = "reading base vulnerabilities" baseVulns, err := s.GetAllVulnerabilities() rowsProgress.Increment() if err != nil { return nil, err } + stager.Current = "preparing" baseVulnPkgMap := buildVulnerabilityPkgsMap(baseVulns) targetVulnPkgMap := buildVulnerabilityPkgsMap(targetVulns) + stager.Current = "comparing vulnerabilities" allDiffsMap := diffVulnerabilities(baseVulns, targetVulns, baseVulnPkgMap, targetVulnPkgMap, diffItems) + stager.Current = "reading base metadata" baseMetadata, err := s.GetAllVulnerabilityMetadata() if err != nil { return nil, err } rowsProgress.Increment() + stager.Current = "reading target metadata" targetMetadata, err := targetStore.GetAllVulnerabilityMetadata() if err != nil { return nil, err } rowsProgress.Increment() + stager.Current = "comparing metadata" metaDiffsMap := diffVulnerabilityMetadata(baseMetadata, targetMetadata, baseVulnPkgMap, targetVulnPkgMap, diffItems) for k, diff := range *metaDiffsMap { (*allDiffsMap)[k] = diff diff --git a/grype/differ/events/events.go b/grype/differ/events/events.go deleted file mode 100644 index f24d6455dba..00000000000 --- a/grype/differ/events/events.go +++ /dev/null @@ -1,8 +0,0 @@ -package events - -import "github.com/wagoodman/go-progress" - -type Monitor struct { - RowsProcessed progress.Monitorable - DifferencesDiscovered progress.Monitorable -} diff --git a/grype/distro/distro_test.go b/grype/distro/distro_test.go index 8b6cecdfc28..c757119c53f 100644 --- a/grype/distro/distro_test.go +++ b/grype/distro/distro_test.go @@ -6,7 +6,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/anchore/grype/internal" + "github.com/anchore/grype/internal/stringutil" "github.com/anchore/syft/syft/linux" "github.com/anchore/syft/syft/source" ) @@ -214,8 +214,8 @@ func Test_NewDistroFromRelease_Coverage(t *testing.T) { }, } - observedDistros := internal.NewStringSet() - definedDistros := internal.NewStringSet() + observedDistros := stringutil.NewStringSet() + definedDistros := stringutil.NewStringSet() for _, distroType := range All { definedDistros.Add(string(distroType)) @@ -227,7 +227,7 @@ func Test_NewDistroFromRelease_Coverage(t *testing.T) { for _, test := range tests { t.Run(test.fixture, func(t *testing.T) { - s, err := source.NewFromDirectory(test.fixture) + s, err := source.NewFromDirectory(source.DirectoryConfig{Path: test.fixture}) require.NoError(t, err) resolver, err := s.FileResolver(source.SquashedScope) diff --git a/grype/event/event.go b/grype/event/event.go index 91cfcde1a01..a95f68b2544 100644 --- a/grype/event/event.go +++ b/grype/event/event.go @@ -1,12 +1,32 @@ package event -import "github.com/wagoodman/go-partybus" +import ( + "github.com/wagoodman/go-partybus" + + "github.com/anchore/grype/internal" +) const ( - AppUpdateAvailable partybus.EventType = "grype-app-update-available" - UpdateVulnerabilityDatabase partybus.EventType = "grype-update-vulnerability-database" - VulnerabilityScanningStarted partybus.EventType = "grype-vulnerability-scanning-started" - VulnerabilityScanningFinished partybus.EventType = "grype-vulnerability-scanning-finished" - NonRootCommandFinished partybus.EventType = "grype-non-root-command-finished" - DatabaseDiffingStarted partybus.EventType = "grype-database-diffing-started" + typePrefix = internal.ApplicationName + cliTypePrefix = typePrefix + "-cli" + + // Events from the grype library + + UpdateVulnerabilityDatabase partybus.EventType = typePrefix + "-update-vulnerability-database" + VulnerabilityScanningStarted partybus.EventType = typePrefix + "-vulnerability-scanning-started" + DatabaseDiffingStarted partybus.EventType = typePrefix + "-database-diffing-started" + + // Events exclusively for the CLI + + // CLIAppUpdateAvailable is a partybus event that occurs when an application update is available + CLIAppUpdateAvailable partybus.EventType = cliTypePrefix + "-app-update-available" + + // CLIReport is a partybus event that occurs when an analysis result is ready for final presentation to stdout + CLIReport partybus.EventType = cliTypePrefix + "-report" + + // CLINotification is a partybus event that occurs when auxiliary information is ready for presentation to stderr + CLINotification partybus.EventType = cliTypePrefix + "-notification" + + // CLIExit is a partybus event that occurs when an analysis result is ready for final presentation + CLIExit partybus.EventType = cliTypePrefix + "-exit-event" ) diff --git a/grype/event/monitor/db_diff.go b/grype/event/monitor/db_diff.go new file mode 100644 index 00000000000..3f09bc2d52e --- /dev/null +++ b/grype/event/monitor/db_diff.go @@ -0,0 +1,9 @@ +package monitor + +import "github.com/wagoodman/go-progress" + +type DBDiff struct { + Stager progress.Stager + StageProgress progress.Progressable + DifferencesDiscovered progress.Monitorable +} diff --git a/grype/event/monitor/matching.go b/grype/event/monitor/matching.go new file mode 100644 index 00000000000..28967521174 --- /dev/null +++ b/grype/event/monitor/matching.go @@ -0,0 +1,14 @@ +package monitor + +import ( + "github.com/wagoodman/go-progress" + + "github.com/anchore/grype/grype/vulnerability" +) + +type Matching struct { + PackagesProcessed progress.Monitorable + VulnerabilitiesDiscovered progress.Monitorable + Fixed progress.Monitorable + BySeverity map[vulnerability.Severity]progress.Monitorable +} diff --git a/grype/event/parsers/parsers.go b/grype/event/parsers/parsers.go index 9b1a3c14155..a12b4187dae 100644 --- a/grype/event/parsers/parsers.go +++ b/grype/event/parsers/parsers.go @@ -6,10 +6,8 @@ import ( "github.com/wagoodman/go-partybus" "github.com/wagoodman/go-progress" - diffEvents "github.com/anchore/grype/grype/differ/events" "github.com/anchore/grype/grype/event" - "github.com/anchore/grype/grype/matcher" - "github.com/anchore/grype/grype/presenter" + "github.com/anchore/grype/grype/event/monitor" ) type ErrBadPayload struct { @@ -37,19 +35,6 @@ func checkEventType(actual, expected partybus.EventType) error { return nil } -func ParseAppUpdateAvailable(e partybus.Event) (string, error) { - if err := checkEventType(e.Type, event.AppUpdateAvailable); err != nil { - return "", err - } - - newVersion, ok := e.Value.(string) - if !ok { - return "", newPayloadErr(e.Type, "Value", e.Value) - } - - return newVersion, nil -} - func ParseUpdateVulnerabilityDatabase(e partybus.Event) (progress.StagedProgressable, error) { if err := checkEventType(e.Type, event.UpdateVulnerabilityDatabase); err != nil { return nil, err @@ -63,54 +48,79 @@ func ParseUpdateVulnerabilityDatabase(e partybus.Event) (progress.StagedProgress return prog, nil } -func ParseVulnerabilityScanningStarted(e partybus.Event) (*matcher.Monitor, error) { +func ParseVulnerabilityScanningStarted(e partybus.Event) (*monitor.Matching, error) { if err := checkEventType(e.Type, event.VulnerabilityScanningStarted); err != nil { return nil, err } - monitor, ok := e.Value.(matcher.Monitor) + mon, ok := e.Value.(monitor.Matching) if !ok { return nil, newPayloadErr(e.Type, "Value", e.Value) } - return &monitor, nil + return &mon, nil } -func ParseVulnerabilityScanningFinished(e partybus.Event) (presenter.Presenter, error) { - if err := checkEventType(e.Type, event.VulnerabilityScanningFinished); err != nil { +func ParseDatabaseDiffingStarted(e partybus.Event) (*monitor.DBDiff, error) { + if err := checkEventType(e.Type, event.DatabaseDiffingStarted); err != nil { return nil, err } - pres, ok := e.Value.(presenter.Presenter) + mon, ok := e.Value.(monitor.DBDiff) if !ok { return nil, newPayloadErr(e.Type, "Value", e.Value) } - return pres, nil + return &mon, nil } -func ParseNonRootCommandFinished(e partybus.Event) (*string, error) { - if err := checkEventType(e.Type, event.NonRootCommandFinished); err != nil { - return nil, err +func ParseCLIAppUpdateAvailable(e partybus.Event) (string, error) { + if err := checkEventType(e.Type, event.CLIAppUpdateAvailable); err != nil { + return "", err } - result, ok := e.Value.(string) + newVersion, ok := e.Value.(string) if !ok { - return nil, newPayloadErr(e.Type, "Value", e.Value) + return "", newPayloadErr(e.Type, "Value", e.Value) } - return &result, nil + return newVersion, nil } -func ParseDatabaseDiffingStarted(e partybus.Event) (*diffEvents.Monitor, error) { - if err := checkEventType(e.Type, event.DatabaseDiffingStarted); err != nil { - return nil, err +func ParseCLIReport(e partybus.Event) (string, string, error) { + if err := checkEventType(e.Type, event.CLIReport); err != nil { + return "", "", err } - monitor, ok := e.Value.(diffEvents.Monitor) + context, ok := e.Source.(string) if !ok { - return nil, newPayloadErr(e.Type, "Value", e.Value) + // this is optional + context = "" + } + + report, ok := e.Value.(string) + if !ok { + return "", "", newPayloadErr(e.Type, "Value", e.Value) + } + + return context, report, nil +} + +func ParseCLINotification(e partybus.Event) (string, string, error) { + if err := checkEventType(e.Type, event.CLINotification); err != nil { + return "", "", err + } + + context, ok := e.Source.(string) + if !ok { + // this is optional + context = "" + } + + notification, ok := e.Value.(string) + if !ok { + return "", "", newPayloadErr(e.Type, "Value", e.Value) } - return &monitor, nil + return context, notification, nil } diff --git a/grype/lib.go b/grype/lib.go index 25cddd9c5ad..c1c0a0d3d02 100644 --- a/grype/lib.go +++ b/grype/lib.go @@ -8,8 +8,8 @@ import ( "github.com/anchore/grype/internal/log" ) -func SetLogger(logger logger.Logger) { - log.Log = logger +func SetLogger(l logger.Logger) { + log.Set(l) } func SetBus(b *partybus.Bus) { diff --git a/grype/match/ignore_test.go b/grype/match/ignore_test.go index 4490bf8a736..57af7c53d20 100644 --- a/grype/match/ignore_test.go +++ b/grype/match/ignore_test.go @@ -9,8 +9,8 @@ import ( grypeDb "github.com/anchore/grype/grype/db/v5" "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/vulnerability" + "github.com/anchore/syft/syft/file" syftPkg "github.com/anchore/syft/syft/pkg" - "github.com/anchore/syft/syft/source" ) var ( @@ -28,7 +28,7 @@ var ( Name: "dive", Version: "0.5.2", Type: "deb", - Locations: source.NewLocationSet(source.NewLocation("/path/that/has/dive")), + Locations: file.NewLocationSet(file.NewLocation("/path/that/has/dive")), }, }, { @@ -45,7 +45,7 @@ var ( Version: "100.0.50", Language: syftPkg.Ruby, Type: syftPkg.GemPkg, - Locations: source.NewLocationSet(source.NewVirtualLocation("/real/path/with/reach", + Locations: file.NewLocationSet(file.NewVirtualLocation("/real/path/with/reach", "/virtual/path/that/has/reach")), }, }, @@ -63,7 +63,7 @@ var ( Version: "100.0.51", Language: syftPkg.Ruby, Type: syftPkg.GemPkg, - Locations: source.NewLocationSet(source.NewVirtualLocation("/real/path/with/beach", + Locations: file.NewLocationSet(file.NewVirtualLocation("/real/path/with/beach", "/virtual/path/that/has/beach")), }, }, @@ -81,7 +81,7 @@ var ( Version: "100.0.52", Language: syftPkg.Ruby, Type: syftPkg.GemPkg, - Locations: source.NewLocationSet(source.NewVirtualLocation("/real/path/with/speach", + Locations: file.NewLocationSet(file.NewVirtualLocation("/real/path/with/speach", "/virtual/path/that/has/speach")), }, }, @@ -337,9 +337,9 @@ var ( ID: pkg.ID(uuid.NewString()), Name: "a-pkg", Version: "1.0", - Locations: source.NewLocationSet( - source.NewLocation("/some/path"), - source.NewVirtualLocation("/some/path", "/some/virtual/path"), + Locations: file.NewLocationSet( + file.NewLocation("/some/path"), + file.NewVirtualLocation("/some/path", "/some/virtual/path"), ), Type: "rpm", }, diff --git a/grype/matcher/dpkg/matcher_test.go b/grype/matcher/dpkg/matcher_test.go index 054855641fe..b04a5c477de 100644 --- a/grype/matcher/dpkg/matcher_test.go +++ b/grype/matcher/dpkg/matcher_test.go @@ -10,7 +10,7 @@ import ( "github.com/anchore/grype/grype/distro" "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/pkg" - "github.com/anchore/grype/internal" + "github.com/anchore/grype/internal/stringutil" syftPkg "github.com/anchore/syft/syft/pkg" ) @@ -39,7 +39,7 @@ func TestMatcherDpkg_matchBySourceIndirection(t *testing.T) { assert.Len(t, actual, 2, "unexpected indirect matches count") - foundCVEs := internal.NewStringSet() + foundCVEs := stringutil.NewStringSet() for _, a := range actual { foundCVEs.Add(a.Vulnerability.ID) diff --git a/grype/matcher/java/matcher_test.go b/grype/matcher/java/matcher_test.go index d80a8c0073c..b3dcdf64371 100644 --- a/grype/matcher/java/matcher_test.go +++ b/grype/matcher/java/matcher_test.go @@ -9,7 +9,7 @@ import ( "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/pkg" - "github.com/anchore/grype/internal" + "github.com/anchore/grype/internal/stringutil" syftPkg "github.com/anchore/syft/syft/pkg" ) @@ -44,7 +44,7 @@ func TestMatcherJava_matchUpstreamMavenPackage(t *testing.T) { assert.Len(t, actual, 2, "unexpected matches count") - foundCVEs := internal.NewStringSet() + foundCVEs := stringutil.NewStringSet() for _, v := range actual { foundCVEs.Add(v.Vulnerability.ID) diff --git a/grype/matcher/matchers.go b/grype/matcher/matchers.go index 6761bbdf986..7181ac330b8 100644 --- a/grype/matcher/matchers.go +++ b/grype/matcher/matchers.go @@ -7,6 +7,7 @@ import ( grypeDb "github.com/anchore/grype/grype/db/v5" "github.com/anchore/grype/grype/distro" "github.com/anchore/grype/grype/event" + "github.com/anchore/grype/grype/event/monitor" "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/matcher/apk" "github.com/anchore/grype/grype/matcher/dotnet" @@ -28,28 +29,21 @@ import ( syftPkg "github.com/anchore/syft/syft/pkg" ) -type Monitor struct { - PackagesProcessed progress.Monitorable - VulnerabilitiesDiscovered progress.Monitorable - Fixed progress.Monitorable - BySeverity map[vulnerability.Severity]progress.Monitorable -} - -type monitor struct { +type monitorWriter struct { PackagesProcessed *progress.Manual VulnerabilitiesDiscovered *progress.Manual Fixed *progress.Manual BySeverity map[vulnerability.Severity]*progress.Manual } -func newMonitor() (monitor, Monitor) { +func newMonitor() (monitorWriter, monitor.Matching) { manualBySev := make(map[vulnerability.Severity]*progress.Manual) for _, severity := range vulnerability.AllSeverities() { manualBySev[severity] = progress.NewManual(-1) } manualBySev[vulnerability.UnknownSeverity] = progress.NewManual(-1) - m := monitor{ + m := monitorWriter{ PackagesProcessed: progress.NewManual(-1), VulnerabilitiesDiscovered: progress.NewManual(-1), Fixed: progress.NewManual(-1), @@ -61,7 +55,7 @@ func newMonitor() (monitor, Monitor) { monitorableBySev[sev] = manual } - return m, Monitor{ + return m, monitor.Matching{ PackagesProcessed: m.PackagesProcessed, VulnerabilitiesDiscovered: m.VulnerabilitiesDiscovered, Fixed: m.Fixed, @@ -69,7 +63,7 @@ func newMonitor() (monitor, Monitor) { } } -func (m *monitor) SetCompleted() { +func (m *monitorWriter) SetCompleted() { m.PackagesProcessed.SetCompleted() m.VulnerabilitiesDiscovered.SetCompleted() m.Fixed.SetCompleted() @@ -106,7 +100,7 @@ func NewDefaultMatchers(mc Config) []Matcher { } } -func trackMatcher() *monitor { +func trackMatcher() *monitorWriter { writer, reader := newMonitor() bus.Publish(partybus.Event{ @@ -200,7 +194,7 @@ func FindMatches(store interface { return res } -func logListSummary(vl *monitor) { +func logListSummary(vl *monitorWriter) { log.Infof("found %d vulnerabilities for %d packages", vl.VulnerabilitiesDiscovered.Current(), vl.PackagesProcessed.Current()) log.Debugf(" ├── fixed: %d", vl.Fixed.Current()) log.Debugf(" └── matched: %d", vl.VulnerabilitiesDiscovered.Current()) @@ -234,7 +228,7 @@ func logIgnoredMatches(ignored []match.IgnoredMatch) { } } -func updateVulnerabilityList(list *monitor, matches []match.Match, metadataProvider vulnerability.MetadataProvider) { +func updateVulnerabilityList(list *monitorWriter, matches []match.Match, metadataProvider vulnerability.MetadataProvider) { for _, m := range matches { metadata, err := metadataProvider.GetMetadata(m.Vulnerability.ID, m.Vulnerability.Namespace) if err != nil || metadata == nil { diff --git a/grype/matcher/portage/matcher_test.go b/grype/matcher/portage/matcher_test.go index f3f691a0cc2..2c3c769a59f 100644 --- a/grype/matcher/portage/matcher_test.go +++ b/grype/matcher/portage/matcher_test.go @@ -9,7 +9,7 @@ import ( "github.com/anchore/grype/grype/distro" "github.com/anchore/grype/grype/pkg" - "github.com/anchore/grype/internal" + "github.com/anchore/grype/internal/stringutil" syftPkg "github.com/anchore/syft/syft/pkg" ) @@ -33,7 +33,7 @@ func TestMatcherPortage_Match(t *testing.T) { assert.Len(t, actual, 1, "unexpected indirect matches count") - foundCVEs := internal.NewStringSet() + foundCVEs := stringutil.NewStringSet() for _, a := range actual { foundCVEs.Add(a.Vulnerability.ID) diff --git a/grype/pkg/context.go b/grype/pkg/context.go index 4a2e65b56c9..5f46a6f9f9c 100644 --- a/grype/pkg/context.go +++ b/grype/pkg/context.go @@ -6,6 +6,6 @@ import ( ) type Context struct { - Source *source.Metadata + Source *source.Description Distro *linux.Release } diff --git a/grype/pkg/package.go b/grype/pkg/package.go index d8b3675d370..7ee1253c7b9 100644 --- a/grype/pkg/package.go +++ b/grype/pkg/package.go @@ -5,8 +5,8 @@ import ( "regexp" "strings" - "github.com/anchore/grype/internal" "github.com/anchore/grype/internal/log" + "github.com/anchore/grype/internal/stringutil" "github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/cpe" "github.com/anchore/syft/syft/file" @@ -231,7 +231,7 @@ func rpmDataFromPkg(p pkg.Package) (metadata *RpmMetadata, upstreams []UpstreamP } func getNameAndELVersion(sourceRpm string) (string, string) { - groupMatches := internal.MatchCaptureGroups(rpmPackageNamePattern, sourceRpm) + groupMatches := stringutil.MatchCaptureGroups(rpmPackageNamePattern, sourceRpm) version := groupMatches["version"] + "-" + groupMatches["release"] return groupMatches["name"], version } diff --git a/grype/pkg/package_test.go b/grype/pkg/package_test.go index 2e4a5703e08..b863d47ce35 100644 --- a/grype/pkg/package_test.go +++ b/grype/pkg/package_test.go @@ -14,7 +14,6 @@ import ( "github.com/anchore/syft/syft/file" syftFile "github.com/anchore/syft/syft/file" syftPkg "github.com/anchore/syft/syft/pkg" - "github.com/anchore/syft/syft/source" ) func TestNew(t *testing.T) { @@ -550,8 +549,8 @@ func TestFromCollection_DoesNotPanic(t *testing.T) { examplePackage := syftPkg.Package{ Name: "test", Version: "1.2.3", - Locations: source.NewLocationSet( - source.NewLocation("/test-path"), + Locations: file.NewLocationSet( + file.NewLocation("/test-path"), ), Type: syftPkg.NpmPkg, } diff --git a/grype/pkg/provider_test.go b/grype/pkg/provider_test.go index a9aaf1f1899..34dd94432c5 100644 --- a/grype/pkg/provider_test.go +++ b/grype/pkg/provider_test.go @@ -6,8 +6,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/anchore/stereoscope/pkg/imagetest" + "github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/pkg/cataloger" - "github.com/anchore/syft/syft/source" ) func TestProviderLocationExcludes(t *testing.T) { @@ -158,10 +158,10 @@ func Test_filterPackageExclusions(t *testing.T) { t.Run(test.name, func(t *testing.T) { var packages []Package for _, pkg := range test.locations { - locations := source.NewLocationSet() + locations := file.NewLocationSet() for _, l := range pkg { locations.Add( - source.NewVirtualLocation(l, l), + file.NewVirtualLocation(l, l), ) } packages = append(packages, Package{Locations: locations}) @@ -221,7 +221,7 @@ func Test_matchesLocation(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { - matches, err := locationMatches(source.NewVirtualLocation(test.realPath, test.virtualPath), test.match) + matches, err := locationMatches(file.NewVirtualLocation(test.realPath, test.virtualPath), test.match) assert.NoError(t, err) assert.Equal(t, test.expected, matches) }) diff --git a/grype/pkg/syft_provider.go b/grype/pkg/syft_provider.go index 8b541ba2a60..8094f9794b5 100644 --- a/grype/pkg/syft_provider.go +++ b/grype/pkg/syft_provider.go @@ -1,26 +1,19 @@ package pkg import ( + "github.com/anchore/stereoscope/pkg/image" "github.com/anchore/syft/syft" "github.com/anchore/syft/syft/sbom" "github.com/anchore/syft/syft/source" ) func syftProvider(userInput string, config ProviderConfig) ([]Package, Context, *sbom.SBOM, error) { - if config.CatalogingOptions.Search.Scope == "" { - return nil, Context{}, nil, errDoesNotProvide - } - - sourceInput, err := source.ParseInputWithName(userInput, config.Platform, config.Name, config.DefaultImagePullSource) + src, err := getSource(userInput, config) if err != nil { return nil, Context{}, nil, err } - src, cleanup, err := source.New(*sourceInput, config.RegistryOptions, config.Exclusions) - if err != nil { - return nil, Context{}, nil, err - } - defer cleanup() + defer src.Close() catalog, relationships, theDistro, err := syft.CatalogPackages(src, config.CatalogingOptions) if err != nil { @@ -29,14 +22,16 @@ func syftProvider(userInput string, config ProviderConfig) ([]Package, Context, catalog = removePackagesByOverlap(catalog, relationships) + srcDescription := src.Describe() + packages := FromCollection(catalog, config.SynthesisConfig) context := Context{ - Source: &src.Metadata, + Source: &srcDescription, Distro: theDistro, } sbom := &sbom.SBOM{ - Source: src.Metadata, + Source: srcDescription, Relationships: relationships, Artifacts: sbom.Artifacts{ Packages: catalog, @@ -45,3 +40,35 @@ func syftProvider(userInput string, config ProviderConfig) ([]Package, Context, return packages, context, sbom, nil } + +func getSource(userInput string, config ProviderConfig) (source.Source, error) { + if config.CatalogingOptions.Search.Scope == "" { + return nil, errDoesNotProvide + } + + detection, err := source.Detect(userInput, source.DetectConfig{ + DefaultImageSource: config.DefaultImagePullSource, + }) + if err != nil { + return nil, err + } + + var platform *image.Platform + if config.Platform != "" { + platform, err = image.NewPlatform(config.Platform) + if err != nil { + return nil, err + } + } + + return detection.NewSource(source.DetectionSourceConfig{ + Alias: source.Alias{ + Name: config.Name, + }, + RegistryOptions: config.RegistryOptions, + Platform: platform, + Exclude: source.ExcludeConfig{ + Paths: config.Exclusions, + }, + }) +} diff --git a/grype/pkg/syft_sbom_provider_test.go b/grype/pkg/syft_sbom_provider_test.go index 774dda2a874..4d1b0a12164 100644 --- a/grype/pkg/syft_sbom_provider_test.go +++ b/grype/pkg/syft_sbom_provider_test.go @@ -10,6 +10,7 @@ import ( "github.com/stretchr/testify/require" "github.com/anchore/syft/syft/cpe" + "github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/linux" "github.com/anchore/syft/syft/source" ) @@ -26,8 +27,8 @@ func TestParseSyftJSON(t *testing.T) { { Name: "alpine-baselayout", Version: "3.2.0-r6", - Locations: source.NewLocationSet( - source.NewLocationFromCoordinates(source.Coordinates{ + Locations: file.NewLocationSet( + file.NewLocationFromCoordinates(file.Coordinates{ RealPath: "/lib/apk/db/installed", FileSystemID: "sha256:8d3ac3489996423f53d6087c81180006263b79f206d3fdec9e66f0e27ceb8759", }), @@ -50,8 +51,8 @@ func TestParseSyftJSON(t *testing.T) { { Name: "fake", Version: "1.2.0", - Locations: source.NewLocationSet( - source.NewLocationFromCoordinates(source.Coordinates{ + Locations: file.NewLocationSet( + file.NewLocationFromCoordinates(file.Coordinates{ RealPath: "/lib/apk/db/installed", FileSystemID: "sha256:93cf4cfb673c7e16a9e74f731d6767b70b92a0b7c9f59d06efd72fbff535371c", }), @@ -76,8 +77,8 @@ func TestParseSyftJSON(t *testing.T) { { Name: "gmp", Version: "6.2.0-r0", - Locations: source.NewLocationSet( - source.NewLocationFromCoordinates(source.Coordinates{ + Locations: file.NewLocationSet( + file.NewLocationFromCoordinates(file.Coordinates{ RealPath: "/lib/apk/db/installed", FileSystemID: "sha256:93cf4cfb673c7e16a9e74f731d6767b70b92a0b7c9f59d06efd72fbff535371c", }), @@ -101,11 +102,10 @@ func TestParseSyftJSON(t *testing.T) { }, }, Context: Context{ - Source: &source.Metadata{ - Scheme: source.ImageScheme, - ImageMetadata: source.ImageMetadata{ + Source: &source.Description{ + Metadata: source.StereoscopeImageSourceMetadata{ UserInput: "alpine:fake", - Layers: []source.LayerMetadata{ + Layers: []source.StereoscopeLayerMetadata{ { MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip", Digest: "sha256:50644c29ef5a27c9a40c393a73ece2479de78325cae7d762ef3cdc19bf42dd0a", @@ -120,7 +120,6 @@ func TestParseSyftJSON(t *testing.T) { "alpine:fake", }, }, - Path: "", }, Distro: &linux.Release{ Name: "alpine", @@ -138,8 +137,12 @@ func TestParseSyftJSON(t *testing.T) { t.Fatalf("unable to parse: %+v", err) } - context.Source.ImageMetadata.RawConfig = nil - context.Source.ImageMetadata.RawManifest = nil + if m, ok := context.Source.Metadata.(source.StereoscopeImageSourceMetadata); ok { + m.RawConfig = nil + m.RawManifest = nil + + context.Source.Metadata = m + } for _, d := range deep.Equal(test.Packages, pkgs) { if strings.Contains(d, ".ID: ") { @@ -179,8 +182,8 @@ var springImageTestCase = struct { { Name: "charsets", Version: "", - Locations: source.NewLocationSet( - source.NewLocationFromCoordinates(source.Coordinates{ + Locations: file.NewLocationSet( + file.NewLocationFromCoordinates(file.Coordinates{ RealPath: "/usr/lib/jvm/java-8-openjdk-amd64/jre/lib/charsets.jar", FileSystemID: "sha256:a1a6ceadb701ab4e6c93b243dc2a0daedc8cee23a24203845ecccd5784cd1393", }), @@ -199,8 +202,8 @@ var springImageTestCase = struct { { Name: "tomcat-embed-el", Version: "9.0.27", - Locations: source.NewLocationSet( - source.NewLocationFromCoordinates(source.Coordinates{ + Locations: file.NewLocationSet( + file.NewLocationFromCoordinates(file.Coordinates{ RealPath: "/app/libs/tomcat-embed-el-9.0.27.jar", FileSystemID: "sha256:89504f083d3f15322f97ae240df44650203f24427860db1b3d32e66dd05940e4", }), @@ -218,11 +221,10 @@ var springImageTestCase = struct { }, }, Context: Context{ - Source: &source.Metadata{ - Scheme: source.ImageScheme, - ImageMetadata: source.ImageMetadata{ + Source: &source.Description{ + Metadata: source.StereoscopeImageSourceMetadata{ UserInput: "springio/gs-spring-boot-docker:latest", - Layers: []source.LayerMetadata{ + Layers: []source.StereoscopeLayerMetadata{ { MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip", Digest: "sha256:42a3027eaac150d2b8f516100921f4bd83b3dbc20bfe64124f686c072b49c602", @@ -238,7 +240,6 @@ var springImageTestCase = struct { }, RepoDigests: []string{"springio/gs-spring-boot-docker@sha256:39c2ffc784f5f34862e22c1f2ccdbcb62430736114c13f60111eabdb79decb08"}, }, - Path: "", }, Distro: &linux.Release{ Name: "debian", diff --git a/grype/presenter/config.go b/grype/presenter/config.go deleted file mode 100644 index 44be14aa96c..00000000000 --- a/grype/presenter/config.go +++ /dev/null @@ -1,65 +0,0 @@ -package presenter - -import ( - "errors" - "fmt" - "os" - "text/template" - - presenterTemplate "github.com/anchore/grype/grype/presenter/template" -) - -// Config is the presenter domain's configuration data structure. -type Config struct { - format format - templateFilePath string - showSuppressed bool -} - -// ValidatedConfig returns a new, validated presenter.Config. If a valid Config cannot be created using the given input, -// an error is returned. -func ValidatedConfig(output, outputTemplateFile string, showSuppressed bool) (Config, error) { - format := parse(output) - - if format == unknownFormat { - return Config{}, fmt.Errorf("unsupported output format %q, supported formats are: %+v", output, - AvailableFormats) - } - - if format == templateFormat { - if outputTemplateFile == "" { - return Config{}, fmt.Errorf("must specify path to template file when using %q output format", - templateFormat) - } - - if _, err := os.Stat(outputTemplateFile); errors.Is(err, os.ErrNotExist) { - // file does not exist - return Config{}, fmt.Errorf("template file %q does not exist", - outputTemplateFile) - } - - if _, err := os.ReadFile(outputTemplateFile); err != nil { - return Config{}, fmt.Errorf("unable to read template file: %w", err) - } - - if _, err := template.New("").Funcs(presenterTemplate.FuncMap).ParseFiles(outputTemplateFile); err != nil { - return Config{}, fmt.Errorf("unable to parse template: %w", err) - } - - return Config{ - format: format, - templateFilePath: outputTemplateFile, - }, nil - } - - if outputTemplateFile != "" { - return Config{}, fmt.Errorf("specified template file %q, but "+ - "%q output format must be selected in order to use a template file", - outputTemplateFile, templateFormat) - } - - return Config{ - format: format, - showSuppressed: showSuppressed, - }, nil -} diff --git a/grype/presenter/config_test.go b/grype/presenter/config_test.go deleted file mode 100644 index 3b90686be7b..00000000000 --- a/grype/presenter/config_test.go +++ /dev/null @@ -1,67 +0,0 @@ -package presenter - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestValidatedConfig(t *testing.T) { - cases := []struct { - name string - outputValue string - includeSuppressed bool - outputTemplateFileValue string - expectedConfig Config - assertErrExpectation func(assert.TestingT, error, ...interface{}) bool - }{ - { - "valid template config", - "template", - false, - "./template/test-fixtures/test.valid.template", - Config{ - format: "template", - templateFilePath: "./template/test-fixtures/test.valid.template", - }, - assert.NoError, - }, - { - "template file with non-template format", - "json", - false, - "./some/path/to/a/custom.template", - Config{}, - assert.Error, - }, - { - "unknown format", - "some-made-up-format", - false, - "", - Config{}, - assert.Error, - }, - - { - "table format", - "table", - true, - "", - Config{ - format: tableFormat, - showSuppressed: true, - }, - assert.NoError, - }, - } - - for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - actualConfig, actualErr := ValidatedConfig(tc.outputValue, tc.outputTemplateFileValue, tc.includeSuppressed) - - assert.Equal(t, tc.expectedConfig, actualConfig) - tc.assertErrExpectation(t, actualErr) - }) - } -} diff --git a/grype/presenter/cyclonedx/presenter.go b/grype/presenter/cyclonedx/presenter.go index 1069d9106a1..54b1a957b3f 100644 --- a/grype/presenter/cyclonedx/presenter.go +++ b/grype/presenter/cyclonedx/presenter.go @@ -20,7 +20,7 @@ import ( type Presenter struct { results match.Matches packages []pkg.Package - srcMetadata *source.Metadata + src *source.Description metadataProvider vulnerability.MetadataProvider format cyclonedx.BOMFileFormat sbom *sbom.SBOM @@ -32,7 +32,7 @@ func NewJSONPresenter(pb models.PresenterConfig) *Presenter { results: pb.Matches, packages: pb.Packages, metadataProvider: pb.MetadataProvider, - srcMetadata: pb.Context.Source, + src: pb.Context.Source, sbom: pb.SBOM, format: cyclonedx.BOMFileFormatJSON, } @@ -44,7 +44,7 @@ func NewXMLPresenter(pb models.PresenterConfig) *Presenter { results: pb.Matches, packages: pb.Packages, metadataProvider: pb.MetadataProvider, - srcMetadata: pb.Context.Source, + src: pb.Context.Source, sbom: pb.SBOM, format: cyclonedx.BOMFileFormatXML, } diff --git a/grype/presenter/cyclonedx/presenter_test.go b/grype/presenter/cyclonedx/presenter_test.go index bd45a9cc888..c0e4debb900 100644 --- a/grype/presenter/cyclonedx/presenter_test.go +++ b/grype/presenter/cyclonedx/presenter_test.go @@ -8,8 +8,8 @@ import ( "github.com/stretchr/testify/require" "github.com/anchore/go-testutils" + "github.com/anchore/grype/grype/presenter/internal" "github.com/anchore/grype/grype/presenter/models" - "github.com/anchore/syft/syft/source" ) var update = flag.Bool("update", false, "update the *.golden files for cyclonedx presenters") @@ -17,8 +17,8 @@ var update = flag.Bool("update", false, "update the *.golden files for cyclonedx func TestCycloneDxPresenterImage(t *testing.T) { var buffer bytes.Buffer - matches, packages, context, metadataProvider, _, _ := models.GenerateAnalysis(t, source.ImageScheme) - sbom := models.SBOMFromPackages(t, packages) + matches, packages, context, metadataProvider, _, _ := internal.GenerateAnalysis(t, internal.ImageSource) + sbom := internal.SBOMFromPackages(t, packages) pb := models.PresenterConfig{ Matches: matches, Packages: packages, @@ -42,16 +42,16 @@ func TestCycloneDxPresenterImage(t *testing.T) { var expected = testutils.GetGoldenFileContents(t) // remove dynamic values, which are tested independently - actual = models.Redact(actual) - expected = models.Redact(expected) + actual = internal.Redact(actual) + expected = internal.Redact(expected) require.JSONEq(t, string(expected), string(actual)) } func TestCycloneDxPresenterDir(t *testing.T) { var buffer bytes.Buffer - matches, packages, ctx, metadataProvider, _, _ := models.GenerateAnalysis(t, source.DirectoryScheme) - sbom := models.SBOMFromPackages(t, packages) + matches, packages, ctx, metadataProvider, _, _ := internal.GenerateAnalysis(t, internal.DirectorySource) + sbom := internal.SBOMFromPackages(t, packages) pb := models.PresenterConfig{ Matches: matches, Packages: packages, @@ -76,8 +76,8 @@ func TestCycloneDxPresenterDir(t *testing.T) { var expected = testutils.GetGoldenFileContents(t) // remove dynamic values, which are tested independently - actual = models.Redact(actual) - expected = models.Redact(expected) + actual = internal.Redact(actual) + expected = internal.Redact(expected) require.JSONEq(t, string(expected), string(actual)) } diff --git a/grype/presenter/format.go b/grype/presenter/format.go deleted file mode 100644 index d1aa05803a8..00000000000 --- a/grype/presenter/format.go +++ /dev/null @@ -1,71 +0,0 @@ -package presenter - -import ( - "strings" -) - -const ( - unknownFormat format = "unknown" - jsonFormat format = "json" - tableFormat format = "table" - cycloneDXFormat format = "cyclonedx" - cycloneDXJSON format = "cyclonedx-json" - cycloneDXXML format = "cyclonedx-xml" - sarifFormat format = "sarif" - templateFormat format = "template" - - // DEPRECATED <-- TODO: remove in v1.0 - embeddedVEXJSON format = "embedded-cyclonedx-vex-json" - embeddedVEXXML format = "embedded-cyclonedx-vex-xml" -) - -// format is a dedicated type to represent a specific kind of presenter output format. -type format string - -func (f format) String() string { - return string(f) -} - -// parse returns the presenter.format specified by the given user input. -func parse(userInput string) format { - switch strings.ToLower(userInput) { - case "": - return tableFormat - case strings.ToLower(jsonFormat.String()): - return jsonFormat - case strings.ToLower(tableFormat.String()): - return tableFormat - case strings.ToLower(sarifFormat.String()): - return sarifFormat - case strings.ToLower(templateFormat.String()): - return templateFormat - case strings.ToLower(cycloneDXFormat.String()): - return cycloneDXFormat - case strings.ToLower(cycloneDXJSON.String()): - return cycloneDXJSON - case strings.ToLower(cycloneDXXML.String()): - return cycloneDXXML - case strings.ToLower(embeddedVEXJSON.String()): - return cycloneDXJSON - case strings.ToLower(embeddedVEXXML.String()): - return cycloneDXFormat - default: - return unknownFormat - } -} - -// AvailableFormats is a list of presenter format options available to users. -var AvailableFormats = []format{ - jsonFormat, - tableFormat, - cycloneDXFormat, - cycloneDXJSON, - sarifFormat, - templateFormat, -} - -// DeprecatedFormats TODO: remove in v1.0 -var DeprecatedFormats = []format{ - embeddedVEXJSON, - embeddedVEXXML, -} diff --git a/grype/presenter/models/models_helpers.go b/grype/presenter/internal/test_helpers.go similarity index 70% rename from grype/presenter/models/models_helpers.go rename to grype/presenter/internal/test_helpers.go index d8a745a83c2..c1471822b77 100644 --- a/grype/presenter/models/models_helpers.go +++ b/grype/presenter/internal/test_helpers.go @@ -1,4 +1,4 @@ -package models +package internal import ( "regexp" @@ -9,6 +9,7 @@ import ( grypeDb "github.com/anchore/grype/grype/db/v5" "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/pkg" + "github.com/anchore/grype/grype/presenter/models" "github.com/anchore/grype/grype/vulnerability" "github.com/anchore/stereoscope/pkg/image" "github.com/anchore/syft/syft/artifact" @@ -20,17 +21,25 @@ import ( syftSource "github.com/anchore/syft/syft/source" ) -func GenerateAnalysis(t *testing.T, scheme syftSource.Scheme) (match.Matches, []pkg.Package, pkg.Context, vulnerability.MetadataProvider, interface{}, interface{}) { +const ( + DirectorySource SyftSource = "directory" + ImageSource SyftSource = "image" + FileSource SyftSource = "file" +) + +type SyftSource string + +func GenerateAnalysis(t *testing.T, scheme SyftSource) (match.Matches, []pkg.Package, pkg.Context, vulnerability.MetadataProvider, interface{}, interface{}) { t.Helper() packages := generatePackages(t) matches := generateMatches(t, packages[0], packages[1]) context := generateContext(t, scheme) - return matches, packages, context, NewMetadataMock(), nil, nil + return matches, packages, context, models.NewMetadataMock(), nil, nil } -func GenerateAnalysisWithIgnoredMatches(t *testing.T, scheme syftSource.Scheme) (match.Matches, []match.IgnoredMatch, []pkg.Package, pkg.Context, vulnerability.MetadataProvider, interface{}, interface{}) { +func GenerateAnalysisWithIgnoredMatches(t *testing.T, scheme SyftSource) (match.Matches, []match.IgnoredMatch, []pkg.Package, pkg.Context, vulnerability.MetadataProvider, interface{}, interface{}) { t.Helper() packages := generatePackages(t) @@ -38,7 +47,7 @@ func GenerateAnalysisWithIgnoredMatches(t *testing.T, scheme syftSource.Scheme) ignoredMatches := generateIgnoredMatches(t, packages[1]) context := generateContext(t, scheme) - return matches, ignoredMatches, packages, context, NewMetadataMock(), nil, nil + return matches, ignoredMatches, packages, context, models.NewMetadataMock(), nil, nil } func SBOMFromPackages(t *testing.T, packages []pkg.Package) *sbom.SBOM { @@ -260,59 +269,84 @@ func generatePackages(t *testing.T) []pkg.Package { return updatedPkgs } -func generateContext(t *testing.T, scheme syftSource.Scheme) pkg.Context { - var src syftSource.Source - img := image.Image{ - Metadata: image.Metadata{ - ID: "sha256:ab5608d634db2716a297adbfa6a5dd5d8f8f5a7d0cab73649ea7fbb8c8da544f", - ManifestDigest: "sha256:ca738abb87a8d58f112d3400ebb079b61ceae7dc290beb34bda735be4b1941d5", - MediaType: "application/vnd.docker.distribution.manifest.v2+json", - Size: 65, - }, - Layers: []*image.Layer{ - { - Metadata: image.LayerMetadata{ - Digest: "sha256:ca738abb87a8d58f112d3400ebb079b61ceae7dc290beb34bda735be4b1941d5", - MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip", - Size: 22, - }, +//nolint:funlen +func generateContext(t *testing.T, scheme SyftSource) pkg.Context { + var ( + src syftSource.Source + desc syftSource.Description + ) + + switch scheme { + case FileSource: + var err error + src, err = syftSource.NewFromFile(syftSource.FileConfig{ + Path: "user-input", + }) + if err != nil { + t.Fatalf("failed to generate mock file source from mock image: %+v", err) + } + desc = src.Describe() + case ImageSource: + img := image.Image{ + Metadata: image.Metadata{ + ID: "sha256:ab5608d634db2716a297adbfa6a5dd5d8f8f5a7d0cab73649ea7fbb8c8da544f", + ManifestDigest: "sha256:ca738abb87a8d58f112d3400ebb079b61ceae7dc290beb34bda735be4b1941d5", + MediaType: "application/vnd.docker.distribution.manifest.v2+json", + Size: 65, }, - { - Metadata: image.LayerMetadata{ - Digest: "sha256:a05cd9ebf88af96450f1e25367281ab232ac0645f314124fe01af759b93f3006", - MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip", - Size: 16, + Layers: []*image.Layer{ + { + Metadata: image.LayerMetadata{ + Digest: "sha256:ca738abb87a8d58f112d3400ebb079b61ceae7dc290beb34bda735be4b1941d5", + MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip", + Size: 22, + }, }, - }, - { - Metadata: image.LayerMetadata{ - Digest: "sha256:ab5608d634db2716a297adbfa6a5dd5d8f8f5a7d0cab73649ea7fbb8c8da544f", - MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip", - Size: 27, + { + Metadata: image.LayerMetadata{ + Digest: "sha256:a05cd9ebf88af96450f1e25367281ab232ac0645f314124fe01af759b93f3006", + MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip", + Size: 16, + }, + }, + { + Metadata: image.LayerMetadata{ + Digest: "sha256:ab5608d634db2716a297adbfa6a5dd5d8f8f5a7d0cab73649ea7fbb8c8da544f", + MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip", + Size: 27, + }, }, }, - }, - } + } - switch scheme { - case syftSource.ImageScheme: var err error - src, err = syftSource.NewFromImage(&img, "user-input") + src, err = syftSource.NewFromStereoscopeImageObject(&img, "user-input", nil) if err != nil { t.Fatalf("failed to generate mock image source from mock image: %+v", err) } - case syftSource.DirectoryScheme: + desc = src.Describe() + case DirectorySource: + // note: the dir must exist for the source to be created + d := t.TempDir() var err error - src, err = syftSource.NewFromDirectory("/some/path") + src, err = syftSource.NewFromDirectory(syftSource.DirectoryConfig{ + Path: d, + }) + if err != nil { t.Fatalf("failed to generate mock directory source from mock dir: %+v", err) } + desc = src.Describe() + if m, ok := desc.Metadata.(syftSource.DirectorySourceMetadata); ok { + m.Path = "/some/path" + desc.Metadata = m + } default: t.Fatalf("unknown scheme: %s", scheme) } return pkg.Context{ - Source: &src.Metadata, + Source: &desc, Distro: &linux.Release{ Name: "centos", IDLike: []string{ diff --git a/grype/presenter/json/presenter_test.go b/grype/presenter/json/presenter_test.go index 12a471c1fc1..7806ba60ef1 100644 --- a/grype/presenter/json/presenter_test.go +++ b/grype/presenter/json/presenter_test.go @@ -11,6 +11,7 @@ import ( "github.com/anchore/go-testutils" "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/pkg" + "github.com/anchore/grype/grype/presenter/internal" "github.com/anchore/grype/grype/presenter/models" "github.com/anchore/syft/syft/linux" "github.com/anchore/syft/syft/source" @@ -21,7 +22,7 @@ var timestampRegexp = regexp.MustCompile(`"timestamp":\s*"[^"]+"`) func TestJsonImgsPresenter(t *testing.T) { var buffer bytes.Buffer - matches, packages, context, metadataProvider, _, _ := models.GenerateAnalysis(t, source.ImageScheme) + matches, packages, context, metadataProvider, _, _ := internal.GenerateAnalysis(t, internal.ImageSource) pb := models.PresenterConfig{ Matches: matches, @@ -54,7 +55,7 @@ func TestJsonImgsPresenter(t *testing.T) { func TestJsonDirsPresenter(t *testing.T) { var buffer bytes.Buffer - matches, packages, context, metadataProvider, _, _ := models.GenerateAnalysis(t, source.DirectoryScheme) + matches, packages, context, metadataProvider, _, _ := internal.GenerateAnalysis(t, internal.DirectorySource) pb := models.PresenterConfig{ Matches: matches, @@ -91,7 +92,7 @@ func TestEmptyJsonPresenter(t *testing.T) { matches := match.NewMatches() ctx := pkg.Context{ - Source: &source.Metadata{}, + Source: &source.Description{}, Distro: &linux.Release{ ID: "centos", IDLike: []string{"rhel"}, diff --git a/grype/presenter/models/document_test.go b/grype/presenter/models/document_test.go index dc0031f4881..588a074cf89 100644 --- a/grype/presenter/models/document_test.go +++ b/grype/presenter/models/document_test.go @@ -72,9 +72,8 @@ func TestPackagesAreSorted(t *testing.T) { packages := []pkg.Package{pkg1, pkg2} ctx := pkg.Context{ - Source: &syftSource.Metadata{ - Scheme: syftSource.DirectoryScheme, - ImageMetadata: syftSource.ImageMetadata{}, + Source: &syftSource.Description{ + Metadata: syftSource.DirectorySourceMetadata{}, }, Distro: &linux.Release{ ID: "centos", diff --git a/grype/presenter/models/source.go b/grype/presenter/models/source.go index 8648770c40d..bdfecbe3c52 100644 --- a/grype/presenter/models/source.go +++ b/grype/presenter/models/source.go @@ -12,39 +12,38 @@ type source struct { } // newSource creates a new source object to be represented into JSON. -func newSource(src syftSource.Metadata) (source, error) { - switch src.Scheme { - case syftSource.ImageScheme: - metadata := src.ImageMetadata +func newSource(src syftSource.Description) (source, error) { + switch m := src.Metadata.(type) { + case syftSource.StereoscopeImageSourceMetadata: // ensure that empty collections are not shown as null - if metadata.RepoDigests == nil { - metadata.RepoDigests = []string{} + if m.RepoDigests == nil { + m.RepoDigests = []string{} } - if metadata.Tags == nil { - metadata.Tags = []string{} + if m.Tags == nil { + m.Tags = []string{} } return source{ Type: "image", - Target: metadata, + Target: m, }, nil - case syftSource.DirectoryScheme: + case syftSource.DirectorySourceMetadata: return source{ Type: "directory", - Target: src.Path, + Target: m.Path, }, nil - case syftSource.FileScheme: + case syftSource.FileSourceMetadata: return source{ Type: "file", - Target: src.Path, + Target: m.Path, }, nil - case "": + case nil: // we may be showing results from a input source that does not support source information return source{ Type: "unknown", Target: "unknown", }, nil default: - return source{}, fmt.Errorf("unsupported source: %q", src.Scheme) + return source{}, fmt.Errorf("unsupported source: %T", src.Metadata) } } diff --git a/grype/presenter/models/source_test.go b/grype/presenter/models/source_test.go index b1e33a5b132..fc24bef2b26 100644 --- a/grype/presenter/models/source_test.go +++ b/grype/presenter/models/source_test.go @@ -12,14 +12,13 @@ import ( func TestNewSource(t *testing.T) { testCases := []struct { name string - metadata syftSource.Metadata + metadata syftSource.Description expected source }{ { name: "image", - metadata: syftSource.Metadata{ - Scheme: syftSource.ImageScheme, - ImageMetadata: syftSource.ImageMetadata{ + metadata: syftSource.Description{ + Metadata: syftSource.StereoscopeImageSourceMetadata{ UserInput: "abc", ID: "def", ManifestDigest: "abcdef", @@ -28,7 +27,7 @@ func TestNewSource(t *testing.T) { }, expected: source{ Type: "image", - Target: syftSource.ImageMetadata{ + Target: syftSource.StereoscopeImageSourceMetadata{ UserInput: "abc", ID: "def", ManifestDigest: "abcdef", @@ -40,9 +39,10 @@ func TestNewSource(t *testing.T) { }, { name: "directory", - metadata: syftSource.Metadata{ - Scheme: syftSource.DirectoryScheme, - Path: "/foo/bar", + metadata: syftSource.Description{ + Metadata: syftSource.DirectorySourceMetadata{ + Path: "/foo/bar", + }, }, expected: source{ Type: "directory", @@ -51,9 +51,10 @@ func TestNewSource(t *testing.T) { }, { name: "file", - metadata: syftSource.Metadata{ - Scheme: syftSource.FileScheme, - Path: "/foo/bar/test.zip", + metadata: syftSource.Description{ + Metadata: syftSource.FileSourceMetadata{ + Path: "/foo/bar/test.zip", + }, }, expected: source{ Type: "file", @@ -62,18 +63,12 @@ func TestNewSource(t *testing.T) { }, } - var testedSchemes []syftSource.Scheme - for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { actual, err := newSource(testCase.metadata) require.NoError(t, err) assert.Equal(t, testCase.expected, actual) - testedSchemes = append(testedSchemes, testCase.metadata.Scheme) }) } - - // Ensure we have test coverage for all possible syftSource.Scheme values. - assert.ElementsMatchf(t, syftSource.AllSchemes, testedSchemes, "not all scheme values are being tested") } diff --git a/grype/presenter/presenter.go b/grype/presenter/presenter.go index 00256d350fd..72f7a80899c 100644 --- a/grype/presenter/presenter.go +++ b/grype/presenter/presenter.go @@ -1,52 +1,17 @@ package presenter import ( - "io" + "github.com/wagoodman/go-presenter" - "github.com/anchore/grype/grype/presenter/cyclonedx" - "github.com/anchore/grype/grype/presenter/json" "github.com/anchore/grype/grype/presenter/models" - "github.com/anchore/grype/grype/presenter/sarif" - "github.com/anchore/grype/grype/presenter/table" - "github.com/anchore/grype/grype/presenter/template" - "github.com/anchore/grype/internal/log" + "github.com/anchore/grype/internal/format" ) -// Presenter is the main interface other Presenters need to implement -type Presenter interface { - Present(io.Writer) error -} - -// GetPresenter retrieves a Presenter that matches a CLI option -// TODO dependency cycle with presenter package to sub formats -func GetPresenter(c Config, pb models.PresenterConfig) Presenter { - switch c.format { - case jsonFormat: - return json.NewPresenter(pb) - case tableFormat: - return table.NewPresenter(pb, c.showSuppressed) - - // NOTE: cyclonedx is identical to embeddedVEXJSON - // The cyclonedx library only provides two BOM formats: JSON and XML - // These embedded formats will be removed in v1.0 - case cycloneDXFormat: - return cyclonedx.NewXMLPresenter(pb) - case cycloneDXJSON: - return cyclonedx.NewJSONPresenter(pb) - case cycloneDXXML: - return cyclonedx.NewXMLPresenter(pb) - case sarifFormat: - return sarif.NewPresenter(pb) - case templateFormat: - return template.NewPresenter(pb, c.templateFilePath) - // DEPRECATED TODO: remove in v1.0 - case embeddedVEXJSON: - log.Warn("embedded-cyclonedx-vex-json format is deprecated and will be removed in v1.0") - return cyclonedx.NewJSONPresenter(pb) - case embeddedVEXXML: - log.Warn("embedded-cyclonedx-vex-xml format is deprecated and will be removed in v1.0") - return cyclonedx.NewXMLPresenter(pb) - default: - return nil - } +// GetPresenter retrieves a Presenter that matches a CLI option. +// Deprecated: this will be removed in v1.0 +func GetPresenter(f string, templatePath string, showSuppressed bool, pb models.PresenterConfig) presenter.Presenter { + return format.GetPresenter(format.Parse(f), format.PresentationConfig{ + TemplateFilePath: templatePath, + ShowSuppressed: showSuppressed, + }, pb) } diff --git a/grype/presenter/sarif/presenter.go b/grype/presenter/sarif/presenter.go index 63a899df3a3..95eb7c2ff15 100644 --- a/grype/presenter/sarif/presenter.go +++ b/grype/presenter/sarif/presenter.go @@ -22,7 +22,7 @@ import ( type Presenter struct { results match.Matches packages []pkg.Package - srcMetadata *source.Metadata + src *source.Description metadataProvider vulnerability.MetadataProvider } @@ -32,7 +32,7 @@ func NewPresenter(pb models.PresenterConfig) *Presenter { results: pb.Matches, packages: pb.Packages, metadataProvider: pb.MetadataProvider, - srcMetadata: pb.Context.Source, + src: pb.Context.Source, } } @@ -163,10 +163,19 @@ func (pres *Presenter) packagePath(p pkg.Package) string { // inputPath returns a friendlier relative path or absolute path depending on the input, not prefixed by . or ./ func (pres *Presenter) inputPath() string { - if pres.srcMetadata == nil { + if pres.src == nil { return "" } - inputPath := strings.TrimPrefix(pres.srcMetadata.Path, "./") + var inputPath string + switch m := pres.src.Metadata.(type) { + case source.FileSourceMetadata: + inputPath = m.Path + case source.DirectorySourceMetadata: + inputPath = m.Path + default: + return "" + } + inputPath = strings.TrimPrefix(inputPath, "./") if inputPath == "." { return "" } @@ -182,13 +191,17 @@ func (pres *Presenter) locationPath(l file.Location) string { in := pres.inputPath() path = strings.TrimPrefix(path, "./") // trimmed off any ./ and accounted for dir:. for both path and input path - if pres.srcMetadata != nil && pres.srcMetadata.Scheme == source.DirectoryScheme { - if filepath.IsAbs(path) || in == "" { - return path + if pres.src != nil { + _, ok := pres.src.Metadata.(source.DirectorySourceMetadata) + if ok { + if filepath.IsAbs(path) || in == "" { + return path + } + // return a path relative to the cwd, if it's not absolute + return fmt.Sprintf("%s/%s", in, path) } - // return a path relative to the cwd, if it's not absolute - return fmt.Sprintf("%s/%s", in, path) } + return path } @@ -198,9 +211,9 @@ func (pres *Presenter) locations(m match.Match) []*sarif.Location { var logicalLocations []*sarif.LogicalLocation - switch pres.srcMetadata.Scheme { - case source.ImageScheme: - img := pres.srcMetadata.ImageMetadata.UserInput + switch metadata := pres.src.Metadata.(type) { + case source.StereoscopeImageSourceMetadata: + img := metadata.UserInput locations := m.Package.Locations.ToSlice() for _, l := range locations { trimmedPath := strings.TrimPrefix(pres.locationPath(l), "/") @@ -215,15 +228,15 @@ func (pres *Presenter) locations(m match.Match) []*sarif.Location { // TODO we could add configuration to specify the prefix, a user might want to specify an image name and architecture // in the case of multiple vuln scans, for example physicalLocation = fmt.Sprintf("image/%s", physicalLocation) - case source.FileScheme: + case source.FileSourceMetadata: locations := m.Package.Locations.ToSlice() for _, l := range locations { logicalLocations = append(logicalLocations, &sarif.LogicalLocation{ - FullyQualifiedName: sp(fmt.Sprintf("%s:/%s", pres.srcMetadata.Path, pres.locationPath(l))), + FullyQualifiedName: sp(fmt.Sprintf("%s:/%s", metadata.Path, pres.locationPath(l))), Name: sp(l.RealPath), }) } - case source.DirectoryScheme: + case source.DirectorySourceMetadata: // DirectoryScheme is already handled, with input prepended if needed } @@ -399,7 +412,7 @@ func (pres *Presenter) resultMessage(m match.Match) sarif.Message { path := pres.packagePath(m.Package) message := fmt.Sprintf("The path %s reports %s at version %s ", path, m.Package.Name, m.Package.Version) - if pres.srcMetadata.Scheme == source.DirectoryScheme { + if _, ok := pres.src.Metadata.(source.DirectorySourceMetadata); ok { message = fmt.Sprintf("%s which would result in a vulnerable (%s) package installed", message, m.Package.Type) } else { message = fmt.Sprintf("%s which is a vulnerable (%s) package installed in the container", message, m.Package.Type) diff --git a/grype/presenter/sarif/presenter_test.go b/grype/presenter/sarif/presenter_test.go index a83fbc7c1e8..f770f5691a6 100644 --- a/grype/presenter/sarif/presenter_test.go +++ b/grype/presenter/sarif/presenter_test.go @@ -10,8 +10,10 @@ import ( "github.com/anchore/go-testutils" "github.com/anchore/grype/grype/pkg" + "github.com/anchore/grype/grype/presenter/internal" "github.com/anchore/grype/grype/presenter/models" "github.com/anchore/grype/grype/vulnerability" + "github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/source" ) @@ -20,15 +22,15 @@ var update = flag.Bool("update", false, "update .golden files for sarif presente func TestSarifPresenter(t *testing.T) { tests := []struct { name string - scheme source.Scheme + scheme internal.SyftSource }{ { name: "directory", - scheme: source.DirectoryScheme, + scheme: internal.DirectorySource, }, { name: "image", - scheme: source.ImageScheme, + scheme: internal.ImageSource, }, } @@ -36,7 +38,7 @@ func TestSarifPresenter(t *testing.T) { tc := tc t.Run(tc.name, func(t *testing.T) { var buffer bytes.Buffer - matches, packages, context, metadataProvider, _, _ := models.GenerateAnalysis(t, tc.scheme) + matches, packages, context, metadataProvider, _, _ := internal.GenerateAnalysis(t, tc.scheme) pb := models.PresenterConfig{ Matches: matches, @@ -57,8 +59,8 @@ func TestSarifPresenter(t *testing.T) { } var expected = testutils.GetGoldenFileContents(t) - actual = models.Redact(actual) - expected = models.Redact(expected) + actual = internal.Redact(actual) + expected = internal.Redact(expected) if !bytes.Equal(expected, actual) { assert.JSONEq(t, string(expected), string(actual)) @@ -70,83 +72,92 @@ func TestSarifPresenter(t *testing.T) { func Test_locationPath(t *testing.T) { tests := []struct { name string - path string - scheme source.Scheme + metadata any real string virtual string expected string }{ { - name: "dir:.", - scheme: source.DirectoryScheme, - path: ".", + name: "dir:.", + metadata: source.DirectorySourceMetadata{ + Path: ".", + }, real: "/home/usr/file", virtual: "file", expected: "file", }, { - name: "dir:./", - scheme: source.DirectoryScheme, - path: "./", + name: "dir:./", + metadata: source.DirectorySourceMetadata{ + Path: "./", + }, real: "/home/usr/file", virtual: "file", expected: "file", }, { - name: "dir:./someplace", - scheme: source.DirectoryScheme, - path: "./someplace", + name: "dir:./someplace", + metadata: source.DirectorySourceMetadata{ + Path: "./someplace", + }, real: "/home/usr/file", virtual: "file", expected: "someplace/file", }, { - name: "dir:/someplace", - scheme: source.DirectoryScheme, - path: "/someplace", + name: "dir:/someplace", + metadata: source.DirectorySourceMetadata{ + Path: "/someplace", + }, real: "file", expected: "/someplace/file", }, { - name: "dir:/someplace symlink", - scheme: source.DirectoryScheme, - path: "/someplace", + name: "dir:/someplace symlink", + metadata: source.DirectorySourceMetadata{ + Path: "/someplace", + }, real: "/someplace/usr/file", virtual: "file", expected: "/someplace/file", }, { - name: "dir:/someplace absolute", - scheme: source.DirectoryScheme, - path: "/someplace", + name: "dir:/someplace absolute", + metadata: source.DirectorySourceMetadata{ + Path: "/someplace", + }, real: "/usr/file", expected: "/usr/file", }, { - name: "file:/someplace/file", - scheme: source.FileScheme, - path: "/someplace/file", + name: "file:/someplace/file", + metadata: source.FileSourceMetadata{ + Path: "/someplace/file", + }, real: "/usr/file", expected: "/usr/file", }, { - name: "file:/someplace/file relative", - scheme: source.FileScheme, - path: "/someplace/file", + name: "file:/someplace/file relative", + metadata: source.FileSourceMetadata{ + Path: "/someplace/file", + }, real: "file", expected: "file", }, { - name: "image", - scheme: source.ImageScheme, - path: "alpine:latest", + name: "image", + metadata: source.StereoscopeImageSourceMetadata{ + UserInput: "alpine:latest", + }, real: "/etc/file", expected: "/etc/file", }, { - name: "image symlink", - scheme: source.ImageScheme, - path: "alpine:latest", + name: "image symlink", + metadata: source.StereoscopeImageSourceMetadata{ + UserInput: "alpine:latest", + }, real: "/etc/elsewhere/file", virtual: "/etc/file", expected: "/etc/file", @@ -155,15 +166,14 @@ func Test_locationPath(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { - pres := createDirPresenter(t, test.path) - pres.srcMetadata = &source.Metadata{ - Scheme: test.scheme, - Path: test.path, + pres := createDirPresenter(t) + pres.src = &source.Description{ + Metadata: test.metadata, } path := pres.packagePath(pkg.Package{ - Locations: source.NewLocationSet( - source.NewVirtualLocation(test.real, test.virtual), + Locations: file.NewLocationSet( + file.NewVirtualLocation(test.real, test.virtual), ), }) @@ -172,19 +182,21 @@ func Test_locationPath(t *testing.T) { } } -func createDirPresenter(t *testing.T, path string) *Presenter { - matches, packages, _, metadataProvider, _, _ := models.GenerateAnalysis(t, source.DirectoryScheme) - s, err := source.NewFromDirectory(path) +func createDirPresenter(t *testing.T) *Presenter { + matches, packages, _, metadataProvider, _, _ := internal.GenerateAnalysis(t, internal.DirectorySource) + d := t.TempDir() + s, err := source.NewFromDirectory(source.DirectoryConfig{Path: d}) if err != nil { t.Fatal(err) } + desc := s.Describe() pb := models.PresenterConfig{ Matches: matches, Packages: packages, MetadataProvider: metadataProvider, Context: pkg.Context{ - Source: &s.Metadata, + Source: &desc, }, } @@ -196,12 +208,12 @@ func createDirPresenter(t *testing.T, path string) *Presenter { func TestToSarifReport(t *testing.T) { tt := []struct { name string - scheme source.Scheme + scheme internal.SyftSource locations map[string]string }{ { name: "directory", - scheme: source.DirectoryScheme, + scheme: internal.DirectorySource, locations: map[string]string{ "CVE-1999-0001-package-1": "/some/path/somefile-1.txt", "CVE-1999-0002-package-2": "/some/path/somefile-2.txt", @@ -209,7 +221,7 @@ func TestToSarifReport(t *testing.T) { }, { name: "image", - scheme: source.ImageScheme, + scheme: internal.ImageSource, locations: map[string]string{ "CVE-1999-0001-package-1": "image/somefile-1.txt", "CVE-1999-0002-package-2": "image/somefile-2.txt", @@ -222,7 +234,7 @@ func TestToSarifReport(t *testing.T) { t.Run(tc.name, func(t *testing.T) { t.Parallel() - matches, packages, context, metadataProvider, _, _ := models.GenerateAnalysis(t, tc.scheme) + matches, packages, context, metadataProvider, _, _ := internal.GenerateAnalysis(t, tc.scheme) pb := models.PresenterConfig{ Matches: matches, diff --git a/grype/presenter/table/presenter_test.go b/grype/presenter/table/presenter_test.go index cf3682f3ea4..ce44ad3b493 100644 --- a/grype/presenter/table/presenter_test.go +++ b/grype/presenter/table/presenter_test.go @@ -12,10 +12,10 @@ import ( "github.com/anchore/go-testutils" "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/pkg" + "github.com/anchore/grype/grype/presenter/internal" "github.com/anchore/grype/grype/presenter/models" "github.com/anchore/grype/grype/vulnerability" syftPkg "github.com/anchore/syft/syft/pkg" - "github.com/anchore/syft/syft/source" ) var update = flag.Bool("update", false, "update the *.golden files for table presenters") @@ -76,7 +76,7 @@ func TestCreateRow(t *testing.T) { func TestTablePresenter(t *testing.T) { var buffer bytes.Buffer - matches, packages, _, metadataProvider, _, _ := models.GenerateAnalysis(t, source.ImageScheme) + matches, packages, _, metadataProvider, _, _ := internal.GenerateAnalysis(t, internal.ImageSource) pb := models.PresenterConfig{ Matches: matches, @@ -174,7 +174,7 @@ func TestRemoveDuplicateRows(t *testing.T) { func TestHidesIgnoredMatches(t *testing.T) { var buffer bytes.Buffer - matches, ignoredMatches, packages, _, metadataProvider, _, _ := models.GenerateAnalysisWithIgnoredMatches(t, source.ImageScheme) + matches, ignoredMatches, packages, _, metadataProvider, _, _ := internal.GenerateAnalysisWithIgnoredMatches(t, internal.ImageSource) pb := models.PresenterConfig{ Matches: matches, @@ -205,7 +205,7 @@ func TestHidesIgnoredMatches(t *testing.T) { func TestDisplaysIgnoredMatches(t *testing.T) { var buffer bytes.Buffer - matches, ignoredMatches, packages, _, metadataProvider, _, _ := models.GenerateAnalysisWithIgnoredMatches(t, source.ImageScheme) + matches, ignoredMatches, packages, _, metadataProvider, _, _ := internal.GenerateAnalysisWithIgnoredMatches(t, internal.ImageSource) pb := models.PresenterConfig{ Matches: matches, diff --git a/grype/presenter/template/presenter_test.go b/grype/presenter/template/presenter_test.go index 9acaf6cd59e..502f8ca3f75 100644 --- a/grype/presenter/template/presenter_test.go +++ b/grype/presenter/template/presenter_test.go @@ -11,14 +11,14 @@ import ( "github.com/stretchr/testify/require" "github.com/anchore/go-testutils" + "github.com/anchore/grype/grype/presenter/internal" "github.com/anchore/grype/grype/presenter/models" - "github.com/anchore/syft/syft/source" ) var update = flag.Bool("update", false, "update the *.golden files for template presenters") func TestPresenter_Present(t *testing.T) { - matches, packages, context, metadataProvider, appConfig, dbStatus := models.GenerateAnalysis(t, source.ImageScheme) + matches, packages, context, metadataProvider, appConfig, dbStatus := internal.GenerateAnalysis(t, internal.ImageSource) workingDirectory, err := os.Getwd() if err != nil { @@ -53,7 +53,7 @@ func TestPresenter_Present(t *testing.T) { } func TestPresenter_SprigDate_Fails(t *testing.T) { - matches, packages, context, metadataProvider, appConfig, dbStatus := models.GenerateAnalysis(t, source.ImageScheme) + matches, packages, context, metadataProvider, appConfig, dbStatus := internal.GenerateAnalysis(t, internal.ImageSource) workingDirectory, err := os.Getwd() require.NoError(t, err) diff --git a/grype/version/constraint_unit.go b/grype/version/constraint_unit.go index d02b4211492..23aef540ea5 100644 --- a/grype/version/constraint_unit.go +++ b/grype/version/constraint_unit.go @@ -6,7 +6,7 @@ import ( "strconv" "strings" - "github.com/anchore/grype/internal" + "github.com/anchore/grype/internal/stringutil" ) // operator group only matches on range operators (GT, LT, GTE, LTE, E) @@ -19,7 +19,7 @@ type constraintUnit struct { } func parseUnit(phrase string) (*constraintUnit, error) { - match := internal.MatchCaptureGroups(constraintPartPattern, phrase) + match := stringutil.MatchCaptureGroups(constraintPartPattern, phrase) version, exists := match["version"] if !exists { return nil, nil diff --git a/internal/bus/bus.go b/internal/bus/bus.go index fdbca50625e..d8a3d61591f 100644 --- a/internal/bus/bus.go +++ b/internal/bus/bus.go @@ -3,17 +3,13 @@ package bus import "github.com/wagoodman/go-partybus" var publisher partybus.Publisher -var active bool func SetPublisher(p partybus.Publisher) { publisher = p - if p != nil { - active = true - } } func Publish(event partybus.Event) { - if active { + if publisher != nil { publisher.Publish(event) } } diff --git a/internal/bus/helpers.go b/internal/bus/helpers.go new file mode 100644 index 00000000000..53464acc058 --- /dev/null +++ b/internal/bus/helpers.go @@ -0,0 +1,27 @@ +package bus + +import ( + "github.com/wagoodman/go-partybus" + + "github.com/anchore/grype/grype/event" +) + +func Exit() { + Publish(partybus.Event{ + Type: event.CLIExit, + }) +} + +func Report(report string) { + Publish(partybus.Event{ + Type: event.CLIReport, + Value: report, + }) +} + +func Notify(message string) { + Publish(partybus.Event{ + Type: event.CLINotification, + Value: message, + }) +} diff --git a/internal/config/application.go b/internal/config/application.go index b1d2357108d..db32b369c47 100644 --- a/internal/config/application.go +++ b/internal/config/application.go @@ -31,7 +31,7 @@ type parser interface { type Application struct { ConfigPath string `yaml:",omitempty" json:"configPath"` // the location where the application config was read from (either from -c or discovered while loading) Verbosity uint `yaml:"verbosity,omitempty" json:"verbosity" mapstructure:"verbosity"` - Output string `yaml:"output" json:"output" mapstructure:"output"` // -o, the Presenter hint string to use for report formatting + Outputs []string `yaml:"output" json:"output" mapstructure:"output"` // -o, = the Presenter hint string to use for report formatting and the output file File string `yaml:"file" json:"file" mapstructure:"file"` // --file, the file to write report output to Distro string `yaml:"distro" json:"distro" mapstructure:"distro"` // --distro, specify a distro to explicitly use GenerateMissingCPEs bool `yaml:"add-cpes-if-none" json:"add-cpes-if-none" mapstructure:"add-cpes-if-none"` // --add-cpes-if-none, automatically generate CPEs if they are not present in import (e.g. from a 3rd party SPDX document) diff --git a/internal/file/getter.go b/internal/file/getter.go index 3e312ec50e1..216a0965a70 100644 --- a/internal/file/getter.go +++ b/internal/file/getter.go @@ -9,7 +9,7 @@ import ( "github.com/hashicorp/go-getter/helper/url" "github.com/wagoodman/go-progress" - "github.com/anchore/grype/internal" + "github.com/anchore/grype/internal/stringutil" ) var ( @@ -62,7 +62,7 @@ func (g HashiGoGetter) GetToDir(dst, src string, monitors ...*progress.Manual) e func validateHTTPSource(src string) error { // we are ignoring any sources that are not destined to use the http getter object - if !internal.HasAnyOfPrefixes(src, "http://", "https://") { + if !stringutil.HasAnyOfPrefixes(src, "http://", "https://") { return nil } @@ -71,7 +71,7 @@ func validateHTTPSource(src string) error { return fmt.Errorf("bad URL provided %q: %w", src, err) } // only allow for sources with archive extensions - if !internal.HasAnyOfSuffixes(u.Path, archiveExtensions...) { + if !stringutil.HasAnyOfSuffixes(u.Path, archiveExtensions...) { return ErrNonArchiveSource } return nil diff --git a/internal/format/format.go b/internal/format/format.go new file mode 100644 index 00000000000..f6c099b346b --- /dev/null +++ b/internal/format/format.go @@ -0,0 +1,71 @@ +package format + +import ( + "strings" +) + +const ( + UnknownFormat Format = "unknown" + JSONFormat Format = "json" + TableFormat Format = "table" + CycloneDXFormat Format = "cyclonedx" + CycloneDXJSON Format = "cyclonedx-json" + CycloneDXXML Format = "cyclonedx-xml" + SarifFormat Format = "sarif" + TemplateFormat Format = "template" + + // DEPRECATED <-- TODO: remove in v1.0 + EmbeddedVEXJSON Format = "embedded-cyclonedx-vex-json" + EmbeddedVEXXML Format = "embedded-cyclonedx-vex-xml" +) + +// Format is a dedicated type to represent a specific kind of presenter output format. +type Format string + +func (f Format) String() string { + return string(f) +} + +// Parse returns the presenter.format specified by the given user input. +func Parse(userInput string) Format { + switch strings.ToLower(userInput) { + case "": + return TableFormat + case strings.ToLower(JSONFormat.String()): + return JSONFormat + case strings.ToLower(TableFormat.String()): + return TableFormat + case strings.ToLower(SarifFormat.String()): + return SarifFormat + case strings.ToLower(TemplateFormat.String()): + return TemplateFormat + case strings.ToLower(CycloneDXFormat.String()): + return CycloneDXFormat + case strings.ToLower(CycloneDXJSON.String()): + return CycloneDXJSON + case strings.ToLower(CycloneDXXML.String()): + return CycloneDXXML + case strings.ToLower(EmbeddedVEXJSON.String()): + return CycloneDXJSON + case strings.ToLower(EmbeddedVEXXML.String()): + return CycloneDXFormat + default: + return UnknownFormat + } +} + +// AvailableFormats is a list of presenter format options available to users. +var AvailableFormats = []Format{ + JSONFormat, + TableFormat, + CycloneDXFormat, + CycloneDXJSON, + SarifFormat, + TemplateFormat, +} + +// DeprecatedFormats TODO: remove in v1.0 +var DeprecatedFormats = []Format{ + EmbeddedVEXJSON, + EmbeddedVEXXML, +} diff --git a/grype/presenter/format_test.go b/internal/format/format_test.go similarity index 74% rename from grype/presenter/format_test.go rename to internal/format/format_test.go index f26a529747d..665b442b749 100644 --- a/grype/presenter/format_test.go +++ b/internal/format/format_test.go @@ -1,4 +1,4 @@ -package presenter +package format import ( "testing" @@ -9,29 +9,29 @@ import ( func TestParse(t *testing.T) { cases := []struct { input string - expected format + expected Format }{ { "", - tableFormat, + TableFormat, }, { "table", - tableFormat, + TableFormat, }, { "jSOn", - jsonFormat, + JSONFormat, }, { "booboodepoopoo", - unknownFormat, + UnknownFormat, }, } for _, tc := range cases { t.Run(tc.input, func(t *testing.T) { - actual := parse(tc.input) + actual := Parse(tc.input) assert.Equal(t, tc.expected, actual, "unexpected result for input %q", tc.input) }) } diff --git a/internal/format/presenter.go b/internal/format/presenter.go new file mode 100644 index 00000000000..e365eaee587 --- /dev/null +++ b/internal/format/presenter.go @@ -0,0 +1,51 @@ +package format + +import ( + "github.com/wagoodman/go-presenter" + + "github.com/anchore/grype/grype/presenter/cyclonedx" + "github.com/anchore/grype/grype/presenter/json" + "github.com/anchore/grype/grype/presenter/models" + "github.com/anchore/grype/grype/presenter/sarif" + "github.com/anchore/grype/grype/presenter/table" + "github.com/anchore/grype/grype/presenter/template" + "github.com/anchore/grype/internal/log" +) + +type PresentationConfig struct { + TemplateFilePath string + ShowSuppressed bool +} + +// GetPresenter retrieves a Presenter that matches a CLI option +func GetPresenter(format Format, c PresentationConfig, pb models.PresenterConfig) presenter.Presenter { + switch format { + case JSONFormat: + return json.NewPresenter(pb) + case TableFormat: + return table.NewPresenter(pb, c.ShowSuppressed) + + // NOTE: cyclonedx is identical to EmbeddedVEXJSON + // The cyclonedx library only provides two BOM formats: JSON and XML + // These embedded formats will be removed in v1.0 + case CycloneDXFormat: + return cyclonedx.NewXMLPresenter(pb) + case CycloneDXJSON: + return cyclonedx.NewJSONPresenter(pb) + case CycloneDXXML: + return cyclonedx.NewXMLPresenter(pb) + case SarifFormat: + return sarif.NewPresenter(pb) + case TemplateFormat: + return template.NewPresenter(pb, c.TemplateFilePath) + // DEPRECATED TODO: remove in v1.0 + case EmbeddedVEXJSON: + log.Warn("embedded-cyclonedx-vex-json format is deprecated and will be removed in v1.0") + return cyclonedx.NewJSONPresenter(pb) + case EmbeddedVEXXML: + log.Warn("embedded-cyclonedx-vex-xml format is deprecated and will be removed in v1.0") + return cyclonedx.NewXMLPresenter(pb) + default: + return nil + } +} diff --git a/internal/format/writer.go b/internal/format/writer.go new file mode 100644 index 00000000000..feb8f4ecdca --- /dev/null +++ b/internal/format/writer.go @@ -0,0 +1,219 @@ +package format + +import ( + "bytes" + "fmt" + "io" + "os" + "path" + "strings" + + "github.com/hashicorp/go-multierror" + "github.com/mitchellh/go-homedir" + + "github.com/anchore/grype/grype/presenter/models" + "github.com/anchore/grype/internal/bus" + "github.com/anchore/grype/internal/log" +) + +type ScanResultWriter interface { + Write(result models.PresenterConfig) error +} + +var _ ScanResultWriter = (*scanResultMultiWriter)(nil) + +var _ interface { + io.Closer + ScanResultWriter +} = (*scanResultStreamWriter)(nil) + +// MakeScanResultWriter creates a ScanResultWriter for output or returns an error. this will either return a valid writer +// or an error but neither both and if there is no error, ScanResultWriter.Close() should be called +func MakeScanResultWriter(outputs []string, defaultFile string, cfg PresentationConfig) (ScanResultWriter, error) { + outputOptions, err := parseOutputFlags(outputs, defaultFile, cfg) + if err != nil { + return nil, err + } + + writer, err := newMultiWriter(outputOptions...) + if err != nil { + return nil, err + } + + return writer, nil +} + +// MakeScanResultWriterForFormat creates a ScanResultWriter for the given format or returns an error. +func MakeScanResultWriterForFormat(f string, path string, cfg PresentationConfig) (ScanResultWriter, error) { + format := Parse(f) + + if format == UnknownFormat { + return nil, fmt.Errorf(`unsupported output format "%s", supported formats are: %+v`, f, AvailableFormats) + } + + writer, err := newMultiWriter(newWriterDescription(format, path, cfg)) + if err != nil { + return nil, err + } + + return writer, nil +} + +// parseOutputFlags utility to parse command-line option strings and retain the existing behavior of default format and file +func parseOutputFlags(outputs []string, defaultFile string, cfg PresentationConfig) (out []scanResultWriterDescription, errs error) { + // always should have one option -- we generally get the default of "table", but just make sure + if len(outputs) == 0 { + outputs = append(outputs, TableFormat.String()) + } + + for _, name := range outputs { + name = strings.TrimSpace(name) + + // split to at most two parts for = + parts := strings.SplitN(name, "=", 2) + + // the format name is the first part + name = parts[0] + + // default to the --file or empty string if not specified + file := defaultFile + + // If a file is specified as part of the output formatName, use that + if len(parts) > 1 { + file = parts[1] + } + + format := Parse(name) + + if format == UnknownFormat { + errs = multierror.Append(errs, fmt.Errorf(`unsupported output format "%s", supported formats are: %+v`, name, AvailableFormats)) + continue + } + + out = append(out, newWriterDescription(format, file, cfg)) + } + return out, errs +} + +// scanResultWriterDescription Format and path strings used to create ScanResultWriter +type scanResultWriterDescription struct { + Format Format + Path string + Cfg PresentationConfig +} + +func newWriterDescription(f Format, p string, cfg PresentationConfig) scanResultWriterDescription { + expandedPath, err := homedir.Expand(p) + if err != nil { + log.Warnf("could not expand given writer output path=%q: %w", p, err) + // ignore errors + expandedPath = p + } + return scanResultWriterDescription{ + Format: f, + Path: expandedPath, + Cfg: cfg, + } +} + +// scanResultMultiWriter holds a list of child ScanResultWriters to apply all Write and Close operations to +type scanResultMultiWriter struct { + writers []ScanResultWriter +} + +// newMultiWriter create all report writers from input options; if a file is not specified the given defaultWriter is used +func newMultiWriter(options ...scanResultWriterDescription) (_ *scanResultMultiWriter, err error) { + if len(options) == 0 { + return nil, fmt.Errorf("no output options provided") + } + + out := &scanResultMultiWriter{} + + for _, option := range options { + switch len(option.Path) { + case 0: + out.writers = append(out.writers, &scanResultPublisher{ + format: option.Format, + cfg: option.Cfg, + }) + default: + // create any missing subdirectories + dir := path.Dir(option.Path) + if dir != "" { + s, err := os.Stat(dir) + if err != nil { + err = os.MkdirAll(dir, 0755) // maybe should be os.ModePerm ? + if err != nil { + return nil, err + } + } else if !s.IsDir() { + return nil, fmt.Errorf("output path does not contain a valid directory: %s", option.Path) + } + } + fileOut, err := os.OpenFile(option.Path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644) + if err != nil { + return nil, fmt.Errorf("unable to create report file: %w", err) + } + out.writers = append(out.writers, &scanResultStreamWriter{ + format: option.Format, + out: fileOut, + cfg: option.Cfg, + }) + } + } + + return out, nil +} + +// Write writes the result to all writers +func (m *scanResultMultiWriter) Write(s models.PresenterConfig) (errs error) { + for _, w := range m.writers { + err := w.Write(s) + if err != nil { + errs = multierror.Append(errs, fmt.Errorf("unable to write result: %w", err)) + } + } + return errs +} + +// scanResultStreamWriter implements ScanResultWriter for a given format and io.Writer, also providing a close function for cleanup +type scanResultStreamWriter struct { + format Format + cfg PresentationConfig + out io.Writer +} + +// Write the provided result to the data stream +func (w *scanResultStreamWriter) Write(s models.PresenterConfig) error { + pres := GetPresenter(w.format, w.cfg, s) + if err := pres.Present(w.out); err != nil { + return fmt.Errorf("unable to encode result: %w", err) + } + return nil +} + +// Close any resources, such as open files +func (w *scanResultStreamWriter) Close() error { + if closer, ok := w.out.(io.Closer); ok { + return closer.Close() + } + return nil +} + +// scanResultPublisher implements ScanResultWriter that publishes results to the event bus +type scanResultPublisher struct { + format Format + cfg PresentationConfig +} + +// Write the provided result to the data stream +func (w *scanResultPublisher) Write(s models.PresenterConfig) error { + pres := GetPresenter(w.format, w.cfg, s) + buf := &bytes.Buffer{} + if err := pres.Present(buf); err != nil { + return fmt.Errorf("unable to encode result: %w", err) + } + + bus.Report(buf.String()) + return nil +} diff --git a/internal/format/writer_test.go b/internal/format/writer_test.go new file mode 100644 index 00000000000..54c3f2a76de --- /dev/null +++ b/internal/format/writer_test.go @@ -0,0 +1,222 @@ +package format + +import ( + "path/filepath" + "strings" + "testing" + + "github.com/docker/docker/pkg/homedir" + "github.com/stretchr/testify/assert" +) + +func Test_MakeScanResultWriter(t *testing.T) { + tests := []struct { + outputs []string + wantErr assert.ErrorAssertionFunc + }{ + { + outputs: []string{"json"}, + wantErr: assert.NoError, + }, + { + outputs: []string{"table", "json"}, + wantErr: assert.NoError, + }, + { + outputs: []string{"unknown"}, + wantErr: func(t assert.TestingT, err error, bla ...interface{}) bool { + return assert.ErrorContains(t, err, `unsupported output format "unknown", supported formats are: [`) + }, + }, + } + + for _, tt := range tests { + _, err := MakeScanResultWriter(tt.outputs, "", PresentationConfig{}) + tt.wantErr(t, err) + } +} + +func Test_newSBOMMultiWriter(t *testing.T) { + type writerConfig struct { + format string + file string + } + + tmp := t.TempDir() + + testName := func(options []scanResultWriterDescription, err bool) string { + var out []string + for _, opt := range options { + out = append(out, string(opt.Format)+"="+opt.Path) + } + errs := "" + if err { + errs = "(err)" + } + return strings.Join(out, ", ") + errs + } + + tests := []struct { + outputs []scanResultWriterDescription + err bool + expected []writerConfig + }{ + { + outputs: []scanResultWriterDescription{}, + err: true, + }, + { + outputs: []scanResultWriterDescription{ + { + Format: "table", + Path: "", + }, + }, + expected: []writerConfig{ + { + format: "table", + }, + }, + }, + { + outputs: []scanResultWriterDescription{ + { + Format: "json", + }, + }, + expected: []writerConfig{ + { + format: "json", + }, + }, + }, + { + outputs: []scanResultWriterDescription{ + { + Format: "json", + Path: "test-2.json", + }, + }, + expected: []writerConfig{ + { + format: "json", + file: "test-2.json", + }, + }, + }, + { + outputs: []scanResultWriterDescription{ + { + Format: "json", + Path: "test-3/1.json", + }, + { + Format: "spdx-json", + Path: "test-3/2.json", + }, + }, + expected: []writerConfig{ + { + format: "json", + file: "test-3/1.json", + }, + { + format: "spdx-json", + file: "test-3/2.json", + }, + }, + }, + { + outputs: []scanResultWriterDescription{ + { + Format: "text", + }, + { + Format: "spdx-json", + Path: "test-4.json", + }, + }, + expected: []writerConfig{ + { + format: "text", + }, + { + format: "spdx-json", + file: "test-4.json", + }, + }, + }, + } + + for _, test := range tests { + t.Run(testName(test.outputs, test.err), func(t *testing.T) { + outputs := test.outputs + for i := range outputs { + if outputs[i].Path != "" { + outputs[i].Path = tmp + outputs[i].Path + } + } + + mw, err := newMultiWriter(outputs...) + + if test.err { + assert.Error(t, err) + return + } else { + assert.NoError(t, err) + } + + assert.Len(t, mw.writers, len(test.expected)) + + for i, e := range test.expected { + switch w := mw.writers[i].(type) { + case *scanResultStreamWriter: + assert.Equal(t, string(w.format), e.format) + if e.file != "" { + assert.NotNil(t, w.out) + } else { + assert.NotNil(t, w.out) + } + if e.file != "" { + assert.FileExists(t, tmp+e.file) + } + case *scanResultPublisher: + assert.Equal(t, string(w.format), e.format) + default: + t.Fatalf("unknown writer type: %T", w) + } + + } + }) + } +} + +func Test_newSBOMWriterDescription(t *testing.T) { + tests := []struct { + name string + path string + expected string + }{ + { + name: "expand home dir", + path: "~/place.txt", + expected: filepath.Join(homedir.Get(), "place.txt"), + }, + { + name: "passthrough other paths", + path: "/other/place.txt", + expected: "/other/place.txt", + }, + { + name: "no path", + path: "", + expected: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + o := newWriterDescription("table", tt.path, PresentationConfig{}) + assert.Equal(t, tt.expected, o.Path) + }) + } +} diff --git a/internal/log/log.go b/internal/log/log.go index 42ef116e2e2..62b8cda9cea 100644 --- a/internal/log/log.go +++ b/internal/log/log.go @@ -5,65 +5,73 @@ import ( "github.com/anchore/go-logger/adapter/discard" ) -// Log is the singleton used to facilitate logging internally within syft -var Log logger.Logger = discard.New() +// log is the singleton used to facilitate logging internally within syft +var log logger.Logger = discard.New() + +func Set(l logger.Logger) { + log = l +} + +func Get() logger.Logger { + return log +} // Errorf takes a formatted template string and template arguments for the error logging level. func Errorf(format string, args ...interface{}) { - Log.Errorf(format, args...) + log.Errorf(format, args...) } // Error logs the given arguments at the error logging level. func Error(args ...interface{}) { - Log.Error(args...) + log.Error(args...) } // Warnf takes a formatted template string and template arguments for the warning logging level. func Warnf(format string, args ...interface{}) { - Log.Warnf(format, args...) + log.Warnf(format, args...) } // Warn logs the given arguments at the warning logging level. func Warn(args ...interface{}) { - Log.Warn(args...) + log.Warn(args...) } // Infof takes a formatted template string and template arguments for the info logging level. func Infof(format string, args ...interface{}) { - Log.Infof(format, args...) + log.Infof(format, args...) } // Info logs the given arguments at the info logging level. func Info(args ...interface{}) { - Log.Info(args...) + log.Info(args...) } // Debugf takes a formatted template string and template arguments for the debug logging level. func Debugf(format string, args ...interface{}) { - Log.Debugf(format, args...) + log.Debugf(format, args...) } // Debug logs the given arguments at the debug logging level. func Debug(args ...interface{}) { - Log.Debug(args...) + log.Debug(args...) } // Tracef takes a formatted template string and template arguments for the trace logging level. func Tracef(format string, args ...interface{}) { - Log.Tracef(format, args...) + log.Tracef(format, args...) } // Trace logs the given arguments at the trace logging level. func Trace(args ...interface{}) { - Log.Trace(args...) + log.Trace(args...) } // WithFields returns a message logger with multiple key-value fields. func WithFields(fields ...interface{}) logger.MessageLogger { - return Log.WithFields(fields...) + return log.WithFields(fields...) } // Nested returns a new logger with hard coded key-value pairs func Nested(fields ...interface{}) logger.Logger { - return Log.Nested(fields...) + return log.Nested(fields...) } diff --git a/internal/format/color.go b/internal/stringutil/color.go similarity index 93% rename from internal/format/color.go rename to internal/stringutil/color.go index fa1757c3415..373b98e20ea 100644 --- a/internal/format/color.go +++ b/internal/stringutil/color.go @@ -1,4 +1,4 @@ -package format +package stringutil import "fmt" diff --git a/internal/parse.go b/internal/stringutil/parse.go similarity index 95% rename from internal/parse.go rename to internal/stringutil/parse.go index 300825c986e..6b33c718d0f 100644 --- a/internal/parse.go +++ b/internal/stringutil/parse.go @@ -1,4 +1,4 @@ -package internal +package stringutil import "regexp" diff --git a/internal/string_helpers.go b/internal/stringutil/string_helpers.go similarity index 96% rename from internal/string_helpers.go rename to internal/stringutil/string_helpers.go index b29850522c9..1ff56e35c54 100644 --- a/internal/string_helpers.go +++ b/internal/stringutil/string_helpers.go @@ -1,4 +1,4 @@ -package internal +package stringutil import "strings" diff --git a/internal/string_helpers_test.go b/internal/stringutil/string_helpers_test.go similarity index 99% rename from internal/string_helpers_test.go rename to internal/stringutil/string_helpers_test.go index 44fd05aadf2..b5171686801 100644 --- a/internal/string_helpers_test.go +++ b/internal/stringutil/string_helpers_test.go @@ -1,4 +1,4 @@ -package internal +package stringutil import ( "testing" diff --git a/internal/stringset.go b/internal/stringutil/stringset.go similarity index 96% rename from internal/stringset.go rename to internal/stringutil/stringset.go index 41518aaade0..49a73daab22 100644 --- a/internal/stringset.go +++ b/internal/stringutil/stringset.go @@ -1,4 +1,4 @@ -package internal +package stringutil type StringSet map[string]struct{} diff --git a/internal/format/tprint.go b/internal/stringutil/tprint.go similarity index 94% rename from internal/format/tprint.go rename to internal/stringutil/tprint.go index fc75400bc89..8d874f298bf 100644 --- a/internal/format/tprint.go +++ b/internal/stringutil/tprint.go @@ -1,4 +1,4 @@ -package format +package stringutil import ( "bytes" diff --git a/internal/ui/common_event_handlers.go b/internal/ui/common_event_handlers.go deleted file mode 100644 index 126a04fa42d..00000000000 --- a/internal/ui/common_event_handlers.go +++ /dev/null @@ -1,36 +0,0 @@ -package ui - -import ( - "fmt" - "io" - - "github.com/wagoodman/go-partybus" - - grypeEventParsers "github.com/anchore/grype/grype/event/parsers" -) - -func handleVulnerabilityScanningFinished(event partybus.Event, reportOutput io.Writer) error { - // show the report to stdout - pres, err := grypeEventParsers.ParseVulnerabilityScanningFinished(event) - if err != nil { - return fmt.Errorf("bad CatalogerFinished event: %w", err) - } - - if err := pres.Present(reportOutput); err != nil { - return fmt.Errorf("unable to show vulnerability report: %w", err) - } - return nil -} - -func handleNonRootCommandFinished(event partybus.Event, reportOutput io.Writer) error { - // show the report to stdout - result, err := grypeEventParsers.ParseNonRootCommandFinished(event) - if err != nil { - return fmt.Errorf("bad NonRootCommandFinished event: %w", err) - } - - if _, err := reportOutput.Write([]byte(*result)); err != nil { - return fmt.Errorf("unable to show vulnerability report: %w", err) - } - return nil -} diff --git a/internal/ui/components/spinner.go b/internal/ui/components/spinner.go deleted file mode 100644 index 9c93687673e..00000000000 --- a/internal/ui/components/spinner.go +++ /dev/null @@ -1,40 +0,0 @@ -package components - -import ( - "strings" - "sync" -) - -// TODO: move me to a common module (used in multiple repos) - -const SpinnerDotSet = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏" - -type Spinner struct { - index int - charset []string - lock sync.Mutex -} - -func NewSpinner(charset string) Spinner { - return Spinner{ - charset: strings.Split(charset, ""), - } -} - -func (s *Spinner) Current() string { - s.lock.Lock() - defer s.lock.Unlock() - - return s.charset[s.index] -} - -func (s *Spinner) Next() string { - s.lock.Lock() - defer s.lock.Unlock() - c := s.charset[s.index] - s.index++ - if s.index >= len(s.charset) { - s.index = 0 - } - return c -} diff --git a/internal/ui/ephemeral_terminal_ui.go b/internal/ui/ephemeral_terminal_ui.go deleted file mode 100644 index 498bebc22e9..00000000000 --- a/internal/ui/ephemeral_terminal_ui.go +++ /dev/null @@ -1,166 +0,0 @@ -//go:build linux || darwin -// +build linux darwin - -package ui - -import ( - "bytes" - "context" - "fmt" - "io" - "os" - "sync" - - "github.com/wagoodman/go-partybus" - "github.com/wagoodman/jotframe/pkg/frame" - - "github.com/anchore/go-logger" - grypeEvent "github.com/anchore/grype/grype/event" - "github.com/anchore/grype/internal/log" - "github.com/anchore/grype/ui" -) - -// ephemeralTerminalUI provides an "ephemeral" terminal user interface to display the application state dynamically. -// The terminal is placed into raw mode and the cursor is manipulated to allow for a dynamic, multi-line -// UI (provided by the jotframe lib), for this reason all other application mechanisms that write to the screen -// must be suppressed before starting (such as logs); since bytes in the device and in application memory combine to make -// a shared state, bytes coming from elsewhere to the screen will disrupt this state. -// -// This UI is primarily driven off of events from the event bus, creating single-line terminal widgets to represent a -// published element on the event bus, typically polling the element for the latest state. This allows for the UI to -// control update frequency to the screen, provide "liveness" indications that are interpolated between bus events, -// and overall loosely couple the bus events from screen interactions. -// -// By convention, all elements published on the bus should be treated as read-only, and publishers on the bus should -// attempt to enforce this when possible by wrapping complex objects with interfaces to prescribe interactions. Also by -// convention, each new event that the UI should respond to should be added either in this package as a handler function, -// or in the shared ui package as a function on the main handler object. All handler functions should be completed -// processing an event before the ETUI exits (coordinated with a sync.WaitGroup) -type ephemeralTerminalUI struct { - unsubscribe func() error - handler *ui.Handler - waitGroup *sync.WaitGroup - frame *frame.Frame - logBuffer *bytes.Buffer - uiOutput *os.File - reportOutput io.Writer -} - -// NewEphemeralTerminalUI writes all events to a TUI and writes the final report to the given writer. -func NewEphemeralTerminalUI(reportWriter io.Writer) UI { - return &ephemeralTerminalUI{ - handler: ui.NewHandler(), - waitGroup: &sync.WaitGroup{}, - uiOutput: os.Stderr, - reportOutput: reportWriter, - } -} - -func (h *ephemeralTerminalUI) Setup(unsubscribe func() error) error { - h.unsubscribe = unsubscribe - hideCursor(h.uiOutput) - - // prep the logger to not clobber the screen from now on (logrus only) - h.logBuffer = bytes.NewBufferString("") - logController, ok := log.Log.(logger.Controller) - if ok { - logController.SetOutput(h.logBuffer) - } - - return h.openScreen() -} - -func (h *ephemeralTerminalUI) Handle(event partybus.Event) error { - ctx := context.Background() - switch { - case h.handler.RespondsTo(event): - if err := h.handler.Handle(ctx, h.frame, event, h.waitGroup); err != nil { - log.Errorf("unable to show %s event: %+v", event.Type, err) - } - - case event.Type == grypeEvent.AppUpdateAvailable: - if err := handleAppUpdateAvailable(ctx, h.frame, event, h.waitGroup); err != nil { - log.Errorf("unable to show %s event: %+v", event.Type, err) - } - - case event.Type == grypeEvent.VulnerabilityScanningFinished: - // we need to close the screen now since signaling the the presenter is ready means that we - // are about to write bytes to stdout, so we should reset the terminal state first - h.closeScreen(false) - - if err := handleVulnerabilityScanningFinished(event, h.reportOutput); err != nil { - log.Errorf("unable to show %s event: %+v", event.Type, err) - } - - // this is the last expected event, stop listening to events - return h.unsubscribe() - - case event.Type == grypeEvent.NonRootCommandFinished: - h.closeScreen(false) - - if err := handleNonRootCommandFinished(event, h.reportOutput); err != nil { - log.Errorf("unable to show %s event: %+v", event.Type, err) - } - - // this is the last expected event, stop listening to events - return h.unsubscribe() - } - return nil -} - -func (h *ephemeralTerminalUI) openScreen() error { - config := frame.Config{ - PositionPolicy: frame.PolicyFloatForward, - // only report output to stderr, reserve report output for stdout - Output: h.uiOutput, - } - - fr, err := frame.New(config) - if err != nil { - return fmt.Errorf("failed to create the screen object: %w", err) - } - h.frame = fr - - return nil -} - -func (h *ephemeralTerminalUI) closeScreen(force bool) { - // we may have other background processes still displaying progress, wait for them to - // finish before discontinuing dynamic content and showing the final report - if !h.frame.IsClosed() { - if !force { - h.waitGroup.Wait() - } - h.frame.Close() - // TODO: there is a race condition within frame.Close() that sometimes leads to an extra blank line being output - frame.Close() - - // only flush the log on close - h.flushLog() - } -} - -func (h *ephemeralTerminalUI) flushLog() { - // flush any errors to the screen before the report - logController, ok := log.Log.(logger.Controller) - if ok { - fmt.Fprint(logController.GetOutput(), h.logBuffer.String()) - logController.SetOutput(h.uiOutput) - } else { - fmt.Fprint(h.uiOutput, h.logBuffer.String()) - } -} - -func (h *ephemeralTerminalUI) Teardown(force bool) error { - h.closeScreen(force) - showCursor(h.uiOutput) - return nil -} - -func hideCursor(output io.Writer) { - fmt.Fprint(output, "\x1b[?25l") -} - -func showCursor(output io.Writer) { - fmt.Fprint(output, "\x1b[?25h") -} diff --git a/internal/ui/etui_event_handlers.go b/internal/ui/etui_event_handlers.go deleted file mode 100644 index 09b2f66559b..00000000000 --- a/internal/ui/etui_event_handlers.go +++ /dev/null @@ -1,36 +0,0 @@ -//go:build linux || darwin -// +build linux darwin - -package ui - -import ( - "context" - "fmt" - "io" - "sync" - - "github.com/gookit/color" - "github.com/wagoodman/go-partybus" - "github.com/wagoodman/jotframe/pkg/frame" - - grypeEventParsers "github.com/anchore/grype/grype/event/parsers" - "github.com/anchore/grype/internal" - "github.com/anchore/grype/internal/version" -) - -func handleAppUpdateAvailable(_ context.Context, fr *frame.Frame, event partybus.Event, _ *sync.WaitGroup) error { - newVersion, err := grypeEventParsers.ParseAppUpdateAvailable(event) - if err != nil { - return fmt.Errorf("bad %s event: %w", event.Type, err) - } - - line, err := fr.Prepend() - if err != nil { - return err - } - - message := color.Magenta.Sprintf("New version of %s is available: %s (currently running: %s)", internal.ApplicationName, newVersion, version.FromBuild().Version) - _, _ = io.WriteString(line, message) - - return nil -} diff --git a/internal/ui/logger_ui.go b/internal/ui/logger_ui.go deleted file mode 100644 index f0eed4ada81..00000000000 --- a/internal/ui/logger_ui.go +++ /dev/null @@ -1,50 +0,0 @@ -package ui - -import ( - "io" - - "github.com/wagoodman/go-partybus" - - grypeEvent "github.com/anchore/grype/grype/event" - "github.com/anchore/grype/internal/log" -) - -type loggerUI struct { - unsubscribe func() error - reportOutput io.Writer -} - -// NewLoggerUI writes all events to the common application logger and writes the final report to the given writer. -func NewLoggerUI(reportWriter io.Writer) UI { - return &loggerUI{ - reportOutput: reportWriter, - } -} - -func (l *loggerUI) Setup(unsubscribe func() error) error { - l.unsubscribe = unsubscribe - return nil -} - -func (l loggerUI) Handle(event partybus.Event) error { - switch event.Type { - case grypeEvent.VulnerabilityScanningFinished: - if err := handleVulnerabilityScanningFinished(event, l.reportOutput); err != nil { - log.Warnf("unable to show catalog image finished event: %+v", err) - } - case grypeEvent.NonRootCommandFinished: - if err := handleNonRootCommandFinished(event, l.reportOutput); err != nil { - log.Warnf("unable to show command finished event: %+v", err) - } - // ignore all events except for the final events - default: - return nil - } - - // this is the last expected event, stop listening to events - return l.unsubscribe() -} - -func (l loggerUI) Teardown(_ bool) error { - return nil -} diff --git a/internal/ui/select_windows.go b/internal/ui/select_windows.go deleted file mode 100644 index 20e7e58f9a6..00000000000 --- a/internal/ui/select_windows.go +++ /dev/null @@ -1,17 +0,0 @@ -//go:build windows -// +build windows - -package ui - -import ( - "io" -) - -// Select is responsible for determining the specific UI function given select user option, the current platform -// config values, and environment status (such as a TTY being present). The first UI in the returned slice of UIs -// is intended to be used and the UIs that follow are meant to be attempted only in a fallback posture when there -// are environmental problems (e.g. cannot write to the terminal). A writer is provided to capture the output of -// the final SBOM report. -func Select(verbose, quiet bool, reportWriter io.Writer) (uis []UI) { - return append(uis, NewLoggerUI(reportWriter)) -} diff --git a/internal/ui/ui.go b/internal/ui/ui.go deleted file mode 100644 index cb551f1cfcb..00000000000 --- a/internal/ui/ui.go +++ /dev/null @@ -1,11 +0,0 @@ -package ui - -import ( - "github.com/wagoodman/go-partybus" -) - -type UI interface { - Setup(unsubscribe func() error) error - partybus.Handler - Teardown(force bool) error -} diff --git a/main.go b/main.go deleted file mode 100644 index 6378cd3f2b5..00000000000 --- a/main.go +++ /dev/null @@ -1,9 +0,0 @@ -package main - -import ( - "github.com/anchore/grype/cmd" -) - -func main() { - cmd.Execute() -} diff --git a/schema/cyclonedx/Makefile b/schema/cyclonedx/Makefile index c54faff4bf5..7242a959669 100644 --- a/schema/cyclonedx/Makefile +++ b/schema/cyclonedx/Makefile @@ -5,10 +5,10 @@ validate-schema: validate-schema-xml validate-schema-json .PHONY: validate-schema-xml validate-schema-xml: - go run ../../main.go -c ../../test/grype-test-config.yaml ubuntu:latest -vv -o cyclonedx-xml > bom.xml + go run ../../cmd/grype -c ../../test/grype-test-config.yaml ubuntu:latest -vv -o cyclonedx-xml > bom.xml xmllint --noout --schema ./cyclonedx.xsd bom.xml .PHONY: validate-schema-json validate-schema-json: - go run ../../main.go -c ../../test/grype-test-config.yaml ubuntu:latest -vv -o cyclonedx-json > bom.json + go run ../../cmd/grype -c ../../test/grype-test-config.yaml ubuntu:latest -vv -o cyclonedx-json > bom.json ../../.tmp/yajsv -s cyclonedx.json bom.json diff --git a/test/cli/cmd_test.go b/test/cli/cmd_test.go index ad9f79303a0..88f73dd4fb2 100644 --- a/test/cli/cmd_test.go +++ b/test/cli/cmd_test.go @@ -54,6 +54,14 @@ func TestCmd(t *testing.T) { assertInOutput("scope: all-layers"), }, }, + { + name: "vulnerabilities in output on -f with failure", + args: []string{"registry:busybox:1.31", "-f", "high", "--platform", "linux/amd64"}, + assertions: []traitAssertion{ + assertInOutput("CVE-2021-42379"), + assertFailingReturnCode, + }, + }, } for _, test := range tests { diff --git a/test/integration/compare_sbom_input_vs_lib_test.go b/test/integration/compare_sbom_input_vs_lib_test.go index 0caed5ac565..c903f48a496 100644 --- a/test/integration/compare_sbom_input_vs_lib_test.go +++ b/test/integration/compare_sbom_input_vs_lib_test.go @@ -36,12 +36,6 @@ func getListingURL() string { } func TestCompareSBOMInputToLibResults(t *testing.T) { - formats := []sbom.FormatID{ - syft.JSONFormatID, - syft.SPDXJSONFormatID, - syft.SPDXTagValueFormatID, - } - // get a grype DB store, _, closer, err := grype.LoadVulnerabilityDB(db.Config{ DBRootDir: "test-fixtures/grype-db", @@ -77,51 +71,195 @@ func TestCompareSBOMInputToLibResults(t *testing.T) { string(syftPkg.Rpkg), ) observedPkgTypes := strset.New() + testCases := []struct { + name string + image string + format sbom.FormatID + }{ + { + image: "anchore/test_images:vulnerabilities-alpine", + format: syft.JSONFormatID, + name: "alpine-syft-json", + }, + + { + image: "anchore/test_images:vulnerabilities-alpine", + format: syft.SPDXJSONFormatID, + name: "alpine-spdx-json", + }, + + { + image: "anchore/test_images:vulnerabilities-alpine", + format: syft.SPDXTagValueFormatID, + name: "alpine-spdx-tag-value", + }, + + { + image: "anchore/test_images:gems", + format: syft.JSONFormatID, + name: "gems-syft-json", + }, + + { + image: "anchore/test_images:gems", + format: syft.SPDXJSONFormatID, + name: "gems-spdx-json", + }, + + { + image: "anchore/test_images:gems", + format: syft.SPDXTagValueFormatID, + name: "gems-spdx-tag-value", + }, + + { + image: "anchore/test_images:vulnerabilities-debian", + format: syft.JSONFormatID, + name: "debian-syft-json", + }, + + { + image: "anchore/test_images:vulnerabilities-debian", + format: syft.SPDXJSONFormatID, + name: "debian-spdx-json", + }, + + { + image: "anchore/test_images:vulnerabilities-debian", + format: syft.SPDXTagValueFormatID, + name: "debian-spdx-tag-value", + }, + + { + image: "anchore/test_images:vulnerabilities-centos", + format: syft.JSONFormatID, + name: "centos-syft-json", + }, + + { + image: "anchore/test_images:vulnerabilities-centos", + format: syft.SPDXJSONFormatID, + name: "centos-spdx-json", + }, + + { + image: "anchore/test_images:vulnerabilities-centos", + format: syft.SPDXTagValueFormatID, + name: "centos-spdx-tag-value", + }, + + { + image: "anchore/test_images:npm", + format: syft.JSONFormatID, + name: "npm-syft-json", + }, + + { + image: "anchore/test_images:npm", + format: syft.SPDXJSONFormatID, + name: "npm-spdx-json", + }, + + { + image: "anchore/test_images:npm", + format: syft.SPDXTagValueFormatID, + name: "npm-spdx-tag-value", + }, + + { + image: "anchore/test_images:java", + format: syft.JSONFormatID, + name: "java-syft-json", + }, - for _, image := range imagesWithVulnerabilities { - imageArchive := PullThroughImageCache(t, image) + { + image: "anchore/test_images:java", + format: syft.SPDXJSONFormatID, + name: "java-spdx-json", + }, + + { + image: "anchore/test_images:java", + format: syft.SPDXTagValueFormatID, + name: "java-spdx-tag-value", + }, + + { + image: "anchore/test_images:golang-56d52bc", + format: syft.JSONFormatID, + name: "go-syft-json", + }, + + { + image: "anchore/test_images:golang-56d52bc", + format: syft.SPDXJSONFormatID, + name: "go-spdx-json", + }, + + { + image: "anchore/test_images:golang-56d52bc", + format: syft.SPDXTagValueFormatID, + name: "go-spdx-tag-value", + }, + + { + image: "anchore/test_images:arch", + format: syft.JSONFormatID, + name: "arch-syft-json", + }, + + { + image: "anchore/test_images:arch", + format: syft.SPDXJSONFormatID, + name: "arch-spdx-json", + }, + + { + image: "anchore/test_images:arch", + format: syft.SPDXTagValueFormatID, + name: "arch-spdx-tag-value", + }, + } + for _, tc := range testCases { + imageArchive := PullThroughImageCache(t, tc.image) imageSource := fmt.Sprintf("docker-archive:%s", imageArchive) + f := syft.FormatByID(tc.format) + if f == nil { + t.Errorf("Invalid formatID: %s", tc.format) + } + t.Run(tc.name, func(t *testing.T) { + // get SBOM from syft, write to temp file + sbomBytes := getSyftSBOM(t, imageSource, f) + sbomFile, err := os.CreateTemp("", "") + assert.NoError(t, err) + t.Cleanup(func() { + assert.NoError(t, os.Remove(sbomFile.Name())) + }) + _, err = sbomFile.WriteString(sbomBytes) + assert.NoError(t, err) + assert.NoError(t, sbomFile.Close()) + + // get vulns (sbom) + matchesFromSbom, _, pkgsFromSbom, err := grype.FindVulnerabilities(*store, fmt.Sprintf("sbom:%s", sbomFile.Name()), source.SquashedScope, nil) + assert.NoError(t, err) + + // get vulns (image) + matchesFromImage, _, _, err := grype.FindVulnerabilities(*store, imageSource, source.SquashedScope, nil) + assert.NoError(t, err) - for _, formatID := range formats { - f := syft.FormatByID(formatID) - if f == nil { - t.Errorf("Invalid formatID: %s", formatID) + // compare packages (shallow) + matchSetFromSbom := getMatchSet(matchesFromSbom) + matchSetFromImage := getMatchSet(matchesFromImage) + + assert.Empty(t, strset.Difference(matchSetFromSbom, matchSetFromImage).List(), "vulnerabilities present only in results when using sbom as input") + assert.Empty(t, strset.Difference(matchSetFromImage, matchSetFromSbom).List(), "vulnerabilities present only in results when using image as input") + + // track all covered package types (for use after the test) + for _, p := range pkgsFromSbom { + observedPkgTypes.Add(string(p.Type)) } - t.Run(fmt.Sprintf("%s/%s", image, formatID), func(t *testing.T) { - - // get SBOM from syft, write to temp file - sbomBytes := getSyftSBOM(t, imageSource, f) - sbomFile, err := os.CreateTemp("", "") - assert.NoError(t, err) - t.Cleanup(func() { - assert.NoError(t, os.Remove(sbomFile.Name())) - }) - _, err = sbomFile.WriteString(sbomBytes) - assert.NoError(t, err) - assert.NoError(t, sbomFile.Close()) - - // get vulns (sbom) - matchesFromSbom, _, pkgsFromSbom, err := grype.FindVulnerabilities(*store, fmt.Sprintf("sbom:%s", sbomFile.Name()), source.SquashedScope, nil) - assert.NoError(t, err) - - // get vulns (image) - matchesFromImage, _, _, err := grype.FindVulnerabilities(*store, imageSource, source.SquashedScope, nil) - assert.NoError(t, err) - - // compare packages (shallow) - matchSetFromSbom := getMatchSet(matchesFromSbom) - matchSetFromImage := getMatchSet(matchesFromImage) - - assert.Empty(t, strset.Difference(matchSetFromSbom, matchSetFromImage).List(), "vulnerabilities present only in results when using sbom as input") - assert.Empty(t, strset.Difference(matchSetFromImage, matchSetFromSbom).List(), "vulnerabilities present only in results when using image as input") - - // track all covered package types (for use after the test) - for _, p := range pkgsFromSbom { - observedPkgTypes.Add(string(p.Type)) - } - }) - } + }) } // ensure we've covered all package types (-rust, -kb) diff --git a/test/integration/match_by_image_test.go b/test/integration/match_by_image_test.go index 75f84cd84ac..b10e3e3c233 100644 --- a/test/integration/match_by_image_test.go +++ b/test/integration/match_by_image_test.go @@ -16,7 +16,7 @@ import ( "github.com/anchore/grype/grype/pkg" "github.com/anchore/grype/grype/store" "github.com/anchore/grype/grype/vulnerability" - "github.com/anchore/grype/internal" + "github.com/anchore/grype/internal/stringutil" "github.com/anchore/stereoscope/pkg/imagetest" "github.com/anchore/syft/syft" syftPkg "github.com/anchore/syft/syft/pkg" @@ -538,8 +538,8 @@ func addHaskellMatches(t *testing.T, theSource source.Source, catalog *syftPkg.C } func TestMatchByImage(t *testing.T) { - observedMatchers := internal.NewStringSet() - definedMatchers := internal.NewStringSet() + observedMatchers := stringutil.NewStringSet() + definedMatchers := stringutil.NewStringSet() for _, l := range match.AllMatcherTypes { definedMatchers.Add(string(l)) } @@ -606,13 +606,15 @@ func TestMatchByImage(t *testing.T) { userImage := "docker-archive:" + tarPath - sourceInput, err := source.ParseInput(userImage, "") + detection, err := source.Detect(userImage, source.DetectConfig{}) require.NoError(t, err) // this is purely done to help setup mocks - theSource, cleanup, err := source.New(*sourceInput, nil, nil) + theSource, err := detection.NewSource(source.DetectionSourceConfig{}) require.NoError(t, err) - defer cleanup() + t.Cleanup(func() { + require.NoError(t, theSource.Close()) + }) // TODO: relationships are not verified at this time config := cataloger.DefaultConfig() @@ -645,7 +647,7 @@ func TestMatchByImage(t *testing.T) { } // build expected matches from what's discovered from the catalog - expectedMatches := test.expectedFn(*theSource, collection, theStore) + expectedMatches := test.expectedFn(theSource, collection, theStore) assertMatches(t, expectedMatches.Sorted(), actualResults.Sorted()) }) diff --git a/test/integration/utils_test.go b/test/integration/utils_test.go index b76686e4a9d..c86ae5c6264 100644 --- a/test/integration/utils_test.go +++ b/test/integration/utils_test.go @@ -11,6 +11,7 @@ import ( "testing" "github.com/scylladb/go-set/strset" + "github.com/stretchr/testify/require" "github.com/anchore/grype/grype/match" "github.com/anchore/syft/syft" @@ -70,16 +71,18 @@ func saveImage(t testing.TB, imageName string, destPath string) { } func getSyftSBOM(t testing.TB, image string, format sbom.Format) string { - sourceInput, err := source.ParseInput(image, "") + detection, err := source.Detect(image, source.DetectConfig{}) if err != nil { t.Fatalf("could not generate source input for packages command: %+v", err) } - src, cleanup, err := source.New(*sourceInput, nil, nil) + src, err := detection.NewSource(source.DetectionSourceConfig{}) if err != nil { t.Fatalf("can't get the source: %+v", err) } - t.Cleanup(cleanup) + t.Cleanup(func() { + require.NoError(t, src.Close()) + }) config := cataloger.DefaultConfig() config.Search.Scope = source.SquashedScope @@ -91,7 +94,7 @@ func getSyftSBOM(t testing.TB, image string, format sbom.Format) string { Packages: collection, LinuxDistribution: distro, }, - Source: src.Metadata, + Source: src.Describe(), } bytes, err := syft.Encode(s, format) diff --git a/test/quality/.yardstick.yaml b/test/quality/.yardstick.yaml index 1ed3389042d..e580fec4b9d 100644 --- a/test/quality/.yardstick.yaml +++ b/test/quality/.yardstick.yaml @@ -32,7 +32,6 @@ x-ref: - docker.io/anchore/test_images:alpine-package-cpe-vuln-match-bd0aaef@sha256:0825acea611c7c5cc792bc7cc20de44d7413fd287dc5afc4aab9c1891d037b4f - docker.io/alpine:3.2@sha256:ddac200f3ebc9902fb8cfcd599f41feb2151f1118929da21bcef57dc276975f9 - docker.io/centos:6@sha256:3688aa867eb84332460e172b9250c9c198fdfd8d987605fd53f246f498c60bcf - - docker.io/ubuntu:16.10@sha256:8dc9652808dc091400d7d5983949043a9f9c7132b15c14814275d25f94bca18a - docker.io/almalinux:8@sha256:cd49d7250ed7bb194d502d8a3e50bd775055ca275d1d9c2785aea72b890afe6a - docker.io/rockylinux:8@sha256:72afc2e1a20c9ddf56a81c51148ebcbe927c0a879849efe813bee77d69df1dd8 - docker.io/oraclelinux:6@sha256:a06327c0f1d18d753f2a60bb17864c84a850bb6dcbcf5946dd1a8123f6e75495 @@ -42,7 +41,7 @@ x-ref: - registry.access.redhat.com/ubi8@sha256:68fecea0d255ee253acbf0c860eaebb7017ef5ef007c25bee9eeffd29ce85b29 - docker.io/python:3.8.0-slim@sha256:5e96e03a493a54904aa8be573fc0414431afb4f47ac58fbffd03b2a725005364 - docker.io/ghost:5.2.4@sha256:42137b9bd1faf4cdea5933279c48a912d010ef614551aeb0e44308600aa3e69f - - docker.io/node:14.1.0-slim@sha256:d8a88e8e15fd26eee7734c9f60531b5ad2abdc3d663be0d818ed26159db80512 + - docker.io/node:4.2.1-slim@sha256:af31633b87d0dc58c306b04ad9f6ca88104626363c5c085e9962832628eb09ce - docker.io/elastic/kibana:8.5.0@sha256:b9e3e52f61e0a347e38eabe80ba0859f859023bc0cc8836410320aa7eb5d3e02 - docker.io/jenkins/jenkins:2.361.4-lts-jdk11@sha256:6fd5699ab182b5d23d0e3936de6047edc30955a3a92e01c392d5a2fd583efac0 - docker.io/neo4j:4.4.14-community@sha256:fcfcbb026e0e538bf66f5fe5c4b2db3dd4931c3aae07f13a5a8c10e979596256 @@ -52,6 +51,33 @@ x-ref: - docker.io/postgres:13.2@sha256:1a67ab960138c479d66834cd6bcb5b5582c53869e6052dbf4ff48d4a94c13da3 - ghcr.io/chainguard-images/scanner-test@sha256:59bddc101fba0c45d5c093575c6bc5bfee7f0e46ff127e6bb4e5acaaafb525f9 - docker.io/keycloak/keycloak:21.0.2@sha256:347a0d748d05a050dc64b92de2246d2240db6eb38afbc17c3c08d0acb0db1b50 + - docker.io/datawire/aes:3.6.0@sha256:86a072278135462b6cbef70e89894df8f9b20f428b361fda2132fbb442ef257b + - docker.io/bitnami/spark:3.2.4-debian-11-r8@sha256:267d5a6345636710b4b57b7fe981c9760203e7e092c705416310ea30a9806d74 + - mcr.microsoft.com/cbl-mariner/base/core:2.0.20220731-arm64@sha256:51101e635f56032d5afd3fb56d66c7b93b34d5a39ddac01695d62b94473cc34e + - docker.io/grafana/grafana:9.2.4@sha256:a11c6829cdfe7fd791e48ba5b511f3562384361fb4c568ec2d8a5041ac52babe + - docker.io/hashicorp/vault:1.12.0@sha256:09354ca0891f7cee8fbfe8db08c62d2d757fad8ae6c91f2b6cce7a34440e3fae + - docker.io/ubuntu:12.04@sha256:18305429afa14ea462f810146ba44d4363ae76e4c8dfc38288cf73aa07485005 + - docker.io/ubuntu:12.10@sha256:002fba3e3255af10be97ea26e476692a7ebed0bb074a9ab960b2e7a1526b15d7 + - docker.io/ubuntu:13.04@sha256:bc48dd7075ce920ebbaa4581d3200e9fb3aaec31591061d7e3a280a04ef0248c + - docker.io/ubuntu:14.04@sha256:881afbae521c910f764f7187dbfbca3cc10c26f8bafa458c76dda009a901c29d + - docker.io/ubuntu:14.10@sha256:6341c688b4b0b82ec735389b3c97df8cf2831b8cb8bd1856779130a86574ac5c + - docker.io/ubuntu:15.04@sha256:2fb27e433b3ecccea2a14e794875b086711f5d49953ef173d8a03e8707f1510f + - docker.io/ubuntu:15.10@sha256:02521a2d079595241c6793b2044f02eecf294034f31d6e235ac4b2b54ffc41f3 + - docker.io/ubuntu:16.10@sha256:8dc9652808dc091400d7d5983949043a9f9c7132b15c14814275d25f94bca18a + - docker.io/ubuntu:17.04@sha256:213e05583a7cb8756a3f998e6dd65204ddb6b4c128e2175dcdf174cdf1877459 + - docker.io/ubuntu:17.10@sha256:9c4bf7dbb981591d4a1169138471afe4bf5ff5418841d00e30a7ba372e38d6c1 + - docker.io/ubuntu:18.04@sha256:971a12d7e92a23183dead8bfc415aa650e7deb1cc5fed11a3d21f759a891fde9 + - docker.io/ubuntu:18.10@sha256:c95b7b93ccd48c3bfd97f8cac6d5ca8053ced584c9e8e6431861ca30b0d73114 + - docker.io/ubuntu:19.04@sha256:3db17bfc30b41cc18552578f4a66d7010050eb9fdc42bf6c3d82bb0dcdf88d58 + - docker.io/ubuntu:19.10@sha256:6852f9e05c5bce8aa77173fa83ce611f69f271ee3a16503c5f80c199969fd1eb + - docker.io/ubuntu:20.04@sha256:9d42d0e3e57bc067d10a75ee33bdd1a5298e95e5fc3c5d1fce98b455cb879249 + - docker.io/ubuntu:20.10@sha256:754eb641a1ba98a8b483c3595a14164fa4ed7f4b457e1aa05f13ce06f8151723 + - docker.io/ubuntu:21.04@sha256:cb92f03e258f965442b883f5402b310dd7a5ea0a661a865ad02a42bc21234bf7 + - docker.io/ubuntu:21.10@sha256:253908b2844746ab3f3a08fc8a44b9b9fc1efc408d5969b093ab9ffa11eb1894 + - docker.io/ubuntu:22.04@sha256:aa6c2c047467afc828e77e306041b7fa4a65734fe3449a54aa9c280822b0d87d + - docker.io/ubuntu:22.10@sha256:80fb4ea0c0a384a3072a6be1879c342bb636b0d105209535ba893ba75ab38ede + - docker.io/ubuntu:23.04@sha256:09f035f46361d193ded647342903b413d57d05cc06acff8285f9dda9f2d269d5 + - gcr.io/distroless/python3-debian11@sha256:69ae7f133d33faab720af28e78fb45707b623bcbc94ae02a07c633bf053f4b40 # new vulnerabilities are added all of the time, instead of keeping up it's easier to ignore newer entries. # This approach helps tremendously with keeping the analysis relatively stable. diff --git a/test/quality/requirements.txt b/test/quality/requirements.txt index 22018744933..37775e3b3cf 100644 --- a/test/quality/requirements.txt +++ b/test/quality/requirements.txt @@ -1,3 +1,3 @@ -git+https://github.com/anchore/yardstick@b24e38cb80212c5fb58dc7a4fb83b7162df4a5b2 +git+https://github.com/anchore/yardstick@v0.7.0 # ../../../yardstick tabulate==0.9.0 diff --git a/test/quality/vulnerability-match-labels b/test/quality/vulnerability-match-labels index 89eb6bb52d9..0529b016d0e 160000 --- a/test/quality/vulnerability-match-labels +++ b/test/quality/vulnerability-match-labels @@ -1 +1 @@ -Subproject commit 89eb6bb52d979bdd7790de8fd702eab6c5b742c2 +Subproject commit 0529b016d0e3fb579d06cab3fe7e842039e1e3c6 diff --git a/ui/event_handlers.go b/ui/event_handlers.go deleted file mode 100644 index 92f7b35a55b..00000000000 --- a/ui/event_handlers.go +++ /dev/null @@ -1,249 +0,0 @@ -package ui - -import ( - "context" - "fmt" - "io" - "sort" - "strings" - "sync" - "time" - - "github.com/dustin/go-humanize" - "github.com/gookit/color" - "github.com/wagoodman/go-partybus" - "github.com/wagoodman/go-progress" - "github.com/wagoodman/go-progress/format" - "github.com/wagoodman/jotframe/pkg/frame" - - grypeEventParsers "github.com/anchore/grype/grype/event/parsers" - "github.com/anchore/grype/grype/matcher" - "github.com/anchore/grype/grype/vulnerability" - "github.com/anchore/grype/internal/ui/components" - syftUI "github.com/anchore/syft/ui" -) - -const maxBarWidth = 50 -const statusSet = components.SpinnerDotSet // SpinnerCircleOutlineSet -const completedStatus = "✔" // "●" -const tileFormat = color.Bold - -var ( - auxInfoFormat = color.HEX("#777777") - statusTitleTemplate = fmt.Sprintf(" %%s %%-%ds ", syftUI.StatusTitleColumn) -) - -func startProcess() (format.Simple, *components.Spinner) { - width, _ := frame.GetTerminalSize() - barWidth := int(0.25 * float64(width)) - if barWidth > maxBarWidth { - barWidth = maxBarWidth - } - formatter := format.NewSimpleWithTheme(barWidth, format.HeavyNoBarTheme, format.ColorCompleted, format.ColorTodo) - spinner := components.NewSpinner(statusSet) - - return formatter, &spinner -} - -func (r *Handler) UpdateVulnerabilityDatabaseHandler(ctx context.Context, fr *frame.Frame, event partybus.Event, wg *sync.WaitGroup) error { - prog, err := grypeEventParsers.ParseUpdateVulnerabilityDatabase(event) - if err != nil { - return fmt.Errorf("bad FetchImage event: %w", err) - } - - line, err := fr.Prepend() - if err != nil { - return err - } - - wg.Add(1) - - formatter, spinner := startProcess() - stream := progress.Stream(ctx, prog, 150*time.Millisecond) - title := tileFormat.Sprint("Vulnerability DB") - - formatFn := func(p progress.Progress) { - progStr, err := formatter.Format(p) - spin := color.Magenta.Sprint(spinner.Next()) - if err != nil { - _, _ = io.WriteString(line, fmt.Sprintf("Error: %+v", err)) - } else { - var auxInfo string - switch prog.Stage() { - case "downloading": - progStr += " " - auxInfo = auxInfoFormat.Sprintf(" [%s / %s]", humanize.Bytes(uint64(prog.Current())), humanize.Bytes(uint64(prog.Size()))) - default: - progStr = "" - auxInfo = auxInfoFormat.Sprintf("[%s]", prog.Stage()) - } - - _, _ = io.WriteString(line, fmt.Sprintf(statusTitleTemplate+"%s%s", spin, title, progStr, auxInfo)) - } - } - - go func() { - defer wg.Done() - - formatFn(progress.Progress{}) - for p := range stream { - formatFn(p) - } - - spin := color.Green.Sprint(completedStatus) - title = tileFormat.Sprint("Vulnerability DB") - auxInfo := auxInfoFormat.Sprintf("[%s]", prog.Stage()) - _, _ = io.WriteString(line, fmt.Sprintf(statusTitleTemplate+"%s", spin, title, auxInfo)) - }() - return err -} - -func scanningAndSummaryLines(fr *frame.Frame) (scanningLine, summaryLine, fixedLine *frame.Line, err error) { - scanningLine, err = fr.Append() - if err != nil { - return nil, nil, nil, err - } - - summaryLine, err = fr.Append() - if err != nil { - return nil, nil, nil, err - } - - fixedLine, err = fr.Append() - if err != nil { - return nil, nil, nil, err - } - return scanningLine, summaryLine, fixedLine, nil -} - -func assembleProgressMonitors(m *matcher.Monitor) []progress.Monitorable { - ret := []progress.Monitorable{ - m.PackagesProcessed, - m.VulnerabilitiesDiscovered, - } - - allSeverities := append([]vulnerability.Severity{vulnerability.UnknownSeverity}, vulnerability.AllSeverities()...) - for _, sev := range allSeverities { - ret = append(ret, m.BySeverity[sev]) - } - - ret = append(ret, m.Fixed) - - return ret -} - -//nolint:funlen -func (r *Handler) VulnerabilityScanningStartedHandler(ctx context.Context, fr *frame.Frame, event partybus.Event, wg *sync.WaitGroup) error { - monitor, err := grypeEventParsers.ParseVulnerabilityScanningStarted(event) - if err != nil { - return fmt.Errorf("bad %s event: %w", event.Type, err) - } - - scanningLine, summaryLine, fixLine, err := scanningAndSummaryLines(fr) - if err != nil { - return err - } - - wg.Add(1) - - monitors := assembleProgressMonitors(monitor) - - _, spinner := startProcess() - stream := progress.StreamMonitors(ctx, monitors, 50*time.Millisecond) - - title := tileFormat.Sprint("Scanning image...") - branch := "├──" - end := "└──" - - fixTempl := "%d fixed" - - formatFn := func(m *matcher.Monitor, complete bool) { - var spin string - if complete { - spin = color.Green.Sprint(completedStatus) - } else { - spin = color.Magenta.Sprint(spinner.Next()) - } - - auxInfo := auxInfoFormat.Sprintf("[%d vulnerabilities]", m.VulnerabilitiesDiscovered.Current()) - _, _ = io.WriteString(scanningLine, fmt.Sprintf(statusTitleTemplate+"%s", spin, title, auxInfo)) - - var unknownStr string - unknown := m.BySeverity[vulnerability.UnknownSeverity].Current() - if unknown > 0 { - unknownStr = fmt.Sprintf(" (%d unknown)", unknown) - } - - allSeverities := vulnerability.AllSeverities() - sort.Sort(sort.Reverse(vulnerability.Severities(allSeverities))) - - var builder strings.Builder - for idx, sev := range allSeverities { - count := m.BySeverity[sev].Current() - builder.WriteString(fmt.Sprintf("%d %s", count, sev)) - if idx < len(allSeverities)-1 { - builder.WriteString(", ") - } - } - builder.WriteString(unknownStr) - - status := builder.String() - auxInfo2 := auxInfoFormat.Sprintf(" %s %s", branch, status) - _, _ = io.WriteString(summaryLine, auxInfo2) - - fixStatus := fmt.Sprintf(fixTempl, m.Fixed.Current()) - _, _ = io.WriteString(fixLine, auxInfoFormat.Sprintf(" %s %s", end, fixStatus)) - } - - go func() { - defer wg.Done() - - formatFn(monitor, false) - for range stream { - formatFn(monitor, false) - } - formatFn(monitor, true) - }() - - return nil -} - -func (r *Handler) DatabaseDiffingStartedHandler(ctx context.Context, fr *frame.Frame, event partybus.Event, wg *sync.WaitGroup) error { - monitor, err := grypeEventParsers.ParseDatabaseDiffingStarted(event) - if err != nil { - return fmt.Errorf("bad %s event: %w", event.Type, err) - } - - line, err := fr.Append() - if err != nil { - return err - } - - wg.Add(1) - - _, spinner := startProcess() - stream := progress.StreamMonitors(ctx, []progress.Monitorable{monitor.RowsProcessed, monitor.DifferencesDiscovered}, 50*time.Millisecond) - title := tileFormat.Sprint("Diffing databases...") - - formatFn := func(val int64) { - spin := color.Magenta.Sprint(spinner.Next()) - auxInfo := auxInfoFormat.Sprintf("[differences %d]", val) - _, _ = io.WriteString(line, fmt.Sprintf(statusTitleTemplate+"%s", spin, title, auxInfo)) - } - - go func() { - defer wg.Done() - - formatFn(0) - for p := range stream { - formatFn(p[1]) - } - - spin := color.Green.Sprint(completedStatus) - title = tileFormat.Sprint("Diff Complete") - auxInfo := auxInfoFormat.Sprintf("[%d differences]", monitor.DifferencesDiscovered.Current()) - _, _ = io.WriteString(line, fmt.Sprintf(statusTitleTemplate+"%s", spin, title, auxInfo)) - }() - - return nil -} diff --git a/ui/handler.go b/ui/handler.go deleted file mode 100644 index ec244d20ee3..00000000000 --- a/ui/handler.go +++ /dev/null @@ -1,46 +0,0 @@ -package ui - -import ( - "context" - "sync" - - "github.com/wagoodman/go-partybus" - "github.com/wagoodman/jotframe/pkg/frame" - - grypeEvent "github.com/anchore/grype/grype/event" - syftUI "github.com/anchore/syft/ui" -) - -type Handler struct { - syftHandler *syftUI.Handler -} - -func NewHandler() *Handler { - return &Handler{ - syftHandler: syftUI.NewHandler(), - } -} - -func (r *Handler) RespondsTo(event partybus.Event) bool { - switch event.Type { - case grypeEvent.VulnerabilityScanningStarted, - grypeEvent.UpdateVulnerabilityDatabase, - grypeEvent.DatabaseDiffingStarted: - return true - default: - return r.syftHandler.RespondsTo(event) - } -} - -func (r *Handler) Handle(ctx context.Context, fr *frame.Frame, event partybus.Event, wg *sync.WaitGroup) error { - switch event.Type { - case grypeEvent.VulnerabilityScanningStarted: - return r.VulnerabilityScanningStartedHandler(ctx, fr, event, wg) - case grypeEvent.UpdateVulnerabilityDatabase: - return r.UpdateVulnerabilityDatabaseHandler(ctx, fr, event, wg) - case grypeEvent.DatabaseDiffingStarted: - return r.DatabaseDiffingStartedHandler(ctx, fr, event, wg) - default: - return r.syftHandler.Handle(ctx, fr, event, wg) - } -}