Merge branch 'master' into remove-dashboard

This commit is contained in:
marco 2025-03-25 15:06:27 +01:00
commit eca00377af
141 changed files with 1830 additions and 997 deletions

View file

@ -10,9 +10,6 @@ on:
jobs:
build:
strategy:
matrix:
test-file: ["hub-1.bats", "hub-2.bats", "hub-3.bats"]
name: "Functional tests"
runs-on: ubuntu-latest
@ -46,11 +43,14 @@ jobs:
- name: "Run hub tests"
run: |
./test/bin/generate-hub-tests
./test/run-tests ./test/dyn-bats/${{ matrix.test-file }} --formatter $(pwd)/test/lib/color-formatter
PATH=$(pwd)/test/local/bin:$PATH
./test/instance-data load
git clone --depth 1 https://github.com/crowdsecurity/hub.git ./hub
cd ./hub
cscli hubtest run --all --clean --max-jobs 8
- name: "Collect hub coverage"
run: ./test/bin/collect-hub-coverage >> $GITHUB_ENV
run: ./test/bin/collect-hub-coverage ./hub >> $GITHUB_ENV
- name: "Create Parsers badge"
uses: schneegans/dynamic-badges-action@v1.7.0

View file

@ -46,6 +46,7 @@ linters-settings:
gomoddirectives:
replace-allow-list:
- golang.org/x/time/rate
- github.com/corazawaf/coraza/v3
govet:
enable-all: true
@ -214,7 +215,6 @@ linters-settings:
enable-all: true
disabled-checks:
- paramTypeCombine
- httpNoBody
- ifElseChain
- importShadow
- hugeParam
@ -280,12 +280,10 @@ linters:
# Recommended? (requires some work)
#
- containedctx # containedctx is a linter that detects struct contained context.Context field
- errname # Checks that sentinel errors are prefixed with the `Err` and error types are suffixed with the `Error`.
- ireturn # Accept Interfaces, Return Concrete Types
- mnd # An analyzer to detect magic numbers.
- nilnil # Checks that there is no simultaneous return of `nil` error and an invalid value.
- noctx # Finds sending http request without context.Context
- unparam # Reports unused function parameters
#
@ -503,3 +501,8 @@ issues:
- usetesting
path: "pkg/apiserver/(.+)_test.go"
text: "os.CreateTemp.* could be replaced by os.CreateTemp.*"
- linters:
- containedctx
path: "cmd/notification-file/main.go"
text: "found a struct that contains a context.Context field"

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

