Skip to content

Commit

Permalink
Add statistics to JSON and YAML reports (#730)
Browse files Browse the repository at this point in the history
* Add statistics to JSON and YAML reports

Signed-off-by: egibs <[email protected]>

* Add JSON stats test  case

Signed-off-by: egibs <[email protected]>

* Avoid rendering empty stats object when running diffs

Signed-off-by: egibs <[email protected]>

* Use final report field instead

Signed-off-by: egibs <[email protected]>

---------

Signed-off-by: egibs <[email protected]>
  • Loading branch information
egibs authored Dec 31, 2024
1 parent bbd5349 commit 3b49925
Show file tree
Hide file tree
Showing 18 changed files with 263 additions and 27 deletions.
6 changes: 3 additions & 3 deletions cmd/mal/mal.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion pkg/action/archive_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
2 changes: 1 addition & 1 deletion pkg/action/oci_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
2 changes: 1 addition & 1 deletion pkg/action/scan.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion pkg/malcontent/malcontent.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
2 changes: 1 addition & 1 deletion pkg/refresh/refresh.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
6 changes: 5 additions & 1 deletion pkg/render/json.go
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -52,6 +52,10 @@ func (r JSON) Full(_ context.Context, rep *malcontent.Report) error {
return true
})

if c != nil && c.Stats && jr.Diff == nil {
jr.Stats = serializedStats(c, rep)
}

j, err := json.MarshalIndent(jr, "", " ")
if err != nil {
return err
Expand Down
2 changes: 1 addition & 1 deletion pkg/render/markdown.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
34 changes: 34 additions & 0 deletions pkg/render/render.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package render
import (
"fmt"
"io"
"sort"

"github.com/chainguard-dev/malcontent/pkg/malcontent"
)
Expand All @@ -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.
Expand Down Expand Up @@ -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,
}
}
2 changes: 1 addition & 1 deletion pkg/render/simple.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
8 changes: 4 additions & 4 deletions pkg/render/stats.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 := "⚠️ "
Expand Down
2 changes: 1 addition & 1 deletion pkg/render/strings.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion pkg/render/tea.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
2 changes: 1 addition & 1 deletion pkg/render/terminal.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion pkg/render/terminal_brief.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 5 additions & 1 deletion pkg/render/yaml.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -52,6 +52,10 @@ func (r YAML) Full(_ context.Context, rep *malcontent.Report) error {
return true
})

if c != nil && c.Stats && yr.Diff == nil {
yr.Stats = serializedStats(c, rep)
}

yaml, err := yaml.Marshal(yr)
if err != nil {
return err
Expand Down
113 changes: 113 additions & 0 deletions tests/macOS/clean/ls.stats.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
{
"Files": {
"macOS/clean/ls": {
"Path": "macOS/clean/ls",
"SHA256": "461b7ef5288c9c4b0d10aefc9ca42f7ddab9954a3f3d032e3a783e3da0c970b6",
"Size": 154352,
"Syscalls": [
"getdents",
"openat",
"readlink"
],
"Pledge": [
"rpath"
],
"Behaviors": [
{
"Description": "binary contains hardcoded URL",
"MatchStrings": [
"http://crl.apple.com/codesigning.crl0",
"http://www.apple.com/DTDs/PropertyList",
"http://www.apple.com/appleca/root.crl0",
"https://www.apple.com/appleca/0"
],
"RiskScore": 1,
"RiskLevel": "LOW",
"RuleURL": "https://github.com/chainguard-dev/malcontent/blob/main/rules/c2/addr/url.yara#binary_with_url",
"ID": "c2/addr/url",
"RuleName": "binary_with_url"
},
{
"Description": "Look up or override terminal settings",
"MatchStrings": [
"TERM"
],
"RiskScore": 1,
"RiskLevel": "LOW",
"RuleURL": "https://github.com/chainguard-dev/malcontent/blob/main/rules/exec/shell/TERM.yara#TERM",
"ReferenceURL": "https://www.gnu.org/software/gettext/manual/html_node/The-TERM-variable.html",
"ID": "exec/shell/TERM",
"RuleName": "TERM"
},
{
"Description": "traverse filesystem hierarchy",
"MatchStrings": [
"_fts_children",
"_fts_close",
"_fts_open",
"_fts_read",
"_fts_set"
],
"RiskScore": 1,
"RiskLevel": "LOW",
"RuleURL": "https://github.com/chainguard-dev/malcontent/blob/main/rules/fs/directory/directory-traverse.yara#fts",
"ID": "fs/directory/traverse",
"RuleName": "fts"
},
{
"Description": "read value of a symbolic link",
"MatchStrings": [
"readlink"
],
"RiskScore": 1,
"RiskLevel": "LOW",
"RuleURL": "https://github.com/chainguard-dev/malcontent/blob/main/rules/fs/link-read.yara#readlink",
"ReferenceURL": "https://man7.org/linux/man-pages/man2/readlink.2.html",
"ID": "fs/link_read",
"RuleName": "readlink"
}
],
"RiskScore": 1,
"RiskLevel": "LOW"
}
},
"Stats": {
"PkgStats": [
{
"Count": 1,
"Key": "c2/addr/url",
"Total": 4,
"Value": 25
},
{
"Count": 1,
"Key": "exec/shell/TERM",
"Total": 4,
"Value": 25
},
{
"Count": 1,
"Key": "fs/directory/traverse",
"Total": 4,
"Value": 25
},
{
"Count": 1,
"Key": "fs/link_read",
"Total": 4,
"Value": 25
}
],
"ProcessedFiles": 1,
"RiskStats": [
{
"Count": 1,
"Key": 1,
"Total": 1,
"Value": 100
}
],
"TotalBehaviors": 4,
"TotalRisks": 1
}
}
Loading

0 comments on commit 3b49925

Please sign in to comment.