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 }