notifications.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352
  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. "github.com/crowdsecurity/go-cs-lib/version"
  20. "github.com/crowdsecurity/crowdsec/pkg/apiclient"
  21. "github.com/crowdsecurity/crowdsec/pkg/csconfig"
  22. "github.com/crowdsecurity/crowdsec/pkg/csplugin"
  23. "github.com/crowdsecurity/crowdsec/pkg/csprofiles"
  24. "github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/require"
  25. )
  26. type NotificationsCfg struct {
  27. Config csplugin.PluginConfig `json:"plugin_config"`
  28. Profiles []*csconfig.ProfileCfg `json:"associated_profiles"`
  29. ids []uint
  30. }
  31. func NewNotificationsCmd() *cobra.Command {
  32. var cmdNotifications = &cobra.Command{
  33. Use: "notifications [action]",
  34. Short: "Helper for notification plugin configuration",
  35. Long: "To list/inspect/test notification template",
  36. Args: cobra.MinimumNArgs(1),
  37. Aliases: []string{"notifications", "notification"},
  38. DisableAutoGenTag: true,
  39. PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
  40. if err := require.LAPI(csConfig); err != nil {
  41. return err
  42. }
  43. if err := require.Profiles(csConfig); err != nil {
  44. return err
  45. }
  46. if err := require.Notifications(csConfig); err != nil {
  47. return err
  48. }
  49. return nil
  50. },
  51. }
  52. cmdNotifications.AddCommand(NewNotificationsListCmd())
  53. cmdNotifications.AddCommand(NewNotificationsInspectCmd())
  54. cmdNotifications.AddCommand(NewNotificationsReinjectCmd())
  55. return cmdNotifications
  56. }
  57. func getNotificationsConfiguration() (map[string]NotificationsCfg, error) {
  58. pcfgs := map[string]csplugin.PluginConfig{}
  59. wf := func(path string, info fs.FileInfo, err error) error {
  60. if info == nil {
  61. return fmt.Errorf("error while traversing directory %s: %w", path, err)
  62. }
  63. name := filepath.Join(csConfig.ConfigPaths.NotificationDir, info.Name()) //Avoid calling info.Name() twice
  64. if (strings.HasSuffix(name, "yaml") || strings.HasSuffix(name, "yml")) && !(info.IsDir()) {
  65. ts, err := csplugin.ParsePluginConfigFile(name)
  66. if err != nil {
  67. return fmt.Errorf("loading notifification plugin configuration with %s: %w", name, err)
  68. }
  69. for _, t := range ts {
  70. pcfgs[t.Name] = t
  71. }
  72. }
  73. return nil
  74. }
  75. if err := filepath.Walk(csConfig.ConfigPaths.NotificationDir, wf); err != nil {
  76. return nil, fmt.Errorf("while loading notifification plugin configuration: %w", err)
  77. }
  78. // A bit of a tricky stuf now: reconcile profiles and notification plugins
  79. ncfgs := map[string]NotificationsCfg{}
  80. profiles, err := csprofiles.NewProfile(csConfig.API.Server.Profiles)
  81. if err != nil {
  82. return nil, fmt.Errorf("while extracting profiles from configuration: %w", err)
  83. }
  84. for profileID, profile := range profiles {
  85. loop:
  86. for _, notif := range profile.Cfg.Notifications {
  87. for name, pc := range pcfgs {
  88. if notif == name {
  89. if _, ok := ncfgs[pc.Name]; !ok {
  90. ncfgs[pc.Name] = NotificationsCfg{
  91. Config: pc,
  92. Profiles: []*csconfig.ProfileCfg{profile.Cfg},
  93. ids: []uint{uint(profileID)},
  94. }
  95. continue loop
  96. }
  97. tmp := ncfgs[pc.Name]
  98. for _, pr := range tmp.Profiles {
  99. var profiles []*csconfig.ProfileCfg
  100. if pr.Name == profile.Cfg.Name {
  101. continue
  102. }
  103. profiles = append(tmp.Profiles, profile.Cfg)
  104. ids := append(tmp.ids, uint(profileID))
  105. ncfgs[pc.Name] = NotificationsCfg{
  106. Config: tmp.Config,
  107. Profiles: profiles,
  108. ids: ids,
  109. }
  110. }
  111. }
  112. }
  113. }
  114. }
  115. return ncfgs, nil
  116. }
  117. func NewNotificationsListCmd() *cobra.Command {
  118. var cmdNotificationsList = &cobra.Command{
  119. Use: "list",
  120. Short: "List active notifications plugins",
  121. Long: `List active notifications plugins`,
  122. Example: `cscli notifications list`,
  123. Args: cobra.ExactArgs(0),
  124. DisableAutoGenTag: true,
  125. RunE: func(cmd *cobra.Command, arg []string) error {
  126. ncfgs, err := getNotificationsConfiguration()
  127. if err != nil {
  128. return fmt.Errorf("can't build profiles configuration: %w", err)
  129. }
  130. if csConfig.Cscli.Output == "human" {
  131. notificationListTable(color.Output, ncfgs)
  132. } else if csConfig.Cscli.Output == "json" {
  133. x, err := json.MarshalIndent(ncfgs, "", " ")
  134. if err != nil {
  135. return fmt.Errorf("failed to marshal notification configuration: %w", err)
  136. }
  137. fmt.Printf("%s", string(x))
  138. } else if csConfig.Cscli.Output == "raw" {
  139. csvwriter := csv.NewWriter(os.Stdout)
  140. err := csvwriter.Write([]string{"Name", "Type", "Profile name"})
  141. if err != nil {
  142. return fmt.Errorf("failed to write raw header: %w", err)
  143. }
  144. for _, b := range ncfgs {
  145. profilesList := []string{}
  146. for _, p := range b.Profiles {
  147. profilesList = append(profilesList, p.Name)
  148. }
  149. err := csvwriter.Write([]string{b.Config.Name, b.Config.Type, strings.Join(profilesList, ", ")})
  150. if err != nil {
  151. return fmt.Errorf("failed to write raw content: %w", err)
  152. }
  153. }
  154. csvwriter.Flush()
  155. }
  156. return nil
  157. },
  158. }
  159. return cmdNotificationsList
  160. }
  161. func NewNotificationsInspectCmd() *cobra.Command {
  162. var cmdNotificationsInspect = &cobra.Command{
  163. Use: "inspect",
  164. Short: "Inspect active notifications plugin configuration",
  165. Long: `Inspect active notifications plugin and show configuration`,
  166. Example: `cscli notifications inspect <plugin_name>`,
  167. Args: cobra.ExactArgs(1),
  168. DisableAutoGenTag: true,
  169. RunE: func(cmd *cobra.Command, arg []string) error {
  170. var (
  171. cfg NotificationsCfg
  172. ok bool
  173. )
  174. pluginName := arg[0]
  175. if pluginName == "" {
  176. return fmt.Errorf("please provide a plugin name to inspect")
  177. }
  178. ncfgs, err := getNotificationsConfiguration()
  179. if err != nil {
  180. return fmt.Errorf("can't build profiles configuration: %w", err)
  181. }
  182. if cfg, ok = ncfgs[pluginName]; !ok {
  183. return fmt.Errorf("plugin '%s' does not exist or is not active", pluginName)
  184. }
  185. if csConfig.Cscli.Output == "human" || csConfig.Cscli.Output == "raw" {
  186. fmt.Printf(" - %15s: %15s\n", "Type", cfg.Config.Type)
  187. fmt.Printf(" - %15s: %15s\n", "Name", cfg.Config.Name)
  188. fmt.Printf(" - %15s: %15s\n", "Timeout", cfg.Config.TimeOut)
  189. fmt.Printf(" - %15s: %15s\n", "Format", cfg.Config.Format)
  190. for k, v := range cfg.Config.Config {
  191. fmt.Printf(" - %15s: %15v\n", k, v)
  192. }
  193. } else if csConfig.Cscli.Output == "json" {
  194. x, err := json.MarshalIndent(cfg, "", " ")
  195. if err != nil {
  196. return fmt.Errorf("failed to marshal notification configuration: %w", err)
  197. }
  198. fmt.Printf("%s", string(x))
  199. }
  200. return nil
  201. },
  202. }
  203. return cmdNotificationsInspect
  204. }
  205. func NewNotificationsReinjectCmd() *cobra.Command {
  206. var remediation bool
  207. var alertOverride string
  208. var cmdNotificationsReinject = &cobra.Command{
  209. Use: "reinject",
  210. Short: "reinject alert into notifications system",
  211. Long: `Reinject alert into notifications system`,
  212. Example: `
  213. cscli notifications reinject <alert_id>
  214. cscli notifications reinject <alert_id> --remediation
  215. cscli notifications reinject <alert_id> -a '{"remediation": true,"scenario":"notification/test"}'
  216. `,
  217. Args: cobra.ExactArgs(1),
  218. DisableAutoGenTag: true,
  219. RunE: func(cmd *cobra.Command, args []string) error {
  220. var (
  221. pluginBroker csplugin.PluginBroker
  222. pluginTomb tomb.Tomb
  223. )
  224. if len(args) != 1 {
  225. printHelp(cmd)
  226. return fmt.Errorf("wrong number of argument: there should be one argument")
  227. }
  228. //first: get the alert
  229. id, err := strconv.Atoi(args[0])
  230. if err != nil {
  231. return fmt.Errorf("bad alert id %s", args[0])
  232. }
  233. if err := csConfig.LoadAPIClient(); err != nil {
  234. return fmt.Errorf("loading api client: %w", err)
  235. }
  236. if csConfig.API.Client == nil {
  237. return fmt.Errorf("missing configuration on 'api_client:'")
  238. }
  239. if csConfig.API.Client.Credentials == nil {
  240. return fmt.Errorf("missing API credentials in '%s'", csConfig.API.Client.CredentialsFilePath)
  241. }
  242. apiURL, err := url.Parse(csConfig.API.Client.Credentials.URL)
  243. if err != nil {
  244. return fmt.Errorf("error parsing the URL of the API: %w", err)
  245. }
  246. client, err := apiclient.NewClient(&apiclient.Config{
  247. MachineID: csConfig.API.Client.Credentials.Login,
  248. Password: strfmt.Password(csConfig.API.Client.Credentials.Password),
  249. UserAgent: fmt.Sprintf("crowdsec/%s", version.String()),
  250. URL: apiURL,
  251. VersionPrefix: "v1",
  252. })
  253. if err != nil {
  254. return fmt.Errorf("error creating the client for the API: %w", err)
  255. }
  256. alert, _, err := client.Alerts.GetByID(context.Background(), id)
  257. if err != nil {
  258. return fmt.Errorf("can't find alert with id %s: %w", args[0], err)
  259. }
  260. if alertOverride != "" {
  261. if err = json.Unmarshal([]byte(alertOverride), alert); err != nil {
  262. return fmt.Errorf("can't unmarshal data in the alert flag: %w", err)
  263. }
  264. }
  265. if !remediation {
  266. alert.Remediation = true
  267. }
  268. // second we start plugins
  269. err = pluginBroker.Init(csConfig.PluginConfig, csConfig.API.Server.Profiles, csConfig.ConfigPaths)
  270. if err != nil {
  271. return fmt.Errorf("can't initialize plugins: %w", err)
  272. }
  273. pluginTomb.Go(func() error {
  274. pluginBroker.Run(&pluginTomb)
  275. return nil
  276. })
  277. //third: get the profile(s), and process the whole stuff
  278. profiles, err := csprofiles.NewProfile(csConfig.API.Server.Profiles)
  279. if err != nil {
  280. return fmt.Errorf("cannot extract profiles from configuration: %w", err)
  281. }
  282. for id, profile := range profiles {
  283. _, matched, err := profile.EvaluateProfile(alert)
  284. if err != nil {
  285. return fmt.Errorf("can't evaluate profile %s: %w", profile.Cfg.Name, err)
  286. }
  287. if !matched {
  288. log.Infof("The profile %s didn't match", profile.Cfg.Name)
  289. continue
  290. }
  291. log.Infof("The profile %s matched, sending to its configured notification plugins", profile.Cfg.Name)
  292. loop:
  293. for {
  294. select {
  295. case pluginBroker.PluginChannel <- csplugin.ProfileAlert{
  296. ProfileID: uint(id),
  297. Alert: alert,
  298. }:
  299. break loop
  300. default:
  301. time.Sleep(50 * time.Millisecond)
  302. log.Info("sleeping\n")
  303. }
  304. }
  305. if profile.Cfg.OnSuccess == "break" {
  306. log.Infof("The profile %s contains a 'on_success: break' so bailing out", profile.Cfg.Name)
  307. break
  308. }
  309. }
  310. // time.Sleep(2 * time.Second) // There's no mechanism to ensure notification has been sent
  311. pluginTomb.Kill(fmt.Errorf("terminating"))
  312. pluginTomb.Wait()
  313. return nil
  314. },
  315. }
  316. cmdNotificationsReinject.Flags().BoolVarP(&remediation, "remediation", "r", false, "Set Alert.Remediation to false in the reinjected alert (see your profile filter configuration)")
  317. 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)")
  318. return cmdNotificationsReinject
  319. }