feat(outputs): allow to set multiple outputs (#648) (#1346)

* 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:
Olivier Boudet 2023-07-11 19:37:17 +02:00 committed by GitHub
parent 6834e2148c
commit 9050883715
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
46 changed files with 765 additions and 449 deletions

View file

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

View file

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

View file

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

View file

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

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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"
UpdateVulnerabilityDatabase partybus.EventType = "grype-update-vulnerability-database"
VulnerabilityScanningStarted partybus.EventType = "grype-vulnerability-scanning-started"
VulnerabilityScanningFinished partybus.EventType = "grype-vulnerability-scanning-finished"
NonRootCommandFinished partybus.EventType = "grype-non-root-command-finished"
DatabaseDiffingStarted partybus.EventType = "grype-database-diffing-started"
typePrefix = internal.ApplicationName
cliTypePrefix = typePrefix + "-cli"
UpdateVulnerabilityDatabase partybus.EventType = "grype-update-vulnerability-database"
VulnerabilityScanningStarted partybus.EventType = "grype-vulnerability-scanning-started"
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"
)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

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

View file

@ -1,4 +1,4 @@
package format
package stringutil
import "fmt"

View file

@ -1,4 +1,4 @@
package internal
package stringutil
import "regexp"

View file

@ -1,4 +1,4 @@
package internal
package stringutil
import "strings"

View file

@ -1,4 +1,4 @@
package internal
package stringutil
import (
"testing"

View file

@ -1,4 +1,4 @@
package internal
package stringutil
type StringSet map[string]struct{}

View file

@ -1,4 +1,4 @@
package format
package stringutil
import (
"bytes"

View file

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

View file

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

View file

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

View file

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

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