diff --git a/bincapz.go b/bincapz.go index e8b53e7d7..43b87af92 100644 --- a/bincapz.go +++ b/bincapz.go @@ -6,6 +6,7 @@ package main import ( "context" + "fmt" "io/fs" "log/slog" "os" @@ -174,13 +175,6 @@ func main() { } } - 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) @@ -200,6 +194,21 @@ func main() { scanPaths = args[2:] } + chosenFormat := formatFlag + if chosenFormat == "auto" { + chosenFormat = "terminal" + if slices.Contains(args, "scan") { + chosenFormat = "terminal_brief" + } + } + + renderer, err = render.New(chosenFormat, outFile) + if err != nil { + log.Error("invalid format", slog.Any("error", err), slog.String("format", formatFlag)) + returnCode = ExitInvalidArgument + return err + } + bc = bincapz.Config{ Concurrency: concurrencyFlag, ErrFirstHit: errFirstHitFlag, @@ -241,7 +250,7 @@ func main() { }, &cli.StringFlag{ Name: "format", - Value: "terminal", + Value: "auto", Usage: "Output format (json, markdown, simple, terminal, yaml)", Destination: &formatFlag, }, @@ -433,6 +442,10 @@ func main() { return err } + if res.Files.Len() > 0 { + fmt.Fprintf(os.Stderr, "\n\ntip: For detailed analysis, run: bincapz analyze \n") + } + return nil }, }, diff --git a/pkg/render/render.go b/pkg/render/render.go index 53dcff1e1..f9a92d450 100644 --- a/pkg/render/render.go +++ b/pkg/render/render.go @@ -15,6 +15,8 @@ func New(kind string, w io.Writer) (bincapz.Renderer, error) { switch kind { case "", "auto", "terminal": return NewTerminal(w), nil + case "terminal_brief": + return NewTerminalBrief(w), nil case "markdown": return NewMarkdown(w), nil case "yaml": diff --git a/pkg/render/terminal_brief.go b/pkg/render/terminal_brief.go new file mode 100644 index 000000000..72966cbee --- /dev/null +++ b/pkg/render/terminal_brief.go @@ -0,0 +1,69 @@ +// Copyright 2024 Chainguard, Inc. +// SPDX-License-Identifier: Apache-2.0 +// +// Terminal Brief renderer +// +// Example: +// +// [CRITICAL] /bin/ls: frobber (whatever), xavier (whatever) +// [HIGH ] /bin/zxa: +// [MED ] /bin/ar: + +package render + +import ( + "context" + "fmt" + "io" + "strings" + + "github.com/chainguard-dev/bincapz/pkg/bincapz" + "github.com/fatih/color" +) + +type TerminalBrief struct { + w io.Writer +} + +func NewTerminalBrief(w io.Writer) TerminalBrief { + return TerminalBrief{w: w} +} + +func briefRiskColor(level string) string { + switch level { + case "LOW": + return color.HiGreenString("LOW ") + case "MEDIUM", "MED": + return color.HiYellowString("MED ") + case "HIGH": + return color.HiRedString("HIGH") + case "CRITICAL", "CRIT": + return color.HiMagentaString("CRIT") + default: + return color.WhiteString(level) + } +} + +func (r TerminalBrief) File(_ context.Context, fr *bincapz.FileReport) error { + if len(fr.Behaviors) == 0 { + return nil + } + + reasons := []string{} + for _, b := range fr.Behaviors { + reasons = append(reasons, fmt.Sprintf("%s %s%s%s", color.HiYellowString(b.ID), color.HiBlackString("("), b.Description, color.HiBlackString(")"))) + } + + fmt.Fprintf(r.w, "%s%s%s %s: %s", color.HiBlackString("["), briefRiskColor(fr.RiskLevel), color.HiBlackString("]"), color.HiGreenString(fr.Path), + strings.Join(reasons, color.HiBlackString(", "))) + return nil +} + +func (r TerminalBrief) Full(_ context.Context, rep *bincapz.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 TerminalBrief renderer") +}