diff --git a/pkg/csconfig/api.go b/pkg/csconfig/api.go index c07d9be56..e4de6696d 100644 --- a/pkg/csconfig/api.go +++ b/pkg/csconfig/api.go @@ -3,7 +3,9 @@ package csconfig import ( "crypto/tls" "crypto/x509" + "errors" "fmt" + "io" "net" "os" "strings" @@ -331,43 +333,59 @@ type capiWhitelists struct { Cidrs []string `yaml:"cidrs"` } +func parseCapiWhitelists(fd io.Reader) (*CapiWhitelist, error) { + fromCfg := capiWhitelists{} + + decoder := yaml.NewDecoder(fd) + if err := decoder.Decode(&fromCfg); err != nil { + if errors.Is(err, io.EOF) { + return nil, fmt.Errorf("empty file") + } + return nil, err + } + ret := &CapiWhitelist{ + Ips: make([]net.IP, len(fromCfg.Ips)), + Cidrs: make([]*net.IPNet, len(fromCfg.Cidrs)), + } + for idx, v := range fromCfg.Ips { + ip := net.ParseIP(v) + if ip == nil { + return nil, fmt.Errorf("invalid IP address: %s", v) + } + ret.Ips[idx] = ip + } + for idx, v := range fromCfg.Cidrs { + _, tnet, err := net.ParseCIDR(v) + if err != nil { + return nil, err + } + ret.Cidrs[idx] = tnet + } + + return ret, nil +} + func (s *LocalApiServerCfg) LoadCapiWhitelists() error { if s.CapiWhitelistsPath == "" { return nil } + if _, err := os.Stat(s.CapiWhitelistsPath); os.IsNotExist(err) { return fmt.Errorf("capi whitelist file '%s' does not exist", s.CapiWhitelistsPath) } + fd, err := os.Open(s.CapiWhitelistsPath) if err != nil { - return fmt.Errorf("unable to open capi whitelist file '%s': %s", s.CapiWhitelistsPath, err) + return fmt.Errorf("while opening capi whitelist file: %s", err) } - var fromCfg capiWhitelists - defer fd.Close() - decoder := yaml.NewDecoder(fd) - if err := decoder.Decode(&fromCfg); err != nil { - return fmt.Errorf("while parsing capi whitelist file '%s': %s", s.CapiWhitelistsPath, err) - } - s.CapiWhitelists = &CapiWhitelist{ - Ips: make([]net.IP, len(fromCfg.Ips)), - Cidrs: make([]*net.IPNet, len(fromCfg.Cidrs)), - } - for _, v := range fromCfg.Ips { - ip := net.ParseIP(v) - if ip == nil { - return fmt.Errorf("unable to parse ip whitelist '%s'", v) - } - s.CapiWhitelists.Ips = append(s.CapiWhitelists.Ips, ip) - } - for _, v := range fromCfg.Cidrs { - _, tnet, err := net.ParseCIDR(v) - if err != nil { - return fmt.Errorf("unable to parse cidr whitelist '%s' : %v", v, err) - } - s.CapiWhitelists.Cidrs = append(s.CapiWhitelists.Cidrs, tnet) + + s.CapiWhitelists, err = parseCapiWhitelists(fd) + if err != nil { + return fmt.Errorf("while parsing capi whitelist file '%s': %w", s.CapiWhitelistsPath, err) } + return nil } diff --git a/pkg/csconfig/api_test.go b/pkg/csconfig/api_test.go index de196123f..cde6368d5 100644 --- a/pkg/csconfig/api_test.go +++ b/pkg/csconfig/api_test.go @@ -2,6 +2,7 @@ package csconfig import ( "fmt" + "net" "os" "path/filepath" "strings" @@ -234,7 +235,7 @@ func TestLoadAPIServer(t *testing.T) { DisableAPI: false, }, expected: &LocalApiServerCfg{ - Enable: ptr.Of(true), + Enable: ptr.Of(true), PapiLogLevel: &logLevel, }, expectedErr: "no database configuration provided", @@ -259,3 +260,67 @@ func TestLoadAPIServer(t *testing.T) { } } } + +func mustParseCIDRNet(s string) *net.IPNet { + _, ipNet, err := net.ParseCIDR(s) + if err != nil { + panic(fmt.Sprintf("MustParseCIDR: parsing %q: %v", s, err)) + } + return ipNet +} + +func TestParseCapiWhitelists(t *testing.T) { + tests := []struct { + name string + input string + expected *CapiWhitelist + expectedErr string + }{ + { + name: "empty file", + input: "", + expected: &CapiWhitelist{ + Ips: []net.IP{}, + Cidrs: []*net.IPNet{}, + }, + expectedErr: "empty file", + }, + { + name: "empty ip and cidr", + input: `{"ips": [], "cidrs": []}`, + expected: &CapiWhitelist{ + Ips: []net.IP{}, + Cidrs: []*net.IPNet{}, + }, + }, + { + name: "some ip", + input: `{"ips": ["1.2.3.4"]}`, + expected: &CapiWhitelist{ + Ips: []net.IP{net.IPv4(1, 2, 3, 4)}, + Cidrs: []*net.IPNet{}, + }, + }, + { + name: "some cidr", + input: `{"cidrs": ["1.2.3.0/24"]}`, + expected: &CapiWhitelist{ + Ips: []net.IP{}, + Cidrs: []*net.IPNet{mustParseCIDRNet("1.2.3.0/24")}, + }, + }, + } + + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + wl, err := parseCapiWhitelists(strings.NewReader(tc.input)) + cstest.RequireErrorContains(t, err, tc.expectedErr) + if tc.expectedErr != "" { + return + } + + assert.Equal(t, tc.expected, wl) + }) + } +} diff --git a/test/bats/13_capi_whitelists.bats b/test/bats/13_capi_whitelists.bats new file mode 100644 index 000000000..491de6498 --- /dev/null +++ b/test/bats/13_capi_whitelists.bats @@ -0,0 +1,76 @@ +#!/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" + CONFIG_DIR=$(dirname "$CONFIG_YAML") + CAPI_WHITELISTS_YAML="$CONFIG_DIR/capi-whitelists.yaml" + export CAPI_WHITELISTS_YAML +} + +teardown_file() { + load "../lib/teardown_file.sh" +} + +setup() { + load "../lib/setup.sh" + load "../lib/bats-file/load.bash" + ./instance-data load + config_set '.api.server.capi_whitelists_path=strenv(CAPI_WHITELISTS_YAML)' +} + +teardown() { + ./instance-crowdsec stop +} + +#---------- + +@test "capi_whitelists: file missing" { + rune -1 timeout 1s "${CROWDSEC}" + assert_stderr --partial "capi whitelist file '$CAPI_WHITELISTS_YAML' does not exist" +} + +@test "capi_whitelists: error on open" { + echo > "$CAPI_WHITELISTS_YAML" + chmod 000 "$CAPI_WHITELISTS_YAML" + rune -1 timeout 1s "${CROWDSEC}" + assert_stderr --partial "while opening capi whitelist file: open $CAPI_WHITELISTS_YAML: permission denied" +} + +@test "capi_whitelists: empty file" { + echo > "$CAPI_WHITELISTS_YAML" + rune -1 timeout 1s "${CROWDSEC}" + assert_stderr --partial "while parsing capi whitelist file '$CAPI_WHITELISTS_YAML': empty file" +} + +@test "capi_whitelists: empty lists" { + echo '{"ips": [], "cidrs": []}' > "$CAPI_WHITELISTS_YAML" + rune -124 timeout 1s "${CROWDSEC}" +} + +@test "capi_whitelists: bad ip" { + echo '{"ips": ["blahblah"], "cidrs": []}' > "$CAPI_WHITELISTS_YAML" + rune -1 timeout 1s "${CROWDSEC}" + assert_stderr --partial "while parsing capi whitelist file '$CAPI_WHITELISTS_YAML': invalid IP address: blahblah" +} + +@test "capi_whitelists: bad cidr" { + echo '{"ips": [], "cidrs": ["blahblah"]}' > "$CAPI_WHITELISTS_YAML" + rune -1 timeout 1s "${CROWDSEC}" + assert_stderr --partial "while parsing capi whitelist file '$CAPI_WHITELISTS_YAML': invalid CIDR address: blahblah" +} + +@test "capi_whitelists: file with ip and cidr values" { + cat <<-EOT > "$CAPI_WHITELISTS_YAML" + ips: + - 1.2.3.4 + - 2.3.4.5 + cidrs: + - 1.2.3.0/24 + EOT + + config_set '.common.log_level="trace"' + rune -0 ./instance-crowdsec start +}