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  }