package sandbox import ( "errors" "fmt" "io" "time" "github.com/safedep/dry/usefulerror" "github.com/safedep/pmg/errcodes" "github.com/safedep/pmg/internal/ui" pmgsandbox "github.com/safedep/pmg/sandbox" "github.com/spf13/cobra" ) const ExitCodeViolationsListFail = 2 type violationsListOptions struct { limit int jsonOut bool } type violationsListFailError struct { usefulerror.UsefulError } func (e *violationsListFailError) ExitCode() int { return ExitCodeViolationsListFail } func newViolationsListFailError(code, msg, help string) *violationsListFailError { return &violationsListFailError{ UsefulError: usefulerror.NewUsefulError(). WithCode(code). WithHumanError(msg). WithHelp(help). Wrap(errors.New(msg)), } } func newViolationsListCommand(factory cacheFactory) *cobra.Command { opts := &violationsListOptions{} cmd := &cobra.Command{ Use: "list", Short: "List cached sandbox violations as one-line summaries", Example: " pmg sandbox violations list --limit 20", SilenceErrors: false, Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { err := runViolationsList(cmd.OutOrStdout(), cmd.ErrOrStderr(), opts, factory) if err != nil { return sandboxErrorExit(cmd, err) } return err }, } cmd.Flags().IntVar(&opts.limit, "limit", 10, "Maximum number of entries to show (0 means all)") cmd.Flags().BoolVar(&opts.jsonOut, "json", false, "Emit entries as JSON") return cmd } func runViolationsList(out, errOut io.Writer, opts *violationsListOptions, factory cacheFactory) error { if opts.limit < 0 { return newViolationsListFailError( errcodes.InvalidArgument, fmt.Sprintf("invalid --limit %d (must be >= 0)", opts.limit), "Pass --limit 0 to show all entries, or a positive limit.", ) } cache := factory() entries, err := cache.List() if err != nil { return newViolationsListFailError( errcodes.Unknown, fmt.Sprintf("read cache: %v", err), "Check the sandbox violation cache directory and retry.", ) } if opts.limit > 0 && len(entries) > opts.limit { entries = entries[:opts.limit] } if opts.jsonOut { return writeViolationsListJSON(out, entries) } if len(entries) == 0 { _, err := fmt.Fprintln(errOut, "no violations cached") return err } return renderViolationsListTable(out, entries) } func renderViolationsListTable(out io.Writer, entries []pmgsandbox.ViolationCacheEntry) error { rows := make([][]string, 0, len(entries)+1) rows = append(rows, []string{ ui.Colors.Bold("RECORDED"), ui.Colors.Bold("SANDBOX"), ui.Colors.Bold("PROFILE"), ui.Colors.Bold("KIND"), ui.Colors.Bold("TARGET"), }) dash := ui.Colors.Dim("—") for _, e := range entries { recorded := "" if !e.Record.RecordedAt.IsZero() { recorded = e.Record.RecordedAt.UTC().Format(time.RFC3339) } sandboxName := "" profile := "" if e.Record.Report != nil { sandboxName = string(e.Record.Report.SandboxName) profile = e.Record.Report.PolicyName } kind := dash target := dash if e.Record.Report != nil { primary := pmgsandbox.BuildExplanation(e.Record.Report).Primary if primary != nil { if primary.Kind != "" { kind = string(primary.Kind) } if primary.Target != "" { target = truncate(primary.Target, 60) } } } rows = append(rows, []string{recorded, sandboxName, profile, kind, target}) } return renderTable(out, rows, nil) } type violationsListJSONPrimary struct { Kind string `json:"kind"` Target string `json:"target,omitempty"` RuleLabel string `json:"rule_label,omitempty"` } type violationsListJSONEntry struct { Path string `json:"path"` RecordedAt string `json:"recorded_at,omitempty"` SandboxName string `json:"sandbox_name,omitempty"` PolicyName string `json:"policy_name,omitempty"` Primary *violationsListJSONPrimary `json:"primary,omitempty"` ViolationCount int `json:"violation_count"` } type violationsListJSONOutput struct { Entries []violationsListJSONEntry `json:"entries"` } func writeViolationsListJSON(out io.Writer, entries []pmgsandbox.ViolationCacheEntry) error { payload := violationsListJSONOutput{Entries: make([]violationsListJSONEntry, 0, len(entries))} for _, e := range entries { item := violationsListJSONEntry{Path: e.Path} if !e.Record.RecordedAt.IsZero() { item.RecordedAt = e.Record.RecordedAt.UTC().Format(time.RFC3339) } if e.Record.Report != nil { item.SandboxName = string(e.Record.Report.SandboxName) item.PolicyName = e.Record.Report.PolicyName item.ViolationCount = len(e.Record.Report.Violations) primary := pmgsandbox.BuildExplanation(e.Record.Report).Primary if primary != nil { item.Primary = &violationsListJSONPrimary{ Kind: string(primary.Kind), Target: primary.Target, RuleLabel: primary.RuleLabel, } } } payload.Entries = append(payload.Entries, item) } return writeJSONIndent(out, payload) }