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  }