Merge branch 'master' into releases/1.6.x

This commit is contained in:
marco 2025-03-12 10:58:43 +01:00
commit bc8a255f89
94 changed files with 1235 additions and 595 deletions

View file

@ -214,7 +214,6 @@ linters-settings:
enable-all: true
disabled-checks:
- paramTypeCombine
- httpNoBody
- ifElseChain
- importShadow
- hugeParam

View file

@ -21,7 +21,7 @@ stages:
- task: GoTool@0
displayName: "Install Go"
inputs:
version: '1.24.0'
version: '1.24.1'
- pwsh: |
choco install -y make

View file

@ -6,22 +6,49 @@ import (
"github.com/spf13/cobra"
)
// MinimumNArgs is a drop-in replacement for cobra.MinimumNArgs that prints the usage in case of wrong number of arguments, but not other errors.
func MinimumNArgs(n int) cobra.PositionalArgs {
return func(cmd *cobra.Command, args []string) error {
if len(args) < n {
cmd.Help() //nolint:errcheck
_ = cmd.Help()
fmt.Fprintln(cmd.OutOrStdout(), "")
return fmt.Errorf("requires at least %d arg(s), only received %d", n, len(args))
}
return nil
}
}
// MaximumNArgs is a drop-in replacement for cobra.MaximumNArgs that prints the usage in case of wrong number of arguments, but not other errors.
func MaximumNArgs(n int) cobra.PositionalArgs {
return func(cmd *cobra.Command, args []string) error {
if len(args) > n {
_ = cmd.Help()
fmt.Fprintln(cmd.OutOrStdout(), "")
return fmt.Errorf("accepts at most %d arg(s), received %d", n, len(args))
}
return nil
}
}
// ExactArgs is a drop-in replacement for cobra.ExactArgs that prints the usage in case of wrong number of arguments, but not other errors.
func ExactArgs(n int) cobra.PositionalArgs {
return func(cmd *cobra.Command, args []string) error {
if len(args) != n {
cmd.Help() //nolint:errcheck
_ = cmd.Help()
fmt.Fprintln(cmd.OutOrStdout(), "")
return fmt.Errorf("accepts %d arg(s), received %d", n, len(args))
}
return nil
}
}
// NoArgs is a drop-in replacement for cobra.NoArgs that prints the usage in case of wrong number of arguments, but not other errors.
func NoArgs(cmd *cobra.Command, args []string) error {
if len(args) > 0 {
_ = cmd.Help()
fmt.Fprintln(cmd.OutOrStdout(), "")
return fmt.Errorf("unknown command %q for %q", args[0], cmd.CommandPath())
}
return nil
}

View file

