mirror of
https://github.com/crowdsecurity/crowdsec.git
synced 2025-05-11 20:36:12 +02:00
command cscli [machines|bouncers] inspect (#3103)
* command cscli [machines|bouncers] inspect * lint
This commit is contained in:
parent
4635d04b22
commit
9cebcf96b4
9 changed files with 527 additions and 147 deletions
|
@ -143,6 +143,8 @@ linters-settings:
|
|||
disabled: true
|
||||
- name: struct-tag
|
||||
disabled: true
|
||||
- name: redundant-import-alias
|
||||
disabled: true
|
||||
- name: time-equal
|
||||
disabled: true
|
||||
- name: var-naming
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
|
@ -12,12 +13,16 @@ import (
|
|||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/fatih/color"
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/require"
|
||||
middlewares "github.com/crowdsecurity/crowdsec/pkg/apiserver/middlewares/v1"
|
||||
"github.com/crowdsecurity/crowdsec/pkg/database"
|
||||
"github.com/crowdsecurity/crowdsec/pkg/database/ent"
|
||||
"github.com/crowdsecurity/crowdsec/pkg/database/ent/bouncer"
|
||||
"github.com/crowdsecurity/crowdsec/pkg/emoji"
|
||||
"github.com/crowdsecurity/crowdsec/pkg/types"
|
||||
)
|
||||
|
||||
|
@ -79,13 +84,92 @@ Note: This command requires database direct access, so is intended to be run on
|
|||
cmd.AddCommand(cli.newAddCmd())
|
||||
cmd.AddCommand(cli.newDeleteCmd())
|
||||
cmd.AddCommand(cli.newPruneCmd())
|
||||
cmd.AddCommand(cli.newInspectCmd())
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (cli *cliBouncers) list() error {
|
||||
out := color.Output
|
||||
func (cli *cliBouncers) listHuman(out io.Writer, bouncers ent.Bouncers) {
|
||||
t := newLightTable(out).Writer
|
||||
t.AppendHeader(table.Row{"Name", "IP Address", "Valid", "Last API pull", "Type", "Version", "Auth Type"})
|
||||
|
||||
for _, b := range bouncers {
|
||||
revoked := emoji.CheckMark
|
||||
if b.Revoked {
|
||||
revoked = emoji.Prohibited
|
||||
}
|
||||
|
||||
lastPull := ""
|
||||
if b.LastPull != nil {
|
||||
lastPull = b.LastPull.Format(time.RFC3339)
|
||||
}
|
||||
|
||||
t.AppendRow(table.Row{b.Name, b.IPAddress, revoked, lastPull, b.Type, b.Version, b.AuthType})
|
||||
}
|
||||
|
||||
fmt.Fprintln(out, t.Render())
|
||||
}
|
||||
|
||||
// bouncerInfo contains only the data we want for inspect/list
|
||||
type bouncerInfo struct {
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
Name string `json:"name"`
|
||||
Revoked bool `json:"revoked"`
|
||||
IPAddress string `json:"ip_address"`
|
||||
Type string `json:"type"`
|
||||
Version string `json:"version"`
|
||||
LastPull *time.Time `json:"last_pull"`
|
||||
AuthType string `json:"auth_type"`
|
||||
OS string `json:"os,omitempty"`
|
||||
Featureflags []string `json:"featureflags,omitempty"`
|
||||
}
|
||||
|
||||
func newBouncerInfo(b *ent.Bouncer) bouncerInfo {
|
||||
return bouncerInfo{
|
||||
CreatedAt: b.CreatedAt,
|
||||
UpdatedAt: b.UpdatedAt,
|
||||
Name: b.Name,
|
||||
Revoked: b.Revoked,
|
||||
IPAddress: b.IPAddress,
|
||||
Type: b.Type,
|
||||
Version: b.Version,
|
||||
LastPull: b.LastPull,
|
||||
AuthType: b.AuthType,
|
||||
OS: b.GetOSNameAndVersion(),
|
||||
Featureflags: b.GetFeatureFlagList(),
|
||||
}
|
||||
}
|
||||
|
||||
func (cli *cliBouncers) listCSV(out io.Writer, bouncers ent.Bouncers) error {
|
||||
csvwriter := csv.NewWriter(out)
|
||||
|
||||
if err := csvwriter.Write([]string{"name", "ip", "revoked", "last_pull", "type", "version", "auth_type"}); err != nil {
|
||||
return fmt.Errorf("failed to write raw header: %w", err)
|
||||
}
|
||||
|
||||
for _, b := range bouncers {
|
||||
valid := "validated"
|
||||
if b.Revoked {
|
||||
valid = "pending"
|
||||
}
|
||||
|
||||
lastPull := ""
|
||||
if b.LastPull != nil {
|
||||
lastPull = b.LastPull.Format(time.RFC3339)
|
||||
}
|
||||
|
||||
if err := csvwriter.Write([]string{b.Name, b.IPAddress, valid, lastPull, b.Type, b.Version, b.AuthType}); err != nil {
|
||||
return fmt.Errorf("failed to write raw: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
csvwriter.Flush()
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
func (cli *cliBouncers) list(out io.Writer) error {
|
||||
bouncers, err := cli.db.ListBouncers()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to list bouncers: %w", err)
|
||||
|
@ -93,40 +177,23 @@ func (cli *cliBouncers) list() error {
|
|||
|
||||
switch cli.cfg().Cscli.Output {
|
||||
case "human":
|
||||
getBouncersTable(out, bouncers)
|
||||
cli.listHuman(out, bouncers)
|
||||
case "json":
|
||||
info := make([]bouncerInfo, 0, len(bouncers))
|
||||
for _, b := range bouncers {
|
||||
info = append(info, newBouncerInfo(b))
|
||||
}
|
||||
|
||||
enc := json.NewEncoder(out)
|
||||
enc.SetIndent("", " ")
|
||||
|
||||
if err := enc.Encode(bouncers); err != nil {
|
||||
return fmt.Errorf("failed to marshal: %w", err)
|
||||
if err := enc.Encode(info); err != nil {
|
||||
return errors.New("failed to marshal")
|
||||
}
|
||||
|
||||
return nil
|
||||
case "raw":
|
||||
csvwriter := csv.NewWriter(out)
|
||||
|
||||
if err := csvwriter.Write([]string{"name", "ip", "revoked", "last_pull", "type", "version", "auth_type"}); err != nil {
|
||||
return fmt.Errorf("failed to write raw header: %w", err)
|
||||
}
|
||||
|
||||
for _, b := range bouncers {
|
||||
valid := "validated"
|
||||
if b.Revoked {
|
||||
valid = "pending"
|
||||
}
|
||||
|
||||
lastPull := ""
|
||||
if b.LastPull != nil {
|
||||
lastPull = b.LastPull.Format(time.RFC3339)
|
||||
}
|
||||
|
||||
if err := csvwriter.Write([]string{b.Name, b.IPAddress, valid, lastPull, b.Type, b.Version, b.AuthType}); err != nil {
|
||||
return fmt.Errorf("failed to write raw: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
csvwriter.Flush()
|
||||
return cli.listCSV(out, bouncers)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
@ -140,7 +207,7 @@ func (cli *cliBouncers) newListCmd() *cobra.Command {
|
|||
Args: cobra.ExactArgs(0),
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(_ *cobra.Command, _ []string) error {
|
||||
return cli.list()
|
||||
return cli.list(color.Output)
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -206,13 +273,14 @@ cscli bouncers add MyBouncerName --key <random-key>`,
|
|||
return cmd
|
||||
}
|
||||
|
||||
func (cli *cliBouncers) deleteValid(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
// need to load config and db because PersistentPreRunE is not called for completions
|
||||
|
||||
// validBouncerID returns a list of bouncer IDs for command completion
|
||||
func (cli *cliBouncers) validBouncerID(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
var err error
|
||||
|
||||
cfg := cli.cfg()
|
||||
|
||||
// need to load config and db because PersistentPreRunE is not called for completions
|
||||
|
||||
if err = require.LAPI(cfg); err != nil {
|
||||
cobra.CompError("unable to list bouncers " + err.Error())
|
||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||
|
@ -261,7 +329,7 @@ func (cli *cliBouncers) newDeleteCmd() *cobra.Command {
|
|||
Args: cobra.MinimumNArgs(1),
|
||||
Aliases: []string{"remove"},
|
||||
DisableAutoGenTag: true,
|
||||
ValidArgsFunction: cli.deleteValid,
|
||||
ValidArgsFunction: cli.validBouncerID,
|
||||
RunE: func(_ *cobra.Command, args []string) error {
|
||||
return cli.delete(args)
|
||||
},
|
||||
|
@ -292,7 +360,7 @@ func (cli *cliBouncers) prune(duration time.Duration, force bool) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
getBouncersTable(color.Output, bouncers)
|
||||
cli.listHuman(color.Output, bouncers)
|
||||
|
||||
if !force {
|
||||
if yes, err := askYesNo(
|
||||
|
@ -341,3 +409,84 @@ cscli bouncers prune -d 45m --force`,
|
|||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (cli *cliBouncers) inspectHuman(out io.Writer, bouncer *ent.Bouncer) {
|
||||
t := newTable(out).Writer
|
||||
|
||||
t.SetTitle("Bouncer: " + bouncer.Name)
|
||||
|
||||
t.SetColumnConfigs([]table.ColumnConfig{
|
||||
{Number: 1, AutoMerge: true},
|
||||
})
|
||||
|
||||
lastPull := ""
|
||||
if bouncer.LastPull != nil {
|
||||
lastPull = bouncer.LastPull.String()
|
||||
}
|
||||
|
||||
t.AppendRows([]table.Row{
|
||||
{"Created At", bouncer.CreatedAt},
|
||||
{"Last Update", bouncer.UpdatedAt},
|
||||
{"Revoked?", bouncer.Revoked},
|
||||
{"IP Address", bouncer.IPAddress},
|
||||
{"Type", bouncer.Type},
|
||||
{"Version", bouncer.Version},
|
||||
{"Last Pull", lastPull},
|
||||
{"Auth type", bouncer.AuthType},
|
||||
{"OS", bouncer.GetOSNameAndVersion()},
|
||||
})
|
||||
|
||||
for _, ff := range bouncer.GetFeatureFlagList() {
|
||||
t.AppendRow(table.Row{"Feature Flags", ff})
|
||||
}
|
||||
|
||||
fmt.Fprintln(out, t.Render())
|
||||
}
|
||||
|
||||
func (cli *cliBouncers) inspect(bouncer *ent.Bouncer) error {
|
||||
out := color.Output
|
||||
outputFormat := cli.cfg().Cscli.Output
|
||||
|
||||
switch outputFormat {
|
||||
case "human":
|
||||
cli.inspectHuman(out, bouncer)
|
||||
case "json":
|
||||
enc := json.NewEncoder(out)
|
||||
enc.SetIndent("", " ")
|
||||
|
||||
if err := enc.Encode(newBouncerInfo(bouncer)); err != nil {
|
||||
return errors.New("failed to marshal")
|
||||
}
|
||||
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("output format '%s' not supported for this command", outputFormat)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
func (cli *cliBouncers) newInspectCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "inspect [bouncer_name]",
|
||||
Short: "inspect a bouncer by name",
|
||||
Example: `cscli bouncers inspect "bouncer1"`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
DisableAutoGenTag: true,
|
||||
ValidArgsFunction: cli.validBouncerID,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
bouncerName := args[0]
|
||||
|
||||
b, err := cli.db.Ent.Bouncer.Query().
|
||||
Where(bouncer.Name(bouncerName)).
|
||||
Only(cmd.Context())
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to read bouncer data '%s': %w", bouncerName, err)
|
||||
}
|
||||
|
||||
return cli.inspect(b)
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
|
|
@ -1,33 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/table"
|
||||
"github.com/crowdsecurity/crowdsec/pkg/database/ent"
|
||||
"github.com/crowdsecurity/crowdsec/pkg/emoji"
|
||||
)
|
||||
|
||||
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 {
|
||||
revoked := emoji.CheckMark
|
||||
if b.Revoked {
|
||||
revoked = emoji.Prohibited
|
||||
}
|
||||
|
||||
lastPull := ""
|
||||
if b.LastPull != nil {
|
||||
lastPull = b.LastPull.Format(time.RFC3339)
|
||||
}
|
||||
|
||||
t.AddRow(b.Name, b.IPAddress, revoked, lastPull, b.Type, b.Version, b.AuthType)
|
||||
}
|
||||
|
||||
t.Render()
|
||||
}
|
|
@ -6,6 +6,7 @@ import (
|
|||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/big"
|
||||
"os"
|
||||
"slices"
|
||||
|
@ -16,6 +17,7 @@ import (
|
|||
"github.com/fatih/color"
|
||||
"github.com/go-openapi/strfmt"
|
||||
"github.com/google/uuid"
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
@ -24,8 +26,10 @@ import (
|
|||
|
||||
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/require"
|
||||
"github.com/crowdsecurity/crowdsec/pkg/csconfig"
|
||||
"github.com/crowdsecurity/crowdsec/pkg/cwhub"
|
||||
"github.com/crowdsecurity/crowdsec/pkg/database"
|
||||
"github.com/crowdsecurity/crowdsec/pkg/database/ent"
|
||||
"github.com/crowdsecurity/crowdsec/pkg/emoji"
|
||||
"github.com/crowdsecurity/crowdsec/pkg/types"
|
||||
)
|
||||
|
||||
|
@ -147,13 +151,126 @@ Note: This command requires database direct access, so is intended to be run on
|
|||
cmd.AddCommand(cli.newDeleteCmd())
|
||||
cmd.AddCommand(cli.newValidateCmd())
|
||||
cmd.AddCommand(cli.newPruneCmd())
|
||||
cmd.AddCommand(cli.newInspectCmd())
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (cli *cliMachines) list() error {
|
||||
out := color.Output
|
||||
func (*cliMachines) inspectHubHuman(out io.Writer, machine *ent.Machine) {
|
||||
state := machine.Hubstate
|
||||
|
||||
if len(state) == 0 {
|
||||
fmt.Println("No hub items found for this machine")
|
||||
return
|
||||
}
|
||||
|
||||
// group state rows by type for multiple tables
|
||||
rowsByType := make(map[string][]table.Row)
|
||||
|
||||
for itemType, items := range state {
|
||||
for _, item := range items {
|
||||
if _, ok := rowsByType[itemType]; !ok {
|
||||
rowsByType[itemType] = make([]table.Row, 0)
|
||||
}
|
||||
|
||||
row := table.Row{item.Name, item.Status, item.Version}
|
||||
rowsByType[itemType] = append(rowsByType[itemType], row)
|
||||
}
|
||||
}
|
||||
|
||||
for itemType, rows := range rowsByType {
|
||||
t := newTable(out).Writer
|
||||
t.AppendHeader(table.Row{"Name", "Status", "Version"})
|
||||
t.SetTitle(itemType)
|
||||
t.AppendRows(rows)
|
||||
fmt.Fprintln(out, t.Render())
|
||||
}
|
||||
}
|
||||
|
||||
func (cli *cliMachines) listHuman(out io.Writer, machines ent.Machines) {
|
||||
t := newLightTable(out).Writer
|
||||
t.AppendHeader(table.Row{"Name", "IP Address", "Last Update", "Status", "Version", "OS", "Auth Type", "Last Heartbeat"})
|
||||
|
||||
for _, m := range machines {
|
||||
validated := emoji.Prohibited
|
||||
if m.IsValidated {
|
||||
validated = emoji.CheckMark
|
||||
}
|
||||
|
||||
hb, active := getLastHeartbeat(m)
|
||||
if !active {
|
||||
hb = emoji.Warning + " " + hb
|
||||
}
|
||||
|
||||
t.AppendRow(table.Row{m.MachineId, m.IpAddress, m.UpdatedAt.Format(time.RFC3339), validated, m.Version, m.GetOSNameAndVersion(), m.AuthType, hb})
|
||||
}
|
||||
|
||||
fmt.Fprintln(out, t.Render())
|
||||
}
|
||||
|
||||
// machineInfo contains only the data we want for inspect/list: no hub status, scenarios, edges, etc.
|
||||
type machineInfo struct {
|
||||
CreatedAt time.Time `json:"created_at,omitempty"`
|
||||
UpdatedAt time.Time `json:"updated_at,omitempty"`
|
||||
LastPush *time.Time `json:"last_push,omitempty"`
|
||||
LastHeartbeat *time.Time `json:"last_heartbeat,omitempty"`
|
||||
MachineId string `json:"machineId,omitempty"`
|
||||
IpAddress string `json:"ipAddress,omitempty"`
|
||||
Version string `json:"version,omitempty"`
|
||||
IsValidated bool `json:"isValidated,omitempty"`
|
||||
AuthType string `json:"auth_type"`
|
||||
OS string `json:"os,omitempty"`
|
||||
Featureflags []string `json:"featureflags,omitempty"`
|
||||
Datasources map[string]int64 `json:"datasources,omitempty"`
|
||||
}
|
||||
|
||||
func newMachineInfo(m *ent.Machine) machineInfo {
|
||||
return machineInfo{
|
||||
CreatedAt: m.CreatedAt,
|
||||
UpdatedAt: m.UpdatedAt,
|
||||
LastPush: m.LastPush,
|
||||
LastHeartbeat: m.LastHeartbeat,
|
||||
MachineId: m.MachineId,
|
||||
IpAddress: m.IpAddress,
|
||||
Version: m.Version,
|
||||
IsValidated: m.IsValidated,
|
||||
AuthType: m.AuthType,
|
||||
OS: m.GetOSNameAndVersion(),
|
||||
Featureflags: m.GetFeatureFlagList(),
|
||||
Datasources: m.Datasources,
|
||||
}
|
||||
}
|
||||
|
||||
func (cli *cliMachines) listCSV(out io.Writer, machines ent.Machines) error {
|
||||
csvwriter := csv.NewWriter(out)
|
||||
|
||||
err := csvwriter.Write([]string{"machine_id", "ip_address", "updated_at", "validated", "version", "auth_type", "last_heartbeat", "os"})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to write header: %w", err)
|
||||
}
|
||||
|
||||
for _, m := range machines {
|
||||
validated := "false"
|
||||
if m.IsValidated {
|
||||
validated = "true"
|
||||
}
|
||||
|
||||
hb := "-"
|
||||
if m.LastHeartbeat != nil {
|
||||
hb = m.LastHeartbeat.Format(time.RFC3339)
|
||||
}
|
||||
|
||||
if err := csvwriter.Write([]string{m.MachineId, m.IpAddress, m.UpdatedAt.Format(time.RFC3339), validated, m.Version, m.AuthType, hb, fmt.Sprintf("%s/%s", m.Osname, m.Osversion)}); err != nil {
|
||||
return fmt.Errorf("failed to write raw output: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
csvwriter.Flush()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cli *cliMachines) list(out io.Writer) error {
|
||||
machines, err := cli.db.ListMachines()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to list machines: %w", err)
|
||||
|
@ -161,38 +278,23 @@ func (cli *cliMachines) list() error {
|
|||
|
||||
switch cli.cfg().Cscli.Output {
|
||||
case "human":
|
||||
getAgentsTable(out, machines)
|
||||
cli.listHuman(out, machines)
|
||||
case "json":
|
||||
info := make([]machineInfo, 0, len(machines))
|
||||
for _, m := range machines {
|
||||
info = append(info, newMachineInfo(m))
|
||||
}
|
||||
|
||||
enc := json.NewEncoder(out)
|
||||
enc.SetIndent("", " ")
|
||||
|
||||
if err := enc.Encode(machines); err != nil {
|
||||
if err := enc.Encode(info); err != nil {
|
||||
return errors.New("failed to marshal")
|
||||
}
|
||||
|
||||
return nil
|
||||
case "raw":
|
||||
csvwriter := csv.NewWriter(out)
|
||||
|
||||
err := csvwriter.Write([]string{"machine_id", "ip_address", "updated_at", "validated", "version", "auth_type", "last_heartbeat"})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to write header: %w", err)
|
||||
}
|
||||
|
||||
for _, m := range machines {
|
||||
validated := "false"
|
||||
if m.IsValidated {
|
||||
validated = "true"
|
||||
}
|
||||
|
||||
hb, _ := getLastHeartbeat(m)
|
||||
|
||||
if err := csvwriter.Write([]string{m.MachineId, m.IpAddress, m.UpdatedAt.Format(time.RFC3339), validated, m.Version, m.AuthType, hb}); err != nil {
|
||||
return fmt.Errorf("failed to write raw output: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
csvwriter.Flush()
|
||||
return cli.listCSV(out, machines)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
@ -207,7 +309,7 @@ func (cli *cliMachines) newListCmd() *cobra.Command {
|
|||
Args: cobra.NoArgs,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(_ *cobra.Command, _ []string) error {
|
||||
return cli.list()
|
||||
return cli.list(color.Output)
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -349,13 +451,14 @@ func (cli *cliMachines) add(args []string, machinePassword string, dumpFile stri
|
|||
return nil
|
||||
}
|
||||
|
||||
func (cli *cliMachines) deleteValid(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
// need to load config and db because PersistentPreRunE is not called for completions
|
||||
|
||||
// validMachineID returns a list of machine IDs for command completion
|
||||
func (cli *cliMachines) validMachineID(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
var err error
|
||||
|
||||
cfg := cli.cfg()
|
||||
|
||||
// need to load config and db because PersistentPreRunE is not called for completions
|
||||
|
||||
if err = require.LAPI(cfg); err != nil {
|
||||
cobra.CompError("unable to list machines " + err.Error())
|
||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||
|
@ -405,7 +508,7 @@ func (cli *cliMachines) newDeleteCmd() *cobra.Command {
|
|||
Args: cobra.MinimumNArgs(1),
|
||||
Aliases: []string{"remove"},
|
||||
DisableAutoGenTag: true,
|
||||
ValidArgsFunction: cli.deleteValid,
|
||||
ValidArgsFunction: cli.validMachineID,
|
||||
RunE: func(_ *cobra.Command, args []string) error {
|
||||
return cli.delete(args)
|
||||
},
|
||||
|
@ -417,7 +520,7 @@ func (cli *cliMachines) newDeleteCmd() *cobra.Command {
|
|||
func (cli *cliMachines) prune(duration time.Duration, notValidOnly bool, force bool) error {
|
||||
if duration < 2*time.Minute && !notValidOnly {
|
||||
if yes, err := askYesNo(
|
||||
"The duration you provided is less than 2 minutes. " +
|
||||
"The duration you provided is less than 2 minutes. "+
|
||||
"This can break installations if the machines are only temporarily disconnected. Continue?", false); err != nil {
|
||||
return err
|
||||
} else if !yes {
|
||||
|
@ -442,11 +545,11 @@ func (cli *cliMachines) prune(duration time.Duration, notValidOnly bool, force b
|
|||
return nil
|
||||
}
|
||||
|
||||
getAgentsTable(color.Output, machines)
|
||||
cli.listHuman(color.Output, machines)
|
||||
|
||||
if !force {
|
||||
if yes, err := askYesNo(
|
||||
"You are about to PERMANENTLY remove the above machines from the database. " +
|
||||
"You are about to PERMANENTLY remove the above machines from the database. "+
|
||||
"These will NOT be recoverable. Continue?", false); err != nil {
|
||||
return err
|
||||
} else if !yes {
|
||||
|
@ -460,7 +563,7 @@ func (cli *cliMachines) prune(duration time.Duration, notValidOnly bool, force b
|
|||
return fmt.Errorf("unable to prune machines: %w", err)
|
||||
}
|
||||
|
||||
fmt.Fprintf(os.Stderr, "successfully delete %d machines\n", deleted)
|
||||
fmt.Fprintf(os.Stderr, "successfully deleted %d machines\n", deleted)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -521,3 +624,136 @@ func (cli *cliMachines) newValidateCmd() *cobra.Command {
|
|||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (*cliMachines) inspectHuman(out io.Writer, machine *ent.Machine) {
|
||||
t := newTable(out).Writer
|
||||
|
||||
t.SetTitle("Machine: " + machine.MachineId)
|
||||
|
||||
t.SetColumnConfigs([]table.ColumnConfig{
|
||||
{Number: 1, AutoMerge: true},
|
||||
})
|
||||
|
||||
t.AppendRows([]table.Row{
|
||||
{"IP Address", machine.IpAddress},
|
||||
{"Created At", machine.CreatedAt},
|
||||
{"Last Update", machine.UpdatedAt},
|
||||
{"Last Heartbeat", machine.LastHeartbeat},
|
||||
{"Validated?", machine.IsValidated},
|
||||
{"CrowdSec version", machine.Version},
|
||||
{"OS", machine.GetOSNameAndVersion()},
|
||||
{"Auth type", machine.AuthType},
|
||||
})
|
||||
|
||||
for dsName, dsCount := range machine.Datasources {
|
||||
t.AppendRow(table.Row{"Datasources", fmt.Sprintf("%s: %d", dsName, dsCount)})
|
||||
}
|
||||
|
||||
for _, ff := range machine.GetFeatureFlagList() {
|
||||
t.AppendRow(table.Row{"Feature Flags", ff})
|
||||
}
|
||||
|
||||
for _, coll := range machine.Hubstate[cwhub.COLLECTIONS] {
|
||||
t.AppendRow(table.Row{"Collections", coll.Name})
|
||||
}
|
||||
|
||||
fmt.Fprintln(out, t.Render())
|
||||
}
|
||||
|
||||
func (cli *cliMachines) inspect(machine *ent.Machine) error {
|
||||
out := color.Output
|
||||
outputFormat := cli.cfg().Cscli.Output
|
||||
|
||||
switch outputFormat {
|
||||
case "human":
|
||||
cli.inspectHuman(out, machine)
|
||||
case "json":
|
||||
enc := json.NewEncoder(out)
|
||||
enc.SetIndent("", " ")
|
||||
|
||||
if err := enc.Encode(newMachineInfo(machine)); err != nil {
|
||||
return errors.New("failed to marshal")
|
||||
}
|
||||
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("output format '%s' not supported for this command", outputFormat)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cli *cliMachines) inspectHub(machine *ent.Machine) error {
|
||||
out := color.Output
|
||||
|
||||
switch cli.cfg().Cscli.Output {
|
||||
case "human":
|
||||
cli.inspectHubHuman(out, machine)
|
||||
case "json":
|
||||
enc := json.NewEncoder(out)
|
||||
enc.SetIndent("", " ")
|
||||
|
||||
if err := enc.Encode(machine.Hubstate); err != nil {
|
||||
return errors.New("failed to marshal")
|
||||
}
|
||||
|
||||
return nil
|
||||
case "raw":
|
||||
csvwriter := csv.NewWriter(out)
|
||||
|
||||
err := csvwriter.Write([]string{"type", "name", "status", "version"})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to write header: %w", err)
|
||||
}
|
||||
|
||||
rows := make([][]string, 0)
|
||||
|
||||
for itemType, items := range machine.Hubstate {
|
||||
for _, item := range items {
|
||||
rows = append(rows, []string{itemType, item.Name, item.Status, item.Version})
|
||||
}
|
||||
}
|
||||
|
||||
for _, row := range rows {
|
||||
if err := csvwriter.Write(row); err != nil {
|
||||
return fmt.Errorf("failed to write raw output: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
csvwriter.Flush()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cli *cliMachines) newInspectCmd() *cobra.Command {
|
||||
var showHub bool
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "inspect [machine_name]",
|
||||
Short: "inspect a machine by name",
|
||||
Example: `cscli machines inspect "machine1"`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
DisableAutoGenTag: true,
|
||||
ValidArgsFunction: cli.validMachineID,
|
||||
RunE: func(_ *cobra.Command, args []string) error {
|
||||
machineID := args[0]
|
||||
machine, err := cli.db.QueryMachineByID(machineID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to read machine data '%s': %w", machineID, err)
|
||||
}
|
||||
|
||||
if showHub {
|
||||
return cli.inspectHub(machine)
|
||||
}
|
||||
|
||||
return cli.inspect(machine)
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
|
||||
flags.BoolVarP(&showHub, "hub", "H", false, "show hub state")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
|
|
@ -1,33 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/table"
|
||||
"github.com/crowdsecurity/crowdsec/pkg/database/ent"
|
||||
"github.com/crowdsecurity/crowdsec/pkg/emoji"
|
||||
)
|
||||
|
||||
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 {
|
||||
validated := emoji.Prohibited
|
||||
if m.IsValidated {
|
||||
validated = emoji.CheckMark
|
||||
}
|
||||
|
||||
hb, active := getLastHeartbeat(m)
|
||||
if !active {
|
||||
hb = emoji.Warning + " " + hb
|
||||
}
|
||||
|
||||
t.AddRow(m.MachineId, m.IpAddress, m.UpdatedAt.Format(time.RFC3339), validated, m.Version, m.AuthType, hb)
|
||||
}
|
||||
|
||||
t.Render()
|
||||
}
|
|
@ -193,12 +193,9 @@ func (cli *cliSupport) dumpBouncers(zw *zip.Writer, db *database.Client) error {
|
|||
|
||||
out := new(bytes.Buffer)
|
||||
|
||||
bouncers, err := db.ListBouncers()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to list bouncers: %w", err)
|
||||
}
|
||||
|
||||
getBouncersTable(out, bouncers)
|
||||
// call the "cscli bouncers list" command directly, skip any preRun
|
||||
cm := cliBouncers{db: db, cfg: cli.cfg}
|
||||
cm.list(out)
|
||||
|
||||
stripped := stripAnsiString(out.String())
|
||||
|
||||
|
@ -216,12 +213,9 @@ func (cli *cliSupport) dumpAgents(zw *zip.Writer, db *database.Client) error {
|
|||
|
||||
out := new(bytes.Buffer)
|
||||
|
||||
machines, err := db.ListMachines()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to list machines: %w", err)
|
||||
}
|
||||
|
||||
getAgentsTable(out, machines)
|
||||
// call the "cscli machines list" command directly, skip any preRun
|
||||
cm := cliMachines{db: db, cfg: cli.cfg}
|
||||
cm.list(out)
|
||||
|
||||
stripped := stripAnsiString(out.String())
|
||||
|
||||
|
@ -617,6 +611,10 @@ cscli support dump -f /tmp/crowdsec-support.zip
|
|||
Args: cobra.NoArgs,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||
output := cli.cfg().Cscli.Output
|
||||
if output != "human" {
|
||||
return fmt.Errorf("output format %s not supported for this command", output)
|
||||
}
|
||||
return cli.dump(cmd.Context(), outFile)
|
||||
},
|
||||
}
|
||||
|
|
55
pkg/database/ent/helpers.go
Normal file
55
pkg/database/ent/helpers.go
Normal file
|
@ -0,0 +1,55 @@
|
|||
package ent
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (m *Machine) GetOSNameAndVersion() string {
|
||||
ret := m.Osname
|
||||
if m.Osversion != "" {
|
||||
if ret != "" {
|
||||
ret += "/"
|
||||
}
|
||||
|
||||
ret += m.Osversion
|
||||
}
|
||||
|
||||
if ret == "" {
|
||||
return "?"
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func (b *Bouncer) GetOSNameAndVersion() string {
|
||||
ret := b.Osname
|
||||
if b.Osversion != "" {
|
||||
if ret != "" {
|
||||
ret += "/"
|
||||
}
|
||||
|
||||
ret += b.Osversion
|
||||
}
|
||||
|
||||
if ret == "" {
|
||||
return "?"
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func (m *Machine) GetFeatureFlagList() []string {
|
||||
if m.Featureflags == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
return strings.Split(m.Featureflags, ",")
|
||||
}
|
||||
|
||||
func (b *Bouncer) GetFeatureFlagList() []string {
|
||||
if b.Featureflags == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
return strings.Split(b.Featureflags, ",")
|
||||
}
|
|
@ -10,6 +10,7 @@ import (
|
|||
|
||||
// ItemState is defined here instead of using pkg/models/HubItem to avoid introducing a dependency
|
||||
type ItemState struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
Version string `json:"version,omitempty"`
|
||||
}
|
||||
|
|
|
@ -62,7 +62,7 @@ teardown() {
|
|||
assert_output 1
|
||||
}
|
||||
|
||||
@test "machines delete has autocompletion" {
|
||||
@test "machines [delete|inspect] has autocompletion" {
|
||||
rune -0 cscli machines add -a -f /dev/null foo1
|
||||
rune -0 cscli machines add -a -f /dev/null foo2
|
||||
rune -0 cscli machines add -a -f /dev/null bar
|
||||
|
@ -72,6 +72,11 @@ teardown() {
|
|||
assert_line --index 1 'foo2'
|
||||
refute_line 'bar'
|
||||
refute_line 'baz'
|
||||
rune -0 cscli __complete machines inspect 'foo'
|
||||
assert_line --index 0 'foo1'
|
||||
assert_line --index 1 'foo2'
|
||||
refute_line 'bar'
|
||||
refute_line 'baz'
|
||||
}
|
||||
|
||||
@test "heartbeat is initially null" {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue