diff --git a/bincapz.go b/bincapz.go index 6921a12cd..e8b53e7d7 100644 --- a/bincapz.go +++ b/bincapz.go @@ -6,13 +6,13 @@ package main import ( "context" - "flag" - "fmt" "io/fs" "log/slog" "os" "runtime" + "slices" "strings" + "time" "github.com/chainguard-dev/bincapz/pkg/action" "github.com/chainguard-dev/bincapz/pkg/bincapz" @@ -23,6 +23,9 @@ import ( "github.com/chainguard-dev/bincapz/rules" thirdparty "github.com/chainguard-dev/bincapz/third_party" "github.com/chainguard-dev/clog" + "github.com/hillu/go-yara/v4" + + "github.com/urfave/cli/v2" ) var ( @@ -36,51 +39,44 @@ var ( ExitInvalidArgument = 22 ) -// parse risk levels. -func parseRisk(s string) int { - levels := map[string]int{ - "0": 0, - "any": 0, - "all": 0, - "1": 1, - "low": 1, - "2": 2, - "medium": 2, - "3": 3, - "high": 3, - "4": 4, - "crit": 4, - "critical": 4, - } - return levels[strings.ToLower(s)] +var ( + allFlag bool + concurrencyFlag int + errFirstHitFlag bool + errFirstMissFlag bool + formatFlag string + ignoreSelfFlag bool + ignoreTagsFlag string + includeDataFilesFlag bool + minFileLevelFlag int + minFileRiskFlag string + minLevelFlag int + minRiskFlag string + ociFlag bool + outputFlag string + profileFlag bool + quantityIncreasesRiskFlag bool + statsFlag bool + thirdPartyFlag bool + verboseFlag bool +) + +var riskMap = map[string]int{ + "0": 0, + "any": 0, + "all": 0, + "1": 1, + "low": 1, + "2": 2, + "medium": 2, + "3": 3, + "high": 3, + "4": 4, + "crit": 4, + "critical": 4, } func main() { - allFlag := flag.Bool("all", false, "Ignore nothing, show all") - concurrencyFlag := flag.Int("j", runtime.NumCPU(), "Concurrently scan files within target directories") - diffFlag := flag.Bool("diff", false, "Show capability drift between two files") - formatFlag := flag.String("format", "terminal", "Output type -- valid values are: json, markdown, simple, terminal, yaml") - ignoreSelfFlag := flag.Bool("ignore-self", true, "Ignore the bincapz binary") - ignoreTagsFlag := flag.String("ignore-tags", "", "Rule tags to ignore") - outputFlag := flag.String("o", "", "write output to this path instead of stdout") - includeDataFilesFlag := flag.Bool("data-files", false, "Include files that are detected as non-program (binary or source) files") - minFileLevelFlag := flag.Int("min-file-level", -1, "Obsoleted by --min-file-risk") - minLevelFlag := flag.Int("min-level", -1, "Obsoleted by --min-risk") - minFileRiskFlag := flag.String("min-file-risk", "low", "Only show results for files that meet this risk level (any,low,medium,high,critical") - minRiskFlag := flag.String("min-risk", "low", "Minimum risk level to show results for (any,low,medium,high,critical)") - errFirstMissFlag := flag.Bool("err-first-miss", false, "exit with error if scan source has no matching capabilities") - errFirstHitFlag := flag.Bool("err-first-hit", false, "exit with error if scan source has matching capabilities") - ociFlag := flag.Bool("oci", false, "Scan an OCI image") - quantityIncreasesRiskFlag := flag.Bool("quantity-increases-risk", true, "increase file risk score based on behavior quantity") - profileFlag := flag.Bool("profile", false, "Generate profile and trace files") - statsFlag := flag.Bool("stats", false, "Show statistics about the scan") - thirdPartyFlag := flag.Bool("third-party", true, "Include third-party rules, which may have licensing restrictions") - verboseFlag := flag.Bool("verbose", false, "Emit verbose logging messages to stderr") - versionFlag := flag.Bool("version", false, "Show version information") - - flag.Parse() - args := flag.Args() - returnCode := ExitOK defer func() { os.Exit(returnCode) }() @@ -89,135 +85,362 @@ func main() { logOpts := &slog.HandlerOptions{Level: logLevel, AddSource: true} log := clog.New(slog.NewTextHandler(os.Stderr, logOpts)) - var stop func() - if *profileFlag { - var err error - stop, err = profile.Profile() - if err != nil { - log.Error("profiling failed", slog.Any("error", err)) - returnCode = ExitProfilerError - return - } - } + // variables to share between stages + var ( + bc bincapz.Config + ctx context.Context + err error + outFile = os.Stdout + renderer bincapz.Renderer + res *bincapz.Report + stop func() + ver string + yrs *yara.Rules + ) - if len(args) == 0 && !*versionFlag { - fmt.Printf("usage: bincapz [flags] ") - returnCode = ExitInvalidArgument - return + ver, err = version.Version() + if err != nil { + returnCode = ExitActionFailed } - if *verboseFlag { - logOpts.AddSource = true - logLevel.Set(slog.LevelDebug) - } + app := &cli.App{ + Name: "bincapz", + Version: ver, + Usage: "Detect malicious program behaviors", + UsageText: "bincapz [diff, scan] ", + Compiled: time.Now(), + // Close the output file and stop profiling if appropriate + After: func(_ *cli.Context) error { + // Close our output file (or stdout) after commands have run + defer func() { + outFile.Close() + }() - if *versionFlag { - ver, err := version.Version() - if err != nil { - fmt.Printf("bincapz unknown version\n") - } - fmt.Printf("%s\n", ver) - return - } + // Stop profiling if command was executed with that flag + if profileFlag { + stop() + } + return nil + }, + // Handle shared initialization (flag parsing, rule compilation, configuration) + Before: func(c *cli.Context) error { + ctx = clog.WithLogger(c.Context, log) + clog.FromContext(ctx).Info("bincapz starting") - ctx := clog.WithLogger(context.Background(), log) - clog.FromContext(ctx).Info("bincapz starting") + if profileFlag { + var err error + stop, err = profile.Profile() + if err != nil { + log.Error("profiling failed", slog.Any("error", err)) + returnCode = ExitProfilerError + return nil + } + } - ignoreTags := strings.Split(*ignoreTagsFlag, ",") - includeDataFiles := *includeDataFilesFlag - minRisk := parseRisk(*minRiskFlag) + if verboseFlag { + logOpts.AddSource = true + logLevel.Set(slog.LevelDebug) + } - // Backwards compatibility - if *minLevelFlag != -1 { - minRisk = *minLevelFlag - } + ignoreTags := strings.Split(ignoreTagsFlag, ",") + includeDataFiles := includeDataFilesFlag - minFileRisk := parseRisk(*minFileRiskFlag) + minRisk := riskMap[minRiskFlag] + // Backwards compatibility + if minLevelFlag != -1 { + minRisk = minLevelFlag + } - // Backwards compatibility - if *minFileLevelFlag != -1 { - minFileRisk = *minFileLevelFlag - } + minFileRisk := riskMap[minFileRiskFlag] + // Backwards compatibility + if minFileLevelFlag != -1 { + minFileRisk = minFileLevelFlag + } - stats := *statsFlag - if *allFlag { - ignoreTags = []string{} - minRisk = -1 - includeDataFiles = true - *ignoreSelfFlag = false - } + if allFlag { + ignoreSelfFlag = false + ignoreTags = []string{} + includeDataFiles = true + minFileRisk = -1 + minRisk = -1 + } - outFile := os.Stdout - var err error - if *outputFlag != "" { - outFile, err = os.OpenFile(*outputFlag, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o600) - if err != nil { - log.Error("open file", slog.Any("error", err), slog.String("path", *outputFlag)) - returnCode = ExitInputOutput - return - } - } - defer func() { - outFile.Close() - }() + if outputFlag != "" { + outFile, err = os.OpenFile(outputFlag, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o600) + if err != nil { + log.Error("open file", slog.Any("error", err), slog.String("path", outputFlag)) + returnCode = ExitInputOutput + return err + } + } - renderer, err := render.New(*formatFlag, outFile) - if err != nil { - log.Error("invalid format", slog.Any("error", err), slog.String("format", *formatFlag)) - returnCode = ExitInvalidArgument - return - } + renderer, err = render.New(formatFlag, outFile) + if err != nil { + log.Error("invalid format", slog.Any("error", err), slog.String("format", formatFlag)) + returnCode = ExitInvalidArgument + return err + } - rfs := []fs.FS{rules.FS} - if *thirdPartyFlag { - rfs = append(rfs, thirdparty.FS) - } + rfs := []fs.FS{rules.FS} + if thirdPartyFlag { + rfs = append(rfs, thirdparty.FS) + } - yrs, err := compile.Recursive(ctx, rfs) - if err != nil { - log.Error("YARA rule compilation", slog.Any("error", err)) - returnCode = ExitInvalidRules - return - } + yrs, err = compile.Recursive(ctx, rfs) + if err != nil { + log.Error("YARA rule compilation", slog.Any("error", err)) + returnCode = ExitInvalidRules + return err + } - bc := bincapz.Config{ - Concurrency: *concurrencyFlag, - ErrFirstHit: *errFirstHitFlag, - ErrFirstMiss: *errFirstMissFlag, - IgnoreSelf: *ignoreSelfFlag, - IgnoreTags: ignoreTags, - IncludeDataFiles: includeDataFiles, - MinFileRisk: minFileRisk, - MinRisk: minRisk, - OCI: *ociFlag, - QuantityIncreasesRisk: *quantityIncreasesRiskFlag, - Renderer: renderer, - Rules: yrs, - ScanPaths: args, - Stats: stats, - } + // when scanning, increment the slice index by one to account for flags + args := c.Args().Slice() + scanPaths := args[1:] + if slices.Contains(args, "analyze") || slices.Contains(args, "scan") { + scanPaths = args[2:] + } - var res *bincapz.Report + bc = bincapz.Config{ + Concurrency: concurrencyFlag, + ErrFirstHit: errFirstHitFlag, + ErrFirstMiss: errFirstMissFlag, + IgnoreSelf: ignoreSelfFlag, + IgnoreTags: ignoreTags, + IncludeDataFiles: includeDataFiles, + MinFileRisk: minFileRisk, + MinRisk: minRisk, + OCI: ociFlag, + QuantityIncreasesRisk: quantityIncreasesRiskFlag, + Renderer: renderer, + Rules: yrs, + ScanPaths: scanPaths, + Stats: statsFlag, + } - if *diffFlag { - res, err = action.Diff(ctx, bc) - } else { - res, err = action.Scan(ctx, bc) - } - if err != nil { - returnCode = ExitActionFailed - fmt.Fprintf(os.Stderr, "%s\n", err) - return - } + return nil + }, + // Global flags shared between commands + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "all", + Value: false, + Usage: "Ignore nothing within a provided scan path", + Destination: &allFlag, + }, + &cli.BoolFlag{ + Name: "err-first-miss", + Value: false, + Usage: "Exit with error if scan source has no matching capabilities", + Destination: &errFirstMissFlag, + }, + &cli.BoolFlag{ + Name: "err-first-hit", + Value: false, + Usage: "Exit with error if scan source has matching capabilities", + Destination: &errFirstHitFlag, + }, + &cli.StringFlag{ + Name: "format", + Value: "terminal", + Usage: "Output format (json, markdown, simple, terminal, yaml)", + Destination: &formatFlag, + }, + &cli.BoolFlag{ + Name: "ignore-self", + Value: true, + Usage: "Ignore the bincapz binary", + Destination: &ignoreSelfFlag, + }, + &cli.StringFlag{ + Name: "ignore-tags", + Value: "", + Usage: "Rule tags to ignore", + Destination: &ignoreTagsFlag, + }, + &cli.BoolFlag{ + Name: "include-data-files", + Value: false, + Usage: "Include files that are detected as non-program (binary or source) files", + Destination: &includeDataFilesFlag, + }, + &cli.IntFlag{ + Name: "jobs", + Aliases: []string{"j"}, + Value: runtime.NumCPU(), + Usage: "Concurrently scan files within target scan paths", + Destination: &concurrencyFlag, + }, + &cli.IntFlag{ + Name: "min-file-level", + Value: -1, + Usage: "Obsoleted by --min-file-risk", + Destination: &minFileLevelFlag, + }, + &cli.StringFlag{ + Name: "min-file-risk", + Value: "low", + Usage: "Only show results for files which meet the given risk level (any, low, medium, high, critical)", + Destination: &minFileRiskFlag, + }, + &cli.IntFlag{ + Name: "min-level", + Value: -1, + Usage: "Obsoleted by --min-risk", + Destination: &minLevelFlag, + }, + &cli.StringFlag{ + Name: "min-risk", + Value: "low", + Usage: "Only show results which meet the given risk level (any, low, medium, high, critical)", + Destination: &minRiskFlag, + }, + &cli.StringFlag{ + Name: "output", + Aliases: []string{"o"}, + Value: "", + Usage: "Write output to specified file instead of stdout", + Destination: &outputFlag, + }, + &cli.BoolFlag{ + Name: "profile", + Aliases: []string{"p"}, + Value: false, + Usage: "Generate profile and trace files", + Destination: &profileFlag, + }, + &cli.BoolFlag{ + Name: "quantity-increases-risk", + Value: true, + Usage: "Increase file risk score based on behavior quantity", + Destination: &quantityIncreasesRiskFlag, + }, + &cli.BoolFlag{ + Name: "stats", + Aliases: []string{"s"}, + Value: false, + Usage: "Show scan statistics", + Destination: &statsFlag, + }, + &cli.BoolFlag{ + Name: "third-party", + Value: true, + Usage: "Include third-party rules which may have licensing restrictions", + Destination: &thirdPartyFlag, + }, + &cli.BoolFlag{ + Name: "verbose", + Value: false, + Usage: "Emit verbose logging messages to stderr", + Destination: &verboseFlag, + }, + }, + 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", + }, + }, + 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") == "": + cmdArgs := c.Args().Slice() + bc.ScanPaths = []string{cmdArgs[0]} + } - err = renderer.Full(ctx, res) - if err != nil { - returnCode = ExitRenderFailed - log.Error("render failed", slog.Any("error", err)) - return + 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", + Action: func(_ *cli.Context) error { + res, err = action.Diff(ctx, bc) + if err != nil { + log.Error("diff 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: "scan", + Usage: "tersely scan a path and return findings of the highest severity", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "image", + Aliases: []string{"i"}, + Value: "", + Usage: "Scan an image", + }, + }, + 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) + switch { + case c.String("image") != "": + bc.OCI = true + case c.String("image") == "": + 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 + }, + }, + }, } - if *profileFlag { - stop() + if err := app.Run(os.Args); err != nil { + log.Error("error running bincapz: %w", slog.Any("error", err)) + returnCode = ExitActionFailed } } diff --git a/go.mod b/go.mod index 1f4850ddb..b1377ddc6 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( github.com/liamg/magic v0.0.1 github.com/olekukonko/tablewriter v0.0.5 github.com/ulikunitz/xz v0.5.12 + github.com/urfave/cli/v2 v2.27.4 github.com/wk8/go-ordered-map/v2 v2.1.8 golang.org/x/sync v0.8.0 golang.org/x/term v0.24.0 @@ -22,6 +23,7 @@ require ( github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/buger/jsonparser v1.1.1 // indirect github.com/containerd/stargz-snapshotter/estargz v0.15.1 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect github.com/docker/cli v27.1.2+incompatible // indirect github.com/docker/distribution v2.8.3+incompatible // indirect github.com/docker/docker-credential-helpers v0.8.2 // indirect @@ -36,7 +38,9 @@ require ( github.com/opencontainers/image-spec v1.1.0 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/rivo/uniseg v0.4.7 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/vbatts/tar-split v0.11.5 // indirect + github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect golang.org/x/sys v0.25.0 // indirect ) diff --git a/go.sum b/go.sum index cde035740..3d083e5a0 100644 --- a/go.sum +++ b/go.sum @@ -8,6 +8,8 @@ github.com/chainguard-dev/clog v1.5.0 h1:VFwdxf+4x7+EG8lRO4/tZFP7Hn/NG8OVkVNfgnn github.com/chainguard-dev/clog v1.5.0/go.mod h1:4+WFhRMsGH79etYXY3plYdp+tCz/KCkU8fAr0HoaPvs= github.com/containerd/stargz-snapshotter/estargz v0.15.1 h1:eXJjw9RbkLFgioVaTG+G/ZW/0kEe2oEKCdS/ZxIyoCU= github.com/containerd/stargz-snapshotter/estargz v0.15.1/go.mod h1:gr2RNwukQ/S9Nv33Lt6UC7xEx58C+LHRdoqbEKjz1Kk= +github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -60,6 +62,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -68,10 +72,14 @@ github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcU github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc= github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/urfave/cli/v2 v2.27.4 h1:o1owoI+02Eb+K107p27wEX9Bb8eqIoZCfLXloLUSWJ8= +github.com/urfave/cli/v2 v2.27.4/go.mod h1:m4QzxcD2qpra4z7WhzEGn74WZLViBnMpb1ToCAKdGRQ= github.com/vbatts/tar-split v0.11.5 h1:3bHCTIheBm1qFTcgh9oPu+nNBtX+XJIupG/vacinCts= github.com/vbatts/tar-split v0.11.5/go.mod h1:yZbwRsSeGjusneWgA781EKej9HF8vme8okylkAeNKLk= github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/pkg/bincapz/bincapz.go b/pkg/bincapz/bincapz.go index 75b758968..7ceff931d 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 2f6a7bb9a..d7925458a 100644 --- a/pkg/report/report.go +++ b/pkg/report/report.go @@ -332,6 +332,16 @@ 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 risk + // Return an empty file report if the highest risk is medium or lower + var highestRisk int + if c.Scan { + highestRisk = highestMatchRisk(mrs) + if highestRisk < 3 { + return bincapz.FileReport{}, nil + } + } + for _, m := range mrs { if all(m.Rule == BINARY, ignoreSelf) { ignoreBincapz = true @@ -343,7 +353,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 risk + switch { + case risk < minScore && !ignoreBincapz: + continue + case c.Scan && risk < highestRisk: continue } key = generateKey(m.Namespace, m.Rule) @@ -536,3 +551,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 +} diff --git a/pkg/version/version.go b/pkg/version/version.go index 8a25e9e4a..44cb0ff06 100644 --- a/pkg/version/version.go +++ b/pkg/version/version.go @@ -35,7 +35,7 @@ func Version() (string, error) { // If present, return that value // Otherwise, fall back to the contents of the VERSION const if v != "" { - return fmt.Sprintf("bincapz %s", v), nil + return v, nil } - return fmt.Sprintf("bincapz %s", ID), nil + return ID, nil } diff --git a/test_data/refresh-testdata.sh b/test_data/refresh-testdata.sh index 21205ef39..dabbb7282 100755 --- a/test_data/refresh-testdata.sh +++ b/test_data/refresh-testdata.sh @@ -32,56 +32,56 @@ ${bincapz} --format=simple \ # diffs don't follow an easy rule ${bincapz} --format=markdown \ - --diff \ -o ../test_data/macOS/2023.3CX/libffmpeg.dirty.mdiff \ + diff \ macOS/2023.3CX/libffmpeg.dylib \ macOS/2023.3CX/libffmpeg.dirty.dylib & ${bincapz} --format=markdown \ - --diff \ -o ../test_data/macOS/clean/ls.mdiff \ + diff \ linux/clean/ls.x86_64 \ macOS/clean/ls & ${bincapz} --format=simple \ - --diff \ --min-level 2 \ --min-file-level 2 \ -o ../test_data/macOS/clean/ls.sdiff.level_2 \ + diff \ linux/clean/ls.x86_64 \ macOS/clean/ls & ${bincapz} --format=simple \ - --diff \ --min-level 1 \ --min-file-level 2 \ -o ../test_data/macOS/clean/ls.sdiff.trigger_2 \ + diff \ linux/clean/ls.x86_64 \ macOS/clean/ls & ${bincapz} --format=simple \ - --diff \ --min-level 1 \ --min-file-level 3 \ -o ../test_data/macOS/clean/ls.sdiff.trigger_3 \ + diff \ linux/clean/ls.x86_64 \ macOS/clean/ls & ${bincapz} --format=simple \ - --diff \ -o ../test_data/linux/2024.sbcl.market/sbcl.sdiff \ + diff \ linux/2024.sbcl.market/sbcl.clean \ linux/2024.sbcl.market/sbcl.dirty & ${bincapz} --format=simple \ - --diff \ -o ../test_data/linux/2023.FreeDownloadManager/freedownloadmanager.sdiff \ + diff \ linux/2023.FreeDownloadManager/freedownloadmanager_clear_postinst \ linux/2023.FreeDownloadManager/freedownloadmanager_infected_postinst & ${bincapz} --format=simple \ - --diff \ -o ../test_data/linux/clean/aws-c-io/aws-c-io.sdiff \ + diff \ linux/clean/aws-c-io/aws-c-io-0.14.10-r0.spdx.json \ linux/clean/aws-c-io/aws-c-io-0.14.11-r0.spdx.json & wait @@ -89,7 +89,7 @@ wait for f in $(find * -name "*.simple"); do prog=$(echo ${f} | sed s/\.simple$//g) if [[ -f "${prog}" ]]; then - ${bincapz} --format=simple -o "../test_data/${f}" "${prog}" & + ${bincapz} --format=simple -o "../test_data/${f}" scan "${prog}" & fi done wait @@ -97,7 +97,7 @@ wait for f in $(find * -name "*.md"); do prog=$(echo ${f} | sed s/\.md$//g) if [[ -f "${prog}" ]]; then - ${bincapz} --format=markdown -o "../test_data/${f}" "${prog}" & + ${bincapz} --format=markdown -o "../test_data/${f}" scan "${prog}" & fi done wait @@ -105,7 +105,7 @@ wait for f in $(find * -name "*.json"); do prog=$(echo ${f} | sed s/\.json$//g) if [[ -f "${prog}" ]]; then - ${bincapz} --format=json -o "../test_data/${f}" "${prog}" & + ${bincapz} --format=json -o "../test_data/${f}" scan "${prog}" & fi done wait