diff --git a/cmd/crowdsec-cli/console.go b/cmd/crowdsec-cli/console.go index 7b12b1550..4896a172b 100644 --- a/cmd/crowdsec-cli/console.go +++ b/cmd/crowdsec-cli/console.go @@ -2,13 +2,22 @@ package main import ( "context" + "encoding/csv" + "encoding/json" + "errors" "fmt" + "io/fs" "net/url" + "os" "github.com/crowdsecurity/crowdsec/pkg/apiclient" + "github.com/crowdsecurity/crowdsec/pkg/csconfig" "github.com/crowdsecurity/crowdsec/pkg/cwhub" "github.com/crowdsecurity/crowdsec/pkg/cwversion" + "github.com/crowdsecurity/crowdsec/pkg/types" + "github.com/enescakir/emoji" "github.com/go-openapi/strfmt" + "github.com/olekukonko/tablewriter" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) @@ -21,12 +30,24 @@ func NewConsoleCmd() *cobra.Command { DisableAutoGenTag: true, PersistentPreRunE: func(cmd *cobra.Command, args []string) error { if err := csConfig.LoadAPIServer(); err != nil || csConfig.DisableAPI { + var fdErr *fs.PathError + if errors.As(err, &fdErr) { + log.Fatalf("Unable to load Local API : %s", fdErr) + } else if err != nil { + log.Fatalf("Unable to load required Local API Configuration : %s", err) + } else { + log.Fatal("Local API is disabled, please run this command on the local API machine") + } + } + if csConfig.DisableAPI { log.Fatal("Local API is disabled, please run this command on the local API machine") } if csConfig.API.Server.OnlineClient == nil { - log.Fatalf("no configuration for Central API (CAPI) in '%s'", *csConfig.FilePath) + log.Fatalf("No configuration for Central API (CAPI) in '%s'", *csConfig.FilePath) + } + if csConfig.API.Server.OnlineClient.Credentials == nil { + log.Fatal("You must configure Central API (CAPI) with `cscli capi register` before enrolling your instance") } - return nil }, } @@ -48,18 +69,6 @@ After running this command your will need to validate the enrollment in the weba `, Args: cobra.ExactArgs(1), DisableAutoGenTag: true, - PersistentPreRunE: func(cmd *cobra.Command, args []string) error { - if err := csConfig.LoadAPIServer(); err != nil || csConfig.DisableAPI { - log.Fatal("Local API is disabled, please run this command on the local API machine") - } - if csConfig.API.Server.OnlineClient == nil { - log.Fatalf("no configuration for Central API (CAPI) in '%s'", *csConfig.FilePath) - } - if csConfig.API.Server.OnlineClient.Credentials == nil { - log.Fatal("You must configure Central API (CAPI) with `cscli capi register` before enrolling your instance") - } - return nil - }, Run: func(cmd *cobra.Command, args []string) { password := strfmt.Password(csConfig.API.Server.OnlineClient.Credentials.Password) apiURL, err := url.Parse(csConfig.API.Server.OnlineClient.Credentials.URL) @@ -97,11 +106,193 @@ After running this command your will need to validate the enrollment in the weba if err != nil { log.Fatalf("Could not enroll instance: %s", err) } + + SetConsoleOpts(csconfig.CONSOLE_CONFIGS, true) + log.Infof("Enabled tainted&manual alerts sharing, see 'cscli console status'.") log.Infof("Watcher successfully enrolled. Visit https://app.crowdsec.net to accept it.") + log.Infof("Please restart crowdsec after accepting the enrollment.") }, } cmdEnroll.Flags().StringVarP(&name, "name", "n", "", "Name to display in the console") cmdEnroll.Flags().StringSliceVarP(&tags, "tags", "t", tags, "Tags to display in the console") cmdConsole.AddCommand(cmdEnroll) + + var enableAll, disableAll bool + + cmdEnable := &cobra.Command{ + Use: "enable [feature-flag]", + Short: "Enable a feature flag", + Example: "enable alerts-tainted", + Long: ` +Enable given information push to the central API. Allows to empower the console`, + ValidArgs: csconfig.CONSOLE_CONFIGS, + Args: cobra.MinimumNArgs(1), + DisableAutoGenTag: true, + Run: func(cmd *cobra.Command, args []string) { + if enableAll { + SetConsoleOpts(csconfig.CONSOLE_CONFIGS, true) + } else { + SetConsoleOpts(args, true) + } + + if err := csConfig.API.Server.DumpConsoleConfig(); err != nil { + log.Fatalf("failed writing console config : %s", err) + } + if enableAll { + log.Infof("All features have been enabled successfully") + } else { + log.Infof("%v have been enabled", args) + } + log.Infof(ReloadMessage()) + }, + } + cmdEnable.Flags().BoolVarP(&enableAll, "all", "a", false, "Enable all feature flags") + cmdConsole.AddCommand(cmdEnable) + + cmdDisable := &cobra.Command{ + Use: "disable [feature-flag]", + Short: "Disable a feature flag", + Example: "disable alerts-tainted", + Long: ` +Disable given information push to the central API.`, + ValidArgs: csconfig.CONSOLE_CONFIGS, + Args: cobra.MinimumNArgs(1), + DisableAutoGenTag: true, + Run: func(cmd *cobra.Command, args []string) { + if disableAll { + SetConsoleOpts(csconfig.CONSOLE_CONFIGS, false) + } else { + SetConsoleOpts(args, false) + } + + if err := csConfig.API.Server.DumpConsoleConfig(); err != nil { + log.Fatalf("failed writing console config : %s", err) + } + if disableAll { + log.Infof("All features have been disabled") + } else { + log.Infof("%v have been disabled", args) + } + log.Infof(ReloadMessage()) + }, + } + cmdDisable.Flags().BoolVarP(&disableAll, "all", "a", false, "Enable all feature flags") + cmdConsole.AddCommand(cmdDisable) + + cmdConsoleStatus := &cobra.Command{ + Use: "status [feature-flag]", + Short: "Shows status of one or all feature flags", + Example: "status alerts-tainted", + DisableAutoGenTag: true, + Run: func(cmd *cobra.Command, args []string) { + switch csConfig.Cscli.Output { + case "human": + table := tablewriter.NewWriter(os.Stdout) + + table.SetHeaderAlignment(tablewriter.ALIGN_LEFT) + table.SetAlignment(tablewriter.ALIGN_LEFT) + table.SetHeader([]string{"Option Name", "Activated", "Description"}) + for _, option := range csconfig.CONSOLE_CONFIGS { + switch option { + case csconfig.SEND_CUSTOM_SCENARIOS: + activated := string(emoji.CrossMark) + if *csConfig.API.Server.ConsoleConfig.ShareCustomScenarios { + activated = string(emoji.CheckMarkButton) + } + table.Append([]string{option, activated, "Send alerts from custom scenarios to the console"}) + case csconfig.SEND_MANUAL_SCENARIOS: + activated := string(emoji.CrossMark) + if *csConfig.API.Server.ConsoleConfig.ShareManualDecisions { + activated = string(emoji.CheckMarkButton) + } + table.Append([]string{option, activated, "Send manual decisions to the console"}) + case csconfig.SEND_TAINTED_SCENARIOS: + activated := string(emoji.CrossMark) + if *csConfig.API.Server.ConsoleConfig.ShareTaintedScenarios { + activated = string(emoji.CheckMarkButton) + } + table.Append([]string{option, activated, "Send alerts from tainted scenarios to the console"}) + } + } + table.Render() + case "json": + data, err := json.MarshalIndent(csConfig.API.Server.ConsoleConfig, "", " ") + if err != nil { + log.Fatalf("failed to marshal configuration: %s", err) + } + fmt.Printf("%s\n", string(data)) + case "raw": + csvwriter := csv.NewWriter(os.Stdout) + err := csvwriter.Write([]string{"option", "enabled"}) + if err != nil { + log.Fatal(err) + } + + rows := [][]string{ + {"share_manual_decisions", fmt.Sprintf("%t", *csConfig.API.Server.ConsoleConfig.ShareManualDecisions)}, + {"share_custom", fmt.Sprintf("%t", *csConfig.API.Server.ConsoleConfig.ShareCustomScenarios)}, + {"share_tainted", fmt.Sprintf("%t", *csConfig.API.Server.ConsoleConfig.ShareTaintedScenarios)}, + } + for _, row := range rows { + err = csvwriter.Write(row) + if err != nil { + log.Fatal(err) + } + } + csvwriter.Flush() + } + }, + } + + cmdConsole.AddCommand(cmdConsoleStatus) return cmdConsole } + +func SetConsoleOpts(args []string, wanted bool) { + for _, arg := range args { + switch arg { + case csconfig.SEND_CUSTOM_SCENARIOS: + /*for each flag check if it's already set before setting it*/ + if csConfig.API.Server.ConsoleConfig.ShareCustomScenarios != nil { + if *csConfig.API.Server.ConsoleConfig.ShareCustomScenarios == wanted { + log.Infof("%s already set to %t", csconfig.SEND_CUSTOM_SCENARIOS, wanted) + } else { + log.Infof("%s set to %t", csconfig.SEND_CUSTOM_SCENARIOS, wanted) + *csConfig.API.Server.ConsoleConfig.ShareCustomScenarios = wanted + } + } else { + log.Infof("%s set to %t", csconfig.SEND_CUSTOM_SCENARIOS, wanted) + csConfig.API.Server.ConsoleConfig.ShareCustomScenarios = types.BoolPtr(wanted) + } + case csconfig.SEND_TAINTED_SCENARIOS: + /*for each flag check if it's already set before setting it*/ + if csConfig.API.Server.ConsoleConfig.ShareTaintedScenarios != nil { + if *csConfig.API.Server.ConsoleConfig.ShareTaintedScenarios == wanted { + log.Infof("%s already set to %t", csconfig.SEND_TAINTED_SCENARIOS, wanted) + } else { + log.Infof("%s set to %t", csconfig.SEND_TAINTED_SCENARIOS, wanted) + *csConfig.API.Server.ConsoleConfig.ShareTaintedScenarios = wanted + } + } else { + log.Infof("%s set to %t", csconfig.SEND_TAINTED_SCENARIOS, wanted) + csConfig.API.Server.ConsoleConfig.ShareTaintedScenarios = types.BoolPtr(wanted) + } + case csconfig.SEND_MANUAL_SCENARIOS: + /*for each flag check if it's already set before setting it*/ + if csConfig.API.Server.ConsoleConfig.ShareManualDecisions != nil { + if *csConfig.API.Server.ConsoleConfig.ShareManualDecisions == wanted { + log.Infof("%s already set to %t", csconfig.SEND_MANUAL_SCENARIOS, wanted) + } else { + log.Infof("%s set to %t", csconfig.SEND_MANUAL_SCENARIOS, wanted) + *csConfig.API.Server.ConsoleConfig.ShareManualDecisions = wanted + } + } else { + log.Infof("%s set to %t", csconfig.SEND_MANUAL_SCENARIOS, wanted) + csConfig.API.Server.ConsoleConfig.ShareManualDecisions = types.BoolPtr(wanted) + } + default: + log.Fatalf("unknown flag %s", arg) + } + } + +} diff --git a/cmd/crowdsec-cli/decisions.go b/cmd/crowdsec-cli/decisions.go index b2fb4f9a6..62804c9d2 100644 --- a/cmd/crowdsec-cli/decisions.go +++ b/cmd/crowdsec-cli/decisions.go @@ -37,8 +37,7 @@ func DecisionsToTable(alerts *models.GetAlertsResponse) error { var spamLimit map[string]bool = make(map[string]bool) var skipped = 0 - /*process in reverse order to keep the latest item only*/ - for aIdx := len(*alerts) - 1; aIdx >= 0; aIdx-- { + for aIdx := 0; aIdx < len(*alerts); aIdx++ { alertItem := (*alerts)[aIdx] newDecisions := make([]*models.Decision, 0) for _, decisionItem := range alertItem.Decisions { @@ -303,7 +302,7 @@ cscli decisions add --scope username --value foobar DisableAutoGenTag: true, Run: func(cmd *cobra.Command, args []string) { var err error - var ip, ipRange string + var ipRange string alerts := models.AddAlertsRequest{} origin := "cscli" capacity := int32(0) @@ -358,7 +357,7 @@ cscli decisions add --scope username --value foobar AsName: empty, AsNumber: empty, Cn: empty, - IP: ip, + IP: addValue, Range: ipRange, Scope: &addScope, Value: &addValue, diff --git a/config/config.yaml b/config/config.yaml index d93c8fbaa..afc771638 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -41,6 +41,7 @@ api: log_level: info listen_uri: 127.0.0.1:8080 profiles_path: /etc/crowdsec/profiles.yaml + console_path: /etc/crowdsec/console_config.yaml online_client: # Central API credentials (to push signals and receive bad IPs) credentials_path: /etc/crowdsec/online_api_credentials.yaml # tls: diff --git a/config/console_config.yaml b/config/console_config.yaml new file mode 100644 index 000000000..e83658d7b --- /dev/null +++ b/config/console_config.yaml @@ -0,0 +1,3 @@ +share_manual_decisions: false +share_custom: true +share_tainted: true diff --git a/debian/rules b/debian/rules index 18ba25ad9..0af9e8c93 100755 --- a/debian/rules +++ b/debian/rules @@ -47,4 +47,5 @@ override_dh_auto_install: cp config/config.yaml debian/crowdsec/etc/crowdsec/config.yaml cp config/simulation.yaml debian/crowdsec/etc/crowdsec/simulation.yaml cp config/profiles.yaml debian/crowdsec/etc/crowdsec/profiles.yaml + cp config/console_config.yaml debian/crowdsec/etc/crowdsec/console_config.yaml cp -a config/patterns debian/crowdsec/etc/crowdsec diff --git a/go.sum b/go.sum index 8a7d357cf..4bf7b9872 100644 --- a/go.sum +++ b/go.sum @@ -204,6 +204,7 @@ github.com/go-openapi/errors v0.19.9 h1:9SnKdGhiPZHF3ttwFMiCBEb8jQ4IDdrK+5+a0oTy github.com/go-openapi/errors v0.19.9/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= github.com/go-openapi/errors v0.20.1 h1:j23mMDtRxMwIobkpId7sWh7Ddcx4ivaoqUbfXx5P+a8= github.com/go-openapi/errors v0.20.1/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= +github.com/go-openapi/inflect v0.19.0 h1:9jCH9scKIbHeV9m12SmPilScz6krDxKRasNNSNPXu/4= github.com/go-openapi/inflect v0.19.0/go.mod h1:lHpZVlpIQqLyKwJ4N+YSc9hchQy/i12fJykb83CRBH4= github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= github.com/go-openapi/jsonpointer v0.18.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= @@ -939,6 +940,7 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.5 h1:ouewzE6p+/VEB31YYnTbEJdi8pFqKp4P4n85vwo3DHA= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/pkg/apiclient/signal.go b/pkg/apiclient/signal.go index 4d29148e4..bfe911699 100644 --- a/pkg/apiclient/signal.go +++ b/pkg/apiclient/signal.go @@ -3,7 +3,8 @@ package apiclient import ( "context" "fmt" - "log" + + log "github.com/sirupsen/logrus" "github.com/crowdsecurity/crowdsec/pkg/models" "github.com/pkg/errors" @@ -24,6 +25,10 @@ func (s *SignalService) Add(ctx context.Context, signals *models.AddSignalsReque if err != nil { return nil, resp, errors.Wrap(err, "while performing request") } - log.Printf("Signal push response : http %s", resp.Response.Status) + if resp.Response.StatusCode != 200 { + log.Warnf("Signal push response : http %s", resp.Response.Status) + } else { + log.Debugf("Signal push response : http %s", resp.Response.Status) + } return &response, resp, nil } diff --git a/pkg/apiserver/apic.go b/pkg/apiserver/apic.go index 715bf3e14..b81207205 100644 --- a/pkg/apiserver/apic.go +++ b/pkg/apiserver/apic.go @@ -44,6 +44,7 @@ type apic struct { startup bool credentials *csconfig.ApiCredentialsCfg scenarioList []string + consoleConfig *csconfig.ConsoleConfig } func IsInSlice(a string, b []string) bool { @@ -75,7 +76,7 @@ func (a *apic) FetchScenariosListFromDB() ([]string, error) { return scenarios, nil } -func AlertToSignal(alert *models.Alert) *models.AddSignalsRequestItem { +func AlertToSignal(alert *models.Alert, scenarioTrust string) *models.AddSignalsRequestItem { return &models.AddSignalsRequestItem{ Message: alert.Message, Scenario: alert.Scenario, @@ -86,21 +87,23 @@ func AlertToSignal(alert *models.Alert) *models.AddSignalsRequestItem { StopAt: alert.StopAt, CreatedAt: alert.CreatedAt, MachineID: alert.MachineID, + ScenarioTrust: &scenarioTrust, } } -func NewAPIC(config *csconfig.OnlineApiClientCfg, dbClient *database.Client) (*apic, error) { +func NewAPIC(config *csconfig.OnlineApiClientCfg, dbClient *database.Client, consoleConfig *csconfig.ConsoleConfig) (*apic, error) { var err error ret := &apic{ - alertToPush: make(chan []*models.Alert), - dbClient: dbClient, - mu: sync.Mutex{}, - startup: true, - credentials: config.Credentials, - pullTomb: tomb.Tomb{}, - pushTomb: tomb.Tomb{}, - metricsTomb: tomb.Tomb{}, - scenarioList: make([]string, 0), + alertToPush: make(chan []*models.Alert), + dbClient: dbClient, + mu: sync.Mutex{}, + startup: true, + credentials: config.Credentials, + pullTomb: tomb.Tomb{}, + pushTomb: tomb.Tomb{}, + metricsTomb: tomb.Tomb{}, + scenarioList: make([]string, 0), + consoleConfig: consoleConfig, } ret.pullInterval, err = time.ParseDuration(PullInterval) @@ -167,20 +170,39 @@ func (a *apic) Push() error { case alerts := <-a.alertToPush: var signals []*models.AddSignalsRequestItem for _, alert := range alerts { - /*we're only interested into decisions coming from scenarios of the hub*/ - if alert.ScenarioHash == nil || *alert.ScenarioHash == "" { - continue - } - /*and we're not interested into tainted scenarios neither*/ - if alert.ScenarioVersion == nil || *alert.ScenarioVersion == "" || *alert.ScenarioVersion == "?" { - continue - } - /*we also ignore alerts in simulated mode*/ if *alert.Simulated { log.Debugf("simulation enabled for alert (id:%d), will not be sent to CAPI", alert.ID) continue } - signals = append(signals, AlertToSignal(alert)) + scenarioTrust := "certified" + if alert.ScenarioHash == nil || *alert.ScenarioHash == "" { + scenarioTrust = "custom" + } else if alert.ScenarioVersion == nil || *alert.ScenarioVersion == "" || *alert.ScenarioVersion == "?" { + scenarioTrust = "tainted" + } + if len(alert.Decisions) > 0 { + if *alert.Decisions[0].Origin == "cscli" { + scenarioTrust = "manual" + } + } + switch scenarioTrust { + case "manual": + if !*a.consoleConfig.ShareManualDecisions { + log.Debugf("manual decision generated an alert, doesn't send it to CAPI because options is disabled") + continue + } + case "tainted": + if !*a.consoleConfig.ShareTaintedScenarios { + log.Debugf("tainted scenario generated an alert, doesn't send it to CAPI because options is disabled") + continue + } + case "custom": + if !*a.consoleConfig.ShareCustomScenarios { + log.Debugf("custom scenario generated an alert, doesn't send it to CAPI because options is disabled") + continue + } + } + signals = append(signals, AlertToSignal(alert, scenarioTrust)) } a.mu.Lock() cache = append(cache, signals...) @@ -477,8 +499,8 @@ func (a *apic) GetMetrics() (*models.Metrics, error) { version := cwversion.VersionStr() metric := &models.Metrics{ ApilVersion: &version, - Machines: make([]*models.MetricsSoftInfo, 0), - Bouncers: make([]*models.MetricsSoftInfo, 0), + Machines: make([]*models.MetricsAgentInfo, 0), + Bouncers: make([]*models.MetricsBouncerInfo, 0), } machines, err := a.dbClient.ListMachines() if err != nil { @@ -489,17 +511,21 @@ func (a *apic) GetMetrics() (*models.Metrics, error) { return metric, err } for _, machine := range machines { - m := &models.MetricsSoftInfo{ - Version: machine.Version, - Name: machine.MachineId, + m := &models.MetricsAgentInfo{ + Version: machine.Version, + Name: machine.MachineId, + LastUpdate: machine.UpdatedAt.String(), + LastPush: machine.LastPush.String(), } metric.Machines = append(metric.Machines, m) } for _, bouncer := range bouncers { - m := &models.MetricsSoftInfo{ - Version: bouncer.Version, - Name: bouncer.Type, + m := &models.MetricsBouncerInfo{ + Version: bouncer.Version, + CustomName: bouncer.Name, + Name: bouncer.Type, + LastPull: bouncer.LastPull.String(), } metric.Bouncers = append(metric.Bouncers, m) } diff --git a/pkg/apiserver/apiserver.go b/pkg/apiserver/apiserver.go index 1f5013984..7fa1adb5b 100644 --- a/pkg/apiserver/apiserver.go +++ b/pkg/apiserver/apiserver.go @@ -38,6 +38,7 @@ type APIServer struct { httpServer *http.Server apic *apic httpServerTomb tomb.Tomb + consoleConfig *csconfig.ConsoleConfig } // RecoveryWithWriter returns a middleware for a given writer that recovers from any panics and writes a 500 if there was one. @@ -165,19 +166,21 @@ func NewServer(config *csconfig.LocalApiServerCfg) (*APIServer, error) { return }) router.Use(CustomRecoveryWithWriter()) + controller := &controllers.Controller{ - DBClient: dbClient, - Ectx: context.Background(), - Router: router, - Profiles: config.Profiles, - Log: clog, + DBClient: dbClient, + Ectx: context.Background(), + Router: router, + Profiles: config.Profiles, + Log: clog, + ConsoleConfig: config.ConsoleConfig, } var apiClient *apic if config.OnlineClient != nil && config.OnlineClient.Credentials != nil { log.Printf("Loading CAPI pusher") - apiClient, err = NewAPIC(config.OnlineClient, dbClient) + apiClient, err = NewAPIC(config.OnlineClient, dbClient, config.ConsoleConfig) if err != nil { return &APIServer{}, err } @@ -197,6 +200,7 @@ func NewServer(config *csconfig.LocalApiServerCfg) (*APIServer, error) { router: router, apic: apiClient, httpServerTomb: tomb.Tomb{}, + consoleConfig: config.ConsoleConfig, }, nil } diff --git a/pkg/apiserver/apiserver_test.go b/pkg/apiserver/apiserver_test.go index 927f118f2..4e3ac3f28 100644 --- a/pkg/apiserver/apiserver_test.go +++ b/pkg/apiserver/apiserver_test.go @@ -49,6 +49,11 @@ func LoadTestConfig() csconfig.Config { ListenURI: "http://127.0.0.1:8080", DbConfig: &dbconfig, ProfilesPath: "./tests/profiles.yaml", + ConsoleConfig: &csconfig.ConsoleConfig{ + ShareManualDecisions: new(bool), + ShareTaintedScenarios: new(bool), + ShareCustomScenarios: new(bool), + }, } apiConfig := csconfig.APICfg{ Server: &apiServerConfig, @@ -76,6 +81,11 @@ func LoadTestConfigForwardedFor() csconfig.Config { DbConfig: &dbconfig, ProfilesPath: "./tests/profiles.yaml", UseForwardedForHeaders: true, + ConsoleConfig: &csconfig.ConsoleConfig{ + ShareManualDecisions: new(bool), + ShareTaintedScenarios: new(bool), + ShareCustomScenarios: new(bool), + }, } apiConfig := csconfig.APICfg{ Server: &apiServerConfig, diff --git a/pkg/apiserver/controllers/controller.go b/pkg/apiserver/controllers/controller.go index 77c7be40d..519418f26 100644 --- a/pkg/apiserver/controllers/controller.go +++ b/pkg/apiserver/controllers/controller.go @@ -2,6 +2,8 @@ package controllers import ( "context" + "net/http" + "github.com/alexliesenfeld/health" v1 "github.com/crowdsecurity/crowdsec/pkg/apiserver/controllers/v1" "github.com/crowdsecurity/crowdsec/pkg/csconfig" @@ -10,7 +12,6 @@ import ( "github.com/crowdsecurity/crowdsec/pkg/models" "github.com/gin-gonic/gin" log "github.com/sirupsen/logrus" - "net/http" ) type Controller struct { @@ -21,6 +22,7 @@ type Controller struct { CAPIChan chan []*models.Alert PluginChannel chan csplugin.ProfileAlert Log *log.Logger + ConsoleConfig *csconfig.ConsoleConfig } func (c *Controller) Init() error { @@ -51,7 +53,7 @@ func serveHealth() http.HandlerFunc { } func (c *Controller) NewV1() error { - handlerV1, err := v1.New(c.DBClient, c.Ectx, c.Profiles, c.CAPIChan, c.PluginChannel) + handlerV1, err := v1.New(c.DBClient, c.Ectx, c.Profiles, c.CAPIChan, c.PluginChannel, *c.ConsoleConfig) if err != nil { return err } diff --git a/pkg/apiserver/controllers/v1/alerts.go b/pkg/apiserver/controllers/v1/alerts.go index f3999d28a..f7a13c572 100644 --- a/pkg/apiserver/controllers/v1/alerts.go +++ b/pkg/apiserver/controllers/v1/alerts.go @@ -156,10 +156,14 @@ func (c *Controller) CreateAlert(gctx *gin.Context) { gctx.JSON(http.StatusInternalServerError, gin.H{"message": err.Error()}) return } + if !matched { continue } - alert.Decisions = append(alert.Decisions, profileDecisions...) + + if len(alert.Decisions) == 0 { // non manual decision + alert.Decisions = append(alert.Decisions, profileDecisions...) + } profileAlert := *alert c.sendAlertToPluginChannel(&profileAlert, uint(pIdx)) if profile.OnSuccess == "break" { diff --git a/pkg/apiserver/controllers/v1/controller.go b/pkg/apiserver/controllers/v1/controller.go index f6bbb3ce9..d636300c2 100644 --- a/pkg/apiserver/controllers/v1/controller.go +++ b/pkg/apiserver/controllers/v1/controller.go @@ -18,9 +18,10 @@ type Controller struct { Profiles []*csconfig.ProfileCfg CAPIChan chan []*models.Alert PluginChannel chan csplugin.ProfileAlert + ConsoleConfig csconfig.ConsoleConfig } -func New(dbClient *database.Client, ctx context.Context, profiles []*csconfig.ProfileCfg, capiChan chan []*models.Alert, pluginChannel chan csplugin.ProfileAlert) (*Controller, error) { +func New(dbClient *database.Client, ctx context.Context, profiles []*csconfig.ProfileCfg, capiChan chan []*models.Alert, pluginChannel chan csplugin.ProfileAlert, consoleConfig csconfig.ConsoleConfig) (*Controller, error) { var err error v1 := &Controller{ Ectx: ctx, @@ -29,6 +30,7 @@ func New(dbClient *database.Client, ctx context.Context, profiles []*csconfig.Pr Profiles: profiles, CAPIChan: capiChan, PluginChannel: pluginChannel, + ConsoleConfig: consoleConfig, } v1.Middlewares, err = middlewares.NewMiddlewares(dbClient) if err != nil { diff --git a/pkg/csconfig/api.go b/pkg/csconfig/api.go index 73da963b5..b2fb8edde 100644 --- a/pkg/csconfig/api.go +++ b/pkg/csconfig/api.go @@ -85,6 +85,8 @@ type LocalApiServerCfg struct { LogMedia string `yaml:"-"` OnlineClient *OnlineApiClientCfg `yaml:"online_client"` ProfilesPath string `yaml:"profiles_path,omitempty"` + ConsoleConfigPath string `yaml:"console_path,omitempty"` + ConsoleConfig *ConsoleConfig `yaml:"-"` Profiles []*ProfileCfg `yaml:"-"` LogLevel *log.Level `yaml:"log_level"` UseForwardedForHeaders bool `yaml:"use_forwarded_for_headers,omitempty"` @@ -114,6 +116,13 @@ func (c *Config) LoadAPIServer() error { if err := c.API.Server.LoadProfiles(); err != nil { return errors.Wrap(err, "while loading profiles for LAPI") } + if c.API.Server.ConsoleConfigPath == "" { + c.API.Server.ConsoleConfigPath = DefaultConsoleConfgFilePath + } + if err := c.API.Server.LoadConsoleConfig(); err != nil { + return errors.Wrap(err, "while loading console options") + } + if c.API.Server.OnlineClient != nil && c.API.Server.OnlineClient.CredentialsFilePath != "" { if err := c.API.Server.OnlineClient.Load(); err != nil { return errors.Wrap(err, "loading online client credentials") diff --git a/pkg/csconfig/api_test.go b/pkg/csconfig/api_test.go index a4f78c97c..730b5d172 100644 --- a/pkg/csconfig/api_test.go +++ b/pkg/csconfig/api_test.go @@ -8,6 +8,7 @@ import ( "strings" "testing" + "github.com/crowdsecurity/crowdsec/pkg/types" "github.com/stretchr/testify/assert" "gopkg.in/yaml.v2" ) @@ -206,6 +207,12 @@ func TestLoadAPIServer(t *testing.T) { DbPath: "./tests/test.db", Type: "sqlite", }, + ConsoleConfigPath: "/etc/crowdsec/console_config.yaml", + ConsoleConfig: &ConsoleConfig{ + ShareManualDecisions: types.BoolPtr(false), + ShareTaintedScenarios: types.BoolPtr(true), + ShareCustomScenarios: types.BoolPtr(true), + }, LogDir: LogDirFullPath, LogMedia: "stdout", OnlineClient: &OnlineApiClientCfg{ diff --git a/pkg/csconfig/console.go b/pkg/csconfig/console.go new file mode 100644 index 000000000..b617e442a --- /dev/null +++ b/pkg/csconfig/console.go @@ -0,0 +1,83 @@ +package csconfig + +import ( + "fmt" + "io/ioutil" + "os" + + "github.com/crowdsecurity/crowdsec/pkg/types" + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" + "gopkg.in/yaml.v2" +) + +const ( + SEND_CUSTOM_SCENARIOS = "custom" + SEND_TAINTED_SCENARIOS = "tainted" + SEND_MANUAL_SCENARIOS = "manual" +) + +var DefaultConsoleConfgFilePath = "/etc/crowdsec/console_config.yaml" + +var CONSOLE_CONFIGS = []string{SEND_CUSTOM_SCENARIOS, SEND_MANUAL_SCENARIOS, SEND_TAINTED_SCENARIOS} + +type ConsoleConfig struct { + ShareManualDecisions *bool `yaml:"share_manual_decisions"` + ShareTaintedScenarios *bool `yaml:"share_tainted"` + ShareCustomScenarios *bool `yaml:"share_custom"` +} + +func (c *LocalApiServerCfg) LoadConsoleConfig() error { + c.ConsoleConfig = &ConsoleConfig{} + if _, err := os.Stat(c.ConsoleConfigPath); err != nil && os.IsNotExist(err) { + log.Debugf("no console configuration to load") + c.ConsoleConfig.ShareCustomScenarios = types.BoolPtr(true) + c.ConsoleConfig.ShareTaintedScenarios = types.BoolPtr(true) + c.ConsoleConfig.ShareManualDecisions = types.BoolPtr(false) + return nil + } + + yamlFile, err := ioutil.ReadFile(c.ConsoleConfigPath) + if err != nil { + return fmt.Errorf("reading console config file '%s': %s", c.ConsoleConfigPath, err) + } + err = yaml.Unmarshal(yamlFile, c.ConsoleConfig) + if err != nil { + return fmt.Errorf("unmarshaling console config file '%s': %s", c.ConsoleConfigPath, err) + } + + if c.ConsoleConfig.ShareCustomScenarios == nil { + log.Debugf("no share_custom scenarios found, setting to true") + c.ConsoleConfig.ShareCustomScenarios = types.BoolPtr(true) + } + if c.ConsoleConfig.ShareTaintedScenarios == nil { + log.Debugf("no share_tainted scenarios found, setting to true") + c.ConsoleConfig.ShareTaintedScenarios = types.BoolPtr(true) + } + if c.ConsoleConfig.ShareManualDecisions == nil { + log.Debugf("no share_manual scenarios found, setting to false") + c.ConsoleConfig.ShareManualDecisions = types.BoolPtr(false) + } + log.Debugf("Console configuration '%s' loaded successfully", c.ConsoleConfigPath) + + return nil +} + +func (c *LocalApiServerCfg) DumpConsoleConfig() error { + var out []byte + var err error + + if out, err = yaml.Marshal(c.ConsoleConfig); err != nil { + return errors.Wrapf(err, "while marshaling ConsoleConfig (for %s)", c.ConsoleConfigPath) + } + if c.ConsoleConfigPath == "" { + log.Debugf("Empty console_path, defaulting to %s", DefaultConsoleConfgFilePath) + c.ConsoleConfigPath = DefaultConsoleConfgFilePath + } + + if err := os.WriteFile(c.ConsoleConfigPath, out, 0600); err != nil { + return errors.Wrapf(err, "while dumping console config to %s", c.ConsoleConfigPath) + } + + return nil +} diff --git a/pkg/database/decisions.go b/pkg/database/decisions.go index 3132dce4d..f2e0946e2 100644 --- a/pkg/database/decisions.go +++ b/pkg/database/decisions.go @@ -15,7 +15,6 @@ import ( func BuildDecisionRequestWithFilter(query *ent.DecisionQuery, filter map[string][]string) (*ent.DecisionQuery, error) { - //func BuildDecisionRequestWithFilter(query *ent.Query, filter map[string][]string) (*ent.DecisionQuery, error) { var err error var start_ip, start_sfx, end_ip, end_sfx int64 var ip_sz int diff --git a/pkg/database/ent/machine.go b/pkg/database/ent/machine.go index f84b2d4b8..73466f1f8 100644 --- a/pkg/database/ent/machine.go +++ b/pkg/database/ent/machine.go @@ -20,6 +20,8 @@ type Machine struct { CreatedAt time.Time `json:"created_at,omitempty"` // UpdatedAt holds the value of the "updated_at" field. UpdatedAt time.Time `json:"updated_at,omitempty"` + // LastPush holds the value of the "last_push" field. + LastPush time.Time `json:"last_push,omitempty"` // MachineId holds the value of the "machineId" field. MachineId string `json:"machineId,omitempty"` // Password holds the value of the "password" field. @@ -68,7 +70,7 @@ func (*Machine) scanValues(columns []string) ([]interface{}, error) { values[i] = new(sql.NullInt64) case machine.FieldMachineId, machine.FieldPassword, machine.FieldIpAddress, machine.FieldScenarios, machine.FieldVersion, machine.FieldStatus: values[i] = new(sql.NullString) - case machine.FieldCreatedAt, machine.FieldUpdatedAt: + case machine.FieldCreatedAt, machine.FieldUpdatedAt, machine.FieldLastPush: values[i] = new(sql.NullTime) default: return nil, fmt.Errorf("unexpected column %q for type Machine", columns[i]) @@ -103,6 +105,12 @@ func (m *Machine) assignValues(columns []string, values []interface{}) error { } else if value.Valid { m.UpdatedAt = value.Time } + case machine.FieldLastPush: + if value, ok := values[i].(*sql.NullTime); !ok { + return fmt.Errorf("unexpected type %T for field last_push", values[i]) + } else if value.Valid { + m.LastPush = value.Time + } case machine.FieldMachineId: if value, ok := values[i].(*sql.NullString); !ok { return fmt.Errorf("unexpected type %T for field machineId", values[i]) @@ -182,6 +190,8 @@ func (m *Machine) String() string { builder.WriteString(m.CreatedAt.Format(time.ANSIC)) builder.WriteString(", updated_at=") builder.WriteString(m.UpdatedAt.Format(time.ANSIC)) + builder.WriteString(", last_push=") + builder.WriteString(m.LastPush.Format(time.ANSIC)) builder.WriteString(", machineId=") builder.WriteString(m.MachineId) builder.WriteString(", password=") diff --git a/pkg/database/ent/machine/machine.go b/pkg/database/ent/machine/machine.go index dbd6d0b90..bca842a73 100644 --- a/pkg/database/ent/machine/machine.go +++ b/pkg/database/ent/machine/machine.go @@ -15,6 +15,8 @@ const ( FieldCreatedAt = "created_at" // FieldUpdatedAt holds the string denoting the updated_at field in the database. FieldUpdatedAt = "updated_at" + // FieldLastPush holds the string denoting the last_push field in the database. + FieldLastPush = "last_push" // FieldMachineId holds the string denoting the machineid field in the database. FieldMachineId = "machine_id" // FieldPassword holds the string denoting the password field in the database. @@ -47,6 +49,7 @@ var Columns = []string{ FieldID, FieldCreatedAt, FieldUpdatedAt, + FieldLastPush, FieldMachineId, FieldPassword, FieldIpAddress, @@ -71,6 +74,8 @@ var ( DefaultCreatedAt func() time.Time // DefaultUpdatedAt holds the default value on creation for the "updated_at" field. DefaultUpdatedAt func() time.Time + // DefaultLastPush holds the default value on creation for the "last_push" field. + DefaultLastPush func() time.Time // ScenariosValidator is a validator for the "scenarios" field. It is called by the builders before save. ScenariosValidator func(string) error // DefaultIsValidated holds the default value on creation for the "isValidated" field. diff --git a/pkg/database/ent/machine/where.go b/pkg/database/ent/machine/where.go index 8fa0f8039..abfe05a81 100644 --- a/pkg/database/ent/machine/where.go +++ b/pkg/database/ent/machine/where.go @@ -107,6 +107,13 @@ func UpdatedAt(v time.Time) predicate.Machine { }) } +// LastPush applies equality check predicate on the "last_push" field. It's identical to LastPushEQ. +func LastPush(v time.Time) predicate.Machine { + return predicate.Machine(func(s *sql.Selector) { + s.Where(sql.EQ(s.C(FieldLastPush), v)) + }) +} + // MachineId applies equality check predicate on the "machineId" field. It's identical to MachineIdEQ. func MachineId(v string) predicate.Machine { return predicate.Machine(func(s *sql.Selector) { @@ -308,6 +315,96 @@ func UpdatedAtLTE(v time.Time) predicate.Machine { }) } +// LastPushEQ applies the EQ predicate on the "last_push" field. +func LastPushEQ(v time.Time) predicate.Machine { + return predicate.Machine(func(s *sql.Selector) { + s.Where(sql.EQ(s.C(FieldLastPush), v)) + }) +} + +// LastPushNEQ applies the NEQ predicate on the "last_push" field. +func LastPushNEQ(v time.Time) predicate.Machine { + return predicate.Machine(func(s *sql.Selector) { + s.Where(sql.NEQ(s.C(FieldLastPush), v)) + }) +} + +// LastPushIn applies the In predicate on the "last_push" field. +func LastPushIn(vs ...time.Time) predicate.Machine { + v := make([]interface{}, len(vs)) + for i := range v { + v[i] = vs[i] + } + return predicate.Machine(func(s *sql.Selector) { + // if not arguments were provided, append the FALSE constants, + // since we can't apply "IN ()". This will make this predicate falsy. + if len(v) == 0 { + s.Where(sql.False()) + return + } + s.Where(sql.In(s.C(FieldLastPush), v...)) + }) +} + +// LastPushNotIn applies the NotIn predicate on the "last_push" field. +func LastPushNotIn(vs ...time.Time) predicate.Machine { + v := make([]interface{}, len(vs)) + for i := range v { + v[i] = vs[i] + } + return predicate.Machine(func(s *sql.Selector) { + // if not arguments were provided, append the FALSE constants, + // since we can't apply "IN ()". This will make this predicate falsy. + if len(v) == 0 { + s.Where(sql.False()) + return + } + s.Where(sql.NotIn(s.C(FieldLastPush), v...)) + }) +} + +// LastPushGT applies the GT predicate on the "last_push" field. +func LastPushGT(v time.Time) predicate.Machine { + return predicate.Machine(func(s *sql.Selector) { + s.Where(sql.GT(s.C(FieldLastPush), v)) + }) +} + +// LastPushGTE applies the GTE predicate on the "last_push" field. +func LastPushGTE(v time.Time) predicate.Machine { + return predicate.Machine(func(s *sql.Selector) { + s.Where(sql.GTE(s.C(FieldLastPush), v)) + }) +} + +// LastPushLT applies the LT predicate on the "last_push" field. +func LastPushLT(v time.Time) predicate.Machine { + return predicate.Machine(func(s *sql.Selector) { + s.Where(sql.LT(s.C(FieldLastPush), v)) + }) +} + +// LastPushLTE applies the LTE predicate on the "last_push" field. +func LastPushLTE(v time.Time) predicate.Machine { + return predicate.Machine(func(s *sql.Selector) { + s.Where(sql.LTE(s.C(FieldLastPush), v)) + }) +} + +// LastPushIsNil applies the IsNil predicate on the "last_push" field. +func LastPushIsNil() predicate.Machine { + return predicate.Machine(func(s *sql.Selector) { + s.Where(sql.IsNull(s.C(FieldLastPush))) + }) +} + +// LastPushNotNil applies the NotNil predicate on the "last_push" field. +func LastPushNotNil() predicate.Machine { + return predicate.Machine(func(s *sql.Selector) { + s.Where(sql.NotNull(s.C(FieldLastPush))) + }) +} + // MachineIdEQ applies the EQ predicate on the "machineId" field. func MachineIdEQ(v string) predicate.Machine { return predicate.Machine(func(s *sql.Selector) { diff --git a/pkg/database/ent/machine_create.go b/pkg/database/ent/machine_create.go index d545091f6..1c1508850 100644 --- a/pkg/database/ent/machine_create.go +++ b/pkg/database/ent/machine_create.go @@ -49,6 +49,20 @@ func (mc *MachineCreate) SetNillableUpdatedAt(t *time.Time) *MachineCreate { return mc } +// SetLastPush sets the "last_push" field. +func (mc *MachineCreate) SetLastPush(t time.Time) *MachineCreate { + mc.mutation.SetLastPush(t) + return mc +} + +// SetNillableLastPush sets the "last_push" field if the given value is not nil. +func (mc *MachineCreate) SetNillableLastPush(t *time.Time) *MachineCreate { + if t != nil { + mc.SetLastPush(*t) + } + return mc +} + // SetMachineId sets the "machineId" field. func (mc *MachineCreate) SetMachineId(s string) *MachineCreate { mc.mutation.SetMachineId(s) @@ -217,6 +231,10 @@ func (mc *MachineCreate) defaults() { v := machine.DefaultUpdatedAt() mc.mutation.SetUpdatedAt(v) } + if _, ok := mc.mutation.LastPush(); !ok { + v := machine.DefaultLastPush() + mc.mutation.SetLastPush(v) + } if _, ok := mc.mutation.IsValidated(); !ok { v := machine.DefaultIsValidated mc.mutation.SetIsValidated(v) @@ -291,6 +309,14 @@ func (mc *MachineCreate) createSpec() (*Machine, *sqlgraph.CreateSpec) { }) _node.UpdatedAt = value } + if value, ok := mc.mutation.LastPush(); ok { + _spec.Fields = append(_spec.Fields, &sqlgraph.FieldSpec{ + Type: field.TypeTime, + Value: value, + Column: machine.FieldLastPush, + }) + _node.LastPush = value + } if value, ok := mc.mutation.MachineId(); ok { _spec.Fields = append(_spec.Fields, &sqlgraph.FieldSpec{ Type: field.TypeString, diff --git a/pkg/database/ent/machine_update.go b/pkg/database/ent/machine_update.go index 4cc103719..5242897c9 100644 --- a/pkg/database/ent/machine_update.go +++ b/pkg/database/ent/machine_update.go @@ -56,6 +56,26 @@ func (mu *MachineUpdate) SetNillableUpdatedAt(t *time.Time) *MachineUpdate { return mu } +// SetLastPush sets the "last_push" field. +func (mu *MachineUpdate) SetLastPush(t time.Time) *MachineUpdate { + mu.mutation.SetLastPush(t) + return mu +} + +// SetNillableLastPush sets the "last_push" field if the given value is not nil. +func (mu *MachineUpdate) SetNillableLastPush(t *time.Time) *MachineUpdate { + if t != nil { + mu.SetLastPush(*t) + } + return mu +} + +// ClearLastPush clears the value of the "last_push" field. +func (mu *MachineUpdate) ClearLastPush() *MachineUpdate { + mu.mutation.ClearLastPush() + return mu +} + // SetMachineId sets the "machineId" field. func (mu *MachineUpdate) SetMachineId(s string) *MachineUpdate { mu.mutation.SetMachineId(s) @@ -291,6 +311,19 @@ func (mu *MachineUpdate) sqlSave(ctx context.Context) (n int, err error) { Column: machine.FieldUpdatedAt, }) } + if value, ok := mu.mutation.LastPush(); ok { + _spec.Fields.Set = append(_spec.Fields.Set, &sqlgraph.FieldSpec{ + Type: field.TypeTime, + Value: value, + Column: machine.FieldLastPush, + }) + } + if mu.mutation.LastPushCleared() { + _spec.Fields.Clear = append(_spec.Fields.Clear, &sqlgraph.FieldSpec{ + Type: field.TypeTime, + Column: machine.FieldLastPush, + }) + } if value, ok := mu.mutation.MachineId(); ok { _spec.Fields.Set = append(_spec.Fields.Set, &sqlgraph.FieldSpec{ Type: field.TypeString, @@ -459,6 +492,26 @@ func (muo *MachineUpdateOne) SetNillableUpdatedAt(t *time.Time) *MachineUpdateOn return muo } +// SetLastPush sets the "last_push" field. +func (muo *MachineUpdateOne) SetLastPush(t time.Time) *MachineUpdateOne { + muo.mutation.SetLastPush(t) + return muo +} + +// SetNillableLastPush sets the "last_push" field if the given value is not nil. +func (muo *MachineUpdateOne) SetNillableLastPush(t *time.Time) *MachineUpdateOne { + if t != nil { + muo.SetLastPush(*t) + } + return muo +} + +// ClearLastPush clears the value of the "last_push" field. +func (muo *MachineUpdateOne) ClearLastPush() *MachineUpdateOne { + muo.mutation.ClearLastPush() + return muo +} + // SetMachineId sets the "machineId" field. func (muo *MachineUpdateOne) SetMachineId(s string) *MachineUpdateOne { muo.mutation.SetMachineId(s) @@ -718,6 +771,19 @@ func (muo *MachineUpdateOne) sqlSave(ctx context.Context) (_node *Machine, err e Column: machine.FieldUpdatedAt, }) } + if value, ok := muo.mutation.LastPush(); ok { + _spec.Fields.Set = append(_spec.Fields.Set, &sqlgraph.FieldSpec{ + Type: field.TypeTime, + Value: value, + Column: machine.FieldLastPush, + }) + } + if muo.mutation.LastPushCleared() { + _spec.Fields.Clear = append(_spec.Fields.Clear, &sqlgraph.FieldSpec{ + Type: field.TypeTime, + Column: machine.FieldLastPush, + }) + } if value, ok := muo.mutation.MachineId(); ok { _spec.Fields.Set = append(_spec.Fields.Set, &sqlgraph.FieldSpec{ Type: field.TypeString, diff --git a/pkg/database/ent/migrate/schema.go b/pkg/database/ent/migrate/schema.go index 94cad488e..c70400612 100644 --- a/pkg/database/ent/migrate/schema.go +++ b/pkg/database/ent/migrate/schema.go @@ -137,6 +137,7 @@ var ( {Name: "id", Type: field.TypeInt, Increment: true}, {Name: "created_at", Type: field.TypeTime}, {Name: "updated_at", Type: field.TypeTime}, + {Name: "last_push", Type: field.TypeTime, Nullable: true}, {Name: "machine_id", Type: field.TypeString, Unique: true}, {Name: "password", Type: field.TypeString}, {Name: "ip_address", Type: field.TypeString}, diff --git a/pkg/database/ent/mutation.go b/pkg/database/ent/mutation.go index b4804d5f0..1bb87c802 100644 --- a/pkg/database/ent/mutation.go +++ b/pkg/database/ent/mutation.go @@ -4986,6 +4986,7 @@ type MachineMutation struct { id *int created_at *time.Time updated_at *time.Time + last_push *time.Time machineId *string password *string ipAddress *string @@ -5153,6 +5154,55 @@ func (m *MachineMutation) ResetUpdatedAt() { m.updated_at = nil } +// SetLastPush sets the "last_push" field. +func (m *MachineMutation) SetLastPush(t time.Time) { + m.last_push = &t +} + +// LastPush returns the value of the "last_push" field in the mutation. +func (m *MachineMutation) LastPush() (r time.Time, exists bool) { + v := m.last_push + if v == nil { + return + } + return *v, true +} + +// OldLastPush returns the old "last_push" field's value of the Machine entity. +// If the Machine object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *MachineMutation) OldLastPush(ctx context.Context) (v time.Time, err error) { + if !m.op.Is(OpUpdateOne) { + return v, fmt.Errorf("OldLastPush is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, fmt.Errorf("OldLastPush requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldLastPush: %w", err) + } + return oldValue.LastPush, nil +} + +// ClearLastPush clears the value of the "last_push" field. +func (m *MachineMutation) ClearLastPush() { + m.last_push = nil + m.clearedFields[machine.FieldLastPush] = struct{}{} +} + +// LastPushCleared returns if the "last_push" field was cleared in this mutation. +func (m *MachineMutation) LastPushCleared() bool { + _, ok := m.clearedFields[machine.FieldLastPush] + return ok +} + +// ResetLastPush resets all changes to the "last_push" field. +func (m *MachineMutation) ResetLastPush() { + m.last_push = nil + delete(m.clearedFields, machine.FieldLastPush) +} + // SetMachineId sets the "machineId" field. func (m *MachineMutation) SetMachineId(s string) { m.machineId = &s @@ -5517,13 +5567,16 @@ func (m *MachineMutation) Type() string { // order to get all numeric fields that were incremented/decremented, call // AddedFields(). func (m *MachineMutation) Fields() []string { - fields := make([]string, 0, 9) + fields := make([]string, 0, 10) if m.created_at != nil { fields = append(fields, machine.FieldCreatedAt) } if m.updated_at != nil { fields = append(fields, machine.FieldUpdatedAt) } + if m.last_push != nil { + fields = append(fields, machine.FieldLastPush) + } if m.machineId != nil { fields = append(fields, machine.FieldMachineId) } @@ -5557,6 +5610,8 @@ func (m *MachineMutation) Field(name string) (ent.Value, bool) { return m.CreatedAt() case machine.FieldUpdatedAt: return m.UpdatedAt() + case machine.FieldLastPush: + return m.LastPush() case machine.FieldMachineId: return m.MachineId() case machine.FieldPassword: @@ -5584,6 +5639,8 @@ func (m *MachineMutation) OldField(ctx context.Context, name string) (ent.Value, return m.OldCreatedAt(ctx) case machine.FieldUpdatedAt: return m.OldUpdatedAt(ctx) + case machine.FieldLastPush: + return m.OldLastPush(ctx) case machine.FieldMachineId: return m.OldMachineId(ctx) case machine.FieldPassword: @@ -5621,6 +5678,13 @@ func (m *MachineMutation) SetField(name string, value ent.Value) error { } m.SetUpdatedAt(v) return nil + case machine.FieldLastPush: + v, ok := value.(time.Time) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetLastPush(v) + return nil case machine.FieldMachineId: v, ok := value.(string) if !ok { @@ -5700,6 +5764,9 @@ func (m *MachineMutation) AddField(name string, value ent.Value) error { // mutation. func (m *MachineMutation) ClearedFields() []string { var fields []string + if m.FieldCleared(machine.FieldLastPush) { + fields = append(fields, machine.FieldLastPush) + } if m.FieldCleared(machine.FieldScenarios) { fields = append(fields, machine.FieldScenarios) } @@ -5723,6 +5790,9 @@ func (m *MachineMutation) FieldCleared(name string) bool { // error if the field is not defined in the schema. func (m *MachineMutation) ClearField(name string) error { switch name { + case machine.FieldLastPush: + m.ClearLastPush() + return nil case machine.FieldScenarios: m.ClearScenarios() return nil @@ -5746,6 +5816,9 @@ func (m *MachineMutation) ResetField(name string) error { case machine.FieldUpdatedAt: m.ResetUpdatedAt() return nil + case machine.FieldLastPush: + m.ResetLastPush() + return nil case machine.FieldMachineId: m.ResetMachineId() return nil diff --git a/pkg/database/ent/runtime.go b/pkg/database/ent/runtime.go index f6be19afa..dc7594caa 100644 --- a/pkg/database/ent/runtime.go +++ b/pkg/database/ent/runtime.go @@ -112,12 +112,16 @@ func init() { machineDescUpdatedAt := machineFields[1].Descriptor() // machine.DefaultUpdatedAt holds the default value on creation for the updated_at field. machine.DefaultUpdatedAt = machineDescUpdatedAt.Default.(func() time.Time) + // machineDescLastPush is the schema descriptor for last_push field. + machineDescLastPush := machineFields[2].Descriptor() + // machine.DefaultLastPush holds the default value on creation for the last_push field. + machine.DefaultLastPush = machineDescLastPush.Default.(func() time.Time) // machineDescScenarios is the schema descriptor for scenarios field. - machineDescScenarios := machineFields[5].Descriptor() + machineDescScenarios := machineFields[6].Descriptor() // machine.ScenariosValidator is a validator for the "scenarios" field. It is called by the builders before save. machine.ScenariosValidator = machineDescScenarios.Validators[0].(func(string) error) // machineDescIsValidated is the schema descriptor for isValidated field. - machineDescIsValidated := machineFields[7].Descriptor() + machineDescIsValidated := machineFields[8].Descriptor() // machine.DefaultIsValidated holds the default value on creation for the isValidated field. machine.DefaultIsValidated = machineDescIsValidated.Default.(bool) metaFields := schema.Meta{}.Fields() diff --git a/pkg/database/ent/schema/machine.go b/pkg/database/ent/schema/machine.go index 00b731f6d..e81529974 100644 --- a/pkg/database/ent/schema/machine.go +++ b/pkg/database/ent/schema/machine.go @@ -20,6 +20,8 @@ func (Machine) Fields() []ent.Field { Default(time.Now), field.Time("updated_at"). Default(time.Now), + field.Time("last_push"). + Default(time.Now).Optional(), field.String("machineId").Unique(), field.String("password").Sensitive(), field.String("ipAddress"), diff --git a/pkg/database/machines.go b/pkg/database/machines.go index 3e424a7dc..00dcc5172 100644 --- a/pkg/database/machines.go +++ b/pkg/database/machines.go @@ -110,6 +110,14 @@ func (c *Client) DeleteWatcher(name string) error { return nil } +func (c *Client) UpdateMachineLastPush(machineID string) error { + _, err := c.Ent.Machine.Update().Where(machine.MachineIdEQ(machineID)).SetLastPush(time.Now()).Save(c.CTX) + if err != nil { + return errors.Wrapf(UpdateFail, "updating machine last_push: %s", err) + } + return nil +} + func (c *Client) UpdateMachineScenarios(scenarios string, ID int) error { _, err := c.Ent.Machine.UpdateOneID(ID). SetUpdatedAt(time.Now()). diff --git a/pkg/models/add_signals_request_item.go b/pkg/models/add_signals_request_item.go index c03930603..2a5acdd23 100644 --- a/pkg/models/add_signals_request_item.go +++ b/pkg/models/add_signals_request_item.go @@ -37,6 +37,10 @@ type AddSignalsRequestItem struct { // Required: true ScenarioHash *string `json:"scenario_hash"` + // scenario trust + // Required: true + ScenarioTrust *string `json:"scenario_trust"` + // scenario version // Required: true ScenarioVersion *string `json:"scenario_version"` @@ -70,6 +74,10 @@ func (m *AddSignalsRequestItem) Validate(formats strfmt.Registry) error { res = append(res, err) } + if err := m.validateScenarioTrust(formats); err != nil { + res = append(res, err) + } + if err := m.validateScenarioVersion(formats); err != nil { res = append(res, err) } @@ -119,6 +127,15 @@ func (m *AddSignalsRequestItem) validateScenarioHash(formats strfmt.Registry) er return nil } +func (m *AddSignalsRequestItem) validateScenarioTrust(formats strfmt.Registry) error { + + if err := validate.Required("scenario_trust", "body", m.ScenarioTrust); err != nil { + return err + } + + return nil +} + func (m *AddSignalsRequestItem) validateScenarioVersion(formats strfmt.Registry) error { if err := validate.Required("scenario_version", "body", m.ScenarioVersion); err != nil { diff --git a/pkg/models/decision.go b/pkg/models/decision.go index 5e317d73f..1a8d1b912 100644 --- a/pkg/models/decision.go +++ b/pkg/models/decision.go @@ -19,7 +19,7 @@ import ( // swagger:model Decision type Decision struct { - // duration + // the duration of the decisions // Required: true Duration *string `json:"duration"` @@ -47,6 +47,9 @@ type Decision struct { // Required: true Type *string `json:"type"` + // the date until the decisions must be active + Until string `json:"until,omitempty"` + // the value of the decision scope : an IP, a range, a username, etc // Required: true Value *string `json:"value"` diff --git a/pkg/models/localapi_swagger.yaml b/pkg/models/localapi_swagger.yaml index b30036fc2..0045c139f 100644 --- a/pkg/models/localapi_swagger.yaml +++ b/pkg/models/localapi_swagger.yaml @@ -777,17 +777,34 @@ definitions: bouncers: type: array items: - $ref: '#/definitions/MetricsSoftInfo' + $ref: '#/definitions/MetricsBouncerInfo' machines: type: array items: - $ref: '#/definitions/MetricsSoftInfo' + $ref: '#/definitions/MetricsAgentInfo' required: - apil_version - bouncers - machines - MetricsSoftInfo: - title: MetricsSoftInfo + MetricsBouncerInfo: + title: MetricsBouncerInfo + description: Software version info (so we can warn users about out-of-date software). The software name and the version are "guessed" from the user-agent + type: object + properties: + custom_name: + type: string + description: name of the component + name: + type: string + description: bouncer type (firewall, php ...) + version: + type: string + description: software version + last_pull: + type: string + description: last bouncer pull date + MetricsAgentInfo: + title: MetricsAgentInfo description: Software version info (so we can warn users about out-of-date software). The software name and the version are "guessed" from the user-agent type: object properties: @@ -797,6 +814,12 @@ definitions: version: type: string description: software version + last_update: + type: string + description: last agent update date + last_push: + type: string + description: last agent push date Decision: title: Decision type: object @@ -818,7 +841,11 @@ definitions: description: 'the value of the decision scope : an IP, a range, a username, etc' type: string duration: + description: 'the duration of the decisions' type: string + until: + type: string + description: 'the date until the decisions must be active' scenario: type: string simulated: @@ -926,6 +953,7 @@ definitions: - "source" - "start_at" - "stop_at" + - "scenario_trust" properties: scenario_hash: type: "string" @@ -939,6 +967,8 @@ definitions: $ref: "#/definitions/Source" scenario_version: type: "string" + scenario_trust: + type: "string" message: type: "string" description: "a human readable message" diff --git a/pkg/models/metrics.go b/pkg/models/metrics.go index 5b647bfbd..573678d1f 100644 --- a/pkg/models/metrics.go +++ b/pkg/models/metrics.go @@ -26,11 +26,11 @@ type Metrics struct { // bouncers // Required: true - Bouncers []*MetricsSoftInfo `json:"bouncers"` + Bouncers []*MetricsBouncerInfo `json:"bouncers"` // machines // Required: true - Machines []*MetricsSoftInfo `json:"machines"` + Machines []*MetricsAgentInfo `json:"machines"` } // Validate validates this metrics diff --git a/pkg/models/metrics_soft_info.go b/pkg/models/metrics_agent_info.go similarity index 54% rename from pkg/models/metrics_soft_info.go rename to pkg/models/metrics_agent_info.go index cd5be2c07..27ffffc5e 100644 --- a/pkg/models/metrics_soft_info.go +++ b/pkg/models/metrics_agent_info.go @@ -12,12 +12,18 @@ import ( "github.com/go-openapi/swag" ) -// MetricsSoftInfo MetricsSoftInfo +// MetricsAgentInfo MetricsAgentInfo // // Software version info (so we can warn users about out-of-date software). The software name and the version are "guessed" from the user-agent // -// swagger:model MetricsSoftInfo -type MetricsSoftInfo struct { +// swagger:model MetricsAgentInfo +type MetricsAgentInfo struct { + + // last agent push date + LastPush string `json:"last_push,omitempty"` + + // last agent update date + LastUpdate string `json:"last_update,omitempty"` // name of the component Name string `json:"name,omitempty"` @@ -26,18 +32,18 @@ type MetricsSoftInfo struct { Version string `json:"version,omitempty"` } -// Validate validates this metrics soft info -func (m *MetricsSoftInfo) Validate(formats strfmt.Registry) error { +// Validate validates this metrics agent info +func (m *MetricsAgentInfo) Validate(formats strfmt.Registry) error { return nil } -// ContextValidate validates this metrics soft info based on context it is used -func (m *MetricsSoftInfo) ContextValidate(ctx context.Context, formats strfmt.Registry) error { +// ContextValidate validates this metrics agent info based on context it is used +func (m *MetricsAgentInfo) ContextValidate(ctx context.Context, formats strfmt.Registry) error { return nil } // MarshalBinary interface implementation -func (m *MetricsSoftInfo) MarshalBinary() ([]byte, error) { +func (m *MetricsAgentInfo) MarshalBinary() ([]byte, error) { if m == nil { return nil, nil } @@ -45,8 +51,8 @@ func (m *MetricsSoftInfo) MarshalBinary() ([]byte, error) { } // UnmarshalBinary interface implementation -func (m *MetricsSoftInfo) UnmarshalBinary(b []byte) error { - var res MetricsSoftInfo +func (m *MetricsAgentInfo) UnmarshalBinary(b []byte) error { + var res MetricsAgentInfo if err := swag.ReadJSON(b, &res); err != nil { return err } diff --git a/pkg/models/metrics_bouncer_info.go b/pkg/models/metrics_bouncer_info.go new file mode 100644 index 000000000..f38960e2e --- /dev/null +++ b/pkg/models/metrics_bouncer_info.go @@ -0,0 +1,61 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package models + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" +) + +// MetricsBouncerInfo MetricsBouncerInfo +// +// Software version info (so we can warn users about out-of-date software). The software name and the version are "guessed" from the user-agent +// +// swagger:model MetricsBouncerInfo +type MetricsBouncerInfo struct { + + // name of the component + CustomName string `json:"custom_name,omitempty"` + + // last bouncer pull date + LastPull string `json:"last_pull,omitempty"` + + // bouncer type (firewall, php ...) + Name string `json:"name,omitempty"` + + // software version + Version string `json:"version,omitempty"` +} + +// Validate validates this metrics bouncer info +func (m *MetricsBouncerInfo) Validate(formats strfmt.Registry) error { + return nil +} + +// ContextValidate validates this metrics bouncer info based on context it is used +func (m *MetricsBouncerInfo) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + return nil +} + +// MarshalBinary interface implementation +func (m *MetricsBouncerInfo) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *MetricsBouncerInfo) UnmarshalBinary(b []byte) error { + var res MetricsBouncerInfo + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} diff --git a/pkg/types/utils.go b/pkg/types/utils.go index d6dd45a33..13782c807 100644 --- a/pkg/types/utils.go +++ b/pkg/types/utils.go @@ -216,3 +216,12 @@ func Int32Ptr(i int32) *int32 { func BoolPtr(b bool) *bool { return &b } + +func InSlice(str string, slice []string) bool { + for _, item := range slice { + if str == item { + return true + } + } + return false +} diff --git a/rpm/SPECS/crowdsec.spec b/rpm/SPECS/crowdsec.spec index 11d3ff0e2..5a963e7ba 100644 --- a/rpm/SPECS/crowdsec.spec +++ b/rpm/SPECS/crowdsec.spec @@ -60,6 +60,7 @@ install -m 644 -D config/patterns/* -t %{buildroot}%{_sysconfdir}/crowdsec/patte install -m 644 -D config/config.yaml %{buildroot}%{_sysconfdir}/crowdsec install -m 644 -D config/simulation.yaml %{buildroot}%{_sysconfdir}/crowdsec install -m 644 -D config/profiles.yaml %{buildroot}%{_sysconfdir}/crowdsec +install -m 644 -D config/console_config.yaml %{buildroot}%{_sysconfdir}/crowdsec install -m 644 -D %{SOURCE1} %{buildroot}%{_presetdir} install -m 551 plugins/notifications/slack/notification-slack %{buildroot}%{_libdir}/%{name}/plugins/ diff --git a/wizard.sh b/wizard.sh index 23d586eae..21791e348 100755 --- a/wizard.sh +++ b/wizard.sh @@ -31,6 +31,8 @@ CSCLI_BIN="./cmd/crowdsec-cli/cscli" CLIENT_SECRETS="local_api_credentials.yaml" LAPI_SECRETS="online_api_credentials.yaml" +CONSOLE_FILE="console_config.yaml" + BIN_INSTALL_PATH="/usr/local/bin" CROWDSEC_BIN_INSTALLED="${BIN_INSTALL_PATH}/crowdsec" @@ -406,6 +408,7 @@ install_crowdsec() { install -v -m 644 -D ./config/acquis.yaml "${CROWDSEC_CONFIG_PATH}" 1> /dev/null || exit install -v -m 644 -D ./config/profiles.yaml "${CROWDSEC_CONFIG_PATH}" 1> /dev/null || exit install -v -m 644 -D ./config/simulation.yaml "${CROWDSEC_CONFIG_PATH}" 1> /dev/null || exit + install -v -m 644 -D ./config/"${CONSOLE_FILE}" "${CROWDSEC_CONFIG_PATH}" 1> /dev/null || exit mkdir -p ${PID_DIR} || exit PID=${PID_DIR} DATA=${CROWDSEC_DATA_DIR} CFG=${CROWDSEC_CONFIG_PATH} envsubst '$CFG $PID $DATA' < ./config/user.yaml > ${CROWDSEC_CONFIG_PATH}"/user.yaml" || log_fatal "unable to generate user configuration file"