mirror of
https://github.com/anchore/grype
synced 2024-11-10 06:34:13 +00:00
* feat(outputs): allow to set multiple outputs (#648) Signed-off-by: Olivier Boudet <o.boudet@gmail.com> Signed-off-by: Olivier Boudet <olivier.boudet@cooperl.com> Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * feat(outputs): allow to set multiple outputs (#648) review Signed-off-by: Olivier Boudet <olivier.boudet@cooperl.com> Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * use syft format writter pattern and de-emphasize presenter package Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> --------- Signed-off-by: Olivier Boudet <o.boudet@gmail.com> Signed-off-by: Olivier Boudet <olivier.boudet@cooperl.com> Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> Co-authored-by: Alex Goodman <wagoodman@users.noreply.github.com>
This commit is contained in:
parent
6834e2148c
commit
9050883715
46 changed files with 765 additions and 449 deletions
|
@ -5,11 +5,9 @@ import (
|
|||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/wagoodman/go-partybus"
|
||||
|
||||
"github.com/anchore/grype/grype/db"
|
||||
"github.com/anchore/grype/grype/differ"
|
||||
"github.com/anchore/grype/grype/event"
|
||||
"github.com/anchore/grype/internal/bus"
|
||||
"github.com/anchore/grype/internal/log"
|
||||
"github.com/anchore/grype/internal/ui"
|
||||
|
@ -38,6 +36,7 @@ func startDBDiffCmd(base string, target string, deleteDatabases bool) <-chan err
|
|||
errs := make(chan error)
|
||||
go func() {
|
||||
defer close(errs)
|
||||
defer bus.Exit()
|
||||
d, err := differ.NewDiffer(appConfig.DB.ToCuratorConfig())
|
||||
if err != nil {
|
||||
errs <- err
|
||||
|
@ -72,11 +71,6 @@ func startDBDiffCmd(base string, target string, deleteDatabases bool) <-chan err
|
|||
if deleteDatabases {
|
||||
errs <- d.DeleteDatabases()
|
||||
}
|
||||
|
||||
bus.Publish(partybus.Event{
|
||||
Type: event.NonRootCommandFinished,
|
||||
Value: "",
|
||||
})
|
||||
}()
|
||||
return errs
|
||||
}
|
||||
|
|
|
@ -4,10 +4,8 @@ import (
|
|||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/wagoodman/go-partybus"
|
||||
|
||||
"github.com/anchore/grype/grype/db"
|
||||
"github.com/anchore/grype/grype/event"
|
||||
"github.com/anchore/grype/internal/bus"
|
||||
"github.com/anchore/grype/internal/log"
|
||||
"github.com/anchore/grype/internal/ui"
|
||||
|
@ -29,6 +27,8 @@ func startDBUpdateCmd() <-chan error {
|
|||
errs := make(chan error)
|
||||
go func() {
|
||||
defer close(errs)
|
||||
defer bus.Exit()
|
||||
|
||||
dbCurator, err := db.NewCurator(appConfig.DB.ToCuratorConfig())
|
||||
if err != nil {
|
||||
errs <- err
|
||||
|
@ -44,10 +44,7 @@ func startDBUpdateCmd() <-chan error {
|
|||
result = "Vulnerability database updated to latest version!\n"
|
||||
}
|
||||
|
||||
bus.Publish(partybus.Event{
|
||||
Type: event.NonRootCommandFinished,
|
||||
Value: result,
|
||||
})
|
||||
bus.Report(result)
|
||||
}()
|
||||
return errs
|
||||
}
|
||||
|
|
|
@ -51,7 +51,7 @@ func Test_eventLoop_gracefulExit(t *testing.T) {
|
|||
t.Cleanup(testBus.Close)
|
||||
|
||||
finalEvent := partybus.Event{
|
||||
Type: event.VulnerabilityScanningFinished,
|
||||
Type: event.CLIExit,
|
||||
}
|
||||
|
||||
worker := func() <-chan error {
|
||||
|
@ -183,7 +183,7 @@ func Test_eventLoop_unsubscribeError(t *testing.T) {
|
|||
t.Cleanup(testBus.Close)
|
||||
|
||||
finalEvent := partybus.Event{
|
||||
Type: event.VulnerabilityScanningFinished,
|
||||
Type: event.CLIExit,
|
||||
}
|
||||
|
||||
worker := func() <-chan error {
|
||||
|
@ -252,7 +252,7 @@ func Test_eventLoop_handlerError(t *testing.T) {
|
|||
t.Cleanup(testBus.Close)
|
||||
|
||||
finalEvent := partybus.Event{
|
||||
Type: event.VulnerabilityScanningFinished,
|
||||
Type: event.CLIExit,
|
||||
Error: fmt.Errorf("unable to create presenter"),
|
||||
}
|
||||
|
||||
|
@ -377,7 +377,7 @@ func Test_eventLoop_uiTeardownError(t *testing.T) {
|
|||
t.Cleanup(testBus.Close)
|
||||
|
||||
finalEvent := partybus.Event{
|
||||
Type: event.VulnerabilityScanningFinished,
|
||||
Type: event.CLIExit,
|
||||
}
|
||||
|
||||
worker := func() <-chan error {
|
||||
|
|
32
cmd/root.go
32
cmd/root.go
|
@ -28,7 +28,6 @@ import (
|
|||
"github.com/anchore/grype/grype/matcher/ruby"
|
||||
"github.com/anchore/grype/grype/matcher/stock"
|
||||
"github.com/anchore/grype/grype/pkg"
|
||||
"github.com/anchore/grype/grype/presenter"
|
||||
"github.com/anchore/grype/grype/presenter/models"
|
||||
"github.com/anchore/grype/grype/store"
|
||||
"github.com/anchore/grype/grype/vulnerability"
|
||||
|
@ -37,6 +36,7 @@ import (
|
|||
"github.com/anchore/grype/internal/config"
|
||||
"github.com/anchore/grype/internal/format"
|
||||
"github.com/anchore/grype/internal/log"
|
||||
"github.com/anchore/grype/internal/stringutil"
|
||||
"github.com/anchore/grype/internal/ui"
|
||||
"github.com/anchore/grype/internal/version"
|
||||
"github.com/anchore/stereoscope"
|
||||
|
@ -62,7 +62,7 @@ var (
|
|||
rootCmd = &cobra.Command{
|
||||
Use: fmt.Sprintf("%s [IMAGE]", internal.ApplicationName),
|
||||
Short: "A vulnerability scanner for container images, filesystems, and SBOMs",
|
||||
Long: format.Tprintf(`A vulnerability scanner for container images, filesystems, and SBOMs.
|
||||
Long: stringutil.Tprintf(`A vulnerability scanner for container images, filesystems, and SBOMs.
|
||||
|
||||
Supports the following image sources:
|
||||
{{.appName}} yourrepo/yourimage:tag defaults to using images from a Docker daemon
|
||||
|
@ -130,14 +130,14 @@ func setRootFlags(flags *pflag.FlagSet) {
|
|||
fmt.Sprintf("selection of layers to analyze, options=%v", source.AllScopes),
|
||||
)
|
||||
|
||||
flags.StringP(
|
||||
"output", "o", "",
|
||||
fmt.Sprintf("report output formatter, formats=%v, deprecated formats=%v", presenter.AvailableFormats, presenter.DeprecatedFormats),
|
||||
flags.StringArrayP(
|
||||
"output", "o", nil,
|
||||
fmt.Sprintf("report output formatter, formats=%v, deprecated formats=%v", format.AvailableFormats, format.DeprecatedFormats),
|
||||
)
|
||||
|
||||
flags.StringP(
|
||||
"file", "", "",
|
||||
"file to write the report output to (default is STDOUT)",
|
||||
"file to write the default report output to (default is STDOUT)",
|
||||
)
|
||||
|
||||
flags.StringP(
|
||||
|
@ -298,8 +298,13 @@ func startWorker(userInput string, failOnSeverity *vulnerability.Severity) <-cha
|
|||
errs := make(chan error)
|
||||
go func() {
|
||||
defer close(errs)
|
||||
defer bus.Exit()
|
||||
|
||||
presenterConfig, err := presenter.ValidatedConfig(appConfig.Output, appConfig.OutputTemplateFile, appConfig.ShowSuppressed)
|
||||
// TODO: appConfig.File
|
||||
writer, err := format.MakeScanResultWriter(appConfig.Outputs, appConfig.OutputTemplateFile, format.PresentationConfig{
|
||||
TemplateFilePath: appConfig.OutputTemplateFile,
|
||||
ShowSuppressed: appConfig.ShowSuppressed,
|
||||
})
|
||||
if err != nil {
|
||||
errs <- err
|
||||
return
|
||||
|
@ -332,7 +337,7 @@ func startWorker(userInput string, failOnSeverity *vulnerability.Severity) <-cha
|
|||
go func() {
|
||||
defer wg.Done()
|
||||
log.Debugf("gathering packages")
|
||||
// packages are grype.Pacakge, not syft.Package
|
||||
// packages are grype.Package, not syft.Package
|
||||
// the SBOM is returned for downstream formatting concerns
|
||||
// grype uses the SBOM in combination with syft formatters to produce cycloneDX
|
||||
// with vulnerability information appended
|
||||
|
@ -379,7 +384,7 @@ func startWorker(userInput string, failOnSeverity *vulnerability.Severity) <-cha
|
|||
}
|
||||
}
|
||||
|
||||
pb := models.PresenterConfig{
|
||||
if err := writer.Write(models.PresenterConfig{
|
||||
Matches: *remainingMatches,
|
||||
IgnoredMatches: ignoredMatches,
|
||||
Packages: packages,
|
||||
|
@ -388,12 +393,9 @@ func startWorker(userInput string, failOnSeverity *vulnerability.Severity) <-cha
|
|||
SBOM: sbom,
|
||||
AppConfig: appConfig,
|
||||
DBStatus: status,
|
||||
}); err != nil {
|
||||
errs <- err
|
||||
}
|
||||
|
||||
bus.Publish(partybus.Event{
|
||||
Type: event.VulnerabilityScanningFinished,
|
||||
Value: presenter.GetPresenter(presenterConfig, pb),
|
||||
})
|
||||
}()
|
||||
return errs
|
||||
}
|
||||
|
@ -447,7 +449,7 @@ func checkForAppUpdate() {
|
|||
log.Infof("new version of %s is available: %s (currently running: %s)", internal.ApplicationName, newVersion, version.FromBuild().Version)
|
||||
|
||||
bus.Publish(partybus.Event{
|
||||
Type: event.AppUpdateAvailable,
|
||||
Type: event.CLIAppUpdateAvailable,
|
||||
Value: newVersion,
|
||||
})
|
||||
} else {
|
||||
|
|
1
go.mod
1
go.mod
|
@ -56,6 +56,7 @@ require (
|
|||
github.com/anchore/syft v0.84.2-0.20230705174713-cfbb9f703bd7
|
||||
github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b
|
||||
github.com/mitchellh/mapstructure v1.5.0
|
||||
github.com/wagoodman/go-presenter v0.0.0-20211015174752-f9c01afc824b
|
||||
)
|
||||
|
||||
require (
|
||||
|
|
2
go.sum
2
go.sum
|
@ -842,6 +842,8 @@ github.com/vmihailenco/msgpack/v4 v4.3.12/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+
|
|||
github.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI=
|
||||
github.com/wagoodman/go-partybus v0.0.0-20210627031916-db1f5573bbc5 h1:phTLPgMRDYTizrBSKsNSOa2zthoC2KsJsaY/8sg3rD8=
|
||||
github.com/wagoodman/go-partybus v0.0.0-20210627031916-db1f5573bbc5/go.mod h1:JPirS5jde/CF5qIjcK4WX+eQmKXdPc6vcZkJ/P0hfPw=
|
||||
github.com/wagoodman/go-presenter v0.0.0-20211015174752-f9c01afc824b h1:uWNQ0khA6RdFzODOMwKo9XXu7fuewnnkHykUtuKru8s=
|
||||
github.com/wagoodman/go-presenter v0.0.0-20211015174752-f9c01afc824b/go.mod h1:ewlIKbKV8l+jCj8rkdXIs361ocR5x3qGyoCSca47Gx8=
|
||||
github.com/wagoodman/go-progress v0.0.0-20230301185719-21920a456ad5 h1:lwgTsTy18nYqASnH58qyfRW/ldj7Gt2zzBvgYPzdA4s=
|
||||
github.com/wagoodman/go-progress v0.0.0-20230301185719-21920a456ad5/go.mod h1:jLXFoL31zFaHKAAyZUh+sxiTDFe1L1ZHrcK2T1itVKA=
|
||||
github.com/wagoodman/jotframe v0.0.0-20211129225309-56b0d0a4aebb h1:Yz6VVOcLuWLAHYlJzTw7JKnWxdV/WXpug2X0quEzRnY=
|
||||
|
|
|
@ -21,14 +21,14 @@ import (
|
|||
"github.com/stretchr/testify/require"
|
||||
"github.com/wagoodman/go-progress"
|
||||
|
||||
"github.com/anchore/grype/internal"
|
||||
"github.com/anchore/grype/internal/file"
|
||||
"github.com/anchore/grype/internal/stringutil"
|
||||
)
|
||||
|
||||
type testGetter struct {
|
||||
file map[string]string
|
||||
dir map[string]string
|
||||
calls internal.StringSet
|
||||
calls stringutil.StringSet
|
||||
fs afero.Fs
|
||||
}
|
||||
|
||||
|
@ -36,7 +36,7 @@ func newTestGetter(fs afero.Fs, f, d map[string]string) *testGetter {
|
|||
return &testGetter{
|
||||
file: f,
|
||||
dir: d,
|
||||
calls: internal.NewStringSet(),
|
||||
calls: stringutil.NewStringSet(),
|
||||
fs: fs,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ import (
|
|||
"github.com/anchore/grype/grype/db/internal/gormadapter"
|
||||
v1 "github.com/anchore/grype/grype/db/v1"
|
||||
"github.com/anchore/grype/grype/db/v1/store/model"
|
||||
"github.com/anchore/grype/internal"
|
||||
"github.com/anchore/grype/internal/stringutil"
|
||||
_ "github.com/anchore/sqlite" // provide the sqlite dialect to gorm via import
|
||||
)
|
||||
|
||||
|
@ -172,7 +172,7 @@ func (s *store) AddVulnerabilityMetadata(metadata ...v1.VulnerabilityMetadata) e
|
|||
existing.CvssV3 = m.CvssV3
|
||||
}
|
||||
|
||||
links := internal.NewStringSetFromSlice(existing.Links)
|
||||
links := stringutil.NewStringSetFromSlice(existing.Links)
|
||||
for _, l := range m.Links {
|
||||
links.Add(l)
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ import (
|
|||
"github.com/anchore/grype/grype/db/internal/gormadapter"
|
||||
v2 "github.com/anchore/grype/grype/db/v2"
|
||||
"github.com/anchore/grype/grype/db/v2/store/model"
|
||||
"github.com/anchore/grype/internal"
|
||||
"github.com/anchore/grype/internal/stringutil"
|
||||
_ "github.com/anchore/sqlite" // provide the sqlite dialect to gorm via import
|
||||
)
|
||||
|
||||
|
@ -171,7 +171,7 @@ func (s *store) AddVulnerabilityMetadata(metadata ...v2.VulnerabilityMetadata) e
|
|||
existing.CvssV3 = m.CvssV3
|
||||
}
|
||||
|
||||
links := internal.NewStringSetFromSlice(existing.Links)
|
||||
links := stringutil.NewStringSetFromSlice(existing.Links)
|
||||
for _, l := range m.Links {
|
||||
links.Add(l)
|
||||
}
|
||||
|
|
|
@ -6,8 +6,8 @@ import (
|
|||
|
||||
"github.com/anchore/grype/grype/distro"
|
||||
"github.com/anchore/grype/grype/pkg"
|
||||
"github.com/anchore/grype/internal"
|
||||
"github.com/anchore/grype/internal/log"
|
||||
"github.com/anchore/grype/internal/stringutil"
|
||||
packageurl "github.com/anchore/packageurl-go"
|
||||
syftPkg "github.com/anchore/syft/syft/pkg"
|
||||
)
|
||||
|
@ -110,7 +110,7 @@ func defaultPackageNamer(p pkg.Package) []string {
|
|||
}
|
||||
|
||||
func githubJavaPackageNamer(p pkg.Package) []string {
|
||||
names := internal.NewStringSet()
|
||||
names := stringutil.NewStringSet()
|
||||
|
||||
// all github advisories are stored by "<group-name>:<artifact-name>"
|
||||
if metadata, ok := p.Metadata.(pkg.JavaMetadata); ok {
|
||||
|
|
|
@ -10,7 +10,7 @@ import (
|
|||
"github.com/anchore/grype/grype/db/internal/gormadapter"
|
||||
v3 "github.com/anchore/grype/grype/db/v3"
|
||||
"github.com/anchore/grype/grype/db/v3/store/model"
|
||||
"github.com/anchore/grype/internal"
|
||||
"github.com/anchore/grype/internal/stringutil"
|
||||
_ "github.com/anchore/sqlite" // provide the sqlite dialect to gorm via import
|
||||
)
|
||||
|
||||
|
@ -179,7 +179,7 @@ func (s *store) AddVulnerabilityMetadata(metadata ...v3.VulnerabilityMetadata) e
|
|||
existing.Cvss = append(existing.Cvss, incomingCvss)
|
||||
}
|
||||
|
||||
links := internal.NewStringSetFromSlice(existing.URLs)
|
||||
links := stringutil.NewStringSetFromSlice(existing.URLs)
|
||||
for _, l := range m.URLs {
|
||||
links.Add(l)
|
||||
}
|
||||
|
|
|
@ -5,8 +5,8 @@ import (
|
|||
"strings"
|
||||
|
||||
grypePkg "github.com/anchore/grype/grype/pkg"
|
||||
"github.com/anchore/grype/internal"
|
||||
"github.com/anchore/grype/internal/log"
|
||||
"github.com/anchore/grype/internal/stringutil"
|
||||
"github.com/anchore/packageurl-go"
|
||||
)
|
||||
|
||||
|
@ -18,7 +18,7 @@ func (r *Resolver) Normalize(name string) string {
|
|||
}
|
||||
|
||||
func (r *Resolver) Resolve(p grypePkg.Package) []string {
|
||||
names := internal.NewStringSet()
|
||||
names := stringutil.NewStringSet()
|
||||
|
||||
// The current default for the Java ecosystem is to use a Maven-like identifier of the form
|
||||
// "<group-name>:<artifact-name>"
|
||||
|
|
|
@ -10,7 +10,7 @@ import (
|
|||
"github.com/anchore/grype/grype/db/internal/gormadapter"
|
||||
v4 "github.com/anchore/grype/grype/db/v4"
|
||||
"github.com/anchore/grype/grype/db/v4/store/model"
|
||||
"github.com/anchore/grype/internal"
|
||||
"github.com/anchore/grype/internal/stringutil"
|
||||
_ "github.com/anchore/sqlite" // provide the sqlite dialect to gorm via import
|
||||
)
|
||||
|
||||
|
@ -189,7 +189,7 @@ func (s *store) AddVulnerabilityMetadata(metadata ...v4.VulnerabilityMetadata) e
|
|||
existing.Cvss = append(existing.Cvss, incomingCvss)
|
||||
}
|
||||
|
||||
links := internal.NewStringSetFromSlice(existing.URLs)
|
||||
links := stringutil.NewStringSetFromSlice(existing.URLs)
|
||||
for _, l := range m.URLs {
|
||||
links.Add(l)
|
||||
}
|
||||
|
|
|
@ -5,8 +5,8 @@ import (
|
|||
"strings"
|
||||
|
||||
grypePkg "github.com/anchore/grype/grype/pkg"
|
||||
"github.com/anchore/grype/internal"
|
||||
"github.com/anchore/grype/internal/log"
|
||||
"github.com/anchore/grype/internal/stringutil"
|
||||
"github.com/anchore/packageurl-go"
|
||||
)
|
||||
|
||||
|
@ -18,7 +18,7 @@ func (r *Resolver) Normalize(name string) string {
|
|||
}
|
||||
|
||||
func (r *Resolver) Resolve(p grypePkg.Package) []string {
|
||||
names := internal.NewStringSet()
|
||||
names := stringutil.NewStringSet()
|
||||
|
||||
// The current default for the Java ecosystem is to use a Maven-like identifier of the form
|
||||
// "<group-name>:<artifact-name>"
|
||||
|
|
|
@ -10,7 +10,7 @@ import (
|
|||
"github.com/anchore/grype/grype/db/internal/gormadapter"
|
||||
v5 "github.com/anchore/grype/grype/db/v5"
|
||||
"github.com/anchore/grype/grype/db/v5/store/model"
|
||||
"github.com/anchore/grype/internal"
|
||||
"github.com/anchore/grype/internal/stringutil"
|
||||
_ "github.com/anchore/sqlite" // provide the sqlite dialect to gorm via import
|
||||
)
|
||||
|
||||
|
@ -207,7 +207,7 @@ func (s *store) AddVulnerabilityMetadata(metadata ...v5.VulnerabilityMetadata) e
|
|||
existing.Cvss = append(existing.Cvss, incomingCvss)
|
||||
}
|
||||
|
||||
links := internal.NewStringSetFromSlice(existing.URLs)
|
||||
links := stringutil.NewStringSetFromSlice(existing.URLs)
|
||||
for _, l := range m.URLs {
|
||||
links.Add(l)
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ import (
|
|||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/anchore/grype/internal"
|
||||
"github.com/anchore/grype/internal/stringutil"
|
||||
"github.com/anchore/syft/syft/linux"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
@ -214,8 +214,8 @@ func Test_NewDistroFromRelease_Coverage(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
observedDistros := internal.NewStringSet()
|
||||
definedDistros := internal.NewStringSet()
|
||||
observedDistros := stringutil.NewStringSet()
|
||||
definedDistros := stringutil.NewStringSet()
|
||||
|
||||
for _, distroType := range All {
|
||||
definedDistros.Add(string(distroType))
|
||||
|
|
|
@ -1,12 +1,27 @@
|
|||
package event
|
||||
|
||||
import "github.com/wagoodman/go-partybus"
|
||||
import (
|
||||
"github.com/wagoodman/go-partybus"
|
||||
|
||||
"github.com/anchore/grype/internal"
|
||||
)
|
||||
|
||||
const (
|
||||
AppUpdateAvailable partybus.EventType = "grype-app-update-available"
|
||||
typePrefix = internal.ApplicationName
|
||||
cliTypePrefix = typePrefix + "-cli"
|
||||
|
||||
UpdateVulnerabilityDatabase partybus.EventType = "grype-update-vulnerability-database"
|
||||
VulnerabilityScanningStarted partybus.EventType = "grype-vulnerability-scanning-started"
|
||||
VulnerabilityScanningFinished partybus.EventType = "grype-vulnerability-scanning-finished"
|
||||
NonRootCommandFinished partybus.EventType = "grype-non-root-command-finished"
|
||||
DatabaseDiffingStarted partybus.EventType = "grype-database-diffing-started"
|
||||
|
||||
// Events exclusively for the CLI
|
||||
|
||||
// CLIAppUpdateAvailable is a partybus event that occurs when an application update is available
|
||||
CLIAppUpdateAvailable partybus.EventType = cliTypePrefix + "-app-update-available"
|
||||
|
||||
// CLIReport is a partybus event that occurs when an analysis result is ready for final presentation to stdout
|
||||
CLIReport partybus.EventType = cliTypePrefix + "-report"
|
||||
|
||||
// CLIExit is a partybus event that occurs when an analysis result is ready for final presentation
|
||||
CLIExit partybus.EventType = cliTypePrefix + "-exit-event"
|
||||
)
|
||||
|
|
|
@ -9,7 +9,6 @@ import (
|
|||
diffEvents "github.com/anchore/grype/grype/differ/events"
|
||||
"github.com/anchore/grype/grype/event"
|
||||
"github.com/anchore/grype/grype/matcher"
|
||||
"github.com/anchore/grype/grype/presenter"
|
||||
)
|
||||
|
||||
type ErrBadPayload struct {
|
||||
|
@ -37,19 +36,6 @@ func checkEventType(actual, expected partybus.EventType) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func ParseAppUpdateAvailable(e partybus.Event) (string, error) {
|
||||
if err := checkEventType(e.Type, event.AppUpdateAvailable); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
newVersion, ok := e.Value.(string)
|
||||
if !ok {
|
||||
return "", newPayloadErr(e.Type, "Value", e.Value)
|
||||
}
|
||||
|
||||
return newVersion, nil
|
||||
}
|
||||
|
||||
func ParseUpdateVulnerabilityDatabase(e partybus.Event) (progress.StagedProgressable, error) {
|
||||
if err := checkEventType(e.Type, event.UpdateVulnerabilityDatabase); err != nil {
|
||||
return nil, err
|
||||
|
@ -76,32 +62,6 @@ func ParseVulnerabilityScanningStarted(e partybus.Event) (*matcher.Monitor, erro
|
|||
return &monitor, nil
|
||||
}
|
||||
|
||||
func ParseVulnerabilityScanningFinished(e partybus.Event) (presenter.Presenter, error) {
|
||||
if err := checkEventType(e.Type, event.VulnerabilityScanningFinished); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pres, ok := e.Value.(presenter.Presenter)
|
||||
if !ok {
|
||||
return nil, newPayloadErr(e.Type, "Value", e.Value)
|
||||
}
|
||||
|
||||
return pres, nil
|
||||
}
|
||||
|
||||
func ParseNonRootCommandFinished(e partybus.Event) (*string, error) {
|
||||
if err := checkEventType(e.Type, event.NonRootCommandFinished); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result, ok := e.Value.(string)
|
||||
if !ok {
|
||||
return nil, newPayloadErr(e.Type, "Value", e.Value)
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func ParseDatabaseDiffingStarted(e partybus.Event) (*diffEvents.Monitor, error) {
|
||||
if err := checkEventType(e.Type, event.DatabaseDiffingStarted); err != nil {
|
||||
return nil, err
|
||||
|
@ -114,3 +74,35 @@ func ParseDatabaseDiffingStarted(e partybus.Event) (*diffEvents.Monitor, error)
|
|||
|
||||
return &monitor, nil
|
||||
}
|
||||
|
||||
func ParseCLIAppUpdateAvailable(e partybus.Event) (string, error) {
|
||||
if err := checkEventType(e.Type, event.CLIAppUpdateAvailable); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
newVersion, ok := e.Value.(string)
|
||||
if !ok {
|
||||
return "", newPayloadErr(e.Type, "Value", e.Value)
|
||||
}
|
||||
|
||||
return newVersion, nil
|
||||
}
|
||||
|
||||
func ParseCLIReport(e partybus.Event) (string, string, error) {
|
||||
if err := checkEventType(e.Type, event.CLIReport); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
context, ok := e.Source.(string)
|
||||
if !ok {
|
||||
// this is optional
|
||||
context = ""
|
||||
}
|
||||
|
||||
report, ok := e.Value.(string)
|
||||
if !ok {
|
||||
return "", "", newPayloadErr(e.Type, "Value", e.Value)
|
||||
}
|
||||
|
||||
return context, report, nil
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ import (
|
|||
"github.com/anchore/grype/grype/distro"
|
||||
"github.com/anchore/grype/grype/match"
|
||||
"github.com/anchore/grype/grype/pkg"
|
||||
"github.com/anchore/grype/internal"
|
||||
"github.com/anchore/grype/internal/stringutil"
|
||||
syftPkg "github.com/anchore/syft/syft/pkg"
|
||||
)
|
||||
|
||||
|
@ -39,7 +39,7 @@ func TestMatcherDpkg_matchBySourceIndirection(t *testing.T) {
|
|||
|
||||
assert.Len(t, actual, 2, "unexpected indirect matches count")
|
||||
|
||||
foundCVEs := internal.NewStringSet()
|
||||
foundCVEs := stringutil.NewStringSet()
|
||||
for _, a := range actual {
|
||||
foundCVEs.Add(a.Vulnerability.ID)
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ import (
|
|||
|
||||
"github.com/anchore/grype/grype/match"
|
||||
"github.com/anchore/grype/grype/pkg"
|
||||
"github.com/anchore/grype/internal"
|
||||
"github.com/anchore/grype/internal/stringutil"
|
||||
syftPkg "github.com/anchore/syft/syft/pkg"
|
||||
)
|
||||
|
||||
|
@ -44,7 +44,7 @@ func TestMatcherJava_matchUpstreamMavenPackage(t *testing.T) {
|
|||
|
||||
assert.Len(t, actual, 2, "unexpected matches count")
|
||||
|
||||
foundCVEs := internal.NewStringSet()
|
||||
foundCVEs := stringutil.NewStringSet()
|
||||
for _, v := range actual {
|
||||
foundCVEs.Add(v.Vulnerability.ID)
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ import (
|
|||
|
||||
"github.com/anchore/grype/grype/distro"
|
||||
"github.com/anchore/grype/grype/pkg"
|
||||
"github.com/anchore/grype/internal"
|
||||
"github.com/anchore/grype/internal/stringutil"
|
||||
syftPkg "github.com/anchore/syft/syft/pkg"
|
||||
)
|
||||
|
||||
|
@ -33,7 +33,7 @@ func TestMatcherPortage_Match(t *testing.T) {
|
|||
|
||||
assert.Len(t, actual, 1, "unexpected indirect matches count")
|
||||
|
||||
foundCVEs := internal.NewStringSet()
|
||||
foundCVEs := stringutil.NewStringSet()
|
||||
for _, a := range actual {
|
||||
foundCVEs.Add(a.Vulnerability.ID)
|
||||
|
||||
|
|
|
@ -5,8 +5,8 @@ import (
|
|||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/anchore/grype/internal"
|
||||
"github.com/anchore/grype/internal/log"
|
||||
"github.com/anchore/grype/internal/stringutil"
|
||||
"github.com/anchore/syft/syft/artifact"
|
||||
"github.com/anchore/syft/syft/cpe"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
|
@ -231,7 +231,7 @@ func rpmDataFromPkg(p pkg.Package) (metadata *RpmMetadata, upstreams []UpstreamP
|
|||
}
|
||||
|
||||
func getNameAndELVersion(sourceRpm string) (string, string) {
|
||||
groupMatches := internal.MatchCaptureGroups(rpmPackageNamePattern, sourceRpm)
|
||||
groupMatches := stringutil.MatchCaptureGroups(rpmPackageNamePattern, sourceRpm)
|
||||
version := groupMatches["version"] + "-" + groupMatches["release"]
|
||||
return groupMatches["name"], version
|
||||
}
|
||||
|
|
|
@ -1,65 +0,0 @@
|
|||
package presenter
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"text/template"
|
||||
|
||||
presenterTemplate "github.com/anchore/grype/grype/presenter/template"
|
||||
)
|
||||
|
||||
// Config is the presenter domain's configuration data structure.
|
||||
type Config struct {
|
||||
format format
|
||||
templateFilePath string
|
||||
showSuppressed bool
|
||||
}
|
||||
|
||||
// ValidatedConfig returns a new, validated presenter.Config. If a valid Config cannot be created using the given input,
|
||||
// an error is returned.
|
||||
func ValidatedConfig(output, outputTemplateFile string, showSuppressed bool) (Config, error) {
|
||||
format := parse(output)
|
||||
|
||||
if format == unknownFormat {
|
||||
return Config{}, fmt.Errorf("unsupported output format %q, supported formats are: %+v", output,
|
||||
AvailableFormats)
|
||||
}
|
||||
|
||||
if format == templateFormat {
|
||||
if outputTemplateFile == "" {
|
||||
return Config{}, fmt.Errorf("must specify path to template file when using %q output format",
|
||||
templateFormat)
|
||||
}
|
||||
|
||||
if _, err := os.Stat(outputTemplateFile); errors.Is(err, os.ErrNotExist) {
|
||||
// file does not exist
|
||||
return Config{}, fmt.Errorf("template file %q does not exist",
|
||||
outputTemplateFile)
|
||||
}
|
||||
|
||||
if _, err := os.ReadFile(outputTemplateFile); err != nil {
|
||||
return Config{}, fmt.Errorf("unable to read template file: %w", err)
|
||||
}
|
||||
|
||||
if _, err := template.New("").Funcs(presenterTemplate.FuncMap).ParseFiles(outputTemplateFile); err != nil {
|
||||
return Config{}, fmt.Errorf("unable to parse template: %w", err)
|
||||
}
|
||||
|
||||
return Config{
|
||||
format: format,
|
||||
templateFilePath: outputTemplateFile,
|
||||
}, nil
|
||||
}
|
||||
|
||||
if outputTemplateFile != "" {
|
||||
return Config{}, fmt.Errorf("specified template file %q, but "+
|
||||
"%q output format must be selected in order to use a template file",
|
||||
outputTemplateFile, templateFormat)
|
||||
}
|
||||
|
||||
return Config{
|
||||
format: format,
|
||||
showSuppressed: showSuppressed,
|
||||
}, nil
|
||||
}
|
|
@ -1,67 +0,0 @@
|
|||
package presenter
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestValidatedConfig(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
outputValue string
|
||||
includeSuppressed bool
|
||||
outputTemplateFileValue string
|
||||
expectedConfig Config
|
||||
assertErrExpectation func(assert.TestingT, error, ...interface{}) bool
|
||||
}{
|
||||
{
|
||||
"valid template config",
|
||||
"template",
|
||||
false,
|
||||
"./template/test-fixtures/test.valid.template",
|
||||
Config{
|
||||
format: "template",
|
||||
templateFilePath: "./template/test-fixtures/test.valid.template",
|
||||
},
|
||||
assert.NoError,
|
||||
},
|
||||
{
|
||||
"template file with non-template format",
|
||||
"json",
|
||||
false,
|
||||
"./some/path/to/a/custom.template",
|
||||
Config{},
|
||||
assert.Error,
|
||||
},
|
||||
{
|
||||
"unknown format",
|
||||
"some-made-up-format",
|
||||
false,
|
||||
"",
|
||||
Config{},
|
||||
assert.Error,
|
||||
},
|
||||
|
||||
{
|
||||
"table format",
|
||||
"table",
|
||||
true,
|
||||
"",
|
||||
Config{
|
||||
format: tableFormat,
|
||||
showSuppressed: true,
|
||||
},
|
||||
assert.NoError,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
actualConfig, actualErr := ValidatedConfig(tc.outputValue, tc.outputTemplateFileValue, tc.includeSuppressed)
|
||||
|
||||
assert.Equal(t, tc.expectedConfig, actualConfig)
|
||||
tc.assertErrExpectation(t, actualErr)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,71 +0,0 @@
|
|||
package presenter
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
unknownFormat format = "unknown"
|
||||
jsonFormat format = "json"
|
||||
tableFormat format = "table"
|
||||
cycloneDXFormat format = "cyclonedx"
|
||||
cycloneDXJSON format = "cyclonedx-json"
|
||||
cycloneDXXML format = "cyclonedx-xml"
|
||||
sarifFormat format = "sarif"
|
||||
templateFormat format = "template"
|
||||
|
||||
// DEPRECATED <-- TODO: remove in v1.0
|
||||
embeddedVEXJSON format = "embedded-cyclonedx-vex-json"
|
||||
embeddedVEXXML format = "embedded-cyclonedx-vex-xml"
|
||||
)
|
||||
|
||||
// format is a dedicated type to represent a specific kind of presenter output format.
|
||||
type format string
|
||||
|
||||
func (f format) String() string {
|
||||
return string(f)
|
||||
}
|
||||
|
||||
// parse returns the presenter.format specified by the given user input.
|
||||
func parse(userInput string) format {
|
||||
switch strings.ToLower(userInput) {
|
||||
case "":
|
||||
return tableFormat
|
||||
case strings.ToLower(jsonFormat.String()):
|
||||
return jsonFormat
|
||||
case strings.ToLower(tableFormat.String()):
|
||||
return tableFormat
|
||||
case strings.ToLower(sarifFormat.String()):
|
||||
return sarifFormat
|
||||
case strings.ToLower(templateFormat.String()):
|
||||
return templateFormat
|
||||
case strings.ToLower(cycloneDXFormat.String()):
|
||||
return cycloneDXFormat
|
||||
case strings.ToLower(cycloneDXJSON.String()):
|
||||
return cycloneDXJSON
|
||||
case strings.ToLower(cycloneDXXML.String()):
|
||||
return cycloneDXXML
|
||||
case strings.ToLower(embeddedVEXJSON.String()):
|
||||
return cycloneDXJSON
|
||||
case strings.ToLower(embeddedVEXXML.String()):
|
||||
return cycloneDXFormat
|
||||
default:
|
||||
return unknownFormat
|
||||
}
|
||||
}
|
||||
|
||||
// AvailableFormats is a list of presenter format options available to users.
|
||||
var AvailableFormats = []format{
|
||||
jsonFormat,
|
||||
tableFormat,
|
||||
cycloneDXFormat,
|
||||
cycloneDXJSON,
|
||||
sarifFormat,
|
||||
templateFormat,
|
||||
}
|
||||
|
||||
// DeprecatedFormats TODO: remove in v1.0
|
||||
var DeprecatedFormats = []format{
|
||||
embeddedVEXJSON,
|
||||
embeddedVEXXML,
|
||||
}
|
|
@ -1,52 +1,17 @@
|
|||
package presenter
|
||||
|
||||
import (
|
||||
"io"
|
||||
"github.com/wagoodman/go-presenter"
|
||||
|
||||
"github.com/anchore/grype/grype/presenter/cyclonedx"
|
||||
"github.com/anchore/grype/grype/presenter/json"
|
||||
"github.com/anchore/grype/grype/presenter/models"
|
||||
"github.com/anchore/grype/grype/presenter/sarif"
|
||||
"github.com/anchore/grype/grype/presenter/table"
|
||||
"github.com/anchore/grype/grype/presenter/template"
|
||||
"github.com/anchore/grype/internal/log"
|
||||
"github.com/anchore/grype/internal/format"
|
||||
)
|
||||
|
||||
// Presenter is the main interface other Presenters need to implement
|
||||
type Presenter interface {
|
||||
Present(io.Writer) error
|
||||
}
|
||||
|
||||
// GetPresenter retrieves a Presenter that matches a CLI option
|
||||
// TODO dependency cycle with presenter package to sub formats
|
||||
func GetPresenter(c Config, pb models.PresenterConfig) Presenter {
|
||||
switch c.format {
|
||||
case jsonFormat:
|
||||
return json.NewPresenter(pb)
|
||||
case tableFormat:
|
||||
return table.NewPresenter(pb, c.showSuppressed)
|
||||
|
||||
// NOTE: cyclonedx is identical to embeddedVEXJSON
|
||||
// The cyclonedx library only provides two BOM formats: JSON and XML
|
||||
// These embedded formats will be removed in v1.0
|
||||
case cycloneDXFormat:
|
||||
return cyclonedx.NewXMLPresenter(pb)
|
||||
case cycloneDXJSON:
|
||||
return cyclonedx.NewJSONPresenter(pb)
|
||||
case cycloneDXXML:
|
||||
return cyclonedx.NewXMLPresenter(pb)
|
||||
case sarifFormat:
|
||||
return sarif.NewPresenter(pb)
|
||||
case templateFormat:
|
||||
return template.NewPresenter(pb, c.templateFilePath)
|
||||
// DEPRECATED TODO: remove in v1.0
|
||||
case embeddedVEXJSON:
|
||||
log.Warn("embedded-cyclonedx-vex-json format is deprecated and will be removed in v1.0")
|
||||
return cyclonedx.NewJSONPresenter(pb)
|
||||
case embeddedVEXXML:
|
||||
log.Warn("embedded-cyclonedx-vex-xml format is deprecated and will be removed in v1.0")
|
||||
return cyclonedx.NewXMLPresenter(pb)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
// GetPresenter retrieves a Presenter that matches a CLI option.
|
||||
// Deprecated: this will be removed in v1.0
|
||||
func GetPresenter(f string, templatePath string, showSuppressed bool, pb models.PresenterConfig) presenter.Presenter {
|
||||
return format.GetPresenter(format.Parse(f), format.PresentationConfig{
|
||||
TemplateFilePath: templatePath,
|
||||
ShowSuppressed: showSuppressed,
|
||||
}, pb)
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ import (
|
|||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/anchore/grype/internal"
|
||||
"github.com/anchore/grype/internal/stringutil"
|
||||
)
|
||||
|
||||
// operator group only matches on range operators (GT, LT, GTE, LTE, E)
|
||||
|
@ -19,7 +19,7 @@ type constraintUnit struct {
|
|||
}
|
||||
|
||||
func parseUnit(phrase string) (*constraintUnit, error) {
|
||||
match := internal.MatchCaptureGroups(constraintPartPattern, phrase)
|
||||
match := stringutil.MatchCaptureGroups(constraintPartPattern, phrase)
|
||||
version, exists := match["version"]
|
||||
if !exists {
|
||||
return nil, nil
|
||||
|
|
20
internal/bus/helpers.go
Normal file
20
internal/bus/helpers.go
Normal file
|
@ -0,0 +1,20 @@
|
|||
package bus
|
||||
|
||||
import (
|
||||
"github.com/wagoodman/go-partybus"
|
||||
|
||||
"github.com/anchore/grype/grype/event"
|
||||
)
|
||||
|
||||
func Exit() {
|
||||
Publish(partybus.Event{
|
||||
Type: event.CLIExit,
|
||||
})
|
||||
}
|
||||
|
||||
func Report(report string) {
|
||||
Publish(partybus.Event{
|
||||
Type: event.CLIReport,
|
||||
Value: report,
|
||||
})
|
||||
}
|
|
@ -31,7 +31,7 @@ type parser interface {
|
|||
type Application struct {
|
||||
ConfigPath string `yaml:",omitempty" json:"configPath"` // the location where the application config was read from (either from -c or discovered while loading)
|
||||
Verbosity uint `yaml:"verbosity,omitempty" json:"verbosity" mapstructure:"verbosity"`
|
||||
Output string `yaml:"output" json:"output" mapstructure:"output"` // -o, the Presenter hint string to use for report formatting
|
||||
Outputs []string `yaml:"output" json:"output" mapstructure:"output"` // -o, <presenter>=<file> the Presenter hint string to use for report formatting and the output file
|
||||
File string `yaml:"file" json:"file" mapstructure:"file"` // --file, the file to write report output to
|
||||
Distro string `yaml:"distro" json:"distro" mapstructure:"distro"` // --distro, specify a distro to explicitly use
|
||||
GenerateMissingCPEs bool `yaml:"add-cpes-if-none" json:"add-cpes-if-none" mapstructure:"add-cpes-if-none"` // --add-cpes-if-none, automatically generate CPEs if they are not present in import (e.g. from a 3rd party SPDX document)
|
||||
|
|
|
@ -9,7 +9,7 @@ import (
|
|||
"github.com/hashicorp/go-getter/helper/url"
|
||||
"github.com/wagoodman/go-progress"
|
||||
|
||||
"github.com/anchore/grype/internal"
|
||||
"github.com/anchore/grype/internal/stringutil"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -62,7 +62,7 @@ func (g HashiGoGetter) GetToDir(dst, src string, monitors ...*progress.Manual) e
|
|||
|
||||
func validateHTTPSource(src string) error {
|
||||
// we are ignoring any sources that are not destined to use the http getter object
|
||||
if !internal.HasAnyOfPrefixes(src, "http://", "https://") {
|
||||
if !stringutil.HasAnyOfPrefixes(src, "http://", "https://") {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -71,7 +71,7 @@ func validateHTTPSource(src string) error {
|
|||
return fmt.Errorf("bad URL provided %q: %w", src, err)
|
||||
}
|
||||
// only allow for sources with archive extensions
|
||||
if !internal.HasAnyOfSuffixes(u.Path, archiveExtensions...) {
|
||||
if !stringutil.HasAnyOfSuffixes(u.Path, archiveExtensions...) {
|
||||
return ErrNonArchiveSource
|
||||
}
|
||||
return nil
|
||||
|
|
71
internal/format/format.go
Normal file
71
internal/format/format.go
Normal file
|
@ -0,0 +1,71 @@
|
|||
package format
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
UnknownFormat Format = "unknown"
|
||||
JSONFormat Format = "json"
|
||||
TableFormat Format = "table"
|
||||
CycloneDXFormat Format = "cyclonedx"
|
||||
CycloneDXJSON Format = "cyclonedx-json"
|
||||
CycloneDXXML Format = "cyclonedx-xml"
|
||||
SarifFormat Format = "sarif"
|
||||
TemplateFormat Format = "template"
|
||||
|
||||
// DEPRECATED <-- TODO: remove in v1.0
|
||||
EmbeddedVEXJSON Format = "embedded-cyclonedx-vex-json"
|
||||
EmbeddedVEXXML Format = "embedded-cyclonedx-vex-xml"
|
||||
)
|
||||
|
||||
// Format is a dedicated type to represent a specific kind of presenter output format.
|
||||
type Format string
|
||||
|
||||
func (f Format) String() string {
|
||||
return string(f)
|
||||
}
|
||||
|
||||
// Parse returns the presenter.format specified by the given user input.
|
||||
func Parse(userInput string) Format {
|
||||
switch strings.ToLower(userInput) {
|
||||
case "":
|
||||
return TableFormat
|
||||
case strings.ToLower(JSONFormat.String()):
|
||||
return JSONFormat
|
||||
case strings.ToLower(TableFormat.String()):
|
||||
return TableFormat
|
||||
case strings.ToLower(SarifFormat.String()):
|
||||
return SarifFormat
|
||||
case strings.ToLower(TemplateFormat.String()):
|
||||
return TemplateFormat
|
||||
case strings.ToLower(CycloneDXFormat.String()):
|
||||
return CycloneDXFormat
|
||||
case strings.ToLower(CycloneDXJSON.String()):
|
||||
return CycloneDXJSON
|
||||
case strings.ToLower(CycloneDXXML.String()):
|
||||
return CycloneDXXML
|
||||
case strings.ToLower(EmbeddedVEXJSON.String()):
|
||||
return CycloneDXJSON
|
||||
case strings.ToLower(EmbeddedVEXXML.String()):
|
||||
return CycloneDXFormat
|
||||
default:
|
||||
return UnknownFormat
|
||||
}
|
||||
}
|
||||
|
||||
// AvailableFormats is a list of presenter format options available to users.
|
||||
var AvailableFormats = []Format{
|
||||
JSONFormat,
|
||||
TableFormat,
|
||||
CycloneDXFormat,
|
||||
CycloneDXJSON,
|
||||
SarifFormat,
|
||||
TemplateFormat,
|
||||
}
|
||||
|
||||
// DeprecatedFormats TODO: remove in v1.0
|
||||
var DeprecatedFormats = []Format{
|
||||
EmbeddedVEXJSON,
|
||||
EmbeddedVEXXML,
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package presenter
|
||||
package format
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
@ -9,29 +9,29 @@ import (
|
|||
func TestParse(t *testing.T) {
|
||||
cases := []struct {
|
||||
input string
|
||||
expected format
|
||||
expected Format
|
||||
}{
|
||||
{
|
||||
"",
|
||||
tableFormat,
|
||||
TableFormat,
|
||||
},
|
||||
{
|
||||
"table",
|
||||
tableFormat,
|
||||
TableFormat,
|
||||
},
|
||||
{
|
||||
"jSOn",
|
||||
jsonFormat,
|
||||
JSONFormat,
|
||||
},
|
||||
{
|
||||
"booboodepoopoo",
|
||||
unknownFormat,
|
||||
UnknownFormat,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.input, func(t *testing.T) {
|
||||
actual := parse(tc.input)
|
||||
actual := Parse(tc.input)
|
||||
assert.Equal(t, tc.expected, actual, "unexpected result for input %q", tc.input)
|
||||
})
|
||||
}
|
51
internal/format/presenter.go
Normal file
51
internal/format/presenter.go
Normal file
|
@ -0,0 +1,51 @@
|
|||
package format
|
||||
|
||||
import (
|
||||
"github.com/wagoodman/go-presenter"
|
||||
|
||||
"github.com/anchore/grype/grype/presenter/cyclonedx"
|
||||
"github.com/anchore/grype/grype/presenter/json"
|
||||
"github.com/anchore/grype/grype/presenter/models"
|
||||
"github.com/anchore/grype/grype/presenter/sarif"
|
||||
"github.com/anchore/grype/grype/presenter/table"
|
||||
"github.com/anchore/grype/grype/presenter/template"
|
||||
"github.com/anchore/grype/internal/log"
|
||||
)
|
||||
|
||||
type PresentationConfig struct {
|
||||
TemplateFilePath string
|
||||
ShowSuppressed bool
|
||||
}
|
||||
|
||||
// GetPresenter retrieves a Presenter that matches a CLI option
|
||||
func GetPresenter(format Format, c PresentationConfig, pb models.PresenterConfig) presenter.Presenter {
|
||||
switch format {
|
||||
case JSONFormat:
|
||||
return json.NewPresenter(pb)
|
||||
case TableFormat:
|
||||
return table.NewPresenter(pb, c.ShowSuppressed)
|
||||
|
||||
// NOTE: cyclonedx is identical to EmbeddedVEXJSON
|
||||
// The cyclonedx library only provides two BOM formats: JSON and XML
|
||||
// These embedded formats will be removed in v1.0
|
||||
case CycloneDXFormat:
|
||||
return cyclonedx.NewXMLPresenter(pb)
|
||||
case CycloneDXJSON:
|
||||
return cyclonedx.NewJSONPresenter(pb)
|
||||
case CycloneDXXML:
|
||||
return cyclonedx.NewXMLPresenter(pb)
|
||||
case SarifFormat:
|
||||
return sarif.NewPresenter(pb)
|
||||
case TemplateFormat:
|
||||
return template.NewPresenter(pb, c.TemplateFilePath)
|
||||
// DEPRECATED TODO: remove in v1.0
|
||||
case EmbeddedVEXJSON:
|
||||
log.Warn("embedded-cyclonedx-vex-json format is deprecated and will be removed in v1.0")
|
||||
return cyclonedx.NewJSONPresenter(pb)
|
||||
case EmbeddedVEXXML:
|
||||
log.Warn("embedded-cyclonedx-vex-xml format is deprecated and will be removed in v1.0")
|
||||
return cyclonedx.NewXMLPresenter(pb)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
219
internal/format/writer.go
Normal file
219
internal/format/writer.go
Normal file
|
@ -0,0 +1,219 @@
|
|||
package format
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/mitchellh/go-homedir"
|
||||
|
||||
"github.com/anchore/grype/grype/presenter/models"
|
||||
"github.com/anchore/grype/internal/bus"
|
||||
"github.com/anchore/grype/internal/log"
|
||||
)
|
||||
|
||||
type ScanResultWriter interface {
|
||||
Write(result models.PresenterConfig) error
|
||||
}
|
||||
|
||||
var _ ScanResultWriter = (*scanResultMultiWriter)(nil)
|
||||
|
||||
var _ interface {
|
||||
io.Closer
|
||||
ScanResultWriter
|
||||
} = (*scanResultStreamWriter)(nil)
|
||||
|
||||
// MakeScanResultWriter creates a ScanResultWriter for output or returns an error. this will either return a valid writer
|
||||
// or an error but neither both and if there is no error, ScanResultWriter.Close() should be called
|
||||
func MakeScanResultWriter(outputs []string, defaultFile string, cfg PresentationConfig) (ScanResultWriter, error) {
|
||||
outputOptions, err := parseOutputFlags(outputs, defaultFile, cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
writer, err := newMultiWriter(outputOptions...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return writer, nil
|
||||
}
|
||||
|
||||
// MakeScanResultWriterForFormat creates a ScanResultWriter for the given format or returns an error.
|
||||
func MakeScanResultWriterForFormat(f string, path string, cfg PresentationConfig) (ScanResultWriter, error) {
|
||||
format := Parse(f)
|
||||
|
||||
if format == UnknownFormat {
|
||||
return nil, fmt.Errorf(`unsupported output format "%s", supported formats are: %+v`, f, AvailableFormats)
|
||||
}
|
||||
|
||||
writer, err := newMultiWriter(newWriterDescription(format, path, cfg))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return writer, nil
|
||||
}
|
||||
|
||||
// parseOutputFlags utility to parse command-line option strings and retain the existing behavior of default format and file
|
||||
func parseOutputFlags(outputs []string, defaultFile string, cfg PresentationConfig) (out []scanResultWriterDescription, errs error) {
|
||||
// always should have one option -- we generally get the default of "table", but just make sure
|
||||
if len(outputs) == 0 {
|
||||
outputs = append(outputs, TableFormat.String())
|
||||
}
|
||||
|
||||
for _, name := range outputs {
|
||||
name = strings.TrimSpace(name)
|
||||
|
||||
// split to at most two parts for <format>=<file>
|
||||
parts := strings.SplitN(name, "=", 2)
|
||||
|
||||
// the format name is the first part
|
||||
name = parts[0]
|
||||
|
||||
// default to the --file or empty string if not specified
|
||||
file := defaultFile
|
||||
|
||||
// If a file is specified as part of the output formatName, use that
|
||||
if len(parts) > 1 {
|
||||
file = parts[1]
|
||||
}
|
||||
|
||||
format := Parse(name)
|
||||
|
||||
if format == UnknownFormat {
|
||||
errs = multierror.Append(errs, fmt.Errorf(`unsupported output format "%s", supported formats are: %+v`, name, AvailableFormats))
|
||||
continue
|
||||
}
|
||||
|
||||
out = append(out, newWriterDescription(format, file, cfg))
|
||||
}
|
||||
return out, errs
|
||||
}
|
||||
|
||||
// scanResultWriterDescription Format and path strings used to create ScanResultWriter
|
||||
type scanResultWriterDescription struct {
|
||||
Format Format
|
||||
Path string
|
||||
Cfg PresentationConfig
|
||||
}
|
||||
|
||||
func newWriterDescription(f Format, p string, cfg PresentationConfig) scanResultWriterDescription {
|
||||
expandedPath, err := homedir.Expand(p)
|
||||
if err != nil {
|
||||
log.Warnf("could not expand given writer output path=%q: %w", p, err)
|
||||
// ignore errors
|
||||
expandedPath = p
|
||||
}
|
||||
return scanResultWriterDescription{
|
||||
Format: f,
|
||||
Path: expandedPath,
|
||||
Cfg: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
// scanResultMultiWriter holds a list of child ScanResultWriters to apply all Write and Close operations to
|
||||
type scanResultMultiWriter struct {
|
||||
writers []ScanResultWriter
|
||||
}
|
||||
|
||||
// newMultiWriter create all report writers from input options; if a file is not specified the given defaultWriter is used
|
||||
func newMultiWriter(options ...scanResultWriterDescription) (_ *scanResultMultiWriter, err error) {
|
||||
if len(options) == 0 {
|
||||
return nil, fmt.Errorf("no output options provided")
|
||||
}
|
||||
|
||||
out := &scanResultMultiWriter{}
|
||||
|
||||
for _, option := range options {
|
||||
switch len(option.Path) {
|
||||
case 0:
|
||||
out.writers = append(out.writers, &scanResultPublisher{
|
||||
format: option.Format,
|
||||
cfg: option.Cfg,
|
||||
})
|
||||
default:
|
||||
// create any missing subdirectories
|
||||
dir := path.Dir(option.Path)
|
||||
if dir != "" {
|
||||
s, err := os.Stat(dir)
|
||||
if err != nil {
|
||||
err = os.MkdirAll(dir, 0755) // maybe should be os.ModePerm ?
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else if !s.IsDir() {
|
||||
return nil, fmt.Errorf("output path does not contain a valid directory: %s", option.Path)
|
||||
}
|
||||
}
|
||||
fileOut, err := os.OpenFile(option.Path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to create report file: %w", err)
|
||||
}
|
||||
out.writers = append(out.writers, &scanResultStreamWriter{
|
||||
format: option.Format,
|
||||
out: fileOut,
|
||||
cfg: option.Cfg,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// Write writes the result to all writers
|
||||
func (m *scanResultMultiWriter) Write(s models.PresenterConfig) (errs error) {
|
||||
for _, w := range m.writers {
|
||||
err := w.Write(s)
|
||||
if err != nil {
|
||||
errs = multierror.Append(errs, fmt.Errorf("unable to write result: %w", err))
|
||||
}
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
// scanResultStreamWriter implements ScanResultWriter for a given format and io.Writer, also providing a close function for cleanup
|
||||
type scanResultStreamWriter struct {
|
||||
format Format
|
||||
cfg PresentationConfig
|
||||
out io.Writer
|
||||
}
|
||||
|
||||
// Write the provided result to the data stream
|
||||
func (w *scanResultStreamWriter) Write(s models.PresenterConfig) error {
|
||||
pres := GetPresenter(w.format, w.cfg, s)
|
||||
if err := pres.Present(w.out); err != nil {
|
||||
return fmt.Errorf("unable to encode result: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close any resources, such as open files
|
||||
func (w *scanResultStreamWriter) Close() error {
|
||||
if closer, ok := w.out.(io.Closer); ok {
|
||||
return closer.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// scanResultPublisher implements ScanResultWriter that publishes results to the event bus
|
||||
type scanResultPublisher struct {
|
||||
format Format
|
||||
cfg PresentationConfig
|
||||
}
|
||||
|
||||
// Write the provided result to the data stream
|
||||
func (w *scanResultPublisher) Write(s models.PresenterConfig) error {
|
||||
pres := GetPresenter(w.format, w.cfg, s)
|
||||
buf := &bytes.Buffer{}
|
||||
if err := pres.Present(buf); err != nil {
|
||||
return fmt.Errorf("unable to encode result: %w", err)
|
||||
}
|
||||
|
||||
bus.Report(buf.String())
|
||||
return nil
|
||||
}
|
222
internal/format/writer_test.go
Normal file
222
internal/format/writer_test.go
Normal file
|
@ -0,0 +1,222 @@
|
|||
package format
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker/pkg/homedir"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_MakeScanResultWriter(t *testing.T) {
|
||||
tests := []struct {
|
||||
outputs []string
|
||||
wantErr assert.ErrorAssertionFunc
|
||||
}{
|
||||
{
|
||||
outputs: []string{"json"},
|
||||
wantErr: assert.NoError,
|
||||
},
|
||||
{
|
||||
outputs: []string{"table", "json"},
|
||||
wantErr: assert.NoError,
|
||||
},
|
||||
{
|
||||
outputs: []string{"unknown"},
|
||||
wantErr: func(t assert.TestingT, err error, bla ...interface{}) bool {
|
||||
return assert.ErrorContains(t, err, `unsupported output format "unknown", supported formats are: [`)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
_, err := MakeScanResultWriter(tt.outputs, "", PresentationConfig{})
|
||||
tt.wantErr(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_newSBOMMultiWriter(t *testing.T) {
|
||||
type writerConfig struct {
|
||||
format string
|
||||
file string
|
||||
}
|
||||
|
||||
tmp := t.TempDir()
|
||||
|
||||
testName := func(options []scanResultWriterDescription, err bool) string {
|
||||
var out []string
|
||||
for _, opt := range options {
|
||||
out = append(out, string(opt.Format)+"="+opt.Path)
|
||||
}
|
||||
errs := ""
|
||||
if err {
|
||||
errs = "(err)"
|
||||
}
|
||||
return strings.Join(out, ", ") + errs
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
outputs []scanResultWriterDescription
|
||||
err bool
|
||||
expected []writerConfig
|
||||
}{
|
||||
{
|
||||
outputs: []scanResultWriterDescription{},
|
||||
err: true,
|
||||
},
|
||||
{
|
||||
outputs: []scanResultWriterDescription{
|
||||
{
|
||||
Format: "table",
|
||||
Path: "",
|
||||
},
|
||||
},
|
||||
expected: []writerConfig{
|
||||
{
|
||||
format: "table",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
outputs: []scanResultWriterDescription{
|
||||
{
|
||||
Format: "json",
|
||||
},
|
||||
},
|
||||
expected: []writerConfig{
|
||||
{
|
||||
format: "json",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
outputs: []scanResultWriterDescription{
|
||||
{
|
||||
Format: "json",
|
||||
Path: "test-2.json",
|
||||
},
|
||||
},
|
||||
expected: []writerConfig{
|
||||
{
|
||||
format: "json",
|
||||
file: "test-2.json",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
outputs: []scanResultWriterDescription{
|
||||
{
|
||||
Format: "json",
|
||||
Path: "test-3/1.json",
|
||||
},
|
||||
{
|
||||
Format: "spdx-json",
|
||||
Path: "test-3/2.json",
|
||||
},
|
||||
},
|
||||
expected: []writerConfig{
|
||||
{
|
||||
format: "json",
|
||||
file: "test-3/1.json",
|
||||
},
|
||||
{
|
||||
format: "spdx-json",
|
||||
file: "test-3/2.json",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
outputs: []scanResultWriterDescription{
|
||||
{
|
||||
Format: "text",
|
||||
},
|
||||
{
|
||||
Format: "spdx-json",
|
||||
Path: "test-4.json",
|
||||
},
|
||||
},
|
||||
expected: []writerConfig{
|
||||
{
|
||||
format: "text",
|
||||
},
|
||||
{
|
||||
format: "spdx-json",
|
||||
file: "test-4.json",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(testName(test.outputs, test.err), func(t *testing.T) {
|
||||
outputs := test.outputs
|
||||
for i := range outputs {
|
||||
if outputs[i].Path != "" {
|
||||
outputs[i].Path = tmp + outputs[i].Path
|
||||
}
|
||||
}
|
||||
|
||||
mw, err := newMultiWriter(outputs...)
|
||||
|
||||
if test.err {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
assert.Len(t, mw.writers, len(test.expected))
|
||||
|
||||
for i, e := range test.expected {
|
||||
switch w := mw.writers[i].(type) {
|
||||
case *scanResultStreamWriter:
|
||||
assert.Equal(t, string(w.format), e.format)
|
||||
if e.file != "" {
|
||||
assert.NotNil(t, w.out)
|
||||
} else {
|
||||
assert.NotNil(t, w.out)
|
||||
}
|
||||
if e.file != "" {
|
||||
assert.FileExists(t, tmp+e.file)
|
||||
}
|
||||
case *scanResultPublisher:
|
||||
assert.Equal(t, string(w.format), e.format)
|
||||
default:
|
||||
t.Fatalf("unknown writer type: %T", w)
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_newSBOMWriterDescription(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
path string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "expand home dir",
|
||||
path: "~/place.txt",
|
||||
expected: filepath.Join(homedir.Get(), "place.txt"),
|
||||
},
|
||||
{
|
||||
name: "passthrough other paths",
|
||||
path: "/other/place.txt",
|
||||
expected: "/other/place.txt",
|
||||
},
|
||||
{
|
||||
name: "no path",
|
||||
path: "",
|
||||
expected: "",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
o := newWriterDescription("table", tt.path, PresentationConfig{})
|
||||
assert.Equal(t, tt.expected, o.Path)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package format
|
||||
package stringutil
|
||||
|
||||
import "fmt"
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package internal
|
||||
package stringutil
|
||||
|
||||
import "regexp"
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package internal
|
||||
package stringutil
|
||||
|
||||
import "strings"
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package internal
|
||||
package stringutil
|
||||
|
||||
import (
|
||||
"testing"
|
|
@ -1,4 +1,4 @@
|
|||
package internal
|
||||
package stringutil
|
||||
|
||||
type StringSet map[string]struct{}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package format
|
||||
package stringutil
|
||||
|
||||
import (
|
||||
"bytes"
|
|
@ -1,36 +0,0 @@
|
|||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/wagoodman/go-partybus"
|
||||
|
||||
grypeEventParsers "github.com/anchore/grype/grype/event/parsers"
|
||||
)
|
||||
|
||||
func handleVulnerabilityScanningFinished(event partybus.Event, reportOutput io.Writer) error {
|
||||
// show the report to stdout
|
||||
pres, err := grypeEventParsers.ParseVulnerabilityScanningFinished(event)
|
||||
if err != nil {
|
||||
return fmt.Errorf("bad CatalogerFinished event: %w", err)
|
||||
}
|
||||
|
||||
if err := pres.Present(reportOutput); err != nil {
|
||||
return fmt.Errorf("unable to show vulnerability report: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleNonRootCommandFinished(event partybus.Event, reportOutput io.Writer) error {
|
||||
// show the report to stdout
|
||||
result, err := grypeEventParsers.ParseNonRootCommandFinished(event)
|
||||
if err != nil {
|
||||
return fmt.Errorf("bad NonRootCommandFinished event: %w", err)
|
||||
}
|
||||
|
||||
if _, err := reportOutput.Write([]byte(*result)); err != nil {
|
||||
return fmt.Errorf("unable to show vulnerability report: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -16,6 +16,7 @@ import (
|
|||
|
||||
"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"
|
||||
)
|
||||
|
@ -44,6 +45,7 @@ type ephemeralTerminalUI struct {
|
|||
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.
|
||||
|
@ -78,30 +80,22 @@ func (h *ephemeralTerminalUI) Handle(event partybus.Event) error {
|
|||
log.Errorf("unable to show %s event: %+v", event.Type, err)
|
||||
}
|
||||
|
||||
case event.Type == grypeEvent.AppUpdateAvailable:
|
||||
if err := handleAppUpdateAvailable(ctx, h.frame, event, h.waitGroup); err != nil {
|
||||
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.VulnerabilityScanningFinished:
|
||||
// we need to close the screen now since signaling the the presenter is ready means that we
|
||||
// are about to write bytes to stdout, so we should reset the terminal state first
|
||||
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)
|
||||
|
||||
if err := handleVulnerabilityScanningFinished(event, h.reportOutput); err != nil {
|
||||
log.Errorf("unable to show %s event: %+v", event.Type, err)
|
||||
}
|
||||
|
||||
// this is the last expected event, stop listening to events
|
||||
return h.unsubscribe()
|
||||
|
||||
case event.Type == grypeEvent.NonRootCommandFinished:
|
||||
h.closeScreen(false)
|
||||
|
||||
if err := handleNonRootCommandFinished(event, h.reportOutput); err != nil {
|
||||
log.Errorf("unable to show %s event: %+v", event.Type, err)
|
||||
}
|
||||
|
||||
// this is the last expected event, stop listening to events
|
||||
return h.unsubscribe()
|
||||
}
|
||||
|
@ -154,6 +148,11 @@ func (h *ephemeralTerminalUI) flushLog() {
|
|||
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
|
||||
}
|
||||
|
||||
|
|
|
@ -18,8 +18,8 @@ import (
|
|||
"github.com/anchore/grype/internal/version"
|
||||
)
|
||||
|
||||
func handleAppUpdateAvailable(_ context.Context, fr *frame.Frame, event partybus.Event, _ *sync.WaitGroup) error {
|
||||
newVersion, err := grypeEventParsers.ParseAppUpdateAvailable(event)
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -6,12 +6,14 @@ import (
|
|||
"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.
|
||||
|
@ -26,25 +28,28 @@ func (l *loggerUI) Setup(unsubscribe func() error) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (l loggerUI) Handle(event partybus.Event) error {
|
||||
func (l *loggerUI) Handle(event partybus.Event) error {
|
||||
switch event.Type {
|
||||
case grypeEvent.VulnerabilityScanningFinished:
|
||||
if err := handleVulnerabilityScanningFinished(event, l.reportOutput); err != nil {
|
||||
log.Warnf("unable to show catalog image finished event: %+v", err)
|
||||
case grypeEvent.CLIReport:
|
||||
_, report, err := parsers.ParseCLIReport(event)
|
||||
if err != nil {
|
||||
log.Errorf("unable to show %s event: %+v", event.Type, err)
|
||||
break
|
||||
}
|
||||
case grypeEvent.NonRootCommandFinished:
|
||||
if err := handleNonRootCommandFinished(event, l.reportOutput); err != nil {
|
||||
log.Warnf("unable to show command finished event: %+v", err)
|
||||
}
|
||||
// ignore all events except for the final events
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
l.reports = append(l.reports, report)
|
||||
case grypeEvent.CLIExit:
|
||||
// this is the last expected event, stop listening to events
|
||||
return l.unsubscribe()
|
||||
}
|
||||
|
||||
func (l loggerUI) Teardown(_ bool) error {
|
||||
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
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ import (
|
|||
"github.com/anchore/grype/grype/pkg"
|
||||
"github.com/anchore/grype/grype/store"
|
||||
"github.com/anchore/grype/grype/vulnerability"
|
||||
"github.com/anchore/grype/internal"
|
||||
"github.com/anchore/grype/internal/stringutil"
|
||||
"github.com/anchore/stereoscope/pkg/imagetest"
|
||||
"github.com/anchore/syft/syft"
|
||||
syftPkg "github.com/anchore/syft/syft/pkg"
|
||||
|
@ -538,8 +538,8 @@ func addHaskellMatches(t *testing.T, theSource source.Source, catalog *syftPkg.C
|
|||
}
|
||||
|
||||
func TestMatchByImage(t *testing.T) {
|
||||
observedMatchers := internal.NewStringSet()
|
||||
definedMatchers := internal.NewStringSet()
|
||||
observedMatchers := stringutil.NewStringSet()
|
||||
definedMatchers := stringutil.NewStringSet()
|
||||
for _, l := range match.AllMatcherTypes {
|
||||
definedMatchers.Add(string(l))
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue