notifications.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433
  1. package main
  2. import (
  3. "context"
  4. "encoding/csv"
  5. "encoding/json"
  6. "fmt"
  7. "io/fs"
  8. "net/url"
  9. "os"
  10. "path/filepath"
  11. "strconv"
  12. "strings"
  13. "time"
  14. "github.com/fatih/color"
  15. "github.com/go-openapi/strfmt"
  16. log "github.com/sirupsen/logrus"
  17. "github.com/spf13/cobra"
  18. "gopkg.in/tomb.v2"
  19. "gopkg.in/yaml.v3"
  20. "github.com/crowdsecurity/go-cs-lib/ptr"
  21. "github.com/crowdsecurity/go-cs-lib/version"
  22. "github.com/crowdsecurity/crowdsec/pkg/apiclient"
  23. "github.com/crowdsecurity/crowdsec/pkg/csconfig"
  24. "github.com/crowdsecurity/crowdsec/pkg/csplugin"
  25. "github.com/crowdsecurity/crowdsec/pkg/csprofiles"
  26. "github.com/crowdsecurity/crowdsec/pkg/types"
  27. "github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/require"
  28. "github.com/crowdsecurity/crowdsec/pkg/models"
  29. )
  30. type NotificationsCfg struct {
  31. Config csplugin.PluginConfig `json:"plugin_config"`
  32. Profiles []*csconfig.ProfileCfg `json:"associated_profiles"`
  33. ids []uint
  34. }
  35. func NewNotificationsCmd() *cobra.Command {
  36. var cmdNotifications = &cobra.Command{
  37. Use: "notifications [action]",
  38. Short: "Helper for notification plugin configuration",
  39. Long: "To list/inspect/test notification template",
  40. Args: cobra.MinimumNArgs(1),
  41. Aliases: []string{"notifications", "notification"},
  42. DisableAutoGenTag: true,
  43. PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
  44. if err := require.LAPI(csConfig); err != nil {
  45. return err
  46. }
  47. if err := require.Profiles(csConfig); err != nil {
  48. return err
  49. }
  50. if err := require.Notifications(csConfig); err != nil {
  51. return err
  52. }
  53. return nil
  54. },
  55. }
  56. cmdNotifications.AddCommand(NewNotificationsListCmd())
  57. cmdNotifications.AddCommand(NewNotificationsInspectCmd())
  58. cmdNotifications.AddCommand(NewNotificationsReinjectCmd())
  59. cmdNotifications.AddCommand(NewNotificationsTestCmd())
  60. return cmdNotifications
  61. }
  62. func getPluginConfigs() (map[string]csplugin.PluginConfig, error) {
  63. pcfgs := map[string]csplugin.PluginConfig{}
  64. wf := func(path string, info fs.FileInfo, err error) error {
  65. if info == nil {
  66. return fmt.Errorf("error while traversing directory %s: %w", path, err)
  67. }
  68. name := filepath.Join(csConfig.ConfigPaths.NotificationDir, info.Name()) //Avoid calling info.Name() twice
  69. if (strings.HasSuffix(name, "yaml") || strings.HasSuffix(name, "yml")) && !(info.IsDir()) {
  70. ts, err := csplugin.ParsePluginConfigFile(name)
  71. if err != nil {
  72. return fmt.Errorf("loading notifification plugin configuration with %s: %w", name, err)
  73. }
  74. for _, t := range ts {
  75. csplugin.SetRequiredFields(&t)
  76. pcfgs[t.Name] = t
  77. }
  78. }
  79. return nil
  80. }
  81. if err := filepath.Walk(csConfig.ConfigPaths.NotificationDir, wf); err != nil {
  82. return nil, fmt.Errorf("while loading notifification plugin configuration: %w", err)
  83. }
  84. return pcfgs, nil
  85. }
  86. func getProfilesConfigs() (map[string]NotificationsCfg, error) {
  87. // A bit of a tricky stuf now: reconcile profiles and notification plugins
  88. pcfgs, err := getPluginConfigs()
  89. if err != nil {
  90. return nil, err
  91. }
  92. ncfgs := map[string]NotificationsCfg{}
  93. profiles, err := csprofiles.NewProfile(csConfig.API.Server.Profiles)
  94. if err != nil {
  95. return nil, fmt.Errorf("while extracting profiles from configuration: %w", err)
  96. }
  97. for profileID, profile := range profiles {
  98. loop:
  99. for _, notif := range profile.Cfg.Notifications {
  100. for name, pc := range pcfgs {
  101. if notif == name {
  102. if _, ok := ncfgs[pc.Name]; !ok {
  103. ncfgs[pc.Name] = NotificationsCfg{
  104. Config: pc,
  105. Profiles: []*csconfig.ProfileCfg{profile.Cfg},
  106. ids: []uint{uint(profileID)},
  107. }
  108. continue loop
  109. }
  110. tmp := ncfgs[pc.Name]
  111. for _, pr := range tmp.Profiles {
  112. var profiles []*csconfig.ProfileCfg
  113. if pr.Name == profile.Cfg.Name {
  114. continue
  115. }
  116. profiles = append(tmp.Profiles, profile.Cfg)
  117. ids := append(tmp.ids, uint(profileID))
  118. ncfgs[pc.Name] = NotificationsCfg{
  119. Config: tmp.Config,
  120. Profiles: profiles,
  121. ids: ids,
  122. }
  123. }
  124. }
  125. }
  126. }
  127. }
  128. return ncfgs, nil
  129. }
  130. func NewNotificationsListCmd() *cobra.Command {
  131. var cmdNotificationsList = &cobra.Command{
  132. Use: "list",
  133. Short: "list active notifications plugins",
  134. Long: `list active notifications plugins`,
  135. Example: `cscli notifications list`,
  136. Args: cobra.ExactArgs(0),
  137. DisableAutoGenTag: true,
  138. RunE: func(cmd *cobra.Command, arg []string) error {
  139. ncfgs, err := getProfilesConfigs()
  140. if err != nil {
  141. return fmt.Errorf("can't build profiles configuration: %w", err)
  142. }
  143. if csConfig.Cscli.Output == "human" {
  144. notificationListTable(color.Output, ncfgs)
  145. } else if csConfig.Cscli.Output == "json" {
  146. x, err := json.MarshalIndent(ncfgs, "", " ")
  147. if err != nil {
  148. return fmt.Errorf("failed to marshal notification configuration: %w", err)
  149. }
  150. fmt.Printf("%s", string(x))
  151. } else if csConfig.Cscli.Output == "raw" {
  152. csvwriter := csv.NewWriter(os.Stdout)
  153. err := csvwriter.Write([]string{"Name", "Type", "Profile name"})
  154. if err != nil {
  155. return fmt.Errorf("failed to write raw header: %w", err)
  156. }
  157. for _, b := range ncfgs {
  158. profilesList := []string{}
  159. for _, p := range b.Profiles {
  160. profilesList = append(profilesList, p.Name)
  161. }
  162. err := csvwriter.Write([]string{b.Config.Name, b.Config.Type, strings.Join(profilesList, ", ")})
  163. if err != nil {
  164. return fmt.Errorf("failed to write raw content: %w", err)
  165. }
  166. }
  167. csvwriter.Flush()
  168. }
  169. return nil
  170. },
  171. }
  172. return cmdNotificationsList
  173. }
  174. func NewNotificationsInspectCmd() *cobra.Command {
  175. var cmdNotificationsInspect = &cobra.Command{
  176. Use: "inspect",
  177. Short: "Inspect active notifications plugin configuration",
  178. Long: `Inspect active notifications plugin and show configuration`,
  179. Example: `cscli notifications inspect <plugin_name>`,
  180. Args: cobra.ExactArgs(1),
  181. DisableAutoGenTag: true,
  182. PreRunE: func(cmd *cobra.Command, args []string) error {
  183. if args[0] == "" {
  184. return fmt.Errorf("please provide a plugin name to inspect")
  185. }
  186. return nil
  187. },
  188. RunE: func(cmd *cobra.Command, args []string) error {
  189. ncfgs, err := getProfilesConfigs()
  190. if err != nil {
  191. return fmt.Errorf("can't build profiles configuration: %w", err)
  192. }
  193. cfg, ok := ncfgs[args[0]]
  194. if !ok {
  195. return fmt.Errorf("plugin '%s' does not exist or is not active", args[0])
  196. }
  197. if csConfig.Cscli.Output == "human" || csConfig.Cscli.Output == "raw" {
  198. fmt.Printf(" - %15s: %15s\n", "Type", cfg.Config.Type)
  199. fmt.Printf(" - %15s: %15s\n", "Name", cfg.Config.Name)
  200. fmt.Printf(" - %15s: %15s\n", "Timeout", cfg.Config.TimeOut)
  201. fmt.Printf(" - %15s: %15s\n", "Format", cfg.Config.Format)
  202. for k, v := range cfg.Config.Config {
  203. fmt.Printf(" - %15s: %15v\n", k, v)
  204. }
  205. } else if csConfig.Cscli.Output == "json" {
  206. x, err := json.MarshalIndent(cfg, "", " ")
  207. if err != nil {
  208. return fmt.Errorf("failed to marshal notification configuration: %w", err)
  209. }
  210. fmt.Printf("%s", string(x))
  211. }
  212. return nil
  213. },
  214. }
  215. return cmdNotificationsInspect
  216. }
  217. func NewNotificationsTestCmd() *cobra.Command {
  218. var (
  219. pluginBroker csplugin.PluginBroker
  220. pluginTomb tomb.Tomb
  221. alertOverride string
  222. )
  223. var cmdNotificationsTest = &cobra.Command{
  224. Use: "test [plugin name]",
  225. Short: "send a generic test alert to notification plugin",
  226. Long: `send a generic test alert to a notification plugin to test configuration even if is not active`,
  227. Example: `cscli notifications test [plugin_name]`,
  228. Args: cobra.ExactArgs(1),
  229. DisableAutoGenTag: true,
  230. PreRunE: func(cmd *cobra.Command, args []string) error {
  231. pconfigs, err := getPluginConfigs()
  232. if err != nil {
  233. return fmt.Errorf("can't build profiles configuration: %w", err)
  234. }
  235. cfg, ok := pconfigs[args[0]]
  236. if !ok {
  237. return fmt.Errorf("plugin name: '%s' does not exist", args[0])
  238. }
  239. //Create a single profile with plugin name as notification name
  240. return pluginBroker.Init(csConfig.PluginConfig, []*csconfig.ProfileCfg{
  241. {
  242. Notifications: []string{
  243. cfg.Name,
  244. },
  245. },
  246. }, csConfig.ConfigPaths)
  247. },
  248. RunE: func(cmd *cobra.Command, args []string) error {
  249. pluginTomb.Go(func() error {
  250. pluginBroker.Run(&pluginTomb)
  251. return nil
  252. })
  253. alert := &models.Alert{
  254. Capacity: ptr.Of(int32(0)),
  255. Decisions: []*models.Decision{{
  256. Duration: ptr.Of("4h"),
  257. Scope: ptr.Of("Ip"),
  258. Value: ptr.Of("10.10.10.10"),
  259. Type: ptr.Of("ban"),
  260. Scenario: ptr.Of("test alert"),
  261. Origin: ptr.Of(types.CscliOrigin),
  262. }},
  263. Events: []*models.Event{},
  264. EventsCount: ptr.Of(int32(1)),
  265. Leakspeed: ptr.Of("0"),
  266. Message: ptr.Of("test alert"),
  267. ScenarioHash: ptr.Of(""),
  268. Scenario: ptr.Of("test alert"),
  269. ScenarioVersion: ptr.Of(""),
  270. Simulated: ptr.Of(false),
  271. Source: &models.Source{
  272. AsName: "",
  273. AsNumber: "",
  274. Cn: "",
  275. IP: "10.10.10.10",
  276. Range: "",
  277. Scope: ptr.Of("Ip"),
  278. Value: ptr.Of("10.10.10.10"),
  279. },
  280. StartAt: ptr.Of(time.Now().UTC().Format(time.RFC3339)),
  281. StopAt: ptr.Of(time.Now().UTC().Format(time.RFC3339)),
  282. CreatedAt: time.Now().UTC().Format(time.RFC3339),
  283. }
  284. if err := yaml.Unmarshal([]byte(alertOverride), alert); err != nil {
  285. return fmt.Errorf("failed to unmarshal alert override: %w", err)
  286. }
  287. pluginBroker.PluginChannel <- csplugin.ProfileAlert{
  288. ProfileID: uint(0),
  289. Alert: alert,
  290. }
  291. //time.Sleep(2 * time.Second) // There's no mechanism to ensure notification has been sent
  292. pluginTomb.Kill(fmt.Errorf("terminating"))
  293. pluginTomb.Wait()
  294. return nil
  295. },
  296. }
  297. cmdNotificationsTest.Flags().StringVarP(&alertOverride, "alert", "a", "", "JSON string used to override alert fields in the generic alert (see crowdsec/pkg/models/alert.go in the source tree for the full definition of the object)")
  298. return cmdNotificationsTest
  299. }
  300. func NewNotificationsReinjectCmd() *cobra.Command {
  301. var alertOverride string
  302. var alert *models.Alert
  303. var cmdNotificationsReinject = &cobra.Command{
  304. Use: "reinject",
  305. Short: "reinject an alert into profiles to trigger notifications",
  306. Long: `reinject an alert into profiles to be evaluated by the filter and sent to matched notifications plugins`,
  307. Example: `
  308. cscli notifications reinject <alert_id>
  309. cscli notifications reinject <alert_id> -a '{"remediation": false,"scenario":"notification/test"}'
  310. cscli notifications reinject <alert_id> -a '{"remediation": true,"scenario":"notification/test"}'
  311. `,
  312. Args: cobra.ExactArgs(1),
  313. DisableAutoGenTag: true,
  314. PreRunE: func(cmd *cobra.Command, args []string) error {
  315. var err error
  316. alert, err = FetchAlertFromArgString(args[0])
  317. if err != nil {
  318. return err
  319. }
  320. return nil
  321. },
  322. RunE: func(cmd *cobra.Command, args []string) error {
  323. var (
  324. pluginBroker csplugin.PluginBroker
  325. pluginTomb tomb.Tomb
  326. )
  327. if alertOverride != "" {
  328. if err := json.Unmarshal([]byte(alertOverride), alert); err != nil {
  329. return fmt.Errorf("can't unmarshal data in the alert flag: %w", err)
  330. }
  331. }
  332. err := pluginBroker.Init(csConfig.PluginConfig, csConfig.API.Server.Profiles, csConfig.ConfigPaths)
  333. if err != nil {
  334. return fmt.Errorf("can't initialize plugins: %w", err)
  335. }
  336. pluginTomb.Go(func() error {
  337. pluginBroker.Run(&pluginTomb)
  338. return nil
  339. })
  340. profiles, err := csprofiles.NewProfile(csConfig.API.Server.Profiles)
  341. if err != nil {
  342. return fmt.Errorf("cannot extract profiles from configuration: %w", err)
  343. }
  344. for id, profile := range profiles {
  345. _, matched, err := profile.EvaluateProfile(alert)
  346. if err != nil {
  347. return fmt.Errorf("can't evaluate profile %s: %w", profile.Cfg.Name, err)
  348. }
  349. if !matched {
  350. log.Infof("The profile %s didn't match", profile.Cfg.Name)
  351. continue
  352. }
  353. log.Infof("The profile %s matched, sending to its configured notification plugins", profile.Cfg.Name)
  354. loop:
  355. for {
  356. select {
  357. case pluginBroker.PluginChannel <- csplugin.ProfileAlert{
  358. ProfileID: uint(id),
  359. Alert: alert,
  360. }:
  361. break loop
  362. default:
  363. time.Sleep(50 * time.Millisecond)
  364. log.Info("sleeping\n")
  365. }
  366. }
  367. if profile.Cfg.OnSuccess == "break" {
  368. log.Infof("The profile %s contains a 'on_success: break' so bailing out", profile.Cfg.Name)
  369. break
  370. }
  371. }
  372. //time.Sleep(2 * time.Second) // There's no mechanism to ensure notification has been sent
  373. pluginTomb.Kill(fmt.Errorf("terminating"))
  374. pluginTomb.Wait()
  375. return nil
  376. },
  377. }
  378. cmdNotificationsReinject.Flags().StringVarP(&alertOverride, "alert", "a", "", "JSON string used to override alert fields in the reinjected alert (see crowdsec/pkg/models/alert.go in the source tree for the full definition of the object)")
  379. return cmdNotificationsReinject
  380. }
  381. func FetchAlertFromArgString(toParse string) (*models.Alert, error) {
  382. id, err := strconv.Atoi(toParse)
  383. if err != nil {
  384. return nil, fmt.Errorf("bad alert id %s", toParse)
  385. }
  386. apiURL, err := url.Parse(csConfig.API.Client.Credentials.URL)
  387. if err != nil {
  388. return nil, fmt.Errorf("error parsing the URL of the API: %w", err)
  389. }
  390. client, err := apiclient.NewClient(&apiclient.Config{
  391. MachineID: csConfig.API.Client.Credentials.Login,
  392. Password: strfmt.Password(csConfig.API.Client.Credentials.Password),
  393. UserAgent: fmt.Sprintf("crowdsec/%s", version.String()),
  394. URL: apiURL,
  395. VersionPrefix: "v1",
  396. })
  397. if err != nil {
  398. return nil, fmt.Errorf("error creating the client for the API: %w", err)
  399. }
  400. alert, _, err := client.Alerts.GetByID(context.Background(), id)
  401. if err != nil {
  402. return nil, fmt.Errorf("can't find alert with id %d: %w", id, err)
  403. }
  404. return alert, nil
  405. }