notifications.go 11 KB

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