bitbucket.org/Aishee/synsec@v0.0.0-20210414005726-236fc01a153d/pkg/apiserver/apic.go (about) 1 package apiserver 2 3 import ( 4 "context" 5 "fmt" 6 "net/url" 7 "strings" 8 "sync" 9 "time" 10 11 "bitbucket.org/Aishee/synsec/pkg/apiclient" 12 "bitbucket.org/Aishee/synsec/pkg/csconfig" 13 "bitbucket.org/Aishee/synsec/pkg/cwversion" 14 "bitbucket.org/Aishee/synsec/pkg/database" 15 "bitbucket.org/Aishee/synsec/pkg/models" 16 "bitbucket.org/Aishee/synsec/pkg/types" 17 "github.com/go-openapi/strfmt" 18 "github.com/pkg/errors" 19 log "github.com/sirupsen/logrus" 20 21 "gopkg.in/tomb.v2" 22 ) 23 24 const ( 25 PullInterval = "2h" 26 PushInterval = "30s" 27 MetricsInterval = "30m" 28 ) 29 30 type apic struct { 31 pullInterval time.Duration 32 pushInterval time.Duration 33 metricsInterval time.Duration 34 dbClient *database.Client 35 apiClient *apiclient.ApiClient 36 alertToPush chan []*models.Alert 37 mu sync.Mutex 38 pushTomb tomb.Tomb 39 pullTomb tomb.Tomb 40 metricsTomb tomb.Tomb 41 startup bool 42 credentials *csconfig.ApiCredentialsCfg 43 scenarioList []string 44 } 45 46 func IsInSlice(a string, b []string) bool { 47 for _, v := range b { 48 if a == v { 49 return true 50 } 51 } 52 return false 53 } 54 55 func (a *apic) FetchScenariosListFromDB() ([]string, error) { 56 scenarios := make([]string, 0) 57 machines, err := a.dbClient.ListMachines() 58 if err != nil { 59 return nil, errors.Wrap(err, "while listing machines") 60 } 61 //merge all scenarios together 62 for _, v := range machines { 63 machineScenarios := strings.Split(v.Scenarios, ",") 64 log.Debugf("%d scenarios for machine %d", len(machineScenarios), v.ID) 65 for _, sv := range machineScenarios { 66 if !IsInSlice(sv, scenarios) && sv != "" { 67 scenarios = append(scenarios, sv) 68 } 69 } 70 } 71 log.Debugf("Returning list of scenarios : %+v", scenarios) 72 return scenarios, nil 73 } 74 75 func AlertToSignal(alert *models.Alert) *models.AddSignalsRequestItem { 76 return &models.AddSignalsRequestItem{ 77 Message: alert.Message, 78 Scenario: alert.Scenario, 79 ScenarioHash: alert.ScenarioHash, 80 ScenarioVersion: alert.ScenarioVersion, 81 Source: alert.Source, 82 StartAt: alert.StartAt, 83 StopAt: alert.StopAt, 84 CreatedAt: alert.CreatedAt, 85 MachineID: alert.MachineID, 86 } 87 } 88 89 func NewAPIC(config *csconfig.OnlineApiClientCfg, dbClient *database.Client) (*apic, error) { 90 var err error 91 ret := &apic{ 92 alertToPush: make(chan []*models.Alert), 93 dbClient: dbClient, 94 mu: sync.Mutex{}, 95 startup: true, 96 credentials: config.Credentials, 97 pullTomb: tomb.Tomb{}, 98 pushTomb: tomb.Tomb{}, 99 metricsTomb: tomb.Tomb{}, 100 scenarioList: make([]string, 0), 101 } 102 103 ret.pullInterval, err = time.ParseDuration(PullInterval) 104 if err != nil { 105 return ret, err 106 } 107 ret.pushInterval, err = time.ParseDuration(PushInterval) 108 if err != nil { 109 return ret, err 110 } 111 ret.metricsInterval, err = time.ParseDuration(MetricsInterval) 112 if err != nil { 113 return ret, err 114 } 115 116 password := strfmt.Password(config.Credentials.Password) 117 apiURL, err := url.Parse(config.Credentials.URL) 118 if err != nil { 119 return nil, errors.Wrapf(err, "while parsing '%s'", config.Credentials.URL) 120 } 121 ret.scenarioList, err = ret.FetchScenariosListFromDB() 122 if err != nil { 123 return nil, errors.Wrap(err, "while fetching scenarios from db") 124 } 125 ret.apiClient, err = apiclient.NewClient(&apiclient.Config{ 126 MachineID: config.Credentials.Login, 127 Password: password, 128 UserAgent: fmt.Sprintf("synsec/%s", cwversion.VersionStr()), 129 URL: apiURL, 130 VersionPrefix: "v2", 131 Scenarios: ret.scenarioList, 132 UpdateScenario: ret.FetchScenariosListFromDB, 133 }) 134 return ret, nil 135 } 136 137 func (a *apic) Push() error { 138 defer types.CatchPanic("lapi/pushToAPIC") 139 140 var cache models.AddSignalsRequest 141 ticker := time.NewTicker(a.pushInterval) 142 log.Infof("start synsec api push (interval: %s)", PushInterval) 143 144 for { 145 select { 146 case <-a.pushTomb.Dying(): // if one apic routine is dying, do we kill the others? 147 a.pullTomb.Kill(nil) 148 a.metricsTomb.Kill(nil) 149 log.Infof("push tomb is dying, sending cache (%d elements) before exiting", len(cache)) 150 if len(cache) == 0 { 151 return nil 152 } 153 go a.Send(&cache) 154 return nil 155 case <-ticker.C: 156 if len(cache) > 0 { 157 a.mu.Lock() 158 cacheCopy := cache 159 cache = make(models.AddSignalsRequest, 0) 160 a.mu.Unlock() 161 log.Infof("Signal push: %d signals to push", len(cacheCopy)) 162 go a.Send(&cacheCopy) 163 } 164 case alerts := <-a.alertToPush: 165 var signals []*models.AddSignalsRequestItem 166 for _, alert := range alerts { 167 /*we're only interested into decisions coming from scenarios of the hub*/ 168 if alert.ScenarioHash == nil || *alert.ScenarioHash == "" { 169 continue 170 } 171 /*and we're not interested into tainted scenarios neither*/ 172 if alert.ScenarioVersion == nil || *alert.ScenarioVersion == "" || *alert.ScenarioVersion == "?" { 173 continue 174 } 175 signals = append(signals, AlertToSignal(alert)) 176 } 177 a.mu.Lock() 178 cache = append(cache, signals...) 179 a.mu.Unlock() 180 } 181 } 182 } 183 184 func (a *apic) Send(cacheOrig *models.AddSignalsRequest) { 185 /*we do have a problem with this : 186 The apic.Push background routine reads from alertToPush chan. 187 This chan is filled by Controller.CreateAlert 188 189 If the chan apic.Send hangs, the alertToPush chan will become full, 190 with means that Controller.CreateAlert is going to hang, blocking API worker(s). 191 192 So instead, we prefer to cancel write. 193 194 I don't know enough about gin to tell how much of an issue it can be. 195 */ 196 var cache []*models.AddSignalsRequestItem = *cacheOrig 197 var send models.AddSignalsRequest 198 199 bulkSize := 50 200 pageStart := 0 201 pageEnd := bulkSize 202 203 for { 204 205 if pageEnd >= len(cache) { 206 send = cache[pageStart:] 207 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 208 defer cancel() 209 _, _, err := a.apiClient.Signal.Add(ctx, &send) 210 if err != nil { 211 log.Errorf("Error while sending final chunk to central API : %s", err) 212 return 213 } 214 break 215 } 216 send = cache[pageStart:pageEnd] 217 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 218 defer cancel() 219 _, _, err := a.apiClient.Signal.Add(ctx, &send) 220 if err != nil { 221 //we log it here as well, because the return value of func might be discarded 222 log.Errorf("Error while sending chunk to central API : %s", err) 223 } 224 pageStart += bulkSize 225 pageEnd += bulkSize 226 } 227 } 228 229 func (a *apic) PullTop() error { 230 var err error 231 232 data, _, err := a.apiClient.Decisions.GetStream(context.Background(), a.startup) 233 if err != nil { 234 return errors.Wrap(err, "get stream") 235 } 236 if a.startup { 237 a.startup = false 238 } 239 // process deleted decisions 240 var filter map[string][]string 241 for _, decision := range data.Deleted { 242 if strings.ToLower(*decision.Scope) == "ip" { 243 filter = make(map[string][]string, 1) 244 filter["value"] = []string{*decision.Value} 245 } else { 246 filter = make(map[string][]string, 3) 247 filter["value"] = []string{*decision.Value} 248 filter["type"] = []string{*decision.Type} 249 filter["value"] = []string{*decision.Scope} 250 } 251 252 nbDeleted, err := a.dbClient.SoftDeleteDecisionsWithFilter(filter) 253 if err != nil { 254 return err 255 } 256 257 log.Printf("pull top: deleted %s entries", nbDeleted) 258 } 259 260 alertCreated, err := a.dbClient.Ent.Alert. 261 Create(). 262 SetScenario(fmt.Sprintf("update : +%d/-%d IPs", len(data.New), len(data.Deleted))). 263 SetSourceScope("Community blocklist"). 264 Save(a.dbClient.CTX) 265 if err != nil { 266 return errors.Wrap(err, "create alert from synsec-api") 267 } 268 269 // process new decisions 270 for _, decision := range data.New { 271 var start_ip, start_sfx, end_ip, end_sfx int64 272 var sz int 273 274 /*if the scope is IP or Range, convert the value to integers */ 275 if strings.ToLower(*decision.Scope) == "ip" || strings.ToLower(*decision.Scope) == "range" { 276 sz, start_ip, start_sfx, end_ip, end_sfx, err = types.Addr2Ints(*decision.Value) 277 if err != nil { 278 return errors.Wrapf(err, "invalid ip/range %s", *decision.Value) 279 } 280 } 281 282 duration, err := time.ParseDuration(*decision.Duration) 283 if err != nil { 284 return errors.Wrapf(err, "parse decision duration '%s':", *decision.Duration) 285 } 286 _, err = a.dbClient.Ent.Decision.Create(). 287 SetUntil(time.Now().Add(duration)). 288 SetScenario(*decision.Scenario). 289 SetType(*decision.Type). 290 SetIPSize(int64(sz)). 291 SetStartIP(start_ip). 292 SetStartSuffix(start_sfx). 293 SetEndIP(end_ip). 294 SetEndSuffix(end_sfx). 295 SetValue(*decision.Value). 296 SetScope(*decision.Scope). 297 SetOrigin(*decision.Origin). 298 SetOwner(alertCreated).Save(a.dbClient.CTX) 299 if err != nil { 300 return errors.Wrap(err, "decision creation from synsec-api:") 301 } 302 } 303 log.Printf("pull top: added %d entries", len(data.New)) 304 return nil 305 } 306 307 func (a *apic) Pull() error { 308 defer types.CatchPanic("lapi/pullFromAPIC") 309 log.Infof("start synsec api pull (interval: %s)", PullInterval) 310 var err error 311 312 scenario := a.scenarioList 313 toldOnce := false 314 for { 315 if len(scenario) > 0 { 316 break 317 } 318 if !toldOnce { 319 log.Warningf("scenario list is empty, will not pull yet") 320 toldOnce = true 321 } 322 time.Sleep(1 * time.Second) 323 scenario, err = a.FetchScenariosListFromDB() 324 if err != nil { 325 log.Errorf("unable to fetch scenarios from db: %s", err) 326 } 327 } 328 for { 329 select { 330 case <-a.pullTomb.Dying(): // if one apic routine is dying, do we kill the others? 331 a.metricsTomb.Kill(nil) 332 a.pushTomb.Kill(nil) 333 return nil 334 } 335 } 336 } 337 338 func (a *apic) SendMetrics() error { 339 defer types.CatchPanic("lapi/metricsToAPIC") 340 341 log.Infof("start synsec api send metrics (interval: %s)", MetricsInterval) 342 ticker := time.NewTicker(a.metricsInterval) 343 for { 344 select { 345 case <-ticker.C: 346 version := cwversion.VersionStr() 347 metric := &models.Metrics{ 348 ApilVersion: &version, 349 Machines: make([]*models.MetricsSoftInfo, 0), 350 Bouncers: make([]*models.MetricsSoftInfo, 0), 351 } 352 machines, err := a.dbClient.ListMachines() 353 if err != nil { 354 return err 355 } 356 bouncers, err := a.dbClient.ListBouncers() 357 if err != nil { 358 return err 359 } 360 // models.metric structure : len(machines), len(bouncers), a.credentials.Login 361 // _, _, err := a.apiClient.Metrics.Add(//*models.Metrics) 362 for _, machine := range machines { 363 m := &models.MetricsSoftInfo{ 364 Version: machine.Version, 365 Name: machine.MachineId, 366 } 367 metric.Machines = append(metric.Machines, m) 368 } 369 370 for _, bouncer := range bouncers { 371 m := &models.MetricsSoftInfo{ 372 Version: bouncer.Version, 373 Name: bouncer.Type, 374 } 375 metric.Bouncers = append(metric.Bouncers, m) 376 } 377 _, _, err = a.apiClient.Metrics.Add(context.Background(), metric) 378 if err != nil { 379 return errors.Wrap(err, "sending metrics failed") 380 } 381 case <-a.metricsTomb.Dying(): // if one apic routine is dying, do we kill the others? 382 a.pullTomb.Kill(nil) 383 a.pushTomb.Kill(nil) 384 return nil 385 } 386 } 387 } 388 389 func (a *apic) Shutdown() { 390 a.pushTomb.Kill(nil) 391 a.pullTomb.Kill(nil) 392 a.metricsTomb.Kill(nil) 393 }