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 }