crowdsec/pkg/apiserver/papi_cmd.go
Thibault "bui" Koechlin b63e64ee9f
Fix locking logic for HA + add list unsubscribe for PAPI (#2904)
* add list unsubscribe operation for papi

* fix the locking logic for HA
2024-03-19 10:29:16 +01:00

238 lines
7.4 KiB
Go

package apiserver
import (
"encoding/json"
"fmt"
"time"
log "github.com/sirupsen/logrus"
"github.com/crowdsecurity/go-cs-lib/ptr"
"github.com/crowdsecurity/crowdsec/pkg/apiclient"
"github.com/crowdsecurity/crowdsec/pkg/models"
"github.com/crowdsecurity/crowdsec/pkg/modelscapi"
"github.com/crowdsecurity/crowdsec/pkg/types"
)
type deleteDecisions struct {
UUID string `json:"uuid"`
Decisions []string `json:"decisions"`
}
type blocklistLink struct {
// blocklist name
Name string `json:"name"`
// blocklist url
Url string `json:"url"`
// blocklist remediation
Remediation string `json:"remediation"`
// blocklist scope
Scope string `json:"scope,omitempty"`
// blocklist duration
Duration string `json:"duration,omitempty"`
}
type forcePull struct {
Blocklist *blocklistLink `json:"blocklist,omitempty"`
}
type listUnsubscribe struct {
Name string `json:"name"`
}
func DecisionCmd(message *Message, p *Papi, sync bool) error {
switch message.Header.OperationCmd {
case "delete":
data, err := json.Marshal(message.Data)
if err != nil {
return err
}
UUIDs := make([]string, 0)
deleteDecisionMsg := deleteDecisions{
Decisions: make([]string, 0),
}
if err := json.Unmarshal(data, &deleteDecisionMsg); err != nil {
return fmt.Errorf("message for '%s' contains bad data format: %w", message.Header.OperationType, err)
}
UUIDs = append(UUIDs, deleteDecisionMsg.Decisions...)
log.Infof("Decisions UUIDs to remove: %+v", UUIDs)
filter := make(map[string][]string)
filter["uuid"] = UUIDs
_, deletedDecisions, err := p.DBClient.SoftDeleteDecisionsWithFilter(filter)
if err != nil {
return fmt.Errorf("unable to delete decisions %+v: %w", UUIDs, err)
}
decisions := make([]*models.Decision, 0)
for _, deletedDecision := range deletedDecisions {
log.Infof("Decision from '%s' for '%s' (%s) has been deleted", deletedDecision.Origin, deletedDecision.Value, deletedDecision.Type)
dec := &models.Decision{
UUID: deletedDecision.UUID,
Origin: &deletedDecision.Origin,
Scenario: &deletedDecision.Scenario,
Scope: &deletedDecision.Scope,
Value: &deletedDecision.Value,
ID: int64(deletedDecision.ID),
Until: deletedDecision.Until.String(),
Type: &deletedDecision.Type,
}
decisions = append(decisions, dec)
}
p.Channels.DeleteDecisionChannel <- decisions
default:
return fmt.Errorf("unknown command '%s' for operation type '%s'", message.Header.OperationCmd, message.Header.OperationType)
}
return nil
}
func AlertCmd(message *Message, p *Papi, sync bool) error {
switch message.Header.OperationCmd {
case "add":
data, err := json.Marshal(message.Data)
if err != nil {
return err
}
alert := &models.Alert{}
if err := json.Unmarshal(data, alert); err != nil {
return fmt.Errorf("message for '%s' contains bad alert format: %w", message.Header.OperationType, err)
}
log.Infof("Received order %s from PAPI (%d decisions)", alert.UUID, len(alert.Decisions))
/*Fix the alert with missing mandatory items*/
if alert.StartAt == nil || *alert.StartAt == "" {
log.Warnf("Alert %d has no StartAt, setting it to now", alert.ID)
alert.StartAt = ptr.Of(time.Now().UTC().Format(time.RFC3339))
}
if alert.StopAt == nil || *alert.StopAt == "" {
log.Warnf("Alert %d has no StopAt, setting it to now", alert.ID)
alert.StopAt = ptr.Of(time.Now().UTC().Format(time.RFC3339))
}
alert.EventsCount = ptr.Of(int32(0))
alert.Capacity = ptr.Of(int32(0))
alert.Leakspeed = ptr.Of("")
alert.Simulated = ptr.Of(false)
alert.ScenarioHash = ptr.Of("")
alert.ScenarioVersion = ptr.Of("")
alert.Message = ptr.Of("")
alert.Scenario = ptr.Of("")
alert.Source = &models.Source{}
//if we're setting Source.Scope to types.ConsoleOrigin, it messes up the alert's value
if len(alert.Decisions) >= 1 {
alert.Source.Scope = alert.Decisions[0].Scope
alert.Source.Value = alert.Decisions[0].Value
} else {
log.Warningf("No decision found in alert for Polling API (%s : %s)", message.Header.Source.User, message.Header.Message)
alert.Source.Scope = ptr.Of(types.ConsoleOrigin)
alert.Source.Value = &message.Header.Source.User
}
alert.Scenario = &message.Header.Message
for _, decision := range alert.Decisions {
if *decision.Scenario == "" {
decision.Scenario = &message.Header.Message
}
log.Infof("Adding decision for '%s' with UUID: %s", *decision.Value, decision.UUID)
}
//use a different method : alert and/or decision might already be partially present in the database
_, err = p.DBClient.CreateOrUpdateAlert("", alert)
if err != nil {
log.Errorf("Failed to create alerts in DB: %s", err)
} else {
p.Channels.AddAlertChannel <- []*models.Alert{alert}
}
default:
return fmt.Errorf("unknown command '%s' for operation type '%s'", message.Header.OperationCmd, message.Header.OperationType)
}
return nil
}
func ManagementCmd(message *Message, p *Papi, sync bool) error {
if sync {
p.Logger.Infof("Ignoring management command from PAPI in sync mode")
return nil
}
switch message.Header.OperationCmd {
case "blocklist_unsubscribe":
data, err := json.Marshal(message.Data)
if err != nil {
return err
}
unsubscribeMsg := listUnsubscribe{}
if err := json.Unmarshal(data, &unsubscribeMsg); err != nil {
return fmt.Errorf("message for '%s' contains bad data format: %s", message.Header.OperationType, err)
}
if unsubscribeMsg.Name == "" {
return fmt.Errorf("message for '%s' contains bad data format: missing blocklist name", message.Header.OperationType)
}
p.Logger.Infof("Received blocklist_unsubscribe command from PAPI, unsubscribing from blocklist %s", unsubscribeMsg.Name)
filter := make(map[string][]string)
filter["origin"] = []string{types.ListOrigin}
filter["scenario"] = []string{unsubscribeMsg.Name}
_, deletedDecisions, err := p.DBClient.SoftDeleteDecisionsWithFilter(filter)
if err != nil {
return fmt.Errorf("unable to delete decisions for list %s : %w", unsubscribeMsg.Name, err)
}
p.Logger.Infof("deleted %d decisions for list %s", len(deletedDecisions), unsubscribeMsg.Name)
case "reauth":
p.Logger.Infof("Received reauth command from PAPI, resetting token")
p.apiClient.GetClient().Transport.(*apiclient.JWTTransport).ResetToken()
case "force_pull":
data, err := json.Marshal(message.Data)
if err != nil {
return err
}
forcePullMsg := forcePull{}
if err := json.Unmarshal(data, &forcePullMsg); err != nil {
return fmt.Errorf("message for '%s' contains bad data format: %s", message.Header.OperationType, err)
}
if forcePullMsg.Blocklist == nil {
p.Logger.Infof("Received force_pull command from PAPI, pulling community and 3rd-party blocklists")
err = p.apic.PullTop(true)
if err != nil {
return fmt.Errorf("failed to force pull operation: %s", err)
}
} else {
p.Logger.Infof("Received force_pull command from PAPI, pulling blocklist %s", forcePullMsg.Blocklist.Name)
err = p.apic.PullBlocklist(&modelscapi.BlocklistLink{
Name: &forcePullMsg.Blocklist.Name,
URL: &forcePullMsg.Blocklist.Url,
Remediation: &forcePullMsg.Blocklist.Remediation,
Scope: &forcePullMsg.Blocklist.Scope,
Duration: &forcePullMsg.Blocklist.Duration,
}, true)
if err != nil {
return fmt.Errorf("failed to force pull operation: %w", err)
}
}
default:
return fmt.Errorf("unknown command '%s' for operation type '%s'", message.Header.OperationCmd, message.Header.OperationType)
}
return nil
}