cscli: new tables, --color yes|no|auto option (#1763)
This commit is contained in:
parent
b95a67751e
commit
ddd75eae9a
38 changed files with 1055 additions and 679 deletions
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
100
cmd/crowdsec-cli/alerts_table.go
Normal file
100
cmd/crowdsec-cli/alerts_table.go
Normal file
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
|
|
31
cmd/crowdsec-cli/bouncers_table.go
Normal file
31
cmd/crowdsec-cli/bouncers_table.go
Normal file
|
@ -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()
|
||||
}
|
|
@ -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")
|
||||
|
|
|
@ -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 {
|
||||
|
|
48
cmd/crowdsec-cli/console_table.go
Normal file
48
cmd/crowdsec-cli/console_table.go
Normal file
|
@ -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()
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
46
cmd/crowdsec-cli/decisions_table.go
Normal file
46
cmd/crowdsec-cli/decisions_table.go
Normal file
|
@ -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()
|
||||
}
|
|
@ -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")
|
||||
|
|
|
@ -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 {
|
||||
|
|
80
cmd/crowdsec-cli/hubtest_table.go
Normal file
80
cmd/crowdsec-cli/hubtest_table.go
Normal file
|
@ -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()
|
||||
}
|
|
@ -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)
|
||||
|
|
31
cmd/crowdsec-cli/machines_table.go
Normal file
31
cmd/crowdsec-cli/machines_table.go
Normal file
|
@ -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()
|
||||
}
|
|
@ -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.")
|
||||
|
|
|
@ -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://<ip>:<port>/metrics)")
|
||||
|
|
272
cmd/crowdsec-cli/metrics_table.go
Normal file
272
cmd/crowdsec-cli/metrics_table.go
Normal file
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
25
cmd/crowdsec-cli/notifications_table.go
Normal file
25
cmd/crowdsec-cli/notifications_table.go
Normal file
|
@ -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()
|
||||
}
|
|
@ -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")
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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 {
|
||||
|
|
95
cmd/crowdsec-cli/tables.go
Normal file
95
cmd/crowdsec-cli/tables.go
Normal file
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
|
||||
|
|
66
cmd/crowdsec-cli/utils_table.go
Normal file
66
cmd/crowdsec-cli/utils_table.go
Normal file
|
@ -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()
|
||||
}
|
||||
}
|
6
go.mod
6
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
|
||||
|
|
8
go.sum
8
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=
|
|
@ -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{
|
||||
|
|
|
@ -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{}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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, "")
|
||||
}
|
||||
|
|
|
@ -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:"
|
||||
|
||||
|
|
|
@ -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:"
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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" {
|
||||
|
|
|
@ -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}" {
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue