improve support for parsing time durations with 'day' units (#3599)

* custom duration type for "cscli decisions list", "cscli alerts list"
* custom duration type for "cscli allowlist add"
* custom duration type for "cscli machines prune"
* custom duration type for "cscli bouncers prune"
* replace old function ParseDuration
* use custom duration type in expr helpers
* update dependency
* lint
* test fix
* support days in 'metrics_max_age'
* DurationWithDays for 'max_age'
This commit is contained in:
mmetc 2025-05-05 15:12:29 +02:00 committed by GitHub
parent d10067e772
commit f8f0b2a211
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 153 additions and 202 deletions

View file

@ -12,6 +12,7 @@ import (
"strconv"
"strings"
"text/template"
"time"
"github.com/fatih/color"
"github.com/go-openapi/strfmt"
@ -19,6 +20,7 @@ import (
"github.com/spf13/cobra"
"gopkg.in/yaml.v3"
"github.com/crowdsecurity/go-cs-lib/cstime"
"github.com/crowdsecurity/go-cs-lib/maptools"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/args"
@ -247,34 +249,6 @@ func (cli *cliAlerts) list(ctx context.Context, alertListFilter apiclient.Alerts
alertListFilter.Limit = limit
}
if *alertListFilter.Until == "" {
alertListFilter.Until = nil
} else if strings.HasSuffix(*alertListFilter.Until, "d") {
/*time.ParseDuration support hours 'h' as bigger unit, let's make the user's life easier*/
realDuration := strings.TrimSuffix(*alertListFilter.Until, "d")
days, err := strconv.Atoi(realDuration)
if err != nil {
return fmt.Errorf("can't parse duration %s, valid durations format: 1d, 4h, 4h15m", *alertListFilter.Until)
}
*alertListFilter.Until = fmt.Sprintf("%d%s", days*24, "h")
}
if *alertListFilter.Since == "" {
alertListFilter.Since = nil
} else if strings.HasSuffix(*alertListFilter.Since, "d") {
// time.ParseDuration support hours 'h' as bigger unit, let's make the user's life easier
realDuration := strings.TrimSuffix(*alertListFilter.Since, "d")
days, err := strconv.Atoi(realDuration)
if err != nil {
return fmt.Errorf("can't parse duration %s, valid durations format: 1d, 4h, 4h15m", *alertListFilter.Since)
}
*alertListFilter.Since = fmt.Sprintf("%d%s", days*24, "h")
}
if *alertListFilter.IncludeCAPI {
*alertListFilter.Limit = 0
}
@ -330,8 +304,8 @@ func (cli *cliAlerts) newListCmd() *cobra.Command {
ScenarioEquals: new(string),
IPEquals: new(string),
RangeEquals: new(string),
Since: new(string),
Until: new(string),
Since: cstime.DurationWithDays(0),
Until: cstime.DurationWithDays(0),
TypeEquals: new(string),
IncludeCAPI: new(bool),
OriginEquals: new(string),
@ -362,8 +336,8 @@ cscli alerts list --type ban`,
flags := cmd.Flags()
flags.SortFlags = false
flags.BoolVarP(alertListFilter.IncludeCAPI, "all", "a", false, "Include decisions from Central API")
flags.StringVar(alertListFilter.Until, "until", "", "restrict to alerts older than until (ie. 4h, 30d)")
flags.StringVar(alertListFilter.Since, "since", "", "restrict to alerts newer than since (ie. 4h, 30d)")
flags.Var(&alertListFilter.Until, "until", "restrict to alerts older than until (ie. 4h, 30d)")
flags.Var(&alertListFilter.Since, "since", "restrict to alerts newer than since (ie. 4h, 30d)")
flags.StringVarP(alertListFilter.IPEquals, "ip", "i", "", "restrict to alerts from this source ip (shorthand for --scope ip --value <IP>)")
flags.StringVarP(alertListFilter.ScenarioEquals, "scenario", "s", "", "the scenario (ie. crowdsecurity/ssh-bf)")
flags.StringVarP(alertListFilter.RangeEquals, "range", "r", "", "restrict to alerts from this range (shorthand for --scope range --value <RANGE/X>)")
@ -560,10 +534,9 @@ func (cli *cliAlerts) newInspectCmd() *cobra.Command {
}
func (cli *cliAlerts) newFlushCmd() *cobra.Command {
var (
maxItems int
maxAge string
)
var maxItems int
maxAge := cstime.DurationWithDays(7*24*time.Hour)
cmd := &cobra.Command{
Use: `flush`,
@ -584,7 +557,7 @@ func (cli *cliAlerts) newFlushCmd() *cobra.Command {
return err
}
log.Info("Flushing alerts. !! This may take a long time !!")
err = db.FlushAlerts(ctx, maxAge, maxItems)
err = db.FlushAlerts(ctx, time.Duration(maxAge), maxItems)
if err != nil {
return fmt.Errorf("unable to flush alerts: %w", err)
}
@ -596,7 +569,7 @@ func (cli *cliAlerts) newFlushCmd() *cobra.Command {
cmd.Flags().SortFlags = false
cmd.Flags().IntVar(&maxItems, "max-items", 5000, "Maximum number of alert items to keep in the database")
cmd.Flags().StringVar(&maxAge, "max-age", "7d", "Maximum age of alert items to keep in the database")
cmd.Flags().Var(&maxAge, "max-age", "Maximum age of alert items to keep in the database")
return cmd
}

View file

@ -399,8 +399,8 @@ func (cli *cliAllowLists) delete(ctx context.Context, db *database.Client, name
func (cli *cliAllowLists) newAddCmd() *cobra.Command {
var (
expirationStr string
comment string
expiration cstime.DurationWithDays
comment string
)
cmd := &cobra.Command{
@ -424,25 +424,16 @@ func (cli *cliAllowLists) newAddCmd() *cobra.Command {
return err
}
var expiration time.Duration
if expirationStr != "" {
expiration, err = cstime.ParseDuration(expirationStr)
if err != nil {
return err
}
}
name := args[0]
values := args[1:]
return cli.add(ctx, db, name, values, expiration, comment)
return cli.add(ctx, db, name, values, time.Duration(expiration), comment)
},
}
flags := cmd.Flags()
flags.StringVarP(&expirationStr, "expiration", "e", "", "expiration duration")
flags.VarP(&expiration, "expiration", "e", "expiration duration")
flags.StringVarP(&comment, "comment", "d", "", "comment for the value")
return cmd

View file

@ -9,10 +9,14 @@ import (
"github.com/fatih/color"
"github.com/spf13/cobra"
"github.com/crowdsecurity/go-cs-lib/cstime"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/args"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/ask"
)
const defaultPruneDuration = 60 * time.Minute
func (cli *cliBouncers) prune(ctx context.Context, duration time.Duration, force bool) error {
if duration < 2*time.Minute {
if yes, err := ask.YesNo(
@ -59,12 +63,9 @@ func (cli *cliBouncers) prune(ctx context.Context, duration time.Duration, force
}
func (cli *cliBouncers) newPruneCmd() *cobra.Command {
var (
duration time.Duration
force bool
)
var force bool
const defaultDuration = 60 * time.Minute
duration := cstime.DurationWithDays(defaultPruneDuration)
cmd := &cobra.Command{
Use: "prune",
@ -74,12 +75,12 @@ func (cli *cliBouncers) newPruneCmd() *cobra.Command {
Example: `cscli bouncers prune -d 45m
cscli bouncers prune -d 45m --force`,
RunE: func(cmd *cobra.Command, _ []string) error {
return cli.prune(cmd.Context(), duration, force)
return cli.prune(cmd.Context(), time.Duration(duration), force)
},
}
flags := cmd.Flags()
flags.DurationVarP(&duration, "duration", "d", defaultDuration, "duration of time since last pull")
flags.VarP(&duration, "duration", "d", "duration of time since last pull")
flags.BoolVar(&force, "force", false, "force prune without asking for confirmation")
return cmd

View file

@ -23,6 +23,8 @@ import (
"github.com/crowdsecurity/crowdsec/pkg/csconfig"
"github.com/crowdsecurity/crowdsec/pkg/models"
"github.com/crowdsecurity/crowdsec/pkg/types"
"github.com/crowdsecurity/go-cs-lib/cstime"
)
type configGetter func() *csconfig.Config
@ -184,34 +186,8 @@ func (cli *cliDecisions) list(ctx context.Context, filter apiclient.AlertsListOp
if noSimu != nil && *noSimu {
filter.IncludeSimulated = new(bool)
}
/* nullify the empty entries to avoid bad filter */
if *filter.Until == "" {
filter.Until = nil
} else if strings.HasSuffix(*filter.Until, "d") {
/*time.ParseDuration support hours 'h' as bigger unit, let's make the user's life easier*/
realDuration := strings.TrimSuffix(*filter.Until, "d")
days, err := strconv.Atoi(realDuration)
if err != nil {
return fmt.Errorf("can't parse duration %s, valid durations format: 1d, 4h, 4h15m", *filter.Until)
}
*filter.Until = fmt.Sprintf("%d%s", days*24, "h")
}
if *filter.Since == "" {
filter.Since = nil
} else if strings.HasSuffix(*filter.Since, "d") {
/*time.ParseDuration support hours 'h' as bigger unit, let's make the user's life easier*/
realDuration := strings.TrimSuffix(*filter.Since, "d")
days, err := strconv.Atoi(realDuration)
if err != nil {
return fmt.Errorf("can't parse duration %s, valid durations format: 1d, 4h, 4h15m", *filter.Since)
}
*filter.Since = fmt.Sprintf("%d%s", days*24, "h")
}
if *filter.IncludeCAPI {
*filter.Limit = 0
@ -270,8 +246,8 @@ func (cli *cliDecisions) newListCmd() *cobra.Command {
OriginEquals: new(string),
IPEquals: new(string),
RangeEquals: new(string),
Since: new(string),
Until: new(string),
Since: cstime.DurationWithDays(0),
Until: cstime.DurationWithDays(0),
TypeEquals: new(string),
IncludeCAPI: new(bool),
Limit: new(int),
@ -300,8 +276,8 @@ cscli decisions list --origin lists --scenario list_name
flags := cmd.Flags()
flags.SortFlags = false
flags.BoolVarP(filter.IncludeCAPI, "all", "a", false, "Include decisions from Central API")
flags.StringVar(filter.Since, "since", "", "restrict to alerts newer than since (ie. 4h, 30d)")
flags.StringVar(filter.Until, "until", "", "restrict to alerts older than until (ie. 4h, 30d)")
flags.Var(&filter.Since, "since", "restrict to alerts newer than since (ie. 4h, 30d)")
flags.Var(&filter.Until, "until", "restrict to alerts older than until (ie. 4h, 30d)")
flags.StringVarP(filter.TypeEquals, "type", "t", "", "restrict to this decision type (ie. ban,captcha)")
flags.StringVar(filter.ScopeEquals, "scope", "", "restrict to this scope (ie. ip,range,session)")
flags.StringVar(filter.OriginEquals, "origin", "", fmt.Sprintf("the value to match for the specified origin (%s ...)", strings.Join(types.GetOrigins(), ",")))

View file

@ -9,11 +9,15 @@ import (
"github.com/fatih/color"
"github.com/spf13/cobra"
"github.com/crowdsecurity/go-cs-lib/cstime"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/args"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/ask"
"github.com/crowdsecurity/crowdsec/pkg/database/ent"
)
const defaultPruneDuration = 10 * time.Minute
func (cli *cliMachines) prune(ctx context.Context, duration time.Duration, notValidOnly bool, force bool) error {
if duration < 2*time.Minute && !notValidOnly {
if yes, err := ask.YesNo(
@ -67,12 +71,11 @@ func (cli *cliMachines) prune(ctx context.Context, duration time.Duration, notVa
func (cli *cliMachines) newPruneCmd() *cobra.Command {
var (
duration time.Duration
notValidOnly bool
force bool
)
const defaultDuration = 10 * time.Minute
duration := cstime.DurationWithDays(defaultPruneDuration)
cmd := &cobra.Command{
Use: "prune",
@ -84,12 +87,12 @@ cscli machines prune --not-validated-only --force`,
Args: args.NoArgs,
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, _ []string) error {
return cli.prune(cmd.Context(), duration, notValidOnly, force)
return cli.prune(cmd.Context(), time.Duration(duration), notValidOnly, force)
},
}
flags := cmd.Flags()
flags.DurationVarP(&duration, "duration", "d", defaultDuration, "duration of time since validated machine last heartbeat")
flags.VarP(&duration, "duration", "d", "duration of time since validated machine last heartbeat")
flags.BoolVar(&notValidOnly, "not-validated-only", false, "only prune machines that are not validated")
flags.BoolVar(&force, "force", false, "force prune without asking for confirmation")

10
go.mod
View file

@ -24,7 +24,7 @@ require (
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
github.com/creack/pty v1.1.21 // indirect
github.com/crowdsecurity/dlog v0.0.0-20170105205344-4fb5f8204f26
github.com/crowdsecurity/go-cs-lib v0.0.18
github.com/crowdsecurity/go-cs-lib v0.0.19
github.com/crowdsecurity/grokky v0.2.2
github.com/crowdsecurity/machineid v1.0.2
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc
@ -87,8 +87,8 @@ require (
github.com/shirou/gopsutil/v3 v3.23.5
github.com/sirupsen/logrus v1.9.3
github.com/slack-go/slack v0.16.0
github.com/spf13/cobra v1.8.1
github.com/spf13/pflag v1.0.5 // indirect
github.com/spf13/cobra v1.9.1
github.com/spf13/pflag v1.0.6 // indirect
github.com/stretchr/testify v1.10.0
github.com/umahmood/haversine v0.0.0-20151105152445-808ab04add26
github.com/wasilibs/go-re2 v1.7.0
@ -100,7 +100,7 @@ require (
go.opentelemetry.io/otel/trace v1.28.0 // indirect
golang.org/x/crypto v0.36.0
golang.org/x/mod v0.23.0
golang.org/x/net v0.38.0 // indirect
golang.org/x/net v0.38.0
golang.org/x/sync v0.12.0
golang.org/x/sys v0.31.0
golang.org/x/text v0.23.0
@ -131,7 +131,7 @@ require (
github.com/bytedance/sonic/loader v0.2.1 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/gabriel-vasile/mimetype v1.4.7 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect

15
go.sum
View file

@ -100,8 +100,8 @@ github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
@ -111,8 +111,8 @@ github.com/crowdsecurity/coraza/v3 v3.0.0-20250320231801-749b8bded21a h1:2Nyr+47
github.com/crowdsecurity/coraza/v3 v3.0.0-20250320231801-749b8bded21a/go.mod h1:xSaXWOhFMSbrV8qOOfBKAyw3aOqfwaSaOy5BgSF8XlA=
github.com/crowdsecurity/dlog v0.0.0-20170105205344-4fb5f8204f26 h1:r97WNVC30Uen+7WnLs4xDScS/Ex988+id2k6mDf8psU=
github.com/crowdsecurity/dlog v0.0.0-20170105205344-4fb5f8204f26/go.mod h1:zpv7r+7KXwgVUZnUNjyP22zc/D7LKjyoY02weH2RBbk=
github.com/crowdsecurity/go-cs-lib v0.0.18 h1:GNyvaag5MXfuapIy4E30pIOvIE5AyHoanJBNSMA1cmE=
github.com/crowdsecurity/go-cs-lib v0.0.18/go.mod h1:XwGcvTt4lMq4Tm1IRMSKMDf0CVrnytTU8Uoofa7AR+g=
github.com/crowdsecurity/go-cs-lib v0.0.19 h1:wA4O8hGrEntTGn7eZTJqnQ3mrAje5JvQAj8DNbe5IZg=
github.com/crowdsecurity/go-cs-lib v0.0.19/go.mod h1:hz2FOHFXc0vWzH78uxo2VebtPQ9Snkbdzy3TMA20tVQ=
github.com/crowdsecurity/grokky v0.2.2 h1:yALsI9zqpDArYzmSSxfBq2dhYuGUTKMJq8KOEIAsuo4=
github.com/crowdsecurity/grokky v0.2.2/go.mod h1:33usDIYzGDsgX1kHAThCbseso6JuWNJXOzRQDGXHtWM=
github.com/crowdsecurity/machineid v1.0.2 h1:wpkpsUghJF8Khtmn/tg6GxgdhLA1Xflerh5lirI+bdc=
@ -660,11 +660,12 @@ github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w=
github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g=
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=

View file

@ -7,42 +7,44 @@ import (
qs "github.com/google/go-querystring/query"
"github.com/crowdsecurity/go-cs-lib/cstime"
"github.com/crowdsecurity/crowdsec/pkg/models"
)
type AlertsService service
type AlertsListOpts struct {
ScopeEquals *string `url:"scope,omitempty"`
ValueEquals *string `url:"value,omitempty"`
ScenarioEquals *string `url:"scenario,omitempty"`
IPEquals *string `url:"ip,omitempty"`
RangeEquals *string `url:"range,omitempty"`
OriginEquals *string `url:"origin,omitempty"`
Since *string `url:"since,omitempty"`
TypeEquals *string `url:"decision_type,omitempty"`
Until *string `url:"until,omitempty"`
IncludeSimulated *bool `url:"simulated,omitempty"`
ActiveDecisionEquals *bool `url:"has_active_decision,omitempty"`
IncludeCAPI *bool `url:"include_capi,omitempty"`
Limit *int `url:"limit,omitempty"`
Contains *bool `url:"contains,omitempty"`
ScopeEquals *string `url:"scope,omitempty"`
ValueEquals *string `url:"value,omitempty"`
ScenarioEquals *string `url:"scenario,omitempty"`
IPEquals *string `url:"ip,omitempty"`
RangeEquals *string `url:"range,omitempty"`
OriginEquals *string `url:"origin,omitempty"`
Since cstime.DurationWithDays `url:"since,omitempty"`
TypeEquals *string `url:"decision_type,omitempty"`
Until cstime.DurationWithDays `url:"until,omitempty"`
IncludeSimulated *bool `url:"simulated,omitempty"`
ActiveDecisionEquals *bool `url:"has_active_decision,omitempty"`
IncludeCAPI *bool `url:"include_capi,omitempty"`
Limit *int `url:"limit,omitempty"`
Contains *bool `url:"contains,omitempty"`
ListOpts
}
type AlertsDeleteOpts struct {
ScopeEquals *string `url:"scope,omitempty"`
ValueEquals *string `url:"value,omitempty"`
ScenarioEquals *string `url:"scenario,omitempty"`
IPEquals *string `url:"ip,omitempty"`
RangeEquals *string `url:"range,omitempty"`
Since *string `url:"since,omitempty"`
Until *string `url:"until,omitempty"`
OriginEquals *string `url:"origin,omitempty"`
ActiveDecisionEquals *bool `url:"has_active_decision,omitempty"`
SourceEquals *string `url:"alert_source,omitempty"`
Contains *bool `url:"contains,omitempty"`
Limit *int `url:"limit,omitempty"`
ScopeEquals *string `url:"scope,omitempty"`
ValueEquals *string `url:"value,omitempty"`
ScenarioEquals *string `url:"scenario,omitempty"`
IPEquals *string `url:"ip,omitempty"`
RangeEquals *string `url:"range,omitempty"`
Since cstime.DurationWithDays `url:"since,omitempty"`
Until cstime.DurationWithDays `url:"until,omitempty"`
OriginEquals *string `url:"origin,omitempty"`
ActiveDecisionEquals *bool `url:"has_active_decision,omitempty"`
SourceEquals *string `url:"alert_source,omitempty"`
Contains *bool `url:"contains,omitempty"`
Limit *int `url:"limit,omitempty"`
ListOpts
}

View file

@ -18,6 +18,7 @@ import (
"github.com/stretchr/testify/require"
"github.com/crowdsecurity/go-cs-lib/cstest"
"github.com/crowdsecurity/go-cs-lib/cstime"
"github.com/crowdsecurity/go-cs-lib/ptr"
"github.com/crowdsecurity/go-cs-lib/version"
@ -47,9 +48,9 @@ var (
func LoadTestConfig(t *testing.T) csconfig.Config {
config := csconfig.Config{}
maxAge := "1h"
maxAge := cstime.DurationWithDays(1*time.Hour)
flushConfig := csconfig.FlushDBCfg{
MaxAge: &maxAge,
MaxAge: maxAge,
}
tempDir, _ := os.MkdirTemp("", "crowdsec_tests")
@ -97,9 +98,9 @@ func LoadTestConfig(t *testing.T) csconfig.Config {
func LoadTestConfigForwardedFor(t *testing.T) csconfig.Config {
config := csconfig.Config{}
maxAge := "1h"
maxAge := cstime.DurationWithDays(1*time.Hour)
flushConfig := csconfig.FlushDBCfg{
MaxAge: &maxAge,
MaxAge: maxAge,
}
tempDir, _ := os.MkdirTemp("", "crowdsec_tests")
@ -363,9 +364,9 @@ func TestLoggingDebugToFileConfig(t *testing.T) {
ctx := t.Context()
/*declare settings*/
maxAge := "1h"
maxAge := cstime.DurationWithDays(1*time.Hour)
flushConfig := csconfig.FlushDBCfg{
MaxAge: &maxAge,
MaxAge: maxAge,
}
tempDir, _ := os.MkdirTemp("", "crowdsec_tests")
@ -416,9 +417,9 @@ func TestLoggingErrorToFileConfig(t *testing.T) {
ctx := t.Context()
/*declare settings*/
maxAge := "1h"
maxAge := cstime.DurationWithDays(1*time.Hour)
flushConfig := csconfig.FlushDBCfg{
MaxAge: &maxAge,
MaxAge: maxAge,
}
tempDir, _ := os.MkdirTemp("", "crowdsec_tests")

View file

@ -14,6 +14,7 @@ import (
"github.com/go-sql-driver/mysql"
log "github.com/sirupsen/logrus"
"github.com/crowdsecurity/go-cs-lib/cstime"
"github.com/crowdsecurity/go-cs-lib/ptr"
"github.com/crowdsecurity/crowdsec/pkg/types"
@ -58,10 +59,10 @@ type AuthGCCfg struct {
type FlushDBCfg struct {
MaxItems *int `yaml:"max_items,omitempty"`
// We could unmarshal as time.Duration, but alert filters right now are a map of strings
MaxAge *string `yaml:"max_age,omitempty"`
BouncersGC *AuthGCCfg `yaml:"bouncers_autodelete,omitempty"`
AgentsGC *AuthGCCfg `yaml:"agents_autodelete,omitempty"`
MetricsMaxAge *time.Duration `yaml:"metrics_max_age,omitempty"`
MaxAge cstime.DurationWithDays `yaml:"max_age,omitempty"`
BouncersGC *AuthGCCfg `yaml:"bouncers_autodelete,omitempty"`
AgentsGC *AuthGCCfg `yaml:"agents_autodelete,omitempty"`
MetricsMaxAge cstime.DurationWithDays `yaml:"metrics_max_age,omitempty"`
}
func (c *Config) LoadDBConfig(inCli bool) error {

View file

@ -7,12 +7,13 @@ import (
"github.com/expr-lang/expr/vm"
log "github.com/sirupsen/logrus"
"github.com/crowdsecurity/go-cs-lib/cstime"
"github.com/crowdsecurity/go-cs-lib/ptr"
"github.com/crowdsecurity/crowdsec/pkg/csconfig"
utils "github.com/crowdsecurity/crowdsec/pkg/database"
"github.com/crowdsecurity/crowdsec/pkg/exprhelpers"
"github.com/crowdsecurity/crowdsec/pkg/models"
"github.com/crowdsecurity/crowdsec/pkg/types"
"github.com/crowdsecurity/go-cs-lib/ptr"
)
type Runtime struct {
@ -84,7 +85,7 @@ func NewProfile(profilesCfg []*csconfig.ProfileCfg) ([]*Runtime, error) {
duration = defaultDuration
}
if _, err := utils.ParseDuration(duration); err != nil {
if _, err := cstime.ParseDurationWithDays(duration); err != nil {
return nil, fmt.Errorf("error parsing duration '%s' of %s: %w", duration, profile.Name, err)
}
}
@ -136,7 +137,7 @@ func (profile *Runtime) GenerateDecisionFromProfile(alert *models.Alert) ([]*mod
profile.Logger.Warningf("Failed to run duration_expr : %v", err)
} else {
durationStr := fmt.Sprint(duration)
if _, err := utils.ParseDuration(durationStr); err != nil {
if _, err := cstime.ParseDurationWithDays(durationStr); err != nil {
profile.Logger.Warningf("Failed to parse expr duration result '%s'", duration)
} else {
*decision.Duration = durationStr

View file

@ -9,6 +9,8 @@ import (
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
"github.com/crowdsecurity/go-cs-lib/cstime"
"github.com/crowdsecurity/crowdsec/pkg/database/ent"
"github.com/crowdsecurity/crowdsec/pkg/database/ent/alert"
"github.com/crowdsecurity/crowdsec/pkg/database/ent/decision"
@ -40,7 +42,9 @@ func handleScopeFilter(scope string, predicates *[]predicate.Alert) {
}
func handleTimeFilters(param, value string, predicates *[]predicate.Alert) error {
duration, err := ParseDuration(value)
// crowsdec now always sends duration without days, but we allow them for
// compatibility with other tools
duration, err := cstime.ParseDurationWithDays(value)
if err != nil {
return fmt.Errorf("while parsing duration: %w", err)
}

View file

@ -13,6 +13,7 @@ import (
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
"github.com/crowdsecurity/go-cs-lib/cstime"
"github.com/crowdsecurity/go-cs-lib/slicetools"
"github.com/crowdsecurity/crowdsec/pkg/database/ent"
@ -382,7 +383,7 @@ func (c *Client) createDecisionChunk(ctx context.Context, simulated bool, stopAt
sz int
)
duration, err := ParseDuration(*decisionItem.Duration)
duration, err := cstime.ParseDurationWithDays(*decisionItem.Duration)
if err != nil {
return nil, errors.Wrapf(ParseDurationFail, "decision duration '%+v' : %s", *decisionItem.Duration, err)
}

View file

@ -9,7 +9,7 @@ import (
"github.com/go-co-op/gocron"
log "github.com/sirupsen/logrus"
"github.com/crowdsecurity/go-cs-lib/ptr"
"github.com/crowdsecurity/go-cs-lib/cstime"
"github.com/crowdsecurity/crowdsec/pkg/csconfig"
"github.com/crowdsecurity/crowdsec/pkg/database/ent/alert"
@ -30,7 +30,6 @@ const (
func (c *Client) StartFlushScheduler(ctx context.Context, config *csconfig.FlushDBCfg) (*gocron.Scheduler, error) {
maxItems := 0
maxAge := ""
if config.MaxItems != nil && *config.MaxItems <= 0 {
return nil, errors.New("max_items can't be zero or negative")
@ -40,14 +39,10 @@ func (c *Client) StartFlushScheduler(ctx context.Context, config *csconfig.Flush
maxItems = *config.MaxItems
}
if config.MaxAge != nil && *config.MaxAge != "" {
maxAge = *config.MaxAge
}
// Init & Start cronjob every minute for alerts
scheduler := gocron.NewScheduler(time.UTC)
job, err := scheduler.Every(1).Minute().Do(c.FlushAlerts, ctx, maxAge, maxItems)
job, err := scheduler.Every(1).Minute().Do(c.FlushAlerts, ctx, time.Duration(config.MaxAge), maxItems)
if err != nil {
return nil, fmt.Errorf("while starting FlushAlerts scheduler: %w", err)
}
@ -56,7 +51,7 @@ func (c *Client) StartFlushScheduler(ctx context.Context, config *csconfig.Flush
// Init & Start cronjob every hour for bouncers/agents
if config.AgentsGC != nil {
if config.AgentsGC.Cert != nil {
duration, err := ParseDuration(*config.AgentsGC.Cert)
duration, err := cstime.ParseDurationWithDays(*config.AgentsGC.Cert)
if err != nil {
return nil, fmt.Errorf("while parsing agents cert auto-delete duration: %w", err)
}
@ -65,7 +60,7 @@ func (c *Client) StartFlushScheduler(ctx context.Context, config *csconfig.Flush
}
if config.AgentsGC.LoginPassword != nil {
duration, err := ParseDuration(*config.AgentsGC.LoginPassword)
duration, err := cstime.ParseDurationWithDays(*config.AgentsGC.LoginPassword)
if err != nil {
return nil, fmt.Errorf("while parsing agents login/password auto-delete duration: %w", err)
}
@ -80,7 +75,7 @@ func (c *Client) StartFlushScheduler(ctx context.Context, config *csconfig.Flush
if config.BouncersGC != nil {
if config.BouncersGC.Cert != nil {
duration, err := ParseDuration(*config.BouncersGC.Cert)
duration, err := cstime.ParseDurationWithDays(*config.BouncersGC.Cert)
if err != nil {
return nil, fmt.Errorf("while parsing bouncers cert auto-delete duration: %w", err)
}
@ -89,7 +84,7 @@ func (c *Client) StartFlushScheduler(ctx context.Context, config *csconfig.Flush
}
if config.BouncersGC.Api != nil {
duration, err := ParseDuration(*config.BouncersGC.Api)
duration, err := cstime.ParseDurationWithDays(*config.BouncersGC.Api)
if err != nil {
return nil, fmt.Errorf("while parsing bouncers api auto-delete duration: %w", err)
}
@ -109,7 +104,7 @@ func (c *Client) StartFlushScheduler(ctx context.Context, config *csconfig.Flush
baJob.SingletonMode()
metricsJob, err := scheduler.Every(flushInterval).Do(c.flushMetrics, ctx, config.MetricsMaxAge)
metricsJob, err := scheduler.Every(flushInterval).Do(c.flushMetrics, ctx, time.Duration(config.MetricsMaxAge))
if err != nil {
return nil, fmt.Errorf("while starting flushMetrics scheduler: %w", err)
}
@ -129,15 +124,15 @@ func (c *Client) StartFlushScheduler(ctx context.Context, config *csconfig.Flush
}
// flushMetrics deletes metrics older than maxAge, regardless if they have been pushed to CAPI or not
func (c *Client) flushMetrics(ctx context.Context, maxAge *time.Duration) {
if maxAge == nil {
maxAge = ptr.Of(defaultMetricsMaxAge)
func (c *Client) flushMetrics(ctx context.Context, maxAge time.Duration) {
if maxAge == 0 {
maxAge = defaultMetricsMaxAge
}
c.Log.Debugf("flushing metrics older than %s", maxAge)
deleted, err := c.Ent.Metric.Delete().Where(
metric.ReceivedAtLTE(time.Now().UTC().Add(-*maxAge)),
metric.ReceivedAtLTE(time.Now().UTC().Add(-maxAge)),
).Exec(ctx)
if err != nil {
c.Log.Errorf("while flushing metrics: %s", err)
@ -230,7 +225,7 @@ func (c *Client) FlushAgentsAndBouncers(ctx context.Context, agentsCfg *csconfig
return nil
}
func (c *Client) FlushAlerts(ctx context.Context, maxAge string, maxItems int) error {
func (c *Client) FlushAlerts(ctx context.Context, maxAge time.Duration, maxItems int) error {
var (
deletedByAge int
deletedByNbItem int
@ -255,9 +250,9 @@ func (c *Client) FlushAlerts(ctx context.Context, maxAge string, maxItems int) e
c.Log.Debugf("FlushAlerts (Total alerts): %d", totalAlerts)
if maxAge != "" {
if maxAge != 0 {
filter := map[string][]string{
"created_before": {maxAge},
"created_before": {maxAge.String()},
}
nbDeleted, err := c.DeleteAlertWithFilter(ctx, filter)

View file

@ -4,9 +4,6 @@ import (
"encoding/binary"
"fmt"
"net"
"strconv"
"strings"
"time"
)
func IP2Int(ip net.IP) uint32 {
@ -69,28 +66,3 @@ func GetIpsFromIpRange(host string) (int64, int64, error) {
return ipStart, ipEnd, nil
}
func ParseDuration(d string) (time.Duration, error) {
durationStr := d
if strings.HasSuffix(d, "d") {
days := strings.Split(d, "d")[0]
if days == "" {
return 0, fmt.Errorf("'%s' can't be parsed as duration", d)
}
daysInt, err := strconv.Atoi(days)
if err != nil {
return 0, err
}
durationStr = strconv.Itoa(daysInt*24) + "h"
}
duration, err := time.ParseDuration(durationStr)
if err != nil {
return 0, err
}
return duration, nil
}

View file

@ -29,6 +29,8 @@ import (
"github.com/umahmood/haversine"
"github.com/wasilibs/go-re2"
"github.com/crowdsecurity/go-cs-lib/cstime"
"github.com/crowdsecurity/crowdsec/pkg/cache"
"github.com/crowdsecurity/crowdsec/pkg/database"
"github.com/crowdsecurity/crowdsec/pkg/fflag"
@ -661,7 +663,7 @@ func GetDecisionsSinceCount(params ...any) (any, error) {
return 0, nil
}
sinceDuration, err := time.ParseDuration(since)
sinceDuration, err := cstime.ParseDurationWithDays(since)
if err != nil {
log.Errorf("Failed to parse since parameter '%s' : %s", since, err)
return 0, nil

View file

@ -133,6 +133,13 @@ teardown() {
}
@test "cscli bouncers prune" {
rune -1 cscli bouncers prune --duration foobar
assert_stderr 'Error: invalid argument "foobar" for "-d, --duration" flag: time: invalid duration "foobar"'
# duration takes days as well
rune -0 cscli bouncers prune --duration 1d30m
assert_output 'No bouncers to prune.'
rune -0 cscli bouncers prune
assert_output 'No bouncers to prune.'
rune -0 cscli bouncers add ciTestBouncer

View file

@ -124,12 +124,19 @@ teardown() {
@test "cscli machines prune" {
rune -0 cscli metrics
rune -1 cscli machines prune --duration foobar
assert_stderr 'Error: invalid argument "foobar" for "-d, --duration" flag: time: invalid duration "foobar"'
# if the fixture has been created some time ago,
# the machines may be old enough to trigger a user prompt.
# make sure the prune duration is high enough.
rune -0 cscli machines prune --duration 1000000h
assert_output 'No machines to prune.'
# duration takes days as well
rune -0 cscli machines prune --duration 1000d30m
assert_output 'No machines to prune.'
rune -0 cscli machines list -o json
rune -0 jq -r '.[-1].machineId' <(output)
rune -0 cscli machines delete "$output"

View file

@ -43,6 +43,15 @@ teardown() {
assert_output --regexp " githubciXXXXXXXXXXXXXXXXXXXXXXXX([a-zA-Z0-9]{16})? "
}
@test "cscli alerts list, accept duration parameters with days" {
rune -1 cscli alerts list --until toto
assert_stderr 'Error: invalid argument "toto" for "--until" flag: time: invalid duration "toto"'
rune -0 cscli alerts list --until 2d12h --debug
assert_stderr --partial "until=60h0m0s"
rune -0 cscli alerts list --since 2d12h --debug
assert_stderr --partial "since=60h0m0s"
}
@test "cscli alerts list, human/json/raw" {
rune -0 cscli decisions add -i 10.20.30.40 -t ban

View file

@ -54,9 +54,13 @@ teardown() {
assert_output --regexp " githubciXXXXXXXXXXXXXXXXXXXXXXXX([a-zA-Z0-9]{16})? "
}
@test "cscli decisions list, incorrect parameters" {
@test "cscli decisions list, accept duration parameters with days" {
rune -1 cscli decisions list --until toto
assert_stderr 'Error: unable to retrieve decisions: performing request: API error: while parsing duration: time: invalid duration "toto"'
assert_stderr 'Error: invalid argument "toto" for "--until" flag: time: invalid duration "toto"'
rune -0 cscli decisions list --until 2d12h --debug
assert_stderr --partial "until=60h0m0s"
rune -0 cscli decisions list --since 2d12h --debug
assert_stderr --partial "since=60h0m0s"
}
@test "cscli decisions import" {

View file

@ -119,14 +119,14 @@ teardown() {
# comment and expiration are applied to all values
rune -1 cscli allowlist add foo 10.10.10.10 10.20.30.40 -d comment -e toto
assert_stderr 'Error: time: invalid duration "toto"'
assert_stderr 'Error: invalid argument "toto" for "-e, --expiration" flag: time: invalid duration "toto"'
refute_output
rune -1 cscli allowlist add foo 10.10.10.10 10.20.30.40 -d comment -e '1 day'
refute_output
assert_stderr 'Error: strconv.Atoi: parsing "1 ": invalid syntax'
assert_stderr 'Error: invalid argument "1 day" for "-e, --expiration" flag: invalid day value in duration "1 day"'
rune -0 cscli allowlist add foo 10.10.10.10 -d comment -e '1d'
rune -0 cscli allowlist add foo 10.10.10.10 -d comment -e '1d12h'
assert_output 'added 1 values to allowlist foo'
refute_stderr