Bläddra i källkod

allow use of literal $ in config.yaml (#2012)

mmetc 2 år sedan
förälder
incheckning
47cc60bda9
4 ändrade filer med 193 tillägg och 1 borttagningar
  1. 2 1
      pkg/csconfig/config.go
  2. 75 0
      pkg/csstring/expand.go
  3. 98 0
      pkg/csstring/expand_test.go
  4. 18 0
      tests/bats/01_base.bats

+ 2 - 1
pkg/csconfig/config.go

@@ -9,6 +9,7 @@ import (
 	log "github.com/sirupsen/logrus"
 	log "github.com/sirupsen/logrus"
 	"gopkg.in/yaml.v2"
 	"gopkg.in/yaml.v2"
 
 
+	"github.com/crowdsecurity/crowdsec/pkg/csstring"
 	"github.com/crowdsecurity/crowdsec/pkg/types"
 	"github.com/crowdsecurity/crowdsec/pkg/types"
 	"github.com/crowdsecurity/crowdsec/pkg/yamlpatch"
 	"github.com/crowdsecurity/crowdsec/pkg/yamlpatch"
 )
 )
@@ -53,7 +54,7 @@ func NewConfig(configFile string, disableAgent bool, disableAPI bool, quiet bool
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
-	configData := os.ExpandEnv(string(fcontent))
+	configData := csstring.StrictExpand(string(fcontent), os.LookupEnv)
 	cfg := Config{
 	cfg := Config{
 		FilePath:     &configFile,
 		FilePath:     &configFile,
 		DisableAgent: disableAgent,
 		DisableAgent: disableAgent,

+ 75 - 0
pkg/csstring/expand.go

@@ -0,0 +1,75 @@
+package csstring
+
+func seekClosingBracket(s string, i int) int {
+	for ; i < len(s); i++ {
+		if s[i] == '}' {
+			return i
+		}
+	}
+
+	return -1
+}
+
+func seekEndVarname(s string, i int) int {
+	// envvar names are more strict but this is good enough
+	for ; i < len(s); i++ {
+		if (s[i] < 'a' || s[i] > 'z') && (s[i] < 'A' || s[i] > 'Z') && (s[i] < '0' || s[i] > '9') && s[i] != '_' {
+			break
+		}
+	}
+
+	return i
+}
+
+func replaceVarBracket(s string, i int, mapping func(string) (string, bool)) string {
+	j := seekClosingBracket(s, i+2)
+	if j < 0 {
+		return s
+	}
+
+	if j < len(s) {
+		varName := s[i+2 : j]
+		if val, ok := mapping(varName); ok {
+			s = s[:i] + val + s[j+1:]
+		}
+	}
+
+	return s
+}
+
+func replaceVar(s string, i int, mapping func(string) (string, bool)) string {
+	if s[i+1] == '{' {
+		return replaceVarBracket(s, i, mapping)
+	}
+
+	j := seekEndVarname(s, i+1)
+	if j < 0 {
+		return s
+	}
+
+	if j > i+1 {
+		varName := s[i+1 : j]
+		if val, ok := mapping(varName); ok {
+			s = s[:i] + val + s[j:]
+		}
+	}
+
+	return s
+}
+
+// StrictExpand replaces ${var} or $var in the string according to the mapping
+// function, like os.Expand. The difference is that the mapping function
+// returns a boolean indicating whether the variable was found.
+// If the variable was not found, the string is not modified.
+//
+// Whereas os.ExpandEnv uses os.Getenv, here we can use os.LookupEnv
+// to distinguish between an empty variable and an undefined one.
+func StrictExpand(s string, mapping func(string) (string, bool)) string {
+	for i := 0; i < len(s); i++ {
+		if s[i] == '$' {
+			s = replaceVar(s, i, mapping)
+		}
+	}
+
+	return s
+}

+ 98 - 0
pkg/csstring/expand_test.go

@@ -0,0 +1,98 @@
+package csstring_test
+
+import (
+	"fmt"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+
+	"github.com/crowdsecurity/crowdsec/pkg/csstring"
+)
+
+func TestStrictExpand(t *testing.T) {
+	t.Parallel()
+
+	testenv := func(key string) (string, bool) {
+		switch key {
+		case "USER":
+			return "testuser", true
+		case "HOME":
+			return "/home/testuser", true
+		case "empty":
+			return "", true
+		default:
+			return "", false
+		}
+	}
+
+	home, _ := testenv("HOME")
+	user, _ := testenv("USER")
+
+	tests := []struct {
+		input    string
+		expected string
+	}{
+		{
+			input:    "$HOME",
+			expected: home,
+		},
+		{
+			input:    "${USER}",
+			expected: user,
+		},
+		{
+			input:    "Hello, $USER!",
+			expected: fmt.Sprintf("Hello, %s!", user),
+		},
+		{
+			input:    "My home directory is ${HOME}",
+			expected: fmt.Sprintf("My home directory is %s", home),
+		},
+		{
+			input:    "This is a $SINGLE_VAR string with ${HOME}",
+			expected: fmt.Sprintf("This is a $SINGLE_VAR string with %s", home),
+		},
+		{
+			input:    "This is a $SINGLE_VAR string with $HOME",
+			expected: fmt.Sprintf("This is a $SINGLE_VAR string with %s", home),
+		},
+		{
+			input:    "This variable does not exist: $NON_EXISTENT_VAR",
+			expected: "This variable does not exist: $NON_EXISTENT_VAR",
+		},
+		{
+			input:    "This is a $MULTI_VAR string with ${HOME} and ${USER}",
+			expected: fmt.Sprintf("This is a $MULTI_VAR string with %s and %s", home, user),
+		},
+		{
+			input:    "This is a ${MULTI_VAR} string with $HOME and $USER",
+			expected: fmt.Sprintf("This is a ${MULTI_VAR} string with %s and %s", home, user),
+		},
+		{
+			input:    "This is a plain string with no variables",
+			expected: "This is a plain string with no variables",
+		},
+		{
+			input:    "$empty",
+			expected: "",
+		},
+		{
+			input:    "",
+			expected: "",
+		},
+		{
+			input:    "$USER:$empty:$HOME",
+			expected: fmt.Sprintf("%s::%s", user, home),
+		},
+	}
+
+	for _, tc := range tests {
+		tc := tc
+		t.Run(tc.input, func(t *testing.T) {
+			t.Parallel()
+
+			output := csstring.StrictExpand(tc.input, testenv)
+			assert.Equal(t, tc.expected, output)
+		})
+	}
+}

+ 18 - 0
tests/bats/01_base.bats

@@ -279,3 +279,21 @@ declare stderr
     rune -0 cscli explain --log "Sep 19 18:33:22 scw-d95986 sshd[24347]: pam_unix(sshd:auth): authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=1.2.3.4" --type syslog --crowdsec "$CROWDSEC"
     rune -0 cscli explain --log "Sep 19 18:33:22 scw-d95986 sshd[24347]: pam_unix(sshd:auth): authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=1.2.3.4" --type syslog --crowdsec "$CROWDSEC"
     assert_output - <"$BATS_TEST_DIRNAME"/testdata/explain/explain-log.txt
     assert_output - <"$BATS_TEST_DIRNAME"/testdata/explain/explain-log.txt
 }
 }
+
+@test "Allow variable expansion and literal \$ characters in passwords' {
+    export DB_PASSWORD='P@ssw0rd'
+    # shellcheck disable=SC2016
+    config_set '.db_config.password="$DB_PASSWORD"'
+    rune -0 cscli config show --key Config.DbConfig.Password
+    assert_output 'P@ssw0rd'
+
+    # shellcheck disable=SC2016
+    config_set '.db_config.password="$3cureP@ssw0rd"'
+    rune -0 cscli config show --key Config.DbConfig.Password
+    # shellcheck disable=SC2016
+    assert_output '$3cureP@ssw0rd'
+
+    config_set '.db_config.password="P@ssw0rd$"'
+    rune -0 cscli config show --key Config.DbConfig.Password
+    assert_output 'P@ssw0rd$'
+}