@ -22,6 +22,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/cmd/crowdsec-cli/reload"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/require"
"github.com/crowdsecurity/crowdsec/pkg/apiclient"
@ -45,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()
@ -191,7 +191,7 @@ After running this command your will need to validate the enrollment in the weba
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}
@ -224,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: `
@ -277,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
}
@ -300,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,10 +43,10 @@ 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,
}
cmd.AddCommand(cli.newBranchCmd())
cmd.AddCommand(cli.newListCmd())
cmd.AddCommand(cli.newUpdateCmd())
cmd.AddCommand(cli.newUpgradeCmd())
@ -84,13 +85,35 @@ func (cli *cliHub) List(out io.Writer, hub *cwhub.Hub, all bool) error {
return nil
}
func (cli *cliHub) newBranchCmd() *cobra.Command {
var all bool
cmd := &cobra.Command{
Use: "branch",
Short: "Show selected hub branch",
Long: "Display the hub branch to be used, depending on configuration and crowdsec version",
Args: args.NoArgs,
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, _ []string) error {
branch := require.HubBranch(cmd.Context(), cli.cfg())
fmt.Println(branch)
return nil
},
}
flags := cmd.Flags()
flags.BoolVarP(&all, "all", "a", false, "List all available items, including those not installed")
return cmd
}
func (cli *cliHub) newListCmd() *cobra.Command {
var all bool
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 +175,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 +251,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 +299,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

@ -1,31 +1,55 @@
package clihubtest
import (
"errors"
"fmt"
"github.com/spf13/cobra"
"github.com/crowdsecurity/crowdsec/pkg/hubtest"
)
func (cli *cliHubTest) newCleanCmd() *cobra.Command {
var all bool
cmd := &cobra.Command{
Use: "clean",
Short: "clean [test_name]",
Args: cobra.MinimumNArgs(1),
DisableAutoGenTag: true,
RunE: func(_ *cobra.Command, args []string) error {
for _, testName := range args {
test, err := hubPtr.LoadTestItem(testName)
if err != nil {
return fmt.Errorf("unable to load test '%s': %w", testName, err)
if !all && len(args) == 0 {
return errors.New("please provide test to run or --all flag")
}
fmt.Println("Cleaning test data...")
tests := []*hubtest.HubTestItem{}
if all {
if err := hubPtr.LoadAllTests(); err != nil {
return fmt.Errorf("unable to load all tests: %w", err)
}
if err := test.Clean(); err != nil {
return fmt.Errorf("unable to clean test '%s' env: %w", test.Name, err)
tests = hubPtr.Tests
} else {
for _, testName := range args {
test, err := hubPtr.LoadTestItem(testName)
if err != nil {
return fmt.Errorf("unable to load test '%s': %w", testName, err)
}
tests = append(tests, test)
}
}
for _, test := range tests {
test.Clean()
}
return nil
},
}
cmd.Flags().BoolVar(&all, "all", false, "Run all tests")
return cmd
}

View file

@ -9,16 +9,17 @@ import (
"github.com/fatih/color"
"github.com/spf13/cobra"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/args"
"github.com/crowdsecurity/crowdsec/pkg/hubtest"
)
// getCoverage returns the coverage and the percentage of tests that passed
func getCoverage(show bool, getCoverageFunc func() ([]hubtest.Coverage, error)) ([]hubtest.Coverage, int, error) {
func getCoverage(show bool, getCoverageFunc func(string) ([]hubtest.Coverage, error), hubDir string) ([]hubtest.Coverage, int, error) {
if !show {
return nil, 0, nil
}
coverage, err := getCoverageFunc()
coverage, err := getCoverageFunc(hubDir)
if err != nil {
return nil, 0, fmt.Errorf("while getting coverage: %w", err)
}
@ -45,7 +46,7 @@ func (cli *cliHubTest) coverage(showScenarioCov bool, showParserCov bool, showAp
// for this one we explicitly don't do for appsec
if err := HubTest.LoadAllTests(); err != nil {
return fmt.Errorf("unable to load all tests: %+v", err)
return fmt.Errorf("unable to load all tests: %w", err)
}
var err error
@ -57,17 +58,17 @@ func (cli *cliHubTest) coverage(showScenarioCov bool, showParserCov bool, showAp
showAppsecCov = true
}
parserCoverage, parserCoveragePercent, err := getCoverage(showParserCov, HubTest.GetParsersCoverage)
parserCoverage, parserCoveragePercent, err := getCoverage(showParserCov, HubTest.GetParsersCoverage, cfg.Hub.HubDir)
if err != nil {
return err
}
scenarioCoverage, scenarioCoveragePercent, err := getCoverage(showScenarioCov, HubTest.GetScenariosCoverage)
scenarioCoverage, scenarioCoveragePercent, err := getCoverage(showScenarioCov, HubTest.GetScenariosCoverage, cfg.Hub.HubDir)
if err != nil {
return err
}
appsecRuleCoverage, appsecRuleCoveragePercent, err := getCoverage(showAppsecCov, HubTest.GetAppsecCoverage)
appsecRuleCoverage, appsecRuleCoveragePercent, err := getCoverage(showAppsecCov, HubTest.GetAppsecCoverage, cfg.Hub.HubDir)
if err != nil {
return err
}
@ -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

@ -1,17 +1,19 @@
package clihubtest
import (
"context"
"fmt"
"github.com/spf13/cobra"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/args"
"github.com/crowdsecurity/crowdsec/pkg/dumps"
)
func (cli *cliHubTest) explain(testName string, details bool, skipOk bool) error {
func (cli *cliHubTest) explain(ctx context.Context, testName string, details bool, skipOk bool) error {
test, err := HubTest.LoadTestItem(testName)
if err != nil {
return fmt.Errorf("can't load test: %+v", err)
return fmt.Errorf("can't load test: %w", err)
}
cfg := cli.cfg()
@ -19,8 +21,8 @@ func (cli *cliHubTest) explain(testName string, details bool, skipOk bool) error
err = test.ParserAssert.LoadTest(test.ParserResultFile)
if err != nil {
if err = test.Run(patternDir); err != nil {
return fmt.Errorf("running test '%s' failed: %+v", test.Name, err)
if err = test.Run(ctx, patternDir); err != nil {
return fmt.Errorf("running test '%s' failed: %w", test.Name, err)
}
if err = test.ParserAssert.LoadTest(test.ParserResultFile); err != nil {
@ -30,8 +32,8 @@ func (cli *cliHubTest) explain(testName string, details bool, skipOk bool) error
err = test.ScenarioAssert.LoadTest(test.ScenarioResultFile, test.BucketPourResultFile)
if err != nil {
if err = test.Run(patternDir); err != nil {
return fmt.Errorf("running test '%s' failed: %+v", test.Name, err)
if err = test.Run(ctx, patternDir); err != nil {
return fmt.Errorf("running test '%s' failed: %w", test.Name, err)
}
if err = test.ScenarioAssert.LoadTest(test.ScenarioResultFile, test.BucketPourResultFile); err != nil {
@ -58,11 +60,12 @@ 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 {
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
for _, testName := range args {
if err := cli.explain(testName, details, skipOk); err != nil {
if err := cli.explain(ctx, testName, details, skipOk); err != nil {
return err
}
}

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

@ -1,34 +1,36 @@
package clihubtest
import (
"context"
"encoding/json"
"errors"
"fmt"
"os"
"runtime"
"strings"
"github.com/AlecAivazis/survey/v2"
"github.com/fatih/color"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"golang.org/x/sync/errgroup"
"github.com/crowdsecurity/crowdsec/pkg/emoji"
"github.com/crowdsecurity/crowdsec/pkg/hubtest"
)
func (cli *cliHubTest) run(runAll bool, nucleiTargetHost string, appSecHost string, args []string) error {
func (cli *cliHubTest) run(ctx context.Context, all bool, nucleiTargetHost string, appSecHost string, args []string, maxJobs uint) error {
cfg := cli.cfg()
if !runAll && len(args) == 0 {
if !all && len(args) == 0 {
return errors.New("please provide test to run or --all flag")
}
hubPtr.NucleiTargetHost = nucleiTargetHost
hubPtr.AppSecHost = appSecHost
if runAll {
if all {
if err := hubPtr.LoadAllTests(); err != nil {
return fmt.Errorf("unable to load all tests: %+v", err)
return fmt.Errorf("unable to load all tests: %w", err)
}
} else {
for _, testName := range args {
@ -39,23 +41,29 @@ func (cli *cliHubTest) run(runAll bool, nucleiTargetHost string, appSecHost stri
}
}
// set timezone to avoid DST issues
os.Setenv("TZ", "UTC")
patternDir := cfg.ConfigPaths.PatternDir
var eg errgroup.Group
if isAppsecTest {
log.Info("Appsec tests can not run in parallel: setting max_jobs=1")
maxJobs = 1
}
eg.SetLimit(int(maxJobs))
for _, test := range hubPtr.Tests {
if cfg.Cscli.Output == "human" {
log.Infof("Running test '%s'", test.Name)
fmt.Printf("Running test '%s'\n", test.Name)
}
err := test.Run(patternDir)
if err != nil {
log.Errorf("running test '%s' failed: %+v", test.Name, err)
}
eg.Go(func() error {
return test.Run(ctx, patternDir)
})
}
return nil
return eg.Wait()
}
func printParserFailures(test *hubtest.HubTestItem) {
@ -101,24 +109,31 @@ func printScenarioFailures(test *hubtest.HubTestItem) {
func (cli *cliHubTest) newRunCmd() *cobra.Command {
var (
noClean bool
runAll bool
all bool
reportSuccess bool
forceClean bool
nucleiTargetHost string
appSecHost string
)
maxJobs := uint(runtime.NumCPU())
cmd := &cobra.Command{
Use: "run",
Short: "run [test_name]",
DisableAutoGenTag: true,
RunE: func(_ *cobra.Command, args []string) error {
return cli.run(runAll, nucleiTargetHost, appSecHost, args)
RunE: func(cmd *cobra.Command, args []string) error {
if all {
fmt.Printf("Running all tests (max_jobs: %d)\n", maxJobs)
}
return cli.run(cmd.Context(), all, nucleiTargetHost, appSecHost, args, maxJobs)
},
PersistentPostRunE: func(_ *cobra.Command, _ []string) error {
cfg := cli.cfg()
success := true
testResult := make(map[string]bool)
testMap := make(map[string]*hubtest.HubTestItem)
for _, test := range hubPtr.Tests {
if test.AutoGen && !isAppsecTest {
if test.ParserAssert.AutoGenAssert {
@ -132,22 +147,15 @@ func (cli *cliHubTest) newRunCmd() *cobra.Command {
fmt.Println(test.ScenarioAssert.AutoGenAssertData)
}
if !noClean {
if err := test.Clean(); err != nil {
return fmt.Errorf("unable to clean test '%s' env: %w", test.Name, err)
}
test.Clean()
}
return fmt.Errorf("please fill your assert file(s) for test '%s', exiting", test.Name)
}
testResult[test.Name] = test.Success
testMap[test.Name] = test
if test.Success {
if cfg.Cscli.Output == "human" {
log.Infof("Test '%s' passed successfully (%d assertions)\n", test.Name, test.ParserAssert.NbAssert+test.ScenarioAssert.NbAssert)
}
if !noClean {
if err := test.Clean(); err != nil {
return fmt.Errorf("unable to clean test '%s' env: %w", test.Name, err)
}
test.Clean()
}
} else {
success = false
@ -157,7 +165,7 @@ func (cli *cliHubTest) newRunCmd() *cobra.Command {
printScenarioFailures(test)
if !forceClean && !noClean {
prompt := &survey.Confirm{
Message: fmt.Sprintf("\nDo you want to remove runtime folder for test '%s'? (default: Yes)", test.Name),
Message: fmt.Sprintf("Do you want to remove runtime and result folder for '%s'?", test.Name),
Default: true,
}
if err := survey.AskOne(prompt, &cleanTestEnv); err != nil {
@ -167,22 +175,20 @@ func (cli *cliHubTest) newRunCmd() *cobra.Command {
}
if cleanTestEnv || forceClean {
if err := test.Clean(); err != nil {
return fmt.Errorf("unable to clean test '%s' env: %w", test.Name, err)
}
test.Clean()
}
}
}
switch cfg.Cscli.Output {
case "human":
hubTestResultTable(color.Output, cfg.Cscli.Color, testResult)
hubTestResultTable(color.Output, cfg.Cscli.Color, testMap, reportSuccess)
case "json":
jsonResult := make(map[string][]string, 0)
jsonResult["success"] = make([]string, 0)
jsonResult["fail"] = make([]string, 0)
for testName, success := range testResult {
if success {
for testName, test := range testMap {
if test.Success {
jsonResult["success"] = append(jsonResult["success"], testName)
} else {
jsonResult["fail"] = append(jsonResult["fail"], testName)
@ -198,7 +204,11 @@ func (cli *cliHubTest) newRunCmd() *cobra.Command {
}
if !success {
return errors.New("some tests failed")
if reportSuccess {
return errors.New("some tests failed")
}
return errors.New("some tests failed, use --report-success to show them all")
}
return nil
@ -209,7 +219,9 @@ func (cli *cliHubTest) newRunCmd() *cobra.Command {
cmd.Flags().BoolVar(&forceClean, "clean", false, "Clean runtime environment if test fail")
cmd.Flags().StringVar(&nucleiTargetHost, "target", hubtest.DefaultNucleiTarget, "Target for AppSec Test")
cmd.Flags().StringVar(&appSecHost, "host", hubtest.DefaultAppsecHost, "Address to expose AppSec for hubtest")
cmd.Flags().BoolVar(&runAll, "all", false, "Run all tests")
cmd.Flags().BoolVar(&all, "all", false, "Run all tests")
cmd.Flags().BoolVar(&reportSuccess, "report-success", false, "Report successful tests too (implied with json output)")
cmd.Flags().UintVar(&maxJobs, "max-jobs", maxJobs, "Max number of concurrent tests (does not apply to appsec)")
return cmd
}

View file

@ -3,6 +3,7 @@ package clihubtest
import (
"fmt"
"io"
"strconv"
"github.com/jedib0t/go-pretty/v6/text"
@ -11,22 +12,31 @@ import (
"github.com/crowdsecurity/crowdsec/pkg/hubtest"
)
func hubTestResultTable(out io.Writer, wantColor string, testResult map[string]bool) {
func hubTestResultTable(out io.Writer, wantColor string, testMap map[string]*hubtest.HubTestItem, reportSuccess bool) {
t := cstable.NewLight(out, wantColor)
t.SetHeaders("Test", "Result")
t.SetHeaders("Test", "Result", "Assertions")
t.SetHeaderAlignment(text.AlignLeft)
t.SetAlignment(text.AlignLeft)
for testName, success := range testResult {
showTable := reportSuccess
for testName, test := range testMap {
status := emoji.CheckMarkButton
if !success {
if !test.Success {
status = emoji.CrossMark
showTable = true
}
t.AddRow(testName, status)
if !test.Success || reportSuccess {
t.AddRow(testName, status, strconv.Itoa(test.ParserAssert.NbAssert+test.ScenarioAssert.NbAssert))
}
}
t.Render()
if showTable {
t.Render()
} else {
fmt.Println("All tests passed, use --report-success for more details.")
}
}
func hubTestListTable(out io.Writer, wantColor string, tests []*hubtest.HubTestItem) {

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

@ -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

@ -168,8 +168,9 @@ func (cli *cliRoot) initialize() error {
}
}
csConfig.DbConfig.LogLevel = ptr.Of(cli.wantedLogLevel())
if csConfig.DbConfig != nil {
csConfig.DbConfig.LogLevel = ptr.Of(cli.wantedLogLevel())
}
return nil
}

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
}

63
debian/migrate-hub.sh vendored Executable file
View file

@ -0,0 +1,63 @@
#!/usr/bin/env sh
# This script is provided (only in the source distribution) as an ad-hoc solution
# to migrate an installation from the crowdsec package maintained in the debian or ubuntu repositories
# to the official crowdsec repository.
set -eu
if [ ! -d /var/lib/crowdsec/hub/ ]; then
echo "You don't have a hub directory to migrate."
echo
echo "Use this script only if you upgrade from the crowdsec package included in the debian or ubuntu repositories."
exit 1
fi
# Download everything on the new hub but don't install anything yet
echo "Downloading Hub content..."
for itemtype in $(cscli hub types -o raw); do
ALL_ITEMS=$(cscli "$itemtype" list -a -o raw | tail +2 | cut -d, -f1)
if [ -n "${ALL_ITEMS}" ]; then
# shellcheck disable=SC2086
cscli "$itemtype" install \
$ALL_ITEMS \
--download-only -y
fi
done
# Fix links
BASEDIR=/etc/crowdsec/
OLD_PATH=/var/lib/crowdsec/hub/
NEW_PATH=/etc/crowdsec/hub/
find "$BASEDIR" -type l 2>/dev/null | while IFS= read -r link
do
target="$(readlink "$link")" || continue
case "$target" in
"$OLD_PATH"*)
suffix="${target#"$OLD_PATH"}"
new_target="${NEW_PATH}${suffix}"
if [ -e "$target" ]; then
continue
fi
if [ ! -e "$new_target" ]; then
continue
fi
echo "Update symlink: $link"
ln -sf "$new_target" "$link"
;;
*)
;;
esac
done
# upgrade tainted collections
cscli hub upgrade --force

27
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
@ -23,12 +23,11 @@ require (
github.com/corazawaf/libinjection-go v0.2.2
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
github.com/creack/pty v1.1.21 // indirect
github.com/crowdsecurity/coraza/v3 v3.0.0-20250121111732-9b0043b679d7
github.com/crowdsecurity/dlog v0.0.0-20170105205344-4fb5f8204f26
github.com/crowdsecurity/go-cs-lib v0.0.16
github.com/crowdsecurity/grokky v0.2.2
github.com/crowdsecurity/machineid v1.0.2
github.com/davecgh/go-spew v1.1.1
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc
github.com/distribution/reference v0.6.0 // indirect
github.com/docker/docker v27.3.1+incompatible
github.com/docker/go-connections v0.5.0 // indirect
@ -45,7 +44,7 @@ require (
github.com/go-sql-driver/mysql v1.6.0
github.com/goccy/go-yaml v1.11.0
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt/v4 v4.5.1
github.com/golang-jwt/jwt/v4 v4.5.2
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/go-querystring v1.1.0
@ -97,12 +96,12 @@ require (
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0 // indirect
go.opentelemetry.io/otel/sdk v1.28.0 // indirect
go.opentelemetry.io/otel/trace v1.28.0 // indirect
golang.org/x/crypto v0.32.0
golang.org/x/crypto v0.36.0
golang.org/x/mod v0.23.0
golang.org/x/net v0.34.0 // indirect
golang.org/x/sync v0.11.0 // indirect
golang.org/x/sys v0.30.0
golang.org/x/text v0.21.0
golang.org/x/net v0.37.0 // indirect
golang.org/x/sync v0.12.0
golang.org/x/sys v0.31.0
golang.org/x/text v0.23.0
golang.org/x/time v0.6.0 // indirect
google.golang.org/grpc v1.67.1
google.golang.org/protobuf v1.36.3
@ -115,6 +114,8 @@ require (
)
require github.com/corazawaf/coraza/v3 v3.3.2
require (
ariga.io/atlas v0.31.1-0.20250212144724-069be8033e83 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
@ -128,7 +129,6 @@ require (
github.com/bytedance/sonic/loader v0.2.1 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect
github.com/corazawaf/coraza-coreruleset v0.0.0-20240226094324-415b1017abdc // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/gabriel-vasile/mimetype v1.4.7 // indirect
@ -162,7 +162,6 @@ require (
github.com/jackc/pgproto3/v2 v2.3.3 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/pgtype v1.14.0 // indirect
github.com/jcchavezs/mergefs v0.1.0 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
@ -186,7 +185,7 @@ require (
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
github.com/petar-dambovaliev/aho-corasick v0.0.0-20240411101913-e07a1f0e8eb4 // indirect
github.com/pierrec/lz4/v4 v4.1.18 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/prometheus/common v0.44.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
@ -217,7 +216,7 @@ require (
go.opentelemetry.io/otel/metric v1.28.0 // indirect
go.uber.org/atomic v1.10.0 // indirect
golang.org/x/arch v0.12.0 // indirect
golang.org/x/term v0.28.0 // indirect
golang.org/x/term v0.30.0 // indirect
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 // indirect
@ -233,3 +232,5 @@ require (
)
replace golang.org/x/time/rate => github.com/crowdsecurity/crowdsec/pkg/time/rate v0.0.0
replace github.com/corazawaf/coraza/v3 => github.com/crowdsecurity/coraza/v3 v3.0.0-20250320231801-749b8bded21a

40
go.sum
View file

@ -93,8 +93,6 @@ github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
github.com/corazawaf/coraza-coreruleset v0.0.0-20240226094324-415b1017abdc h1:OlJhrgI3I+FLUCTI3JJW8MoqyM78WbqJjecqMnqG+wc=
github.com/corazawaf/coraza-coreruleset v0.0.0-20240226094324-415b1017abdc/go.mod h1:7rsocqNDkTCira5T0M7buoKR2ehh7YZiPkzxRuAgvVU=
github.com/corazawaf/coraza/v3 v3.3.2 h1:eG1HPLySTR9lND6y6fPOajubwbuHRF6aXCsCtxyqKTY=
github.com/corazawaf/coraza/v3 v3.3.2/go.mod h1:4EqMZkRoil11FnResCT/2JIg61dH+6D7F48VG8SVzuA=
github.com/corazawaf/libinjection-go v0.2.2 h1:Chzodvb6+NXh6wew5/yhD0Ggioif9ACrQGR4qjTCs1g=
github.com/corazawaf/libinjection-go v0.2.2/go.mod h1:OP4TM7xdJ2skyXqNX1AN1wN5nNZEmJNuWbNPOItn7aw=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
@ -109,8 +107,8 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3
github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0=
github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/crowdsecurity/coraza/v3 v3.0.0-20250121111732-9b0043b679d7 h1:nIwAjapWmiQD3W/uAWYE3z+DC5Coy/zTyPBCJ379fAw=
github.com/crowdsecurity/coraza/v3 v3.0.0-20250121111732-9b0043b679d7/go.mod h1:A+uciRXu+yhZcHMtM052bSM6vyJsMMU37NJN+tVoGqo=
github.com/crowdsecurity/coraza/v3 v3.0.0-20250320231801-749b8bded21a h1:2Nyr+47Y/K68wohQWCrE7jKRIOpp6hJ29XCEQO3FhOw=
github.com/crowdsecurity/coraza/v3 v3.0.0-20250320231801-749b8bded21a/go.mod h1:xSaXWOhFMSbrV8qOOfBKAyw3aOqfwaSaOy5BgSF8XlA=
github.com/crowdsecurity/dlog v0.0.0-20170105205344-4fb5f8204f26 h1:r97WNVC30Uen+7WnLs4xDScS/Ex988+id2k6mDf8psU=
github.com/crowdsecurity/dlog v0.0.0-20170105205344-4fb5f8204f26/go.mod h1:zpv7r+7KXwgVUZnUNjyP22zc/D7LKjyoY02weH2RBbk=
github.com/crowdsecurity/go-cs-lib v0.0.16 h1:2/htodjwc/sfsv4deX8F/2Fzg1bOI8w3O1/BPSvvsB0=
@ -121,8 +119,9 @@ github.com/crowdsecurity/machineid v1.0.2 h1:wpkpsUghJF8Khtmn/tg6GxgdhLA1Xflerh5
github.com/crowdsecurity/machineid v1.0.2/go.mod h1:XWUSlnS0R0+u/JK5ulidwlbceNT3ZOCKteoVQEn6Luo=
github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/docker v27.3.1+incompatible h1:KttF0XoteNTicmUtBO0L2tP+J7FGRFTjaEF4k6WdhfI=
@ -307,8 +306,8 @@ github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRx
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo=
github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang/glog v1.2.4 h1:CNNw5U8lSiiBk7druxtSHHTsRWcxKoac6kZKm2peBBc=
github.com/golang/glog v1.2.4/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@ -584,8 +583,9 @@ github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
@ -800,8 +800,8 @@ golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
@ -833,8 +833,8 @@ golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -844,8 +844,8 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -881,8 +881,8 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@ -890,8 +890,8 @@ golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@ -904,8 +904,8 @@ golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=

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

@ -3,6 +3,8 @@ package appsecacquisition
import (
"net/http"
"net/url"
"os"
"path/filepath"
"testing"
log "github.com/sirupsen/logrus"
@ -346,7 +348,7 @@ func TestAppsecRuleMatches(t *testing.T) {
input_request: appsec.ParsedRequest{
ClientIP: "1.2.3.4",
RemoteAddr: "127.0.0.1",
Method: "GET",
Method: "POST",
URI: "/urllll",
Headers: http.Header{"Content-Type": []string{"multipart/form-data; boundary=boundary"}},
Body: []byte(`
@ -368,6 +370,11 @@ toto
require.Len(t, responses, 1)
require.True(t, responses[0].InBandInterrupt)
// Might fail if you have artifacts from previous tests, but good enough 99% of the time
tmpFiles, err := filepath.Glob(filepath.Join(os.TempDir(), "crzmp*"))
require.NoError(t, err)
require.Empty(t, tmpFiles)
},
},
{

View file

@ -11,8 +11,8 @@ import (
log "github.com/sirupsen/logrus"
"gopkg.in/tomb.v2"
"github.com/crowdsecurity/coraza/v3"
corazatypes "github.com/crowdsecurity/coraza/v3/types"
"github.com/corazawaf/coraza/v3"
corazatypes "github.com/corazawaf/coraza/v3/types"
// load body processors via init()
_ "github.com/crowdsecurity/crowdsec/pkg/acquisition/modules/appsec/bodyprocessors"
@ -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
}
}
}
}
@ -354,6 +355,10 @@ func (r *AppsecRunner) handleRequest(request *appsec.ParsedRequest) {
err := r.ProcessInBandRules(request)
if err != nil {
logger.Errorf("unable to process InBand rules: %s", err)
err = request.Tx.Close()
if err != nil {
logger.Errorf("unable to close inband transaction: %s", err)
}
return
}
@ -365,6 +370,11 @@ func (r *AppsecRunner) handleRequest(request *appsec.ParsedRequest) {
r.handleInBandInterrupt(request)
}
err = request.Tx.Close()
if err != nil {
r.logger.Errorf("unable to close inband transaction: %s", err)
}
// send back the result to the HTTP handler for the InBand part
request.ResponseChannel <- r.AppsecRuntime.Response
@ -384,6 +394,10 @@ func (r *AppsecRunner) handleRequest(request *appsec.ParsedRequest) {
err = r.ProcessOutOfBandRules(request)
if err != nil {
logger.Errorf("unable to process OutOfBand rules: %s", err)
err = request.Tx.Close()
if err != nil {
logger.Errorf("unable to close outband transaction: %s", err)
}
return
}
@ -394,6 +408,10 @@ func (r *AppsecRunner) handleRequest(request *appsec.ParsedRequest) {
r.handleOutBandInterrupt(request)
}
}
err = request.Tx.Close()
if err != nil {
r.logger.Errorf("unable to close outband transaction: %s", err)
}
// time spent to process inband AND out of band rules
globalParsingElapsed := time.Since(startGlobalParsing)
AppsecGlobalParsingHistogram.With(prometheus.Labels{"source": request.RemoteAddrNormalized, "appsec_engine": request.AppsecEngine}).Observe(globalParsingElapsed.Seconds())

View file

@ -5,8 +5,8 @@ import (
"strconv"
"strings"
"github.com/crowdsecurity/coraza/v3/experimental/plugins"
"github.com/crowdsecurity/coraza/v3/experimental/plugins/plugintypes"
"github.com/corazawaf/coraza/v3/experimental/plugins"
"github.com/corazawaf/coraza/v3/experimental/plugins/plugintypes"
)
type rawBodyProcessor struct{}

View file

@ -8,8 +8,8 @@ import (
"github.com/wasilibs/go-re2"
"github.com/wasilibs/go-re2/experimental"
"github.com/crowdsecurity/coraza/v3/experimental/plugins"
"github.com/crowdsecurity/coraza/v3/experimental/plugins/plugintypes"
"github.com/corazawaf/coraza/v3/experimental/plugins"
"github.com/corazawaf/coraza/v3/experimental/plugins/plugintypes"
)
type rx struct {

View file

@ -11,8 +11,8 @@ import (
"github.com/prometheus/client_golang/prometheus"
log "github.com/sirupsen/logrus"
"github.com/crowdsecurity/coraza/v3/collection"
"github.com/crowdsecurity/coraza/v3/types/variables"
"github.com/corazawaf/coraza/v3/collection"
"github.com/corazawaf/coraza/v3/types/variables"
"github.com/crowdsecurity/go-cs-lib/ptr"
"github.com/crowdsecurity/crowdsec/pkg/alertcontext"
@ -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

@ -254,7 +254,12 @@ basic_auth:
time.Sleep(1 * time.Second)
res, err := http.Get(fmt.Sprintf("%s/test", testHTTPServerAddr))
ctx := t.Context()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, testHTTPServerAddr + "/test", http.NoBody)
require.NoError(t, err)
res, err := http.DefaultClient.Do(req)
require.NoError(t, err)
assert.Equal(t, http.StatusMethodNotAllowed, res.StatusCode)
@ -265,6 +270,8 @@ basic_auth:
}
func TestStreamingAcquisitionUnknownPath(t *testing.T) {
ctx := t.Context()
h := &HTTPSource{}
_, _, tomb := SetupAndRunHTTPSource(t, h, []byte(`
source: http
@ -277,7 +284,10 @@ basic_auth:
time.Sleep(1 * time.Second)
res, err := http.Get(fmt.Sprintf("%s/unknown", testHTTPServerAddr))
req, err := http.NewRequestWithContext(ctx, http.MethodGet, testHTTPServerAddr + "/unknown", http.NoBody)
require.NoError(t, err)
res, err := http.DefaultClient.Do(req)
require.NoError(t, err)
assert.Equal(t, http.StatusNotFound, res.StatusCode)
@ -303,11 +313,15 @@ basic_auth:
client := &http.Client{}
resp, err := http.Post(fmt.Sprintf("%s/test", testHTTPServerAddr), "application/json", strings.NewReader("test"))
req, err := http.NewRequestWithContext(ctx, http.MethodPost, testHTTPServerAddr + "/test", strings.NewReader("test"))
require.NoError(t, err)
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, fmt.Sprintf("%s/test", testHTTPServerAddr), strings.NewReader("test"))
req, err = http.NewRequestWithContext(ctx, http.MethodPost, testHTTPServerAddr + "/test", strings.NewReader("test"))
require.NoError(t, err)
req.SetBasicAuth("test", "WrongPassword")
@ -553,6 +567,8 @@ timeout: 1s`), 0)
}
func TestStreamingAcquisitionTLSHTTPRequest(t *testing.T) {
ctx := t.Context()
h := &HTTPSource{}
_, _, tomb := SetupAndRunHTTPSource(t, h, []byte(`
source: http
@ -566,7 +582,11 @@ tls:
time.Sleep(1 * time.Second)
resp, err := http.Post(fmt.Sprintf("%s/test", testHTTPServerAddr), "application/json", strings.NewReader("test"))
req, err := http.NewRequestWithContext(ctx, http.MethodPost, testHTTPServerAddr + "/test", strings.NewReader("test"))
require.NoError(t, err)
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)

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

@ -49,7 +49,7 @@ type AlertsDeleteOpts struct {
func (s *AlertsService) Add(ctx context.Context, alerts models.AddAlertsRequest) (*models.AddAlertsResponse, *Response, error) {
u := fmt.Sprintf("%s/alerts", s.client.URLPrefix)
req, err := s.client.NewRequestWithContext(ctx, http.MethodPost, u, &alerts)
req, err := s.client.PrepareRequest(ctx, http.MethodPost, u, &alerts)
if err != nil {
return nil, nil, err
}
@ -78,7 +78,7 @@ func (s *AlertsService) List(ctx context.Context, opts AlertsListOpts) (*models.
URI = fmt.Sprintf("%s?%s", URI, params.Encode())
}
req, err := s.client.NewRequestWithContext(ctx, http.MethodGet, URI, nil)
req, err := s.client.PrepareRequest(ctx, http.MethodGet, URI, nil)
if err != nil {
return nil, nil, fmt.Errorf("building request: %w", err)
}
@ -102,7 +102,7 @@ func (s *AlertsService) Delete(ctx context.Context, opts AlertsDeleteOpts) (*mod
u := fmt.Sprintf("%s/alerts?%s", s.client.URLPrefix, params.Encode())
req, err := s.client.NewRequestWithContext(ctx, http.MethodDelete, u, nil)
req, err := s.client.PrepareRequest(ctx, http.MethodDelete, u, nil)
if err != nil {
return nil, nil, err
}
@ -120,7 +120,7 @@ func (s *AlertsService) Delete(ctx context.Context, opts AlertsDeleteOpts) (*mod
func (s *AlertsService) DeleteOne(ctx context.Context, alertID string) (*models.DeleteAlertsResponse, *Response, error) {
u := fmt.Sprintf("%s/alerts/%s", s.client.URLPrefix, alertID)
req, err := s.client.NewRequestWithContext(ctx, http.MethodDelete, u, nil)
req, err := s.client.PrepareRequest(ctx, http.MethodDelete, u, nil)
if err != nil {
return nil, nil, err
}
@ -138,7 +138,7 @@ func (s *AlertsService) DeleteOne(ctx context.Context, alertID string) (*models.
func (s *AlertsService) GetByID(ctx context.Context, alertID int) (*models.Alert, *Response, error) {
u := fmt.Sprintf("%s/alerts/%d", s.client.URLPrefix, alertID)
req, err := s.client.NewRequestWithContext(ctx, http.MethodGet, u, nil)
req, err := s.client.PrepareRequest(ctx, http.MethodGet, u, nil)
if err != nil {
return nil, nil, err
}

View file

@ -4,6 +4,7 @@ import (
"context"
"fmt"
"net/http"
"net/url"
qs "github.com/google/go-querystring/query"
log "github.com/sirupsen/logrus"
@ -27,7 +28,7 @@ func (s *AllowlistsService) List(ctx context.Context, opts AllowlistListOpts) (*
u += "?" + params.Encode()
req, err := s.client.NewRequestWithContext(ctx, http.MethodGet, u, nil)
req, err := s.client.PrepareRequest(ctx, http.MethodGet, u, nil)
if err != nil {
return nil, nil, err
}
@ -58,7 +59,7 @@ func (s *AllowlistsService) Get(ctx context.Context, name string, opts Allowlist
log.Debugf("GET %s", u)
req, err := s.client.NewRequestWithContext(ctx, http.MethodGet, u, nil)
req, err := s.client.PrepareRequest(ctx, http.MethodGet, u, nil)
if err != nil {
return nil, nil, err
}
@ -74,9 +75,10 @@ func (s *AllowlistsService) Get(ctx context.Context, name string, opts Allowlist
}
func (s *AllowlistsService) CheckIfAllowlisted(ctx context.Context, value string) (bool, *Response, error) {
u := s.client.URLPrefix + "/allowlists/check/" + value
escapedValue := url.PathEscape(value)
u := s.client.URLPrefix + "/allowlists/check/" + escapedValue
req, err := s.client.NewRequestWithContext(ctx, http.MethodHead, u, nil)
req, err := s.client.PrepareRequest(ctx, http.MethodHead, u, nil)
if err != nil {
return false, nil, err
}
@ -92,9 +94,10 @@ func (s *AllowlistsService) CheckIfAllowlisted(ctx context.Context, value string
}
func (s *AllowlistsService) CheckIfAllowlistedWithReason(ctx context.Context, value string) (*models.CheckAllowlistResponse, *Response, error) {
u := s.client.URLPrefix + "/allowlists/check/" + value
escapedValue := url.PathEscape(value)
u := s.client.URLPrefix + "/allowlists/check/" + escapedValue
req, err := s.client.NewRequestWithContext(ctx, http.MethodGet, u, nil)
req, err := s.client.PrepareRequest(ctx, http.MethodGet, u, nil)
if err != nil {
return nil, nil, err
}

View file

@ -21,7 +21,7 @@ type enrollRequest struct {
func (s *AuthService) UnregisterWatcher(ctx context.Context) (*Response, error) {
u := fmt.Sprintf("%s/watchers", s.client.URLPrefix)
req, err := s.client.NewRequestWithContext(ctx, http.MethodDelete, u, nil)
req, err := s.client.PrepareRequest(ctx, http.MethodDelete, u, nil)
if err != nil {
return nil, err
}
@ -37,7 +37,7 @@ func (s *AuthService) UnregisterWatcher(ctx context.Context) (*Response, error)
func (s *AuthService) RegisterWatcher(ctx context.Context, registration models.WatcherRegistrationRequest) (*Response, error) {
u := fmt.Sprintf("%s/watchers", s.client.URLPrefix)
req, err := s.client.NewRequestWithContext(ctx, http.MethodPost, u, &registration)
req, err := s.client.PrepareRequest(ctx, http.MethodPost, u, &registration)
if err != nil {
return nil, err
}
@ -55,7 +55,7 @@ func (s *AuthService) AuthenticateWatcher(ctx context.Context, auth models.Watch
u := fmt.Sprintf("%s/watchers/login", s.client.URLPrefix)
req, err := s.client.NewRequestWithContext(ctx, http.MethodPost, u, &auth)
req, err := s.client.PrepareRequest(ctx, http.MethodPost, u, &auth)
if err != nil {
return authResp, nil, err
}
@ -71,7 +71,7 @@ func (s *AuthService) AuthenticateWatcher(ctx context.Context, auth models.Watch
func (s *AuthService) EnrollWatcher(ctx context.Context, enrollKey string, name string, tags []string, overwrite bool) (*Response, error) {
u := fmt.Sprintf("%s/watchers/enroll", s.client.URLPrefix)
req, err := s.client.NewRequestWithContext(ctx, http.MethodPost, u, &enrollRequest{EnrollKey: enrollKey, Name: name, Tags: tags, Overwrite: overwrite})
req, err := s.client.PrepareRequest(ctx, http.MethodPost, u, &enrollRequest{EnrollKey: enrollKey, Name: name, Tags: tags, Overwrite: overwrite})
if err != nil {
return nil, err
}

View file

@ -72,10 +72,6 @@ type service struct {
}
func InitLAPIClient(ctx context.Context, apiUrl string, papiUrl string, login string, password string, scenarios []string) error {
if lapiClient != nil {
return errors.New("client already initialized")
}
apiURL, err := url.Parse(apiUrl)
if err != nil {
return fmt.Errorf("parsing api url ('%s'): %w", apiURL, err)

View file

@ -15,7 +15,7 @@ import (
log "github.com/sirupsen/logrus"
)
func (c *ApiClient) NewRequestWithContext(ctx context.Context, method, url string, body interface{}) (*http.Request, error) {
func (c *ApiClient) PrepareRequest(ctx context.Context, method, url string, body interface{}) (*http.Request, error) {
if !strings.HasSuffix(c.BaseURL.Path, "/") {
return nil, fmt.Errorf("BaseURL must have a trailing slash, but %q does not", c.BaseURL)
}

View file

@ -81,7 +81,7 @@ func (s *DecisionsService) List(ctx context.Context, opts DecisionsListOpts) (*m
u := fmt.Sprintf("%s/decisions?%s", s.client.URLPrefix, params.Encode())
req, err := s.client.NewRequestWithContext(ctx, http.MethodGet, u, nil)
req, err := s.client.PrepareRequest(ctx, http.MethodGet, u, nil)
if err != nil {
return nil, nil, err
}
@ -97,7 +97,7 @@ func (s *DecisionsService) List(ctx context.Context, opts DecisionsListOpts) (*m
}
func (s *DecisionsService) FetchV2Decisions(ctx context.Context, url string) (*models.DecisionsStreamResponse, *Response, error) {
req, err := s.client.NewRequestWithContext(ctx, http.MethodGet, url, nil)
req, err := s.client.PrepareRequest(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, nil, err
}
@ -138,7 +138,7 @@ func (s *DecisionsService) FetchV3Decisions(ctx context.Context, url string) (*m
scenarioDeleted := "deleted"
durationDeleted := "1h"
req, err := s.client.NewRequestWithContext(ctx, http.MethodGet, url, nil)
req, err := s.client.PrepareRequest(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, nil, err
}
@ -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
}
@ -271,7 +271,7 @@ func (s *DecisionsService) GetStreamV3(ctx context.Context, opts DecisionsStream
return nil, nil, err
}
req, err := s.client.NewRequestWithContext(ctx, http.MethodGet, u, nil)
req, err := s.client.PrepareRequest(ctx, http.MethodGet, u, nil)
if err != nil {
return nil, nil, err
}
@ -289,7 +289,7 @@ func (s *DecisionsService) GetStreamV3(ctx context.Context, opts DecisionsStream
func (s *DecisionsService) StopStream(ctx context.Context) (*Response, error) {
u := fmt.Sprintf("%s/decisions", s.client.URLPrefix)
req, err := s.client.NewRequestWithContext(ctx, http.MethodDelete, u, nil)
req, err := s.client.PrepareRequest(ctx, http.MethodDelete, u, nil)
if err != nil {
return nil, err
}
@ -310,7 +310,7 @@ func (s *DecisionsService) Delete(ctx context.Context, opts DecisionsDeleteOpts)
u := fmt.Sprintf("%s/decisions?%s", s.client.URLPrefix, params.Encode())
req, err := s.client.NewRequestWithContext(ctx, http.MethodDelete, u, nil)
req, err := s.client.PrepareRequest(ctx, http.MethodDelete, u, nil)
if err != nil {
return nil, nil, err
}
@ -328,7 +328,7 @@ func (s *DecisionsService) Delete(ctx context.Context, opts DecisionsDeleteOpts)
func (s *DecisionsService) DeleteOne(ctx context.Context, decisionID string) (*models.DeleteDecisionResponse, *Response, error) {
u := fmt.Sprintf("%s/decisions/%s", s.client.URLPrefix, decisionID)
req, err := s.client.NewRequestWithContext(ctx, http.MethodDelete, u, nil)
req, err := s.client.PrepareRequest(ctx, http.MethodDelete, u, nil)
if err != nil {
return nil, nil, err
}

View file

@ -16,7 +16,7 @@ type DecisionDeleteService service
func (d *DecisionDeleteService) Add(ctx context.Context, deletedDecisions *models.DecisionsDeleteRequest) (interface{}, *Response, error) {
u := fmt.Sprintf("%s/decisions/delete", d.client.URLPrefix)
req, err := d.client.NewRequestWithContext(ctx, http.MethodPost, u, &deletedDecisions)
req, err := d.client.PrepareRequest(ctx, http.MethodPost, u, &deletedDecisions)
if err != nil {
return nil, nil, fmt.Errorf("while building request: %w", err)
}

View file

@ -17,7 +17,7 @@ type HeartBeatService service
func (h *HeartBeatService) Ping(ctx context.Context) (bool, *Response, error) {
u := fmt.Sprintf("%s/heartbeat", h.client.URLPrefix)
req, err := h.client.NewRequestWithContext(ctx, http.MethodGet, u, nil)
req, err := h.client.PrepareRequest(ctx, http.MethodGet, u, nil)
if err != nil {
return false, nil, err
}

View file

@ -13,7 +13,7 @@ type MetricsService service
func (s *MetricsService) Add(ctx context.Context, metrics *models.Metrics) (interface{}, *Response, error) {
u := fmt.Sprintf("%s/metrics/", s.client.URLPrefix)
req, err := s.client.NewRequestWithContext(ctx, http.MethodPost, u, &metrics)
req, err := s.client.PrepareRequest(ctx, http.MethodPost, u, &metrics)
if err != nil {
return nil, nil, err
}

View file

@ -15,7 +15,7 @@ type SignalService service
func (s *SignalService) Add(ctx context.Context, signals *models.AddSignalsRequest) (interface{}, *Response, error) {
u := fmt.Sprintf("%s/signals", s.client.URLPrefix)
req, err := s.client.NewRequestWithContext(ctx, http.MethodPost, u, &signals)
req, err := s.client.PrepareRequest(ctx, http.MethodPost, u, &signals)
if err != nil {
return nil, nil, fmt.Errorf("while building request: %w", err)
}

View file

@ -13,7 +13,7 @@ type UsageMetricsService service
func (s *UsageMetricsService) Add(ctx context.Context, metrics *models.AllMetrics) (interface{}, *Response, error) {
u := fmt.Sprintf("%s/usage-metrics", s.client.URLPrefix)
req, err := s.client.NewRequestWithContext(ctx, http.MethodPost, u, &metrics)
req, err := s.client.PrepareRequest(ctx, http.MethodPost, u, &metrics)
if err != nil {
return nil, nil, err
}

View file

@ -115,13 +115,35 @@ func TestCheckInAllowlist(t *testing.T) {
require.NoError(t, err)
require.False(t, resp.Allowlisted)
// GET request, should return 200 and status in body
w = lapi.RecordResponse(t, ctx, http.MethodGet, "/v1/allowlists/check/2.3.4.0%2F24", emptyBody, passwordAuthType)
require.Equal(t, http.StatusOK, w.Code)
resp = models.CheckAllowlistResponse{}
err = json.Unmarshal(w.Body.Bytes(), &resp)
require.NoError(t, err)
require.False(t, resp.Allowlisted)
// HEAD request, should return 200
w = lapi.RecordResponse(t, ctx, http.MethodHead, "/v1/allowlists/check/1.2.3.4", emptyBody, passwordAuthType)
require.Equal(t, http.StatusOK, w.Code)
// HEAD request, should return 200
w = lapi.RecordResponse(t, ctx, http.MethodHead, "/v1/allowlists/check/1.2.3.0%2F24", emptyBody, passwordAuthType)
require.Equal(t, http.StatusOK, w.Code)
// HEAD request, should return 204
w = lapi.RecordResponse(t, ctx, http.MethodHead, "/v1/allowlists/check/2.3.4.5", emptyBody, passwordAuthType)
require.Equal(t, http.StatusNoContent, w.Code)
// HEAD request, should return 204
w = lapi.RecordResponse(t, ctx, http.MethodHead, "/v1/allowlists/check/2.3.4.5%2F24", emptyBody, passwordAuthType)
require.Equal(t, http.StatusNoContent, w.Code)
}

View file

@ -754,7 +754,13 @@ func (a *apic) UpdateAllowlists(ctx context.Context, allowlistsLinks []*modelsca
description = *link.Description
}
resp, err := defaultClient.GetClient().Get(*link.URL)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, *link.URL, http.NoBody)
if err != nil {
log.Errorf("while pulling allowlist: %s", err)
continue
}
resp, err := defaultClient.GetClient().Do(req)
if err != nil {
log.Errorf("while pulling allowlist: %s", err)
continue
@ -854,6 +860,10 @@ func (a *apic) ApplyApicWhitelists(ctx context.Context, decisions []*models.Deci
log.Errorf("while getting allowlists content: %s", err)
}
if a.whitelists != nil && (len(a.whitelists.Cidrs) > 0 || len(a.whitelists.Ips) > 0) {
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

@ -98,6 +98,8 @@ func (c *Controller) NewV1() error {
c.Router.GET("/health", gin.WrapF(serveHealth()))
c.Router.Use(v1.PrometheusMiddleware())
c.Router.HandleMethodNotAllowed = true
c.Router.UnescapePathValues = true
c.Router.UseRawPath = true
c.Router.NoRoute(func(ctx *gin.Context) {
ctx.AbortWithStatus(http.StatusNotFound)
})

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

@ -6,7 +6,7 @@ import (
log "github.com/sirupsen/logrus"
dbg "github.com/crowdsecurity/coraza/v3/debuglog"
dbg "github.com/corazawaf/coraza/v3/debuglog"
)
var DebugRules = map[int]bool{}

View file

@ -21,6 +21,8 @@ For those hashes, the value used was the one returned by our code (because we de
*/
func TestJA4H_A(t *testing.T) {
ctx := t.Context()
tests := []struct {
name string
request func() *http.Request
@ -29,7 +31,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.NewRequestWithContext(ctx, http.MethodGet, "http://example.com", http.NoBody)
return req
},
expectedResult: "ge11nn000000",
@ -37,7 +39,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.NewRequestWithContext(ctx, http.MethodGet, "http://example.com", http.NoBody)
req.Header.Set("Accept-Language", "en-US")
return req
},
@ -46,7 +48,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.NewRequestWithContext(ctx, 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 +58,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.NewRequestWithContext(ctx, http.MethodGet, "http://example.com", http.NoBody)
req.Header.Set("Accept-Language", "aksjdhaslkdhalkjsd")
return req
},
@ -65,7 +67,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.NewRequestWithContext(ctx, http.MethodGet, "http://example.com", http.NoBody)
req.Header.Set("Accept-Language", ",")
return req
},
@ -86,6 +88,9 @@ func TestJA4H_A(t *testing.T) {
func TestJA4H_B(t *testing.T) {
// This test is only for non-regression
// Because go does not keep headers order, we just want to make sure our code always process the headers in the same order
ctx := t.Context()
tests := []struct {
name string
request func() *http.Request
@ -94,7 +99,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.NewRequestWithContext(ctx, http.MethodGet, "http://example.com", http.NoBody)
return req
},
expectedResult: "e3b0c44298fc",
@ -102,7 +107,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.NewRequestWithContext(ctx, http.MethodGet, "http://example.com", http.NoBody)
req.Header.Set("X-Custom-Header", "some value")
return req
},
@ -111,7 +116,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.NewRequestWithContext(ctx, 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 +126,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.NewRequestWithContext(ctx, 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 +155,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 +163,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 +172,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 +182,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 +214,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 +222,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 +231,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 +241,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()
@ -260,6 +265,8 @@ func TestJA4H_D(t *testing.T) {
}
func TestJA4H(t *testing.T) {
ctx := t.Context()
tests := []struct {
name string
req func() *http.Request
@ -268,7 +275,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.NewRequestWithContext(ctx, http.MethodGet, "http://example.com", http.NoBody)
return req
},
expectedHash: "ge11nn000000_e3b0c44298fc_000000000000_000000000000",
@ -276,7 +283,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.NewRequestWithContext(ctx, http.MethodGet, "http://example.com", http.NoBody)
req.AddCookie(&http.Cookie{Name: "session", Value: "12345"})
return req
},
@ -285,7 +292,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.NewRequestWithContext(ctx, 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,

View file

@ -1,10 +1,10 @@
package appsec
import (
"github.com/crowdsecurity/coraza/v3"
"github.com/crowdsecurity/coraza/v3/experimental"
"github.com/crowdsecurity/coraza/v3/experimental/plugins/plugintypes"
"github.com/crowdsecurity/coraza/v3/types"
"github.com/corazawaf/coraza/v3"
"github.com/corazawaf/coraza/v3/experimental"
"github.com/corazawaf/coraza/v3/experimental/plugins/plugintypes"
"github.com/corazawaf/coraza/v3/types"
)
type ExtendedTransaction struct {
@ -91,3 +91,7 @@ func (t *ExtendedTransaction) MatchedRules() []types.MatchedRule {
func (t *ExtendedTransaction) ID() string {
return t.Tx.ID()
}
func (t *ExtendedTransaction) Close() error {
return t.Tx.Close()
}

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)
}
}

Some files were not shown because too many files have changed in this diff Show more