From ebd4643930984f953df2ff94bd45f012b5167805 Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Thu, 13 Jul 2023 13:13:48 -0400 Subject: [PATCH] Port UI to bubbletea (#1385) * initial port to bubbletea Signed-off-by: Alex Goodman * remove jotframe UI Signed-off-by: Alex Goodman * add bubbletea component tests Signed-off-by: Alex Goodman * update main.go refs to cmd package Signed-off-by: Alex Goodman * move goreleaser build dir to cmd Signed-off-by: Alex Goodman * upgrade yardstick for grype source installs and fix post-ui tests Signed-off-by: Alex Goodman * ensure stable severity map in UI component test Signed-off-by: Alex Goodman * add windows support for tui Signed-off-by: Alex Goodman --------- Signed-off-by: Alex Goodman --- .goreleaser.yaml | 3 + DEVELOPING.md | 4 +- README.md | 2 +- cmd/{ => grype/cli/legacy}/cmd.go | 2 +- cmd/{ => grype/cli/legacy}/completion.go | 2 +- cmd/{ => grype/cli/legacy}/db.go | 2 +- cmd/{ => grype/cli/legacy}/db_check.go | 2 +- cmd/{ => grype/cli/legacy}/db_delete.go | 2 +- cmd/{ => grype/cli/legacy}/db_diff.go | 27 +- cmd/{ => grype/cli/legacy}/db_import.go | 2 +- cmd/{ => grype/cli/legacy}/db_list.go | 2 +- cmd/{ => grype/cli/legacy}/db_status.go | 2 +- cmd/{ => grype/cli/legacy}/db_update.go | 16 +- cmd/{ => grype/cli/legacy}/event_loop.go | 14 +- cmd/{ => grype/cli/legacy}/event_loop_test.go | 37 +-- cmd/{ => grype/cli/legacy}/root.go | 17 +- cmd/{ => grype/cli/legacy}/root_test.go | 2 +- cmd/{ => grype/cli/legacy}/signals.go | 2 +- cmd/{ => grype/cli/legacy}/util.go | 2 +- cmd/{ => grype/cli/legacy}/version.go | 2 +- .../handle_database_diff_started_test.snap | 8 + ...le_update_vulnerability_database_test.snap | 8 + ...e_vulnerability_scanning_started_test.snap | 18 ++ .../cli/ui/handle_database_diff_started.go | 58 ++++ .../ui/handle_database_diff_started_test.go | 96 +++++++ .../handle_update_vulnerability_database.go | 53 ++++ ...ndle_update_vulnerability_database_test.go | 97 +++++++ .../handle_vulnerability_scanning_started.go | 204 ++++++++++++++ ...dle_vulnerability_scanning_started_test.go | 145 ++++++++++ cmd/grype/cli/ui/handler.go | 66 +++++ cmd/grype/cli/ui/new_task_progress.go | 19 ++ cmd/grype/cli/ui/util_test.go | 69 +++++ .../post_ui_event_writer_test.snap | 46 ++++ cmd/grype/internal/ui/no_ui.go | 44 ++++ cmd/grype/internal/ui/post_ui_event_writer.go | 133 ++++++++++ .../internal/ui/post_ui_event_writer_test.go | 95 +++++++ {internal => cmd/grype/internal}/ui/select.go | 27 +- cmd/grype/internal/ui/ui.go | 163 ++++++++++++ cmd/grype/main.go | 9 + cmd/report_writer.go | 33 --- go.mod | 28 +- go.sum | 61 ++++- grype/db/test-fixtures/tls/Makefile | 4 +- grype/db/v3/store/diff.go | 19 +- grype/db/v3/store/store.go | 10 +- grype/db/v4/store/diff.go | 19 +- grype/db/v4/store/store.go | 10 +- grype/db/v5/store/diff.go | 19 +- grype/db/v5/store/store.go | 10 +- grype/differ/events/events.go | 8 - grype/event/event.go | 11 +- grype/event/monitor/db_diff.go | 9 + grype/event/monitor/matching.go | 14 + grype/event/parsers/parsers.go | 34 ++- grype/lib.go | 4 +- grype/matcher/matchers.go | 24 +- internal/bus/bus.go | 6 +- internal/bus/helpers.go | 7 + internal/log/log.go | 36 ++- internal/ui/components/spinner.go | 40 --- internal/ui/ephemeral_terminal_ui.go | 165 ------------ internal/ui/etui_event_handlers.go | 36 --- internal/ui/logger_ui.go | 55 ---- internal/ui/select_windows.go | 17 -- internal/ui/ui.go | 11 - main.go | 9 - schema/cyclonedx/Makefile | 4 +- test/quality/requirements.txt | 2 +- ui/event_handlers.go | 249 ------------------ ui/handler.go | 46 ---- 70 files changed, 1654 insertions(+), 848 deletions(-) rename cmd/{ => grype/cli/legacy}/cmd.go (99%) rename cmd/{ => grype/cli/legacy}/completion.go (99%) rename cmd/{ => grype/cli/legacy}/db.go (91%) rename cmd/{ => grype/cli/legacy}/db_check.go (98%) rename cmd/{ => grype/cli/legacy}/db_delete.go (97%) rename cmd/{ => grype/cli/legacy}/db_diff.go (86%) rename cmd/{ => grype/cli/legacy}/db_import.go (98%) rename cmd/{ => grype/cli/legacy}/db_list.go (99%) rename cmd/{ => grype/cli/legacy}/db_status.go (98%) rename cmd/{ => grype/cli/legacy}/db_update.go (75%) rename cmd/{ => grype/cli/legacy}/event_loop.go (91%) rename cmd/{ => grype/cli/legacy}/event_loop_test.go (92%) rename cmd/{ => grype/cli/legacy}/root.go (98%) rename cmd/{ => grype/cli/legacy}/root_test.go (98%) rename cmd/{ => grype/cli/legacy}/signals.go (95%) rename cmd/{ => grype/cli/legacy}/util.go (93%) rename cmd/{ => grype/cli/legacy}/version.go (99%) create mode 100755 cmd/grype/cli/ui/__snapshots__/handle_database_diff_started_test.snap create mode 100755 cmd/grype/cli/ui/__snapshots__/handle_update_vulnerability_database_test.snap create mode 100755 cmd/grype/cli/ui/__snapshots__/handle_vulnerability_scanning_started_test.snap create mode 100644 cmd/grype/cli/ui/handle_database_diff_started.go create mode 100644 cmd/grype/cli/ui/handle_database_diff_started_test.go create mode 100644 cmd/grype/cli/ui/handle_update_vulnerability_database.go create mode 100644 cmd/grype/cli/ui/handle_update_vulnerability_database_test.go create mode 100644 cmd/grype/cli/ui/handle_vulnerability_scanning_started.go create mode 100644 cmd/grype/cli/ui/handle_vulnerability_scanning_started_test.go create mode 100644 cmd/grype/cli/ui/handler.go create mode 100644 cmd/grype/cli/ui/new_task_progress.go create mode 100644 cmd/grype/cli/ui/util_test.go create mode 100755 cmd/grype/internal/ui/__snapshots__/post_ui_event_writer_test.snap create mode 100644 cmd/grype/internal/ui/no_ui.go create mode 100644 cmd/grype/internal/ui/post_ui_event_writer.go create mode 100644 cmd/grype/internal/ui/post_ui_event_writer_test.go rename {internal => cmd/grype/internal}/ui/select.go (55%) create mode 100644 cmd/grype/internal/ui/ui.go create mode 100644 cmd/grype/main.go delete mode 100644 cmd/report_writer.go delete mode 100644 grype/differ/events/events.go create mode 100644 grype/event/monitor/db_diff.go create mode 100644 grype/event/monitor/matching.go delete mode 100644 internal/ui/components/spinner.go delete mode 100644 internal/ui/ephemeral_terminal_ui.go delete mode 100644 internal/ui/etui_event_handlers.go delete mode 100644 internal/ui/logger_ui.go delete mode 100644 internal/ui/select_windows.go delete mode 100644 internal/ui/ui.go delete mode 100644 main.go delete mode 100644 ui/event_handlers.go delete mode 100644 ui/handler.go diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 6c3274f6..1d9552aa 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 9f40c10a..3c1993ec 100644 --- a/DEVELOPING.md +++ b/DEVELOPING.md @@ -6,7 +6,7 @@ There are a few useful things to know before diving into the codebase. This proj 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: @@ -31,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/README.md b/README.md index 9c37ed98..a59345e4 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 20b4c14e..9c5520c2 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 1ba1bb0f..ae7dd3e0 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 9a56d617..247ed7c5 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 94f9bcec..0c1eec98 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 ff404ab7..4ca36db3 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 86% rename from cmd/db_diff.go rename to cmd/grype/cli/legacy/db_diff.go index a1a165c0..180ec99e 100644 --- a/cmd/db_diff.go +++ b/cmd/grype/cli/legacy/db_diff.go @@ -1,16 +1,15 @@ -package cmd +package legacy import ( - "fmt" - "os" + "strings" "github.com/spf13/cobra" + "github.com/anchore/grype/cmd/grype/internal/ui" "github.com/anchore/grype/grype/db" "github.com/anchore/grype/grype/differ" "github.com/anchore/grype/internal/bus" "github.com/anchore/grype/internal/log" - "github.com/anchore/grype/internal/ui" "github.com/anchore/stereoscope" ) @@ -59,15 +58,19 @@ 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() } @@ -76,16 +79,6 @@ func startDBDiffCmd(base string, target string, deleteDatabases bool) <-chan err } 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 @@ -117,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 fc00261d..400d19d1 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 7fcc2880..e07240b6 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 891c6c08..4ed655f6 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 75% rename from cmd/db_update.go rename to cmd/grype/cli/legacy/db_update.go index 2dd6cc33..1a72ce6d 100644 --- a/cmd/db_update.go +++ b/cmd/grype/cli/legacy/db_update.go @@ -1,14 +1,13 @@ -package cmd +package legacy import ( "fmt" "github.com/spf13/cobra" + "github.com/anchore/grype/cmd/grype/internal/ui" "github.com/anchore/grype/grype/db" "github.com/anchore/grype/internal/bus" - "github.com/anchore/grype/internal/log" - "github.com/anchore/grype/internal/ui" "github.com/anchore/stereoscope" ) @@ -50,20 +49,11 @@ func startDBUpdateCmd() <-chan error { } 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 91% rename from cmd/event_loop.go rename to cmd/grype/cli/legacy/event_loop.go index 596f8b52..51faa023 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,20 @@ import ( "github.com/hashicorp/go-multierror" "github.com/wagoodman/go-partybus" + "github.com/anchore/clio" "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 { 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 } @@ -85,9 +85,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 92% rename from cmd/event_loop_test.go rename to cmd/grype/cli/legacy/event_loop_test.go index 4edb9fe3..489065d3 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() @@ -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() @@ -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() @@ -253,7 +256,7 @@ func Test_eventLoop_handlerError(t *testing.T) { finalEvent := partybus.Event{ Type: event.CLIExit, - Error: fmt.Errorf("unable to create presenter"), + 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() diff --git a/cmd/root.go b/cmd/grype/cli/legacy/root.go similarity index 98% rename from cmd/root.go rename to cmd/grype/cli/legacy/root.go index b39a91fc..7a9e3ddc 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" @@ -37,7 +38,6 @@ import ( "github.com/anchore/grype/internal/format" "github.com/anchore/grype/internal/log" "github.com/anchore/grype/internal/stringutil" - "github.com/anchore/grype/internal/ui" "github.com/anchore/grype/internal/version" "github.com/anchore/stereoscope" "github.com/anchore/syft/syft/linux" @@ -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)..., ) } 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 70f85c15..75b0433f 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 f20379d1..e7013323 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 e9fa6bc3..da07f95a 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 fcdd3379..ce58d161 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 00000000..9386bb14 --- /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 00000000..04810b86 --- /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 00000000..6a0244c4 --- /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 00000000..c8dbecd4 --- /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 00000000..b890f791 --- /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 00000000..938ae9e1 --- /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 00000000..c3b64e15 --- /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 00000000..54ef042b --- /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 00000000..ab23fd7d --- /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 00000000..42321918 --- /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 00000000..036f7b37 --- /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 00000000..81c8525d --- /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 00000000..05ee3a5a --- /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 00000000..e78489fc --- /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 00000000..c008195e --- /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 00000000..1d8509de --- /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 2ecd90cf..5628907e 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 00000000..5814e803 --- /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 00000000..e4cb2d5f --- /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 99cdbfcd..00000000 --- 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 9c848ee8..02e0f3fd 100644 --- a/go.mod +++ b/go.mod @@ -43,7 +43,6 @@ require ( github.com/stretchr/testify v1.8.4 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.10.0 gopkg.in/yaml.v2 v2.4.0 @@ -51,9 +50,14 @@ require ( ) require ( + 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.85.0 + github.com/charmbracelet/bubbletea v0.24.2 + github.com/charmbracelet/lipgloss v0.7.1 + github.com/gkampitakis/go-snaps v0.4.7 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 @@ -73,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 @@ -95,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 @@ -125,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-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 @@ -148,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 @@ -159,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 @@ -167,6 +192,7 @@ 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.11.0 // indirect diff --git a/go.sum b/go.sum index d0b4a091..e9567878 100644 --- a/go.sum +++ b/go.sum @@ -227,6 +227,12 @@ 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/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= @@ -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= @@ -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.7 h1:w1hOUdiKELxiTcRySQLmLuV3KW40mIRw3rzcYJGrb3I= +github.com/gkampitakis/go-snaps v0.4.7/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,8 +671,11 @@ 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.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= @@ -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= @@ -846,8 +897,6 @@ github.com/wagoodman/go-presenter v0.0.0-20211015174752-f9c01afc824b h1:uWNQ0khA 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= @@ -864,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= @@ -885,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= @@ -1155,7 +1205,6 @@ 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= diff --git a/grype/db/test-fixtures/tls/Makefile b/grype/db/test-fixtures/tls/Makefile index f0ca3949..be149c85 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/v3/store/diff.go b/grype/db/v3/store/diff.go index 24a473a4..ff324868 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 e9c1aaa5..212c0e94 100644 --- a/grype/db/v3/store/store.go +++ b/grype/db/v3/store/store.go @@ -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/store/diff.go b/grype/db/v4/store/diff.go index 4cff4b93..88ed92f5 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 fb039344..01b00559 100644 --- a/grype/db/v4/store/store.go +++ b/grype/db/v4/store/store.go @@ -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/store/diff.go b/grype/db/v5/store/diff.go index d1a27800..3aaddd9a 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 24264fdc..e1505ce3 100644 --- a/grype/db/v5/store/store.go +++ b/grype/db/v5/store/store.go @@ -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 f24d6455..00000000 --- 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/event/event.go b/grype/event/event.go index f0d77783..a95f68b2 100644 --- a/grype/event/event.go +++ b/grype/event/event.go @@ -10,9 +10,11 @@ const ( typePrefix = internal.ApplicationName cliTypePrefix = typePrefix + "-cli" - UpdateVulnerabilityDatabase partybus.EventType = "grype-update-vulnerability-database" - VulnerabilityScanningStarted partybus.EventType = "grype-vulnerability-scanning-started" - DatabaseDiffingStarted partybus.EventType = "grype-database-diffing-started" + // 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 @@ -22,6 +24,9 @@ const ( // 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 00000000..3f09bc2d --- /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 00000000..28967521 --- /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 eab840d7..a12b4187 100644 --- a/grype/event/parsers/parsers.go +++ b/grype/event/parsers/parsers.go @@ -6,9 +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/event/monitor" ) type ErrBadPayload struct { @@ -49,30 +48,30 @@ 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 ParseDatabaseDiffingStarted(e partybus.Event) (*diffEvents.Monitor, error) { +func ParseDatabaseDiffingStarted(e partybus.Event) (*monitor.DBDiff, error) { if err := checkEventType(e.Type, event.DatabaseDiffingStarted); err != nil { return nil, err } - monitor, ok := e.Value.(diffEvents.Monitor) + mon, ok := e.Value.(monitor.DBDiff) if !ok { return nil, newPayloadErr(e.Type, "Value", e.Value) } - return &monitor, nil + return &mon, nil } func ParseCLIAppUpdateAvailable(e partybus.Event) (string, error) { @@ -106,3 +105,22 @@ func ParseCLIReport(e partybus.Event) (string, string, error) { 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 context, notification, nil +} diff --git a/grype/lib.go b/grype/lib.go index 25cddd9c..c1c0a0d3 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/matcher/matchers.go b/grype/matcher/matchers.go index 6761bbdf..7181ac33 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/internal/bus/bus.go b/internal/bus/bus.go index fdbca506..d8a3d615 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 index e3a59db7..53464acc 100644 --- a/internal/bus/helpers.go +++ b/internal/bus/helpers.go @@ -18,3 +18,10 @@ func Report(report string) { Value: report, }) } + +func Notify(message string) { + Publish(partybus.Event{ + Type: event.CLINotification, + Value: message, + }) +} diff --git a/internal/log/log.go b/internal/log/log.go index 42ef116e..62b8cda9 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/ui/components/spinner.go b/internal/ui/components/spinner.go deleted file mode 100644 index 9c936876..00000000 --- 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 fb0bd3e3..00000000 --- a/internal/ui/ephemeral_terminal_ui.go +++ /dev/null @@ -1,165 +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/grype/event/parsers" - "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 - reports []string -} - -// 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.CLIAppUpdateAvailable: - if err := handleCLIAppUpdateAvailable(ctx, h.frame, event, h.waitGroup); err != nil { - log.Errorf("unable to show %s event: %+v", event.Type, err) - } - - case event.Type == grypeEvent.CLIReport: - _, report, err := parsers.ParseCLIReport(event) - if err != nil { - log.Errorf("unable to show %s event: %+v", event.Type, err) - break - } - h.reports = append(h.reports, report) - - case event.Type == grypeEvent.CLIExit: - h.closeScreen(false) - - // 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) - for _, report := range h.reports { - if _, err := fmt.Fprintln(h.reportOutput, report); err != nil { - return fmt.Errorf("failed to write report: %w", err) - } - } - 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 efa87c54..00000000 --- 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 handleCLIAppUpdateAvailable(_ context.Context, fr *frame.Frame, event partybus.Event, _ *sync.WaitGroup) error { - newVersion, err := grypeEventParsers.ParseCLIAppUpdateAvailable(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 c3ad9ee5..00000000 --- a/internal/ui/logger_ui.go +++ /dev/null @@ -1,55 +0,0 @@ -package ui - -import ( - "io" - - "github.com/wagoodman/go-partybus" - - grypeEvent "github.com/anchore/grype/grype/event" - "github.com/anchore/grype/grype/event/parsers" - "github.com/anchore/grype/internal/log" -) - -type loggerUI struct { - unsubscribe func() error - reportOutput io.Writer - reports []string -} - -// 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.CLIReport: - _, report, err := parsers.ParseCLIReport(event) - if err != nil { - log.Errorf("unable to show %s event: %+v", event.Type, err) - break - } - l.reports = append(l.reports, report) - case grypeEvent.CLIExit: - // this is the last expected event, stop listening to events - return l.unsubscribe() - } - return nil -} - -func (l loggerUI) Teardown(_ bool) error { - for _, report := range l.reports { - _, err := l.reportOutput.Write([]byte(report)) - if err != nil { - return err - } - } - return nil -} diff --git a/internal/ui/select_windows.go b/internal/ui/select_windows.go deleted file mode 100644 index 20e7e58f..00000000 --- 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 cb551f1c..00000000 --- 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 6378cd3f..00000000 --- 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 c54faff4..7242a959 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/quality/requirements.txt b/test/quality/requirements.txt index 22018744..d38d58a1 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.6.0 # ../../../yardstick tabulate==0.9.0 diff --git a/ui/event_handlers.go b/ui/event_handlers.go deleted file mode 100644 index 92f7b35a..00000000 --- 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 ec244d20..00000000 --- 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) - } -}