diff --git a/cmd/crowdsec-cli/alerts.go b/cmd/crowdsec-cli/alerts.go index 85e1613fa..545a52cf7 100644 --- a/cmd/crowdsec-cli/alerts.go +++ b/cmd/crowdsec-cli/alerts.go @@ -7,21 +7,20 @@ import ( "fmt" "net/url" "os" - "sort" "strconv" "strings" - "time" + + "github.com/go-openapi/strfmt" + colorable "github.com/mattn/go-colorable" + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "gopkg.in/yaml.v2" "github.com/crowdsecurity/crowdsec/pkg/apiclient" "github.com/crowdsecurity/crowdsec/pkg/cwversion" "github.com/crowdsecurity/crowdsec/pkg/database" "github.com/crowdsecurity/crowdsec/pkg/models" - "github.com/go-openapi/strfmt" - "github.com/olekukonko/tablewriter" - "github.com/pkg/errors" - log "github.com/sirupsen/logrus" - "github.com/spf13/cobra" - "gopkg.in/yaml.v2" ) var printMachine bool @@ -83,39 +82,11 @@ func AlertsToTable(alerts *models.GetAlertsResponse, printMachine bool) error { x, _ := json.MarshalIndent(alerts, "", " ") fmt.Printf("%s", string(x)) } else if csConfig.Cscli.Output == "human" { - - table := tablewriter.NewWriter(os.Stdout) - header := []string{"ID", "value", "reason", "country", "as", "decisions", "created_at"} - if printMachine { - header = append(header, "machine") - } - table.SetHeader(header) - if len(*alerts) == 0 { fmt.Println("No active alerts") return nil } - for _, alertItem := range *alerts { - - displayVal := *alertItem.Source.Scope - if *alertItem.Source.Value != "" { - displayVal += ":" + *alertItem.Source.Value - } - row := []string{ - strconv.Itoa(int(alertItem.ID)), - displayVal, - *alertItem.Scenario, - alertItem.Source.Cn, - alertItem.Source.AsNumber + " " + alertItem.Source.AsName, - DecisionsFromAlert(alertItem), - *alertItem.StartAt, - } - if printMachine { - row = append(row, alertItem.MachineID) - } - table.Append(row) - } - table.Render() // Send output + alertsTable(colorable.NewColorableStdout(), alerts, printMachine) } return nil } @@ -138,53 +109,13 @@ func DisplayOneAlert(alert *models.Alert, withDetail bool) error { fmt.Printf(" - AS : %s\n", alert.Source.AsName) fmt.Printf(" - Begin : %s\n", *alert.StartAt) fmt.Printf(" - End : %s\n\n", *alert.StopAt) - foundActive := false - table := tablewriter.NewWriter(os.Stdout) - table.SetHeader([]string{"ID", "scope:value", "action", "expiration", "created_at"}) - for _, decision := range alert.Decisions { - parsedDuration, err := time.ParseDuration(*decision.Duration) - if err != nil { - log.Errorf(err.Error()) - } - expire := time.Now().UTC().Add(parsedDuration) - if time.Now().UTC().After(expire) { - continue - } - foundActive = true - scopeAndValue := *decision.Scope - if *decision.Value != "" { - scopeAndValue += ":" + *decision.Value - } - table.Append([]string{ - strconv.Itoa(int(decision.ID)), - scopeAndValue, - *decision.Type, - *decision.Duration, - alert.CreatedAt, - }) - } - if foundActive { - fmt.Printf(" - Active Decisions :\n") - table.Render() // Send output - } + + alertDecisionsTable(colorable.NewColorableStdout(), alert) if withDetail { fmt.Printf("\n - Events :\n") for _, event := range alert.Events { - fmt.Printf("\n- Date: %s\n", *event.Timestamp) - table = tablewriter.NewWriter(os.Stdout) - table.SetHeader([]string{"Key", "Value"}) - sort.Slice(event.Meta, func(i, j int) bool { - return event.Meta[i].Key < event.Meta[j].Key - }) - for _, meta := range event.Meta { - table.Append([]string{ - meta.Key, - meta.Value, - }) - } - - table.Render() // Send output + alertEventTable(colorable.NewColorableStdout(), event) } } } diff --git a/cmd/crowdsec-cli/alerts_table.go b/cmd/crowdsec-cli/alerts_table.go new file mode 100644 index 000000000..2c9d12d3d --- /dev/null +++ b/cmd/crowdsec-cli/alerts_table.go @@ -0,0 +1,100 @@ +package main + +import ( + "fmt" + "io" + "sort" + "strconv" + "time" + + log "github.com/sirupsen/logrus" + + "github.com/crowdsecurity/crowdsec/pkg/models" +) + +func alertsTable(out io.Writer, alerts *models.GetAlertsResponse, printMachine bool) { + t := newTable(out) + t.SetRowLines(false) + header := []string{"ID", "value", "reason", "country", "as", "decisions", "created_at"} + if printMachine { + header = append(header, "machine") + } + t.SetHeaders(header...) + + for _, alertItem := range *alerts { + displayVal := *alertItem.Source.Scope + if *alertItem.Source.Value != "" { + displayVal += ":" + *alertItem.Source.Value + } + + row := []string{ + strconv.Itoa(int(alertItem.ID)), + displayVal, + *alertItem.Scenario, + alertItem.Source.Cn, + alertItem.Source.AsNumber + " " + alertItem.Source.AsName, + DecisionsFromAlert(alertItem), + *alertItem.StartAt, + } + + if printMachine { + row = append(row, alertItem.MachineID) + } + + t.AddRow(row...) + } + + t.Render() +} + +func alertDecisionsTable(out io.Writer, alert *models.Alert) { + foundActive := false + t := newTable(out) + t.SetRowLines(false) + t.SetHeaders("ID", "scope:value", "action", "expiration", "created_at") + for _, decision := range alert.Decisions { + parsedDuration, err := time.ParseDuration(*decision.Duration) + if err != nil { + log.Errorf(err.Error()) + } + expire := time.Now().UTC().Add(parsedDuration) + if time.Now().UTC().After(expire) { + continue + } + foundActive = true + scopeAndValue := *decision.Scope + if *decision.Value != "" { + scopeAndValue += ":" + *decision.Value + } + t.AddRow( + strconv.Itoa(int(decision.ID)), + scopeAndValue, + *decision.Type, + *decision.Duration, + alert.CreatedAt, + ) + } + if foundActive { + fmt.Printf(" - Active Decisions :\n") + t.Render() // Send output + } +} + +func alertEventTable(out io.Writer, event *models.Event) { + fmt.Fprintf(out, "\n- Date: %s\n", *event.Timestamp) + + t := newTable(out) + t.SetHeaders("Key", "Value") + sort.Slice(event.Meta, func(i, j int) bool { + return event.Meta[i].Key < event.Meta[j].Key + }) + + for _, meta := range event.Meta { + t.AddRow( + meta.Key, + meta.Value, + ) + } + + t.Render() // Send output +} diff --git a/cmd/crowdsec-cli/bouncers.go b/cmd/crowdsec-cli/bouncers.go index afe5d2f9b..eb9a2c919 100644 --- a/cmd/crowdsec-cli/bouncers.go +++ b/cmd/crowdsec-cli/bouncers.go @@ -1,62 +1,45 @@ package main import ( - "bytes" "encoding/csv" "encoding/json" "fmt" + "io" "time" + colorable "github.com/mattn/go-colorable" + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + middlewares "github.com/crowdsecurity/crowdsec/pkg/apiserver/middlewares/v1" "github.com/crowdsecurity/crowdsec/pkg/database" "github.com/crowdsecurity/crowdsec/pkg/types" - "github.com/enescakir/emoji" - "github.com/olekukonko/tablewriter" - "github.com/pkg/errors" - log "github.com/sirupsen/logrus" - "github.com/spf13/cobra" ) var keyIP string var keyLength int var key string -func getBouncers(dbClient *database.Client) ([]byte, error) { +func getBouncers(out io.Writer, dbClient *database.Client) error { bouncers, err := dbClient.ListBouncers() - w := bytes.NewBuffer(nil) if err != nil { - return nil, fmt.Errorf("unable to list bouncers: %s", err) + return fmt.Errorf("unable to list bouncers: %s", err) } if csConfig.Cscli.Output == "human" { - - table := tablewriter.NewWriter(w) - table.SetCenterSeparator("") - table.SetColumnSeparator("") - - table.SetHeaderAlignment(tablewriter.ALIGN_LEFT) - table.SetAlignment(tablewriter.ALIGN_LEFT) - table.SetHeader([]string{"Name", "IP Address", "Valid", "Last API pull", "Type", "Version", "Auth Type"}) - for _, b := range bouncers { - var revoked string - if !b.Revoked { - revoked = emoji.CheckMark.String() - } else { - revoked = emoji.Prohibited.String() - } - table.Append([]string{b.Name, b.IPAddress, revoked, b.LastPull.Format(time.RFC3339), b.Type, b.Version, b.AuthType}) - } - table.Render() + getBouncersTable(out, bouncers) } else if csConfig.Cscli.Output == "json" { - x, err := json.MarshalIndent(bouncers, "", " ") - if err != nil { - return nil, errors.Wrap(err, "failed to unmarshal") + enc := json.NewEncoder(out) + enc.SetIndent("", " ") + if err := enc.Encode(bouncers); err != nil { + return errors.Wrap(err, "failed to unmarshal") } - return x, nil + return nil } else if csConfig.Cscli.Output == "raw" { - csvwriter := csv.NewWriter(w) + csvwriter := csv.NewWriter(out) err := csvwriter.Write([]string{"name", "ip", "revoked", "last_pull", "type", "version", "auth_type"}) if err != nil { - return nil, errors.Wrap(err, "failed to write raw header") + return errors.Wrap(err, "failed to write raw header") } for _, b := range bouncers { var revoked string @@ -67,12 +50,12 @@ func getBouncers(dbClient *database.Client) ([]byte, error) { } err := csvwriter.Write([]string{b.Name, b.IPAddress, revoked, b.LastPull.Format(time.RFC3339), b.Type, b.Version, b.AuthType}) if err != nil { - return nil, errors.Wrap(err, "failed to write raw") + return errors.Wrap(err, "failed to write raw") } } csvwriter.Flush() } - return w.Bytes(), nil + return nil } func NewBouncersCmd() *cobra.Command { @@ -109,11 +92,10 @@ Note: This command requires database direct access, so is intended to be run on Args: cobra.ExactArgs(0), DisableAutoGenTag: true, Run: func(cmd *cobra.Command, arg []string) { - bouncers, err := getBouncers(dbClient) + err := getBouncers(colorable.NewColorableStdout(), dbClient) if err != nil { log.Fatalf("unable to list bouncers: %s", err) } - fmt.Printf("%s", bouncers) }, } cmdBouncers.AddCommand(cmdBouncersList) diff --git a/cmd/crowdsec-cli/bouncers_table.go b/cmd/crowdsec-cli/bouncers_table.go new file mode 100644 index 000000000..0ea725f55 --- /dev/null +++ b/cmd/crowdsec-cli/bouncers_table.go @@ -0,0 +1,31 @@ +package main + +import ( + "io" + "time" + + "github.com/aquasecurity/table" + "github.com/enescakir/emoji" + + "github.com/crowdsecurity/crowdsec/pkg/database/ent" +) + +func getBouncersTable(out io.Writer, bouncers []*ent.Bouncer) { + t := newLightTable(out) + t.SetHeaders("Name", "IP Address", "Valid", "Last API pull", "Type", "Version", "Auth Type") + t.SetHeaderAlignment(table.AlignLeft, table.AlignLeft, table.AlignLeft, table.AlignLeft, table.AlignLeft, table.AlignLeft) + t.SetAlignment(table.AlignLeft, table.AlignLeft, table.AlignLeft, table.AlignLeft, table.AlignLeft, table.AlignLeft) + + for _, b := range bouncers { + var revoked string + if !b.Revoked { + revoked = emoji.CheckMark.String() + } else { + revoked = emoji.Prohibited.String() + } + + t.AddRow(b.Name, b.IPAddress, revoked, b.LastPull.Format(time.RFC3339), b.Type, b.Version, b.AuthType) + } + + t.Render() +} diff --git a/cmd/crowdsec-cli/collections.go b/cmd/crowdsec-cli/collections.go index 33c845004..f5edcabf9 100644 --- a/cmd/crowdsec-cli/collections.go +++ b/cmd/crowdsec-cli/collections.go @@ -3,11 +3,11 @@ package main import ( "fmt" - "github.com/crowdsecurity/crowdsec/pkg/cwhub" - + colorable "github.com/mattn/go-colorable" log "github.com/sirupsen/logrus" - "github.com/spf13/cobra" + + "github.com/crowdsecurity/crowdsec/pkg/cwhub" ) func NewCollectionsCmd() *cobra.Command { @@ -173,8 +173,7 @@ func NewCollectionsCmd() *cobra.Command { Args: cobra.ExactArgs(0), DisableAutoGenTag: true, Run: func(cmd *cobra.Command, args []string) { - items := ListItems([]string{cwhub.COLLECTIONS}, args, false, true, all) - fmt.Printf("%s\n", string(items)) + ListItems(colorable.NewColorableStdout(), []string{cwhub.COLLECTIONS}, args, false, true, all) }, } cmdCollectionsList.PersistentFlags().BoolVarP(&all, "all", "a", false, "List disabled items as well") diff --git a/cmd/crowdsec-cli/console.go b/cmd/crowdsec-cli/console.go index c92a06c01..2b7aa9f16 100644 --- a/cmd/crowdsec-cli/console.go +++ b/cmd/crowdsec-cli/console.go @@ -10,16 +10,16 @@ import ( "net/url" "os" + "github.com/go-openapi/strfmt" + colorable "github.com/mattn/go-colorable" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "github.com/crowdsecurity/crowdsec/pkg/apiclient" "github.com/crowdsecurity/crowdsec/pkg/csconfig" "github.com/crowdsecurity/crowdsec/pkg/cwhub" "github.com/crowdsecurity/crowdsec/pkg/cwversion" "github.com/crowdsecurity/crowdsec/pkg/types" - "github.com/enescakir/emoji" - "github.com/go-openapi/strfmt" - "github.com/olekukonko/tablewriter" - log "github.com/sirupsen/logrus" - "github.com/spf13/cobra" ) func NewConsoleCmd() *cobra.Command { @@ -194,34 +194,7 @@ Disable given information push to the central API.`, Run: func(cmd *cobra.Command, args []string) { switch csConfig.Cscli.Output { case "human": - table := tablewriter.NewWriter(os.Stdout) - - table.SetHeaderAlignment(tablewriter.ALIGN_LEFT) - table.SetAlignment(tablewriter.ALIGN_LEFT) - table.SetHeader([]string{"Option Name", "Activated", "Description"}) - for _, option := range csconfig.CONSOLE_CONFIGS { - switch option { - case csconfig.SEND_CUSTOM_SCENARIOS: - activated := string(emoji.CrossMark) - if *csConfig.API.Server.ConsoleConfig.ShareCustomScenarios { - activated = string(emoji.CheckMarkButton) - } - table.Append([]string{option, activated, "Send alerts from custom scenarios to the console"}) - case csconfig.SEND_MANUAL_SCENARIOS: - activated := string(emoji.CrossMark) - if *csConfig.API.Server.ConsoleConfig.ShareManualDecisions { - activated = string(emoji.CheckMarkButton) - } - table.Append([]string{option, activated, "Send manual decisions to the console"}) - case csconfig.SEND_TAINTED_SCENARIOS: - activated := string(emoji.CrossMark) - if *csConfig.API.Server.ConsoleConfig.ShareTaintedScenarios { - activated = string(emoji.CheckMarkButton) - } - table.Append([]string{option, activated, "Send alerts from tainted scenarios to the console"}) - } - } - table.Render() + cmdConsoleStatusTable(colorable.NewColorableStdout(), *csConfig) case "json": data, err := json.MarshalIndent(csConfig.API.Server.ConsoleConfig, "", " ") if err != nil { diff --git a/cmd/crowdsec-cli/console_table.go b/cmd/crowdsec-cli/console_table.go new file mode 100644 index 000000000..014ffc9ad --- /dev/null +++ b/cmd/crowdsec-cli/console_table.go @@ -0,0 +1,48 @@ +package main + +import ( + "io" + + "github.com/aquasecurity/table" + "github.com/enescakir/emoji" + + "github.com/crowdsecurity/crowdsec/pkg/csconfig" +) + +func cmdConsoleStatusTable(out io.Writer, csConfig csconfig.Config) { + t := newTable(out) + t.SetRowLines(false) + + t.SetHeaders("Option Name", "Activated", "Description") + t.SetHeaderAlignment(table.AlignLeft, table.AlignLeft, table.AlignLeft) + + for _, option := range csconfig.CONSOLE_CONFIGS { + switch option { + case csconfig.SEND_CUSTOM_SCENARIOS: + activated := string(emoji.CrossMark) + if *csConfig.API.Server.ConsoleConfig.ShareCustomScenarios { + activated = string(emoji.CheckMarkButton) + } + + t.AddRow(option, activated, "Send alerts from custom scenarios to the console") + + case csconfig.SEND_MANUAL_SCENARIOS: + activated := string(emoji.CrossMark) + if *csConfig.API.Server.ConsoleConfig.ShareManualDecisions { + activated = string(emoji.CheckMarkButton) + } + + t.AddRow(option, activated, "Send manual decisions to the console") + + case csconfig.SEND_TAINTED_SCENARIOS: + activated := string(emoji.CrossMark) + if *csConfig.API.Server.ConsoleConfig.ShareTaintedScenarios { + activated = string(emoji.CheckMarkButton) + } + + t.AddRow(option, activated, "Send alerts from tainted scenarios to the console") + } + } + + t.Render() +} diff --git a/cmd/crowdsec-cli/decisions.go b/cmd/crowdsec-cli/decisions.go index 5c24dc26c..7703e37a4 100644 --- a/cmd/crowdsec-cli/decisions.go +++ b/cmd/crowdsec-cli/decisions.go @@ -12,16 +12,17 @@ import ( "strings" "time" + "github.com/go-openapi/strfmt" + "github.com/jszwec/csvutil" + colorable "github.com/mattn/go-colorable" + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "github.com/crowdsecurity/crowdsec/pkg/apiclient" "github.com/crowdsecurity/crowdsec/pkg/cwversion" "github.com/crowdsecurity/crowdsec/pkg/models" "github.com/crowdsecurity/crowdsec/pkg/types" - "github.com/go-openapi/strfmt" - "github.com/jszwec/csvutil" - "github.com/olekukonko/tablewriter" - "github.com/pkg/errors" - log "github.com/sirupsen/logrus" - "github.com/spf13/cobra" ) var Client *apiclient.ApiClient @@ -92,44 +93,11 @@ func DecisionsToTable(alerts *models.GetAlertsResponse, printMachine bool) error x, _ := json.MarshalIndent(alerts, "", " ") fmt.Printf("%s", string(x)) } else if csConfig.Cscli.Output == "human" { - table := tablewriter.NewWriter(os.Stdout) - header := []string{"ID", "Source", "Scope:Value", "Reason", "Action", "Country", "AS", "Events", "expiration", "Alert ID"} - if printMachine { - header = append(header, "Machine") - } - table.SetHeader(header) - if len(*alerts) == 0 { fmt.Println("No active decisions") return nil } - - for _, alertItem := range *alerts { - for _, decisionItem := range alertItem.Decisions { - if *alertItem.Simulated { - *decisionItem.Type = fmt.Sprintf("(simul)%s", *decisionItem.Type) - } - raw := []string{ - strconv.Itoa(int(decisionItem.ID)), - *decisionItem.Origin, - *decisionItem.Scope + ":" + *decisionItem.Value, - *decisionItem.Scenario, - *decisionItem.Type, - alertItem.Source.Cn, - alertItem.Source.AsNumber + " " + alertItem.Source.AsName, - strconv.Itoa(int(*alertItem.EventsCount)), - *decisionItem.Duration, - strconv.Itoa(int(alertItem.ID)), - } - - if printMachine { - raw = append(raw, alertItem.MachineID) - } - - table.Append(raw) - } - } - table.Render() // Send output + decisionsTable(colorable.NewColorableStdout(), alerts, printMachine) if skipped > 0 { fmt.Printf("%d duplicated entries skipped\n", skipped) } diff --git a/cmd/crowdsec-cli/decisions_table.go b/cmd/crowdsec-cli/decisions_table.go new file mode 100644 index 000000000..00087e63b --- /dev/null +++ b/cmd/crowdsec-cli/decisions_table.go @@ -0,0 +1,46 @@ +package main + +import ( + "fmt" + "io" + "strconv" + + "github.com/crowdsecurity/crowdsec/pkg/models" +) + +func decisionsTable(out io.Writer, alerts *models.GetAlertsResponse, printMachine bool) { + t := newTable(out) + t.SetRowLines(false) + header := []string{"ID", "Source", "Scope:Value", "Reason", "Action", "Country", "AS", "Events", "expiration", "Alert ID"} + if printMachine { + header = append(header, "Machine") + } + t.SetHeaders(header...) + + for _, alertItem := range *alerts { + for _, decisionItem := range alertItem.Decisions { + if *alertItem.Simulated { + *decisionItem.Type = fmt.Sprintf("(simul)%s", *decisionItem.Type) + } + row := []string{ + strconv.Itoa(int(decisionItem.ID)), + *decisionItem.Origin, + *decisionItem.Scope + ":" + *decisionItem.Value, + *decisionItem.Scenario, + *decisionItem.Type, + alertItem.Source.Cn, + alertItem.Source.AsNumber + " " + alertItem.Source.AsName, + strconv.Itoa(int(*alertItem.EventsCount)), + *decisionItem.Duration, + strconv.Itoa(int(alertItem.ID)), + } + + if printMachine { + row = append(row, alertItem.MachineID) + } + + t.AddRow(row...) + } + } + t.Render() +} diff --git a/cmd/crowdsec-cli/hub.go b/cmd/crowdsec-cli/hub.go index e6419bccf..d2404111a 100644 --- a/cmd/crowdsec-cli/hub.go +++ b/cmd/crowdsec-cli/hub.go @@ -3,10 +3,11 @@ package main import ( "fmt" - "github.com/crowdsecurity/crowdsec/pkg/cwhub" - + colorable "github.com/mattn/go-colorable" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" + + "github.com/crowdsecurity/crowdsec/pkg/cwhub" ) func NewHubCmd() *cobra.Command { @@ -56,10 +57,9 @@ cscli hub update # Download list of available configurations from the hub log.Info(v) } cwhub.DisplaySummary() - items := ListItems([]string{ + ListItems(colorable.NewColorableStdout(), []string{ cwhub.COLLECTIONS, cwhub.PARSERS, cwhub.SCENARIOS, cwhub.PARSERS_OVFLW, }, args, true, false, all) - fmt.Printf("%s\n", items) }, } cmdHubList.PersistentFlags().BoolVarP(&all, "all", "a", false, "List disabled items as well") diff --git a/cmd/crowdsec-cli/hubtest.go b/cmd/crowdsec-cli/hubtest.go index f4824984f..a17f25991 100644 --- a/cmd/crowdsec-cli/hubtest.go +++ b/cmd/crowdsec-cli/hubtest.go @@ -9,12 +9,13 @@ import ( "strings" "github.com/AlecAivazis/survey/v2" - "github.com/crowdsecurity/crowdsec/pkg/cstest" "github.com/enescakir/emoji" - "github.com/olekukonko/tablewriter" + colorable "github.com/mattn/go-colorable" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "gopkg.in/yaml.v2" + + "github.com/crowdsecurity/crowdsec/pkg/cstest" ) var ( @@ -272,22 +273,7 @@ cscli hubtest create my-scenario-test --parsers crowdsecurity/nginx --scenarios } } if csConfig.Cscli.Output == "human" { - table := tablewriter.NewWriter(os.Stdout) - table.SetCenterSeparator("") - table.SetColumnSeparator("") - - table.SetHeaderAlignment(tablewriter.ALIGN_LEFT) - table.SetAlignment(tablewriter.ALIGN_LEFT) - - table.SetHeader([]string{"Test", "Result"}) - for testName, success := range testResult { - status := emoji.CheckMarkButton.String() - if !success { - status = emoji.CrossMark.String() - } - table.Append([]string{testName, status}) - } - table.Render() + hubTestResultTable(colorable.NewColorableStdout(), testResult) } else if csConfig.Cscli.Output == "json" { jsonResult := make(map[string][]string, 0) jsonResult["success"] = make([]string, 0) @@ -367,18 +353,18 @@ cscli hubtest create my-scenario-test --parsers crowdsecurity/nginx --scenarios log.Fatalf("unable to load all tests: %+v", err) } - table := tablewriter.NewWriter(os.Stdout) - table.SetCenterSeparator("") - table.SetColumnSeparator("") - - table.SetHeaderAlignment(tablewriter.ALIGN_LEFT) - table.SetAlignment(tablewriter.ALIGN_LEFT) - table.SetHeader([]string{"Name", "Path"}) - for _, test := range HubTest.Tests { - table.Append([]string{test.Name, test.Path}) + switch csConfig.Cscli.Output { + case "human": + hubTestListTable(colorable.NewColorableStdout(), HubTest.Tests) + case "json": + j, err := json.MarshalIndent(HubTest.Tests, " ", " ") + if err != nil { + log.Fatal(err) + } + fmt.Println(string(j)) + default: + log.Fatalf("only human/json output modes are supported") } - table.Render() - }, } cmdHubTest.AddCommand(cmdHubTestList) @@ -399,11 +385,9 @@ cscli hubtest create my-scenario-test --parsers crowdsecurity/nginx --scenarios parserCoverage := []cstest.ParserCoverage{} scenarioCoveragePercent := 0 parserCoveragePercent := 0 - showAll := false - if !showScenarioCov && !showParserCov { // if both are false (flag by default), show both - showAll = true - } + // if both are false (flag by default), show both + showAll := !showScenarioCov && !showParserCov if showParserCov || showAll { parserCoverage, err = HubTest.GetParsersCoverage() @@ -446,43 +430,11 @@ cscli hubtest create my-scenario-test --parsers crowdsecurity/nginx --scenarios if csConfig.Cscli.Output == "human" { if showParserCov || showAll { - table := tablewriter.NewWriter(os.Stdout) - table.SetCenterSeparator("") - table.SetColumnSeparator("") - - table.SetHeaderAlignment(tablewriter.ALIGN_LEFT) - table.SetAlignment(tablewriter.ALIGN_LEFT) - - table.SetHeader([]string{"Parser", "Status", "Number of tests"}) - parserTested := 0 - for _, test := range parserCoverage { - status := emoji.RedCircle.String() - if test.TestsCount > 0 { - status = emoji.GreenCircle.String() - parserTested += 1 - } - table.Append([]string{test.Parser, status, fmt.Sprintf("%d times (across %d tests)", test.TestsCount, len(test.PresentIn))}) - } - table.Render() + hubTestParserCoverageTable(colorable.NewColorableStdout(), parserCoverage) } if showScenarioCov || showAll { - table := tablewriter.NewWriter(os.Stdout) - table.SetCenterSeparator("") - table.SetColumnSeparator("") - - table.SetHeaderAlignment(tablewriter.ALIGN_LEFT) - table.SetAlignment(tablewriter.ALIGN_LEFT) - - table.SetHeader([]string{"Scenario", "Status", "Number of tests"}) - for _, test := range scenarioCoverage { - status := emoji.RedCircle.String() - if test.TestsCount > 0 { - status = emoji.GreenCircle.String() - } - table.Append([]string{test.Scenario, status, fmt.Sprintf("%d times (across %d tests)", test.TestsCount, len(test.PresentIn))}) - } - table.Render() + hubTestScenarioCoverageTable(colorable.NewColorableStdout(), scenarioCoverage) } fmt.Println() if showParserCov || showAll { diff --git a/cmd/crowdsec-cli/hubtest_table.go b/cmd/crowdsec-cli/hubtest_table.go new file mode 100644 index 000000000..219c2b82a --- /dev/null +++ b/cmd/crowdsec-cli/hubtest_table.go @@ -0,0 +1,80 @@ +package main + +import ( + "fmt" + "io" + + "github.com/aquasecurity/table" + "github.com/enescakir/emoji" + + "github.com/crowdsecurity/crowdsec/pkg/cstest" +) + +func hubTestResultTable(out io.Writer, testResult map[string]bool) { + t := newLightTable(out) + t.SetHeaders("Test", "Result") + t.SetHeaderAlignment(table.AlignLeft) + t.SetAlignment(table.AlignLeft) + + for testName, success := range testResult { + status := emoji.CheckMarkButton.String() + if !success { + status = emoji.CrossMark.String() + } + + t.AddRow(testName, status) + } + + t.Render() +} + +func hubTestListTable(out io.Writer, tests []*cstest.HubTestItem) { + t := newLightTable(out) + t.SetHeaders("Name", "Path") + t.SetHeaderAlignment(table.AlignLeft, table.AlignLeft) + t.SetAlignment(table.AlignLeft, table.AlignLeft) + + for _, test := range tests { + t.AddRow(test.Name, test.Path) + } + + t.Render() +} + +func hubTestParserCoverageTable(out io.Writer, coverage []cstest.ParserCoverage) { + t := newLightTable(out) + t.SetHeaders("Parser", "Status", "Number of tests") + t.SetHeaderAlignment(table.AlignLeft, table.AlignLeft, table.AlignLeft) + t.SetAlignment(table.AlignLeft, table.AlignLeft, table.AlignLeft) + + parserTested := 0 + for _, test := range coverage { + status := emoji.RedCircle.String() + if test.TestsCount > 0 { + status = emoji.GreenCircle.String() + parserTested++ + } + t.AddRow(test.Parser, status, fmt.Sprintf("%d times (across %d tests)", test.TestsCount, len(test.PresentIn))) + } + + t.Render() +} + +func hubTestScenarioCoverageTable(out io.Writer, coverage []cstest.ScenarioCoverage) { + t := newLightTable(out) + t.SetHeaders("Scenario", "Status", "Number of tests") + t.SetHeaderAlignment(table.AlignLeft, table.AlignLeft, table.AlignLeft) + t.SetAlignment(table.AlignLeft, table.AlignLeft, table.AlignLeft) + + parserTested := 0 + for _, test := range coverage { + status := emoji.RedCircle.String() + if test.TestsCount > 0 { + status = emoji.GreenCircle.String() + parserTested++ + } + t.AddRow(test.Scenario, status, fmt.Sprintf("%d times (across %d tests)", test.TestsCount, len(test.PresentIn))) + } + + t.Render() +} diff --git a/cmd/crowdsec-cli/machines.go b/cmd/crowdsec-cli/machines.go index a102da35c..5b8f9d59b 100644 --- a/cmd/crowdsec-cli/machines.go +++ b/cmd/crowdsec-cli/machines.go @@ -1,30 +1,32 @@ package main import ( - "bytes" saferand "crypto/rand" "encoding/csv" "encoding/json" "fmt" + "io" "math/big" "os" "strings" "time" "github.com/AlecAivazis/survey/v2" - "github.com/crowdsecurity/crowdsec/pkg/csconfig" - "github.com/crowdsecurity/crowdsec/pkg/database" - "github.com/crowdsecurity/crowdsec/pkg/database/ent" - "github.com/crowdsecurity/crowdsec/pkg/types" - "github.com/crowdsecurity/machineid" "github.com/enescakir/emoji" "github.com/go-openapi/strfmt" "github.com/google/uuid" - "github.com/olekukonko/tablewriter" + colorable "github.com/mattn/go-colorable" "github.com/pkg/errors" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "gopkg.in/yaml.v2" + + "github.com/crowdsecurity/machineid" + + "github.com/crowdsecurity/crowdsec/pkg/csconfig" + "github.com/crowdsecurity/crowdsec/pkg/database" + "github.com/crowdsecurity/crowdsec/pkg/database/ent" + "github.com/crowdsecurity/crowdsec/pkg/types" ) var machineID string @@ -43,7 +45,6 @@ var ( ) func generatePassword(length int) string { - charset := upper + lower + digits charsetLength := len(charset) @@ -109,50 +110,34 @@ func displayLastHeartBeat(m *ent.Machine, fancy bool) string { return hbDisplay } -func getAgents(dbClient *database.Client) ([]byte, error) { - w := bytes.NewBuffer(nil) +func getAgents(out io.Writer, dbClient *database.Client) error { machines, err := dbClient.ListMachines() if err != nil { - return nil, fmt.Errorf("unable to list machines: %s", err) + return fmt.Errorf("unable to list machines: %s", err) } if csConfig.Cscli.Output == "human" { - table := tablewriter.NewWriter(w) - table.SetCenterSeparator("") - table.SetColumnSeparator("") - - table.SetHeaderAlignment(tablewriter.ALIGN_LEFT) - table.SetAlignment(tablewriter.ALIGN_LEFT) - table.SetHeader([]string{"Name", "IP Address", "Last Update", "Status", "Version", "Auth Type", "Last Heartbeat"}) - for _, w := range machines { - var validated string - if w.IsValidated { - validated = emoji.CheckMark.String() - } else { - validated = emoji.Prohibited.String() - } - table.Append([]string{w.MachineId, w.IpAddress, w.UpdatedAt.Format(time.RFC3339), validated, w.Version, w.AuthType, displayLastHeartBeat(w, true)}) - } - table.Render() + getAgentsTable(out, machines) } else if csConfig.Cscli.Output == "json" { - x, err := json.MarshalIndent(machines, "", " ") - if err != nil { + enc := json.NewEncoder(out) + enc.SetIndent("", " ") + if err := enc.Encode(machines); err != nil { log.Fatalf("failed to unmarshal") } - return x, nil + return nil } else if csConfig.Cscli.Output == "raw" { - csvwriter := csv.NewWriter(w) + csvwriter := csv.NewWriter(out) err := csvwriter.Write([]string{"machine_id", "ip_address", "updated_at", "validated", "version", "auth_type", "last_heartbeat"}) if err != nil { log.Fatalf("failed to write header: %s", err) } - for _, w := range machines { + for _, m := range machines { var validated string - if w.IsValidated { + if m.IsValidated { validated = "true" } else { validated = "false" } - err := csvwriter.Write([]string{w.MachineId, w.IpAddress, w.UpdatedAt.Format(time.RFC3339), validated, w.Version, w.AuthType, displayLastHeartBeat(w, false)}) + err := csvwriter.Write([]string{m.MachineId, m.IpAddress, m.UpdatedAt.Format(time.RFC3339), validated, m.Version, m.AuthType, displayLastHeartBeat(m, false)}) if err != nil { log.Fatalf("failed to write raw output : %s", err) } @@ -161,7 +146,7 @@ func getAgents(dbClient *database.Client) ([]byte, error) { } else { log.Errorf("unknown output '%s'", csConfig.Cscli.Output) } - return w.Bytes(), nil + return nil } func NewMachinesCmd() *cobra.Command { @@ -204,11 +189,10 @@ Note: This command requires database direct access, so is intended to be run on } }, Run: func(cmd *cobra.Command, args []string) { - agents, err := getAgents(dbClient) + err := getAgents(colorable.NewColorableStdout(), dbClient) if err != nil { log.Fatalf("unable to list machines: %s", err) } - fmt.Printf("%s\n", agents) }, } cmdMachines.AddCommand(cmdMachinesList) diff --git a/cmd/crowdsec-cli/machines_table.go b/cmd/crowdsec-cli/machines_table.go new file mode 100644 index 000000000..cc15bb51b --- /dev/null +++ b/cmd/crowdsec-cli/machines_table.go @@ -0,0 +1,31 @@ +package main + +import ( + "io" + "time" + + "github.com/aquasecurity/table" + "github.com/enescakir/emoji" + + "github.com/crowdsecurity/crowdsec/pkg/database/ent" +) + +func getAgentsTable(out io.Writer, machines []*ent.Machine) { + t := newLightTable(out) + t.SetHeaders("Name", "IP Address", "Last Update", "Status", "Version", "Auth Type", "Last Heartbeat") + t.SetHeaderAlignment(table.AlignLeft, table.AlignLeft, table.AlignLeft, table.AlignLeft, table.AlignLeft, table.AlignLeft, table.AlignLeft) + t.SetAlignment(table.AlignLeft, table.AlignLeft, table.AlignLeft, table.AlignLeft, table.AlignLeft, table.AlignLeft, table.AlignLeft) + + for _, m := range machines { + var validated string + if m.IsValidated { + validated = emoji.CheckMark.String() + } else { + validated = emoji.Prohibited.String() + } + + t.AddRow(m.MachineId, m.IpAddress, m.UpdatedAt.Format(time.RFC3339), validated, m.Version, m.AuthType, displayLastHeartBeat(m, true)) + } + + t.Render() +} diff --git a/cmd/crowdsec-cli/main.go b/cmd/crowdsec-cli/main.go index acb8c9379..b6cc9eb27 100644 --- a/cmd/crowdsec-cli/main.go +++ b/cmd/crowdsec-cli/main.go @@ -8,14 +8,14 @@ import ( "strings" "github.com/confluentinc/bincover" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "github.com/spf13/cobra/doc" + "github.com/crowdsecurity/crowdsec/pkg/csconfig" "github.com/crowdsecurity/crowdsec/pkg/cwhub" "github.com/crowdsecurity/crowdsec/pkg/cwversion" "github.com/crowdsecurity/crowdsec/pkg/database" - - log "github.com/sirupsen/logrus" - "github.com/spf13/cobra" - "github.com/spf13/cobra/doc" ) var bincoverTesting = "" @@ -27,6 +27,7 @@ var csConfig *csconfig.Config var dbClient *database.Client var OutputFormat string +var OutputColor string var downloadOnly bool var forceAction bool @@ -88,6 +89,12 @@ func initConfig() { log.SetLevel(log.ErrorLevel) } + if OutputColor != "" { + csConfig.Cscli.Color = OutputColor + if OutputColor != "yes" && OutputColor != "no" && OutputColor != "auto" { + log.Fatalf("output color %s unknown", OutputColor) + } + } } var validArgs = []string{ @@ -159,7 +166,8 @@ It is meant to allow you to manage bans, parsers/scenarios/etc, api and generall rootCmd.AddCommand(cmdVersion) rootCmd.PersistentFlags().StringVarP(&ConfigFilePath, "config", "c", csconfig.DefaultConfigPath("config.yaml"), "path to crowdsec config file") - rootCmd.PersistentFlags().StringVarP(&OutputFormat, "output", "o", "", "Output format : human, json, raw.") + rootCmd.PersistentFlags().StringVarP(&OutputFormat, "output", "o", "", "Output format: human, json, raw.") + rootCmd.PersistentFlags().StringVarP(&OutputColor, "color", "", csconfig.ColorDefault(), "Output color: yes, no, auto.") rootCmd.PersistentFlags().BoolVar(&dbg_lvl, "debug", false, "Set logging to debug.") rootCmd.PersistentFlags().BoolVar(&nfo_lvl, "info", false, "Set logging to info.") rootCmd.PersistentFlags().BoolVar(&wrn_lvl, "warning", false, "Set logging to warning.") diff --git a/cmd/crowdsec-cli/metrics.go b/cmd/crowdsec-cli/metrics.go index e3310f5ee..5057d4346 100644 --- a/cmd/crowdsec-cli/metrics.go +++ b/cmd/crowdsec-cli/metrics.go @@ -1,96 +1,27 @@ package main import ( - "bytes" "encoding/json" "fmt" + "io" "net/http" "os" - "sort" "strconv" "strings" "time" - "github.com/crowdsecurity/crowdsec/pkg/types" - log "github.com/sirupsen/logrus" - "gopkg.in/yaml.v2" - - "github.com/olekukonko/tablewriter" + colorable "github.com/mattn/go-colorable" dto "github.com/prometheus/client_model/go" "github.com/prometheus/prom2json" + log "github.com/sirupsen/logrus" "github.com/spf13/cobra" + "gopkg.in/yaml.v2" + + "github.com/crowdsecurity/crowdsec/pkg/types" ) -func lapiMetricsToTable(table *tablewriter.Table, stats map[string]map[string]map[string]int) error { - - //stats : machine -> route -> method -> count - /*we want consistent display order*/ - machineKeys := []string{} - for k := range stats { - machineKeys = append(machineKeys, k) - } - sort.Strings(machineKeys) - - for _, machine := range machineKeys { - //oneRow : route -> method -> count - machineRow := stats[machine] - for routeName, route := range machineRow { - for methodName, count := range route { - row := []string{} - row = append(row, machine) - row = append(row, routeName) - row = append(row, methodName) - if count != 0 { - row = append(row, fmt.Sprintf("%d", count)) - } else { - row = append(row, "-") - } - table.Append(row) - } - } - } - return nil -} - -func metricsToTable(table *tablewriter.Table, stats map[string]map[string]int, keys []string) error { - - var sortedKeys []string - - if table == nil { - return fmt.Errorf("nil table") - } - //sort keys to keep consistent order when printing - sortedKeys = []string{} - for akey := range stats { - sortedKeys = append(sortedKeys, akey) - } - sort.Strings(sortedKeys) - // - for _, alabel := range sortedKeys { - astats, ok := stats[alabel] - if !ok { - continue - } - row := []string{} - row = append(row, alabel) //name - for _, sl := range keys { - if v, ok := astats[sl]; ok && v != 0 { - numberToShow := fmt.Sprintf("%d", v) - if !noUnit { - numberToShow = formatNumber(v) - } - row = append(row, numberToShow) - } else { - row = append(row, "-") - } - } - table.Append(row) - } - return nil -} - -/*This is a complete rip from prom2json*/ -func FormatPrometheusMetric(url string, formatType string) ([]byte, error) { +// FormatPrometheusMetrics is a complete rip from prom2json +func FormatPrometheusMetrics(out io.Writer, url string, formatType string) error { mfChan := make(chan *dto.MetricFamily, 1024) // Start with the DefaultTransport for sane defaults. @@ -284,171 +215,37 @@ func FormatPrometheusMetric(url string, formatType string) ([]byte, error) { } } - ret := bytes.NewBuffer(nil) - if formatType == "human" { - - acquisTable := tablewriter.NewWriter(ret) - acquisTable.SetHeader([]string{"Source", "Lines read", "Lines parsed", "Lines unparsed", "Lines poured to bucket"}) - keys := []string{"reads", "parsed", "unparsed", "pour"} - if err := metricsToTable(acquisTable, acquis_stats, keys); err != nil { - log.Warningf("while collecting acquis stats : %s", err) - } - bucketsTable := tablewriter.NewWriter(ret) - bucketsTable.SetHeader([]string{"Bucket", "Current Count", "Overflows", "Instantiated", "Poured", "Expired"}) - keys = []string{"curr_count", "overflow", "instantiation", "pour", "underflow"} - if err := metricsToTable(bucketsTable, buckets_stats, keys); err != nil { - log.Warningf("while collecting acquis stats : %s", err) - } - - parsersTable := tablewriter.NewWriter(ret) - parsersTable.SetHeader([]string{"Parsers", "Hits", "Parsed", "Unparsed"}) - keys = []string{"hits", "parsed", "unparsed"} - if err := metricsToTable(parsersTable, parsers_stats, keys); err != nil { - log.Warningf("while collecting acquis stats : %s", err) - } - - lapiMachinesTable := tablewriter.NewWriter(ret) - lapiMachinesTable.SetHeader([]string{"Machine", "Route", "Method", "Hits"}) - if err := lapiMetricsToTable(lapiMachinesTable, lapi_machine_stats); err != nil { - log.Warningf("while collecting machine lapi stats : %s", err) - } - - //lapiMetricsToTable - lapiBouncersTable := tablewriter.NewWriter(ret) - lapiBouncersTable.SetHeader([]string{"Bouncer", "Route", "Method", "Hits"}) - if err := lapiMetricsToTable(lapiBouncersTable, lapi_bouncer_stats); err != nil { - log.Warningf("while collecting bouncer lapi stats : %s", err) - } - - lapiDecisionsTable := tablewriter.NewWriter(ret) - lapiDecisionsTable.SetHeader([]string{"Bouncer", "Empty answers", "Non-empty answers"}) - for bouncer, hits := range lapi_decisions_stats { - row := []string{} - row = append(row, bouncer) - row = append(row, fmt.Sprintf("%d", hits.Empty)) - row = append(row, fmt.Sprintf("%d", hits.NonEmpty)) - lapiDecisionsTable.Append(row) - } - - /*unfortunately, we can't reuse metricsToTable as the structure is too different :/*/ - lapiTable := tablewriter.NewWriter(ret) - lapiTable.SetHeader([]string{"Route", "Method", "Hits"}) - sortedKeys := []string{} - for akey := range lapi_stats { - sortedKeys = append(sortedKeys, akey) - } - sort.Strings(sortedKeys) - for _, alabel := range sortedKeys { - astats := lapi_stats[alabel] - subKeys := []string{} - for skey := range astats { - subKeys = append(subKeys, skey) - } - sort.Strings(subKeys) - for _, sl := range subKeys { - row := []string{} - row = append(row, alabel) - row = append(row, sl) - row = append(row, fmt.Sprintf("%d", astats[sl])) - lapiTable.Append(row) - } - } - - decisionsTable := tablewriter.NewWriter(ret) - decisionsTable.SetHeader([]string{"Reason", "Origin", "Action", "Count"}) - for reason, origins := range decisions_stats { - for origin, actions := range origins { - for action, hits := range actions { - row := []string{} - row = append(row, reason) - row = append(row, origin) - row = append(row, action) - row = append(row, fmt.Sprintf("%d", hits)) - decisionsTable.Append(row) - } - } - } - - alertsTable := tablewriter.NewWriter(ret) - alertsTable.SetHeader([]string{"Reason", "Count"}) - for scenario, hits := range alerts_stats { - row := []string{} - row = append(row, scenario) - row = append(row, fmt.Sprintf("%d", hits)) - alertsTable.Append(row) - } - - if bucketsTable.NumLines() > 0 { - fmt.Fprintf(ret, "Buckets Metrics:\n") - bucketsTable.SetAlignment(tablewriter.ALIGN_LEFT) - bucketsTable.Render() - } - if acquisTable.NumLines() > 0 { - fmt.Fprintf(ret, "Acquisition Metrics:\n") - acquisTable.SetAlignment(tablewriter.ALIGN_LEFT) - acquisTable.Render() - } - if parsersTable.NumLines() > 0 { - fmt.Fprintf(ret, "Parser Metrics:\n") - parsersTable.SetAlignment(tablewriter.ALIGN_LEFT) - parsersTable.Render() - } - if lapiTable.NumLines() > 0 { - fmt.Fprintf(ret, "Local Api Metrics:\n") - lapiTable.SetAlignment(tablewriter.ALIGN_LEFT) - lapiTable.Render() - } - if lapiMachinesTable.NumLines() > 0 { - fmt.Fprintf(ret, "Local Api Machines Metrics:\n") - lapiMachinesTable.SetAlignment(tablewriter.ALIGN_LEFT) - lapiMachinesTable.Render() - } - if lapiBouncersTable.NumLines() > 0 { - fmt.Fprintf(ret, "Local Api Bouncers Metrics:\n") - lapiBouncersTable.SetAlignment(tablewriter.ALIGN_LEFT) - lapiBouncersTable.Render() - } - - if lapiDecisionsTable.NumLines() > 0 { - fmt.Fprintf(ret, "Local Api Bouncers Decisions:\n") - lapiDecisionsTable.SetAlignment(tablewriter.ALIGN_LEFT) - lapiDecisionsTable.Render() - } - - if decisionsTable.NumLines() > 0 { - fmt.Fprintf(ret, "Local Api Decisions:\n") - decisionsTable.SetAlignment(tablewriter.ALIGN_LEFT) - decisionsTable.Render() - } - - if alertsTable.NumLines() > 0 { - fmt.Fprintf(ret, "Local Api Alerts:\n") - alertsTable.SetAlignment(tablewriter.ALIGN_LEFT) - alertsTable.Render() - } - + acquisStatsTable(out, acquis_stats) + bucketStatsTable(out, buckets_stats) + parserStatsTable(out, parsers_stats) + lapiStatsTable(out, lapi_stats) + lapiMachineStatsTable(out, lapi_machine_stats) + lapiBouncerStatsTable(out, lapi_bouncer_stats) + lapiDecisionStatsTable(out, lapi_decisions_stats) + decisionStatsTable(out, decisions_stats) + alertStatsTable(out, alerts_stats) } else if formatType == "json" { for _, val := range []interface{}{acquis_stats, parsers_stats, buckets_stats, lapi_stats, lapi_bouncer_stats, lapi_machine_stats, lapi_decisions_stats, decisions_stats, alerts_stats} { x, err := json.MarshalIndent(val, "", " ") if err != nil { - return nil, fmt.Errorf("failed to unmarshal metrics : %v", err) + return fmt.Errorf("failed to unmarshal metrics : %v", err) } - ret.Write(x) + out.Write(x) } - return ret.Bytes(), nil + return nil } else if formatType == "raw" { for _, val := range []interface{}{acquis_stats, parsers_stats, buckets_stats, lapi_stats, lapi_bouncer_stats, lapi_machine_stats, lapi_decisions_stats, decisions_stats, alerts_stats} { x, err := yaml.Marshal(val) if err != nil { - return nil, fmt.Errorf("failed to unmarshal metrics : %v", err) + return fmt.Errorf("failed to unmarshal metrics : %v", err) } - ret.Write(x) + out.Write(x) } - return ret.Bytes(), nil + return nil } - return ret.Bytes(), nil + return nil } var noUnit bool @@ -479,11 +276,10 @@ func NewMetricsCmd() *cobra.Command { os.Exit(1) } - metrics, err := FormatPrometheusMetric(prometheusURL+"/metrics", csConfig.Cscli.Output) + err := FormatPrometheusMetrics(colorable.NewColorableStdout(), prometheusURL+"/metrics", csConfig.Cscli.Output) if err != nil { log.Fatalf("could not fetch prometheus metrics: %s", err) } - fmt.Printf("%s", metrics) }, } cmdMetrics.PersistentFlags().StringVarP(&prometheusURL, "url", "u", "", "Prometheus url (http://:/metrics)") diff --git a/cmd/crowdsec-cli/metrics_table.go b/cmd/crowdsec-cli/metrics_table.go new file mode 100644 index 000000000..5fe71e763 --- /dev/null +++ b/cmd/crowdsec-cli/metrics_table.go @@ -0,0 +1,272 @@ +package main + +import ( + "fmt" + "io" + "sort" + + "github.com/aquasecurity/table" + log "github.com/sirupsen/logrus" +) + +func lapiMetricsToTable(t *table.Table, stats map[string]map[string]map[string]int) int { + // stats: machine -> route -> method -> count + + // sort keys to keep consistent order when printing + machineKeys := []string{} + for k := range stats { + machineKeys = append(machineKeys, k) + } + sort.Strings(machineKeys) + + numRows := 0 + for _, machine := range machineKeys { + // oneRow: route -> method -> count + machineRow := stats[machine] + for routeName, route := range machineRow { + for methodName, count := range route { + row := []string{ + machine, + routeName, + methodName, + } + if count != 0 { + row = append(row, fmt.Sprintf("%d", count)) + } else { + row = append(row, "-") + } + t.AddRow(row...) + numRows++ + } + } + } + return numRows +} + +func metricsToTable(t *table.Table, stats map[string]map[string]int, keys []string) (int, error) { + if t == nil { + return 0, fmt.Errorf("nil table") + } + + // sort keys to keep consistent order when printing + sortedKeys := []string{} + for k := range stats { + sortedKeys = append(sortedKeys, k) + } + sort.Strings(sortedKeys) + + numRows := 0 + for _, alabel := range sortedKeys { + astats, ok := stats[alabel] + if !ok { + continue + } + row := []string{ + alabel, + } + for _, sl := range keys { + if v, ok := astats[sl]; ok && v != 0 { + numberToShow := fmt.Sprintf("%d", v) + if !noUnit { + numberToShow = formatNumber(v) + } + + row = append(row, numberToShow) + } else { + row = append(row, "-") + } + } + t.AddRow(row...) + } + return numRows, nil +} + +func bucketStatsTable(out io.Writer, stats map[string]map[string]int) { + t := newTable(out) + t.SetRowLines(false) + t.SetHeaders("Bucket", "Current Count", "Overflows", "Instantiated", "Poured", "Expired") + t.SetAlignment(table.AlignLeft, table.AlignLeft, table.AlignLeft, table.AlignLeft, table.AlignLeft, table.AlignLeft) + + keys := []string{"curr_count", "overflow", "instantiation", "pour", "underflow"} + + if numRows, err := metricsToTable(t, stats, keys); err != nil { + log.Warningf("while collecting acquis stats: %s", err) + } else if numRows > 0 { + renderTableTitle(out, "\nBucket Metrics:") + t.Render() + } +} + +func acquisStatsTable(out io.Writer, stats map[string]map[string]int) { + t := newTable(out) + t.SetRowLines(false) + t.SetHeaders("Source", "Lines read", "Lines parsed", "Lines unparsed", "Lines poured to bucket") + t.SetAlignment(table.AlignLeft, table.AlignLeft, table.AlignLeft, table.AlignLeft, table.AlignLeft) + + keys := []string{"reads", "parsed", "unparsed", "pour"} + + if numRows, err := metricsToTable(t, stats, keys); err != nil { + log.Warningf("while collecting acquis stats: %s", err) + } else if numRows > 0 { + renderTableTitle(out, "\nAcquisition Metrics:") + t.Render() + } +} + +func parserStatsTable(out io.Writer, stats map[string]map[string]int) { + t := newTable(out) + t.SetRowLines(false) + t.SetHeaders("Parsers", "Hits", "Parsed", "Unparsed") + t.SetAlignment(table.AlignLeft, table.AlignLeft, table.AlignLeft, table.AlignLeft) + + keys := []string{"hits", "parsed", "unparsed"} + + if numRows, err := metricsToTable(t, stats, keys); err != nil { + log.Warningf("while collecting acquis stats: %s", err) + } else if numRows > 0 { + renderTableTitle(out, "\nParser Metrics:") + t.Render() + } +} + +func lapiStatsTable(out io.Writer, stats map[string]map[string]int) { + t := newTable(out) + t.SetRowLines(false) + t.SetHeaders("Route", "Method", "Hits") + t.SetAlignment(table.AlignLeft, table.AlignLeft, table.AlignLeft) + + // unfortunately, we can't reuse metricsToTable as the structure is too different :/ + sortedKeys := []string{} + for k := range stats { + sortedKeys = append(sortedKeys, k) + } + sort.Strings(sortedKeys) + + numRows := 0 + for _, alabel := range sortedKeys { + astats := stats[alabel] + + subKeys := []string{} + for skey := range astats { + subKeys = append(subKeys, skey) + } + sort.Strings(subKeys) + + for _, sl := range subKeys { + row := []string{ + alabel, + sl, + fmt.Sprintf("%d", astats[sl]), + } + t.AddRow(row...) + numRows++ + } + } + + if numRows > 0 { + renderTableTitle(out, "\nLocal Api Metrics:") + t.Render() + } +} + +func lapiMachineStatsTable(out io.Writer, stats map[string]map[string]map[string]int) { + t := newTable(out) + t.SetRowLines(false) + t.SetHeaders("Machine", "Route", "Method", "Hits") + t.SetAlignment(table.AlignLeft, table.AlignLeft, table.AlignLeft, table.AlignLeft) + + numRows := lapiMetricsToTable(t, stats) + + if numRows > 0 { + renderTableTitle(out, "\nLocal Api Machines Metrics:") + t.Render() + } +} + +func lapiBouncerStatsTable(out io.Writer, stats map[string]map[string]map[string]int) { + t := newTable(out) + t.SetRowLines(false) + t.SetHeaders("Bouncer", "Route", "Method", "Hits") + t.SetAlignment(table.AlignLeft, table.AlignLeft, table.AlignLeft, table.AlignLeft) + + numRows := lapiMetricsToTable(t, stats) + + if numRows > 0 { + renderTableTitle(out, "\nLocal Api Bouncers Metrics:") + t.Render() + } +} + +func lapiDecisionStatsTable(out io.Writer, stats map[string]struct { + NonEmpty int + Empty int +}, +) { + t := newTable(out) + t.SetRowLines(false) + t.SetHeaders("Bouncer", "Empty answers", "Non-empty answers") + t.SetAlignment(table.AlignLeft, table.AlignLeft, table.AlignLeft) + + numRows := 0 + for bouncer, hits := range stats { + t.AddRow( + bouncer, + fmt.Sprintf("%d", hits.Empty), + fmt.Sprintf("%d", hits.NonEmpty), + ) + numRows++ + } + + if numRows > 0 { + renderTableTitle(out, "\nLocal Api Bouncers Decisions:") + t.Render() + } +} + +func decisionStatsTable(out io.Writer, stats map[string]map[string]map[string]int) { + t := newTable(out) + t.SetRowLines(false) + t.SetHeaders("Reason", "Origin", "Action", "Count") + t.SetAlignment(table.AlignLeft, table.AlignLeft, table.AlignLeft, table.AlignLeft) + + numRows := 0 + for reason, origins := range stats { + for origin, actions := range origins { + for action, hits := range actions { + t.AddRow( + reason, + origin, + action, + fmt.Sprintf("%d", hits), + ) + numRows++ + } + } + } + + if numRows > 0 { + renderTableTitle(out, "\nLocal Api Decisions:") + t.Render() + } +} + +func alertStatsTable(out io.Writer, stats map[string]int) { + t := newTable(out) + t.SetRowLines(false) + t.SetHeaders("Reason", "Count") + t.SetAlignment(table.AlignLeft, table.AlignLeft) + + numRows := 0 + for scenario, hits := range stats { + t.AddRow( + scenario, + fmt.Sprintf("%d", hits), + ) + numRows++ + } + + if numRows > 0 { + renderTableTitle(out, "\nLocal Api Alerts:") + t.Render() + } +} diff --git a/cmd/crowdsec-cli/notifications.go b/cmd/crowdsec-cli/notifications.go index 37b4d4ac4..e6a579536 100644 --- a/cmd/crowdsec-cli/notifications.go +++ b/cmd/crowdsec-cli/notifications.go @@ -13,17 +13,18 @@ import ( "strings" "time" + "github.com/go-openapi/strfmt" + colorable "github.com/mattn/go-colorable" + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "gopkg.in/tomb.v2" + "github.com/crowdsecurity/crowdsec/pkg/apiclient" "github.com/crowdsecurity/crowdsec/pkg/csconfig" "github.com/crowdsecurity/crowdsec/pkg/csplugin" "github.com/crowdsecurity/crowdsec/pkg/csprofiles" "github.com/crowdsecurity/crowdsec/pkg/cwversion" - "github.com/go-openapi/strfmt" - "github.com/olekukonko/tablewriter" - "github.com/pkg/errors" - log "github.com/sirupsen/logrus" - "github.com/spf13/cobra" - "gopkg.in/tomb.v2" ) type NotificationsCfg struct { @@ -67,22 +68,7 @@ func NewNotificationsCmd() *cobra.Command { } if csConfig.Cscli.Output == "human" { - table := tablewriter.NewWriter(os.Stdout) - table.SetCenterSeparator("") - table.SetColumnSeparator("") - - table.SetHeaderAlignment(tablewriter.ALIGN_LEFT) - table.SetAlignment(tablewriter.ALIGN_LEFT) - table.SetHeader([]string{"Name", "Type", "Profile name"}) - for _, b := range ncfgs { - profilesList := []string{} - for _, p := range b.Profiles { - profilesList = append(profilesList, p.Name) - } - table.Append([]string{b.Config.Name, b.Config.Type, strings.Join(profilesList, ", ")}) - } - table.Render() - + notificationListTable(colorable.NewColorableStdout(), ncfgs) } else if csConfig.Cscli.Output == "json" { x, err := json.MarshalIndent(ncfgs, "", " ") if err != nil { diff --git a/cmd/crowdsec-cli/notifications_table.go b/cmd/crowdsec-cli/notifications_table.go new file mode 100644 index 000000000..1113bb7c8 --- /dev/null +++ b/cmd/crowdsec-cli/notifications_table.go @@ -0,0 +1,25 @@ +package main + +import ( + "io" + "strings" + + "github.com/aquasecurity/table" +) + +func notificationListTable(out io.Writer, ncfgs map[string]NotificationsCfg) { + t := newLightTable(out) + t.SetHeaders("Name", "Type", "Profile name") + t.SetHeaderAlignment(table.AlignLeft, table.AlignLeft, table.AlignLeft) + t.SetAlignment(table.AlignLeft, table.AlignLeft, table.AlignLeft) + + for _, b := range ncfgs { + profilesList := []string{} + for _, p := range b.Profiles { + profilesList = append(profilesList, p.Name) + } + t.AddRow(b.Config.Name, b.Config.Type, strings.Join(profilesList, ", ")) + } + + t.Render() +} diff --git a/cmd/crowdsec-cli/parsers.go b/cmd/crowdsec-cli/parsers.go index 30c789f63..0c70b3867 100644 --- a/cmd/crowdsec-cli/parsers.go +++ b/cmd/crowdsec-cli/parsers.go @@ -3,11 +3,11 @@ package main import ( "fmt" - "github.com/crowdsecurity/crowdsec/pkg/cwhub" - + colorable "github.com/mattn/go-colorable" log "github.com/sirupsen/logrus" - "github.com/spf13/cobra" + + "github.com/crowdsecurity/crowdsec/pkg/cwhub" ) func NewParsersCmd() *cobra.Command { @@ -164,8 +164,7 @@ cscli parsers remove crowdsecurity/sshd-logs cscli parser list crowdsecurity/xxx`, DisableAutoGenTag: true, Run: func(cmd *cobra.Command, args []string) { - items := ListItems([]string{cwhub.PARSERS}, args, false, true, all) - fmt.Printf("%s\n", items) + ListItems(colorable.NewColorableStdout(), []string{cwhub.PARSERS}, args, false, true, all) }, } cmdParsersList.PersistentFlags().BoolVarP(&all, "all", "a", false, "List disabled items as well") diff --git a/cmd/crowdsec-cli/postoverflows.go b/cmd/crowdsec-cli/postoverflows.go index f3efdc89c..d6d933553 100644 --- a/cmd/crowdsec-cli/postoverflows.go +++ b/cmd/crowdsec-cli/postoverflows.go @@ -3,11 +3,11 @@ package main import ( "fmt" - "github.com/crowdsecurity/crowdsec/pkg/cwhub" - + colorable "github.com/mattn/go-colorable" log "github.com/sirupsen/logrus" - "github.com/spf13/cobra" + + "github.com/crowdsecurity/crowdsec/pkg/cwhub" ) func NewPostOverflowsCmd() *cobra.Command { @@ -162,8 +162,7 @@ func NewPostOverflowsCmd() *cobra.Command { cscli postoverflows list crowdsecurity/xxx`, DisableAutoGenTag: true, Run: func(cmd *cobra.Command, args []string) { - items := ListItems([]string{cwhub.PARSERS_OVFLW}, args, false, true, all) - fmt.Printf("%s\n", items) + ListItems(colorable.NewColorableStdout(), []string{cwhub.PARSERS_OVFLW}, args, false, true, all) }, } cmdPostOverflowsList.PersistentFlags().BoolVarP(&all, "all", "a", false, "List disabled items as well") diff --git a/cmd/crowdsec-cli/scenarios.go b/cmd/crowdsec-cli/scenarios.go index c0ed30fe7..44ebd9297 100644 --- a/cmd/crowdsec-cli/scenarios.go +++ b/cmd/crowdsec-cli/scenarios.go @@ -3,11 +3,12 @@ package main import ( "fmt" - "github.com/crowdsecurity/crowdsec/pkg/cwhub" + colorable "github.com/mattn/go-colorable" "github.com/pkg/errors" log "github.com/sirupsen/logrus" - "github.com/spf13/cobra" + + "github.com/crowdsecurity/crowdsec/pkg/cwhub" ) func NewScenariosCmd() *cobra.Command { @@ -166,8 +167,7 @@ cscli scenarios remove crowdsecurity/ssh-bf cscli scenarios list crowdsecurity/xxx`, DisableAutoGenTag: true, Run: func(cmd *cobra.Command, args []string) { - items := ListItems([]string{cwhub.SCENARIOS}, args, false, true, all) - fmt.Printf("%s\n", items) + ListItems(colorable.NewColorableStdout(), []string{cwhub.SCENARIOS}, args, false, true, all) }, } cmdScenariosList.PersistentFlags().BoolVarP(&all, "all", "a", false, "List disabled items as well") diff --git a/cmd/crowdsec-cli/support.go b/cmd/crowdsec-cli/support.go index dc858c9bd..84c0e2700 100644 --- a/cmd/crowdsec-cli/support.go +++ b/cmd/crowdsec-cli/support.go @@ -14,15 +14,16 @@ import ( "strings" "github.com/blackfireio/osinfo" + "github.com/go-openapi/strfmt" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "github.com/crowdsecurity/crowdsec/pkg/apiclient" "github.com/crowdsecurity/crowdsec/pkg/cwhub" "github.com/crowdsecurity/crowdsec/pkg/cwversion" "github.com/crowdsecurity/crowdsec/pkg/database" "github.com/crowdsecurity/crowdsec/pkg/models" - "github.com/go-openapi/strfmt" - log "github.com/sirupsen/logrus" - - "github.com/spf13/cobra" + "github.com/crowdsecurity/crowdsec/pkg/types" ) const ( @@ -55,7 +56,8 @@ func collectMetrics() ([]byte, []byte, error) { return nil, nil, fmt.Errorf("prometheus_uri is not set") } - humanMetrics, err := FormatPrometheusMetric(csConfig.Cscli.PrometheusUrl+"/metrics", "human") + humanMetrics := bytes.NewBuffer(nil) + err = FormatPrometheusMetrics(humanMetrics, csConfig.Cscli.PrometheusUrl+"/metrics", "human") if err != nil { return nil, nil, fmt.Errorf("could not fetch promtheus metrics: %s", err) @@ -79,7 +81,7 @@ func collectMetrics() ([]byte, []byte, error) { return nil, nil, fmt.Errorf("could not read metrics from prometheus endpoint: %s", err) } - return humanMetrics, body, nil + return humanMetrics.Bytes(), body, nil } func collectVersion() []byte { @@ -126,17 +128,28 @@ func initHub() error { } func collectHubItems(itemType string) []byte { + out := bytes.NewBuffer(nil) log.Infof("Collecting %s list", itemType) - items := ListItems([]string{itemType}, []string{}, false, true, all) - return items + ListItems(out, []string{itemType}, []string{}, false, true, all) + return out.Bytes() } func collectBouncers(dbClient *database.Client) ([]byte, error) { - return getBouncers(dbClient) + out := bytes.NewBuffer(nil) + err := getBouncers(out, dbClient) + if err != nil { + return nil, err + } + return out.Bytes(), nil } func collectAgents(dbClient *database.Client) ([]byte, error) { - return getAgents(dbClient) + out := bytes.NewBuffer(nil) + err := getAgents(out, dbClient) + if err != nil { + return nil, err + } + return out.Bytes(), nil } func collectAPIStatus(login string, password string, endpoint string, prefix string) []byte { @@ -374,7 +387,7 @@ cscli support dump -f /tmp/crowdsec-support.zip log.Errorf("Could not add zip entry for %s: %s", filename, err) continue } - fw.Write(data) + fw.Write([]byte(types.StripAnsiString(string(data)))) } err = zipWriter.Close() if err != nil { diff --git a/cmd/crowdsec-cli/tables.go b/cmd/crowdsec-cli/tables.go new file mode 100644 index 000000000..2c3173d0b --- /dev/null +++ b/cmd/crowdsec-cli/tables.go @@ -0,0 +1,95 @@ +package main + +import ( + "fmt" + "io" + "os" + + "github.com/aquasecurity/table" + isatty "github.com/mattn/go-isatty" +) + +func shouldWeColorize() bool { + if csConfig.Cscli.Color == "yes" { + return true + } + if csConfig.Cscli.Color == "no" { + return false + } + return isatty.IsTerminal(os.Stdout.Fd()) || isatty.IsCygwinTerminal(os.Stdout.Fd()) +} + +func newTable(out io.Writer) *table.Table { + if out == nil { + panic("newTable: out is nil") + } + t := table.New(out) + if shouldWeColorize() { + t.SetLineStyle(table.StyleBrightBlack) + t.SetHeaderStyle(table.StyleItalic) + } + + if shouldWeColorize() { + t.SetDividers(table.UnicodeRoundedDividers) + } else { + t.SetDividers(table.ASCIIDividers) + } + + return t +} + +func newLightTable(out io.Writer) *table.Table { + if out == nil { + panic("newTable: out is nil") + } + t := newTable(out) + t.SetRowLines(false) + t.SetBorderLeft(false) + t.SetBorderRight(false) + // This leaves three spaces between columns: + // left padding, invisible border, right padding + // There is no way to make two spaces without + // a SetColumnLines() method, but it's close enough. + t.SetPadding(1) + + if shouldWeColorize() { + t.SetDividers(table.Dividers{ + ALL: "─", + NES: "─", + NSW: "─", + NEW: "─", + ESW: "─", + NE: "─", + NW: "─", + SW: "─", + ES: "─", + EW: "─", + NS: " ", + }) + } else { + t.SetDividers(table.Dividers{ + ALL: "-", + NES: "-", + NSW: "-", + NEW: "-", + ESW: "-", + NE: "-", + NW: "-", + SW: "-", + ES: "-", + EW: "-", + NS: " ", + }) + } + return t +} + +func renderTableTitle(out io.Writer, title string) { + if out == nil { + panic("renderTableTitle: out is nil") + } + if title == "" { + return + } + fmt.Fprintln(out, title) +} diff --git a/cmd/crowdsec-cli/utils.go b/cmd/crowdsec-cli/utils.go index eb52737ec..e1daaba73 100644 --- a/cmd/crowdsec-cli/utils.go +++ b/cmd/crowdsec-cli/utils.go @@ -1,10 +1,10 @@ package main import ( - "bytes" "encoding/csv" "encoding/json" "fmt" + "io" "math" "net" "net/http" @@ -13,16 +13,16 @@ import ( "strings" "time" - "github.com/crowdsecurity/crowdsec/pkg/cwhub" - "github.com/crowdsecurity/crowdsec/pkg/types" - "github.com/enescakir/emoji" - "github.com/olekukonko/tablewriter" + colorable "github.com/mattn/go-colorable" dto "github.com/prometheus/client_model/go" "github.com/prometheus/prom2json" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/texttheater/golang-levenshtein/levenshtein" "gopkg.in/yaml.v2" + + "github.com/crowdsecurity/crowdsec/pkg/cwhub" + "github.com/crowdsecurity/crowdsec/pkg/types" ) const MaxDistance = 7 @@ -161,8 +161,7 @@ func compInstalledItems(itemType string, args []string, toComplete string) ([]st return comp, cobra.ShellCompDirectiveNoFileComp } -func ListItems(itemTypes []string, args []string, showType bool, showHeader bool, all bool) []byte { - +func ListItems(out io.Writer, itemTypes []string, args []string, showType bool, showHeader bool, all bool) { var hubStatusByItemType = make(map[string][]cwhub.ItemHubStatus) for _, itemType := range itemTypes { @@ -173,8 +172,6 @@ func ListItems(itemTypes []string, args []string, showType bool, showHeader bool hubStatusByItemType[itemType] = cwhub.GetHubStatusForItemType(itemType, itemName, all) } - w := bytes.NewBuffer(nil) - if csConfig.Cscli.Output == "human" { for _, itemType := range itemTypes { var statuses []cwhub.ItemHubStatus @@ -183,26 +180,16 @@ func ListItems(itemTypes []string, args []string, showType bool, showHeader bool log.Errorf("unknown item type: %s", itemType) continue } - fmt.Fprintf(w, "%s\n", strings.ToUpper(itemType)) - table := tablewriter.NewWriter(w) - table.SetCenterSeparator("") - table.SetColumnSeparator("") - table.SetHeaderAlignment(tablewriter.ALIGN_LEFT) - table.SetAlignment(tablewriter.ALIGN_LEFT) - table.SetHeader([]string{"Name", fmt.Sprintf("%v Status", emoji.Package), "Version", "Local Path"}) - for _, status := range statuses { - table.Append([]string{status.Name, status.UTF8_Status, status.LocalVersion, status.LocalPath}) - } - table.Render() + listHubItemTable(out, "\n"+strings.ToUpper(itemType), statuses) } } else if csConfig.Cscli.Output == "json" { x, err := json.MarshalIndent(hubStatusByItemType, "", " ") if err != nil { log.Fatalf("failed to unmarshal") } - w.Write(x) + out.Write(x) } else if csConfig.Cscli.Output == "raw" { - csvwriter := csv.NewWriter(w) + csvwriter := csv.NewWriter(out) if showHeader { header := []string{"name", "status", "version", "description"} if showType { @@ -242,7 +229,6 @@ func ListItems(itemTypes []string, args []string, showType bool, showHeader bool } csvwriter.Flush() } - return w.Bytes() } func InspectItem(name string, objecitemType string) { @@ -279,7 +265,7 @@ func InspectItem(name string, objecitemType string) { log.Debugf("No prometheus URL provided using: %s:%d", csConfig.Prometheus.ListenAddr, csConfig.Prometheus.ListenPort) prometheusURL = fmt.Sprintf("http://%s:%d/metrics", csConfig.Prometheus.ListenAddr, csConfig.Prometheus.ListenPort) } - fmt.Printf("\nCurrent metrics : \n\n") + fmt.Printf("\nCurrent metrics : \n") ShowMetrics(hubItem) } } @@ -318,18 +304,18 @@ func ShowMetrics(hubItem *cwhub.Item) { switch hubItem.Type { case cwhub.PARSERS: metrics := GetParserMetric(prometheusURL, hubItem.Name) - ShowParserMetric(hubItem.Name, metrics) + parserMetricsTable(colorable.NewColorableStdout(), hubItem.Name, metrics) case cwhub.SCENARIOS: metrics := GetScenarioMetric(prometheusURL, hubItem.Name) - ShowScenarioMetric(hubItem.Name, metrics) + scenarioMetricsTable(colorable.NewColorableStdout(), hubItem.Name, metrics) case cwhub.COLLECTIONS: for _, item := range hubItem.Parsers { metrics := GetParserMetric(prometheusURL, item) - ShowParserMetric(item, metrics) + parserMetricsTable(colorable.NewColorableStdout(), item, metrics) } for _, item := range hubItem.Scenarios { metrics := GetScenarioMetric(prometheusURL, item) - ShowScenarioMetric(item, metrics) + scenarioMetricsTable(colorable.NewColorableStdout(), item, metrics) } for _, item := range hubItem.Collections { hubItem = cwhub.GetItem(cwhub.COLLECTIONS, item) @@ -343,7 +329,7 @@ func ShowMetrics(hubItem *cwhub.Item) { } } -/*This is a complete rip from prom2json*/ +// GetParserMetric is a complete rip from prom2json func GetParserMetric(url string, itemName string) map[string]map[string]int { stats := make(map[string]map[string]int) @@ -480,7 +466,7 @@ func GetScenarioMetric(url string, itemName string) map[string]int { return stats } -//it's a rip of the cli version, but in silent-mode +// it's a rip of the cli version, but in silent-mode func silenceInstallItem(name string, obtype string) (string, error) { var item = cwhub.GetItem(obtype, name) if item == nil { @@ -539,37 +525,6 @@ func GetPrometheusMetric(url string) []*prom2json.Family { return result } -func ShowScenarioMetric(itemName string, metrics map[string]int) { - if metrics["instantiation"] == 0 { - return - } - table := tablewriter.NewWriter(os.Stdout) - table.SetHeader([]string{"Current Count", "Overflows", "Instantiated", "Poured", "Expired"}) - table.Append([]string{fmt.Sprintf("%d", metrics["curr_count"]), fmt.Sprintf("%d", metrics["overflow"]), fmt.Sprintf("%d", metrics["instantiation"]), fmt.Sprintf("%d", metrics["pour"]), fmt.Sprintf("%d", metrics["underflow"])}) - - fmt.Printf(" - (Scenario) %s: \n", itemName) - table.Render() - fmt.Println() -} - -func ShowParserMetric(itemName string, metrics map[string]map[string]int) { - skip := true - - table := tablewriter.NewWriter(os.Stdout) - table.SetHeader([]string{"Parsers", "Hits", "Parsed", "Unparsed"}) - for source, stats := range metrics { - if stats["hits"] > 0 { - table.Append([]string{source, fmt.Sprintf("%d", stats["hits"]), fmt.Sprintf("%d", stats["parsed"]), fmt.Sprintf("%d", stats["unparsed"])}) - skip = false - } - } - if !skip { - fmt.Printf(" - (Parser) %s: \n", itemName) - table.Render() - fmt.Println() - } -} - func RestoreHub(dirPath string) error { var err error diff --git a/cmd/crowdsec-cli/utils_table.go b/cmd/crowdsec-cli/utils_table.go new file mode 100644 index 000000000..aef1e94f7 --- /dev/null +++ b/cmd/crowdsec-cli/utils_table.go @@ -0,0 +1,66 @@ +package main + +import ( + "fmt" + "io" + + "github.com/aquasecurity/table" + "github.com/enescakir/emoji" + + "github.com/crowdsecurity/crowdsec/pkg/cwhub" +) + +func listHubItemTable(out io.Writer, title string, statuses []cwhub.ItemHubStatus) { + t := newLightTable(out) + t.SetHeaders("Name", fmt.Sprintf("%v Status", emoji.Package), "Version", "Local Path") + t.SetHeaderAlignment(table.AlignLeft, table.AlignLeft, table.AlignLeft, table.AlignLeft) + t.SetAlignment(table.AlignLeft, table.AlignLeft, table.AlignLeft, table.AlignLeft) + + for _, status := range statuses { + t.AddRow(status.Name, status.UTF8_Status, status.LocalVersion, status.LocalPath) + } + renderTableTitle(out, title) + t.Render() +} + +func scenarioMetricsTable(out io.Writer, itemName string, metrics map[string]int) { + if metrics["instantiation"] == 0 { + return + } + t := newTable(out) + t.SetHeaders("Current Count", "Overflows", "Instantiated", "Poured", "Expired") + + t.AddRow( + fmt.Sprintf("%d", metrics["curr_count"]), + fmt.Sprintf("%d", metrics["overflow"]), + fmt.Sprintf("%d", metrics["instantiation"]), + fmt.Sprintf("%d", metrics["pour"]), + fmt.Sprintf("%d", metrics["underflow"]), + ) + + renderTableTitle(out, fmt.Sprintf("\n - (Scenario) %s:", itemName)) + t.Render() +} + +func parserMetricsTable(out io.Writer, itemName string, metrics map[string]map[string]int) { + skip := true + t := newTable(out) + t.SetHeaders("Parsers", "Hits", "Parsed", "Unparsed") + + for source, stats := range metrics { + if stats["hits"] > 0 { + t.AddRow( + source, + fmt.Sprintf("%d", stats["hits"]), + fmt.Sprintf("%d", stats["parsed"]), + fmt.Sprintf("%d", stats["unparsed"]), + ) + skip = false + } + } + + if !skip { + renderTableTitle(out, fmt.Sprintf("\n - (Parser) %s:", itemName)) + t.Render() + } +} diff --git a/go.mod b/go.mod index db57aa40f..35348d131 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,6 @@ require ( entgo.io/ent v0.11.3 github.com/AlecAivazis/survey/v2 v2.2.7 github.com/Microsoft/go-winio v0.5.2 // indirect - github.com/crowdsecurity/dlog v0.0.0-20170105205344-4fb5f8204f26 github.com/alexliesenfeld/health v0.5.1 github.com/antonmedv/expr v1.9.0 github.com/appleboy/gin-jwt/v2 v2.8.0 @@ -15,6 +14,7 @@ require ( github.com/c-robinson/iplib v1.0.3 github.com/confluentinc/bincover v0.2.0 github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf + github.com/crowdsecurity/dlog v0.0.0-20170105205344-4fb5f8204f26 github.com/crowdsecurity/grokky v0.1.0 github.com/crowdsecurity/machineid v1.0.2 github.com/davecgh/go-spew v1.1.1 @@ -83,8 +83,8 @@ require ( github.com/PuerkitoBio/purell v1.1.1 // indirect github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect github.com/agext/levenshtein v1.2.3 // indirect - github.com/crowdsecurity/dlog v0.0.0-20170105205344-4fb5f8204f26 // indirect github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect + github.com/aquasecurity/table v1.8.0 // indirect github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.1.2 // indirect @@ -165,7 +165,7 @@ require ( go.mongodb.org/mongo-driver v1.9.0 // indirect golang.org/x/net v0.0.0-20220706163947-c90051bbdb60 // indirect golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 // indirect - golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect + golang.org/x/term v0.0.0-20220526004731-065cf7ba2467 // indirect golang.org/x/text v0.3.7 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4 // indirect diff --git a/go.sum b/go.sum index 446ac16de..dcb3a4222 100644 --- a/go.sum +++ b/go.sum @@ -88,6 +88,8 @@ github.com/appleboy/gin-jwt/v2 v2.8.0 h1:Glo7cb9eBR+hj8Y7WzgfkOlqCaNLjP+RV4dNO3f github.com/appleboy/gin-jwt/v2 v2.8.0/go.mod h1:KsK7E8HTvRg3vOiumTsr/ntNTHbZ3IbHLe4Eto31p7k= github.com/appleboy/gofight/v2 v2.1.2 h1:VOy3jow4vIK8BRQJoC/I9muxyYlJ2yb9ht2hZoS3rf4= github.com/appleboy/gofight/v2 v2.1.2/go.mod h1:frW+U1QZEdDgixycTj4CygQ48yLTUhplt43+Wczp3rw= +github.com/aquasecurity/table v1.8.0 h1:9ntpSwrUfjrM6/YviArlx/ZBGd6ix8W+MtojQcM7tv0= +github.com/aquasecurity/table v1.8.0/go.mod h1:eqOmvjjB7AhXFgFqpJUEE/ietg7RrMSJZXyTN8E/wZw= github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg= @@ -140,6 +142,8 @@ github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7Do github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw= github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/crowdsecurity/dlog v0.0.0-20170105205344-4fb5f8204f26 h1:r97WNVC30Uen+7WnLs4xDScS/Ex988+id2k6mDf8psU= +github.com/crowdsecurity/dlog v0.0.0-20170105205344-4fb5f8204f26/go.mod h1:zpv7r+7KXwgVUZnUNjyP22zc/D7LKjyoY02weH2RBbk= github.com/crowdsecurity/grokky v0.0.0-20220120093523-d5b3478363fa h1:pcHZgbBbIkNDO1cAgipEgaGeFJ0se+FOPvq6A4d/g9c= github.com/crowdsecurity/grokky v0.0.0-20220120093523-d5b3478363fa/go.mod h1:fx5UYUYAFIrOUNAkFCUOM2wJcsp9EWSQE9R0/9kaFJg= github.com/crowdsecurity/grokky v0.1.0 h1:jLUzZd3vKxYrM4hQ8n5HWLfvs5ag4UP08eT9OTekI4U= @@ -997,6 +1001,8 @@ golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXR golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.0.0-20220526004731-065cf7ba2467 h1:CBpWXWQpIRjzmkkA+M7q9Fqnwd2mZr3AFqexg8YTfoM= +golang.org/x/term v0.0.0-20220526004731-065cf7ba2467/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1207,5 +1213,3 @@ honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9 rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= -github.com/crowdsecurity/dlog v0.0.0-20170105205344-4fb5f8204f26 h1:r97WNVC30Uen+7WnLs4xDScS/Ex988+id2k6mDf8psU= -github.com/crowdsecurity/dlog v0.0.0-20170105205344-4fb5f8204f26/go.mod h1:zpv7r+7KXwgVUZnUNjyP22zc/D7LKjyoY02weH2RBbk= \ No newline at end of file diff --git a/pkg/csconfig/config.go b/pkg/csconfig/config.go index 0e08363a4..0da2cf9a8 100644 --- a/pkg/csconfig/config.go +++ b/pkg/csconfig/config.go @@ -5,11 +5,12 @@ import ( "os" "path/filepath" - "github.com/crowdsecurity/crowdsec/pkg/types" - "github.com/crowdsecurity/crowdsec/pkg/yamlpatch" "github.com/pkg/errors" log "github.com/sirupsen/logrus" "gopkg.in/yaml.v2" + + "github.com/crowdsecurity/crowdsec/pkg/types" + "github.com/crowdsecurity/crowdsec/pkg/yamlpatch" ) // defaultConfigDir is the base path to all configuration files, to be overridden in the Makefile */ @@ -94,6 +95,7 @@ func NewDefaultConfig() *Config { cscliCfg := CscliCfg{ Output: "human", + Color: ColorDefault(), } apiCfg := APICfg{ diff --git a/pkg/csconfig/cscli.go b/pkg/csconfig/cscli.go index 77b135614..338a1b631 100644 --- a/pkg/csconfig/cscli.go +++ b/pkg/csconfig/cscli.go @@ -1,8 +1,13 @@ package csconfig +import ( + "runtime" +) + /*cscli specific config, such as hub directory*/ type CscliCfg struct { Output string `yaml:"output,omitempty"` + Color string `yaml:"color,omitempty"` HubBranch string `yaml:"hub_branch"` SimulationConfig *SimulationConfig `yaml:"-"` DbConfig *DatabaseCfg `yaml:"-"` @@ -14,6 +19,13 @@ type CscliCfg struct { PrometheusUrl string `yaml:"prometheus_uri"` } +func ColorDefault() string { + if runtime.GOOS == "windows" { + return "no" + } + return "auto" +} + func (c *Config) LoadCSCLI() error { if c.Cscli == nil { c.Cscli = &CscliCfg{} diff --git a/pkg/leakybucket/bucket.go b/pkg/leakybucket/bucket.go index 4f6132351..d9fcc168d 100644 --- a/pkg/leakybucket/bucket.go +++ b/pkg/leakybucket/bucket.go @@ -24,7 +24,7 @@ const ( TIMEMACHINE ) -//Leaky represents one instance of a bucket +// Leaky represents one instance of a bucket type Leaky struct { Name string Mode int //LIVE or TIMEMACHINE diff --git a/pkg/types/utils.go b/pkg/types/utils.go index 23738c2f6..c460bc7e7 100644 --- a/pkg/types/utils.go +++ b/pkg/types/utils.go @@ -8,14 +8,16 @@ import ( "io" "os" "path/filepath" + "regexp" "runtime/debug" "strconv" "strings" "time" - "github.com/crowdsecurity/crowdsec/pkg/cwversion" log "github.com/sirupsen/logrus" "gopkg.in/natefinch/lumberjack.v2" + + "github.com/crowdsecurity/crowdsec/pkg/cwversion" ) var logFormatter log.Formatter @@ -257,3 +259,11 @@ func GetLineCountForFile(filepath string) int { } return lc } + +// from https://github.com/acarl005/stripansi +var reStripAnsi = regexp.MustCompile("[\u001B\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[a-zA-Z\\d]*)*)?\u0007)|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PRZcf-ntqry=><~]))") + +func StripAnsiString(str string) string { + // the byte version doesn't strip correctly + return reStripAnsi.ReplaceAllString(str, "") +} diff --git a/tests/bats/01_base.bats b/tests/bats/01_base.bats index 4e1ddd9e3..bdcbaa3a7 100644 --- a/tests/bats/01_base.bats +++ b/tests/bats/01_base.bats @@ -213,7 +213,7 @@ declare stderr @test "cscli metrics" { run -0 cscli lapi status run -0 --separate-stderr cscli metrics - assert_output --partial "ROUTE" + assert_output --partial "Route" assert_output --partial '/v1/watchers/login' assert_output --partial "Local Api Metrics:" diff --git a/tests/bats/04_nocapi.bats b/tests/bats/04_nocapi.bats index f3ef5c44a..d77c64b49 100644 --- a/tests/bats/04_nocapi.bats +++ b/tests/bats/04_nocapi.bats @@ -75,8 +75,7 @@ teardown() { ./instance-crowdsec start run -0 cscli lapi status run -0 --separate-stderr cscli metrics - assert_output --partial "ROUTE" + assert_output --partial "Route" assert_output --partial '/v1/watchers/login' assert_output --partial "Local Api Metrics:" - } diff --git a/tests/bats/80_alerts.bats b/tests/bats/80_alerts.bats index d1d0f3f73..ea7e0fcaa 100644 --- a/tests/bats/80_alerts.bats +++ b/tests/bats/80_alerts.bats @@ -28,27 +28,28 @@ teardown() { run -0 cscli decisions add -i 10.20.30.40 -t ban run -0 cscli alerts list - refute_output --partial 'MACHINE' + refute_output --partial 'machine' # machine name appears quoted in the "REASON" column - assert_output --regexp "\| 'githubciXXXXXXXXXXXXXXXXXXXXXXXX([a-zA-Z0-9]{16})?' \|" - refute_output --regexp "\| githubciXXXXXXXXXXXXXXXXXXXXXXXX([a-zA-Z0-9]{16})? \|" + assert_output --regexp " 'githubciXXXXXXXXXXXXXXXXXXXXXXXX([a-zA-Z0-9]{16})?' " + refute_output --regexp " githubciXXXXXXXXXXXXXXXXXXXXXXXX([a-zA-Z0-9]{16})? " run -0 cscli alerts list -m - assert_output --partial 'MACHINE' - assert_output --regexp "\| 'githubciXXXXXXXXXXXXXXXXXXXXXXXX([a-zA-Z0-9]{16})?' \|" - assert_output --regexp "\| githubciXXXXXXXXXXXXXXXXXXXXXXXX([a-zA-Z0-9]{16})? \|" + assert_output --partial 'machine' + assert_output --regexp " 'githubciXXXXXXXXXXXXXXXXXXXXXXXX([a-zA-Z0-9]{16})?' " + assert_output --regexp " githubciXXXXXXXXXXXXXXXXXXXXXXXX([a-zA-Z0-9]{16})? " run -0 cscli alerts list --machine - assert_output --partial 'MACHINE' - assert_output --regexp "\| 'githubciXXXXXXXXXXXXXXXXXXXXXXXX([a-zA-Z0-9]{16})?' \|" - assert_output --regexp "\| githubciXXXXXXXXXXXXXXXXXXXXXXXX([a-zA-Z0-9]{16})? \|" + assert_output --partial 'machine' + assert_output --regexp " 'githubciXXXXXXXXXXXXXXXXXXXXXXXX([a-zA-Z0-9]{16})?' " + assert_output --regexp " githubciXXXXXXXXXXXXXXXXXXXXXXXX([a-zA-Z0-9]{16})? " } @test "cscli alerts list, human/json/raw" { run -0 cscli decisions add -i 10.20.30.40 -t ban run -0 cscli alerts list -o human - assert_output --regexp ".* ID .* VALUE .* REASON .* COUNTRY .* AS .* DECISIONS .* CREATED AT .*" + run -0 plaintext < <(output) + assert_output --regexp ".* ID .* value .* reason .* country .* as .* decisions .* created_at .*" assert_output --regexp ".*Ip:10.20.30.40.*manual 'ban' from.*ban:1.*" run -0 cscli alerts list -o json @@ -72,6 +73,7 @@ teardown() { ALERT_ID="${output}" run -0 cscli alerts inspect "${ALERT_ID}" -o human + run -0 plaintext < <(output) assert_line --regexp '^#+$' assert_line --regexp "^ - ID *: ${ALERT_ID}$" assert_line --regexp "^ - Date *: .*$" @@ -85,7 +87,7 @@ teardown() { assert_line --regexp "^ - Begin *: .*$" assert_line --regexp "^ - End *: .*$" assert_line --regexp "^ - Active Decisions *:$" - assert_line --regexp "^.* ID .* SCOPE:VALUE .* ACTION .* EXPIRATION .* CREATED AT .*$" + assert_line --regexp "^.* ID .* scope:value .* action .* expiration .* created_at .*$" assert_line --regexp "^.* Ip:10.20.30.40 .* ban .*$" run -0 cscli alerts inspect "${ALERT_ID}" -o human --details diff --git a/tests/bats/90_decisions.bats b/tests/bats/90_decisions.bats index 1625b59d1..bcf3ebb38 100644 --- a/tests/bats/90_decisions.bats +++ b/tests/bats/90_decisions.bats @@ -41,20 +41,20 @@ declare stderr run -0 cscli decisions add -i 10.20.30.40 -t ban run -0 cscli decisions list - refute_output --partial 'MACHINE' + refute_output --partial 'Machine' # machine name appears quoted in the "REASON" column - assert_output --regexp "\| 'githubciXXXXXXXXXXXXXXXXXXXXXXXX([a-zA-Z0-9]{16})?' \|" - refute_output --regexp "\| githubciXXXXXXXXXXXXXXXXXXXXXXXX([a-zA-Z0-9]{16})? \|" + assert_output --regexp " 'githubciXXXXXXXXXXXXXXXXXXXXXXXX([a-zA-Z0-9]{16})?' " + refute_output --regexp " githubciXXXXXXXXXXXXXXXXXXXXXXXX([a-zA-Z0-9]{16})? " run -0 cscli decisions list -m - assert_output --partial 'MACHINE' - assert_output --regexp "\| 'githubciXXXXXXXXXXXXXXXXXXXXXXXX([a-zA-Z0-9]{16})?' \|" - assert_output --regexp "\| githubciXXXXXXXXXXXXXXXXXXXXXXXX([a-zA-Z0-9]{16})? \|" + assert_output --partial 'Machine' + assert_output --regexp " 'githubciXXXXXXXXXXXXXXXXXXXXXXXX([a-zA-Z0-9]{16})?' " + assert_output --regexp " githubciXXXXXXXXXXXXXXXXXXXXXXXX([a-zA-Z0-9]{16})? " run -0 cscli decisions list --machine - assert_output --partial 'MACHINE' - assert_output --regexp "\| 'githubciXXXXXXXXXXXXXXXXXXXXXXXX([a-zA-Z0-9]{16})?' \|" - assert_output --regexp "\| githubciXXXXXXXXXXXXXXXXXXXXXXXX([a-zA-Z0-9]{16})? \|" + assert_output --partial 'Machine' + assert_output --regexp " 'githubciXXXXXXXXXXXXXXXXXXXXXXXX([a-zA-Z0-9]{16})?' " + assert_output --regexp " githubciXXXXXXXXXXXXXXXXXXXXXXXX([a-zA-Z0-9]{16})? " } @test "cscli decisions list, incorrect parameters" { diff --git a/tests/bin/generate-hub-tests b/tests/bin/generate-hub-tests index 396438ed6..21031285c 100755 --- a/tests/bin/generate-hub-tests +++ b/tests/bin/generate-hub-tests @@ -37,7 +37,7 @@ EOT echo "Generating hub tests..." -for testname in $("${CSCLI}" --crowdsec "${CROWDSEC}" --cscli "${CSCLI}" hubtest --hub "${hubdir}" list -o json | grep -v NAME | grep -v -- '-------' | awk '{print $1}'); do +for testname in $("${CSCLI}" --crowdsec "${CROWDSEC}" --cscli "${CSCLI}" hubtest --hub "${hubdir}" list -o json | jq -r '.[] | .Name'); do cat << EOT >> "${HUBTESTS_BATS}" @test "${testname}" { diff --git a/tests/lib/setup_file.sh b/tests/lib/setup_file.sh index 206545e3a..e89fc1127 100755 --- a/tests/lib/setup_file.sh +++ b/tests/lib/setup_file.sh @@ -148,6 +148,7 @@ assert_json() { } export -f assert_json +# like assert_output, but for stderr assert_stderr() { oldout="${output}" run -0 echo "${stderr}" @@ -156,6 +157,7 @@ assert_stderr() { } export -f assert_stderr +# like refute_output, but for stderr refute_stderr() { oldout="${output}" run -0 echo "${stderr}" @@ -164,6 +166,7 @@ refute_stderr() { } export -f refute_stderr +# like assert_output, but for stderr assert_stderr_line() { oldout="${output}" run -0 echo "${stderr}" @@ -172,3 +175,8 @@ assert_stderr_line() { } export -f assert_stderr_line +# remove color and style sequences from stdin +plaintext() { + sed -E 's/\x1B\[[0-9;]*[JKmsu]//g' +} +export -f plaintext