Skip to content

Commit

Permalink
Add new renderer to display string matches for rules (#488)
Browse files Browse the repository at this point in the history
* Add new renderer to display string matches for rules

Signed-off-by: egibs <20933572+egibs@users.noreply.github.com>

* Update prefix; drop divider

Signed-off-by: egibs <20933572+egibs@users.noreply.github.com>

* Add rule and string match count, appease the linter

Signed-off-by: egibs <20933572+egibs@users.noreply.github.com>

* Update example comment

Signed-off-by: egibs <20933572+egibs@users.noreply.github.com>

* Add 'strings' to format flag usage

Signed-off-by: egibs <20933572+egibs@users.noreply.github.com>

---------

Signed-off-by: egibs <20933572+egibs@users.noreply.github.com>
  • Loading branch information
egibs authored Oct 6, 2024
1 parent ab8a15b commit 2a59576
Show file tree
Hide file tree
Showing 4 changed files with 111 additions and 3 deletions.
2 changes: 1 addition & 1 deletion cmd/mal/mal.go
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,7 @@ func main() {
&cli.StringFlag{
Name: "format",
Value: "auto",
Usage: "Output format (json, markdown, simple, terminal, yaml)",
Usage: "Output format (json, markdown, simple, strings, terminal, yaml)",
Destination: &formatFlag,
},
&cli.BoolFlag{
Expand Down
2 changes: 2 additions & 0 deletions pkg/render/render.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ func New(kind string, w io.Writer) (malcontent.Renderer, error) {
return NewJSON(w), nil
case "simple":
return NewSimple(w), nil
case "strings":
return NewStringMatches(w), nil
default:
return nil, fmt.Errorf("unknown renderer: %q", kind)
}
Expand Down
106 changes: 106 additions & 0 deletions pkg/render/strings.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// Copyright 2024 Chainguard, Inc.
// SPDX-License-Identifier: Apache-2.0
//
// String matches renderer
//
// Example:
//
// Matches for /sbin/ping [MED] (15 rules):
// _connect [MED] (1 string):
// - _connect
// bsd_if [LOW] (1 string):
// - if_nametoindex
// bsd_ifaddrs [MED] (2 strings):
// - freeifaddrs
// - getifaddrs
// generic_scan_tool [MED] (5 strings):
// - connect
// - gethostbyname
// - port
// - scan
// - socket
// ...

package render

import (
"context"
"fmt"
"io"
"sort"
"strings"

"github.com/chainguard-dev/malcontent/pkg/malcontent"
"github.com/fatih/color"
)

// Map to handle RiskScore -> RiskLevel conversions.
var riskLevels = map[int]string{
0: "NONE", // harmless: common to all executables, no system impact
1: "LOW", // undefined: low impact, common to good and bad executables
2: "MEDIUM", // notable: may have impact, but common
3: "HIGH", // suspicious: uncommon, but could be legit
4: "CRITICAL", // critical: certainly malware
}

type StringMatches struct {
w io.Writer
}

func NewStringMatches(w io.Writer) StringMatches {
return StringMatches{w: w}
}

type Match struct {
Description string
Risk int
Rule string
Strings []string
}

func (r StringMatches) File(_ context.Context, fr *malcontent.FileReport) error {
if len(fr.Behaviors) == 0 {
return nil
}

matches := []Match{}
sort.Slice(fr.Behaviors, func(i, j int) bool {
return fr.Behaviors[i].RuleName < fr.Behaviors[j].RuleName
})
for _, b := range fr.Behaviors {
if len(b.MatchStrings) > 0 {
matches = append(matches, Match{
Risk: b.RiskScore,
Rule: b.RuleName,
Strings: b.MatchStrings,
})
}
}

prefix := "Matches for"
rUnit := plural("rule", len(matches))
fmt.Fprintf(r.w, "%s %s %s%s%s %s%s %s%s:\n", prefix, color.HiGreenString(fr.Path), color.HiBlackString("["), briefRiskColor(fr.RiskLevel), color.HiBlackString("]"), color.HiBlackString("("), color.HiGreenString(fmt.Sprintf("%d", len(matches))), color.HiGreenString(rUnit), color.HiBlackString(")"))
for _, m := range matches {
sUnit := plural("string", len(m.Strings))
fmt.Fprintf(r.w, "%s %s%s%s %s%s %s%s: \n%s%s\n", color.HiCyanString(m.Rule), color.HiBlackString("["), briefRiskColor(riskLevels[m.Risk]), color.HiBlackString("]"), color.HiBlackString("("), color.HiGreenString(fmt.Sprintf("%d", len(m.Strings))), color.HiGreenString(sUnit), color.HiBlackString(")"), color.HiBlackString("- "), strings.Join(m.Strings, color.HiBlackString("\n- ")))
}

return nil
}

func (r StringMatches) Full(_ context.Context, rep *malcontent.Report) error {
// Non-diff files are handled on the fly by File()
if rep.Diff == nil {
return nil
}

return fmt.Errorf("diffs are unsupported by the StringMatches renderer")
}

// plural returns a pluralized string if the length of l is greater than 1.
func plural(s string, l int) string {
if l > 1 {
return fmt.Sprintf("%ss", s)
}
return s
}
4 changes: 2 additions & 2 deletions pkg/render/terminal_brief.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@ func NewTerminalBrief(w io.Writer) TerminalBrief {
func briefRiskColor(level string) string {
switch level {
case "LOW":
return color.HiGreenString("LOW ")
return color.HiGreenString("LOW")
case "MEDIUM", "MED":
return color.HiYellowString("MED ")
return color.HiYellowString("MED")
case "HIGH":
return color.HiRedString("HIGH")
case "CRITICAL", "CRIT":
Expand Down

0 comments on commit 2a59576

Please sign in to comment.