Port UI to bubbletea (#1385)

* initial port to bubbletea

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* remove jotframe UI

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* add bubbletea component tests

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* update main.go refs to cmd package

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* move goreleaser build dir to cmd

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* upgrade yardstick for grype source installs and fix post-ui tests

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* ensure stable severity map in UI component test

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* add windows support for tui

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

---------

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
This commit is contained in:
Alex Goodman 2023-07-13 13:13:48 -04:00 committed by GitHub
parent 37f436cfb6
commit ebd4643930
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
70 changed files with 1654 additions and 848 deletions

View file

@ -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

View file

@ -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 <name>` 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 <name>` 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<semantic-version>`
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

View file

@ -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 <bash|zsh|fish>`
- `go run main.go completion <bash|zsh|fish>`
- `go run ./cmd/grype completion <bash|zsh|fish>`
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.

View file

@ -1,4 +1,4 @@
package cmd
package legacy
import (
"encoding/json"

View file

@ -1,4 +1,4 @@
package cmd
package legacy
import (
"context"

View file

@ -1,4 +1,4 @@
package cmd
package legacy
import (
"github.com/spf13/cobra"

View file

@ -1,4 +1,4 @@
package cmd
package legacy
import (
"fmt"

View file

@ -1,4 +1,4 @@
package cmd
package legacy
import (
"fmt"

View file

@ -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)...,
)
}

View file

@ -1,4 +1,4 @@
package cmd
package legacy
import (
"fmt"

View file

@ -1,4 +1,4 @@
package cmd
package legacy
import (
"encoding/json"

View file

@ -1,4 +1,4 @@
package cmd
package legacy
import (
"fmt"

View file

@ -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)...,
)
}

View file

@ -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
}

View file

@ -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()

View file

@ -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)...,
)
}

View file

@ -1,4 +1,4 @@
package cmd
package legacy
import (
"testing"

View file

@ -1,4 +1,4 @@
package cmd
package legacy
import (
"os"

View file

@ -1,4 +1,4 @@
package cmd
package legacy
import (
"fmt"

View file

@ -1,4 +1,4 @@
package cmd
package legacy
import (
"encoding/json"

View file

@ -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]
---

View file

@ -0,0 +1,8 @@
[TestHandler_handleUpdateVulnerabilityDatabase/downloading_DB - 1]
⠋ Vulnerability DB ━━━━━━━━━━━━━━━━━━━━ [current]
---
[TestHandler_handleUpdateVulnerabilityDatabase/DB_download_complete - 1]
✔ Vulnerability DB [current]
---

View file

@ -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
---

View file

@ -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}
}

View file

@ -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)
})
}
}

View file

@ -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}
}

View file

@ -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)
})
}
}

View file

@ -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()
}

View file

@ -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,
}
}

View file

@ -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()
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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]
<my --
-
-
report 1!!>
<report 2>
---
[Test_postUIEventWriter_write/all_events/stderr - 1]
<my notification 1!!
...still notifying>
<notification 2>
<notification 3>
<my app can be updated!!
...to this version>
---
[Test_postUIEventWriter_write/quiet_only_shows_report/stdout - 1]
<report 1>
---
[Test_postUIEventWriter_write/quiet_only_shows_report/stderr - 1]
---

View file

@ -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...)
}

View file

@ -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
}

View file

@ -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<my notification 1!!\n...still notifying>\n\n",
},
{
Type: event.CLINotification,
Value: "<notification 2>",
},
{
Type: event.CLIAppUpdateAvailable,
Value: "\n\n<my app can be updated!!\n...to this version>\n\n",
},
{
Type: event.CLINotification,
Value: "<notification 3>",
},
{
Type: event.CLIReport,
Value: "\n\n<my --\n-\n-\nreport 1!!>\n\n",
},
{
Type: event.CLIReport,
Value: "<report 2>",
},
},
},
{
name: "quiet only shows report",
quiet: true,
events: []partybus.Event{
{
Type: event.CLINotification,
Value: "<notification 1>",
},
{
Type: event.CLIAppUpdateAvailable,
Value: "<app update>",
},
{
Type: event.CLIReport,
Value: "<report 1>",
},
},
},
}
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())
})
})
}
}

View file

@ -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

163
cmd/grype/internal/ui/ui.go Normal file
View file

@ -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()
}

9
cmd/grype/main.go Normal file
View file

@ -0,0 +1,9 @@
package main
import (
"github.com/anchore/grype/cmd/grype/cli/legacy"
)
func main() {
legacy.Execute()
}

View file

@ -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
}
}

28
go.mod
View file

@ -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

61
go.sum
View file

@ -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=

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -1,8 +0,0 @@
package events
import "github.com/wagoodman/go-progress"
type Monitor struct {
RowsProcessed progress.Monitorable
DifferencesDiscovered progress.Monitorable
}

View file

@ -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"
)

View file

@ -0,0 +1,9 @@
package monitor
import "github.com/wagoodman/go-progress"
type DBDiff struct {
Stager progress.Stager
StageProgress progress.Progressable
DifferencesDiscovered progress.Monitorable
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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) {

View file

@ -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 {

View file

@ -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)
}
}

View file

@ -18,3 +18,10 @@ func Report(report string) {
Value: report,
})
}
func Notify(message string) {
Publish(partybus.Event{
Type: event.CLINotification,
Value: message,
})
}

View file

@ -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...)
}

View file

@ -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
}

View file

@ -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")
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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))
}

View file

@ -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
}

View file

@ -1,9 +0,0 @@
package main
import (
"github.com/anchore/grype/cmd"
)
func main() {
cmd.Execute()
}

View file

@ -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

View file

@ -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

View file

@ -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
}

View file

@ -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)
}
}