diff --git a/azure-pipelines.yml b/azure-pipelines.yml index a26fc2a..f176b52 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -9,7 +9,7 @@ jobs: vmImage: ubuntu-latest go: container: golang - versions: [ 1.13, 1.12, 1.11 ] + versions: [ 1.13, 1.12 ] targets: GOARCH: [ amd64, 386 ] @@ -17,12 +17,11 @@ jobs: parameters: name: Alpine_Linux vmImage: ubuntu-latest - continueOnError: true go: container: julio/azure-pipelines-golang versions: [ 1.13-alpine, 1.12-alpine ] targets: - GOARCH: [ amd64, 386 ] + GOARCH: [ amd64 ] - template: tools/azure-pipelines/test-go-versions.yml parameters: @@ -30,7 +29,7 @@ jobs: vmImage: windows-latest go: container: golang - versions: [1.13, 1.12, 1.11] + versions: [ 1.13, 1.12 ] targets: GOARCH: [ amd64, 386 ] @@ -41,7 +40,7 @@ jobs: - script: | go env clang -v - go test -v -x ./... + go test -v ./... - job: MacOS_386 pool: @@ -50,4 +49,4 @@ jobs: - script: | go env clang -v - env GOARCH=386 go test -v -x ./... \ No newline at end of file + env GOARCH=386 go test -v ./... \ No newline at end of file diff --git a/go.mod b/go.mod index 0892154..b4f011b 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/sqreen/go-libsqreen -go 1.10 +go 1.12 require ( github.com/pkg/errors v0.8.1 diff --git a/tools/azure-pipelines/test-go-versions.yml b/tools/azure-pipelines/test-go-versions.yml index 9d79473..2b3b944 100644 --- a/tools/azure-pipelines/test-go-versions.yml +++ b/tools/azure-pipelines/test-go-versions.yml @@ -4,7 +4,7 @@ parameters: continueOnError: false go: container: golang - versions: [ 1.13, 1.12, 1.11 ] + versions: [ 1.13, 1.12 ] targets: GOARCH: [ amd64 ] @@ -26,4 +26,4 @@ jobs: GOARCH: $(GOARCH) steps: - script: go env - - script: go test -v -x ./... + - script: go test -v ./... diff --git a/waf/internal/bindings/logs.go b/waf/internal/bindings/logs.go deleted file mode 100644 index e14aee6..0000000 --- a/waf/internal/bindings/logs.go +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright (c) 2016 - 2019 Sqreen. All Rights Reserved. -// Please refer to our terms for more information: -// https://www.sqreen.io/terms.html - -// +build !sqreen_nowaf -// +build !windows -// +build amd64 -// +build linux darwin - -package bindings - -// #include "waf.h" -// extern void goOnLogMessage(PW_LOG_LEVEL level, const char *function, const char *file, int line, const char *message, size_t message_len); -// void onLogMessage(PW_LOG_LEVEL level, const char *function, const char *file, int line, const char *message, size_t message_len) { -// goOnLogMessage(level, function, file, line, message, message_len); -// } -import "C" diff --git a/waf/internal/bindings/waf.go b/waf/internal/bindings/waf.go index d8d88af..ceb22e5 100644 --- a/waf/internal/bindings/waf.go +++ b/waf/internal/bindings/waf.go @@ -10,12 +10,12 @@ package bindings import ( - "errors" "fmt" "reflect" "time" "unsafe" + "github.com/pkg/errors" "github.com/sqreen/go-libsqreen/waf/types" ) @@ -63,14 +63,14 @@ func (r Rule) Close() error { return nil } -func (r Rule) Run(data types.RunInput, timeout time.Duration) (action types.Action, info []byte, err error) { - dataIn, err := WAFInput(data) +func (r Rule) Run(data types.DataSet, timeout time.Duration) (action types.Action, info []byte, err error) { + wafValue, err := marshalWAFValue(data) if err != nil { return 0, nil, err } - defer C.powerwaf_freeInput(dataIn, C.bool(false)) + defer freeWAFValue(wafValue) - ret := C.powerwaf_runPowerWAF(r.id, dataIn, C.size_t(timeout/time.Microsecond)) + ret := C.powerwaf_runPowerWAF(r.id, (*C.PWArgs)(wafValue), C.size_t(timeout/time.Microsecond)) defer C.powerwaf_freeReturn(ret) switch a := ret.action; a { @@ -113,65 +113,59 @@ func goRunError(cErr C.PW_RET_CODE, data *C.char) error { return err } -func WAFInput(data types.RunInput) (*C.PWArgs, error) { - return valueToWAFInput(reflect.ValueOf(data)) +type ( + WAFValue C.PWArgs + WAFInt C.PWArgs + WAFUInt C.PWArgs + WAFString C.PWArgs + WAFMap C.PWArgs + WAFMapEntry C.PWArgs + WAFArray C.PWArgs +) + +const maxWAFValueDepth = 10 + +func marshalWAFValue(data types.DataSet) (*WAFValue, error) { + v := new(WAFValue) + if err := marshalWAFValueRec(reflect.ValueOf(data), v, maxWAFValueDepth); err != nil { + freeWAFValue(v) + return nil, err + } + return v, nil } -func valueToWAFInput(v reflect.Value) (in *C.PWArgs, err error) { - switch v.Kind() { +func marshalWAFValueRec(data reflect.Value, v *WAFValue, depth int) error { + if depth == 0 { + // Stop traversing and keep v to its current zero value. + return nil + } + + switch data.Kind() { default: - return nil, fmt.Errorf("unexpected WAF input type `%T`", v.Interface()) + return fmt.Errorf("unexpected WAF input type `%T`", data.Interface()) case reflect.Ptr: fallthrough case reflect.Interface: - return valueToWAFInput(v.Elem()) + // This interface or pointer traversal is not counted in the depth + return marshalWAFValueRec(data.Elem(), v, depth) case reflect.String: - str := v.String() - cstr := C.CString(str) - defer C.free(unsafe.Pointer(cstr)) - wstr := C.powerwaf_createStringWithLength(cstr, C.size_t(len(str))) - return &wstr, nil + return makeWAFString((*WAFString)(v), data.String()) case reflect.Map: - if v.Type().Key().Kind() != reflect.String { - return nil, fmt.Errorf("unexpected WAF map key type `%T` instead of `string`", v.Interface()) - } - m := C.powerwaf_createMap() - in = &m - for _, k := range v.MapKeys() { - value, err := valueToWAFInput(v.MapIndex(k)) - if err != nil { - C.powerwaf_freeInput(in, C.bool(false)) - return nil, err - } - k := k.String() - key := C.CString(k) - defer C.free(unsafe.Pointer(key)) - if !C.powerwaf_addToPWArgsMap(in, key, C.size_t(len(k)), *value) { - C.powerwaf_freeInput(value, C.bool(false)) - C.powerwaf_freeInput(in, C.bool(false)) - return nil, errors.New("could not insert a key element into a map") - } + if err := makeWAFMap((*WAFMap)(v), data.Len()); err != nil { + return err } - return in, nil + return marshalWAFMap(data, (*WAFMap)(v), depth-1) + case reflect.Array: + fallthrough case reflect.Slice: - a := C.powerwaf_createArray() - in = &a - for i := 0; i < v.Len(); i++ { - value, err := valueToWAFInput(v.Index(i)) - if err != nil { - C.powerwaf_freeInput(in, C.bool(false)) - return nil, err - } - if !C.powerwaf_addToPWArgsArray(in, *value) { - C.powerwaf_freeInput(in, C.bool(false)) - return nil, fmt.Errorf("could not insert element `%d` of an array", i) - } + if err := makeWAFArray((*WAFArray)(v), data.Len()); err != nil { + return err } - return in, nil + return marshalWAFArray(data, (*WAFArray)(v), depth-1) case reflect.Int: fallthrough @@ -182,8 +176,7 @@ func valueToWAFInput(v reflect.Value) (in *C.PWArgs, err error) { case reflect.Int32: fallthrough case reflect.Int64: - arg := C.powerwaf_createInt((C.int64_t)(v.Int())) - return &arg, nil + return makeWAFInt((*WAFInt)(v), data.Int()) case reflect.Uint: fallthrough @@ -194,16 +187,146 @@ func valueToWAFInput(v reflect.Value) (in *C.PWArgs, err error) { case reflect.Uint32: fallthrough case reflect.Uint64: - arg := C.powerwaf_createUint((C.uint64_t)(v.Uint())) - return &arg, nil + return makeWAFUInt((*WAFUInt)(v), data.Uint()) } } -//export goOnLogMessage -func goOnLogMessage(level C.PW_LOG_LEVEL, _, _ *C.char, _ C.int, message *C.char, length C.size_t) { - fmt.Println(C.GoStringN(message, C.int(length))) +func marshalWAFMap(data reflect.Value, v *WAFMap, depth int) error { + // Only allow string key types + if data.Type().Key().Kind() != reflect.String { + return errors.Errorf("unexpected WAF map key type `%T` instead of `string`", data.Interface()) + } + // Marshal map entries + for i, iter := 0, data.MapRange(); iter.Next(); i++ { + entry := v.Index(i) + // Add the key first in order to get key insertion errors before traversing + // the value. It would be a waste if in the end the key cannot be added. + key := iter.Key().String() + if err := makeWAFMapKey(entry, key); err != nil { + return errors.Wrap(err, "could not add a new map key") + } + // Marshal the key's value + if err := marshalWAFValueRec(iter.Value(), (*WAFValue)(entry), depth); err != nil { + return err + } + } + return nil +} + +func marshalWAFArray(data reflect.Value, v *WAFArray, depth int) error { + // Profiling shows `data.Len()` is called every loop if it is + // used in the loop condition. + l := data.Len() + for i := 0; i < l; i++ { + if err := marshalWAFValueRec(data.Index(i), v.Index(i), depth); err != nil { + return err + } + } + return nil +} + +func makeWAFMap(v *WAFMap, len int) error { + return makeWAFLengthedValue((*WAFValue)(v), len, C.PWI_MAP) } -func SetupLogging() { - C.powerwaf_setupLogging(C.powerwaf_logging_cb_t(C.onLogMessage), C.PWL_DEBUG) +func (m *WAFMap) Index(i int) *WAFMapEntry { + entry := (*WAFArray)(m).Index(i) + return (*WAFMapEntry)(entry) +} + +func (a *WAFArray) Index(i int) *WAFValue { + if C.uint64_t(i) >= a.nbEntries { + panic(errors.New("out of bounds access to WAFArray")) + } + // Go pointer arithmetic equivalent to the C expression + // `(PWArgs*)(a->value)[i]` + return (*WAFValue)(unsafe.Pointer(uintptr(a.value) + C.sizeof_PWArgs*uintptr(i))) +} + +func makeWAFMapKey(v *WAFMapEntry, key string) error { + cstr, length := cstring(key) + if cstr == nil { + return types.ErrOutOfMemory + } + v.parameterName = cstr + v.parameterNameLength = C.uint64_t(length) + return nil +} + +func makeWAFArray(v *WAFArray, len int) error { + return makeWAFLengthedValue((*WAFValue)(v), len, C.PWI_ARRAY) +} + +const maxWAFStringSize = 4 * 1024 + +func makeWAFString(v *WAFString, str string) error { + cstr, length := cstring(str) + if cstr == nil { + return types.ErrOutOfMemory + } + v.value = unsafe.Pointer(cstr) + v.nbEntries = C.uint64_t(length) + v._type = C.PWI_STRING + return nil +} + +// cstring returns the C string of the given Go string `str` with up to +// maxWAFStringSize bytes, along with the string size that was copied. +func cstring(str string) (*C.char, int) { + // Limit the maximum string size to copy + l := len(str) + if l > maxWAFStringSize { + l = maxWAFStringSize + } + // Copy the string up to l. + // The copy is required as the pointer will be stored into the C structures, + // so using a Go pointer is impossible (and detected by the cgo pointer checks + // anyway). + return C.CString(str[:l]), l +} + +func makeWAFInt(v *WAFInt, n int64) error { + return makeWAFBasicValue((*WAFValue)(v), uintptr(n), C.PWI_SIGNED_NUMBER) +} + +func makeWAFUInt(v *WAFUInt, n uint64) error { + return makeWAFBasicValue((*WAFValue)(v), uintptr(n), C.PWI_UNSIGNED_NUMBER) +} + +func makeWAFBasicValue(v *WAFValue, data uintptr, wafType C.PW_INPUT_TYPE) error { + v.value = unsafe.Pointer(data) + v._type = wafType + return nil +} + +func makeWAFLengthedValue(v *WAFValue, len int, wafType C.PW_INPUT_TYPE) error { + // Allocate the zero'd array. + a := C.calloc(C.size_t(len), C.sizeof_PWArgs) + if a == nil { + return types.ErrOutOfMemory + } + + v.value = a + v.nbEntries = C.uint64_t(len) + v._type = wafType + return nil +} + +func freeWAFValue(v *WAFValue) { + switch v._type { + case C.PWI_MAP: + fallthrough + case C.PWI_ARRAY: + for child := 0; C.uint64_t(child) < v.nbEntries; child++ { + entry := (*WAFArray)(v).Index(child) + if entry.parameterName != nil { + C.free(unsafe.Pointer(entry.parameterName)) + } + freeWAFValue(entry) + } + } + + if v.value != nil { + C.free(v.value) + } } diff --git a/waf/internal/bindings/waf_test.go b/waf/internal/bindings/waf_test.go new file mode 100644 index 0000000..3c620b0 --- /dev/null +++ b/waf/internal/bindings/waf_test.go @@ -0,0 +1,36 @@ +// Copyright (c) 2016 - 2019 Sqreen. All Rights Reserved. +// Please refer to our terms for more information: +// https://www.sqreen.io/terms.html + +package bindings_test + +import ( + "crypto/rand" + "testing" + "unsafe" + + "github.com/stretchr/testify/require" +) + +func TestCGO(t *testing.T) { + t.Run("a Go string address is the address of the string buffer", func(t *testing.T) { + // Create a random string + b := make([]byte, 1024) + if _, err := rand.Read(b); err != nil { + panic(err) + } + str := string(b) + + // []byte(str) returns a copy of str because slices are mutable while + // strings are not. + // So check that it is indeed possible to get the underlying slice of bytes + // using unsafe.Pointer() casts. + buf := *(*[]byte)(unsafe.Pointer(&str)) + // The slice should now be usable: len() works and buf[i] gives the string + // characters + require.Equal(t, len(buf), len(str)) + for i := range str { + require.Equal(t, str[i], buf[i]) + } + }) +} diff --git a/waf/types/types.go b/waf/types/types.go index 48725b9..fc39c3a 100644 --- a/waf/types/types.go +++ b/waf/types/types.go @@ -11,13 +11,12 @@ import ( ) type Rule interface { - Run(data RunInput, timeout time.Duration) (action Action, info []byte, err error) + Run(data DataSet, timeout time.Duration) (action Action, info []byte, err error) io.Closer } -// RunInput is a map type whose keys must are binding accessor expressions and -// their result as value. -type RunInput map[string]interface{} +// DataSet is a map type to associate binding accessor expressions to their results. +type DataSet map[string]interface{} type Action int @@ -36,6 +35,7 @@ const ( ErrInvalidRule ErrInvalidFlow ErrNoRule + ErrOutOfMemory ) func (e RunError) Error() string { @@ -52,6 +52,8 @@ func (e RunError) Error() string { return "invalid flow" case ErrNoRule: return "no rule" + case ErrOutOfMemory: + return "out of memory" default: return fmt.Sprintf("unknown error `%d`", e) } @@ -69,6 +71,7 @@ var ( _ error = ErrInvalidRule _ error = ErrInvalidFlow _ error = ErrNoRule + _ error = ErrOutOfMemory ) type NewRuleFunc = func(string, string) (Rule, error) diff --git a/waf/waf_test.go b/waf/waf_test.go index 76c3840..1854fea 100644 --- a/waf/waf_test.go +++ b/waf/waf_test.go @@ -12,6 +12,7 @@ package waf_test import ( "fmt" "math/rand" + "runtime" "sync" "testing" "time" @@ -33,7 +34,7 @@ func TestUsage(t *testing.T) { r, err := waf.NewRule("my rule", "{\"rules\": [{\"rule_id\": \"1\",\"filters\": [{\"operator\": \"@rx\",\"targets\": [\"#._server['HTTP_USER_AGENT']\"],\"value\": \"Arachni\"}]}],\"flows\": [{\"name\": \"arachni_detection\",\"steps\": [{\"id\": \"start\",\"rule_ids\": [\"1\"],\"on_match\": \"exit_monitor\"}]}]}") require.NoError(t, err) defer r.Close() - action, match, err := r.Run(types.RunInput{"#._server['HTTP_USER_AGENT']": "Arachni"}, time.Second) + action, match, err := r.Run(types.DataSet{"#._server['HTTP_USER_AGENT']": "Arachni"}, time.Second) require.NoError(t, err) require.Equal(t, types.MonitorAction, action) require.NotEmpty(t, match) @@ -43,7 +44,7 @@ func TestUsage(t *testing.T) { r, err := waf.NewRule("my rule", "{\"rules\": [{\"rule_id\": \"1\",\"filters\": [{\"operator\": \"@rx\",\"targets\": [\"#._server['HTTP_USER_AGENT']\"],\"value\": \"Arachni\"}]}],\"flows\": [{\"name\": \"arachni_detection\",\"steps\": [{\"id\": \"start\",\"rule_ids\": [\"1\"],\"on_match\": \"exit_block\"}]}]}") require.NoError(t, err) defer r.Close() - action, match, err := r.Run(types.RunInput{"#._server['HTTP_USER_AGENT']": "Arachni"}, time.Second) + action, match, err := r.Run(types.DataSet{"#._server['HTTP_USER_AGENT']": "Arachni"}, time.Second) require.NoError(t, err) require.Equal(t, types.BlockAction, action) require.NotEmpty(t, match) @@ -53,7 +54,7 @@ func TestUsage(t *testing.T) { r, err := waf.NewRule("my rule", "{\"rules\": [{\"rule_id\": \"1\",\"filters\": [{\"operator\": \"@rx\",\"targets\": [\"#._server['HTTP_USER_AGENT']\"],\"value\": \"Arachni\"}]}],\"flows\": [{\"name\": \"arachni_detection\",\"steps\": [{\"id\": \"start\",\"rule_ids\": [\"1\"],\"on_match\": \"exit_block\"}]}]}") require.NoError(t, err) defer r.Close() - action, match, err := r.Run(types.RunInput{"#._server['HTTP_USER_AGENT']": "go client"}, time.Second) + action, match, err := r.Run(types.DataSet{"#._server['HTTP_USER_AGENT']": "go client"}, time.Second) require.NoError(t, err) require.Equal(t, types.NoAction, action) require.Empty(t, match) @@ -63,7 +64,7 @@ func TestUsage(t *testing.T) { r, err := waf.NewRule("my rule", "{\"rules\": [{\"rule_id\": \"1\",\"filters\": [{\"operator\": \"@rx\",\"targets\": [\"#._server['HTTP_USER_AGENT']\"],\"value\": \"Arachni\"}]}],\"flows\": [{\"name\": \"arachni_detection\",\"steps\": [{\"id\": \"start\",\"rule_ids\": [\"1\"],\"on_match\": \"exit_block\"}]}]}") require.NoError(t, err) defer r.Close() - action, match, err := r.Run(types.RunInput{"#._server['HTTP_USER_AGENT']": "Arachni"}, 0) + action, match, err := r.Run(types.DataSet{"#._server['HTTP_USER_AGENT']": "Arachni"}, 0) require.Equal(t, types.ErrTimeout, err) require.Equal(t, types.NoAction, action) require.Empty(t, match) @@ -73,7 +74,7 @@ func TestUsage(t *testing.T) { t.Run("update an existing rule", func(t *testing.T) { r, err := waf.NewRule("my rule", "{\"rules\": [{\"rule_id\": \"1\",\"filters\": [{\"operator\": \"@rx\",\"targets\": [\"#._server['HTTP_USER_AGENT']\"],\"value\": \"Arachni\"}]}],\"flows\": [{\"name\": \"arachni_detection\",\"steps\": [{\"id\": \"start\",\"rule_ids\": [\"1\"],\"on_match\": \"exit_monitor\"}]}]}") require.NoError(t, err) - action, match, err := r.Run(types.RunInput{"#._server['HTTP_USER_AGENT']": "Arachni"}, time.Second) + action, match, err := r.Run(types.DataSet{"#._server['HTTP_USER_AGENT']": "Arachni"}, time.Second) require.NoError(t, err) require.Equal(t, types.MonitorAction, action) require.NotEmpty(t, match) @@ -81,7 +82,7 @@ func TestUsage(t *testing.T) { r, err = waf.NewRule("my rule", "{\"rules\": [{\"rule_id\": \"1\",\"filters\": [{\"operator\": \"@rx\",\"targets\": [\"#._server['HTTP_USER_AGENT']\"],\"value\": \"Toto\"}]}],\"flows\": [{\"name\": \"arachni_detection\",\"steps\": [{\"id\": \"start\",\"rule_ids\": [\"1\"],\"on_match\": \"exit_monitor\"}]}]}") require.NoError(t, err) // It should no longer be detected - action, match, err = r.Run(types.RunInput{"#._server['HTTP_USER_AGENT']": "Arachni"}, time.Second) + action, match, err = r.Run(types.DataSet{"#._server['HTTP_USER_AGENT']": "Arachni"}, time.Second) require.NoError(t, err) require.Equal(t, types.NoAction, action) require.Empty(t, match) @@ -116,7 +117,7 @@ func TestUsage(t *testing.T) { defer stopBarrier.Done() // Signal we are done when returning for c := 0; c < nbRun; c++ { i := rand.Int() % len(userAgents) - action, match, err := r.Run(types.RunInput{"#._server['HTTP_USER_AGENT']": userAgents[i]}, time.Second) + action, match, err := r.Run(types.DataSet{"#._server['HTTP_USER_AGENT']": userAgents[i]}, time.Second) require.NoError(t, err) if i <= okIndex { require.Equal(t, types.MonitorAction, action) @@ -136,6 +137,7 @@ func TestUsage(t *testing.T) { }) t.Run("one concurrent updater - 8000 concurrent users", func(t *testing.T) { + fmt.Println(runtime.GOMAXPROCS(0)) userAgents := [...]string{"Arachni", "Toto", "Tata", "Titi"} var ( currentUserAgentIndex int @@ -146,7 +148,6 @@ func TestUsage(t *testing.T) { updateRule := func() (previousRule types.Rule) { lock.Lock() defer lock.Unlock() - // Select a random user agent currentUserAgentIndex = rand.Intn(len(userAgents)) userAgent := userAgents[currentUserAgentIndex] @@ -171,7 +172,7 @@ func TestUsage(t *testing.T) { updateRule() // The WAF rule will be updated once per second - updatePeriod := 100 * time.Millisecond + updatePeriod := 10 * time.Millisecond tick := time.Tick(updatePeriod) // Signal channel between this test and the updater to tear down the test @@ -215,7 +216,7 @@ func TestUsage(t *testing.T) { myUserAgentIndex := rand.Intn(len(userAgents)) myUserAgent := userAgents[myUserAgentIndex] // Use the rule - action, match, err := rule.Run(types.RunInput{"#._server['HTTP_USER_AGENT']": myUserAgent}, time.Second) + action, match, err := rule.Run(types.DataSet{"#._server['HTTP_USER_AGENT']": myUserAgent}, time.Second) require.NoError(t, err) if myUserAgentIndex == currentUserAgentIndex { require.Equal(t, types.MonitorAction, action) @@ -237,3 +238,56 @@ func TestUsage(t *testing.T) { <-done // Wait for the reader to be done }) } + +func TestWAFValues(t *testing.T) { + wafRule := `{"rules": [{"rule_id": "rule_custom_552203d1f33ce0705f6c215f462199f1", "filters": [{"operator": "@rx", "targets": ["v1"], "transformations": [], "value": "Arachni1"}, {"operator": "@rx", "targets": ["v2"], "transformations": [], "value": "Arachni2"}, {"operator": "@rx", "targets": ["v3"], "transformations": [], "value": "Arachni3"}, {"operator": "@rx", "targets": ["v4"], "transformations": [], "value": "Arachni4"}]}], "flows": [{"name": "rs_728137e2322e1d7a692ca3099f08e831-blocking", "steps": [{"id": "start", "rule_ids": ["rule_custom_552203d1f33ce0705f6c215f462199f1"], "on_match": "exit_block"}]}]}` + r, err := waf.NewRule("my rule", wafRule) + require.NoError(t, err) + defer r.Close() + + t.Run("supported Go types", func(t *testing.T) { + ds := types.DataSet{ + // string + "v1": "fooArachni1bar", + // string of map key + "v2": map[string]string{ + "k1": "", + "k2": "", + " Arachni2": "", + "k4": "", + }, + // string of map value + "v3": map[string]string{ + "k1": "", + "k2": "Arachni3", + "k3": "", + "k4": "", + }, + // string of array entry + "v4": []string{"ok", "ok", "\000Arachni4", "ok"}, + // TODO: numbers + } + action, match, err := r.Run(ds, time.Second) + require.NoError(t, err) + require.Equal(t, types.BlockAction, action) + require.NotEmpty(t, match) + }) + + t.Run("unsupported Go types", func(t *testing.T) { + for _, ds := range []types.DataSet{ + {"k1": 33.33}, + {"k1": true}, + // Struct could be considered as a map but is not implemented for now + // (probably useless for now). + {"k1": map[string]struct{ V string }{"k": {}}}, + {"k1": func() {}}, + {"k1": make(chan struct{})}, + } { + ds := ds + t.Run("", func(t *testing.T) { + _, _, err := r.Run(ds, time.Second) + require.Error(t, err) + }) + } + }) +}