decisions_import.go 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  1. package main
  2. import (
  3. "bufio"
  4. "bytes"
  5. "context"
  6. "encoding/json"
  7. "fmt"
  8. "io"
  9. "os"
  10. "strings"
  11. "time"
  12. "github.com/jszwec/csvutil"
  13. log "github.com/sirupsen/logrus"
  14. "github.com/spf13/cobra"
  15. "github.com/crowdsecurity/go-cs-lib/ptr"
  16. "github.com/crowdsecurity/go-cs-lib/slicetools"
  17. "github.com/crowdsecurity/crowdsec/pkg/models"
  18. "github.com/crowdsecurity/crowdsec/pkg/types"
  19. )
  20. // decisionRaw is only used to unmarshall json/csv decisions
  21. type decisionRaw struct {
  22. Duration string `csv:"duration,omitempty" json:"duration,omitempty"`
  23. Scenario string `csv:"reason,omitempty" json:"reason,omitempty"`
  24. Scope string `csv:"scope,omitempty" json:"scope,omitempty"`
  25. Type string `csv:"type,omitempty" json:"type,omitempty"`
  26. Value string `csv:"value" json:"value"`
  27. }
  28. func parseDecisionList(content []byte, format string) ([]decisionRaw, error) {
  29. ret := []decisionRaw{}
  30. switch format {
  31. case "values":
  32. log.Infof("Parsing values")
  33. scanner := bufio.NewScanner(bytes.NewReader(content))
  34. for scanner.Scan() {
  35. value := strings.TrimSpace(scanner.Text())
  36. ret = append(ret, decisionRaw{Value: value})
  37. }
  38. if err := scanner.Err(); err != nil {
  39. return nil, fmt.Errorf("unable to parse values: '%s'", err)
  40. }
  41. case "json":
  42. log.Infof("Parsing json")
  43. if err := json.Unmarshal(content, &ret); err != nil {
  44. return nil, err
  45. }
  46. case "csv":
  47. log.Infof("Parsing csv")
  48. if err := csvutil.Unmarshal(content, &ret); err != nil {
  49. return nil, fmt.Errorf("unable to parse csv: '%s'", err)
  50. }
  51. default:
  52. return nil, fmt.Errorf("invalid format '%s', expected one of 'json', 'csv', 'values'", format)
  53. }
  54. return ret, nil
  55. }
  56. func runDecisionsImport(cmd *cobra.Command, args []string) error {
  57. flags := cmd.Flags()
  58. input, err := flags.GetString("input")
  59. if err != nil {
  60. return err
  61. }
  62. defaultDuration, err := flags.GetString("duration")
  63. if err != nil {
  64. return err
  65. }
  66. if defaultDuration == "" {
  67. return fmt.Errorf("--duration cannot be empty")
  68. }
  69. defaultScope, err := flags.GetString("scope")
  70. if err != nil {
  71. return err
  72. }
  73. if defaultScope == "" {
  74. return fmt.Errorf("--scope cannot be empty")
  75. }
  76. defaultReason, err := flags.GetString("reason")
  77. if err != nil {
  78. return err
  79. }
  80. if defaultReason == "" {
  81. return fmt.Errorf("--reason cannot be empty")
  82. }
  83. defaultType, err := flags.GetString("type")
  84. if err != nil {
  85. return err
  86. }
  87. if defaultType == "" {
  88. return fmt.Errorf("--type cannot be empty")
  89. }
  90. batchSize, err := flags.GetInt("batch")
  91. if err != nil {
  92. return err
  93. }
  94. format, err := flags.GetString("format")
  95. if err != nil {
  96. return err
  97. }
  98. var (
  99. content []byte
  100. fin *os.File
  101. )
  102. // set format if the file has a json or csv extension
  103. if format == "" {
  104. if strings.HasSuffix(input, ".json") {
  105. format = "json"
  106. } else if strings.HasSuffix(input, ".csv") {
  107. format = "csv"
  108. }
  109. }
  110. if format == "" {
  111. return fmt.Errorf("unable to guess format from file extension, please provide a format with --format flag")
  112. }
  113. if input == "-" {
  114. fin = os.Stdin
  115. input = "stdin"
  116. } else {
  117. fin, err = os.Open(input)
  118. if err != nil {
  119. return fmt.Errorf("unable to open %s: %s", input, err)
  120. }
  121. }
  122. content, err = io.ReadAll(fin)
  123. if err != nil {
  124. return fmt.Errorf("unable to read from %s: %s", input, err)
  125. }
  126. decisionsListRaw, err := parseDecisionList(content, format)
  127. if err != nil {
  128. return err
  129. }
  130. decisions := make([]*models.Decision, len(decisionsListRaw))
  131. for i, d := range decisionsListRaw {
  132. if d.Value == "" {
  133. return fmt.Errorf("item %d: missing 'value'", i)
  134. }
  135. if d.Duration == "" {
  136. d.Duration = defaultDuration
  137. log.Debugf("item %d: missing 'duration', using default '%s'", i, defaultDuration)
  138. }
  139. if d.Scenario == "" {
  140. d.Scenario = defaultReason
  141. log.Debugf("item %d: missing 'reason', using default '%s'", i, defaultReason)
  142. }
  143. if d.Type == "" {
  144. d.Type = defaultType
  145. log.Debugf("item %d: missing 'type', using default '%s'", i, defaultType)
  146. }
  147. if d.Scope == "" {
  148. d.Scope = defaultScope
  149. log.Debugf("item %d: missing 'scope', using default '%s'", i, defaultScope)
  150. }
  151. decisions[i] = &models.Decision{
  152. Value: ptr.Of(d.Value),
  153. Duration: ptr.Of(d.Duration),
  154. Origin: ptr.Of(types.CscliImportOrigin),
  155. Scenario: ptr.Of(d.Scenario),
  156. Type: ptr.Of(d.Type),
  157. Scope: ptr.Of(d.Scope),
  158. Simulated: ptr.Of(false),
  159. }
  160. }
  161. if len(decisions) > 1000 {
  162. log.Infof("You are about to add %d decisions, this may take a while", len(decisions))
  163. }
  164. for _, chunk := range slicetools.Chunks(decisions, batchSize) {
  165. log.Debugf("Processing chunk of %d decisions", len(chunk))
  166. importAlert := models.Alert{
  167. CreatedAt: time.Now().UTC().Format(time.RFC3339),
  168. Scenario: ptr.Of(fmt.Sprintf("import %s: %d IPs", input, len(chunk))),
  169. Message: ptr.Of(""),
  170. Events: []*models.Event{},
  171. Source: &models.Source{
  172. Scope: ptr.Of(""),
  173. Value: ptr.Of(""),
  174. },
  175. StartAt: ptr.Of(time.Now().UTC().Format(time.RFC3339)),
  176. StopAt: ptr.Of(time.Now().UTC().Format(time.RFC3339)),
  177. Capacity: ptr.Of(int32(0)),
  178. Simulated: ptr.Of(false),
  179. EventsCount: ptr.Of(int32(len(chunk))),
  180. Leakspeed: ptr.Of(""),
  181. ScenarioHash: ptr.Of(""),
  182. ScenarioVersion: ptr.Of(""),
  183. Decisions: chunk,
  184. }
  185. _, _, err = Client.Alerts.Add(context.Background(), models.AddAlertsRequest{&importAlert})
  186. if err != nil {
  187. return err
  188. }
  189. }
  190. log.Infof("Imported %d decisions", len(decisions))
  191. return nil
  192. }
  193. func NewDecisionsImportCmd() *cobra.Command {
  194. var cmdDecisionsImport = &cobra.Command{
  195. Use: "import [options]",
  196. Short: "Import decisions from a file or pipe",
  197. Long: "expected format:\n" +
  198. "csv : any of duration,reason,scope,type,value, with a header line\n" +
  199. `json : {"duration" : "24h", "reason" : "my_scenario", "scope" : "ip", "type" : "ban", "value" : "x.y.z.z"}`,
  200. DisableAutoGenTag: true,
  201. Example: `decisions.csv:
  202. duration,scope,value
  203. 24h,ip,1.2.3.4
  204. $ cscli decisions import -i decisions.csv
  205. decisions.json:
  206. [{"duration" : "4h", "scope" : "ip", "type" : "ban", "value" : "1.2.3.4"}]
  207. The file format is detected from the extension, but can be forced with the --format option
  208. which is required when reading from standard input.
  209. Raw values, standard input:
  210. $ echo "1.2.3.4" | cscli decisions import -i - --format values
  211. `,
  212. RunE: runDecisionsImport,
  213. }
  214. flags := cmdDecisionsImport.Flags()
  215. flags.SortFlags = false
  216. flags.StringP("input", "i", "", "Input file")
  217. flags.StringP("duration", "d", "4h", "Decision duration: 1h,4h,30m")
  218. flags.String("scope", types.Ip, "Decision scope: ip,range,username")
  219. flags.StringP("reason", "R", "manual", "Decision reason: <scenario-name>")
  220. flags.StringP("type", "t", "ban", "Decision type: ban,captcha,throttle")
  221. flags.Int("batch", 0, "Split import in batches of N decisions")
  222. flags.String("format", "", "Input format: 'json', 'csv' or 'values' (each line is a value, no headers)")
  223. cmdDecisionsImport.MarkFlagRequired("input")
  224. return cmdDecisionsImport
  225. }