github.com/crowdsecurity/crowdsec@v1.6.1/pkg/apiserver/papi.go (about)

     1  package apiserver
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"fmt"
     7  	"net/http"
     8  	"sync"
     9  	"time"
    10  
    11  	log "github.com/sirupsen/logrus"
    12  	"gopkg.in/tomb.v2"
    13  
    14  	"github.com/crowdsecurity/go-cs-lib/trace"
    15  
    16  	"github.com/crowdsecurity/crowdsec/pkg/apiclient"
    17  	"github.com/crowdsecurity/crowdsec/pkg/csconfig"
    18  	"github.com/crowdsecurity/crowdsec/pkg/database"
    19  	"github.com/crowdsecurity/crowdsec/pkg/longpollclient"
    20  	"github.com/crowdsecurity/crowdsec/pkg/models"
    21  	"github.com/crowdsecurity/crowdsec/pkg/types"
    22  )
    23  
    24  var (
    25  	SyncInterval = time.Second * 10
    26  )
    27  
    28  const (
    29  	PapiPullKey = "papi:last_pull"
    30  )
    31  
    32  var (
    33  	operationMap = map[string]func(*Message, *Papi, bool) error{
    34  		"decision":   DecisionCmd,
    35  		"alert":      AlertCmd,
    36  		"management": ManagementCmd,
    37  	}
    38  )
    39  
    40  type Header struct {
    41  	OperationType string    `json:"operation_type"`
    42  	OperationCmd  string    `json:"operation_cmd"`
    43  	Timestamp     time.Time `json:"timestamp"`
    44  	Message       string    `json:"message"`
    45  	UUID          string    `json:"uuid"`
    46  	Source        *Source   `json:"source"`
    47  	Destination   string    `json:"destination"`
    48  }
    49  
    50  type Source struct {
    51  	User string `json:"user"`
    52  }
    53  
    54  type Message struct {
    55  	Header *Header
    56  	Data   interface{} `json:"data"`
    57  }
    58  
    59  type OperationChannels struct {
    60  	AddAlertChannel       chan []*models.Alert
    61  	DeleteDecisionChannel chan []*models.Decision
    62  }
    63  
    64  type Papi struct {
    65  	URL           string
    66  	Client        *longpollclient.LongPollClient
    67  	DBClient      *database.Client
    68  	apiClient     *apiclient.ApiClient
    69  	Channels      *OperationChannels
    70  	mu            sync.Mutex
    71  	pullTomb      tomb.Tomb
    72  	syncTomb      tomb.Tomb
    73  	SyncInterval  time.Duration
    74  	consoleConfig *csconfig.ConsoleConfig
    75  	Logger        *log.Entry
    76  	apic          *apic
    77  }
    78  
    79  type PapiPermCheckError struct {
    80  	Error string `json:"error"`
    81  }
    82  
    83  type PapiPermCheckSuccess struct {
    84  	Status     string   `json:"status"`
    85  	Plan       string   `json:"plan"`
    86  	Categories []string `json:"categories"`
    87  }
    88  
    89  func NewPAPI(apic *apic, dbClient *database.Client, consoleConfig *csconfig.ConsoleConfig, logLevel log.Level) (*Papi, error) {
    90  
    91  	logger := log.New()
    92  	if err := types.ConfigureLogger(logger); err != nil {
    93  		return &Papi{}, fmt.Errorf("creating papi logger: %s", err)
    94  	}
    95  	logger.SetLevel(logLevel)
    96  
    97  	papiUrl := *apic.apiClient.PapiURL
    98  	papiUrl.Path = fmt.Sprintf("%s%s", types.PAPIVersion, types.PAPIPollUrl)
    99  	longPollClient, err := longpollclient.NewLongPollClient(longpollclient.LongPollClientConfig{
   100  		Url:        papiUrl,
   101  		Logger:     logger,
   102  		HttpClient: apic.apiClient.GetClient(),
   103  	})
   104  
   105  	if err != nil {
   106  		return &Papi{}, fmt.Errorf("failed to create PAPI client: %w", err)
   107  	}
   108  
   109  	channels := &OperationChannels{
   110  		AddAlertChannel:       apic.AlertsAddChan,
   111  		DeleteDecisionChannel: make(chan []*models.Decision),
   112  	}
   113  
   114  	papi := &Papi{
   115  		URL:           apic.apiClient.PapiURL.String(),
   116  		Client:        longPollClient,
   117  		DBClient:      dbClient,
   118  		Channels:      channels,
   119  		SyncInterval:  SyncInterval,
   120  		mu:            sync.Mutex{},
   121  		pullTomb:      tomb.Tomb{},
   122  		syncTomb:      tomb.Tomb{},
   123  		apiClient:     apic.apiClient,
   124  		apic:          apic,
   125  		consoleConfig: consoleConfig,
   126  		Logger:        logger.WithFields(log.Fields{"interval": SyncInterval.Seconds(), "source": "papi"}),
   127  	}
   128  
   129  	return papi, nil
   130  }
   131  
   132  func (p *Papi) handleEvent(event longpollclient.Event, sync bool) error {
   133  	logger := p.Logger.WithField("request-id", event.RequestId)
   134  	logger.Debugf("message received: %+v", event.Data)
   135  	message := &Message{}
   136  	if err := json.Unmarshal([]byte(event.Data), message); err != nil {
   137  		return fmt.Errorf("polling papi message format is not compatible: %+v: %s", event.Data, err)
   138  	}
   139  	if message.Header == nil {
   140  		return fmt.Errorf("no header in message, skipping")
   141  	}
   142  	if message.Header.Source == nil {
   143  		return fmt.Errorf("no source user in header message, skipping")
   144  	}
   145  
   146  	if operationFunc, ok := operationMap[message.Header.OperationType]; ok {
   147  		logger.Debugf("Calling operation '%s'", message.Header.OperationType)
   148  		err := operationFunc(message, p, sync)
   149  		if err != nil {
   150  			return fmt.Errorf("'%s %s failed: %s", message.Header.OperationType, message.Header.OperationCmd, err)
   151  		}
   152  	} else {
   153  		return fmt.Errorf("operation '%s' unknown, continue", message.Header.OperationType)
   154  	}
   155  	return nil
   156  }
   157  
   158  func (p *Papi) GetPermissions() (PapiPermCheckSuccess, error) {
   159  	httpClient := p.apiClient.GetClient()
   160  	papiCheckUrl := fmt.Sprintf("%s%s%s", p.URL, types.PAPIVersion, types.PAPIPermissionsUrl)
   161  	req, err := http.NewRequest(http.MethodGet, papiCheckUrl, nil)
   162  	if err != nil {
   163  		return PapiPermCheckSuccess{}, fmt.Errorf("failed to create request : %s", err)
   164  	}
   165  	resp, err := httpClient.Do(req)
   166  	if err != nil {
   167  		log.Fatalf("failed to get response : %s", err)
   168  	}
   169  
   170  	defer resp.Body.Close()
   171  	if resp.StatusCode != http.StatusOK {
   172  		errResp := PapiPermCheckError{}
   173  		err = json.NewDecoder(resp.Body).Decode(&errResp)
   174  		if err != nil {
   175  			return PapiPermCheckSuccess{}, fmt.Errorf("failed to decode response : %s", err)
   176  		}
   177  		return PapiPermCheckSuccess{}, fmt.Errorf("unable to query PAPI : %s (%d)", errResp.Error, resp.StatusCode)
   178  	}
   179  	respBody := PapiPermCheckSuccess{}
   180  	err = json.NewDecoder(resp.Body).Decode(&respBody)
   181  	if err != nil {
   182  		return PapiPermCheckSuccess{}, fmt.Errorf("failed to decode response : %s", err)
   183  	}
   184  	return respBody, nil
   185  }
   186  
   187  func reverse(s []longpollclient.Event) []longpollclient.Event {
   188  	a := make([]longpollclient.Event, len(s))
   189  	copy(a, s)
   190  
   191  	for i := len(a)/2 - 1; i >= 0; i-- {
   192  		opp := len(a) - 1 - i
   193  		a[i], a[opp] = a[opp], a[i]
   194  	}
   195  
   196  	return a
   197  }
   198  
   199  func (p *Papi) PullOnce(since time.Time, sync bool) error {
   200  	events, err := p.Client.PullOnce(since)
   201  	if err != nil {
   202  		return err
   203  	}
   204  
   205  	reversedEvents := reverse(events) //PAPI sends events in the reverse order, which is not an issue when pulling them in real time, but here we need the correct order
   206  	eventsCount := len(events)
   207  	p.Logger.Infof("received %d events", eventsCount)
   208  
   209  	for i, event := range reversedEvents {
   210  		if err := p.handleEvent(event, sync); err != nil {
   211  			p.Logger.WithField("request-id", event.RequestId).Errorf("failed to handle event: %s", err)
   212  		}
   213  
   214  		p.Logger.Debugf("handled event %d/%d", i, eventsCount)
   215  	}
   216  
   217  	p.Logger.Debugf("finished handling events")
   218  	//Don't update the timestamp in DB, as a "real" LAPI might be running
   219  	//Worst case, crowdsec will receive a few duplicated events and will discard them
   220  	return nil
   221  }
   222  
   223  // PullPAPI is the long polling client for real-time decisions from PAPI
   224  func (p *Papi) Pull() error {
   225  	defer trace.CatchPanic("lapi/PullPAPI")
   226  	p.Logger.Infof("Starting Polling API Pull")
   227  
   228  	lastTimestamp := time.Time{}
   229  
   230  	lastTimestampStr, err := p.DBClient.GetConfigItem(PapiPullKey)
   231  	if err != nil {
   232  		p.Logger.Warningf("failed to get last timestamp for papi pull: %s", err)
   233  	}
   234  
   235  	//value doesn't exist, it's first time we're pulling
   236  	if lastTimestampStr == nil {
   237  		binTime, err := lastTimestamp.MarshalText()
   238  		if err != nil {
   239  			return fmt.Errorf("failed to marshal last timestamp: %w", err)
   240  		}
   241  
   242  		if err := p.DBClient.SetConfigItem(PapiPullKey, string(binTime)); err != nil {
   243  			p.Logger.Errorf("error setting papi pull last key: %s", err)
   244  		} else {
   245  			p.Logger.Debugf("config item '%s' set in database with value '%s'", PapiPullKey, string(binTime))
   246  		}
   247  	} else {
   248  		if err := lastTimestamp.UnmarshalText([]byte(*lastTimestampStr)); err != nil {
   249  			return fmt.Errorf("failed to unmarshal last timestamp: %w", err)
   250  		}
   251  	}
   252  
   253  	p.Logger.Infof("Starting PAPI pull (since:%s)", lastTimestamp)
   254  
   255  	for event := range p.Client.Start(lastTimestamp) {
   256  		logger := p.Logger.WithField("request-id", event.RequestId)
   257  		//update last timestamp in database
   258  		newTime := time.Now().UTC()
   259  
   260  		binTime, err := newTime.MarshalText()
   261  		if err != nil {
   262  			return fmt.Errorf("failed to marshal last timestamp: %w", err)
   263  		}
   264  
   265  		err = p.handleEvent(event, false)
   266  		if err != nil {
   267  			logger.Errorf("failed to handle event: %s", err)
   268  			continue
   269  		}
   270  
   271  		if err := p.DBClient.SetConfigItem(PapiPullKey, string(binTime)); err != nil {
   272  			return fmt.Errorf("failed to update last timestamp: %w", err)
   273  		}
   274  
   275  		logger.Debugf("set last timestamp to %s", newTime)
   276  	}
   277  
   278  	return nil
   279  }
   280  
   281  func (p *Papi) SyncDecisions() error {
   282  	defer trace.CatchPanic("lapi/syncDecisionsToCAPI")
   283  
   284  	var cache models.DecisionsDeleteRequest
   285  
   286  	ticker := time.NewTicker(p.SyncInterval)
   287  	p.Logger.Infof("Start decisions sync to CrowdSec Central API (interval: %s)", p.SyncInterval)
   288  
   289  	for {
   290  		select {
   291  		case <-p.syncTomb.Dying(): // if one apic routine is dying, do we kill the others?
   292  			p.Logger.Infof("sync decisions tomb is dying, sending cache (%d elements) before exiting", len(cache))
   293  
   294  			if len(cache) == 0 {
   295  				return nil
   296  			}
   297  
   298  			go p.SendDeletedDecisions(&cache)
   299  
   300  			return nil
   301  		case <-ticker.C:
   302  			if len(cache) > 0 {
   303  				p.mu.Lock()
   304  				cacheCopy := cache
   305  				cache = make([]models.DecisionsDeleteRequestItem, 0)
   306  				p.mu.Unlock()
   307  				p.Logger.Infof("sync decisions: %d deleted decisions to push", len(cacheCopy))
   308  
   309  				go p.SendDeletedDecisions(&cacheCopy)
   310  			}
   311  		case deletedDecisions := <-p.Channels.DeleteDecisionChannel:
   312  			if (p.consoleConfig.ShareManualDecisions != nil && *p.consoleConfig.ShareManualDecisions) || (p.consoleConfig.ConsoleManagement != nil && *p.consoleConfig.ConsoleManagement) {
   313  				var tmpDecisions []models.DecisionsDeleteRequestItem
   314  
   315  				p.Logger.Debugf("%d decisions deletion to add in cache", len(deletedDecisions))
   316  
   317  				for _, decision := range deletedDecisions {
   318  					tmpDecisions = append(tmpDecisions, models.DecisionsDeleteRequestItem(decision.UUID))
   319  				}
   320  
   321  				p.mu.Lock()
   322  				cache = append(cache, tmpDecisions...)
   323  				p.mu.Unlock()
   324  			}
   325  		}
   326  	}
   327  }
   328  
   329  func (p *Papi) SendDeletedDecisions(cacheOrig *models.DecisionsDeleteRequest) {
   330  	var (
   331  		cache []models.DecisionsDeleteRequestItem = *cacheOrig
   332  		send models.DecisionsDeleteRequest
   333  	)
   334  
   335  	bulkSize := 50
   336  	pageStart := 0
   337  	pageEnd := bulkSize
   338  
   339  	for {
   340  		if pageEnd >= len(cache) {
   341  			send = cache[pageStart:]
   342  			ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
   343  
   344  			defer cancel()
   345  
   346  			_, _, err := p.apiClient.DecisionDelete.Add(ctx, &send)
   347  			if err != nil {
   348  				p.Logger.Errorf("sending deleted decisions to central API: %s", err)
   349  				return
   350  			}
   351  
   352  			break
   353  		}
   354  
   355  		send = cache[pageStart:pageEnd]
   356  		ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
   357  
   358  		defer cancel()
   359  
   360  		_, _, err := p.apiClient.DecisionDelete.Add(ctx, &send)
   361  		if err != nil {
   362  			//we log it here as well, because the return value of func might be discarded
   363  			p.Logger.Errorf("sending deleted decisions to central API: %s", err)
   364  		}
   365  
   366  		pageStart += bulkSize
   367  		pageEnd += bulkSize
   368  	}
   369  }
   370  
   371  func (p *Papi) Shutdown() {
   372  	p.Logger.Infof("Shutting down PAPI")
   373  	p.syncTomb.Kill(nil)
   374  	p.Client.Stop()
   375  }