@ -21,6 +21,7 @@ import (
"github.com/crowdsecurity/go-cs-lib/maptools"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/args"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/cstable"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/require"
"github.com/crowdsecurity/crowdsec/pkg/apiclient"
@ -200,7 +201,6 @@ func (cli *cliAlerts) NewCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "alerts [action]",
Short: "Manage alerts",
Args: cobra.MinimumNArgs(1),
DisableAutoGenTag: true,
Aliases: []string{"alert"},
PersistentPreRunE: func(_ *cobra.Command, _ []string) error {
@ -352,6 +352,7 @@ cscli alerts list --origin lists
cscli alerts list -s crowdsecurity/ssh-bf
cscli alerts list --type ban`,
Long: `List alerts with optional filters`,
Args: args.NoArgs,
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, _ []string) error {
return cli.list(cmd.Context(), alertListFilter, limit, contained, printMachine)
@ -465,7 +466,7 @@ cscli alerts delete --range 1.2.3.0/24
cscli alerts delete -s crowdsecurity/ssh-bf"`,
DisableAutoGenTag: true,
Aliases: []string{"remove"},
Args: cobra.NoArgs,
Args: args.NoArgs,
PreRunE: func(cmd *cobra.Command, _ []string) error {
if deleteAll {
return nil
@ -545,12 +546,9 @@ func (cli *cliAlerts) newInspectCmd() *cobra.Command {
Use: `inspect "alert_id"`,
Short: `Show info about an alert`,
Example: `cscli alerts inspect 123`,
Args: args.MinimumNArgs(1),
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
_ = cmd.Help()
return errors.New("missing alert_id")
}
return cli.inspect(cmd.Context(), details, args...)
},
}
@ -572,6 +570,7 @@ func (cli *cliAlerts) newFlushCmd() *cobra.Command {
Short: `Flush alerts
/!\ This command can be used only on the same machine than the local API`,
Example: `cscli alerts flush --max-items 1000 --max-age 7d`,
Args: args.NoArgs,
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, _ []string) error {
cfg := cli.cfg()

View file

@ -228,7 +228,6 @@ func (cli *cliAllowLists) NewCommand() *cobra.Command {
Use: "allowlists [action]",
Short: "Manage centralized allowlists",
Aliases: []string{"allowlist"},
Args: cobra.MinimumNArgs(1),
DisableAutoGenTag: true,
}
@ -294,7 +293,7 @@ func (cli *cliAllowLists) newListCmd() *cobra.Command {
Use: "list",
Example: `cscli allowlists list`,
Short: "List all allowlists",
Args: cobra.NoArgs,
Args: args.NoArgs,
RunE: func(cmd *cobra.Command, _ []string) error {
cfg := cli.cfg()
if err := cfg.LoadAPIClient(); err != nil {

View file

@ -8,6 +8,7 @@ import (
"github.com/spf13/cobra"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/args"
middlewares "github.com/crowdsecurity/crowdsec/pkg/apiserver/middlewares/v1"
"github.com/crowdsecurity/crowdsec/pkg/types"
)
@ -56,7 +57,7 @@ func (cli *cliBouncers) newAddCmd() *cobra.Command {
Short: "add a single bouncer to the database",
Example: `cscli bouncers add MyBouncerName
cscli bouncers add MyBouncerName --key <random-key>`,
Args: cobra.ExactArgs(1),
Args: args.ExactArgs(1),
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error {
return cli.add(cmd.Context(), args[0], key)

View file

@ -34,7 +34,6 @@ func (cli *cliBouncers) NewCommand() *cobra.Command {
Long: `To list/add/delete/prune bouncers.
Note: This command requires database direct access, so is intended to be run on Local API/master.
`,
Args: cobra.MinimumNArgs(1),
Aliases: []string{"bouncer"},
DisableAutoGenTag: true,
PersistentPreRunE: func(cmd *cobra.Command, _ []string) error {

View file

@ -9,6 +9,7 @@ import (
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/args"
"github.com/crowdsecurity/crowdsec/pkg/database/ent"
"github.com/crowdsecurity/crowdsec/pkg/types"
)
@ -83,7 +84,7 @@ func (cli *cliBouncers) newDeleteCmd() *cobra.Command {
Use: "delete MyBouncerName",
Short: "delete bouncer(s) from the database",
Example: `cscli bouncers delete "bouncer1" "bouncer2"`,
Args: cobra.MinimumNArgs(1),
Args: args.MinimumNArgs(1),
Aliases: []string{"remove"},
DisableAutoGenTag: true,
ValidArgsFunction: cli.validBouncerID,

View file

@ -10,6 +10,7 @@ import (
"github.com/jedib0t/go-pretty/v6/table"
"github.com/spf13/cobra"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/args"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/clientinfo"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/cstable"
"github.com/crowdsecurity/crowdsec/pkg/database/ent"
@ -78,7 +79,7 @@ func (cli *cliBouncers) newInspectCmd() *cobra.Command {
Use: "inspect [bouncer_name]",
Short: "inspect a bouncer by name",
Example: `cscli bouncers inspect "bouncer1"`,
Args: cobra.ExactArgs(1),
Args: args.ExactArgs(1),
DisableAutoGenTag: true,
ValidArgsFunction: cli.validBouncerID,
RunE: func(cmd *cobra.Command, args []string) error {

View file

@ -13,6 +13,7 @@ import (
"github.com/jedib0t/go-pretty/v6/table"
"github.com/spf13/cobra"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/args"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/cstable"
"github.com/crowdsecurity/crowdsec/pkg/database"
"github.com/crowdsecurity/crowdsec/pkg/database/ent"
@ -105,7 +106,7 @@ func (cli *cliBouncers) newListCmd() *cobra.Command {
Use: "list",
Short: "list all bouncers within the database",
Example: `cscli bouncers list`,
Args: cobra.NoArgs,
Args: args.NoArgs,
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, _ []string) error {
return cli.List(cmd.Context(), color.Output, cli.db)

View file

@ -9,6 +9,7 @@ import (
"github.com/fatih/color"
"github.com/spf13/cobra"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/args"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/ask"
)
@ -68,7 +69,7 @@ func (cli *cliBouncers) newPruneCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "prune",
Short: "prune multiple bouncers from the database",
Args: cobra.NoArgs,
Args: args.NoArgs,
DisableAutoGenTag: true,
Example: `cscli bouncers prune -d 45m
cscli bouncers prune -d 45m --force`,

View file

@ -14,6 +14,7 @@ import (
"github.com/spf13/cobra"
"gopkg.in/yaml.v3"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/args"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/idgen"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/reload"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/require"
@ -40,7 +41,6 @@ func (cli *cliCapi) NewCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "capi [action]",
Short: "Manage interaction with Central API (CAPI)",
Args: cobra.MinimumNArgs(1),
DisableAutoGenTag: true,
PersistentPreRunE: func(_ *cobra.Command, _ []string) error {
cfg := cli.cfg()
@ -139,7 +139,7 @@ func (cli *cliCapi) newRegisterCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "register",
Short: "Register to Central API (CAPI)",
Args: cobra.MinimumNArgs(0),
Args: args.NoArgs,
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, _ []string) error {
return cli.register(cmd.Context(), capiUserPrefix, outputFile)
@ -260,7 +260,7 @@ func (cli *cliCapi) newStatusCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "status",
Short: "Check status with the Central API (CAPI)",
Args: cobra.MinimumNArgs(0),
Args: args.NoArgs,
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, _ []string) error {
hub, err := require.Hub(cli.cfg(), nil)

View file

@ -24,7 +24,6 @@ func (cli *cliConfig) NewCommand(mergedConfigGetter mergedConfigGetter) *cobra.C
cmd := &cobra.Command{
Use: "config [command]",
Short: "Allows to view current config",
Args: cobra.NoArgs,
DisableAutoGenTag: true,
}

View file

@ -7,6 +7,7 @@ import (
"github.com/fatih/color"
"github.com/spf13/cobra"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/args"
"github.com/crowdsecurity/crowdsec/pkg/csconfig"
"github.com/crowdsecurity/crowdsec/pkg/fflag"
)
@ -121,7 +122,7 @@ func (cli *cliConfig) newFeatureFlagsCmd() *cobra.Command {
Use: "feature-flags",
Short: "Displays feature flag status",
Long: `Displays the supported feature flags and their current status.`,
Args: cobra.NoArgs,
Args: args.NoArgs,
DisableAutoGenTag: true,
RunE: func(_ *cobra.Command, _ []string) error {
return cli.featureFlags(showRetired)

View file

@ -12,6 +12,7 @@ import (
"github.com/spf13/cobra"
"gopkg.in/yaml.v3"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/args"
"github.com/crowdsecurity/crowdsec/pkg/csconfig"
"github.com/crowdsecurity/crowdsec/pkg/exprhelpers"
)
@ -235,7 +236,7 @@ func (cli *cliConfig) newShowCmd() *cobra.Command {
Use: "show",
Short: "Displays current config",
Long: `Displays the current cli configuration.`,
Args: cobra.NoArgs,
Args: args.NoArgs,
DisableAutoGenTag: true,
RunE: func(_ *cobra.Command, _ []string) error {
if err := cli.cfg().LoadAPIClient(); err != nil {

View file

@ -4,6 +4,8 @@ import (
"fmt"
"github.com/spf13/cobra"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/args"
)
func (cli *cliConfig) showYAML(mergedConfig string) error {
@ -15,7 +17,7 @@ func (cli *cliConfig) newShowYAMLCmd(mergedConfigGetter mergedConfigGetter) *cob
cmd := &cobra.Command{
Use: "show-yaml",
Short: "Displays merged config.yaml + config.yaml.local",
Args: cobra.NoArgs,
Args: args.NoArgs,
DisableAutoGenTag: true,
RunE: func(_ *cobra.Command, _ []string) error {
return cli.showYAML(mergedConfigGetter())

View file

@ -10,6 +10,7 @@ import (
"net/url"
"os"
"strconv"
"slices"
"strings"
"github.com/fatih/color"
@ -19,7 +20,9 @@ import (
"gopkg.in/yaml.v3"
"github.com/crowdsecurity/go-cs-lib/ptr"
"github.com/crowdsecurity/go-cs-lib/slicetools"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/args"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/reload"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/require"
"github.com/crowdsecurity/crowdsec/pkg/apiclient"
@ -43,7 +46,6 @@ func (cli *cliConsole) NewCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "console [action]",
Short: "Manage interaction with Crowdsec console (https://app.crowdsec.net)",
Args: cobra.MinimumNArgs(1),
DisableAutoGenTag: true,
PersistentPreRunE: func(_ *cobra.Command, _ []string) error {
cfg := cli.cfg()
@ -75,45 +77,6 @@ func (cli *cliConsole) enroll(ctx context.Context, key string, name string, over
return fmt.Errorf("could not parse CAPI URL: %w", err)
}
enableOpts := []string{csconfig.SEND_MANUAL_SCENARIOS, csconfig.SEND_TAINTED_SCENARIOS}
if len(opts) != 0 {
for _, opt := range opts {
valid := false
if opt == "all" {
enableOpts = csconfig.CONSOLE_CONFIGS
break
}
for _, availableOpt := range csconfig.CONSOLE_CONFIGS {
if opt != availableOpt {
continue
}
valid = true
enable := true
for _, enabledOpt := range enableOpts {
if opt == enabledOpt {
enable = false
continue
}
}
if enable {
enableOpts = append(enableOpts, opt)
}
break
}
if !valid {
return fmt.Errorf("option %s doesn't exist", opt)
}
}
}
hub, err := require.Hub(cfg, nil)
if err != nil {
return err
@ -137,11 +100,11 @@ func (cli *cliConsole) enroll(ctx context.Context, key string, name string, over
return nil
}
if err := cli.setConsoleOpts(enableOpts, true); err != nil {
if err := cli.setConsoleOpts(opts, true); err != nil {
return err
}
for _, opt := range enableOpts {
for _, opt := range opts {
log.Infof("Enabled %s : %s", opt, csconfig.CONSOLE_CONFIGS_HELP[opt])
}
@ -151,11 +114,67 @@ func (cli *cliConsole) enroll(ctx context.Context, key string, name string, over
return nil
}
func optionFilterEnable(opts []string, enableOpts []string) ([]string, error) {
if len(enableOpts) == 0 {
return opts, nil
}
for _, opt := range enableOpts {
if opt == "all" {
opts = append(opts, csconfig.CONSOLE_CONFIGS...)
// keep validating the rest of the option names
continue
}
if !slices.Contains(csconfig.CONSOLE_CONFIGS, opt) {
return nil, fmt.Errorf("option %s doesn't exist", opt)
}
opts = append(opts, opt)
}
opts = slicetools.Deduplicate(opts)
return opts, nil
}
func optionFilterDisable(opts []string, disableOpts []string) ([]string, error) {
if len(disableOpts) == 0 {
return opts, nil
}
for _, opt := range disableOpts {
if opt == "all" {
opts = []string{}
// keep validating the rest of the option names
continue
}
if !slices.Contains(csconfig.CONSOLE_CONFIGS, opt) {
return nil, fmt.Errorf("option %s doesn't exist", opt)
}
// discard all elements == opt
j := 0
for _, o := range opts {
if o != opt {
opts[j] = o
j++
}
}
opts = opts[:j]
}
return opts, nil
}
func (cli *cliConsole) newEnrollCmd() *cobra.Command {
name := ""
overwrite := false
tags := []string{}
opts := []string{}
enableOpts := []string{}
disableOpts := []string{}
cmd := &cobra.Command{
Use: "enroll [enroll-key]",
@ -168,12 +187,25 @@ After running this command your will need to validate the enrollment in the weba
Example: fmt.Sprintf(`cscli console enroll YOUR-ENROLL-KEY
cscli console enroll --name [instance_name] YOUR-ENROLL-KEY
cscli console enroll --name [instance_name] --tags [tag_1] --tags [tag_2] YOUR-ENROLL-KEY
cscli console enroll --enable context,manual YOUR-ENROLL-KEY
cscli console enroll --enable console_management YOUR-ENROLL-KEY
cscli console enroll --disable context YOUR-ENROLL-KEY
valid options are : %s,all (see 'cscli console status' for details)`, strings.Join(csconfig.CONSOLE_CONFIGS, ",")),
Args: cobra.ExactArgs(1),
Args: args.ExactArgs(1),
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error {
opts := []string{csconfig.SEND_MANUAL_SCENARIOS, csconfig.SEND_TAINTED_SCENARIOS, csconfig.SEND_CONTEXT}
opts, err := optionFilterEnable(opts, enableOpts)
if err != nil {
return err
}
opts, err = optionFilterDisable(opts, disableOpts)
if err != nil {
return err
}
return cli.enroll(cmd.Context(), args[0], name, overwrite, tags, opts)
},
}
@ -182,7 +214,8 @@ After running this command your will need to validate the enrollment in the weba
flags.StringVarP(&name, "name", "n", "", "Name to display in the console")
flags.BoolVarP(&overwrite, "overwrite", "", false, "Force enroll the instance")
flags.StringSliceVarP(&tags, "tags", "t", tags, "Tags to display in the console")
flags.StringSliceVarP(&opts, "enable", "e", opts, "Enable console options")
flags.StringSliceVarP(&enableOpts, "enable", "e", enableOpts, "Enable console options")
flags.StringSliceVarP(&disableOpts, "disable", "d", disableOpts, "Disable console options")
return cmd
}
@ -191,7 +224,7 @@ func (cli *cliConsole) newEnableCmd() *cobra.Command {
var enableAll bool
cmd := &cobra.Command{
Use: "enable [option]",
Use: "enable [option]...",
Short: "Enable a console option",
Example: "sudo cscli console enable tainted",
Long: `
@ -244,6 +277,9 @@ Disable given information push to the central API.`,
}
log.Infof("All features have been disabled")
} else {
if len(args) == 0 {
return errors.New("you must specify at least one feature to disable")
}
if err := cli.setConsoleOpts(args, false); err != nil {
return err
}
@ -267,6 +303,7 @@ func (cli *cliConsole) newStatusCmd() *cobra.Command {
Use: "status",
Short: "Shows status of the console options",
Example: `sudo cscli console status`,
Args: args.NoArgs,
DisableAutoGenTag: true,
RunE: func(_ *cobra.Command, _ []string) error {
cfg := cli.cfg()

View file

@ -17,6 +17,7 @@ import (
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/args"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/clialert"
"github.com/crowdsecurity/crowdsec/pkg/apiclient"
"github.com/crowdsecurity/crowdsec/pkg/csconfig"
@ -136,7 +137,6 @@ func (cli *cliDecisions) NewCommand() *cobra.Command {
Example: `cscli decisions [action] [filter]`,
Aliases: []string{"decision"},
/*TBD example*/
Args: cobra.MinimumNArgs(1),
DisableAutoGenTag: true,
PersistentPreRunE: func(_ *cobra.Command, _ []string) error {
cfg := cli.cfg()
@ -290,7 +290,7 @@ cscli decisions list -r 1.2.3.0/24
cscli decisions list -s crowdsecurity/ssh-bf
cscli decisions list --origin lists --scenario list_name
`,
Args: cobra.NoArgs,
Args: args.NoArgs,
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, _ []string) error {
return cli.list(cmd.Context(), filter, NoSimu, contained, printMachine)
@ -427,7 +427,7 @@ cscli decisions add --ip 1.2.3.4 --duration 24h --type captcha
cscli decisions add --scope username --value foobar
`,
/*TBD : fix long and example*/
Args: cobra.NoArgs,
Args: args.NoArgs,
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, _ []string) error {
return cli.add(cmd.Context(), addIP, addRange, addDuration, addValue, addScope, addReason, addType, bypassAllowlist)
@ -532,6 +532,7 @@ func (cli *cliDecisions) newDeleteCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "delete [options]",
Short: "Delete decisions",
Args: args.NoArgs,
DisableAutoGenTag: true,
Aliases: []string{"remove"},
Example: `cscli decisions delete -r 1.2.3.0/24

View file

@ -19,6 +19,7 @@ import (
"github.com/crowdsecurity/go-cs-lib/ptr"
"github.com/crowdsecurity/go-cs-lib/slicetools"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/args"
"github.com/crowdsecurity/crowdsec/pkg/models"
"github.com/crowdsecurity/crowdsec/pkg/types"
)
@ -216,7 +217,7 @@ func (cli *cliDecisions) newImportCmd() *cobra.Command {
Long: "expected format:\n" +
"csv : any of duration,reason,scope,type,value, with a header line\n" +
"json :" + "`{" + `"duration": "24h", "reason": "my_scenario", "scope": "ip", "type": "ban", "value": "x.y.z.z"` + "}`",
Args: cobra.NoArgs,
Args: args.NoArgs,
DisableAutoGenTag: true,
Example: `decisions.csv:
duration,scope,value

View file

@ -12,6 +12,7 @@ import (
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/args"
"github.com/crowdsecurity/crowdsec/pkg/csconfig"
"github.com/crowdsecurity/crowdsec/pkg/dumps"
"github.com/crowdsecurity/crowdsec/pkg/hubtest"
@ -80,7 +81,7 @@ cscli explain --log "Sep 19 18:33:22 scw-d95986 sshd[24347]: pam_unix(sshd:auth)
cscli explain --dsn "file://myfile.log" --type nginx
tail -n 5 myfile.log | cscli explain --type nginx -f -
`,
Args: cobra.NoArgs,
Args: args.NoArgs,
DisableAutoGenTag: true,
RunE: func(_ *cobra.Command, _ []string) error {
return cli.run()

View file

@ -12,6 +12,7 @@ import (
"github.com/spf13/cobra"
"gopkg.in/yaml.v3"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/args"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/reload"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/require"
"github.com/crowdsecurity/crowdsec/pkg/csconfig"
@ -42,7 +43,6 @@ The Hub is managed by cscli, to get the latest hub files from [Crowdsec Hub](htt
Example: `cscli hub list
cscli hub update
cscli hub upgrade`,
Args: cobra.NoArgs,
DisableAutoGenTag: true,
}
@ -90,7 +90,7 @@ func (cli *cliHub) newListCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "list [-a]",
Short: "List all installed configurations",
Args: cobra.NoArgs,
Args: args.NoArgs,
DisableAutoGenTag: true,
RunE: func(_ *cobra.Command, _ []string) error {
hub, err := require.Hub(cli.cfg(), log.StandardLogger())
@ -152,7 +152,7 @@ cscli hub update
# Download a 4x bigger version with all item contents (effectively pre-caching item downloads, but not data files).
cscli hub update --with-content`,
Args: cobra.NoArgs,
Args: args.NoArgs,
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, _ []string) error {
if cmd.Flags().Changed("with-content") {
@ -228,7 +228,7 @@ cscli hub upgrade --force
# Prompt for confirmation if running in an interactive terminal; otherwise, the option is ignored.
cscli hub upgrade --interactive
cscli hub upgrade -i`,
Args: cobra.NoArgs,
Args: args.NoArgs,
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, _ []string) error {
return cli.upgrade(cmd.Context(), interactive, dryRun, force)
@ -276,7 +276,7 @@ func (cli *cliHub) newTypesCmd() *cobra.Command {
Long: `
List the types of supported hub items.
`,
Args: cobra.NoArgs,
Args: args.NoArgs,
DisableAutoGenTag: true,
RunE: func(_ *cobra.Command, _ []string) error {
return cli.types()

View file

@ -4,13 +4,15 @@ import (
"fmt"
"github.com/spf13/cobra"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/args"
)
func (cli *cliHubTest) newCleanCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "clean",
Short: "clean [test_name]",
Args: cobra.MinimumNArgs(1),
Args: args.MinimumNArgs(1),
DisableAutoGenTag: true,
RunE: func(_ *cobra.Command, args []string) error {
for _, testName := range args {

View file

@ -9,6 +9,7 @@ import (
"github.com/fatih/color"
"github.com/spf13/cobra"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/args"
"github.com/crowdsecurity/crowdsec/pkg/hubtest"
)
@ -151,6 +152,7 @@ func (cli *cliHubTest) newCoverageCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "coverage",
Short: "coverage",
Args: args.NoArgs,
DisableAutoGenTag: true,
RunE: func(_ *cobra.Command, _ []string) error {
return cli.coverage(showScenarioCov, showParserCov, showAppsecCov, showOnlyPercent)

View file

@ -10,6 +10,7 @@ import (
"github.com/spf13/cobra"
"gopkg.in/yaml.v3"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/args"
"github.com/crowdsecurity/crowdsec/pkg/hubtest"
)
@ -30,7 +31,7 @@ func (cli *cliHubTest) newCreateCmd() *cobra.Command {
Example: `cscli hubtest create my-awesome-test --type syslog
cscli hubtest create my-nginx-custom-test --type nginx
cscli hubtest create my-scenario-test --parsers crowdsecurity/nginx --scenarios crowdsecurity/http-probing`,
Args: cobra.ExactArgs(1),
Args: args.ExactArgs(1),
DisableAutoGenTag: true,
RunE: func(_ *cobra.Command, args []string) error {
testName := args[0]

View file

@ -4,6 +4,8 @@ import (
"fmt"
"github.com/spf13/cobra"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/args"
)
func (cli *cliHubTest) newEvalCmd() *cobra.Command {
@ -11,8 +13,8 @@ func (cli *cliHubTest) newEvalCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "eval",
Short: "eval [test_name]",
Args: cobra.ExactArgs(1),
Short: "eval [test_name]...",
Args: args.MinimumNArgs(1),
DisableAutoGenTag: true,
RunE: func(_ *cobra.Command, args []string) error {
for _, testName := range args {

View file

@ -6,6 +6,8 @@ import (
"github.com/spf13/cobra"
"github.com/crowdsecurity/crowdsec/pkg/dumps"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/args"
)
func (cli *cliHubTest) explain(testName string, details bool, skipOk bool) error {
@ -58,7 +60,7 @@ func (cli *cliHubTest) newExplainCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "explain",
Short: "explain [test_name]",
Args: cobra.ExactArgs(1),
Args: args.MinimumNArgs(1),
DisableAutoGenTag: true,
RunE: func(_ *cobra.Command, args []string) error {
for _, testName := range args {

View file

@ -39,7 +39,6 @@ func (cli *cliHubTest) NewCommand() *cobra.Command {
Use: "hubtest",
Short: "Run functional tests on hub configurations",
Long: "Run functional tests on hub configurations (parsers, scenarios, collections...)",
Args: cobra.NoArgs,
DisableAutoGenTag: true,
PersistentPreRunE: func(_ *cobra.Command, _ []string) error {
var err error

View file

@ -7,6 +7,7 @@ import (
"github.com/spf13/cobra"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/args"
"github.com/crowdsecurity/crowdsec/pkg/hubtest"
)
@ -14,7 +15,7 @@ func (cli *cliHubTest) newInfoCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "info",
Short: "info [test_name]",
Args: cobra.MinimumNArgs(1),
Args: args.MinimumNArgs(1),
DisableAutoGenTag: true,
RunE: func(_ *cobra.Command, args []string) error {
for _, testName := range args {

View file

@ -7,12 +7,15 @@ import (
"github.com/fatih/color"
"github.com/spf13/cobra"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/args"
)
func (cli *cliHubTest) newListCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "list",
Short: "list",
Args: args.NoArgs,
DisableAutoGenTag: true,
RunE: func(_ *cobra.Command, _ []string) error {
cfg := cli.cfg()

View file

@ -17,6 +17,7 @@ import (
"github.com/spf13/cobra"
"gopkg.in/yaml.v3"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/args"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/require"
"github.com/crowdsecurity/crowdsec/pkg/cwhub"
)
@ -171,7 +172,7 @@ func (cli cliItem) newInspectCmd() *cobra.Command {
Short: cmp.Or(cli.inspectHelp.short, "Inspect given "+cli.oneOrMore),
Long: cmp.Or(cli.inspectHelp.long, "Inspect the state of one or more "+cli.name),
Example: cli.inspectHelp.example,
Args: cobra.MinimumNArgs(1),
Args: args.MinimumNArgs(1),
DisableAutoGenTag: true,
ValidArgsFunction: func(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return compInstalledItems(cli.name, args, toComplete, cli.cfg)

View file

@ -12,6 +12,7 @@ import (
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/args"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/reload"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/require"
"github.com/crowdsecurity/crowdsec/pkg/cwhub"
@ -129,7 +130,7 @@ func (cli cliItem) newInstallCmd() *cobra.Command {
Short: cmp.Or(cli.installHelp.short, "Install given "+cli.oneOrMore),
Long: cmp.Or(cli.installHelp.long, fmt.Sprintf("Fetch and install one or more %s from the hub", cli.name)),
Example: cli.installHelp.example,
Args: cobra.MinimumNArgs(1),
Args: args.MinimumNArgs(1),
DisableAutoGenTag: true,
ValidArgsFunction: func(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return compAllItems(cli.name, args, toComplete, cli.cfg)

View file

@ -46,7 +46,6 @@ func (cli cliItem) NewCommand() *cobra.Command {
Short: cmp.Or(cli.help.short, "Manage hub "+cli.name),
Long: cli.help.long,
Example: cli.help.example,
Args: cobra.MinimumNArgs(1),
Aliases: []string{cli.singular},
DisableAutoGenTag: true,
}

View file

@ -11,6 +11,7 @@ import (
"github.com/spf13/cobra"
"gopkg.in/yaml.v3"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/args"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/require"
"github.com/crowdsecurity/crowdsec/pkg/alertcontext"
"github.com/crowdsecurity/crowdsec/pkg/exprhelpers"
@ -57,6 +58,7 @@ func (cli *cliLapi) newContextAddCmd() *cobra.Command {
cscli lapi context add --key file_source --value evt.Line.Src
cscli lapi context add --value evt.Meta.source_ip --value evt.Meta.target_user
`,
Args: args.NoArgs,
DisableAutoGenTag: true,
RunE: func(_ *cobra.Command, _ []string) error {
hub, err := require.Hub(cli.cfg(), nil)
@ -98,6 +100,7 @@ func (cli *cliLapi) newContextStatusCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "status",
Short: "List context to send with alerts",
Args: args.NoArgs,
DisableAutoGenTag: true,
RunE: func(_ *cobra.Command, _ []string) error {
cfg := cli.cfg()

View file

@ -24,7 +24,6 @@ func (cli *cliLapi) NewCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "lapi [action]",
Short: "Manage interaction with Local API (LAPI)",
Args: cobra.MinimumNArgs(1),
DisableAutoGenTag: true,
PersistentPreRunE: func(_ *cobra.Command, _ []string) error {
if err := cli.cfg().LoadAPIClient(); err != nil {

View file

@ -10,6 +10,7 @@ import (
"github.com/spf13/cobra"
"gopkg.in/yaml.v3"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/args"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/idgen"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/reload"
"github.com/crowdsecurity/crowdsec/pkg/apiclient"
@ -107,7 +108,7 @@ func (cli *cliLapi) newRegisterCmd() *cobra.Command {
Short: "Register a machine to Local API (LAPI)",
Long: `Register your machine to the Local API (LAPI).
Keep in mind the machine needs to be validated by an administrator on LAPI side to be effective.`,
Args: cobra.MinimumNArgs(0),
Args: args.NoArgs,
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, _ []string) error {
return cli.register(cmd.Context(), apiURL, outputFile, machine, token)

View file

@ -12,6 +12,7 @@ import (
"github.com/go-openapi/strfmt"
"github.com/spf13/cobra"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/args"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/require"
"github.com/crowdsecurity/crowdsec/pkg/apiclient"
"github.com/crowdsecurity/crowdsec/pkg/csconfig"
@ -99,7 +100,7 @@ func (cli *cliLapi) newStatusCmd() *cobra.Command {
cmdLapiStatus := &cobra.Command{
Use: "status",
Short: "Check authentication to Local API (LAPI)",
Args: cobra.MinimumNArgs(0),
Args: args.NoArgs,
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, _ []string) error {
hub, err := require.Hub(cli.cfg(), nil)

View file

@ -11,6 +11,7 @@ import (
"github.com/spf13/cobra"
"gopkg.in/yaml.v3"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/args"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/idgen"
"github.com/crowdsecurity/crowdsec/pkg/csconfig"
"github.com/crowdsecurity/crowdsec/pkg/types"
@ -134,6 +135,7 @@ func (cli *cliMachines) newAddCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "add",
Short: "add a single machine to the database",
Args: args.MaximumNArgs(1),
DisableAutoGenTag: true,
Long: `Register a new machine in the database. cscli should be on the same machine as LAPI.`,
Example: `cscli machines add --auto

View file

@ -7,6 +7,7 @@ import (
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/args"
"github.com/crowdsecurity/crowdsec/pkg/database"
)
@ -36,7 +37,7 @@ func (cli *cliMachines) newDeleteCmd() *cobra.Command {
Use: "delete [machine_name]...",
Short: "delete machine(s) by name",
Example: `cscli machines delete "machine1" "machine2"`,
Args: cobra.MinimumNArgs(1),
Args: args.MinimumNArgs(1),
Aliases: []string{"remove"},
DisableAutoGenTag: true,
ValidArgsFunction: cli.validMachineID,

View file

@ -11,6 +11,7 @@ import (
"github.com/jedib0t/go-pretty/v6/table"
"github.com/spf13/cobra"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/args"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/clientinfo"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/cstable"
"github.com/crowdsecurity/crowdsec/pkg/cwhub"
@ -156,7 +157,7 @@ func (cli *cliMachines) newInspectCmd() *cobra.Command {
Use: "inspect [machine_name]",
Short: "inspect a machine by name",
Example: `cscli machines inspect "machine1"`,
Args: cobra.ExactArgs(1),
Args: args.ExactArgs(1),
DisableAutoGenTag: true,
ValidArgsFunction: cli.validMachineID,
RunE: func(cmd *cobra.Command, args []string) error {

View file

@ -13,6 +13,7 @@ import (
"github.com/jedib0t/go-pretty/v6/table"
"github.com/spf13/cobra"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/args"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/clientinfo"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/cstable"
"github.com/crowdsecurity/crowdsec/pkg/database"
@ -125,7 +126,7 @@ func (cli *cliMachines) newListCmd() *cobra.Command {
Short: "list all machines in the database",
Long: `list all machines in the database with their status and last heartbeat`,
Example: `cscli machines list`,
Args: cobra.NoArgs,
Args: args.NoArgs,
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, _ []string) error {
return cli.List(cmd.Context(), color.Output, cli.db)

View file

@ -9,6 +9,7 @@ import (
"github.com/fatih/color"
"github.com/spf13/cobra"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/args"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/ask"
"github.com/crowdsecurity/crowdsec/pkg/database/ent"
)
@ -80,7 +81,7 @@ func (cli *cliMachines) newPruneCmd() *cobra.Command {
Example: `cscli machines prune
cscli machines prune --duration 1h
cscli machines prune --not-validated-only --force`,
Args: cobra.NoArgs,
Args: args.NoArgs,
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, _ []string) error {
return cli.prune(cmd.Context(), duration, notValidOnly, force)

View file

@ -6,6 +6,8 @@ import (
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/args"
)
func (cli *cliMachines) validate(ctx context.Context, machineID string) error {
@ -24,7 +26,7 @@ func (cli *cliMachines) newValidateCmd() *cobra.Command {
Short: "validate a machine to access the local API",
Long: `validate a machine to access the local API.`,
Example: `cscli machines validate "machine_name"`,
Args: cobra.ExactArgs(1),
Args: args.ExactArgs(1),
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error {
return cli.validate(cmd.Context(), args[0])

View file

@ -11,6 +11,7 @@ import (
"github.com/crowdsecurity/go-cs-lib/maptools"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/args"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/cstable"
)
@ -83,7 +84,7 @@ func (cli *cliMetrics) newListCmd() *cobra.Command {
Use: "list",
Short: "List available types of metrics.",
Long: `List available types of metrics.`,
Args: cobra.NoArgs,
Args: args.NoArgs,
DisableAutoGenTag: true,
RunE: func(_ *cobra.Command, _ []string) error {
return cli.list()

View file

@ -36,7 +36,6 @@ cscli metrics --url http://lapi.local:6060/metrics show acquisition parsers
# List available metric types
cscli metrics list`,
Args: cobra.NoArgs,
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, _ []string) error {
return cli.show(cmd.Context(), nil, url, noUnit)

View file

@ -24,6 +24,7 @@ import (
"github.com/crowdsecurity/go-cs-lib/ptr"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/args"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/require"
"github.com/crowdsecurity/crowdsec/pkg/apiclient"
"github.com/crowdsecurity/crowdsec/pkg/csconfig"
@ -56,7 +57,6 @@ func (cli *cliNotifications) NewCommand() *cobra.Command {
Use: "notifications [action]",
Short: "Helper for notification plugin configuration",
Long: "To list/inspect/test notification template",
Args: cobra.MinimumNArgs(1),
Aliases: []string{"notifications", "notification"},
DisableAutoGenTag: true,
PersistentPreRunE: func(_ *cobra.Command, _ []string) error {
@ -158,7 +158,7 @@ func (cli *cliNotifications) newListCmd() *cobra.Command {
Short: "list notifications plugins",
Long: `list notifications plugins and their status (active or not)`,
Example: `cscli notifications list`,
Args: cobra.NoArgs,
Args: args.NoArgs,
DisableAutoGenTag: true,
RunE: func(_ *cobra.Command, _ []string) error {
cfg := cli.cfg()
@ -207,7 +207,7 @@ func (cli *cliNotifications) newInspectCmd() *cobra.Command {
Short: "Inspect notifications plugin",
Long: `Inspect notifications plugin and show configuration`,
Example: `cscli notifications inspect <plugin_name>`,
Args: cobra.ExactArgs(1),
Args: args.ExactArgs(1),
ValidArgsFunction: cli.notificationConfigFilter,
DisableAutoGenTag: true,
RunE: func(_ *cobra.Command, args []string) error {
@ -272,7 +272,7 @@ func (cli *cliNotifications) newTestCmd() *cobra.Command {
Short: "send a generic test alert to notification plugin",
Long: `send a generic test alert to a notification plugin even if it is not active in profiles`,
Example: `cscli notifications test [plugin_name]`,
Args: cobra.ExactArgs(1),
Args: args.ExactArgs(1),
DisableAutoGenTag: true,
ValidArgsFunction: cli.notificationConfigFilter,
PreRunE: func(cmd *cobra.Command, args []string) error {
@ -367,7 +367,7 @@ cscli notifications reinject <alert_id>
cscli notifications reinject <alert_id> -a '{"remediation": false,"scenario":"notification/test"}'
cscli notifications reinject <alert_id> -a '{"remediation": true,"scenario":"notification/test"}'
`,
Args: cobra.ExactArgs(1),
Args: args.ExactArgs(1),
DisableAutoGenTag: true,
PreRunE: func(cmd *cobra.Command, args []string) error {
var err error

View file

@ -13,6 +13,7 @@ import (
"github.com/crowdsecurity/go-cs-lib/ptr"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/args"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/require"
"github.com/crowdsecurity/crowdsec/pkg/apiserver"
"github.com/crowdsecurity/crowdsec/pkg/csconfig"
@ -35,7 +36,6 @@ func (cli *cliPapi) NewCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "papi [action]",
Short: "Manage interaction with Polling API (PAPI)",
Args: cobra.MinimumNArgs(1),
DisableAutoGenTag: true,
PersistentPreRunE: func(_ *cobra.Command, _ []string) error {
cfg := cli.cfg()
@ -100,7 +100,7 @@ func (cli *cliPapi) newStatusCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "status",
Short: "Get status of the Polling API",
Args: cobra.MinimumNArgs(0),
Args: args.NoArgs,
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, _ []string) error {
cfg := cli.cfg()
@ -155,7 +155,7 @@ func (cli *cliPapi) newSyncCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "sync",
Short: "Sync with the Polling API, pulling all non-expired orders for the instance",
Args: cobra.MinimumNArgs(0),
Args: args.NoArgs,
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, _ []string) error {
cfg := cli.cfg()

View file

@ -13,6 +13,7 @@ import (
"github.com/spf13/cobra"
"gopkg.in/yaml.v3"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/args"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/require"
"github.com/crowdsecurity/crowdsec/pkg/csconfig"
"github.com/crowdsecurity/crowdsec/pkg/setup"
@ -35,7 +36,6 @@ func (cli *cliSetup) NewCommand() *cobra.Command {
Use: "setup",
Short: "Tools to configure crowdsec",
Long: "Manage hub configuration and service detection",
Args: cobra.MinimumNArgs(0),
DisableAutoGenTag: true,
}
@ -82,6 +82,7 @@ func (cli *cliSetup) newDetectCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "detect",
Short: "detect running services, generate a setup file",
Args: args.NoArgs,
DisableAutoGenTag: true,
RunE: func(_ *cobra.Command, args []string) error {
return cli.detect(f)
@ -102,7 +103,7 @@ func (cli *cliSetup) newInstallHubCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "install-hub [setup_file] [flags]",
Short: "install items from a setup file",
Args: cobra.ExactArgs(1),
Args: args.ExactArgs(1),
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error {
return cli.install(cmd.Context(), interactive, dryRun, args[0])
@ -123,7 +124,7 @@ func (cli *cliSetup) newDataSourcesCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "datasources [setup_file] [flags]",
Short: "generate datasource (acquisition) configuration from a setup file",
Args: cobra.ExactArgs(1),
Args: args.ExactArgs(1),
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error {
return cli.dataSources(args[0], toDir)
@ -140,7 +141,7 @@ func (cli *cliSetup) newValidateCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "validate [setup_file]",
Short: "validate a setup file",
Args: cobra.ExactArgs(1),
Args: args.ExactArgs(1),
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error {
return cli.validate(args[0])

View file

@ -10,6 +10,7 @@ import (
"github.com/spf13/cobra"
"gopkg.in/yaml.v3"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/args"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/reload"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/require"
"github.com/crowdsecurity/crowdsec/pkg/csconfig"
@ -174,6 +175,7 @@ func (cli *cliSimulation) newStatusCmd() *cobra.Command {
Use: "status",
Short: "Show simulation mode status",
Example: `cscli simulation status`,
Args: args.NoArgs,
DisableAutoGenTag: true,
Run: func(_ *cobra.Command, _ []string) {
cli.status()

View file

@ -22,6 +22,7 @@ import (
"github.com/crowdsecurity/go-cs-lib/trace"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/args"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/clibouncer"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/clicapi"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/clihub"
@ -112,7 +113,7 @@ func (cli *cliSupport) dumpMetrics(ctx context.Context, db *database.Client, zw
return fmt.Errorf("could not format prometheus metrics: %w", err)
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, cfg.Cscli.PrometheusUrl, nil)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, cfg.Cscli.PrometheusUrl, http.NoBody)
if err != nil {
return fmt.Errorf("could not create request to prometheus endpoint: %w", err)
}
@ -327,7 +328,7 @@ func (cli *cliSupport) dumpPprof(ctx context.Context, zw *zip.Writer, prometheus
),
endpoint,
),
nil,
http.NoBody,
)
if err != nil {
return fmt.Errorf("could not create request to pprof endpoint: %w", err)
@ -409,7 +410,6 @@ func (cli *cliSupport) NewCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "support [action]",
Short: "Provide commands to help during support",
Args: cobra.MinimumNArgs(1),
DisableAutoGenTag: true,
}
@ -625,7 +625,7 @@ func (cli *cliSupport) NewDumpCmd() *cobra.Command {
Example: `cscli support dump
cscli support dump -f /tmp/crowdsec-support.zip
`,
Args: cobra.NoArgs,
Args: args.NoArgs,
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, _ []string) error {
output := cli.cfg().Cscli.Output

View file

@ -4,6 +4,8 @@ import (
"os"
"github.com/spf13/cobra"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/args"
)
func NewCompletionCmd() *cobra.Command {
@ -67,7 +69,7 @@ func NewCompletionCmd() *cobra.Command {
DisableFlagsInUseLine: true,
DisableAutoGenTag: true,
ValidArgs: []string{"bash", "zsh", "powershell", "fish"},
Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs),
Args: cobra.MatchAll(args.ExactArgs(1), cobra.OnlyValidArgs),
Run: func(cmd *cobra.Command, args []string) {
switch args[0] {
case "bash":

View file

@ -22,6 +22,7 @@ import (
"github.com/crowdsecurity/go-cs-lib/version"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/args"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/idgen"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/require"
"github.com/crowdsecurity/crowdsec/pkg/metabase"
@ -64,7 +65,6 @@ func (cli *cliDashboard) NewCommand() *cobra.Command {
Long: `Install/Start/Stop/Remove a metabase container exposing dashboard and metrics.
Note: This command requires database direct access, so is intended to be run on Local API/master.
`,
Args: cobra.ExactArgs(1),
DisableAutoGenTag: true,
Example: `
cscli dashboard setup
@ -130,7 +130,7 @@ func (cli *cliDashboard) newSetupCmd() *cobra.Command {
Use: "setup",
Short: "Setup a metabase container.",
Long: `Perform a metabase docker setup, download standard dashboards, create a fresh user and start the container`,
Args: cobra.NoArgs,
Args: args.NoArgs,
DisableAutoGenTag: true,
Example: `
cscli dashboard setup
@ -206,7 +206,7 @@ func (cli *cliDashboard) newStartCmd() *cobra.Command {
Use: "start",
Short: "Start the metabase container.",
Long: `Stats the metabase container using docker.`,
Args: cobra.NoArgs,
Args: args.NoArgs,
DisableAutoGenTag: true,
RunE: func(_ *cobra.Command, _ []string) error {
mb, err := metabase.NewMetabase(metabaseConfigPath, metabaseContainerID)
@ -237,7 +237,7 @@ func (cli *cliDashboard) newStopCmd() *cobra.Command {
Use: "stop",
Short: "Stops the metabase container.",
Long: `Stops the metabase container using docker.`,
Args: cobra.NoArgs,
Args: args.NoArgs,
DisableAutoGenTag: true,
RunE: func(_ *cobra.Command, _ []string) error {
if err := metabase.StopContainer(metabaseContainerID); err != nil {
@ -254,7 +254,7 @@ func (cli *cliDashboard) newShowPasswordCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "show-password",
Short: "displays password of metabase.",
Args: cobra.NoArgs,
Args: args.NoArgs,
DisableAutoGenTag: true,
RunE: func(_ *cobra.Command, _ []string) error {
m := metabase.Metabase{}
@ -277,7 +277,7 @@ func (cli *cliDashboard) newRemoveCmd() *cobra.Command {
Use: "remove",
Short: "removes the metabase container.",
Long: `removes the metabase container using docker.`,
Args: cobra.NoArgs,
Args: args.NoArgs,
DisableAutoGenTag: true,
Example: `
cscli dashboard remove

View file

@ -7,6 +7,8 @@ import (
"github.com/spf13/cobra"
"github.com/spf13/cobra/doc"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/args"
)
type cliDoc struct{}
@ -23,7 +25,7 @@ func (cli cliDoc) NewCommand(rootCmd *cobra.Command) *cobra.Command {
cmd := &cobra.Command{
Use: "doc",
Short: "Generate the documentation related to cscli commands. Target directory must exist.",
Args: cobra.NoArgs,
Args: args.NoArgs,
Hidden: true,
DisableAutoGenTag: true,
RunE: func(_ *cobra.Command, args []string) error {

View file

@ -25,7 +25,7 @@ func lookupLatest(ctx context.Context) (string, error) {
url := "https://version.crowdsec.net/latest"
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, http.NoBody)
if err != nil {
return "", fmt.Errorf("unable to create request for %s: %w", url, err)
}

View file

@ -5,6 +5,7 @@ import (
"github.com/spf13/cobra"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/args"
"github.com/crowdsecurity/crowdsec/pkg/cwversion"
)
@ -18,7 +19,7 @@ func (cliVersion) NewCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "version",
Short: "Display version",
Args: cobra.NoArgs,
Args: args.NoArgs,
DisableAutoGenTag: true,
Run: func(_ *cobra.Command, _ []string) {
_, _ = os.Stdout.WriteString(cwversion.FullString())

View file

@ -186,9 +186,10 @@ func (f *Flags) Parse() {
flag.Parse()
}
func newLogLevel(curLevelPtr *log.Level, f *Flags) *log.Level {
func newLogLevel(curLevelPtr *log.Level, f *Flags) (*log.Level, bool) {
// mother of all defaults
ret := log.InfoLevel
logLevelViaFlag := true
// keep if already set
if curLevelPtr != nil {
@ -210,14 +211,16 @@ func newLogLevel(curLevelPtr *log.Level, f *Flags) *log.Level {
case f.LogLevelFatal:
ret = log.FatalLevel
default:
// We set logLevelViaFlag to false in default cause no flag was provided
logLevelViaFlag = false
}
if curLevelPtr != nil && ret == *curLevelPtr {
// avoid returning a new ptr to the same value
return curLevelPtr
return curLevelPtr, logLevelViaFlag
}
return &ret
return &ret, logLevelViaFlag
}
// LoadConfig returns a configuration parsed from configuration file
@ -230,8 +233,8 @@ func LoadConfig(configFile string, disableAgent bool, disableAPI bool, quiet boo
if err := trace.Init(filepath.Join(cConfig.ConfigPaths.DataDir, "trace")); err != nil {
return nil, fmt.Errorf("while setting up trace directory: %w", err)
}
cConfig.Common.LogLevel = newLogLevel(cConfig.Common.LogLevel, flags)
var logLevelViaFlag bool
cConfig.Common.LogLevel, logLevelViaFlag = newLogLevel(cConfig.Common.LogLevel, flags)
if dumpFolder != "" {
parser.ParseDump = true
@ -250,7 +253,7 @@ func LoadConfig(configFile string, disableAgent bool, disableAPI bool, quiet boo
cConfig.Common.LogDir, *cConfig.Common.LogLevel,
cConfig.Common.LogMaxSize, cConfig.Common.LogMaxFiles,
cConfig.Common.LogMaxAge, cConfig.Common.LogFormat, cConfig.Common.CompressLogs,
cConfig.Common.ForceColorLogs); err != nil {
cConfig.Common.ForceColorLogs, logLevelViaFlag); err != nil {
return nil, err
}

2
go.mod
View file

@ -1,6 +1,6 @@
module github.com/crowdsecurity/crowdsec
go 1.24.0
go 1.24.1
require (
entgo.io/ent v0.14.2

View file

@ -92,14 +92,10 @@ func registerDataSource(dataSourceType string, dsGetter func() DataSource) {
// setupLogger creates a logger for the datasource to use at runtime.
func setupLogger(source, name string, level *log.Level) (*log.Entry, error) {
clog := log.New()
if err := types.ConfigureLogger(clog); err != nil {
if err := types.ConfigureLogger(clog, level); err != nil {
return nil, fmt.Errorf("while configuring datasource logger: %w", err)
}
if level != nil {
clog.SetLevel(*level)
}
fields := log.Fields{
"type": source,
}

View file

@ -2,9 +2,12 @@ package appsecacquisition
import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/json"
"errors"
"fmt"
"io/fs"
"net"
"net/http"
"os"
@ -64,6 +67,7 @@ type AppsecSource struct {
AuthCache AuthCache
AppsecRunners []AppsecRunner // one for each go-routine
appsecAllowlistClient *allowlists.AppsecAllowlist
lapiCACertPool *x509.CertPool
}
// Struct to handle cache of authentication
@ -158,6 +162,28 @@ func (w *AppsecSource) GetAggregMetrics() []prometheus.Collector {
return []prometheus.Collector{AppsecReqCounter, AppsecBlockCounter, AppsecRuleHits, AppsecOutbandParsingHistogram, AppsecInbandParsingHistogram, AppsecGlobalParsingHistogram}
}
func loadCertPool(caCertPath string, logger log.FieldLogger) (*x509.CertPool, error) {
caCertPool, err := x509.SystemCertPool()
if err != nil {
logger.Warnf("Error loading system CA certificates: %s", err)
}
if caCertPool == nil {
caCertPool = x509.NewCertPool()
}
if caCertPath != "" {
caCert, err := os.ReadFile(caCertPath)
if err != nil {
return nil, fmt.Errorf("while opening cert file: %w", err)
}
caCertPool.AppendCertsFromPEM(caCert)
}
return caCertPool, nil
}
func (w *AppsecSource) Configure(yamlConfig []byte, logger *log.Entry, metricsLevel int) error {
err := w.UnmarshalConfig(yamlConfig)
if err != nil {
@ -241,8 +267,7 @@ func (w *AppsecSource) Configure(yamlConfig []byte, logger *log.Entry, metricsLe
appsecAllowlistsClient: w.appsecAllowlistClient,
}
err := runner.Init(appsecCfg.GetDataDir())
if err != nil {
if err = runner.Init(appsecCfg.GetDataDir()); err != nil {
return fmt.Errorf("unable to initialize runner: %w", err)
}
@ -254,6 +279,19 @@ func (w *AppsecSource) Configure(yamlConfig []byte, logger *log.Entry, metricsLe
// We don´t use the wrapper provided by coraza because we want to fully control what happens when a rule match to send the information in crowdsec
w.mux.HandleFunc(w.config.Path, w.appsecHandler)
csConfig := csconfig.GetConfig()
caCertPath := ""
if csConfig.API.Client != nil && csConfig.API.Client.Credentials != nil {
caCertPath = csConfig.API.Client.Credentials.CACertPath
}
w.lapiCACertPool, err = loadCertPool(caCertPath, w.logger)
if err != nil {
return fmt.Errorf("unable to load LAPI CA cert pool: %w", err)
}
return nil
}
@ -273,6 +311,103 @@ func (w *AppsecSource) OneShotAcquisition(_ context.Context, _ chan types.Event,
return errors.New("AppSec datasource does not support command line acquisition")
}
func (w *AppsecSource) listenAndServe(ctx context.Context, t *tomb.Tomb) error {
defer trace.CatchPanic("crowdsec/acquis/appsec/listenAndServe")
w.logger.Infof("%d appsec runner to start", len(w.AppsecRunners))
serverError := make(chan error, 2)
startServer := func(listener net.Listener, canTLS bool) {
var err error
if canTLS && (w.config.CertFilePath != "" || w.config.KeyFilePath != "") {
if w.config.KeyFilePath == "" {
serverError <- errors.New("missing TLS key file")
return
}
if w.config.CertFilePath == "" {
serverError <- errors.New("missing TLS cert file")
return
}
err = w.server.ServeTLS(listener, w.config.CertFilePath, w.config.KeyFilePath)
} else {
err = w.server.Serve(listener)
}
switch {
case errors.Is(err, http.ErrServerClosed):
break
case err != nil:
serverError <- err
}
}
// Starting Unix socket listener
go func(socket string) {
if socket == "" {
return
}
if err := os.Remove(w.config.ListenSocket); err != nil {
if !errors.Is(err, fs.ErrNotExist) {
w.logger.Errorf("can't remove socket %s: %s", socket, err)
}
}
w.logger.Infof("creating unix socket %s", socket)
listener, err := net.Listen("unix", socket)
if err != nil {
serverError <- fmt.Errorf("appsec server failed: %w", err)
return
}
w.logger.Infof("Appsec listening on Unix socket %s", socket)
startServer(listener, false)
}(w.config.ListenSocket)
// Starting TCP listener
go func(url string) {
if url == "" {
return
}
listener, err := net.Listen("tcp", url)
if err != nil {
serverError <- fmt.Errorf("listening on %s: %w", url, err)
}
w.logger.Infof("Appsec listening on %s", url)
startServer(listener, true)
}(w.config.ListenAddr)
select {
case err := <-serverError:
return err
case <-t.Dying():
w.logger.Info("Shutting down Appsec server")
// xx let's clean up the appsec runners :)
appsec.AppsecRulesDetails = make(map[int]appsec.RulesDetails)
if err := w.server.Shutdown(ctx); err != nil {
w.logger.Errorf("Error shutting down Appsec server: %s", err.Error())
}
if w.config.ListenSocket != "" {
if err := os.Remove(w.config.ListenSocket); err != nil {
if !errors.Is(err, fs.ErrNotExist) {
w.logger.Errorf("can't remove socket %s: %s", w.config.ListenSocket, err)
}
}
}
}
return nil
}
func (w *AppsecSource) StreamingAcquisition(ctx context.Context, out chan types.Event, t *tomb.Tomb) error {
w.outChan = out
@ -285,13 +420,12 @@ func (w *AppsecSource) StreamingAcquisition(ctx context.Context, out chan types.
if err != nil {
return fmt.Errorf("failed to fetch allowlists: %w", err)
}
w.appsecAllowlistClient.StartRefresh(ctx, t)
t.Go(func() error {
defer trace.CatchPanic("crowdsec/acquis/appsec/live")
w.logger.Infof("%d appsec runner to start", len(w.AppsecRunners))
for _, runner := range w.AppsecRunners {
runner.outChan = out
@ -301,60 +435,7 @@ func (w *AppsecSource) StreamingAcquisition(ctx context.Context, out chan types.
})
}
t.Go(func() error {
if w.config.ListenSocket != "" {
w.logger.Infof("creating unix socket %s", w.config.ListenSocket)
_ = os.RemoveAll(w.config.ListenSocket)
listener, err := net.Listen("unix", w.config.ListenSocket)
if err != nil {
return fmt.Errorf("appsec server failed: %w", err)
}
defer listener.Close()
if w.config.CertFilePath != "" && w.config.KeyFilePath != "" {
err = w.server.ServeTLS(listener, w.config.CertFilePath, w.config.KeyFilePath)
} else {
err = w.server.Serve(listener)
}
if err != nil && !errors.Is(err, http.ErrServerClosed) {
return fmt.Errorf("appsec server failed: %w", err)
}
}
return nil
})
t.Go(func() error {
var err error
if w.config.ListenAddr != "" {
w.logger.Infof("creating TCP server on %s", w.config.ListenAddr)
if w.config.CertFilePath != "" && w.config.KeyFilePath != "" {
err = w.server.ListenAndServeTLS(w.config.CertFilePath, w.config.KeyFilePath)
} else {
err = w.server.ListenAndServe()
}
if err != nil && err != http.ErrServerClosed {
return fmt.Errorf("appsec server failed: %w", err)
}
}
return nil
})
<-t.Dying()
w.logger.Info("Shutting down Appsec server")
// xx let's clean up the appsec runners :)
appsec.AppsecRulesDetails = make(map[int]appsec.RulesDetails)
if err := w.server.Shutdown(ctx); err != nil {
w.logger.Errorf("Error shutting down Appsec server: %s", err.Error())
}
return nil
return w.listenAndServe(ctx, t)
})
return nil
@ -373,21 +454,29 @@ func (w *AppsecSource) Dump() interface{} {
}
func (w *AppsecSource) IsAuth(ctx context.Context, apiKey string) bool {
client := &http.Client{
Timeout: 200 * time.Millisecond,
}
req, err := http.NewRequestWithContext(ctx, http.MethodHead, w.lapiURL, nil)
req, err := http.NewRequestWithContext(ctx, http.MethodHead, w.lapiURL, http.NoBody)
if err != nil {
log.Errorf("Error creating request: %s", err)
w.logger.Errorf("Error creating request: %s", err)
return false
}
req.Header.Add("X-Api-Key", apiKey)
client := &http.Client{
Timeout: 200 * time.Millisecond,
}
if w.lapiCACertPool != nil {
client.Transport = &http.Transport{
TLSClientConfig: &tls.Config{
RootCAs: w.lapiCACertPool,
},
}
}
resp, err := client.Do(req)
if err != nil {
log.Errorf("Error performing request: %s", err)
w.logger.Errorf("Error performing request: %s", err)
return false
}

View file

@ -332,6 +332,67 @@ func TestAppsecOnMatchHooks(t *testing.T) {
require.Equal(t, appsec.CaptchaRemediation, responses[0].Action)
},
},
{
name: "on_match: SendAlert() with out-of-band rule",
expected_load_ok: true,
outofband_rules: []appsec_rule.CustomRule{
{
Name: "rule42",
Zones: []string{"ARGS"},
Variables: []string{"foo"},
Match: appsec_rule.Match{Type: "regex", Value: "^toto"},
Transform: []string{"lowercase"},
},
},
DefaultRemediation: appsec.AllowRemediation,
on_match: []appsec.Hook{
{Filter: "IsInBand == false", Apply: []string{"SendAlert()"}},
},
input_request: appsec.ParsedRequest{
ClientIP: "1.2.3.4",
RemoteAddr: "1.2.3.4",
Method: "GET",
URI: "/urllll",
Args: url.Values{"foo": []string{"toto"}},
},
output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse, appsecResponse appsec.BodyResponse, statusCode int) {
require.Equal(t, appsec.AllowRemediation, appsecResponse.Action)
require.Equal(t, http.StatusOK, appsecResponse.HTTPStatus)
require.Equal(t, http.StatusOK, statusCode)
// We have both an event an overflow
require.Len(t, events, 2)
require.Equal(t, types.LOG, events[0].Type)
require.Equal(t, types.APPSEC, events[1].Type)
require.Nil(t, events[0].Overflow.Alert)
require.NotNil(t, events[1].Overflow.Alert)
},
},
{
name: "on_match: no alert with default config",
expected_load_ok: true,
outofband_rules: []appsec_rule.CustomRule{
{
Name: "rule42",
Zones: []string{"ARGS"},
Variables: []string{"foo"},
Match: appsec_rule.Match{Type: "regex", Value: "^toto"},
Transform: []string{"lowercase"},
},
},
on_match: []appsec.Hook{},
input_request: appsec.ParsedRequest{
RemoteAddr: "1.2.3.4",
Method: "GET",
URI: "/urllll",
Args: url.Values{"foo": []string{"toto"}},
},
output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse, appsecResponse appsec.BodyResponse, statusCode int) {
require.Len(t, events, 1)
require.Equal(t, types.LOG, events[0].Type)
require.Len(t, responses, 1)
require.Equal(t, appsec.AllowRemediation, responses[0].Action)
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {

View file

@ -286,7 +286,6 @@ func (r *AppsecRunner) handleInBandInterrupt(request *appsec.ParsedRequest) {
r.outChan <- *appsecOvlfw
}
}
// Should the in band match trigger an event ?
if r.AppsecRuntime.Response.SendEvent {
r.outChan <- evt
@ -332,7 +331,9 @@ func (r *AppsecRunner) handleOutBandInterrupt(request *appsec.ParsedRequest) {
r.logger.Errorf("unable to generate appsec event : %s", err)
return
}
r.outChan <- *appsecOvlfw
if appsecOvlfw != nil {
r.outChan <- *appsecOvlfw
}
}
}
}

View file

@ -60,8 +60,8 @@ func AppsecEventGenerationGeoIPEnrich(src *models.Source) error {
}
func AppsecEventGeneration(inEvt types.Event, request *http.Request) (*types.Event, error) {
// if the request didnd't trigger inband rules, we don't want to generate an event to LAPI/CAPI
if !inEvt.Appsec.HasInBandMatches {
// if the request didn't trigger inband rules or out-of-band rules, we don't want to generate an event to LAPI/CAPI
if !inEvt.Appsec.HasInBandMatches && !inEvt.Appsec.HasOutBandMatches {
return nil, nil
}

View file

@ -302,7 +302,7 @@ func (lc *LokiClient) QueryRange(ctx context.Context, infinite bool) chan *LokiQ
// Create a wrapper for http.Get to be able to set headers and auth
func (lc *LokiClient) Get(ctx context.Context, url string) (*http.Response, error) {
request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, http.NoBody)
if err != nil {
return nil, err
}

View file

@ -370,7 +370,7 @@ func (lc *VLClient) QueryRange(ctx context.Context, infinite bool) chan *Log {
}
func (lc *VLClient) Get(ctx context.Context, url string) (*http.Response, error) {
request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, http.NoBody)
if err != nil {
return nil, err
}

View file

@ -45,7 +45,7 @@ func ValidateContextExpr(key string, expressions []string) error {
func NewAlertContext(contextToSend map[string][]string, valueLength int) error {
clog := log.New()
if err := types.ConfigureLogger(clog); err != nil {
if err := types.ConfigureLogger(clog, nil); err != nil {
return fmt.Errorf("couldn't create logger for alert context: %w", err)
}

View file

@ -183,7 +183,7 @@ func (s *DecisionsService) GetDecisionsFromBlocklist(ctx context.Context, blockl
client := http.Client{}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, *blocklist.URL, nil)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, *blocklist.URL, http.NoBody)
if err != nil {
return nil, false, err
}

View file

@ -854,6 +854,10 @@ func (a *apic) ApplyApicWhitelists(ctx context.Context, decisions []*models.Deci
log.Errorf("while getting allowlists content: %s", err)
}
if a.whitelists != nil {
log.Warn("capi_whitelists_path is deprecated, please use centralized allowlists instead. See https://docs.crowdsec.net/docs/next/local_api/centralized_allowlists.")
}
if (a.whitelists == nil || len(a.whitelists.Cidrs) == 0 && len(a.whitelists.Ips) == 0) && len(allowlisted_ips) == 0 && len(allowlisted_cidrs) == 0 {
return decisions
}

View file

@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"io"
"io/fs"
"net"
"net/http"
"os"
@ -118,14 +119,10 @@ func CustomRecoveryWithWriter() gin.HandlerFunc {
func newGinLogger(config *csconfig.LocalApiServerCfg) (*log.Logger, string, error) {
clog := log.New()
if err := types.ConfigureLogger(clog); err != nil {
if err := types.ConfigureLogger(clog, config.LogLevel); err != nil {
return nil, "", fmt.Errorf("while configuring gin logger: %w", err)
}
if config.LogLevel != nil {
clog.SetLevel(*config.LogLevel)
}
if config.LogMedia != "file" {
return clog, "", nil
}
@ -411,15 +408,11 @@ func (s *APIServer) Run(apiReady chan bool) error {
// it also updates the URL field with the actual address the server is listening on
// it's meant to be run in a separate goroutine
func (s *APIServer) listenAndServeLAPI(apiReady chan bool) error {
var (
tcpListener net.Listener
unixListener net.Listener
err error
serverError = make(chan error, 2)
listenerClosed = make(chan struct{})
)
serverError := make(chan error, 2)
startServer := func(listener net.Listener, canTLS bool) {
var err error
if canTLS && s.TLS != nil && (s.TLS.CertFilePath != "" || s.TLS.KeyFilePath != "") {
if s.TLS.KeyFilePath == "" {
serverError <- errors.New("missing TLS key file")
@ -445,38 +438,42 @@ func (s *APIServer) listenAndServeLAPI(apiReady chan bool) error {
}
// Starting TCP listener
go func() {
if s.URL == "" {
go func(url string) {
if url == "" {
return
}
tcpListener, err = net.Listen("tcp", s.URL)
listener, err := net.Listen("tcp", url)
if err != nil {
serverError <- fmt.Errorf("listening on %s: %w", s.URL, err)
serverError <- fmt.Errorf("listening on %s: %w", url, err)
return
}
log.Infof("CrowdSec Local API listening on %s", s.URL)
startServer(tcpListener, true)
}()
log.Infof("CrowdSec Local API listening on %s", url)
startServer(listener, true)
}(s.URL)
// Starting Unix socket listener
go func() {
if s.UnixSocket == "" {
go func(socket string) {
if socket == "" {
return
}
_ = os.RemoveAll(s.UnixSocket)
if err := os.Remove(socket); err != nil {
if !errors.Is(err, fs.ErrNotExist) {
log.Errorf("can't remove socket %s: %s", socket, err)
}
}
unixListener, err = net.Listen("unix", s.UnixSocket)
listener, err := net.Listen("unix", socket)
if err != nil {
serverError <- fmt.Errorf("while creating unix listener: %w", err)
return
}
log.Infof("CrowdSec Local API listening on Unix socket %s", s.UnixSocket)
startServer(unixListener, false)
}()
log.Infof("CrowdSec Local API listening on Unix socket %s", socket)
startServer(listener, false)
}(s.UnixSocket)
apiReady <- true
@ -493,10 +490,12 @@ func (s *APIServer) listenAndServeLAPI(apiReady chan bool) error {
log.Errorf("while shutting down http server: %v", err)
}
close(listenerClosed)
case <-listenerClosed:
if s.UnixSocket != "" {
_ = os.RemoveAll(s.UnixSocket)
if err := os.Remove(s.UnixSocket); err != nil {
if !errors.Is(err, fs.ErrNotExist) {
log.Errorf("can't remove socket %s: %s", s.UnixSocket, err)
}
}
}
}

View file

@ -337,7 +337,7 @@ func TestUnknownPath(t *testing.T) {
router, _ := NewAPITest(t, ctx)
w := httptest.NewRecorder()
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, "/test", nil)
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, "/test", http.NoBody)
req.Header.Set("User-Agent", UserAgent)
router.ServeHTTP(w, req)
@ -388,7 +388,7 @@ func TestLoggingDebugToFileConfig(t *testing.T) {
cfg.LogLevel = ptr.Of(log.DebugLevel)
// Configure logging
err := types.SetDefaultLoggerConfig(cfg.LogMedia, cfg.LogDir, *cfg.LogLevel, cfg.LogMaxSize, cfg.LogMaxFiles, cfg.LogMaxAge, cfg.LogFormat, cfg.CompressLogs, false)
err := types.SetDefaultLoggerConfig(cfg.LogMedia, cfg.LogDir, *cfg.LogLevel, cfg.LogMaxSize, cfg.LogMaxFiles, cfg.LogMaxAge, cfg.LogFormat, cfg.CompressLogs, false, false)
require.NoError(t, err)
api, err := NewServer(ctx, &cfg)
@ -396,7 +396,7 @@ func TestLoggingDebugToFileConfig(t *testing.T) {
require.NotNil(t, api)
w := httptest.NewRecorder()
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, "/test42", nil)
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, "/test42", http.NoBody)
req.Header.Set("User-Agent", UserAgent)
api.router.ServeHTTP(w, req)
assert.Equal(t, http.StatusNotFound, w.Code)
@ -440,7 +440,7 @@ func TestLoggingErrorToFileConfig(t *testing.T) {
cfg.LogLevel = ptr.Of(log.ErrorLevel)
// Configure logging
err := types.SetDefaultLoggerConfig(cfg.LogMedia, cfg.LogDir, *cfg.LogLevel, cfg.LogMaxSize, cfg.LogMaxFiles, cfg.LogMaxAge, cfg.LogFormat, cfg.CompressLogs, false)
err := types.SetDefaultLoggerConfig(cfg.LogMedia, cfg.LogDir, *cfg.LogLevel, cfg.LogMaxSize, cfg.LogMaxFiles, cfg.LogMaxAge, cfg.LogFormat, cfg.CompressLogs, false, false)
require.NoError(t, err)
api, err := NewServer(ctx, &cfg)
@ -448,7 +448,7 @@ func TestLoggingErrorToFileConfig(t *testing.T) {
require.NotNil(t, api)
w := httptest.NewRecorder()
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, "/test42", nil)
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, "/test42", http.NoBody)
req.Header.Set("User-Agent", UserAgent)
api.router.ServeHTTP(w, req)
assert.Equal(t, http.StatusNotFound, w.Code)

View file

@ -83,12 +83,10 @@ type PapiPermCheckSuccess struct {
func NewPAPI(apic *apic, dbClient *database.Client, consoleConfig *csconfig.ConsoleConfig, logLevel log.Level) (*Papi, error) {
logger := log.New()
if err := types.ConfigureLogger(logger); err != nil {
if err := types.ConfigureLogger(logger, &logLevel); err != nil {
return &Papi{}, fmt.Errorf("creating papi logger: %w", err)
}
logger.SetLevel(logLevel)
papiUrl := *apic.apiClient.PapiURL
papiUrl.Path = fmt.Sprintf("%s%s", types.PAPIVersion, types.PAPIPollUrl)
@ -160,7 +158,7 @@ func (p *Papi) GetPermissions(ctx context.Context) (PapiPermCheckSuccess, error)
httpClient := p.apiClient.GetClient()
papiCheckUrl := fmt.Sprintf("%s%s%s", p.URL, types.PAPIVersion, types.PAPIPermissionsUrl)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, papiCheckUrl, nil)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, papiCheckUrl, http.NoBody)
if err != nil {
return PapiPermCheckSuccess{}, fmt.Errorf("failed to create request: %w", err)
}

View file

@ -62,6 +62,8 @@ func (a *AppsecAllowlist) FetchAllowlists(ctx context.Context) error {
a.lock.Lock()
defer a.lock.Unlock()
prevIPsLen := len(a.ips)
prevRangesLen := len(a.ranges)
a.ranges = []rangeAllowlist{}
a.ips = []ipAllowlist{}
@ -93,7 +95,7 @@ func (a *AppsecAllowlist) FetchAllowlists(ctx context.Context) error {
}
}
if len(a.ips) != 0 || len(a.ranges) != 0 {
if (len(a.ips) != 0 || len(a.ranges) != 0) && (prevIPsLen != len(a.ips) || prevRangesLen != len(a.ranges)) {
a.logger.Infof("fetched %d IPs and %d ranges", len(a.ips), len(a.ranges))
}
a.logger.Debugf("fetched %d IPs and %d ranges", len(a.ips), len(a.ranges))

View file

@ -29,7 +29,7 @@ func TestJA4H_A(t *testing.T) {
{
name: "basic GET request - HTTP1.1 - no accept-language header",
request: func() *http.Request {
req, _ := http.NewRequest(http.MethodGet, "http://example.com", nil)
req, _ := http.NewRequest(http.MethodGet, "http://example.com", http.NoBody)
return req
},
expectedResult: "ge11nn000000",
@ -37,7 +37,7 @@ func TestJA4H_A(t *testing.T) {
{
name: "basic GET request - HTTP1.1 - with accept-language header",
request: func() *http.Request {
req, _ := http.NewRequest(http.MethodGet, "http://example.com", nil)
req, _ := http.NewRequest(http.MethodGet, "http://example.com", http.NoBody)
req.Header.Set("Accept-Language", "en-US")
return req
},
@ -46,7 +46,7 @@ func TestJA4H_A(t *testing.T) {
{
name: "basic POST request - HTTP1.1 - no accept-language header - cookies - referer",
request: func() *http.Request {
req, _ := http.NewRequest(http.MethodPost, "http://example.com", nil)
req, _ := http.NewRequest(http.MethodPost, "http://example.com", http.NoBody)
req.AddCookie(&http.Cookie{Name: "foo", Value: "bar"})
req.Header.Set("Referer", "http://example.com")
return req
@ -56,7 +56,7 @@ func TestJA4H_A(t *testing.T) {
{
name: "bad accept-language header",
request: func() *http.Request {
req, _ := http.NewRequest(http.MethodGet, "http://example.com", nil)
req, _ := http.NewRequest(http.MethodGet, "http://example.com", http.NoBody)
req.Header.Set("Accept-Language", "aksjdhaslkdhalkjsd")
return req
},
@ -65,7 +65,7 @@ func TestJA4H_A(t *testing.T) {
{
name: "bad accept-language header 2",
request: func() *http.Request {
req, _ := http.NewRequest(http.MethodGet, "http://example.com", nil)
req, _ := http.NewRequest(http.MethodGet, "http://example.com", http.NoBody)
req.Header.Set("Accept-Language", ",")
return req
},
@ -94,7 +94,7 @@ func TestJA4H_B(t *testing.T) {
{
name: "no headers",
request: func() *http.Request {
req, _ := http.NewRequest(http.MethodGet, "http://example.com", nil)
req, _ := http.NewRequest(http.MethodGet, "http://example.com", http.NoBody)
return req
},
expectedResult: "e3b0c44298fc",
@ -102,7 +102,7 @@ func TestJA4H_B(t *testing.T) {
{
name: "header with arbitrary content",
request: func() *http.Request {
req, _ := http.NewRequest(http.MethodGet, "http://example.com", nil)
req, _ := http.NewRequest(http.MethodGet, "http://example.com", http.NoBody)
req.Header.Set("X-Custom-Header", "some value")
return req
},
@ -111,7 +111,7 @@ func TestJA4H_B(t *testing.T) {
{
name: "header with multiple headers",
request: func() *http.Request {
req, _ := http.NewRequest(http.MethodGet, "http://example.com", nil)
req, _ := http.NewRequest(http.MethodGet, "http://example.com", http.NoBody)
req.Header.Set("X-Custom-Header", "some value")
req.Header.Set("Authorization", "Bearer token")
return req
@ -121,7 +121,7 @@ func TestJA4H_B(t *testing.T) {
{
name: "curl-like request",
request: func() *http.Request {
req, _ := http.NewRequest(http.MethodGet, "http://localhost", nil)
req, _ := http.NewRequest(http.MethodGet, "http://localhost", http.NoBody)
req.Header.Set("Host", "localhost")
req.Header.Set("User-Agent", "curl/8.12.1")
req.Header.Set("Accept", "*/*")
@ -150,7 +150,7 @@ func TestJA4H_C(t *testing.T) {
{
name: "no cookies",
cookies: func() []*http.Cookie {
req, _ := http.NewRequest(http.MethodGet, "http://example.com", nil)
req, _ := http.NewRequest(http.MethodGet, "http://example.com", http.NoBody)
return req.Cookies()
},
expectedResult: "000000000000",
@ -158,7 +158,7 @@ func TestJA4H_C(t *testing.T) {
{
name: "one cookie",
cookies: func() []*http.Cookie {
req, _ := http.NewRequest(http.MethodGet, "http://example.com", nil)
req, _ := http.NewRequest(http.MethodGet, "http://example.com", http.NoBody)
req.AddCookie(&http.Cookie{Name: "foo", Value: "bar"})
return req.Cookies()
},
@ -167,7 +167,7 @@ func TestJA4H_C(t *testing.T) {
{
name: "duplicate cookies",
cookies: func() []*http.Cookie {
req, _ := http.NewRequest(http.MethodGet, "http://example.com", nil)
req, _ := http.NewRequest(http.MethodGet, "http://example.com", http.NoBody)
req.AddCookie(&http.Cookie{Name: "foo", Value: "bar"})
req.AddCookie(&http.Cookie{Name: "foo", Value: "bar2"})
return req.Cookies()
@ -177,7 +177,7 @@ func TestJA4H_C(t *testing.T) {
{
name: "multiple cookies",
cookies: func() []*http.Cookie {
req, _ := http.NewRequest(http.MethodGet, "http://example.com", nil)
req, _ := http.NewRequest(http.MethodGet, "http://example.com", http.NoBody)
req.AddCookie(&http.Cookie{Name: "foo", Value: "bar"})
req.AddCookie(&http.Cookie{Name: "bar", Value: "foo"})
cookies := req.Cookies()
@ -209,7 +209,7 @@ func TestJA4H_D(t *testing.T) {
{
name: "no cookies",
cookies: func() []*http.Cookie {
req, _ := http.NewRequest(http.MethodGet, "http://example.com", nil)
req, _ := http.NewRequest(http.MethodGet, "http://example.com", http.NoBody)
return req.Cookies()
},
expectedResult: "000000000000",
@ -217,7 +217,7 @@ func TestJA4H_D(t *testing.T) {
{
name: "one cookie",
cookies: func() []*http.Cookie {
req, _ := http.NewRequest(http.MethodGet, "http://example.com", nil)
req, _ := http.NewRequest(http.MethodGet, "http://example.com", http.NoBody)
req.AddCookie(&http.Cookie{Name: "foo", Value: "bar"})
return req.Cookies()
},
@ -226,7 +226,7 @@ func TestJA4H_D(t *testing.T) {
{
name: "duplicate cookies",
cookies: func() []*http.Cookie {
req, _ := http.NewRequest(http.MethodGet, "http://example.com", nil)
req, _ := http.NewRequest(http.MethodGet, "http://example.com", http.NoBody)
req.AddCookie(&http.Cookie{Name: "foo", Value: "bar"})
req.AddCookie(&http.Cookie{Name: "foo", Value: "bar2"})
return req.Cookies()
@ -236,7 +236,7 @@ func TestJA4H_D(t *testing.T) {
{
name: "multiple cookies",
cookies: func() []*http.Cookie {
req, _ := http.NewRequest(http.MethodGet, "http://example.com", nil)
req, _ := http.NewRequest(http.MethodGet, "http://example.com", http.NoBody)
req.AddCookie(&http.Cookie{Name: "foo", Value: "bar"})
req.AddCookie(&http.Cookie{Name: "bar", Value: "foo"})
cookies := req.Cookies()
@ -268,7 +268,7 @@ func TestJA4H(t *testing.T) {
{
name: "Basic GET - No cookies",
req: func() *http.Request {
req, _ := http.NewRequest(http.MethodGet, "http://example.com", nil)
req, _ := http.NewRequest(http.MethodGet, "http://example.com", http.NoBody)
return req
},
expectedHash: "ge11nn000000_e3b0c44298fc_000000000000_000000000000",
@ -276,7 +276,7 @@ func TestJA4H(t *testing.T) {
{
name: "Basic GET - With cookies",
req: func() *http.Request {
req, _ := http.NewRequest(http.MethodGet, "http://example.com", nil)
req, _ := http.NewRequest(http.MethodGet, "http://example.com", http.NoBody)
req.AddCookie(&http.Cookie{Name: "session", Value: "12345"})
return req
},
@ -285,7 +285,7 @@ func TestJA4H(t *testing.T) {
{
name: "Basic GET - Multiple cookies",
req: func() *http.Request {
req, _ := http.NewRequest(http.MethodGet, "http://example.com", nil)
req, _ := http.NewRequest(http.MethodGet, "http://example.com", http.NoBody)
req.AddCookie(&http.Cookie{Name: "foo", Value: "bar"})
req.AddCookie(&http.Cookie{Name: "baz", Value: "qux"})
return req

View file

@ -1,78 +0,0 @@
package appsec
// This file is mostly stolen from net/url package, but with some modifications to allow less strict parsing of query strings
import (
"net/url"
"strings"
)
// parseQuery and parseQuery are copied net/url package, but allow semicolon in values
func ParseQuery(query string) url.Values {
m := make(url.Values)
parseQuery(m, query)
return m
}
func parseQuery(m url.Values, query string) {
for query != "" {
var key string
key, query, _ = strings.Cut(query, "&")
if key == "" {
continue
}
key, value, _ := strings.Cut(key, "=")
//for now we'll just ignore the errors, but ideally we want to fire some "internal" rules when we see invalid query strings
key = unescape(key)
value = unescape(value)
m[key] = append(m[key], value)
}
}
func hexDigitToByte(digit byte) (byte, bool) {
switch {
case digit >= '0' && digit <= '9':
return digit - '0', true
case digit >= 'a' && digit <= 'f':
return digit - 'a' + 10, true
case digit >= 'A' && digit <= 'F':
return digit - 'A' + 10, true
default:
return 0, false
}
}
func unescape(input string) string {
ilen := len(input)
res := strings.Builder{}
res.Grow(ilen)
for i := 0; i < ilen; i++ {
ci := input[i]
if ci == '+' {
res.WriteByte(' ')
continue
}
if ci == '%' {
if i+2 >= ilen {
res.WriteByte(ci)
continue
}
hi, ok := hexDigitToByte(input[i+1])
if !ok {
res.WriteByte(ci)
continue
}
lo, ok := hexDigitToByte(input[i+2])
if !ok {
res.WriteByte(ci)
continue
}
res.WriteByte(hi<<4 | lo)
i += 2
continue
}
res.WriteByte(ci)
}
return res.String()
}

View file

@ -1,207 +0,0 @@
package appsec
import (
"net/url"
"reflect"
"testing"
)
func TestParseQuery(t *testing.T) {
tests := []struct {
name string
query string
expected url.Values
}{
{
name: "Simple query",
query: "foo=bar",
expected: url.Values{
"foo": []string{"bar"},
},
},
{
name: "Multiple values",
query: "foo=bar&foo=baz",
expected: url.Values{
"foo": []string{"bar", "baz"},
},
},
{
name: "Empty value",
query: "foo=",
expected: url.Values{
"foo": []string{""},
},
},
{
name: "Empty key",
query: "=bar",
expected: url.Values{
"": []string{"bar"},
},
},
{
name: "Empty query",
query: "",
expected: url.Values{},
},
{
name: "Multiple keys",
query: "foo=bar&baz=qux",
expected: url.Values{
"foo": []string{"bar"},
"baz": []string{"qux"},
},
},
{
name: "Multiple keys with empty value",
query: "foo=bar&baz=qux&quux=",
expected: url.Values{
"foo": []string{"bar"},
"baz": []string{"qux"},
"quux": []string{""},
},
},
{
name: "Multiple keys with empty value and empty key",
query: "foo=bar&baz=qux&quux=&=quuz",
expected: url.Values{
"foo": []string{"bar"},
"baz": []string{"qux"},
"quux": []string{""},
"": []string{"quuz"},
},
},
{
name: "Multiple keys with empty value and empty key and multiple values",
query: "foo=bar&baz=qux&quux=&=quuz&foo=baz",
expected: url.Values{
"foo": []string{"bar", "baz"},
"baz": []string{"qux"},
"quux": []string{""},
"": []string{"quuz"},
},
},
{
name: "Multiple keys with empty value and empty key and multiple values and escaped characters",
query: "foo=bar&baz=qux&quux=&=quuz&foo=baz&foo=bar%20baz",
expected: url.Values{
"foo": []string{"bar", "baz", "bar baz"},
"baz": []string{"qux"},
"quux": []string{""},
"": []string{"quuz"},
},
},
{
name: "Multiple keys with empty value and empty key and multiple values and escaped characters and semicolon",
query: "foo=bar&baz=qux&quux=&=quuz&foo=baz&foo=bar%20baz&foo=bar%3Bbaz",
expected: url.Values{
"foo": []string{"bar", "baz", "bar baz", "bar;baz"},
"baz": []string{"qux"},
"quux": []string{""},
"": []string{"quuz"},
},
},
{
name: "Multiple keys with empty value and empty key and multiple values and escaped characters and semicolon and ampersand",
query: "foo=bar&baz=qux&quux=&=quuz&foo=baz&foo=bar%20baz&foo=bar%3Bbaz&foo=bar%26baz",
expected: url.Values{
"foo": []string{"bar", "baz", "bar baz", "bar;baz", "bar&baz"},
"baz": []string{"qux"},
"quux": []string{""},
"": []string{"quuz"},
},
},
{
name: "Multiple keys with empty value and empty key and multiple values and escaped characters and semicolon and ampersand and equals",
query: "foo=bar&baz=qux&quux=&=quuz&foo=baz&foo=bar%20baz&foo=bar%3Bbaz&foo=bar%26baz&foo=bar%3Dbaz",
expected: url.Values{
"foo": []string{"bar", "baz", "bar baz", "bar;baz", "bar&baz", "bar=baz"},
"baz": []string{"qux"},
"quux": []string{""},
"": []string{"quuz"},
},
},
{
name: "Multiple keys with empty value and empty key and multiple values and escaped characters and semicolon and ampersand and equals and question mark",
query: "foo=bar&baz=qux&quux=&=quuz&foo=baz&foo=bar%20baz&foo=bar%3Bbaz&foo=bar%26baz&foo=bar%3Dbaz&foo=bar%3Fbaz",
expected: url.Values{
"foo": []string{"bar", "baz", "bar baz", "bar;baz", "bar&baz", "bar=baz", "bar?baz"},
"baz": []string{"qux"},
"quux": []string{""},
"": []string{"quuz"},
},
},
{
name: "keys with escaped characters",
query: "foo=ba;r&baz=qu;;x&quux=x\\&ww&xx=qu?uz&",
expected: url.Values{
"foo": []string{"ba;r"},
"baz": []string{"qu;;x"},
"quux": []string{"x\\"},
"ww": []string{""},
"xx": []string{"qu?uz"},
},
},
{
name: "hexadecimal characters",
query: "foo=bar%20baz",
expected: url.Values{
"foo": []string{"bar baz"},
},
},
{
name: "hexadecimal characters upper and lower case",
query: "foo=Ba%42%42&bar=w%2f%2F",
expected: url.Values{
"foo": []string{"BaBB"},
"bar": []string{"w//"},
},
},
{
name: "hexadecimal characters with invalid characters",
query: "foo=bar%20baz%2",
expected: url.Values{
"foo": []string{"bar baz%2"},
},
},
{
name: "hexadecimal characters with invalid hex characters",
query: "foo=bar%xx",
expected: url.Values{
"foo": []string{"bar%xx"},
},
},
{
name: "hexadecimal characters with invalid 2nd hex character",
query: "foo=bar%2x",
expected: url.Values{
"foo": []string{"bar%2x"},
},
},
{
name: "url +",
query: "foo=bar+x",
expected: url.Values{
"foo": []string{"bar x"},
},
},
{
name: "url &&",
query: "foo=bar&&lol=bur",
expected: url.Values{
"foo": []string{"bar"},
"lol": []string{"bur"},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
res := ParseQuery(test.query)
if !reflect.DeepEqual(res, test.expected) {
t.Fatalf("unexpected result: %v", res)
}
})
}
}

View file

@ -11,6 +11,7 @@ import (
"os"
"regexp"
"github.com/crowdsecurity/crowdsec/pkg/exprhelpers"
"github.com/google/uuid"
log "github.com/sirupsen/logrus"
)
@ -332,7 +333,7 @@ func NewParsedRequestFromRequest(r *http.Request, logger *log.Entry) (ParsedRequ
} else {
r.Proto = "HTTP/" + string(major) + "." + string(minor)
}
} else {
} else if httpVersion != "" {
logger.Warnf("Invalid value %s for HTTP version header", httpVersion)
}
@ -396,7 +397,7 @@ func NewParsedRequestFromRequest(r *http.Request, logger *log.Entry) (ParsedRequ
URL: parsedURL,
Proto: r.Proto,
Body: body,
Args: ParseQuery(parsedURL.RawQuery),
Args: exprhelpers.ParseQuery(parsedURL.RawQuery),
TransferEncoding: r.TransferEncoding,
ResponseChannel: make(chan AppsecTempResponse),
RemoteAddrNormalized: remoteAddrNormalized,

3
pkg/cache/cache.go vendored
View file

@ -59,11 +59,10 @@ func CacheInit(cfg CacheCfg) error {
clog := log.New()
if err := types.ConfigureLogger(clog); err != nil {
if err := types.ConfigureLogger(clog, cfg.LogLevel); err != nil {
return fmt.Errorf("while creating cache logger: %w", err)
}
clog.SetLevel(*cfg.LogLevel)
cfg.Logger = clog.WithField("cache", cfg.Name)
tmpCache := gcache.New(cfg.Size)

View file

@ -473,6 +473,8 @@ func (c *LocalApiServerCfg) LoadCapiWhitelists() error {
return nil
}
log.Warn("capi_whitelists_path is deprecated, please use centralized allowlists instead. See https://docs.crowdsec.net/docs/next/local_api/centralized_allowlists.")
fd, err := os.Open(c.CapiWhitelistsPath)
if err != nil {
return fmt.Errorf("while opening capi whitelist file: %w", err)

View file

@ -26,7 +26,10 @@ type DatabaseCfg struct {
User string `yaml:"user"`
Password string `yaml:"password"`
DbName string `yaml:"db_name"`
Sslmode string `yaml:"sslmode"`
SSLMode string `yaml:"sslmode"`
SSLCACert string `yaml:"ssl_ca_cert"`
SSLClientCert string `yaml:"ssl_client_cert"`
SSLClientKey string `yaml:"ssl_client_key"`
Host string `yaml:"host"`
Port int `yaml:"port"`
DbPath string `yaml:"db_path"`
@ -136,14 +139,34 @@ func (d *DatabaseCfg) ConnectionString() string {
connString = fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?parseTime=True", d.User, d.Password, d.Host, d.Port, d.DbName)
}
if d.Sslmode != "" {
connString = fmt.Sprintf("%s&tls=%s", connString, d.Sslmode)
if d.SSLMode != "" {
connString = fmt.Sprintf("%s&tls=%s", connString, d.SSLMode)
}
if d.SSLCACert != "" {
connString = fmt.Sprintf("%s&tls-ca=%s", connString, d.SSLCACert)
}
if d.SSLClientCert != "" && d.SSLClientKey != "" {
connString = fmt.Sprintf("%s&tls-cert=%s&tls-key=%s", connString, d.SSLClientCert, d.SSLClientKey)
}
case "postgres", "postgresql", "pgx":
if d.isSocketConfig() {
connString = fmt.Sprintf("host=%s user=%s dbname=%s password=%s", d.DbPath, d.User, d.DbName, d.Password)
} else {
connString = fmt.Sprintf("host=%s port=%d user=%s dbname=%s password=%s sslmode=%s", d.Host, d.Port, d.User, d.DbName, d.Password, d.Sslmode)
connString = fmt.Sprintf("host=%s port=%d user=%s dbname=%s password=%s", d.Host, d.Port, d.User, d.DbName, d.Password)
}
if d.SSLMode != "" {
connString = fmt.Sprintf("%s sslmode=%s", connString, d.SSLMode)
}
if d.SSLCACert != "" {
connString = fmt.Sprintf("%s sslrootcert=%s", connString, d.SSLCACert)
}
if d.SSLClientCert != "" && d.SSLClientKey != "" {
connString = fmt.Sprintf("%s sslcert=%s sslkey=%s", connString, d.SSLClientCert, d.SSLClientKey)
}
}

View file

@ -20,6 +20,7 @@ import (
"gopkg.in/yaml.v2"
"github.com/crowdsecurity/go-cs-lib/csstring"
"github.com/crowdsecurity/go-cs-lib/ptr"
"github.com/crowdsecurity/go-cs-lib/slicetools"
"github.com/crowdsecurity/crowdsec/pkg/csconfig"
@ -321,13 +322,12 @@ func (pb *PluginBroker) loadNotificationPlugin(name string, binaryPath string) (
pb.pluginMap[name] = &NotifierPlugin{}
l := log.New()
err = types.ConfigureLogger(l)
err = types.ConfigureLogger(l, ptr.Of(log.TraceLevel))
if err != nil {
return nil, err
}
// We set the highest level to permit plugins to set their own log level
// without that, crowdsec log level is controlling plugins level
l.SetLevel(log.TraceLevel)
logger := NewHCLogAdapter(l, "")
c := plugin.NewClient(&plugin.ClientConfig{
HandshakeConfig: handshake,

View file

@ -12,6 +12,7 @@ import (
"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 {
@ -34,11 +35,10 @@ func NewProfile(profilesCfg []*csconfig.ProfileCfg) ([]*Runtime, error) {
runtime := &Runtime{}
xlog := log.New()
if err := types.ConfigureLogger(xlog); err != nil {
if err := types.ConfigureLogger(xlog, ptr.Of(log.InfoLevel)); err != nil {
return nil, fmt.Errorf("while configuring profiles-specific logger: %w", err)
}
xlog.SetLevel(log.InfoLevel)
runtime.Logger = xlog.WithFields(log.Fields{
"type": "profile",
"name": profile.Name,

View file

@ -42,8 +42,7 @@ func (c *CrowdsecCTIClient) doRequest(ctx context.Context, method string, endpoi
url += fmt.Sprintf("%s=%s&", k, v)
}
}
req, err := http.NewRequestWithContext(ctx, method, url, nil)
req, err := http.NewRequestWithContext(ctx, method, url, http.NoBody)
if err != nil {
return nil, err
}

View file

@ -52,14 +52,10 @@ func NewClient(ctx context.Context, config *csconfig.DatabaseCfg) (*Client, erro
}
/*The logger that will be used by db operations*/
clog := log.New()
if err := types.ConfigureLogger(clog); err != nil {
if err := types.ConfigureLogger(clog, config.LogLevel); err != nil {
return nil, fmt.Errorf("while configuring db logger: %w", err)
}
if config.LogLevel != nil {
clog.SetLevel(*config.LogLevel)
}
entLogger := clog.WithField("context", "ent")
entOpt := ent.Log(entLogger.Debug)

View file

@ -44,12 +44,9 @@ func InitCrowdsecCTI(key *string, ttl *time.Duration, size *int, logLevel *log.L
*ttl = 5 * time.Minute
}
clog := log.New()
if err := types.ConfigureLogger(clog); err != nil {
if err := types.ConfigureLogger(clog, logLevel); err != nil {
return fmt.Errorf("while configuring datasource logger: %w", err)
}
if logLevel != nil {
clog.SetLevel(*logLevel)
}
subLogger := clog.WithField("type", "crowdsec-cti")
CrowdsecCTIInitCache(*size, *ttl)
ctiClient = cticlient.NewCrowdsecCTIClient(cticlient.WithAPIKey(CTIApiKey), cticlient.WithLogger(subLogger))

View file

@ -3,6 +3,7 @@ package exprhelpers
import (
"net"
"net/http"
"net/url"
"time"
"github.com/oschwald/geoip2-golang"
@ -151,6 +152,20 @@ var exprFuncs = []exprCustomFunc{
new(func(string) map[string][]string),
},
},
{
name: "ParseQuery",
function: ExprWrapParseQuery,
signature: []interface{}{
new(func(string) url.Values),
},
},
{
name: "ExtractQueryParam",
function: ExprWrapExtractQueryParam,
signature: []interface{}{
new(func(string, string) []string),
},
},
{
name: "PathUnescape",
function: PathUnescape,

View file

@ -2,6 +2,7 @@ package exprhelpers
import (
"errors"
"net/url"
"testing"
"time"
@ -160,6 +161,68 @@ func TestMatch(t *testing.T) {
}
}
// just to verify that the function is available, real tests are in TestExtractQueryParam
func TestExtractQueryParamExpr(t *testing.T) {
err := Init(nil)
require.NoError(t, err)
tests := []struct {
name string
env map[string]interface{}
code string
result []string
err string
}{
{
name: "ExtractQueryParam() test: basic test",
env: map[string]interface{}{
"query": "/foo?a=1&b=2",
},
code: "ExtractQueryParam(query, 'a')",
result: []string{"1"},
},
}
for _, test := range tests {
program, err := expr.Compile(test.code, GetExprOptions(test.env)...)
require.NoError(t, err)
output, err := expr.Run(program, test.env)
require.NoError(t, err)
require.Equal(t, test.result, output)
log.Printf("test '%s' : OK", test.name)
}
}
// just to verify that the function is available, real tests are in TestParseQuery
func TestParseQueryInExpr(t *testing.T) {
err := Init(nil)
require.NoError(t, err)
tests := []struct {
name string
env map[string]interface{}
code string
result url.Values
err string
}{
{
name: "ParseQuery() test: basic test",
env: map[string]interface{}{
"query": "a=1&b=2",
"ParseQuery": ParseQuery,
},
code: "ParseQuery(query)",
result: url.Values{"a": {"1"}, "b": {"2"}},
},
}
for _, test := range tests {
program, err := expr.Compile(test.code, GetExprOptions(test.env)...)
require.NoError(t, err)
output, err := expr.Run(program, test.env)
require.NoError(t, err)
require.Equal(t, test.result, output)
log.Printf("test '%s' : OK", test.name)
}
}
func TestDistanceHelper(t *testing.T) {
err := Init(nil)
require.NoError(t, err)

View file

@ -2,6 +2,8 @@ package exprhelpers
import (
"net/http"
"net/url"
"strings"
"github.com/crowdsecurity/crowdsec/pkg/appsec/ja4h"
)
@ -11,3 +13,112 @@ func JA4H(params ...any) (any, error) {
req := params[0].(*http.Request)
return ja4h.JA4H(req), nil
}
// just a expr wrapper for ParseQuery
func ExprWrapParseQuery(params ...any) (any, error) {
query := params[0].(string)
return ParseQuery(query), nil
}
// parseQuery and parseQuery are copied net/url package, but allow semicolon in values
func ParseQuery(query string) url.Values {
m := make(url.Values)
ParseQueryIntoValues(m, query)
return m
}
func ParseQueryIntoValues(m url.Values, query string) {
for query != "" {
var key string
key, query, _ = strings.Cut(query, "&")
if key == "" {
continue
}
key, value, _ := strings.Cut(key, "=")
//for now we'll just ignore the errors, but ideally we want to fire some "internal" rules when we see invalid query strings
key = unescape(key)
value = unescape(value)
m[key] = append(m[key], value)
}
}
func hexDigitToByte(digit byte) (byte, bool) {
switch {
case digit >= '0' && digit <= '9':
return digit - '0', true
case digit >= 'a' && digit <= 'f':
return digit - 'a' + 10, true
case digit >= 'A' && digit <= 'F':
return digit - 'A' + 10, true
default:
return 0, false
}
}
func unescape(input string) string {
ilen := len(input)
res := strings.Builder{}
res.Grow(ilen)
for i := 0; i < ilen; i++ {
ci := input[i]
if ci == '+' {
res.WriteByte(' ')
continue
}
if ci == '%' {
if i+2 >= ilen {
res.WriteByte(ci)
continue
}
hi, ok := hexDigitToByte(input[i+1])
if !ok {
res.WriteByte(ci)
continue
}
lo, ok := hexDigitToByte(input[i+2])
if !ok {
res.WriteByte(ci)
continue
}
res.WriteByte(hi<<4 | lo)
i += 2
continue
}
res.WriteByte(ci)
}
return res.String()
}
// just a expr wrapper for ExtractQueryParam
func ExprWrapExtractQueryParam(params ...any) (any, error) {
uri := params[0].(string)
param := params[1].(string)
return ExtractQueryParam(uri, param), nil
}
// ExtractQueryParam extracts values for a given query parameter from a raw URI string.
func ExtractQueryParam(uri, param string) []string {
// Find the first occurrence of "?"
idx := strings.Index(uri, "?")
if idx == -1 {
// No query string present
return []string{}
}
// Extract the query string part
queryString := uri[idx+1:]
// Parse the query string using a function that supports both `&` and `;`
values := ParseQuery(queryString)
if values == nil {
// No query string present
return []string{}
}
// Retrieve the values for the specified parameter
if _, ok := values[param]; !ok {
return []string{}
}
return values[param]
}

View file

@ -2,11 +2,275 @@ package exprhelpers
import (
"net/http"
"net/url"
"reflect"
"testing"
"github.com/stretchr/testify/require"
)
func TestParseQuery(t *testing.T) {
tests := []struct {
name string
query string
expected url.Values
}{
{
name: "Full URI",
query: "/foobar/toto?ab=cd&ef=gh",
expected: url.Values{
"/foobar/toto?ab": []string{"cd"},
"ef": []string{"gh"},
},
},
{
name: "Simple query",
query: "foo=bar",
expected: url.Values{
"foo": []string{"bar"},
},
},
{
name: "Multiple values",
query: "foo=bar&foo=baz",
expected: url.Values{
"foo": []string{"bar", "baz"},
},
},
{
name: "Empty value",
query: "foo=",
expected: url.Values{
"foo": []string{""},
},
},
{
name: "Empty key",
query: "=bar",
expected: url.Values{
"": []string{"bar"},
},
},
{
name: "Empty query",
query: "",
expected: url.Values{},
},
{
name: "Multiple keys",
query: "foo=bar&baz=qux",
expected: url.Values{
"foo": []string{"bar"},
"baz": []string{"qux"},
},
},
{
name: "Multiple keys with empty value",
query: "foo=bar&baz=qux&quux=",
expected: url.Values{
"foo": []string{"bar"},
"baz": []string{"qux"},
"quux": []string{""},
},
},
{
name: "Multiple keys with empty value and empty key",
query: "foo=bar&baz=qux&quux=&=quuz",
expected: url.Values{
"foo": []string{"bar"},
"baz": []string{"qux"},
"quux": []string{""},
"": []string{"quuz"},
},
},
{
name: "Multiple keys with empty value and empty key and multiple values",
query: "foo=bar&baz=qux&quux=&=quuz&foo=baz",
expected: url.Values{
"foo": []string{"bar", "baz"},
"baz": []string{"qux"},
"quux": []string{""},
"": []string{"quuz"},
},
},
{
name: "Multiple keys with empty value and empty key and multiple values and escaped characters",
query: "foo=bar&baz=qux&quux=&=quuz&foo=baz&foo=bar%20baz",
expected: url.Values{
"foo": []string{"bar", "baz", "bar baz"},
"baz": []string{"qux"},
"quux": []string{""},
"": []string{"quuz"},
},
},
{
name: "Multiple keys with empty value and empty key and multiple values and escaped characters and semicolon",
query: "foo=bar&baz=qux&quux=&=quuz&foo=baz&foo=bar%20baz&foo=bar%3Bbaz",
expected: url.Values{
"foo": []string{"bar", "baz", "bar baz", "bar;baz"},
"baz": []string{"qux"},
"quux": []string{""},
"": []string{"quuz"},
},
},
{
name: "Multiple keys with empty value and empty key and multiple values and escaped characters and semicolon and ampersand",
query: "foo=bar&baz=qux&quux=&=quuz&foo=baz&foo=bar%20baz&foo=bar%3Bbaz&foo=bar%26baz",
expected: url.Values{
"foo": []string{"bar", "baz", "bar baz", "bar;baz", "bar&baz"},
"baz": []string{"qux"},
"quux": []string{""},
"": []string{"quuz"},
},
},
{
name: "Multiple keys with empty value and empty key and multiple values and escaped characters and semicolon and ampersand and equals",
query: "foo=bar&baz=qux&quux=&=quuz&foo=baz&foo=bar%20baz&foo=bar%3Bbaz&foo=bar%26baz&foo=bar%3Dbaz",
expected: url.Values{
"foo": []string{"bar", "baz", "bar baz", "bar;baz", "bar&baz", "bar=baz"},
"baz": []string{"qux"},
"quux": []string{""},
"": []string{"quuz"},
},
},
{
name: "Multiple keys with empty value and empty key and multiple values and escaped characters and semicolon and ampersand and equals and question mark",
query: "foo=bar&baz=qux&quux=&=quuz&foo=baz&foo=bar%20baz&foo=bar%3Bbaz&foo=bar%26baz&foo=bar%3Dbaz&foo=bar%3Fbaz",
expected: url.Values{
"foo": []string{"bar", "baz", "bar baz", "bar;baz", "bar&baz", "bar=baz", "bar?baz"},
"baz": []string{"qux"},
"quux": []string{""},
"": []string{"quuz"},
},
},
{
name: "keys with escaped characters",
query: "foo=ba;r&baz=qu;;x&quux=x\\&ww&xx=qu?uz&",
expected: url.Values{
"foo": []string{"ba;r"},
"baz": []string{"qu;;x"},
"quux": []string{"x\\"},
"ww": []string{""},
"xx": []string{"qu?uz"},
},
},
{
name: "hexadecimal characters",
query: "foo=bar%20baz",
expected: url.Values{
"foo": []string{"bar baz"},
},
},
{
name: "hexadecimal characters upper and lower case",
query: "foo=Ba%42%42&bar=w%2f%2F",
expected: url.Values{
"foo": []string{"BaBB"},
"bar": []string{"w//"},
},
},
{
name: "hexadecimal characters with invalid characters",
query: "foo=bar%20baz%2",
expected: url.Values{
"foo": []string{"bar baz%2"},
},
},
{
name: "hexadecimal characters with invalid hex characters",
query: "foo=bar%xx",
expected: url.Values{
"foo": []string{"bar%xx"},
},
},
{
name: "hexadecimal characters with invalid 2nd hex character",
query: "foo=bar%2x",
expected: url.Values{
"foo": []string{"bar%2x"},
},
},
{
name: "url +",
query: "foo=bar+x",
expected: url.Values{
"foo": []string{"bar x"},
},
},
{
name: "url &&",
query: "foo=bar&&lol=bur",
expected: url.Values{
"foo": []string{"bar"},
"lol": []string{"bur"},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
res := ParseQuery(test.query)
if !reflect.DeepEqual(res, test.expected) {
t.Fatalf("unexpected result: %v", res)
}
})
}
}
func TestExtractQueryParam(t *testing.T) {
tests := []struct {
name string
query string
param string
expected []string
}{
{
name: "Simple uri",
query: "/foobar/toto?ab=cd&ef=gh",
param: "ab",
expected: []string{"cd"},
},
{
name: "Simple uri, repeating param",
query: "/foobar?foo=bar&foo=baz",
param: "foo",
expected: []string{"bar", "baz"},
},
{
name: "Simple uri with semicolon",
query: "/foobar/toto?ab=cd;ef=gh",
param: "ab",
expected: []string{"cd;ef=gh"},
},
{
name: "Simple query no uri",
query: "foo=bar",
param: "foo",
expected: []string{},
},
{
name: "No QS",
query: "/foobar",
param: "foo",
expected: []string{},
},
{
name: "missing param",
query: "/foobar/toto?ab=cd&ef=gh",
param: "baz",
expected: []string{},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
res := ExtractQueryParam(test.query, test.param)
if !reflect.DeepEqual(res, test.expected) {
t.Fatalf("unexpected result: %v", res)
}
})
}
}
func TestJA4H(t *testing.T) {
tests := []struct {
@ -50,7 +314,7 @@ func TestJA4H(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
req, err := http.NewRequest(test.method, test.url, nil)
req, err := http.NewRequest(test.method, test.url, http.NoBody)
if err != nil {
t.Fatalf("Failed to create request: %s", err)
}

View file

@ -24,6 +24,7 @@ import (
"github.com/crowdsecurity/crowdsec/pkg/cwversion/constraint"
"github.com/crowdsecurity/crowdsec/pkg/exprhelpers"
"github.com/crowdsecurity/crowdsec/pkg/types"
"github.com/crowdsecurity/go-cs-lib/ptr"
)
// BucketFactory struct holds all fields for any bucket configuration. This is to have a
@ -345,11 +346,10 @@ func LoadBucket(bucketFactory *BucketFactory, tomb *tomb.Tomb) error {
if bucketFactory.Debug {
clog := log.New()
if err = types.ConfigureLogger(clog); err != nil {
if err = types.ConfigureLogger(clog, ptr.Of(log.DebugLevel)); err != nil {
return fmt.Errorf("while creating bucket-specific logger: %w", err)
}
clog.SetLevel(log.DebugLevel)
bucketFactory.logger = clog.WithFields(log.Fields{
"cfg": bucketFactory.BucketName,
"name": bucketFactory.Name,

View file

@ -60,7 +60,7 @@ func (c *LongPollClient) doQuery(ctx context.Context) (*http.Response, error) {
logger.Debugf("Query parameters: %s", c.url.RawQuery)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.url.String(), nil)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.url.String(), http.NoBody)
if err != nil {
logger.Errorf("failed to create request: %s", err)
return nil, err

View file

@ -14,6 +14,7 @@ import (
log "github.com/sirupsen/logrus"
yaml "gopkg.in/yaml.v2"
"github.com/crowdsecurity/go-cs-lib/ptr"
"github.com/crowdsecurity/grokky"
"github.com/crowdsecurity/crowdsec/pkg/cache"
@ -462,11 +463,10 @@ func (n *Node) compile(pctx *UnixParserCtx, ectx EnricherCtx) error {
that will be used only for processing this node ;) */
if n.Debug {
clog := log.New()
if err = types.ConfigureLogger(clog); err != nil {
if err = types.ConfigureLogger(clog, ptr.Of(log.DebugLevel)); err != nil {
return fmt.Errorf("while creating bucket-specific logger: %w", err)
}
clog.SetLevel(log.DebugLevel)
n.Logger = clog.WithField("id", n.rn)
n.Logger.Infof("%s has debug enabled", n.Name)
} else {

View file

@ -11,12 +11,13 @@ import (
)
var (
logFormatter log.Formatter
LogOutput *lumberjack.Logger // io.Writer
logLevel log.Level
logFormatter log.Formatter
LogOutput *lumberjack.Logger // io.Writer
logLevel log.Level
logLevelViaFlag bool
)
func SetDefaultLoggerConfig(cfgMode string, cfgFolder string, cfgLevel log.Level, maxSize int, maxFiles int, maxAge int, format string, compress *bool, forceColors bool) error {
func SetDefaultLoggerConfig(cfgMode string, cfgFolder string, cfgLevel log.Level, maxSize int, maxFiles int, maxAge int, format string, compress *bool, forceColors bool, levelViaFlag bool) error {
if format == "" {
format = "text"
}
@ -68,13 +69,14 @@ func SetDefaultLoggerConfig(cfgMode string, cfgFolder string, cfgLevel log.Level
}
logLevel = cfgLevel
logLevelViaFlag = levelViaFlag
log.SetLevel(logLevel)
log.SetFormatter(logFormatter)
return nil
}
func ConfigureLogger(clog *log.Logger) error {
func ConfigureLogger(clog *log.Logger, level *log.Level) error {
/*Configure logs*/
if LogOutput != nil {
clog.SetOutput(LogOutput)
@ -86,6 +88,10 @@ func ConfigureLogger(clog *log.Logger) error {
clog.SetLevel(logLevel)
if level != nil && !logLevelViaFlag {
clog.SetLevel(*level)
}
return nil
}

View file

@ -101,6 +101,24 @@ teardown() {
assert_stderr --regexp 'FATAL.* you must run at least the API Server or crowdsec$'
}
@test "crowdsec - pass log level flag to apiserver" {
LOCAL_API_CREDENTIALS=$(config_get '.api.client.credentials_path')
config_set "$LOCAL_API_CREDENTIALS" '.password="badpassword"'
config_set '.common.log_media="stdout"'
rune -1 "$CROWDSEC"
# info
assert_stderr --partial "/v1/watchers/login"
# fatal
assert_stderr --partial "incorrect Username or Password"
config_set '.common.log_media="stdout"'
rune -1 "$CROWDSEC" -error
refute_stderr --partial "/v1/watchers/login"
}
@test "CS_LAPI_SECRET not strong enough" {
CS_LAPI_SECRET=foo rune -1 wait-for "$CROWDSEC"
assert_stderr --partial "api server init: unable to run local API: controller init: CS_LAPI_SECRET not strong enough"

View file

@ -66,10 +66,10 @@ teardown() {
@test "cscli alerts inspect" {
rune -1 cscli alerts inspect
assert_stderr --partial 'missing alert_id'
assert_stderr 'Error: requires at least 1 arg(s), only received 0'
rune -0 cscli decisions add -i 10.20.30.40 -t ban
rune -0 cscli alerts list -o raw <(output)
rune -0 cscli alerts list -o raw
rune -0 grep 10.20.30.40 <(output)
rune -0 cut -d, -f1 <(output)
ALERT_ID="$output"

174
test/bats/appsec.bats Normal file
View file

@ -0,0 +1,174 @@
#!/usr/bin/env bats
set -u
setup_file() {
load "../lib/setup_file.sh"
CONFIG_DIR=$(dirname "$CONFIG_YAML")
export CONFIG_DIR
ACQUIS_DIR=$(config_get '.crowdsec_service.acquisition_dir')
export ACQUIS_DIR
}
teardown_file() {
load "../lib/teardown_file.sh"
}
setup() {
load "../lib/setup.sh"
./instance-data load
mkdir -p "$ACQUIS_DIR"
}
teardown() {
./instance-crowdsec stop
}
#----------
@test "invalid configuration" {
config_set '.common.log_media="stdout"'
cat > "$ACQUIS_DIR"/appsec.yaml <<-EOT
source: appsec
EOT
rune -1 wait-for "$CROWDSEC"
assert_stderr --partial "crowdsec init: while loading acquisition config: missing labels in $ACQUIS_DIR/appsec.yaml (position 0)"
cat > "$ACQUIS_DIR"/appsec.yaml <<-EOT
source: appsec
labels:
type: appsec
EOT
rune -1 wait-for "$CROWDSEC"
assert_stderr --partial "crowdsec init: while loading acquisition config: while configuring datasource of type appsec from $ACQUIS_DIR/appsec.yaml (position 0): unable to parse appsec configuration: appsec_config or appsec_config_path must be set"
}
@test "appsec allow and ban" {
config_set '.common.log_media="stdout"'
rune -0 cscli collections install crowdsecurity/appsec-virtual-patching
rune -0 cscli collections install crowdsecurity/appsec-generic-rules
socket="$BATS_TEST_TMPDIR"/sock
cat > "$ACQUIS_DIR"/appsec.yaml <<-EOT
source: appsec
listen_socket: $socket
labels:
type: appsec
appsec_config: crowdsecurity/appsec-default
EOT
rune -0 wait-for \
--err "Appsec Runner ready to process event" \
"$CROWDSEC"
assert_stderr --partial "loading inband rule crowdsecurity/base-config"
assert_stderr --partial "loading inband rule crowdsecurity/vpatch-*"
assert_stderr --partial "loading inband rule crowdsecurity/generic-*"
assert_stderr --partial "Created 1 appsec runners"
assert_stderr --partial "Appsec Runner ready to process event"
./instance-crowdsec start
rune -0 cscli bouncers add appsecbouncer --key appkey
# appsec will perform a HEAD request to validate.
# If it fails, check downstream with:
#
# lapisocket=$(config_get '.api.server.listen_socket')
# rune -0 curl -sS --fail-with-body --unix-socket "$lapisocket" -H "X-Api-Key: appkey" "http://fakehost/v1/decisions/stream"
# assert_json '{deleted:null,new:null}'
rune -0 curl -sS --fail-with-body --unix-socket "$socket" \
-H "x-crowdsec-appsec-api-key: appkey" \
-H "x-crowdsec-appsec-ip: 1.2.3.4" \
-H 'x-crowdsec-appsec-uri: /' \
-H 'x-crowdsec-appsec-host: foo.com' \
-H 'x-crowdsec-appsec-verb: GET' \
'http://fakehost'
assert_json '{action:"allow",http_status:200}'
rune -22 curl -sS --fail-with-body --unix-socket "$socket" \
-H "x-crowdsec-appsec-api-key: appkey" \
-H "x-crowdsec-appsec-ip: 1.2.3.4" \
-H 'x-crowdsec-appsec-uri: /.env' \
-H 'x-crowdsec-appsec-host: foo.com' \
-H 'x-crowdsec-appsec-verb: GET' \
'http://fakehost'
assert_json '{action:"ban",http_status:403}'
}
@test "TLS connection to lapi, own CA" {
tmpdir="$BATS_FILE_TMPDIR"
CFDIR="$BATS_TEST_DIRNAME/testdata/cfssl"
# Root CA
cfssl gencert -loglevel 2 \
--initca "$CFDIR/ca_root.json" \
| cfssljson --bare "$tmpdir/root"
# Intermediate CA
cfssl gencert -loglevel 2 \
--initca "$CFDIR/ca_intermediate.json" \
| cfssljson --bare "$tmpdir/inter"
cfssl sign -loglevel 2 \
-ca "$tmpdir/root.pem" -ca-key "$tmpdir/root-key.pem" \
-config "$CFDIR/profiles.json" -profile intermediate_ca "$tmpdir/inter.csr" \
| cfssljson --bare "$tmpdir/inter"
# Server cert for crowdsec with the intermediate
cfssl gencert -loglevel 2 \
-ca "$tmpdir/inter.pem" -ca-key "$tmpdir/inter-key.pem" \
-config "$CFDIR/profiles.json" -profile=server "$CFDIR/server.json" \
| cfssljson --bare "$tmpdir/server"
cat "$tmpdir/root.pem" "$tmpdir/inter.pem" > "$tmpdir/bundle.pem"
export tmpdir
config_set '
.api.server.tls.cert_file=strenv(tmpdir) + "/server.pem" |
.api.server.tls.key_file=strenv(tmpdir) + "/server-key.pem" |
.api.server.tls.ca_cert_path=strenv(tmpdir) + "/inter.pem"
'
rune -0 cscli collections install crowdsecurity/appsec-virtual-patching
rune -0 cscli collections install crowdsecurity/appsec-generic-rules
socket="$BATS_TEST_TMPDIR"/sock
cat > "$ACQUIS_DIR"/appsec.yaml <<-EOT
source: appsec
listen_socket: $socket
labels:
type: appsec
appsec_config: crowdsecurity/appsec-default
EOT
config_set "$CONFIG_DIR/local_api_credentials.yaml" '
.url="https://127.0.0.1:8080" |
.ca_cert_path=strenv(tmpdir) + "/bundle.pem"
'
./instance-crowdsec start
rune -0 cscli bouncers add appsecbouncer --key appkey
rune -0 curl -sS --fail-with-body --unix-socket "$socket" \
-H "x-crowdsec-appsec-api-key: appkey" \
-H "x-crowdsec-appsec-ip: 1.2.3.4" \
-H 'x-crowdsec-appsec-uri: /' \
-H 'x-crowdsec-appsec-host: foo.com' \
-H 'x-crowdsec-appsec-verb: GET' \
'http://fakehost'
assert_json '{action:"allow",http_status:200}'
}

View file

@ -47,13 +47,13 @@ get_latest_version() {
@test "cscli <hubtype> install (no argument)" {
rune -1 cscli parsers install
refute_output
assert_output --partial 'Usage:'
assert_stderr --partial 'requires at least 1 arg(s), only received 0'
}
@test "cscli <hubtype> install (aliased)" {
rune -1 cscli parser install
refute_output
assert_output --partial 'Usage:'
assert_stderr --partial 'requires at least 1 arg(s), only received 0'
}