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

This commit is contained in:
mmetc 2023-01-23 10:29:29 +01:00 committed by GitHub
parent ce60c7b056
commit 47cc60bda9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 193 additions and 1 deletions

View file

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

75
pkg/csstring/expand.go Normal file
View file

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

View file

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

View file

@ -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"
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$'
}