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