refact "cscli notifications" (#2833)

This commit is contained in:
mmetc 2024-02-12 11:40:59 +01:00 committed by GitHub
parent bdecf38616
commit eada3739e6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 96 additions and 63 deletions

View file

@ -246,7 +246,7 @@ It is meant to allow you to manage bans, parsers/scenarios/etc, api and generall
cmd.AddCommand(NewConsoleCmd()) cmd.AddCommand(NewConsoleCmd())
cmd.AddCommand(NewCLIExplain(cli.cfg).NewCommand()) cmd.AddCommand(NewCLIExplain(cli.cfg).NewCommand())
cmd.AddCommand(NewCLIHubTest().NewCommand()) cmd.AddCommand(NewCLIHubTest().NewCommand())
cmd.AddCommand(NewCLINotifications().NewCommand()) cmd.AddCommand(NewCLINotifications(cli.cfg).NewCommand())
cmd.AddCommand(NewCLISupport().NewCommand()) cmd.AddCommand(NewCLISupport().NewCommand())
cmd.AddCommand(NewCLIPapi(cli.cfg).NewCommand()) cmd.AddCommand(NewCLIPapi(cli.cfg).NewCommand())
cmd.AddCommand(NewCLICollection().NewCommand()) cmd.AddCommand(NewCLICollection().NewCommand())

View file

@ -23,14 +23,13 @@ import (
"github.com/crowdsecurity/go-cs-lib/ptr" "github.com/crowdsecurity/go-cs-lib/ptr"
"github.com/crowdsecurity/go-cs-lib/version" "github.com/crowdsecurity/go-cs-lib/version"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/require"
"github.com/crowdsecurity/crowdsec/pkg/apiclient" "github.com/crowdsecurity/crowdsec/pkg/apiclient"
"github.com/crowdsecurity/crowdsec/pkg/csconfig" "github.com/crowdsecurity/crowdsec/pkg/csconfig"
"github.com/crowdsecurity/crowdsec/pkg/csplugin" "github.com/crowdsecurity/crowdsec/pkg/csplugin"
"github.com/crowdsecurity/crowdsec/pkg/csprofiles" "github.com/crowdsecurity/crowdsec/pkg/csprofiles"
"github.com/crowdsecurity/crowdsec/pkg/types"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/require"
"github.com/crowdsecurity/crowdsec/pkg/models" "github.com/crowdsecurity/crowdsec/pkg/models"
"github.com/crowdsecurity/crowdsec/pkg/types"
) )
type NotificationsCfg struct { type NotificationsCfg struct {
@ -39,13 +38,17 @@ type NotificationsCfg struct {
ids []uint ids []uint
} }
type cliNotifications struct{} type cliNotifications struct {
cfg configGetter
func NewCLINotifications() *cliNotifications {
return &cliNotifications{}
} }
func (cli cliNotifications) NewCommand() *cobra.Command { func NewCLINotifications(cfg configGetter) *cliNotifications {
return &cliNotifications{
cfg: cfg,
}
}
func (cli *cliNotifications) NewCommand() *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "notifications [action]", Use: "notifications [action]",
Short: "Helper for notification plugin configuration", Short: "Helper for notification plugin configuration",
@ -53,14 +56,15 @@ func (cli cliNotifications) NewCommand() *cobra.Command {
Args: cobra.MinimumNArgs(1), Args: cobra.MinimumNArgs(1),
Aliases: []string{"notifications", "notification"}, Aliases: []string{"notifications", "notification"},
DisableAutoGenTag: true, DisableAutoGenTag: true,
PersistentPreRunE: func(cmd *cobra.Command, args []string) error { PersistentPreRunE: func(_ *cobra.Command, _ []string) error {
if err := require.LAPI(csConfig); err != nil { cfg := cli.cfg()
if err := require.LAPI(cfg); err != nil {
return err return err
} }
if err := csConfig.LoadAPIClient(); err != nil { if err := cfg.LoadAPIClient(); err != nil {
return fmt.Errorf("loading api client: %w", err) return fmt.Errorf("loading api client: %w", err)
} }
if err := require.Notifications(csConfig); err != nil { if err := require.Notifications(cfg); err != nil {
return err return err
} }
@ -76,67 +80,79 @@ func (cli cliNotifications) NewCommand() *cobra.Command {
return cmd return cmd
} }
func getPluginConfigs() (map[string]csplugin.PluginConfig, error) { func (cli *cliNotifications) getPluginConfigs() (map[string]csplugin.PluginConfig, error) {
cfg := cli.cfg()
pcfgs := map[string]csplugin.PluginConfig{} pcfgs := map[string]csplugin.PluginConfig{}
wf := func(path string, info fs.FileInfo, err error) error { wf := func(path string, info fs.FileInfo, err error) error {
if info == nil { if info == nil {
return fmt.Errorf("error while traversing directory %s: %w", path, err) return fmt.Errorf("error while traversing directory %s: %w", path, err)
} }
name := filepath.Join(csConfig.ConfigPaths.NotificationDir, info.Name()) //Avoid calling info.Name() twice
name := filepath.Join(cfg.ConfigPaths.NotificationDir, info.Name()) //Avoid calling info.Name() twice
if (strings.HasSuffix(name, "yaml") || strings.HasSuffix(name, "yml")) && !(info.IsDir()) { if (strings.HasSuffix(name, "yaml") || strings.HasSuffix(name, "yml")) && !(info.IsDir()) {
ts, err := csplugin.ParsePluginConfigFile(name) ts, err := csplugin.ParsePluginConfigFile(name)
if err != nil { if err != nil {
return fmt.Errorf("loading notifification plugin configuration with %s: %w", name, err) return fmt.Errorf("loading notifification plugin configuration with %s: %w", name, err)
} }
for _, t := range ts { for _, t := range ts {
csplugin.SetRequiredFields(&t) csplugin.SetRequiredFields(&t)
pcfgs[t.Name] = t pcfgs[t.Name] = t
} }
} }
return nil return nil
} }
if err := filepath.Walk(csConfig.ConfigPaths.NotificationDir, wf); err != nil { if err := filepath.Walk(cfg.ConfigPaths.NotificationDir, wf); err != nil {
return nil, fmt.Errorf("while loading notifification plugin configuration: %w", err) return nil, fmt.Errorf("while loading notifification plugin configuration: %w", err)
} }
return pcfgs, nil return pcfgs, nil
} }
func getProfilesConfigs() (map[string]NotificationsCfg, error) { func (cli *cliNotifications) getProfilesConfigs() (map[string]NotificationsCfg, error) {
cfg := cli.cfg()
// A bit of a tricky stuf now: reconcile profiles and notification plugins // A bit of a tricky stuf now: reconcile profiles and notification plugins
pcfgs, err := getPluginConfigs() pcfgs, err := cli.getPluginConfigs()
if err != nil { if err != nil {
return nil, err return nil, err
} }
ncfgs := map[string]NotificationsCfg{} ncfgs := map[string]NotificationsCfg{}
for _, pc := range pcfgs { for _, pc := range pcfgs {
ncfgs[pc.Name] = NotificationsCfg{ ncfgs[pc.Name] = NotificationsCfg{
Config: pc, Config: pc,
} }
} }
profiles, err := csprofiles.NewProfile(csConfig.API.Server.Profiles)
profiles, err := csprofiles.NewProfile(cfg.API.Server.Profiles)
if err != nil { if err != nil {
return nil, fmt.Errorf("while extracting profiles from configuration: %w", err) return nil, fmt.Errorf("while extracting profiles from configuration: %w", err)
} }
for profileID, profile := range profiles { for profileID, profile := range profiles {
for _, notif := range profile.Cfg.Notifications { for _, notif := range profile.Cfg.Notifications {
pc, ok := pcfgs[notif] pc, ok := pcfgs[notif]
if !ok { if !ok {
return nil, fmt.Errorf("notification plugin '%s' does not exist", notif) return nil, fmt.Errorf("notification plugin '%s' does not exist", notif)
} }
tmp, ok := ncfgs[pc.Name] tmp, ok := ncfgs[pc.Name]
if !ok { if !ok {
return nil, fmt.Errorf("notification plugin '%s' does not exist", pc.Name) return nil, fmt.Errorf("notification plugin '%s' does not exist", pc.Name)
} }
tmp.Profiles = append(tmp.Profiles, profile.Cfg) tmp.Profiles = append(tmp.Profiles, profile.Cfg)
tmp.ids = append(tmp.ids, uint(profileID)) tmp.ids = append(tmp.ids, uint(profileID))
ncfgs[pc.Name] = tmp ncfgs[pc.Name] = tmp
} }
} }
return ncfgs, nil return ncfgs, nil
} }
func (cli cliNotifications) NewListCmd() *cobra.Command { func (cli *cliNotifications) NewListCmd() *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "list", Use: "list",
Short: "list active notifications plugins", Short: "list active notifications plugins",
@ -144,21 +160,22 @@ func (cli cliNotifications) NewListCmd() *cobra.Command {
Example: `cscli notifications list`, Example: `cscli notifications list`,
Args: cobra.ExactArgs(0), Args: cobra.ExactArgs(0),
DisableAutoGenTag: true, DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, arg []string) error { RunE: func(_ *cobra.Command, _ []string) error {
ncfgs, err := getProfilesConfigs() cfg := cli.cfg()
ncfgs, err := cli.getProfilesConfigs()
if err != nil { if err != nil {
return fmt.Errorf("can't build profiles configuration: %w", err) return fmt.Errorf("can't build profiles configuration: %w", err)
} }
if csConfig.Cscli.Output == "human" { if cfg.Cscli.Output == "human" {
notificationListTable(color.Output, ncfgs) notificationListTable(color.Output, ncfgs)
} else if csConfig.Cscli.Output == "json" { } else if cfg.Cscli.Output == "json" {
x, err := json.MarshalIndent(ncfgs, "", " ") x, err := json.MarshalIndent(ncfgs, "", " ")
if err != nil { if err != nil {
return fmt.Errorf("failed to marshal notification configuration: %w", err) return fmt.Errorf("failed to marshal notification configuration: %w", err)
} }
fmt.Printf("%s", string(x)) fmt.Printf("%s", string(x))
} else if csConfig.Cscli.Output == "raw" { } else if cfg.Cscli.Output == "raw" {
csvwriter := csv.NewWriter(os.Stdout) csvwriter := csv.NewWriter(os.Stdout)
err := csvwriter.Write([]string{"Name", "Type", "Profile name"}) err := csvwriter.Write([]string{"Name", "Type", "Profile name"})
if err != nil { if err != nil {
@ -176,6 +193,7 @@ func (cli cliNotifications) NewListCmd() *cobra.Command {
} }
csvwriter.Flush() csvwriter.Flush()
} }
return nil return nil
}, },
} }
@ -183,7 +201,7 @@ func (cli cliNotifications) NewListCmd() *cobra.Command {
return cmd return cmd
} }
func (cli cliNotifications) NewInspectCmd() *cobra.Command { func (cli *cliNotifications) NewInspectCmd() *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "inspect", Use: "inspect",
Short: "Inspect active notifications plugin configuration", Short: "Inspect active notifications plugin configuration",
@ -191,36 +209,32 @@ func (cli cliNotifications) NewInspectCmd() *cobra.Command {
Example: `cscli notifications inspect <plugin_name>`, Example: `cscli notifications inspect <plugin_name>`,
Args: cobra.ExactArgs(1), Args: cobra.ExactArgs(1),
DisableAutoGenTag: true, DisableAutoGenTag: true,
PreRunE: func(cmd *cobra.Command, args []string) error { RunE: func(_ *cobra.Command, args []string) error {
if args[0] == "" { cfg := cli.cfg()
return fmt.Errorf("please provide a plugin name to inspect") ncfgs, err := cli.getProfilesConfigs()
}
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
ncfgs, err := getProfilesConfigs()
if err != nil { if err != nil {
return fmt.Errorf("can't build profiles configuration: %w", err) return fmt.Errorf("can't build profiles configuration: %w", err)
} }
cfg, ok := ncfgs[args[0]] ncfg, ok := ncfgs[args[0]]
if !ok { if !ok {
return fmt.Errorf("plugin '%s' does not exist or is not active", args[0]) return fmt.Errorf("plugin '%s' does not exist or is not active", args[0])
} }
if csConfig.Cscli.Output == "human" || csConfig.Cscli.Output == "raw" { if cfg.Cscli.Output == "human" || cfg.Cscli.Output == "raw" {
fmt.Printf(" - %15s: %15s\n", "Type", cfg.Config.Type) fmt.Printf(" - %15s: %15s\n", "Type", ncfg.Config.Type)
fmt.Printf(" - %15s: %15s\n", "Name", cfg.Config.Name) fmt.Printf(" - %15s: %15s\n", "Name", ncfg.Config.Name)
fmt.Printf(" - %15s: %15s\n", "Timeout", cfg.Config.TimeOut) fmt.Printf(" - %15s: %15s\n", "Timeout", ncfg.Config.TimeOut)
fmt.Printf(" - %15s: %15s\n", "Format", cfg.Config.Format) fmt.Printf(" - %15s: %15s\n", "Format", ncfg.Config.Format)
for k, v := range cfg.Config.Config { for k, v := range ncfg.Config.Config {
fmt.Printf(" - %15s: %15v\n", k, v) fmt.Printf(" - %15s: %15v\n", k, v)
} }
} else if csConfig.Cscli.Output == "json" { } else if cfg.Cscli.Output == "json" {
x, err := json.MarshalIndent(cfg, "", " ") x, err := json.MarshalIndent(cfg, "", " ")
if err != nil { if err != nil {
return fmt.Errorf("failed to marshal notification configuration: %w", err) return fmt.Errorf("failed to marshal notification configuration: %w", err)
} }
fmt.Printf("%s", string(x)) fmt.Printf("%s", string(x))
} }
return nil return nil
}, },
} }
@ -228,12 +242,13 @@ func (cli cliNotifications) NewInspectCmd() *cobra.Command {
return cmd return cmd
} }
func (cli cliNotifications) NewTestCmd() *cobra.Command { func (cli *cliNotifications) NewTestCmd() *cobra.Command {
var ( var (
pluginBroker csplugin.PluginBroker pluginBroker csplugin.PluginBroker
pluginTomb tomb.Tomb pluginTomb tomb.Tomb
alertOverride string alertOverride string
) )
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "test [plugin name]", Use: "test [plugin name]",
Short: "send a generic test alert to notification plugin", Short: "send a generic test alert to notification plugin",
@ -241,25 +256,26 @@ func (cli cliNotifications) NewTestCmd() *cobra.Command {
Example: `cscli notifications test [plugin_name]`, Example: `cscli notifications test [plugin_name]`,
Args: cobra.ExactArgs(1), Args: cobra.ExactArgs(1),
DisableAutoGenTag: true, DisableAutoGenTag: true,
PreRunE: func(cmd *cobra.Command, args []string) error { PreRunE: func(_ *cobra.Command, args []string) error {
pconfigs, err := getPluginConfigs() cfg := cli.cfg()
pconfigs, err := cli.getPluginConfigs()
if err != nil { if err != nil {
return fmt.Errorf("can't build profiles configuration: %w", err) return fmt.Errorf("can't build profiles configuration: %w", err)
} }
cfg, ok := pconfigs[args[0]] pcfg, ok := pconfigs[args[0]]
if !ok { if !ok {
return fmt.Errorf("plugin name: '%s' does not exist", args[0]) return fmt.Errorf("plugin name: '%s' does not exist", args[0])
} }
//Create a single profile with plugin name as notification name //Create a single profile with plugin name as notification name
return pluginBroker.Init(csConfig.PluginConfig, []*csconfig.ProfileCfg{ return pluginBroker.Init(cfg.PluginConfig, []*csconfig.ProfileCfg{
{ {
Notifications: []string{ Notifications: []string{
cfg.Name, pcfg.Name,
}, },
}, },
}, csConfig.ConfigPaths) }, cfg.ConfigPaths)
}, },
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(_ *cobra.Command, _ []string) error {
pluginTomb.Go(func() error { pluginTomb.Go(func() error {
pluginBroker.Run(&pluginTomb) pluginBroker.Run(&pluginTomb)
return nil return nil
@ -298,13 +314,16 @@ func (cli cliNotifications) NewTestCmd() *cobra.Command {
if err := yaml.Unmarshal([]byte(alertOverride), alert); err != nil { if err := yaml.Unmarshal([]byte(alertOverride), alert); err != nil {
return fmt.Errorf("failed to unmarshal alert override: %w", err) return fmt.Errorf("failed to unmarshal alert override: %w", err)
} }
pluginBroker.PluginChannel <- csplugin.ProfileAlert{ pluginBroker.PluginChannel <- csplugin.ProfileAlert{
ProfileID: uint(0), ProfileID: uint(0),
Alert: alert, Alert: alert,
} }
//time.Sleep(2 * time.Second) // There's no mechanism to ensure notification has been sent //time.Sleep(2 * time.Second) // There's no mechanism to ensure notification has been sent
pluginTomb.Kill(fmt.Errorf("terminating")) pluginTomb.Kill(fmt.Errorf("terminating"))
pluginTomb.Wait() pluginTomb.Wait()
return nil return nil
}, },
} }
@ -313,9 +332,11 @@ func (cli cliNotifications) NewTestCmd() *cobra.Command {
return cmd return cmd
} }
func (cli cliNotifications) NewReinjectCmd() *cobra.Command { func (cli *cliNotifications) NewReinjectCmd() *cobra.Command {
var alertOverride string var (
var alert *models.Alert alertOverride string
alert *models.Alert
)
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "reinject", Use: "reinject",
@ -328,25 +349,30 @@ cscli notifications reinject <alert_id> -a '{"remediation": true,"scenario":"not
`, `,
Args: cobra.ExactArgs(1), Args: cobra.ExactArgs(1),
DisableAutoGenTag: true, DisableAutoGenTag: true,
PreRunE: func(cmd *cobra.Command, args []string) error { PreRunE: func(_ *cobra.Command, args []string) error {
var err error var err error
alert, err = FetchAlertFromArgString(args[0]) alert, err = cli.fetchAlertFromArgString(args[0])
if err != nil { if err != nil {
return err return err
} }
return nil return nil
}, },
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(_ *cobra.Command, _ []string) error {
var ( var (
pluginBroker csplugin.PluginBroker pluginBroker csplugin.PluginBroker
pluginTomb tomb.Tomb pluginTomb tomb.Tomb
) )
cfg := cli.cfg()
if alertOverride != "" { if alertOverride != "" {
if err := json.Unmarshal([]byte(alertOverride), alert); err != nil { if err := json.Unmarshal([]byte(alertOverride), alert); err != nil {
return fmt.Errorf("can't unmarshal data in the alert flag: %w", err) return fmt.Errorf("can't unmarshal data in the alert flag: %w", err)
} }
} }
err := pluginBroker.Init(csConfig.PluginConfig, csConfig.API.Server.Profiles, csConfig.ConfigPaths)
err := pluginBroker.Init(cfg.PluginConfig, cfg.API.Server.Profiles, cfg.ConfigPaths)
if err != nil { if err != nil {
return fmt.Errorf("can't initialize plugins: %w", err) return fmt.Errorf("can't initialize plugins: %w", err)
} }
@ -356,7 +382,7 @@ cscli notifications reinject <alert_id> -a '{"remediation": true,"scenario":"not
return nil return nil
}) })
profiles, err := csprofiles.NewProfile(csConfig.API.Server.Profiles) profiles, err := csprofiles.NewProfile(cfg.API.Server.Profiles)
if err != nil { if err != nil {
return fmt.Errorf("cannot extract profiles from configuration: %w", err) return fmt.Errorf("cannot extract profiles from configuration: %w", err)
} }
@ -382,9 +408,9 @@ cscli notifications reinject <alert_id> -a '{"remediation": true,"scenario":"not
default: default:
time.Sleep(50 * time.Millisecond) time.Sleep(50 * time.Millisecond)
log.Info("sleeping\n") log.Info("sleeping\n")
} }
} }
if profile.Cfg.OnSuccess == "break" { if profile.Cfg.OnSuccess == "break" {
log.Infof("The profile %s contains a 'on_success: break' so bailing out", profile.Cfg.Name) log.Infof("The profile %s contains a 'on_success: break' so bailing out", profile.Cfg.Name)
break break
@ -393,6 +419,7 @@ cscli notifications reinject <alert_id> -a '{"remediation": true,"scenario":"not
//time.Sleep(2 * time.Second) // There's no mechanism to ensure notification has been sent //time.Sleep(2 * time.Second) // There's no mechanism to ensure notification has been sent
pluginTomb.Kill(fmt.Errorf("terminating")) pluginTomb.Kill(fmt.Errorf("terminating"))
pluginTomb.Wait() pluginTomb.Wait()
return nil return nil
}, },
} }
@ -401,18 +428,22 @@ cscli notifications reinject <alert_id> -a '{"remediation": true,"scenario":"not
return cmd return cmd
} }
func FetchAlertFromArgString(toParse string) (*models.Alert, error) { func (cli *cliNotifications) fetchAlertFromArgString(toParse string) (*models.Alert, error) {
cfg := cli.cfg()
id, err := strconv.Atoi(toParse) id, err := strconv.Atoi(toParse)
if err != nil { if err != nil {
return nil, fmt.Errorf("bad alert id %s", toParse) return nil, fmt.Errorf("bad alert id %s", toParse)
} }
apiURL, err := url.Parse(csConfig.API.Client.Credentials.URL)
apiURL, err := url.Parse(cfg.API.Client.Credentials.URL)
if err != nil { if err != nil {
return nil, fmt.Errorf("error parsing the URL of the API: %w", err) return nil, fmt.Errorf("error parsing the URL of the API: %w", err)
} }
client, err := apiclient.NewClient(&apiclient.Config{ client, err := apiclient.NewClient(&apiclient.Config{
MachineID: csConfig.API.Client.Credentials.Login, MachineID: cfg.API.Client.Credentials.Login,
Password: strfmt.Password(csConfig.API.Client.Credentials.Password), Password: strfmt.Password(cfg.API.Client.Credentials.Password),
UserAgent: fmt.Sprintf("crowdsec/%s", version.String()), UserAgent: fmt.Sprintf("crowdsec/%s", version.String()),
URL: apiURL, URL: apiURL,
VersionPrefix: "v1", VersionPrefix: "v1",
@ -420,9 +451,11 @@ func FetchAlertFromArgString(toParse string) (*models.Alert, error) {
if err != nil { if err != nil {
return nil, fmt.Errorf("error creating the client for the API: %w", err) return nil, fmt.Errorf("error creating the client for the API: %w", err)
} }
alert, _, err := client.Alerts.GetByID(context.Background(), id) alert, _, err := client.Alerts.GetByID(context.Background(), id)
if err != nil { if err != nil {
return nil, fmt.Errorf("can't find alert with id %d: %w", id, err) return nil, fmt.Errorf("can't find alert with id %d: %w", id, err)
} }
return alert, nil return alert, nil
} }