Check cscli preconditions with crowdsec-cli/require package (#2388)

This commit is contained in:
mmetc 2023-07-27 17:02:20 +02:00 committed by GitHub
parent a01ce18b98
commit 5cb7013575
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 260 additions and 105 deletions

View file

@ -25,6 +25,8 @@ import (
"github.com/crowdsecurity/crowdsec/pkg/database"
"github.com/crowdsecurity/crowdsec/pkg/models"
"github.com/crowdsecurity/crowdsec/pkg/types"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/require"
)
func DecisionsFromAlert(alert *models.Alert) string {
@ -525,8 +527,8 @@ func NewAlertsFlushCmd() *cobra.Command {
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error {
var err error
if err := csConfig.LoadAPIServer(); err != nil || csConfig.DisableAPI {
return fmt.Errorf("local API is disabled, please run this command on the local API machine")
if err := require.LAPI(csConfig); err != nil {
return err
}
dbClient, err = database.NewClient(csConfig.DbConfig)
if err != nil {

View file

@ -16,6 +16,8 @@ import (
middlewares "github.com/crowdsecurity/crowdsec/pkg/apiserver/middlewares/v1"
"github.com/crowdsecurity/crowdsec/pkg/database"
"github.com/crowdsecurity/crowdsec/pkg/types"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/require"
)
func getBouncers(out io.Writer, dbClient *database.Client) error {
@ -200,9 +202,10 @@ Note: This command requires database direct access, so is intended to be run on
DisableAutoGenTag: true,
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
var err error
if err := csConfig.LoadAPIServer(); err != nil || csConfig.DisableAPI {
return fmt.Errorf("local API is disabled, please run this command on the local API machine")
if err = require.LAPI(csConfig); err != nil {
return err
}
dbClient, err = database.NewClient(csConfig.DbConfig)
if err != nil {
return fmt.Errorf("unable to create new database client: %s", err)

View file

@ -19,6 +19,8 @@ import (
"github.com/crowdsecurity/crowdsec/pkg/fflag"
"github.com/crowdsecurity/crowdsec/pkg/models"
"github.com/crowdsecurity/crowdsec/pkg/types"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/require"
)
const CAPIBaseURL string = "https://api.crowdsec.net/"
@ -31,14 +33,12 @@ func NewCapiCmd() *cobra.Command {
Args: cobra.MinimumNArgs(1),
DisableAutoGenTag: true,
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
if err := csConfig.LoadAPIServer(); err != nil {
return fmt.Errorf("local API is disabled, please run this command on the local API machine: %w", err)
if err := require.LAPI(csConfig); err != nil {
return err
}
if csConfig.DisableAPI {
return nil
}
if csConfig.API.Server.OnlineClient == nil {
log.Fatalf("no configuration for Central API in '%s'", *csConfig.FilePath)
if err := require.CAPI(csConfig); err != nil {
return err
}
return nil
@ -134,10 +134,6 @@ func NewCapiStatusCmd() *cobra.Command {
Args: cobra.MinimumNArgs(0),
DisableAutoGenTag: true,
Run: func(cmd *cobra.Command, args []string) {
var err error
if csConfig.API.Server == nil {
log.Fatal("There is no configuration on 'api.server:'")
}
if csConfig.API.Server.OnlineClient == nil {
log.Fatalf("Please provide credentials for the Central API (CAPI) in '%s'", csConfig.API.Server.OnlineClient.CredentialsFilePath)
}

View file

@ -71,7 +71,7 @@ var configShowTemplate = `Global:
{{- end }}
{{- if .Crowdsec }}
Crowdsec:
Crowdsec{{if and .Crowdsec.Enable (not (ValueBool .Crowdsec.Enable))}} (disabled){{end}}:
- Acquisition File : {{.Crowdsec.AcquisitionFilePath}}
- Parsers routines : {{.Crowdsec.ParserRoutinesCount}}
{{- if .Crowdsec.AcquisitionDirPath }}
@ -97,7 +97,7 @@ API Client:
{{- end }}
{{- if .API.Server }}
Local API Server:
Local API Server{{if and .API.Server.Enable (not (ValueBool .API.Server.Enable))}} (disabled){{end}}:
- Listen URL : {{.API.Server.ListenURI}}
- Profile File : {{.API.Server.ProfilesPath}}
@ -194,7 +194,15 @@ func runConfigShow(cmd *cobra.Command, args []string) error {
switch csConfig.Cscli.Output {
case "human":
tmp, err := template.New("config").Parse(configShowTemplate)
// The tests on .Enable look funny because the option has a true default which has
// not been set yet (we don't really load the LAPI) and go templates don't dereference
// pointers in boolean tests. Prefix notation is the cherry on top.
funcs := template.FuncMap{
// can't use generics here
"ValueBool": func(b *bool) bool { return b!=nil && *b },
}
tmp, err := template.New("config").Funcs(funcs).Parse(configShowTemplate)
if err != nil {
return err
}

View file

@ -4,9 +4,7 @@ import (
"context"
"encoding/csv"
"encoding/json"
"errors"
"fmt"
"io/fs"
"net/url"
"os"
@ -24,6 +22,8 @@ import (
"github.com/crowdsecurity/crowdsec/pkg/cwhub"
"github.com/crowdsecurity/crowdsec/pkg/fflag"
"github.com/crowdsecurity/crowdsec/pkg/types"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/require"
)
func NewConsoleCmd() *cobra.Command {
@ -33,24 +33,14 @@ func NewConsoleCmd() *cobra.Command {
Args: cobra.MinimumNArgs(1),
DisableAutoGenTag: true,
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
if err := csConfig.LoadAPIServer(); err != nil || csConfig.DisableAPI {
var fdErr *fs.PathError
if errors.As(err, &fdErr) {
log.Fatalf("Unable to load Local API : %s", fdErr)
}
if err != nil {
log.Fatalf("Unable to load required Local API Configuration : %s", err)
}
log.Fatal("Local API is disabled, please run this command on the local API machine")
if err := require.LAPI(csConfig); err != nil {
return err
}
if csConfig.DisableAPI {
log.Fatal("Local API is disabled, please run this command on the local API machine")
if err := require.CAPI(csConfig); err != nil {
return err
}
if csConfig.API.Server.OnlineClient == nil {
log.Fatalf("No configuration for Central API (CAPI) in '%s'", *csConfig.FilePath)
}
if csConfig.API.Server.OnlineClient.Credentials == nil {
log.Fatal("You must configure Central API (CAPI) with `cscli capi register` before accessing console features.")
if err := require.Enrolled(csConfig); err != nil {
return err
}
return nil
},

View file

@ -17,6 +17,8 @@ import (
"github.com/spf13/cobra"
"github.com/crowdsecurity/crowdsec/pkg/metabase"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/require"
)
var (
@ -54,23 +56,23 @@ cscli dashboard start
cscli dashboard stop
cscli dashboard remove
`,
PersistentPreRun: func(cmd *cobra.Command, args []string) {
if err := metabase.TestAvailability(); err != nil {
log.Fatalf("%s", err)
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
if err := require.LAPI(csConfig); err != nil {
return err
}
if err := csConfig.LoadAPIServer(); err != nil || csConfig.DisableAPI {
log.Fatal("Local API is disabled, please run this command on the local API machine")
if err := metabase.TestAvailability(); err != nil {
return err
}
metabaseConfigFolderPath := filepath.Join(csConfig.ConfigPaths.ConfigDir, metabaseConfigFolder)
metabaseConfigPath = filepath.Join(metabaseConfigFolderPath, metabaseConfigFile)
if err := os.MkdirAll(metabaseConfigFolderPath, os.ModePerm); err != nil {
log.Fatal(err)
return err
}
if err := csConfig.LoadDBConfig(); err != nil {
log.Errorf("This command requires direct database access (must be run on the local API machine)")
log.Fatal(err)
if err := require.DB(csConfig); err != nil {
return err
}
/*
@ -84,6 +86,7 @@ cscli dashboard remove
metabaseContainerID = oldContainerID
}
}
return nil
},
}

View file

@ -26,6 +26,8 @@ import (
"github.com/crowdsecurity/crowdsec/pkg/database"
"github.com/crowdsecurity/crowdsec/pkg/database/ent"
"github.com/crowdsecurity/crowdsec/pkg/types"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/require"
)
var (
@ -411,11 +413,8 @@ Note: This command requires database direct access, so is intended to be run on
DisableAutoGenTag: true,
Aliases: []string{"machine"},
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
if err := csConfig.LoadAPIServer(); err != nil || csConfig.DisableAPI {
if err != nil {
log.Errorf("local api : %s", err)
}
return fmt.Errorf("local API is disabled, please run this command on the local API machine")
if err := require.LAPI(csConfig); err != nil {
return err
}
return nil

View file

@ -25,6 +25,8 @@ import (
"github.com/crowdsecurity/crowdsec/pkg/csconfig"
"github.com/crowdsecurity/crowdsec/pkg/csplugin"
"github.com/crowdsecurity/crowdsec/pkg/csprofiles"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/require"
)
type NotificationsCfg struct {
@ -41,16 +43,18 @@ func NewNotificationsCmd() *cobra.Command {
Args: cobra.MinimumNArgs(1),
Aliases: []string{"notifications", "notification"},
DisableAutoGenTag: true,
PersistentPreRun: func(cmd *cobra.Command, args []string) {
var (
err error
)
if err = csConfig.API.Server.LoadProfiles(); err != nil {
log.Fatal(err)
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
if err := require.LAPI(csConfig); err != nil {
return err
}
if csConfig.ConfigPaths.NotificationDir == "" {
log.Fatalf("config_paths.notification_dir is not set in crowdsec config")
if err := require.Profiles(csConfig); err != nil {
return err
}
if err := require.Notifications(csConfig); err != nil {
return err
}
return nil
},
}

View file

@ -1,7 +1,6 @@
package main
import (
"fmt"
"time"
log "github.com/sirupsen/logrus"
@ -12,6 +11,8 @@ import (
"github.com/crowdsecurity/crowdsec/pkg/apiserver"
"github.com/crowdsecurity/crowdsec/pkg/database"
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/require"
)
func NewPapiCmd() *cobra.Command {
@ -21,14 +22,14 @@ func NewPapiCmd() *cobra.Command {
Args: cobra.MinimumNArgs(1),
DisableAutoGenTag: true,
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
if err := csConfig.LoadAPIServer(); err != nil || csConfig.DisableAPI {
return fmt.Errorf("Local API is disabled, please run this command on the local API machine: %w", err)
if err := require.LAPI(csConfig); err != nil {
return err
}
if csConfig.API.Server.OnlineClient == nil {
log.Fatalf("no configuration for Central API in '%s'", *csConfig.FilePath)
if err := require.CAPI(csConfig); err != nil {
return err
}
if csConfig.API.Server.OnlineClient.Credentials.PapiURL == "" {
log.Fatalf("no PAPI URL in configuration")
if err := require.PAPI(csConfig); err != nil {
return err
}
return nil
},

View file

@ -0,0 +1,85 @@
package require
import (
"fmt"
"github.com/crowdsecurity/crowdsec/pkg/csconfig"
)
func LAPI(c *csconfig.Config) error {
if err := c.LoadAPIServer(); err != nil {
return fmt.Errorf("failed to load Local API: %w", err)
}
if c.DisableAPI {
return fmt.Errorf("local API is disabled -- this command must be run on the local API machine")
}
return nil
}
func CAPI(c *csconfig.Config) error {
if c.API.Server.OnlineClient == nil {
return fmt.Errorf("no configuration for Central API (CAPI) in '%s'", *c.FilePath)
}
return nil
}
func PAPI(c *csconfig.Config) error {
if err := LAPI(c); err != nil {
return err
}
if err := CAPI(c); err != nil {
return err
}
if c.API.Server.OnlineClient.Credentials.PapiURL == "" {
return fmt.Errorf("no PAPI URL in configuration")
}
return nil
}
func Enrolled(c *csconfig.Config) error {
if err := CAPI(c); err != nil {
return err
}
if c.API.Server.OnlineClient.Credentials == nil {
return fmt.Errorf("the Central API (CAPI) must be configured with 'cscli capi register'")
}
return nil
}
func DB(c *csconfig.Config) error {
if err := c.LoadDBConfig(); err != nil {
return fmt.Errorf("this command requires direct database access (must be run on the local API machine): %w", err)
}
return nil
}
func Profiles(c *csconfig.Config) error {
if err := LAPI(c); err != nil {
return err
}
if err := c.API.Server.LoadProfiles(); err != nil {
return fmt.Errorf("while loading profiles: %w", err)
}
return nil
}
func Notifications(c *csconfig.Config) error {
if err := LAPI(c); err != nil {
return err
}
if c.ConfigPaths.NotificationDir == "" {
return fmt.Errorf("config_paths.notification_dir is not set in crowdsec config")
}
return nil
}

View file

@ -249,13 +249,13 @@ func LoadConfig(configFile string, disableAgent bool, disableAPI bool, quiet boo
return nil, err
}
if !flags.DisableAgent {
if !cConfig.DisableAgent {
if err := cConfig.LoadCrowdsec(); err != nil {
return nil, err
}
}
if !flags.DisableAPI {
if !cConfig.DisableAPI {
if err := cConfig.LoadAPIServer(); err != nil {
return nil, err
}
@ -290,7 +290,7 @@ func LoadConfig(configFile string, disableAgent bool, disableAPI bool, quiet boo
cConfig.API.Server.OnlineClient = nil
}
/*if the api is disabled as well, just read file and exit, don't daemonize*/
if flags.DisableAPI {
if cConfig.DisableAPI {
cConfig.Common.Daemonize = false
}
log.Infof("single file mode : log_media=%s daemonize=%t", cConfig.Common.LogMedia, cConfig.Common.Daemonize)

View file

@ -243,7 +243,7 @@ if istrue "$DISABLE_ONLINE_API"; then
fi
# registration to online API for signal push
if isfalse "$DISABLE_ONLINE_API" ; then
if isfalse "$DISABLE_LOCAL_API" && isfalse "$DISABLE_ONLINE_API" ; then
CONFIG_DIR=$(conf_get '.config_paths.config_dir')
export CONFIG_DIR
config_exists=$(conf_get '.api.server.online_client | has("credentials_path")')
@ -255,7 +255,7 @@ if isfalse "$DISABLE_ONLINE_API" ; then
fi
# Enroll instance if enroll key is provided
if isfalse "$DISABLE_ONLINE_API" && [ "$ENROLL_KEY" != "" ]; then
if isfalse "$DISABLE_LOCAL_API" && isfalse "$DISABLE_ONLINE_API" && [ "$ENROLL_KEY" != "" ]; then
enroll_args=""
if [ "$ENROLL_INSTANCE_NAME" != "" ]; then
enroll_args="--name $ENROLL_INSTANCE_NAME"
@ -278,8 +278,7 @@ if [ "$GID" != "" ]; then
fi
fi
# XXX only with LAPI
if istrue "$USE_TLS"; then
if isfalse "$DISABLE_LOCAL_API" && istrue "$USE_TLS"; then
agents_allowed_yaml=$(csv2yaml "$AGENTS_ALLOWED_OU")
export agents_allowed_yaml
bouncers_allowed_yaml=$(csv2yaml "$BOUNCERS_ALLOWED_OU")
@ -358,7 +357,7 @@ shopt -s nullglob extglob
for BOUNCER in /run/secrets/@(bouncer_key|BOUNCER_KEY)* ; do
KEY=$(cat "${BOUNCER}")
NAME=$(echo "${BOUNCER}" | awk -F "/" '{printf $NF}' | cut -d_ -f2-)
if [[ -n $KEY ]] && [[ -n $NAME ]]; then
if [[ -n $KEY ]] && [[ -n $NAME ]]; then
register_bouncer "$NAME" "$KEY"
fi
done
@ -369,6 +368,12 @@ shopt -u nullglob extglob
conf_set_if "$CAPI_WHITELISTS_PATH" '.api.server.capi_whitelists_path = strenv(CAPI_WHITELISTS_PATH)'
conf_set_if "$METRICS_PORT" '.prometheus.listen_port=env(METRICS_PORT)'
if istrue "$DISABLE_LOCAL_API"; then
conf_set '.api.server.enable=false'
else
conf_set '.api.server.enable=true'
fi
ARGS=""
if [ "$CONFIG_FILE" != "" ]; then
ARGS="-c $CONFIG_FILE"
@ -390,10 +395,6 @@ if istrue "$DISABLE_AGENT"; then
ARGS="$ARGS -no-cs"
fi
if istrue "$DISABLE_LOCAL_API"; then
ARGS="$ARGS -no-api"
fi
if istrue "$LEVEL_TRACE"; then
ARGS="$ARGS -trace"
fi

View file

@ -234,6 +234,21 @@ func (c *Config) LoadAPIServer() error {
return nil
}
if c.API.Server.Enable == nil {
// if the option is not present, it is enabled by default
c.API.Server.Enable = ptr.Of(true)
}
if !*c.API.Server.Enable {
log.Warning("crowdsec local API is disabled because 'enable' is set to false")
c.DisableAPI = true
return nil
}
if c.DisableAPI {
return nil
}
//inherit log level from common, then api->server
var logLevel log.Level
if c.API.Server.LogLevel != nil {
@ -268,21 +283,6 @@ func (c *Config) LoadAPIServer() error {
log.Infof("loaded capi whitelist from %s: %d IPs, %d CIDRs", c.API.Server.CapiWhitelistsPath, len(c.API.Server.CapiWhitelists.Ips), len(c.API.Server.CapiWhitelists.Cidrs))
}
if c.API.Server.Enable == nil {
// if the option is not present, it is enabled by default
c.API.Server.Enable = ptr.Of(true)
}
if !*c.API.Server.Enable {
log.Warning("crowdsec local API is disabled because 'enable' is set to false")
c.DisableAPI = true
return nil
}
if c.DisableAPI {
return nil
}
if err := c.LoadCommon(); err != nil {
return fmt.Errorf("loading common configuration: %s", err)
}

View file

@ -234,6 +234,7 @@ func TestLoadAPIServer(t *testing.T) {
DisableAPI: false,
},
expected: &LocalApiServerCfg{
Enable: ptr.Of(true),
PapiLogLevel: &logLevel,
},
expectedErr: "no database configuration provided",

View file

@ -45,16 +45,23 @@ teardown() {
config_disable_lapi
rune -1 cscli capi status
assert_stderr --partial "crowdsec local API is disabled"
assert_stderr --partial "There is no configuration on 'api.server:'"
assert_stderr --partial "local API is disabled -- this command must be run on the local API machine"
}
@test "cscli config show -o human" {
config_disable_lapi
@test "no lapi: cscli config show -o human" {
config_set '.api.server.enable=false'
rune -0 cscli config show -o human
assert_output --partial "Global:"
assert_output --partial "Crowdsec:"
assert_output --partial "cscli:"
refute_output --partial "Local API Server:"
assert_output --partial "Local API Server (disabled):"
config_set 'del(.api.server)'
rune -0 cscli config show -o human
assert_output --partial "Global:"
assert_output --partial "Crowdsec:"
assert_output --partial "cscli:"
refute_output --partial "Local API Server"
}
@test "cscli config backup" {
@ -73,7 +80,7 @@ teardown() {
config_disable_lapi
./instance-crowdsec start || true
rune -1 cscli machines list
assert_stderr --partial "local API is disabled, please run this command on the local API machine"
assert_stderr --partial "local API is disabled -- this command must be run on the local API machine"
}
@test "cscli metrics" {
@ -85,5 +92,5 @@ teardown() {
assert_output --partial "/v1/watchers/login"
assert_stderr --partial "crowdsec local API is disabled"
assert_stderr --partial "local API is disabled, please run this command on the local API machine"
assert_stderr --partial "local API is disabled -- this command must be run on the local API machine"
}

View file

@ -40,13 +40,19 @@ teardown() {
}
@test "no agent: cscli config show" {
config_disable_agent
config_set '.crowdsec_service.enable=false'
rune -0 cscli config show -o human
assert_output --partial "Global:"
assert_output --partial "cscli:"
assert_output --partial "Local API Server:"
assert_output --partial "Crowdsec (disabled):"
refute_output --partial "Crowdsec:"
config_set 'del(.crowdsec_service)'
rune -0 cscli config show -o human
assert_output --partial "Global:"
assert_output --partial "cscli:"
assert_output --partial "Local API Server:"
refute_output --partial "Crowdsec"
}
@test "no agent: cscli config backup" {

View file

@ -60,5 +60,11 @@ setup() {
ONLINE_API_CREDENTIALS_YAML="$(config_get '.api.server.online_client.credentials_path')"
rm "${ONLINE_API_CREDENTIALS_YAML}"
rune -1 cscli capi status
assert_stderr --partial "local API is disabled, please run this command on the local API machine: loading online client credentials: failed to read api server credentials configuration file '${ONLINE_API_CREDENTIALS_YAML}': open ${ONLINE_API_CREDENTIALS_YAML}: no such file or directory"
assert_stderr --partial "failed to load Local API: loading online client credentials: failed to read api server credentials configuration file '${ONLINE_API_CREDENTIALS_YAML}': open ${ONLINE_API_CREDENTIALS_YAML}: no such file or directory"
}
@test "capi register must be run from lapi" {
config_disable_lapi
rune -1 cscli capi register --schmilblick githubciXXXXXXXXXXXXXXXXXXXXXXXX
assert_stderr --partial "local API is disabled -- this command must be run on the local API machine"
}

View file

@ -41,7 +41,7 @@ teardown() {
config_disable_capi
./instance-crowdsec start
rune -1 cscli capi status
assert_stderr --partial "no configuration for Central API in "
assert_stderr --partial "no configuration for Central API (CAPI) in "
}
@test "no capi: cscli config show" {

View file

@ -311,7 +311,7 @@ update-notifier-motd.timer enabled enabled
@test "cscli setup detect (process)" {
# This is harder to mock, because gopsutil requires proc/ to be a mount
# point. So we pick a process that exists for sure.
expected_process=$(basename "$SHELL")
expected_process=cscli
cat <<-EOT >"${DETECT_YAML}"
version: 1.0

View file

@ -0,0 +1,39 @@
#!/usr/bin/env bats
# vim: ft=bats:list:ts=8:sts=4:sw=4:et:ai:si:
set -u
setup_file() {
load "../lib/setup_file.sh"
}
teardown_file() {
load "../lib/teardown_file.sh"
}
setup() {
load "../lib/setup.sh"
load "../lib/bats-file/load.bash"
./instance-data load
./instance-crowdsec start
}
teardown() {
cd "$TEST_DIR" || exit 1
./instance-crowdsec stop
}
#----------
@test "cscli notifications list" {
rune -0 cscli notifications list
assert_output --partial "Name"
assert_output --partial "Type"
assert_output --partial "Profile name"
}
@test "cscli notifications must be run from lapi" {
config_disable_lapi
rune -1 cscli notifications list
assert_stderr --partial "local API is disabled -- this command must be run on the local API machine"
}

View file

@ -67,7 +67,9 @@ config_set() {
export -f config_set
config_disable_agent() {
config_set 'del(.crowdsec_service)'
config_set '.crowdsec_service.enable=false'
# this should be equivalent to:
# config_set 'del(.crowdsec_service)'
}
export -f config_disable_agent
@ -77,7 +79,9 @@ config_log_stderr() {
export -f config_log_stderr
config_disable_lapi() {
config_set 'del(.api.server)'
config_set '.api.server.enable=false'
# this should be equivalent to:
# config_set 'del(.api.server)'
}
export -f config_disable_lapi