bitbucket.org/Aishee/synsec@v0.0.0-20210414005726-236fc01a153d/pkg/leakybucket/manager_load.go (about)

     1  package leakybucket
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"io"
     7  	"io/ioutil"
     8  	"os"
     9  	"path/filepath"
    10  	"strings"
    11  	"sync"
    12  	"time"
    13  
    14  	"bitbucket.org/Aishee/synsec/pkg/csconfig"
    15  	"bitbucket.org/Aishee/synsec/pkg/cwhub"
    16  	"bitbucket.org/Aishee/synsec/pkg/cwversion"
    17  	"bitbucket.org/Aishee/synsec/pkg/types"
    18  
    19  	"github.com/davecgh/go-spew/spew"
    20  	"github.com/sirupsen/logrus"
    21  	log "github.com/sirupsen/logrus"
    22  
    23  	"github.com/antonmedv/expr"
    24  	"github.com/antonmedv/expr/vm"
    25  	"github.com/goombaio/namegenerator"
    26  	"gopkg.in/tomb.v2"
    27  	yaml "gopkg.in/yaml.v2"
    28  
    29  	"bitbucket.org/Aishee/synsec/pkg/exprhelpers"
    30  )
    31  
    32  // BucketFactory struct holds all fields for any bucket configuration. This is to have a
    33  // generic struct for buckets. This can be seen as a bucket factory.
    34  type BucketFactory struct {
    35  	FormatVersion   string                    `yaml:"format"`
    36  	Author          string                    `yaml:"author"`
    37  	Description     string                    `yaml:"description"`
    38  	References      []string                  `yaml:"references"`
    39  	Type            string                    `yaml:"type"`                //Type can be : leaky, counter, trigger. It determines the main bucket characteristics
    40  	Name            string                    `yaml:"name"`                //Name of the bucket, used later in log and user-messages. Should be unique
    41  	Capacity        int                       `yaml:"capacity"`            //Capacity is applicable to leaky buckets and determines the "burst" capacity
    42  	LeakSpeed       string                    `yaml:"leakspeed"`           //Leakspeed is a float representing how many events per second leak out of the bucket
    43  	Duration        string                    `yaml:"duration"`            //Duration allows 'counter' buckets to have a fixed life-time
    44  	Filter          string                    `yaml:"filter"`              //Filter is an expr that determines if an event is elligible for said bucket. Filter is evaluated against the Event struct
    45  	GroupBy         string                    `yaml:"groupby,omitempty"`   //groupy is an expr that allows to determine the partitions of the bucket. A common example is the source_ip
    46  	Distinct        string                    `yaml:"distinct"`            //Distinct, when present, adds a `Pour()` processor that will only pour uniq items (based on distinct expr result)
    47  	Debug           bool                      `yaml:"debug"`               //Debug, when set to true, will enable debugging for _this_ scenario specifically
    48  	Labels          map[string]string         `yaml:"labels"`              //Labels is K:V list aiming at providing context the overflow
    49  	Blackhole       string                    `yaml:"blackhole,omitempty"` //Blackhole is a duration that, if present, will prevent same bucket partition to overflow more often than $duration
    50  	logger          *log.Entry                `yaml:"-"`                   //logger is bucket-specific logger (used by Debug as well)
    51  	Reprocess       bool                      `yaml:"reprocess"`           //Reprocess, if true, will for the bucket to be re-injected into processing chain
    52  	CacheSize       int                       `yaml:"cache_size"`          //CacheSize, if > 0, limits the size of in-memory cache of the bucket
    53  	Profiling       bool                      `yaml:"profiling"`           //Profiling, if true, will make the bucket record pours/overflows/etc.
    54  	OverflowFilter  string                    `yaml:"overflow_filter"`     //OverflowFilter if present, is a filter that must return true for the overflow to go through
    55  	ScopeType       types.ScopeType           `yaml:"scope,omitempty"`     //to enforce a different remediation than blocking an IP. Will default this to IP
    56  	BucketName      string                    `yaml:"-"`
    57  	Filename        string                    `yaml:"-"`
    58  	RunTimeFilter   *vm.Program               `json:"-"`
    59  	ExprDebugger    *exprhelpers.ExprDebugger `yaml:"-" json:"-"` // used to debug expression by printing the content of each variable of the expression
    60  	RunTimeGroupBy  *vm.Program               `json:"-"`
    61  	Data            []*types.DataSource       `yaml:"data,omitempty"`
    62  	DataDir         string                    `yaml:"-"`
    63  	leakspeed       time.Duration             //internal representation of `Leakspeed`
    64  	duration        time.Duration             //internal representation of `Duration`
    65  	ret             chan types.Event          //the bucket-specific output chan for overflows
    66  	processors      []Processor               //processors is the list of hooks for pour/overflow/create (cf. uniq, blackhole etc.)
    67  	output          bool                      //??
    68  	ScenarioVersion string                    `yaml:"version,omitempty"`
    69  	hash            string                    `yaml:"-"`
    70  	Simulated       bool                      `yaml:"simulated"` //Set to true if the scenario instanciating the bucket was in the exclusion list
    71  	tomb            *tomb.Tomb                `yaml:"-"`
    72  	wgPour          *sync.WaitGroup           `yaml:"-"`
    73  	wgDumpState     *sync.WaitGroup           `yaml:"-"`
    74  }
    75  
    76  func ValidateFactory(bucketFactory *BucketFactory) error {
    77  	if bucketFactory.Name == "" {
    78  		return fmt.Errorf("bucket must have name")
    79  	}
    80  	if bucketFactory.Description == "" {
    81  		return fmt.Errorf("description is mandatory")
    82  	}
    83  	if bucketFactory.Type == "leaky" {
    84  		if bucketFactory.Capacity <= 0 { //capacity must be a positive int
    85  			return fmt.Errorf("bad capacity for leaky '%d'", bucketFactory.Capacity)
    86  		}
    87  		if bucketFactory.LeakSpeed == "" {
    88  			return fmt.Errorf("leakspeed can't be empty for leaky")
    89  		}
    90  		if bucketFactory.leakspeed == 0 {
    91  			return fmt.Errorf("bad leakspeed for leaky '%s'", bucketFactory.LeakSpeed)
    92  		}
    93  	} else if bucketFactory.Type == "counter" {
    94  		if bucketFactory.Duration == "" {
    95  			return fmt.Errorf("duration ca't be empty for counter")
    96  		}
    97  		if bucketFactory.duration == 0 {
    98  			return fmt.Errorf("bad duration for counter bucket '%d'", bucketFactory.duration)
    99  		}
   100  		if bucketFactory.Capacity != -1 {
   101  			return fmt.Errorf("counter bucket must have -1 capacity")
   102  		}
   103  	} else if bucketFactory.Type == "trigger" {
   104  		if bucketFactory.Capacity != 0 {
   105  			return fmt.Errorf("trigger bucket must have 0 capacity")
   106  		}
   107  	} else {
   108  		return fmt.Errorf("unknown bucket type '%s'", bucketFactory.Type)
   109  	}
   110  
   111  	switch bucketFactory.ScopeType.Scope {
   112  	case types.Undefined:
   113  		bucketFactory.ScopeType.Scope = types.Ip
   114  	case types.Ip:
   115  	case types.Range:
   116  	default:
   117  		//Compile the scope filter
   118  		var (
   119  			runTimeFilter *vm.Program
   120  			err           error
   121  		)
   122  		if runTimeFilter, err = expr.Compile(bucketFactory.ScopeType.Filter, expr.Env(exprhelpers.GetExprEnv(map[string]interface{}{"evt": &types.Event{}}))); err != nil {
   123  			return fmt.Errorf("Error compiling the scope filter: %s", err)
   124  		}
   125  		bucketFactory.ScopeType.RunTimeFilter = runTimeFilter
   126  	}
   127  	return nil
   128  }
   129  
   130  func LoadBuckets(cscfg *csconfig.SynsecServiceCfg, files []string, tomb *tomb.Tomb, buckets *Buckets) ([]BucketFactory, chan types.Event, error) {
   131  	var (
   132  		ret      []BucketFactory = []BucketFactory{}
   133  		response chan types.Event
   134  	)
   135  
   136  	var seed namegenerator.Generator = namegenerator.NewNameGenerator(time.Now().UTC().UnixNano())
   137  
   138  	response = make(chan types.Event, 1)
   139  	for _, f := range files {
   140  		log.Debugf("Loading '%s'", f)
   141  		if !strings.HasSuffix(f, ".yaml") {
   142  			log.Debugf("Skipping %s : not a yaml file", f)
   143  			continue
   144  		}
   145  
   146  		//process the yaml
   147  		bucketConfigurationFile, err := os.Open(f)
   148  		if err != nil {
   149  			log.Errorf("Can't access leaky configuration file %s", f)
   150  			return nil, nil, err
   151  		}
   152  		dec := yaml.NewDecoder(bucketConfigurationFile)
   153  		dec.SetStrict(true)
   154  		for {
   155  			bucketFactory := BucketFactory{}
   156  			err = dec.Decode(&bucketFactory)
   157  			if err != nil {
   158  				if err == io.EOF {
   159  					log.Tracef("End of yaml file")
   160  					break
   161  				} else {
   162  					log.Errorf("Bad yaml in %s : %v", f, err)
   163  					return nil, nil, fmt.Errorf("bad yaml in %s : %v", f, err)
   164  				}
   165  			}
   166  			bucketFactory.DataDir = cscfg.DataDir
   167  			//check empty
   168  			if bucketFactory.Name == "" {
   169  				log.Errorf("Won't load nameless bucket")
   170  				return nil, nil, fmt.Errorf("nameless bucket")
   171  			}
   172  			//check compat
   173  			if bucketFactory.FormatVersion == "" {
   174  				log.Tracef("no version in %s : %s, assuming '1.0'", bucketFactory.Name, f)
   175  				bucketFactory.FormatVersion = "1.0"
   176  			}
   177  			ok, err := cwversion.Statisfies(bucketFactory.FormatVersion, cwversion.Constraint_scenario)
   178  			if err != nil {
   179  				log.Fatalf("Failed to check version : %s", err)
   180  			}
   181  			if !ok {
   182  				log.Errorf("can't load %s : %s doesn't satisfy scenario format %s, skip", bucketFactory.Name, bucketFactory.FormatVersion, cwversion.Constraint_scenario)
   183  				continue
   184  			}
   185  
   186  			bucketFactory.Filename = filepath.Clean(f)
   187  			bucketFactory.BucketName = seed.Generate()
   188  			bucketFactory.ret = response
   189  			hubItem, err := cwhub.GetItemByPath(cwhub.SCENARIOS, bucketFactory.Filename)
   190  			if err != nil {
   191  				log.Errorf("scenario %s (%s) couldn't be find in hub (ignore if in unit tests)", bucketFactory.Name, bucketFactory.Filename)
   192  			} else {
   193  				if cscfg.SimulationConfig != nil {
   194  					bucketFactory.Simulated = cscfg.SimulationConfig.IsSimulated(hubItem.Name)
   195  				}
   196  				if hubItem != nil {
   197  					bucketFactory.ScenarioVersion = hubItem.LocalVersion
   198  					bucketFactory.hash = hubItem.LocalHash
   199  				} else {
   200  					log.Errorf("scenario %s (%s) couldn't be find in hub (ignore if in unit tests)", bucketFactory.Name, bucketFactory.Filename)
   201  				}
   202  			}
   203  
   204  			bucketFactory.wgDumpState = buckets.wgDumpState
   205  			bucketFactory.wgPour = buckets.wgPour
   206  			err = LoadBucket(&bucketFactory, tomb)
   207  			if err != nil {
   208  				log.Errorf("Failed to load bucket %s : %v", bucketFactory.Name, err)
   209  				return nil, nil, fmt.Errorf("loading of %s failed : %v", bucketFactory.Name, err)
   210  			}
   211  			ret = append(ret, bucketFactory)
   212  		}
   213  	}
   214  	log.Warningf("Loaded %d scenarios", len(ret))
   215  	return ret, response, nil
   216  }
   217  
   218  /* Init recursively process yaml files from a directory and loads them as BucketFactory */
   219  func LoadBucket(bucketFactory *BucketFactory, tomb *tomb.Tomb) error {
   220  	var err error
   221  	if bucketFactory.Debug {
   222  		var clog = logrus.New()
   223  		if err := types.ConfigureLogger(clog); err != nil {
   224  			log.Fatalf("While creating bucket-specific logger : %s", err)
   225  		}
   226  		clog.SetLevel(log.DebugLevel)
   227  		bucketFactory.logger = clog.WithFields(log.Fields{
   228  			"cfg":  bucketFactory.BucketName,
   229  			"name": bucketFactory.Name,
   230  			"file": bucketFactory.Filename,
   231  		})
   232  	} else {
   233  		/* else bind it to the default one (might find something more elegant here)*/
   234  		bucketFactory.logger = log.WithFields(log.Fields{
   235  			"cfg":  bucketFactory.BucketName,
   236  			"name": bucketFactory.Name,
   237  			"file": bucketFactory.Filename,
   238  		})
   239  	}
   240  
   241  	if bucketFactory.LeakSpeed != "" {
   242  		if bucketFactory.leakspeed, err = time.ParseDuration(bucketFactory.LeakSpeed); err != nil {
   243  			return fmt.Errorf("bad leakspeed '%s' in %s : %v", bucketFactory.LeakSpeed, bucketFactory.Filename, err)
   244  		}
   245  	} else {
   246  		bucketFactory.leakspeed = time.Duration(0)
   247  	}
   248  	if bucketFactory.Duration != "" {
   249  		if bucketFactory.duration, err = time.ParseDuration(bucketFactory.Duration); err != nil {
   250  			return fmt.Errorf("invalid Duration '%s' in %s : %v", bucketFactory.Duration, bucketFactory.Filename, err)
   251  		}
   252  	}
   253  
   254  	if bucketFactory.Filter == "" {
   255  		bucketFactory.logger.Warningf("Bucket without filter, abort.")
   256  		return fmt.Errorf("bucket without filter directive")
   257  	}
   258  	bucketFactory.RunTimeFilter, err = expr.Compile(bucketFactory.Filter, expr.Env(exprhelpers.GetExprEnv(map[string]interface{}{"evt": &types.Event{}})))
   259  	if err != nil {
   260  		return fmt.Errorf("invalid filter '%s' in %s : %v", bucketFactory.Filter, bucketFactory.Filename, err)
   261  	}
   262  	if bucketFactory.Debug {
   263  		bucketFactory.ExprDebugger, err = exprhelpers.NewDebugger(bucketFactory.Filter, expr.Env(exprhelpers.GetExprEnv(map[string]interface{}{"evt": &types.Event{}})))
   264  		if err != nil {
   265  			log.Errorf("unable to build debug filter for '%s' : %s", bucketFactory.Filter, err)
   266  		}
   267  	}
   268  
   269  	if bucketFactory.GroupBy != "" {
   270  		bucketFactory.RunTimeGroupBy, err = expr.Compile(bucketFactory.GroupBy, expr.Env(exprhelpers.GetExprEnv(map[string]interface{}{"evt": &types.Event{}})))
   271  		if err != nil {
   272  			return fmt.Errorf("invalid groupby '%s' in %s : %v", bucketFactory.GroupBy, bucketFactory.Filename, err)
   273  		}
   274  	}
   275  
   276  	bucketFactory.logger.Infof("Adding %s bucket", bucketFactory.Type)
   277  	//return the Holder correponding to the type of bucket
   278  	bucketFactory.processors = []Processor{}
   279  	switch bucketFactory.Type {
   280  	case "leaky":
   281  		bucketFactory.processors = append(bucketFactory.processors, &DumbProcessor{})
   282  	case "trigger":
   283  		bucketFactory.processors = append(bucketFactory.processors, &Trigger{})
   284  	case "counter":
   285  		bucketFactory.processors = append(bucketFactory.processors, &DumbProcessor{})
   286  	default:
   287  		return fmt.Errorf("invalid type '%s' in %s : %v", bucketFactory.Type, bucketFactory.Filename, err)
   288  	}
   289  
   290  	if bucketFactory.Distinct != "" {
   291  		bucketFactory.logger.Tracef("Adding a non duplicate filter on %s.", bucketFactory.Name)
   292  		bucketFactory.processors = append(bucketFactory.processors, &Uniq{})
   293  	}
   294  
   295  	if bucketFactory.OverflowFilter != "" {
   296  		bucketFactory.logger.Tracef("Adding an overflow filter")
   297  		filovflw, err := NewOverflowFilter(bucketFactory)
   298  		if err != nil {
   299  			bucketFactory.logger.Errorf("Error creating overflow_filter : %s", err)
   300  			return fmt.Errorf("error creating overflow_filter : %s", err)
   301  		}
   302  		bucketFactory.processors = append(bucketFactory.processors, filovflw)
   303  	}
   304  
   305  	if bucketFactory.Blackhole != "" {
   306  		bucketFactory.logger.Tracef("Adding blackhole.")
   307  		blackhole, err := NewBlackhole(bucketFactory)
   308  		if err != nil {
   309  			bucketFactory.logger.Errorf("Error creating blackhole : %s", err)
   310  			return fmt.Errorf("error creating blackhole : %s", err)
   311  		}
   312  		bucketFactory.processors = append(bucketFactory.processors, blackhole)
   313  	}
   314  
   315  	if len(bucketFactory.Data) > 0 {
   316  		for _, data := range bucketFactory.Data {
   317  			if data.DestPath == "" {
   318  				bucketFactory.logger.Errorf("no dest_file provided for '%s'", bucketFactory.Name)
   319  				continue
   320  			}
   321  			err = exprhelpers.FileInit(bucketFactory.DataDir, data.DestPath, data.Type)
   322  			if err != nil {
   323  				bucketFactory.logger.Errorf("unable to init data for file '%s': %s", data.DestPath, err.Error())
   324  			}
   325  		}
   326  	}
   327  
   328  	bucketFactory.output = false
   329  	if err := ValidateFactory(bucketFactory); err != nil {
   330  		return fmt.Errorf("invalid bucket from %s : %v", bucketFactory.Filename, err)
   331  	}
   332  	bucketFactory.tomb = tomb
   333  	return nil
   334  
   335  }
   336  
   337  func LoadBucketsState(file string, buckets *Buckets, bucketFactories []BucketFactory) error {
   338  	var state map[string]Leaky
   339  	body, err := ioutil.ReadFile(file)
   340  	if err != nil {
   341  		return fmt.Errorf("can't state file %s : %s", file, err)
   342  	}
   343  	if err := json.Unmarshal(body, &state); err != nil {
   344  		return fmt.Errorf("can't unmarshal state file %s : %s", file, err)
   345  	}
   346  	for k, v := range state {
   347  		var tbucket *Leaky
   348  		log.Debugf("Reloading bucket %s", k)
   349  		val, ok := buckets.Bucket_map.Load(k)
   350  		if ok {
   351  			log.Fatalf("key %s already exists : %+v", k, val)
   352  		}
   353  		//find back our holder
   354  		found := false
   355  		for _, h := range bucketFactories {
   356  			if h.Name == v.Name {
   357  				log.Debugf("found factory %s/%s -> %s", h.Author, h.Name, h.Description)
   358  				//check in which mode the bucket was
   359  				if v.Mode == TIMEMACHINE {
   360  					tbucket = NewTimeMachine(h)
   361  				} else if v.Mode == LIVE {
   362  					tbucket = NewLeaky(h)
   363  				} else {
   364  					log.Errorf("Unknown bucket type : %d", v.Mode)
   365  				}
   366  				/*Trying to restore queue state*/
   367  				tbucket.Queue = v.Queue
   368  				/*Trying to set the limiter to the saved values*/
   369  				tbucket.Limiter.Load(v.SerializedState)
   370  				tbucket.In = make(chan types.Event)
   371  				tbucket.Mapkey = k
   372  				tbucket.Signal = make(chan bool, 1)
   373  				tbucket.First_ts = v.First_ts
   374  				tbucket.Last_ts = v.Last_ts
   375  				tbucket.Ovflw_ts = v.Ovflw_ts
   376  				tbucket.Total_count = v.Total_count
   377  				buckets.Bucket_map.Store(k, tbucket)
   378  				h.tomb.Go(func() error {
   379  					return LeakRoutine(tbucket)
   380  				})
   381  				<-tbucket.Signal
   382  				found = true
   383  				break
   384  			}
   385  		}
   386  		if !found {
   387  			log.Fatalf("Unable to find holder for bucket %s : %s", k, spew.Sdump(v))
   388  		}
   389  	}
   390  
   391  	log.Infof("Restored %d buckets from dump", len(state))
   392  	return nil
   393  
   394  }