From e7eb3bd05f82689569969dc5ea4f670718fa5551 Mon Sep 17 00:00:00 2001 From: egibs <20933572+egibs@users.noreply.github.com> Date: Tue, 10 Sep 2024 07:18:13 -0500 Subject: [PATCH] Add separate analyze command for displaying all rule matches Signed-off-by: egibs <20933572+egibs@users.noreply.github.com> --- bincapz.go | 56 +++++++++++++++++++++++++++++++++++++++++- pkg/bincapz/bincapz.go | 1 + pkg/report/report.go | 29 +++++++++++++++++++++- 3 files changed, 84 insertions(+), 2 deletions(-) diff --git a/bincapz.go b/bincapz.go index ad2b5538..a912125d 100644 --- a/bincapz.go +++ b/bincapz.go @@ -56,6 +56,7 @@ var ( outputFlag string profileFlag bool quantityIncreasesRiskFlag bool + scanFlag bool statsFlag bool thirdPartyFlag bool verboseFlag bool @@ -314,6 +315,12 @@ func main() { Usage: "Increase file risk score based on behavior quantity", Destination: &quantityIncreasesRiskFlag, }, + &cli.BoolFlag{ + Name: "scan", + Value: false, + Usage: "Only return findings matching the highest severity", + Destination: &scanFlag, + }, &cli.BoolFlag{ Name: "stats", Aliases: []string{"s"}, @@ -335,6 +342,52 @@ func main() { }, }, Commands: []*cli.Command{ + { + Name: "analyze", + Usage: "fully interrogate a path", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "image", + Aliases: []string{"i"}, + Value: "", + Usage: "Scan an image", + }, + &cli.StringFlag{ + Name: "path", + Aliases: []string{"p"}, + Value: "", + Usage: "Scan a file path", + }, + }, + Action: func(c *cli.Context) error { + // Handle edge cases + // Set bc.OCI if the image flag is used + // Default to path scanning if neither flag is passed (images must be scanned via --image or -i) + switch { + case c.String("image") != "": + bc.OCI = true + case c.String("image") == "" || c.String("path") == "": + cmdArgs := c.Args().Slice() + bc.ScanPaths = []string{cmdArgs[0]} + } + + res, err = action.Scan(ctx, bc) + if err != nil { + log.Error("scan failed", slog.Any("error", err)) + returnCode = ExitActionFailed + return err + } + + err = renderer.Full(ctx, res) + if err != nil { + log.Error("render failed", slog.Any("error", err)) + returnCode = ExitRenderFailed + return err + } + + return nil + }, + }, { Name: "diff", Usage: "scan and diff two paths", @@ -357,7 +410,7 @@ func main() { }, { Name: "scan", - Usage: "scan an image or path", + Usage: "tersely scan a path and return findings of the highest severity", Flags: []cli.Flag{ &cli.StringFlag{ Name: "image", @@ -373,6 +426,7 @@ func main() { }, }, Action: func(c *cli.Context) error { + bc.Scan = true // Handle edge cases // Set bc.OCI if the image flag is used // Default to path scanning if neither flag is passed (images must be scanned via --image or -i) diff --git a/pkg/bincapz/bincapz.go b/pkg/bincapz/bincapz.go index 75b75896..7ceff931 100644 --- a/pkg/bincapz/bincapz.go +++ b/pkg/bincapz/bincapz.go @@ -31,6 +31,7 @@ type Config struct { QuantityIncreasesRisk bool Renderer Renderer Rules *yara.Rules + Scan bool ScanPaths []string Stats bool } diff --git a/pkg/report/report.go b/pkg/report/report.go index 4fd935e4..5c187d3b 100644 --- a/pkg/report/report.go +++ b/pkg/report/report.go @@ -331,6 +331,12 @@ func Generate(ctx context.Context, path string, mrs yara.MatchRules, c bincapz.C risk := 0 key := "" + // If we're running a scan, only diplay findings of the highest severity + var highestRisk int + if c.Scan { + highestRisk = highestMatchRisk(mrs) + } + for _, m := range mrs { if all(m.Rule == BINARY, ignoreSelf) { ignoreBincapz = true @@ -342,7 +348,12 @@ func Generate(ctx context.Context, path string, mrs yara.MatchRules, c bincapz.C riskCounts[risk]++ // The bincapz rule is classified as harmless // This will prevent the rule from being filtered - if risk < minScore && !ignoreBincapz { + // If running a scan as opposed to an analyze, + // drop any matches that fall below the highest severity + switch { + case risk < minScore && !ignoreBincapz: + continue + case c.Scan && risk < highestRisk: continue } key = generateKey(m.Namespace, m.Rule) @@ -535,3 +546,19 @@ func all(conditions ...bool) bool { } return true } + +// highestMatchRisk returns the highest risk score from a slice of MatchRules +func highestMatchRisk(mrs yara.MatchRules) int { + if len(mrs) == 0 { + return 0 + } + + highestRisk := 0 + for _, m := range mrs { + risk := behaviorRisk(m.Namespace, m.Rule, m.Tags) + if risk > highestRisk { + highestRisk = risk + } + } + return highestRisk +}