From 14dc8d243f38888c9647dfb097d10b3b3d55e833 Mon Sep 17 00:00:00 2001 From: egibs <20933572+egibs@users.noreply.github.com> Date: Wed, 18 Dec 2024 15:38:55 -0600 Subject: [PATCH] Add statistics to JSON and YAML reports Signed-off-by: egibs <20933572+egibs@users.noreply.github.com> --- cmd/mal/mal.go | 6 +++--- pkg/action/archive_test.go | 2 +- pkg/action/oci_test.go | 2 +- pkg/action/scan.go | 2 +- pkg/malcontent/malcontent.go | 2 +- pkg/refresh/refresh.go | 2 +- pkg/render/json.go | 6 +++++- pkg/render/markdown.go | 2 +- pkg/render/render.go | 34 ++++++++++++++++++++++++++++++++++ pkg/render/simple.go | 2 +- pkg/render/stats.go | 8 ++++---- pkg/render/strings.go | 2 +- pkg/render/tea.go | 2 +- pkg/render/terminal.go | 2 +- pkg/render/terminal_brief.go | 2 +- pkg/render/yaml.go | 6 +++++- tests/samples_test.go | 14 +++++++------- 17 files changed, 69 insertions(+), 27 deletions(-) diff --git a/cmd/mal/mal.go b/cmd/mal/mal.go index 68988b6c2..b9c648cb2 100644 --- a/cmd/mal/mal.go +++ b/cmd/mal/mal.go @@ -424,7 +424,7 @@ func main() { return err } - err = renderer.Full(ctx, res) + err = renderer.Full(ctx, &mc, res) if err != nil { returnCode = ExitRenderFailed return err @@ -467,7 +467,7 @@ func main() { return err } - err = renderer.Full(ctx, res) + err = renderer.Full(ctx, &mc, res) if err != nil { returnCode = ExitRenderFailed return err @@ -550,7 +550,7 @@ func main() { return length }(&res.Files) - err = renderer.Full(ctx, res) + err = renderer.Full(ctx, &mc, res) if err != nil { returnCode = ExitRenderFailed return err diff --git a/pkg/action/archive_test.go b/pkg/action/archive_test.go index 1f3f5d235..dba30983f 100644 --- a/pkg/action/archive_test.go +++ b/pkg/action/archive_test.go @@ -243,7 +243,7 @@ func TestScanArchive(t *testing.T) { if err != nil { t.Fatal(err) } - if err := r.Full(ctx, res); err != nil { + if err := r.Full(ctx, nil, res); err != nil { t.Fatalf("full: %v", err) } diff --git a/pkg/action/oci_test.go b/pkg/action/oci_test.go index 8ea084f64..7014a8936 100644 --- a/pkg/action/oci_test.go +++ b/pkg/action/oci_test.go @@ -46,7 +46,7 @@ func TestOCI(t *testing.T) { if err != nil { t.Fatal(err) } - if err := r.Full(ctx, res); err != nil { + if err := r.Full(ctx, nil, res); err != nil { t.Fatalf("full: %v", err) } diff --git a/pkg/action/scan.go b/pkg/action/scan.go index 2b29465ba..fe1de3c28 100644 --- a/pkg/action/scan.go +++ b/pkg/action/scan.go @@ -572,7 +572,7 @@ func Scan(ctx context.Context, c malcontent.Config) (*malcontent.Report, error) } return true }) - if c.Stats { + if c.Stats && c.Renderer.Name() != "JSON" && c.Renderer.Name() != "YAML" { err = render.Statistics(&c, r) if err != nil { return r, fmt.Errorf("stats: %w", err) diff --git a/pkg/malcontent/malcontent.go b/pkg/malcontent/malcontent.go index 49319b0e4..348ed6066 100644 --- a/pkg/malcontent/malcontent.go +++ b/pkg/malcontent/malcontent.go @@ -17,7 +17,7 @@ import ( type Renderer interface { Scanning(context.Context, string) File(context.Context, *FileReport) error - Full(context.Context, *Report) error + Full(context.Context, *Config, *Report) error Name() string } diff --git a/pkg/refresh/refresh.go b/pkg/refresh/refresh.go index 7a5f0b4aa..6e873b795 100644 --- a/pkg/refresh/refresh.go +++ b/pkg/refresh/refresh.go @@ -195,7 +195,7 @@ func executeRefresh(ctx context.Context, testData []TestData) error { return fmt.Errorf("refresh sample data for %s: %w", data.OutputPath, err) } - if err := data.Config.Renderer.Full(ctx, res); err != nil { + if err := data.Config.Renderer.Full(ctx, nil, res); err != nil { return fmt.Errorf("render results for %s: %w", data.OutputPath, err) } diff --git a/pkg/render/json.go b/pkg/render/json.go index 21d6ab30e..92365a38f 100644 --- a/pkg/render/json.go +++ b/pkg/render/json.go @@ -28,7 +28,7 @@ func (r JSON) File(_ context.Context, _ *malcontent.FileReport) error { return nil } -func (r JSON) Full(_ context.Context, rep *malcontent.Report) error { +func (r JSON) Full(_ context.Context, c *malcontent.Config, rep *malcontent.Report) error { jr := Report{ Diff: rep.Diff, Files: make(map[string]*malcontent.FileReport), @@ -52,6 +52,10 @@ func (r JSON) Full(_ context.Context, rep *malcontent.Report) error { return true }) + if c != nil && c.Stats { + jr.Stats = serializedStats(c, rep) + } + j, err := json.MarshalIndent(jr, "", " ") if err != nil { return err diff --git a/pkg/render/markdown.go b/pkg/render/markdown.go index ae8c38c63..d8ceb9a6b 100644 --- a/pkg/render/markdown.go +++ b/pkg/render/markdown.go @@ -59,7 +59,7 @@ func (r Markdown) File(ctx context.Context, fr *malcontent.FileReport) error { return nil } -func (r Markdown) Full(ctx context.Context, rep *malcontent.Report) error { +func (r Markdown) Full(ctx context.Context, _ *malcontent.Config, rep *malcontent.Report) error { if rep.Diff == nil { return nil } diff --git a/pkg/render/render.go b/pkg/render/render.go index 19a078f6f..9ee8dd6a6 100644 --- a/pkg/render/render.go +++ b/pkg/render/render.go @@ -6,6 +6,7 @@ package render import ( "fmt" "io" + "sort" "github.com/chainguard-dev/malcontent/pkg/malcontent" ) @@ -15,6 +16,17 @@ type Report struct { Diff *malcontent.DiffReport `json:",omitempty" yaml:",omitempty"` Files map[string]*malcontent.FileReport `json:",omitempty" yaml:",omitempty"` Filter string `json:",omitempty" yaml:",omitempty"` + Stats *Stats `json:",omitempty" yaml:",omitempty"` +} + +// Stats stores a JSON- or YAML-friendly Statistics report. +type Stats struct { + PkgStats []malcontent.StrMetric `json:",omitempty" yaml:",omitempty"` + ProcessedFiles int `json:",omitempty" yaml:",omitempty"` + RiskStats []malcontent.IntMetric `json:",omitempty" yaml:",omitempty"` + SkippedFiles int `json:",omitempty" yaml:",omitempty"` + TotalBehaviors int `json:",omitempty" yaml:",omitempty"` + TotalRisks int `json:",omitempty" yaml:",omitempty"` } // New returns a new Renderer. @@ -56,3 +68,25 @@ func riskEmoji(score int) string { return symbol } + +func serializedStats(c *malcontent.Config, r *malcontent.Report) *Stats { + pkgStats, _, totalBehaviors := PkgStatistics(c, &r.Files) + riskStats, totalRisks, processedFiles, skippedFiles := RiskStatistics(c, &r.Files) + + sort.Slice(pkgStats, func(i, j int) bool { + return pkgStats[i].Key < pkgStats[j].Key + }) + + sort.Slice(riskStats, func(i, j int) bool { + return riskStats[i].Key < riskStats[j].Key + }) + + return &Stats{ + PkgStats: pkgStats, + ProcessedFiles: processedFiles, + RiskStats: riskStats, + SkippedFiles: skippedFiles, + TotalBehaviors: totalBehaviors, + TotalRisks: totalRisks, + } +} diff --git a/pkg/render/simple.go b/pkg/render/simple.go index ec3093c0b..97bad7da4 100644 --- a/pkg/render/simple.go +++ b/pkg/render/simple.go @@ -48,7 +48,7 @@ func (r Simple) File(_ context.Context, fr *malcontent.FileReport) error { return nil } -func (r Simple) Full(_ context.Context, rep *malcontent.Report) error { +func (r Simple) Full(_ context.Context, _ *malcontent.Config, rep *malcontent.Report) error { if rep.Diff == nil { return nil } diff --git a/pkg/render/stats.go b/pkg/render/stats.go index 155c724f0..e438d1467 100644 --- a/pkg/render/stats.go +++ b/pkg/render/stats.go @@ -19,7 +19,7 @@ func smLength(m *sync.Map) int { return length } -func riskStatistics(c *malcontent.Config, files *sync.Map) ([]malcontent.IntMetric, int, int, int) { +func RiskStatistics(c *malcontent.Config, files *sync.Map) ([]malcontent.IntMetric, int, int, int) { length := smLength(files) riskMap := make(map[int][]string, length) @@ -73,7 +73,7 @@ func riskStatistics(c *malcontent.Config, files *sync.Map) ([]malcontent.IntMetr return stats, total(), processedFiles, skippedFiles } -func pkgStatistics(_ *malcontent.Config, files *sync.Map) ([]malcontent.StrMetric, int, int) { +func PkgStatistics(_ *malcontent.Config, files *sync.Map) ([]malcontent.StrMetric, int, int) { length := smLength(files) numBehaviors := 0 pkgMap := make(map[string]int, length) @@ -117,8 +117,8 @@ func pkgStatistics(_ *malcontent.Config, files *sync.Map) ([]malcontent.StrMetri } func Statistics(c *malcontent.Config, r *malcontent.Report) error { - riskStats, totalRisks, processedFiles, skippedFiles := riskStatistics(c, &r.Files) - pkgStats, width, totalBehaviors := pkgStatistics(c, &r.Files) + riskStats, totalRisks, processedFiles, skippedFiles := RiskStatistics(c, &r.Files) + pkgStats, width, totalBehaviors := PkgStatistics(c, &r.Files) statsSymbol := "📊" riskSymbol := "⚠️ " diff --git a/pkg/render/strings.go b/pkg/render/strings.go index 269e76047..342ae1470 100644 --- a/pkg/render/strings.go +++ b/pkg/render/strings.go @@ -109,7 +109,7 @@ func (r StringMatches) File(_ context.Context, fr *malcontent.FileReport) error return nil } -func (r StringMatches) Full(_ context.Context, rep *malcontent.Report) error { +func (r StringMatches) Full(_ context.Context, _ *malcontent.Config, rep *malcontent.Report) error { // Non-diff files are handled on the fly by File() if rep.Diff == nil { return nil diff --git a/pkg/render/tea.go b/pkg/render/tea.go index c9f5c8cf1..e959994c5 100644 --- a/pkg/render/tea.go +++ b/pkg/render/tea.go @@ -385,7 +385,7 @@ func (r *Interactive) File(ctx context.Context, fr *malcontent.FileReport) error return nil } -func (r *Interactive) Full(ctx context.Context, rep *malcontent.Report) error { +func (r *Interactive) Full(ctx context.Context, _ *malcontent.Config, rep *malcontent.Report) error { defer func() { r.program.Send(scanCompleteMsg{}) r.wg.Wait() diff --git a/pkg/render/terminal.go b/pkg/render/terminal.go index c330c89cf..d225e8640 100644 --- a/pkg/render/terminal.go +++ b/pkg/render/terminal.go @@ -92,7 +92,7 @@ func (r Terminal) File(ctx context.Context, fr *malcontent.FileReport) error { return nil } -func (r Terminal) Full(ctx context.Context, rep *malcontent.Report) error { +func (r Terminal) Full(ctx context.Context, _ *malcontent.Config, rep *malcontent.Report) error { // Non-diff files are handled on the fly by File() if rep.Diff == nil { return nil diff --git a/pkg/render/terminal_brief.go b/pkg/render/terminal_brief.go index d00bbe338..0254c74a2 100644 --- a/pkg/render/terminal_brief.go +++ b/pkg/render/terminal_brief.go @@ -69,7 +69,7 @@ func (r TerminalBrief) File(_ context.Context, fr *malcontent.FileReport) error return nil } -func (r TerminalBrief) Full(_ context.Context, rep *malcontent.Report) error { +func (r TerminalBrief) Full(_ context.Context, _ *malcontent.Config, rep *malcontent.Report) error { // Non-diff files are handled on the fly by File() if rep.Diff == nil { return nil diff --git a/pkg/render/yaml.go b/pkg/render/yaml.go index 387b72aee..3756036dc 100644 --- a/pkg/render/yaml.go +++ b/pkg/render/yaml.go @@ -28,7 +28,7 @@ func (r YAML) File(_ context.Context, _ *malcontent.FileReport) error { return nil } -func (r YAML) Full(_ context.Context, rep *malcontent.Report) error { +func (r YAML) Full(_ context.Context, c *malcontent.Config, rep *malcontent.Report) error { // Make the sync.Map YAML-friendly yr := Report{ Diff: rep.Diff, @@ -52,6 +52,10 @@ func (r YAML) Full(_ context.Context, rep *malcontent.Report) error { return true }) + if c != nil && c.Stats { + yr.Stats = serializedStats(c, rep) + } + yaml, err := yaml.Marshal(yr) if err != nil { return err diff --git a/tests/samples_test.go b/tests/samples_test.go index 237537285..eb37034fa 100644 --- a/tests/samples_test.go +++ b/tests/samples_test.go @@ -117,7 +117,7 @@ func TestJSON(t *testing.T) { t.Fatalf("scan failed: %v", err) } - if err := render.Full(ctx, res); err != nil { + if err := render.Full(ctx, nil, res); err != nil { t.Fatalf("full: %v", err) } @@ -196,7 +196,7 @@ func TestSimple(t *testing.T) { t.Fatalf("scan failed: %v", err) } - if err := simple.Full(ctx, res); err != nil { + if err := simple.Full(ctx, nil, res); err != nil { t.Fatalf("full: %v", err) } @@ -278,7 +278,7 @@ func TestDiff(t *testing.T) { t.Fatalf("diff failed: %v", err) } - if err := simple.Full(ctx, res); err != nil { + if err := simple.Full(ctx, nil, res); err != nil { t.Fatalf("full: %v", err) } @@ -345,7 +345,7 @@ func TestDiffFileChange(t *testing.T) { t.Fatalf("diff failed: %v", err) } - if err := simple.Full(ctx, res); err != nil { + if err := simple.Full(ctx, nil, res); err != nil { t.Fatalf("full: %v", err) } @@ -412,7 +412,7 @@ func TestDiffFileIncrease(t *testing.T) { t.Fatalf("diff failed: %v", err) } - if err := simple.Full(ctx, res); err != nil { + if err := simple.Full(ctx, nil, res); err != nil { t.Fatalf("full: %v", err) } @@ -512,7 +512,7 @@ func TestMarkdown(t *testing.T) { t.Fatalf("scan failed: %v", err) } - if err := simple.Full(ctx, res); err != nil { + if err := simple.Full(ctx, nil, res); err != nil { t.Fatalf("full: %v", err) } @@ -594,7 +594,7 @@ func Template(b *testing.B, paths []string) func() { if err != nil { b.Fatal(err) } - if err := simple.Full(ctx, res); err != nil { + if err := simple.Full(ctx, nil, res); err != nil { b.Fatalf("full: %v", err) } }