notifications.go 11 KB

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