From cf78dc9e42a0a1e57882ffdf73a1141fb125a251 Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Wed, 10 Jan 2024 09:34:59 -0500 Subject: [PATCH] with tui interactions for vex discovery Signed-off-by: Alex Goodman --- .../handle_vex_document_discovery_started.go | 55 +++++++++++++++++++ cmd/grype/cli/ui/handler.go | 1 + grype/event/event.go | 1 + grype/event/parsers/parsers.go | 13 +++++ grype/vex/openvex/implementation.go | 46 +++++++++++++++- 5 files changed, 114 insertions(+), 2 deletions(-) create mode 100644 cmd/grype/cli/ui/handle_vex_document_discovery_started.go diff --git a/cmd/grype/cli/ui/handle_vex_document_discovery_started.go b/cmd/grype/cli/ui/handle_vex_document_discovery_started.go new file mode 100644 index 00000000000..9ca70377ec5 --- /dev/null +++ b/cmd/grype/cli/ui/handle_vex_document_discovery_started.go @@ -0,0 +1,55 @@ +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 vexDocumentDiscoveryStager struct { + prog progress.StagedProgressable +} + +func (s vexDocumentDiscoveryStager) 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) handleVexDocumentDiscoveryStarted(e partybus.Event) ([]tea.Model, tea.Cmd) { + prog, err := parsers.ParseVexDocumentDiscoveryStarted(e) + if err != nil { + log.WithFields("error", err).Warn("unable to parse event") + return nil, nil + } + + tsk := m.newTaskProgress( + taskprogress.Title{ + Default: "Search for VEX Documents", + Running: "Searching for VEX Documents", + Success: "Searched for VEX Documents", + }, + taskprogress.WithStagedProgressable(prog), // ignore the static stage provided by the event + taskprogress.WithStager(vexDocumentDiscoveryStager{prog: prog}), + ) + + tsk.HideStageOnSuccess = false + + return []tea.Model{tsk}, tsk.Init() +} diff --git a/cmd/grype/cli/ui/handler.go b/cmd/grype/cli/ui/handler.go index 4232191862a..b7d43abed5f 100644 --- a/cmd/grype/cli/ui/handler.go +++ b/cmd/grype/cli/ui/handler.go @@ -50,6 +50,7 @@ func New(cfg HandlerConfig) *Handler { event.UpdateVulnerabilityDatabase: h.handleUpdateVulnerabilityDatabase, event.VulnerabilityScanningStarted: h.handleVulnerabilityScanningStarted, event.DatabaseDiffingStarted: h.handleDatabaseDiffStarted, + event.VexDocumentDiscoveryStarted: h.handleVexDocumentDiscoveryStarted, }) return h diff --git a/grype/event/event.go b/grype/event/event.go index 6f0e36f8861..aa7851bee1d 100644 --- a/grype/event/event.go +++ b/grype/event/event.go @@ -13,6 +13,7 @@ const ( UpdateVulnerabilityDatabase partybus.EventType = typePrefix + "-update-vulnerability-database" VulnerabilityScanningStarted partybus.EventType = typePrefix + "-vulnerability-scanning-started" DatabaseDiffingStarted partybus.EventType = typePrefix + "-database-diffing-started" + VexDocumentDiscoveryStarted partybus.EventType = typePrefix + "-vex-document-discovery-started" // Events exclusively for the CLI diff --git a/grype/event/parsers/parsers.go b/grype/event/parsers/parsers.go index 9606ef1a95f..73ed4ec91c3 100644 --- a/grype/event/parsers/parsers.go +++ b/grype/event/parsers/parsers.go @@ -74,6 +74,19 @@ func ParseDatabaseDiffingStarted(e partybus.Event) (*monitor.DBDiff, error) { return &mon, nil } +func ParseVexDocumentDiscoveryStarted(e partybus.Event) (progress.StagedProgressable, error) { + if err := checkEventType(e.Type, event.VexDocumentDiscoveryStarted); err != nil { + return nil, err + } + + prog, ok := e.Value.(progress.StagedProgressable) + if !ok { + return nil, newPayloadErr(e.Type, "Value", e.Value) + } + + return prog, nil +} + type UpdateCheck struct { New string Current string diff --git a/grype/vex/openvex/implementation.go b/grype/vex/openvex/implementation.go index 53629c63cd6..bf9037e4d52 100644 --- a/grype/vex/openvex/implementation.go +++ b/grype/vex/openvex/implementation.go @@ -3,11 +3,15 @@ package openvex import ( "errors" "fmt" - "strings" - + "github.com/anchore/grype/grype/event" + "github.com/anchore/grype/internal/bus" "github.com/openvex/discovery/pkg/discovery" "github.com/openvex/discovery/pkg/oci" openvex "github.com/openvex/go-vex/pkg/vex" + "github.com/scylladb/go-set/strset" + "github.com/wagoodman/go-partybus" + "github.com/wagoodman/go-progress" + "strings" "github.com/anchore/grype/grype/match" "github.com/anchore/grype/grype/pkg" @@ -293,6 +297,8 @@ func (ovm *Processor) DiscoverVexDocuments(pkgContext *pkg.Context, rawVexData i return nil, fmt.Errorf("extracting identifiers from context") } + prog, stage := trackVexDiscovery(identifiers) + allDocs := []*openvex.VEX{} // If we already have some vex data, add it @@ -301,18 +307,34 @@ func (ovm *Processor) DiscoverVexDocuments(pkgContext *pkg.Context, rawVexData i } agent := discovery.NewAgent() + ids := strset.New() for _, i := range identifiers { if !strings.HasPrefix(i, "pkg:") { continue } + + stage.Set(fmt.Sprintf("searching %s", i)) + discoveredDocs, err := agent.ProbePurl(i) if err != nil { + prog.SetError(err) return nil, fmt.Errorf("probing package url or vex data: %w", err) } + // prune any existing documents so they are not applied multiple times + for j, doc := range discoveredDocs { + if ids.Has(doc.ID) { + discoveredDocs = append(discoveredDocs[:j], discoveredDocs[j+1:]...) + } + ids.Add(doc.ID) + } + allDocs = append(allDocs, discoveredDocs...) } + stage.Set(fmt.Sprintf("%d documents", len(allDocs))) + + prog.SetCompleted() vexdata, err := openvex.MergeDocuments(allDocs) if err != nil { @@ -321,3 +343,23 @@ func (ovm *Processor) DiscoverVexDocuments(pkgContext *pkg.Context, rawVexData i return vexdata, nil } + +func trackVexDiscovery(identifiers []string) (*progress.Manual, *progress.AtomicStage) { + stage := progress.NewAtomicStage("") + prog := progress.NewManual(-1) + + bus.Publish(partybus.Event{ + Type: event.VexDocumentDiscoveryStarted, + Source: identifiers, + Value: progress.StagedProgressable(struct { + progress.Stager + progress.Progressable + }{ + Stager: stage, + Progressable: prog, + }), + Error: nil, + }) + + return prog, stage +}