From 9046c68111f5fb69400bfa5912412eb65ef8883b Mon Sep 17 00:00:00 2001 From: Julio Guerra Date: Wed, 20 Oct 2021 10:49:38 +0200 Subject: [PATCH] internal/appsec: add new AppSec beta functionality and support for contrib/net/http (#1007) Beta functionality for AppSec has been added to internal/appsec. It is also integrated into contrib/internal/httputil and can be enabled for all HTTP integrations using this (all do at this point). The appsec package also has C bindings, which are disabled by default for the beta, but will likely be enabled later on. For this reason, to use AppSec, one must build their program using the "appsec" tag and enable it using DD_APPSEC_ENABLED=true. --- .circleci/config.yml | 40 +- LICENSE-3rdparty.csv | 3 +- .../internal/httputil/make_responsewriter.go | 19 +- contrib/internal/httputil/trace.go | 11 +- contrib/internal/httputil/trace_gen.go | 66 +- ddtrace/tracer/log_test.go | 5 + ddtrace/tracer/option.go | 10 +- ddtrace/tracer/option_test.go | 10 + ddtrace/tracer/spancontext_test.go | 16 + ddtrace/tracer/tracer.go | 15 + ddtrace/tracer/tracer_test.go | 13 +- ddtrace/tracer/transport.go | 3 - go.mod | 2 + go.sum | 3 + internal/appsec/api.go | 303 ++++++ internal/appsec/api_test.go | 98 ++ internal/appsec/appsec.go | 270 +++++ internal/appsec/appsec_disabled.go | 16 + internal/appsec/batching_test.go | 201 ++++ internal/appsec/client.go | 188 ++++ internal/appsec/client_test.go | 244 +++++ .../dyngo/instrumentation/httpinstr/http.go | 175 ++++ internal/appsec/dyngo/operation.go | 238 +++++ internal/appsec/dyngo/operation_test.go | 971 +++++++++++++++++ internal/appsec/events.go | 264 +++++ internal/appsec/rule.go | 13 + internal/appsec/rule_test.go | 27 + internal/appsec/types.go | 49 + internal/appsec/waf.go | 207 ++++ internal/appsec/waf/include/ddwaf.h | 486 +++++++++ internal/appsec/waf/include/vendor.go | 8 + internal/appsec/waf/lib/README.md | 2 + .../appsec/waf/lib/darwin-amd64/libddwaf.a | Bin 0 -> 15678048 bytes .../appsec/waf/lib/darwin-amd64/vendor.go | 8 + .../appsec/waf/lib/linux-amd64/libddwaf.a | Bin 0 -> 27676360 bytes internal/appsec/waf/lib/linux-amd64/vendor.go | 8 + internal/appsec/waf/types.go | 58 + internal/appsec/waf/waf.go | 655 ++++++++++++ internal/appsec/waf/waf_disabled.go | 60 ++ internal/appsec/waf/waf_disabled_build_tag.go | 12 + internal/appsec/waf/waf_disabled_cgo.go | 12 + internal/appsec/waf/waf_disabled_target.go | 19 + internal/appsec/waf/waf_disabled_test.go | 22 + internal/appsec/waf/waf_test.go | 991 ++++++++++++++++++ internal/appsec/waf_test.go | 81 ++ 45 files changed, 5850 insertions(+), 52 deletions(-) create mode 100644 internal/appsec/api.go create mode 100644 internal/appsec/api_test.go create mode 100644 internal/appsec/appsec.go create mode 100644 internal/appsec/appsec_disabled.go create mode 100644 internal/appsec/batching_test.go create mode 100644 internal/appsec/client.go create mode 100644 internal/appsec/client_test.go create mode 100644 internal/appsec/dyngo/instrumentation/httpinstr/http.go create mode 100644 internal/appsec/dyngo/operation.go create mode 100644 internal/appsec/dyngo/operation_test.go create mode 100644 internal/appsec/events.go create mode 100644 internal/appsec/rule.go create mode 100644 internal/appsec/rule_test.go create mode 100644 internal/appsec/types.go create mode 100644 internal/appsec/waf.go create mode 100644 internal/appsec/waf/include/ddwaf.h create mode 100644 internal/appsec/waf/include/vendor.go create mode 100644 internal/appsec/waf/lib/README.md create mode 100644 internal/appsec/waf/lib/darwin-amd64/libddwaf.a create mode 100644 internal/appsec/waf/lib/darwin-amd64/vendor.go create mode 100644 internal/appsec/waf/lib/linux-amd64/libddwaf.a create mode 100644 internal/appsec/waf/lib/linux-amd64/vendor.go create mode 100644 internal/appsec/waf/types.go create mode 100644 internal/appsec/waf/waf.go create mode 100644 internal/appsec/waf/waf_disabled.go create mode 100644 internal/appsec/waf/waf_disabled_build_tag.go create mode 100644 internal/appsec/waf/waf_disabled_cgo.go create mode 100644 internal/appsec/waf/waf_disabled_target.go create mode 100644 internal/appsec/waf/waf_disabled_test.go create mode 100644 internal/appsec/waf/waf_test.go create mode 100644 internal/appsec/waf_test.go diff --git a/.circleci/config.yml b/.circleci/config.yml index e671b1d528..f6dbf8c490 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,4 +1,4 @@ -version: 2 +version: 2.1 plain-go114: &plain-go114 working_directory: /home/circleci/dd-trace-go.v1 @@ -8,8 +8,13 @@ plain-go114: &plain-go114 GOPATH: "/home/circleci/go" jobs: - go1.12-build: + go1_12-build: # Validate that the core builds with go1.12 + parameters: + build_tags: + description: "go build tags used to compile" + default: "" + type: string docker: - image: circleci/golang:1.12 environment: @@ -25,7 +30,7 @@ jobs: # See https://github.com/DataDog/dd-trace-go/pull/1029 sudo apt update && sudo apt install ca-certificates libgnutls30 -y - go build ./ddtrace/... ./profiler/... + go build -v -tags "<< parameters.build_tags >>" ./ddtrace/... ./profiler/... ./internal/appsec/... metadata: <<: *plain-go114 @@ -83,6 +88,11 @@ jobs: test-core: + parameters: + build_tags: + description: "go build tags to use to compile the tests" + default: "" + type: string resource_class: xlarge environment: # environment variables for the build itself TEST_RESULTS: /tmp/test-results # path to where test results will be saved @@ -98,7 +108,7 @@ jobs: name: Testing command: | PACKAGE_NAMES=$(go list ./... | grep -v /contrib/ | circleci tests split --split-by=timings --timings-type=classname) - gotestsum --junitfile ${TEST_RESULTS}/gotestsum-report.xml -- $PACKAGE_NAMES -v -race -coverprofile=coverage.txt -covermode=atomic + env DD_APPSEC_ENABLED=$(test "<< parameters.build_tags >>" = appsec && echo -n true) gotestsum --junitfile ${TEST_RESULTS}/gotestsum-report.xml -- $PACKAGE_NAMES -v -race -coverprofile=coverage.txt -covermode=atomic -tags "<< parameters.build_tags >>" - save_cache: key: go-mod-v4-{{ checksum "go.sum" }} @@ -118,6 +128,11 @@ jobs: test-contrib: + parameters: + build_tags: + description: "go build tags to use to compile the tests" + default: "" + type: string resource_class: xlarge environment: # environment variables for the build itself TEST_RESULTS: /tmp/test-results # path to where test results will be saved @@ -241,7 +256,7 @@ jobs: name: Testing integrations command: | PACKAGE_NAMES=$(go list ./contrib/... | grep -v -e grpc.v12 -e google.golang.org/api | circleci tests split --split-by=timings --timings-type=classname) - gotestsum --junitfile ${TEST_RESULTS}/gotestsum-report.xml -- $PACKAGE_NAMES -v -race -coverprofile=coverage.txt -covermode=atomic + env DD_APPSEC_ENABLED=$(test "<< parameters.build_tags >>" = appsec && echo -n true) gotestsum --junitfile ${TEST_RESULTS}/gotestsum-report.xml -- $PACKAGE_NAMES -v -race -coverprofile=coverage.txt -covermode=atomic -tags "<< parameters.build_tags >>" - save_cache: key: go-mod-v4-{{ checksum "go.sum" }} @@ -288,8 +303,17 @@ workflows: version: 2 build-and-test: jobs: - - go1.12-build + - go1_12-build: + matrix: + parameters: + build_tags: [ "", "appsec" ] - metadata - lint - - test-core - - test-contrib + - test-core: + matrix: + parameters: + build_tags: [ "", "appsec" ] + - test-contrib: + matrix: + parameters: + build_tags: [ "", "appsec" ] diff --git a/LICENSE-3rdparty.csv b/LICENSE-3rdparty.csv index 582ea79106..9b4658fc59 100644 --- a/LICENSE-3rdparty.csv +++ b/LICENSE-3rdparty.csv @@ -1,2 +1,3 @@ Component,Origin,License,Copyright -import,io.opentracing,Apache-2.0,Copyright 2016-2017 The OpenTracing Authors \ No newline at end of file +import,io.opentracing,Apache-2.0,Copyright 2016-2017 The OpenTracing Authors +appsec,https://github.com/DataDog/libddwaf,Apache-2.0 OR BSD-3-Clause,Copyright (c) 2021 Datadog diff --git a/contrib/internal/httputil/make_responsewriter.go b/contrib/internal/httputil/make_responsewriter.go index a6d3d7de10..6c1fb1f4aa 100644 --- a/contrib/internal/httputil/make_responsewriter.go +++ b/contrib/internal/httputil/make_responsewriter.go @@ -3,6 +3,7 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2016 Datadog, Inc. +//go:build ignore // +build ignore // This program generates wrapper implementations of http.ResponseWriter that @@ -31,12 +32,18 @@ func main() { }) } -var tpl = `// Code generated by make_responsewriter.go DO NOT EDIT +var tpl = `// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016 Datadog, Inc. + +// Code generated by make_responsewriter.go DO NOT EDIT package httputil import ( "net/http" + "gopkg.in/DataDog/dd-trace-go.v1/ddtrace" ) @@ -53,17 +60,21 @@ func wrapResponseWriter(w http.ResponseWriter, span ddtrace.Span) http.ResponseW h{{.}}, ok{{.}} := w.(http.{{.}}) {{- end }} - w = newResponseWriter(w, span) + mw := newResponseWriter(w, span) + type monitoredResponseWriter interface { + http.ResponseWriter + Status() int + } switch { {{- range .Combinations }} {{- range . }} case {{ range $i, $v := . }}{{ if gt $i 0 }} && {{ end }}ok{{ $v }}{{ end }}: w = struct { - http.ResponseWriter + monitoredResponseWriter {{- range . }} http.{{.}} {{- end }} - } { w{{ range . }}, h{{.}}{{ end }} } + }{mw{{ range . }}, h{{.}}{{ end }}} {{- end }} {{- end }} } diff --git a/contrib/internal/httputil/trace.go b/contrib/internal/httputil/trace.go index 4682c498fd..92728b7966 100644 --- a/contrib/internal/httputil/trace.go +++ b/contrib/internal/httputil/trace.go @@ -15,6 +15,7 @@ import ( "gopkg.in/DataDog/dd-trace-go.v1/ddtrace" "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/ext" "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer" + "gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/dyngo/instrumentation/httpinstr" ) // TraceConfig defines the configuration for request tracing. @@ -53,8 +54,7 @@ func TraceAndServe(h http.Handler, cfg *TraceConfig) { defer span.Finish(cfg.FinishOpts...) cfg.ResponseWriter = wrapResponseWriter(cfg.ResponseWriter, span) - - h.ServeHTTP(cfg.ResponseWriter, cfg.Request.WithContext(ctx)) + httpinstr.WrapHandler(h, span).ServeHTTP(cfg.ResponseWriter, cfg.Request.WithContext(ctx)) } // responseWriter is a small wrapper around an http response writer that will @@ -69,8 +69,13 @@ func newResponseWriter(w http.ResponseWriter, span ddtrace.Span) *responseWriter return &responseWriter{w, span, 0} } +// Status returns the status code that was monitored. +func (w *responseWriter) Status() int { + return w.status +} + // Write writes the data to the connection as part of an HTTP reply. -// We explicitely call WriteHeader with the 200 status code +// We explicitly call WriteHeader with the 200 status code // in order to get it reported into the span. func (w *responseWriter) Write(b []byte) (int, error) { if w.status == 0 { diff --git a/contrib/internal/httputil/trace_gen.go b/contrib/internal/httputil/trace_gen.go index 79eda8aaae..4e5c523a69 100644 --- a/contrib/internal/httputil/trace_gen.go +++ b/contrib/internal/httputil/trace_gen.go @@ -27,100 +27,104 @@ func wrapResponseWriter(w http.ResponseWriter, span ddtrace.Span) http.ResponseW hCloseNotifier, okCloseNotifier := w.(http.CloseNotifier) hHijacker, okHijacker := w.(http.Hijacker) - w = newResponseWriter(w, span) + mw := newResponseWriter(w, span) + type monitoredResponseWriter interface { + http.ResponseWriter + Status() int + } switch { case okFlusher && okPusher && okCloseNotifier && okHijacker: w = struct { - http.ResponseWriter + monitoredResponseWriter http.Flusher http.Pusher http.CloseNotifier http.Hijacker - }{w, hFlusher, hPusher, hCloseNotifier, hHijacker} + }{mw, hFlusher, hPusher, hCloseNotifier, hHijacker} case okFlusher && okPusher && okCloseNotifier: w = struct { - http.ResponseWriter + monitoredResponseWriter http.Flusher http.Pusher http.CloseNotifier - }{w, hFlusher, hPusher, hCloseNotifier} + }{mw, hFlusher, hPusher, hCloseNotifier} case okFlusher && okPusher && okHijacker: w = struct { - http.ResponseWriter + monitoredResponseWriter http.Flusher http.Pusher http.Hijacker - }{w, hFlusher, hPusher, hHijacker} + }{mw, hFlusher, hPusher, hHijacker} case okFlusher && okCloseNotifier && okHijacker: w = struct { - http.ResponseWriter + monitoredResponseWriter http.Flusher http.CloseNotifier http.Hijacker - }{w, hFlusher, hCloseNotifier, hHijacker} + }{mw, hFlusher, hCloseNotifier, hHijacker} case okPusher && okCloseNotifier && okHijacker: w = struct { - http.ResponseWriter + monitoredResponseWriter http.Pusher http.CloseNotifier http.Hijacker - }{w, hPusher, hCloseNotifier, hHijacker} + }{mw, hPusher, hCloseNotifier, hHijacker} case okFlusher && okPusher: w = struct { - http.ResponseWriter + monitoredResponseWriter http.Flusher http.Pusher - }{w, hFlusher, hPusher} + }{mw, hFlusher, hPusher} case okFlusher && okCloseNotifier: w = struct { - http.ResponseWriter + monitoredResponseWriter http.Flusher http.CloseNotifier - }{w, hFlusher, hCloseNotifier} + }{mw, hFlusher, hCloseNotifier} case okFlusher && okHijacker: w = struct { - http.ResponseWriter + monitoredResponseWriter http.Flusher http.Hijacker - }{w, hFlusher, hHijacker} + }{mw, hFlusher, hHijacker} case okPusher && okCloseNotifier: w = struct { - http.ResponseWriter + monitoredResponseWriter http.Pusher http.CloseNotifier - }{w, hPusher, hCloseNotifier} + }{mw, hPusher, hCloseNotifier} case okPusher && okHijacker: w = struct { - http.ResponseWriter + monitoredResponseWriter http.Pusher http.Hijacker - }{w, hPusher, hHijacker} + }{mw, hPusher, hHijacker} case okCloseNotifier && okHijacker: w = struct { - http.ResponseWriter + monitoredResponseWriter http.CloseNotifier http.Hijacker - }{w, hCloseNotifier, hHijacker} + }{mw, hCloseNotifier, hHijacker} case okFlusher: w = struct { - http.ResponseWriter + monitoredResponseWriter http.Flusher - }{w, hFlusher} + }{mw, hFlusher} case okPusher: w = struct { - http.ResponseWriter + monitoredResponseWriter http.Pusher - }{w, hPusher} + }{mw, hPusher} case okCloseNotifier: w = struct { - http.ResponseWriter + monitoredResponseWriter http.CloseNotifier - }{w, hCloseNotifier} + }{mw, hCloseNotifier} case okHijacker: w = struct { - http.ResponseWriter + monitoredResponseWriter http.Hijacker - }{w, hHijacker} + }{mw, hHijacker} } return w diff --git a/ddtrace/tracer/log_test.go b/ddtrace/tracer/log_test.go index b8b9ad6570..46b357633f 100644 --- a/ddtrace/tracer/log_test.go +++ b/ddtrace/tracer/log_test.go @@ -84,6 +84,11 @@ func TestStartupLog(t *testing.T) { } func TestLogSamplingRules(t *testing.T) { + // Disable AppSec to avoid their logs + if old := os.Getenv("DD_APPSEC_ENABLED"); old != "" { + os.Unsetenv("DD_APPSEC_ENABLED") + defer os.Setenv("DD_APPSEC_ENABLED", old) + } assert := assert.New(t) tp := new(testLogger) os.Setenv("DD_TRACE_SAMPLING_RULES", `[{"service": "some.service", "sample_rate": 0.234}, {"service": "other.service"}, {"service": "last.service", "sample_rate": 0.56}, {"odd": "pairs"}, {"sample_rate": 9.10}]`) diff --git a/ddtrace/tracer/option.go b/ddtrace/tracer/option.go index c2f40c3be9..ab502d8168 100644 --- a/ddtrace/tracer/option.go +++ b/ddtrace/tracer/option.go @@ -109,6 +109,14 @@ func (c *config) HasFeature(f string) bool { return ok } +// client returns the HTTP client to use. +func (c *config) client() *http.Client { + if c.httpClient == nil { + return defaultClient + } + return c.httpClient +} + // StartOption represents a function that can be provided as a parameter to Start. type StartOption func(*config) @@ -215,7 +223,7 @@ func newConfig(opts ...StartOption) *config { } } if c.transport == nil { - c.transport = newHTTPTransport(c.agentAddr, c.httpClient) + c.transport = newHTTPTransport(c.agentAddr, c.client()) } if c.propagator == nil { c.propagator = NewPropagator(nil) diff --git a/ddtrace/tracer/option_test.go b/ddtrace/tracer/option_test.go index 8f315949b7..4ec1bb71f4 100644 --- a/ddtrace/tracer/option_test.go +++ b/ddtrace/tracer/option_test.go @@ -7,6 +7,7 @@ package tracer import ( "math" + "net/http" "os" "path/filepath" "testing" @@ -38,6 +39,15 @@ func TestTracerOptionsDefaults(t *testing.T) { assert.Equal("localhost:8126", c.agentAddr) assert.Equal("localhost:8125", c.dogstatsdAddr) assert.Nil(nil, c.httpClient) + assert.Equal(defaultClient, c.client()) + }) + + t.Run("http-client", func(t *testing.T) { + c := newConfig() + assert.Equal(t, defaultClient, c.client()) + client := &http.Client{} + WithHTTPClient(client)(c) + assert.Equal(t, client, c.client()) }) t.Run("analytics", func(t *testing.T) { diff --git a/ddtrace/tracer/spancontext_test.go b/ddtrace/tracer/spancontext_test.go index 38670419e7..c336e8a20a 100644 --- a/ddtrace/tracer/spancontext_test.go +++ b/ddtrace/tracer/spancontext_test.go @@ -7,6 +7,7 @@ package tracer import ( "context" + "os" "sync" "testing" "time" @@ -29,6 +30,11 @@ func setupteardown(start, max int) func() { } func TestNewSpanContextPushError(t *testing.T) { + // Disable AppSec to avoid their logs + if old := os.Getenv("DD_APPSEC_ENABLED"); old != "" { + os.Unsetenv("DD_APPSEC_ENABLED") + defer os.Setenv("DD_APPSEC_ENABLED", old) + } defer setupteardown(2, 2)() tp := new(testLogger) @@ -123,6 +129,11 @@ func TestSpanTracePushOne(t *testing.T) { } func TestSpanTracePushNoFinish(t *testing.T) { + // Disable AppSec to avoid their logs + if old := os.Getenv("DD_APPSEC_ENABLED"); old != "" { + os.Unsetenv("DD_APPSEC_ENABLED") + defer os.Setenv("DD_APPSEC_ENABLED", old) + } defer setupteardown(2, 5)() assert := assert.New(t) @@ -349,6 +360,11 @@ func TestSpanContextParent(t *testing.T) { } func TestSpanContextPushFull(t *testing.T) { + // Disable AppSec to avoid their logs + if old := os.Getenv("DD_APPSEC_ENABLED"); old != "" { + os.Unsetenv("DD_APPSEC_ENABLED") + defer os.Setenv("DD_APPSEC_ENABLED", old) + } defer func(old int) { traceMaxSize = old }(traceMaxSize) traceMaxSize = 2 tp := new(testLogger) diff --git a/ddtrace/tracer/tracer.go b/ddtrace/tracer/tracer.go index 06cc12f731..2192a385f9 100644 --- a/ddtrace/tracer/tracer.go +++ b/ddtrace/tracer/tracer.go @@ -17,7 +17,9 @@ import ( "gopkg.in/DataDog/dd-trace-go.v1/ddtrace" "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/ext" "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/internal" + "gopkg.in/DataDog/dd-trace-go.v1/internal/appsec" "gopkg.in/DataDog/dd-trace-go.v1/internal/log" + "gopkg.in/DataDog/dd-trace-go.v1/internal/version" ) var _ ddtrace.Tracer = (*tracer)(nil) @@ -213,6 +215,18 @@ func newTracer(opts ...StartOption) *tracer { t.reportHealthMetrics(statsInterval) }() t.stats.Start() + appsec.Start(&appsec.Config{ + Client: c.client(), + Version: version.Tag, + AgentURL: fmt.Sprintf("http://%s/", resolveAddr(c.agentAddr)), + Hostname: c.hostname, + Service: appsec.ServiceConfig{ + Name: c.serviceName, + Version: c.version, + Environment: c.env, + }, + Tags: c.globalTags, + }) return t } @@ -464,6 +478,7 @@ func (t *tracer) Stop() { t.wg.Wait() t.traceWriter.stop() t.config.statsd.Close() + appsec.Stop() } // Inject uses the configured or default TextMap Propagator. diff --git a/ddtrace/tracer/tracer_test.go b/ddtrace/tracer/tracer_test.go index 9f2b40cc07..a94df430f8 100644 --- a/ddtrace/tracer/tracer_test.go +++ b/ddtrace/tracer/tracer_test.go @@ -74,6 +74,12 @@ func TestTracerCleanStop(t *testing.T) { if testing.Short() { return } + // Avoid CI timeouts due to AppSec slowing down this test due to its init + // time. + if old := os.Getenv("DD_APPSEC_ENABLED"); old != "" { + os.Unsetenv("DD_APPSEC_ENABLED") + defer os.Setenv("DD_APPSEC_ENABLED", old) + } os.Setenv("DD_TRACE_STARTUP_LOGS", "0") defer os.Unsetenv("DD_TRACE_STARTUP_LOGS") @@ -325,6 +331,11 @@ func TestTracerRuntimeMetrics(t *testing.T) { }) t.Run("off", func(t *testing.T) { + // Disable AppSec to avoid their logs + if old := os.Getenv("DD_APPSEC_ENABLED"); old != "" { + os.Unsetenv("DD_APPSEC_ENABLED") + defer os.Setenv("DD_APPSEC_ENABLED", old) + } tp := new(testLogger) tracer := newTracer(WithLogger(tp), WithDebugMode(true)) defer tracer.Stop() @@ -1008,7 +1019,7 @@ func TestWorker(t *testing.T) { } flush(-1) - timeout := time.After(1 * time.Second) + timeout := time.After(2 * time.Second) loop: for { select { diff --git a/ddtrace/tracer/transport.go b/ddtrace/tracer/transport.go index 0994ba2d6d..eb93a04451 100644 --- a/ddtrace/tracer/transport.go +++ b/ddtrace/tracer/transport.go @@ -87,9 +87,6 @@ type httpTransport struct { // otherwise needing to customize the transport layer, for instance when using // a unix domain socket. func newHTTPTransport(addr string, client *http.Client) *httpTransport { - if client == nil { - client = defaultClient - } // initialize the default EncoderPool with Encoder headers defaultHeaders := map[string]string{ "Datadog-Meta-Lang": "go", diff --git a/go.mod b/go.mod index 08aac6dbb8..c14cb50b33 100644 --- a/go.mod +++ b/go.mod @@ -7,5 +7,7 @@ require ( github.com/DataDog/gostackparse v0.5.0 github.com/DataDog/sketches-go v1.0.0 github.com/google/pprof v0.0.0-20210423192551-a2663126120b + github.com/google/uuid v1.3.0 + github.com/stretchr/testify v1.7.0 github.com/tinylib/msgp v1.1.2 ) diff --git a/go.sum b/go.sum index b02926739f..cdd1f53fe0 100644 --- a/go.sum +++ b/go.sum @@ -265,6 +265,8 @@ github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm4 github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= @@ -530,6 +532,7 @@ github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzu github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= diff --git a/internal/appsec/api.go b/internal/appsec/api.go new file mode 100644 index 0000000000..818d51d0bf --- /dev/null +++ b/internal/appsec/api.go @@ -0,0 +1,303 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016 Datadog, Inc. + +package appsec + +import ( + "fmt" + "net" + "sort" + "strings" + "time" + + "github.com/google/uuid" +) + +// Intake API payloads. +type ( + // eventBatch intake API payload. + eventBatch struct { + IdempotencyKey string `json:"idempotency_key"` + Events []*attackEvent `json:"events"` + } + + // attackEvent intake API payload. + attackEvent struct { + EventVersion string `json:"event_version"` + EventID string `json:"event_id"` + EventType string `json:"event_type"` + DetectedAt time.Time `json:"detected_at"` + Type string `json:"type"` + Blocked bool `json:"blocked"` + Rule attackRule `json:"rule"` + RuleMatch *attackRuleMatch `json:"rule_match"` + Context attackContext `json:"context"` + } + + // attackRule intake API payload. + attackRule struct { + ID string `json:"id"` + Name string `json:"name"` + } + + // attackRuleMatch intake API payload. + attackRuleMatch struct { + Operator string `json:"operator"` + OperatorValue string `json:"operator_value"` + Parameters []attackRuleMatchParameter `json:"parameters"` + Highlight []string `json:"highlight"` + } + + // attackRuleMatchParameter intake API payload. + attackRuleMatchParameter struct { + Name string `json:"name"` + Value string `json:"value"` + } + + // attackContext intake API payload. + attackContext struct { + Host attackContextHost `json:"host,omitempty"` + HTTP attackContextHTTP `json:"http"` + Service attackContextService `json:"service"` + Tags *attackContextTags `json:"tags,omitempty"` + Span attackContextSpan `json:"span"` + Trace attackContextTrace `json:"trace"` + Tracer attackContextTracer `json:"tracer"` + } + + // attackContextHost intake API payload. + attackContextHost struct { + ContextVersion string `json:"context_version"` + OsType string `json:"os_type"` + Hostname string `json:"hostname,omitempty"` + } + + // attackContextHTTP intake API payload. + attackContextHTTP struct { + ContextVersion string `json:"context_version"` + Request attackContextHTTPRequest `json:"request"` + Response attackContextHTTPResponse `json:"response"` + } + + // attackContextHTTPRequest intake API payload. + attackContextHTTPRequest struct { + Scheme string `json:"scheme"` + Method string `json:"method"` + URL string `json:"url"` + Host string `json:"host"` + Port int `json:"port"` + Path string `json:"path"` + Resource string `json:"resource,omitempty"` + RemoteIP string `json:"remote_ip"` + RemotePort int `json:"remote_port"` + Headers map[string]string `json:"headers"` + Parameters attackContextHTTPRequestParameters `json:"parameters,omitempty"` + } + + attackContextHTTPRequestParameters struct { + Query map[string][]string `json:"query,omitempty"` + } + + // attackContextHTTPResponse intake API payload. + attackContextHTTPResponse struct { + Status int `json:"status"` + } + + // attackContextService intake API payload. + attackContextService struct { + ContextVersion string `json:"context_version"` + Name string `json:"name,omitempty"` + Environment string `json:"environment,omitempty"` + Version string `json:"version,omitempty"` + } + + // attackContextTags intake API payload. + attackContextTags struct { + ContextVersion string `json:"context_version"` + Values []string `json:"values"` + } + + // attackContextTrace intake API payload. + attackContextTrace struct { + ContextVersion string `json:"context_version"` + ID string `json:"id"` + } + + // attackContextSpan intake API payload. + attackContextSpan struct { + ContextVersion string `json:"context_version"` + ID string `json:"id"` + } + + // attackContextTracer intake API payload. + attackContextTracer struct { + ContextVersion string `json:"context_version"` + RuntimeType string `json:"runtime_type"` + RuntimeVersion string `json:"runtime_version"` + LibVersion string `json:"lib_version"` + } +) + +// makeEventBatch returns the event batch of the given security events. +func makeEventBatch(events []*attackEvent) eventBatch { + id, _ := uuid.NewUUID() + return eventBatch{ + IdempotencyKey: id.String(), + Events: events, + } +} + +// newAttackEvent returns a new attack event payload. +func newAttackEvent(ruleID, ruleName, attackType string, at time.Time, match *attackRuleMatch) *attackEvent { + id, _ := uuid.NewUUID() + return &attackEvent{ + EventVersion: "0.1.0", + EventID: id.String(), + EventType: "appsec.threat.attack", + DetectedAt: at, + Type: attackType, + Rule: attackRule{ + ID: ruleID, + Name: ruleName, + }, + RuleMatch: match, + } +} + +// makeAttackContextTrace create an attackContextTrace payload. +func makeAttackContextTrace(traceID string) attackContextTrace { + return attackContextTrace{ + ContextVersion: "0.1.0", + ID: traceID, + } +} + +// makeAttackContextSpan create an attackContextSpan payload. +func makeAttackContextSpan(spanID string) attackContextSpan { + return attackContextSpan{ + ContextVersion: "0.1.0", + ID: spanID, + } +} + +// makeAttackContextHost create an attackContextHost payload. +func makeAttackContextHost(hostname string, os string) attackContextHost { + return attackContextHost{ + ContextVersion: "0.1.0", + OsType: os, + Hostname: hostname, + } +} + +// makeAttackContextTracer create an attackContextTracer payload. +func makeAttackContextTracer(version string, rt string, rtVersion string) attackContextTracer { + return attackContextTracer{ + ContextVersion: "0.1.0", + RuntimeType: rt, + RuntimeVersion: rtVersion, + LibVersion: version, + } +} + +// newAttackContextTags create an attackContextTags payload. +func newAttackContextTags(tags []string) *attackContextTags { + return &attackContextTags{ + ContextVersion: "0.1.0", + Values: tags, + } +} + +// makeServiceContext create an attackContextService payload. +func makeServiceContext(name, version, environment string) attackContextService { + return attackContextService{ + ContextVersion: "0.1.0", + Name: name, + Environment: environment, + Version: version, + } +} + +// makeAttackContextHTTP create an attackContextHTTP payload. +func makeAttackContextHTTP(req attackContextHTTPRequest, res attackContextHTTPResponse) attackContextHTTP { + return attackContextHTTP{ + ContextVersion: "0.1.0", + Request: req, + Response: res, + } +} + +// makeAttackContextHTTPResponse creates an attackContextHTTPResponse payload. +func makeAttackContextHTTPResponse(status int) attackContextHTTPResponse { + return attackContextHTTPResponse{ + Status: status, + } +} + +// splitHostPort splits a network address of the form `host:port` or +// `[host]:port` into `host` and `port`. As opposed to `net.SplitHostPort()`, +// it doesn't fail when there is no port number and returns the given address +// as the host value. +func splitHostPort(addr string) (host, port string) { + addr = strings.TrimSpace(addr) + host, port, err := net.SplitHostPort(addr) + if err == nil { + return + } + if l := len(addr); l >= 2 && addr[0] == '[' && addr[l-1] == ']' { + // ipv6 without port number + return addr[1 : l-1], "" + } + return addr, "" +} + +// List of HTTP headers we collect and send. +var collectedHTTPHeaders = [...]string{ + "host", + "x-forwarded-for", + "x-client-ip", + "x-real-ip", + "x-forwarded", + "x-cluster-client-ip", + "forwarded-for", + "forwarded", + "via", + "true-client-ip", + "content-length", + "content-type", + "content-encoding", + "content-language", + "forwarded", + "user-agent", + "accept", + "accept-encoding", + "accept-language", +} + +func init() { + // Required by sort.SearchStrings + sort.Strings(collectedHTTPHeaders[:]) +} + +// makeHTTPHeaders returns the HTTP headers following the intake payload format. +func makeHTTPHeaders(reqHeaders map[string][]string) (headers map[string]string) { + if len(reqHeaders) == 0 { + return nil + } + headers = make(map[string]string) + for k, v := range reqHeaders { + if i := sort.SearchStrings(collectedHTTPHeaders[:], k); i < len(collectedHTTPHeaders) && collectedHTTPHeaders[i] == k { + headers[k] = strings.Join(v, ";") + } + } + if len(headers) == 0 { + return nil + } + return headers +} + +// makeHTTPURL returns the HTTP URL from the given scheme, host and path. +func makeHTTPURL(scheme, host, path string) string { + return fmt.Sprintf("%s://%s%s", scheme, host, path) +} diff --git a/internal/appsec/api_test.go b/internal/appsec/api_test.go new file mode 100644 index 0000000000..aab4696304 --- /dev/null +++ b/internal/appsec/api_test.go @@ -0,0 +1,98 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016 Datadog, Inc. + +package appsec + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestSplitHostPort(t *testing.T) { + for _, tc := range []struct { + Addr string + ExpectedHost, ExpectedPort string + }{ + { + Addr: "", + ExpectedHost: "", + ExpectedPort: "", + }, + { + Addr: ":33", + ExpectedPort: "33", + }, + { + Addr: "33:", + ExpectedHost: "33", + }, + { + Addr: "[fe80::1.2.3.4]:33", + ExpectedHost: "fe80::1.2.3.4", + ExpectedPort: "33", + }, + { + Addr: "[fe80::1.2.3.4]", + ExpectedHost: "fe80::1.2.3.4", + }, + { + Addr: " [fe80::1.2.3.4] ", + ExpectedHost: "fe80::1.2.3.4", + }, + { + Addr: "localhost:80 ", + ExpectedHost: "localhost", + ExpectedPort: "80", + }, + } { + t.Run(tc.Addr, func(t *testing.T) { + host, port := splitHostPort(tc.Addr) + require.Equal(t, tc.ExpectedHost, host) + require.Equal(t, tc.ExpectedPort, port) + }) + } +} + +func TestMakeHTTPHeaders(t *testing.T) { + for _, tc := range []struct { + headers map[string][]string + expected map[string]string + }{ + { + headers: nil, + expected: nil, + }, + { + headers: map[string][]string{ + "cookie": {"not-collected"}, + }, + expected: nil, + }, + { + headers: map[string][]string{ + "cookie": {"not-collected"}, + "x-forwarded-for": {"1.2.3.4,5.6.7.8"}, + }, + expected: map[string]string{ + "x-forwarded-for": "1.2.3.4,5.6.7.8", + }, + }, + { + headers: map[string][]string{ + "cookie": {"not-collected"}, + "x-forwarded-for": {"1.2.3.4,5.6.7.8", "9.10.11.12,13.14.15.16"}, + }, + expected: map[string]string{ + "x-forwarded-for": "1.2.3.4,5.6.7.8;9.10.11.12,13.14.15.16", + }, + }, + } { + t.Run("makeHTTPHeaders", func(t *testing.T) { + headers := makeHTTPHeaders(tc.headers) + require.Equal(t, tc.expected, headers) + }) + } +} diff --git a/internal/appsec/appsec.go b/internal/appsec/appsec.go new file mode 100644 index 0000000000..de83945fa9 --- /dev/null +++ b/internal/appsec/appsec.go @@ -0,0 +1,270 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016 Datadog, Inc. + +//go:build appsec +// +build appsec + +package appsec + +import ( + "context" + "fmt" + "io/ioutil" + "os" + "runtime" + "strconv" + "sync" + "time" + + "gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/dyngo" + "gopkg.in/DataDog/dd-trace-go.v1/internal/log" +) + +// Default batching configuration values. +const ( + defaultMaxBatchLen = 1024 + defaultMaxBatchStaleTime = time.Second +) + +// Default timeout of intake requests. +const defaultIntakeTimeout = 10 * time.Second + +// Start AppSec when enabled is enabled by both using the appsec build tag and +// setting the environment variable DD_APPSEC_ENABLED to true. +func Start(cfg *Config) { + enabled, err := isEnabled() + if err != nil { + log.Error("appsec: %v", err) + return + } + if !enabled { + return + } + + filepath := os.Getenv("DD_APPSEC_RULES") + if filepath != "" { + rules, err := ioutil.ReadFile(filepath) + if err != nil { + if os.IsNotExist(err) { + log.Error("appsec: could not find the rules file in path %s.\nAppSec will not run any protections in this application. No security activities will be collected: %v", filepath, err) + } else { + logUnexpectedStartError(err) + } + return + } + cfg.rules = rules + log.Info("appsec: using rules from file %s", filepath) + } else { + log.Info("appsec: using the default recommended rules") + } + + appsec, err := newAppSec(cfg) + if err != nil { + logUnexpectedStartError(err) + return + } + if err := appsec.start(); err != nil { + logUnexpectedStartError(err) + return + } + setActiveAppSec(appsec) +} + +// Implement the AppSec log message C1 +func logUnexpectedStartError(err error) { + log.Error("appsec: could not start because of an unexpected error. No security activities will be collected. Please contact support at https://docs.datadoghq.com/help/ for help:\n%v", err) +} + +// Stop AppSec. +func Stop() { + setActiveAppSec(nil) +} + +var ( + activeAppSec *appsec + mu sync.Mutex +) + +func setActiveAppSec(a *appsec) { + mu.Lock() + defer mu.Unlock() + if activeAppSec != nil { + activeAppSec.stop() + } + activeAppSec = a +} + +// isEnabled returns true when appsec is enabled by both using the appsec build +// tag and having the environment variable DD_APPSEC_ENABLED set to true. +func isEnabled() (bool, error) { + enabledStr := os.Getenv("DD_APPSEC_ENABLED") + if enabledStr == "" { + return false, nil + } + enabled, err := strconv.ParseBool(enabledStr) + if err != nil { + return false, fmt.Errorf("could not parse DD_APPSEC_ENABLED value `%s` as a boolean value", enabledStr) + } + return enabled, nil +} + +type appsec struct { + client *client + eventChan chan securityEvent + wg sync.WaitGroup + cfg *Config + unregisterWAF dyngo.UnregisterFunc +} + +func newAppSec(cfg *Config) (*appsec, error) { + intakeClient, err := newClient(cfg.Client, cfg.AgentURL) + if err != nil { + return nil, err + } + + if cfg.MaxBatchLen <= 0 { + cfg.MaxBatchLen = defaultMaxBatchLen + } + + if cfg.MaxBatchStaleTime <= 0 { + cfg.MaxBatchStaleTime = defaultMaxBatchStaleTime + } + + return &appsec{ + eventChan: make(chan securityEvent, 1000), + client: intakeClient, + cfg: cfg, + }, nil +} + +// Start starts the AppSec background goroutine. +func (a *appsec) start() error { + // Register the WAF operation event listener + unregisterWAF, err := registerWAF(a.cfg.rules, a) + if err != nil { + return err + } + a.unregisterWAF = unregisterWAF + + // Start the background goroutine reading the channel of security events and sending them to the backend + a.wg.Add(1) + go func() { + defer a.wg.Done() + + strTags := stringTags(a.cfg.Tags) + osName := fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH) + applyContext := func(event securityEvent) securityEvent { + if len(strTags) > 0 { + event = withTagsContext(event, strTags) + } + event = withServiceContext(event, a.cfg.Service.Name, a.cfg.Service.Version, a.cfg.Service.Environment) + event = withTracerContext(event, "go", runtime.Version(), a.cfg.Version) + event = withHostContext(event, a.cfg.Hostname, osName) + return event + } + eventBatchingLoop(a.client, a.eventChan, applyContext, a.cfg) + }() + + return nil +} + +func stringTags(tagsMap map[string]interface{}) (tags []string) { + tags = make([]string, 0, len(tagsMap)) + for tag, value := range tagsMap { + if str, ok := value.(string); ok { + tags = append(tags, fmt.Sprintf("%s:%v", tag, str)) + } + } + return tags +} + +// Stop stops the AppSec agent goroutine. +func (a *appsec) stop() { + a.unregisterWAF() + // Stop the batching goroutine + close(a.eventChan) + // Gracefully stop by waiting for the event loop goroutine to stop + a.wg.Wait() +} + +type intakeClient interface { + sendBatch(context.Context, eventBatch) error +} + +func eventBatchingLoop(client intakeClient, eventChan <-chan securityEvent, withGlobalContext func(event securityEvent) securityEvent, cfg *Config) { + // The batch of events + batch := make([]securityEvent, 0, cfg.MaxBatchLen) + + // Timer initialized to a first dummy time value to initialize it and so that we can immediately + // use its channel field in the following select statement. + timer := time.NewTimer(time.Hour) + timer.Stop() + + // Helper function stopping the timer, sending the batch and resetting it. + sendBatch := func() { + if !timer.Stop() { + // Remove the time value from the channel in case the timer fired and so that we avoid + // sending the batch again in the next loop iteration due to a value in the timer + // channel. + select { + case <-timer.C: + default: + } + } + ctx, cancel := context.WithTimeout(context.Background(), defaultIntakeTimeout) + defer cancel() + intakeBatch := make([]*attackEvent, 0, len(batch)) + for _, e := range batch { + intakeEvents, err := withGlobalContext(e).toIntakeEvents() + if err != nil { + log.Error("appsec: could not create intake security events: %v", err) + continue + } + intakeBatch = append(intakeBatch, intakeEvents...) + } + log.Debug("appsec: sending %d security events", len(intakeBatch)) + if err := client.sendBatch(ctx, makeEventBatch(intakeBatch)); err != nil { + log.Error("appsec: could not send the event batch: %v", err) + } + batch = batch[0:0] + } + + // Loop-select between the event channel or the stale timer (when enabled). + for { + select { + case event, ok := <-eventChan: + // Add the event to the batch. + // The event might be nil when closing the channel while it was empty. + if event != nil { + batch = append(batch, event) + } + if !ok { + // The event channel has been closed. Send the batch if it's not empty. + if len(batch) > 0 { + sendBatch() + } + return + } + // Send the batch when it's full or start the timer when this is the first value in + // the batch. + if l := len(batch); l == cfg.MaxBatchLen { + sendBatch() + } else if l == 1 { + timer.Reset(cfg.MaxBatchStaleTime) + } + + case <-timer.C: + sendBatch() + } + } +} + +func (a *appsec) sendEvent(event securityEvent) { + select { + case a.eventChan <- event: + default: + // TODO(julio): add metrics on the nb of dropped events + } +} diff --git a/internal/appsec/appsec_disabled.go b/internal/appsec/appsec_disabled.go new file mode 100644 index 0000000000..adbebdc5d1 --- /dev/null +++ b/internal/appsec/appsec_disabled.go @@ -0,0 +1,16 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016 Datadog, Inc. + +//go:build !appsec +// +build !appsec + +package appsec + +// Start AppSec when enabled is enabled by both using the appsec build tag and +// setting the environment variable DD_APPSEC_ENABLED to true. +func Start(*Config) {} + +// Stop AppSec. +func Stop() {} diff --git a/internal/appsec/batching_test.go b/internal/appsec/batching_test.go new file mode 100644 index 0000000000..382cab5127 --- /dev/null +++ b/internal/appsec/batching_test.go @@ -0,0 +1,201 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016 Datadog, Inc. + +//go:build appsec +// +build appsec + +package appsec + +import ( + "context" + "fmt" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/mock" +) + +type IntakeClientMock struct { + mock.Mock + SendBatchCalled chan struct{} +} + +func (i *IntakeClientMock) sendBatch(ctx context.Context, batch eventBatch) error { + err := i.Called(ctx, batch).Error(0) + if i.SendBatchCalled != nil { + i.SendBatchCalled <- struct{}{} + } + return err +} + +func TestEventBatchingLoop(t *testing.T) { + t.Run("batching", func(t *testing.T) { + for _, eventChanLen := range []int{1, 2, 512, 1024} { + eventChanLen := eventChanLen + t.Run(fmt.Sprintf("EventChanLen=%d", eventChanLen), func(t *testing.T) { + for _, maxBatchLen := range []int{1, 2, 512, 1024} { + maxBatchLen := maxBatchLen + t.Run(fmt.Sprintf("MaxBatchLen=%d", maxBatchLen), func(t *testing.T) { + // Send 10 batches of events and check they were properly sent + expectedNbBatches := 10 + client := &IntakeClientMock{ + // Have enough room for the amount of expected batches + SendBatchCalled: make(chan struct{}, expectedNbBatches), + } + eventChan := make(chan securityEvent, eventChanLen) + cfg := &Config{ + MaxBatchLen: maxBatchLen, + MaxBatchStaleTime: time.Hour, // Long enough so that it never triggers and we only test the batching logic + } + + // Start the batching goroutine + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + eventBatchingLoop(client, eventChan, applyGlobalContextNoop, cfg) + }() + + client.On("sendBatch", mock.Anything, mock.AnythingOfType("eventBatch")).Times(expectedNbBatches).Return(nil) + // Send enough events to generate expectedNbBatches + for i := 0; i < maxBatchLen*expectedNbBatches; i++ { + eventChan <- myEvent{} + } + // Sync with the client and check the client calls are being done as expected + for i := 0; i < expectedNbBatches; i++ { + <-client.SendBatchCalled + } + client.AssertExpectations(t) + + // Close the event channel to stop the loop + close(eventChan) + wg.Wait() + }) + } + }) + } + }) + + t.Run("stale-time", func(t *testing.T) { + client := &IntakeClientMock{ + SendBatchCalled: make(chan struct{}, 2), + } + eventChan := make(chan securityEvent, 1024) + maxStaleTime := time.Millisecond + cfg := &Config{ + MaxBatchLen: 1024, + MaxBatchStaleTime: maxStaleTime, + } + + // Start the batching goroutine + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + eventBatchingLoop(client, eventChan, applyGlobalContextNoop, cfg) + }() + + // + client.On("sendBatch", mock.Anything, mock.AnythingOfType("eventBatch")).Times(2).Return(nil) + + // Send a few events and wait for the configured max stale time so that the batch gets sent + eventChan <- myEvent{} + eventChan <- myEvent{} + eventChan <- myEvent{} + eventChan <- myEvent{} + time.Sleep(maxStaleTime) + // Sync with the client + <-client.SendBatchCalled + + // Send a few events and wait for the configured max stale time so that the batch gets sent + eventChan <- myEvent{} + // Sync with the client + <-client.SendBatchCalled + time.Sleep(maxStaleTime) + + // No new events + time.Sleep(maxStaleTime) + + // 2 batches should have been sent + client.AssertExpectations(t) + + // Close the event channel to stop the loop + close(eventChan) + wg.Wait() + }) + + t.Run("canceling-the-loop", func(t *testing.T) { + t.Run("empty-batch", func(t *testing.T) { + client := &IntakeClientMock{} + eventChan := make(chan securityEvent, 1024) + cfg := &Config{ + MaxBatchLen: 1024, + MaxBatchStaleTime: time.Hour, + } + + // Start the batching goroutine + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + eventBatchingLoop(client, eventChan, applyGlobalContextNoop, cfg) + }() + + // No client calls should be made + client.AssertExpectations(t) + + // Close the context to stop the loop + close(eventChan) + // Wait() should therefore return + wg.Wait() + }) + + t.Run("non-empty-batch", func(t *testing.T) { + client := &IntakeClientMock{ + SendBatchCalled: make(chan struct{}, 1), + } + eventChan := make(chan securityEvent, 1024) + cfg := &Config{ + MaxBatchLen: 1024, + MaxBatchStaleTime: time.Hour, + } + + // Start the batching goroutine + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + eventBatchingLoop(client, eventChan, applyGlobalContextNoop, cfg) + }() + + // Perform an event + client.On("sendBatch", mock.Anything, mock.AnythingOfType("eventBatch")).Times(1).Return(nil) + eventChan <- myEvent{} + + // Close the context to stop the loop + close(eventChan) + + // Wait() should therefore return + wg.Wait() + + // The event should be properly sent before returning + <-client.SendBatchCalled + client.AssertExpectations(t) + }) + }) +} + +type myEvent struct{} + +func (m myEvent) toIntakeEvents() ([]*attackEvent, error) { + return []*attackEvent{ + newAttackEvent("my.rule.id", "my.rule.name", "my.attack.type", time.Now(), nil), + }, nil +} + +func applyGlobalContextNoop(e securityEvent) securityEvent { + return e +} diff --git a/internal/appsec/client.go b/internal/appsec/client.go new file mode 100644 index 0000000000..fe5a237f76 --- /dev/null +++ b/internal/appsec/client.go @@ -0,0 +1,188 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016 Datadog, Inc. + +package appsec + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/http/httputil" + "net/url" +) + +// Client is the HTTP client to use to communicate with the intake API via the agent API. +type client struct { + // Logger should be set to obtain debugging logs in debug level to see the HTTP requests and their responses. + Logger debugLogger + client *http.Client + baseURL *url.URL +} + +// debugLogger interface of the debug-level logger. +type debugLogger interface { + Debug(format string, v ...interface{}) +} + +// newClient returns a new intake client using the given HTTP client and base-URL. +func newClient(httpClient *http.Client, baseURL string) (*client, error) { + if httpClient == nil { + httpClient = &http.Client{} + } + u, err := url.Parse(baseURL) + if err != nil { + return nil, err + } + return &client{ + client: httpClient, + baseURL: u, + }, nil +} + +// sendBatch sends the batch. +func (c *client) sendBatch(ctx context.Context, b eventBatch) error { + r, err := c.newRequest("POST", "appsec/proxy/api/v2/appsecevts", b) + if err != nil { + return err + } + return c.do(ctx, r, nil) +} + +func (c *client) newRequest(method, urlStr string, reqBody interface{}) (*http.Request, error) { + u, err := c.baseURL.Parse(urlStr) + if err != nil { + return nil, err + } + + var buf io.ReadWriter + if reqBody != nil { + buf = &bytes.Buffer{} + enc := json.NewEncoder(buf) + enc.SetEscapeHTML(false) + if err := enc.Encode(reqBody); err != nil { + return nil, err + } + } + + req, err := http.NewRequest(method, u.String(), buf) + if err != nil { + return nil, err + } + + if reqBody != nil { + req.Header.Set("Content-Type", "application/json") + } + req.Header.Set("Accept", "application/json") + return req, nil +} + +func (c *client) do(ctx context.Context, req *http.Request, respBody interface{}) error { + if ctx == nil { + return errors.New("context must be non-nil") + } + + req = req.WithContext(ctx) + + c.debug("sending request\n%s\n", (*httpRequestStringer)(req)) + + resp, err := c.client.Do(req) + if err != nil { + return err + } + defer func() { + // Drain the body and close it in order to make the underlying connection + // available again in the pool + _, _ = io.Copy(ioutil.Discard, resp.Body) + _ = resp.Body.Close() + }() + + c.debug("received response\n%s\n", (*httpResponseStringer)(resp)) + + err = checkResponse(resp) + if err != nil { + return err + } + + if respBody != nil { + decErr := json.NewDecoder(resp.Body).Decode(respBody) + if decErr != nil && decErr != io.EOF { + return decErr + } + } + + return nil +} + +func (c *client) debug(fmt string, args ...interface{}) { + if c.Logger == nil { + return + } + c.Logger.Debug(fmt, args...) +} + +type ( + httpRequestStringer http.Request + httpResponseStringer http.Response +) + +func (r *httpRequestStringer) String() string { + dump, _ := httputil.DumpRequestOut((*http.Request)(r), true) + return string(dump) +} + +func (r *httpResponseStringer) String() string { + dump, _ := httputil.DumpResponse((*http.Response)(r), true) + return string(dump) +} + +// Client error types. +type ( + // APIError is the generic request error returned when the request status + // code is unknown. + APIError struct { + Response *http.Response + } + // AuthTokenError is a request error returned when the request could not be + // authenticated. + AuthTokenError APIError + // InvalidSignalError is a request error returned when one or more signal(s) + // sent are invalid. + InvalidSignalError APIError +) + +// Error return the error string representation. +func (e APIError) Error() string { + return fmt.Sprintf("api error: response with status code %s", e.Response.Status) +} + +// Error return the error string representation. +func (e AuthTokenError) Error() string { + return "api error: access token is missing or invalid" +} + +// Error return the error string representation. +func (e InvalidSignalError) Error() string { + return "api error: one of the provided events is invalid" +} + +func checkResponse(r *http.Response) error { + if c := r.StatusCode; 200 <= c && c <= 299 { + return nil + } + errorResponse := APIError{Response: r} + switch r.StatusCode { + case http.StatusUnauthorized: + return AuthTokenError(errorResponse) + case http.StatusUnprocessableEntity: + return InvalidSignalError(errorResponse) + default: + return errorResponse + } +} diff --git a/internal/appsec/client_test.go b/internal/appsec/client_test.go new file mode 100644 index 0000000000..59850eaf24 --- /dev/null +++ b/internal/appsec/client_test.go @@ -0,0 +1,244 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016 Datadog, Inc. + +package appsec + +import ( + "context" + "errors" + "io/ioutil" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func TestClient(t *testing.T) { + t.Run("newClient", func(t *testing.T) { + t.Run("default-client", func(t *testing.T) { + client, err := newClient(nil, "target") + require.NoError(t, err) + require.NotNil(t, client) + }) + + t.Run("given-client", func(t *testing.T) { + httpClient := &http.Client{ + Timeout: 10 * time.Second, + } + client, err := newClient(httpClient, "target") + require.NoError(t, err) + require.NotNil(t, client) + }) + }) + + t.Run("newRequest", func(t *testing.T) { + baseURL := "http://target/" + c, err := newClient(nil, baseURL) + require.NoError(t, err) + + for _, tc := range []struct { + name string + endpoint string + method string + body interface{} + wantError bool + expectedBody string + }{ + { + name: "get-no-body", + endpoint: "endpoint", + method: http.MethodGet, + body: nil, + wantError: false, + }, + { + name: "post-no-body", + endpoint: "endpoint", + method: http.MethodPost, + body: nil, + wantError: false, + }, + { + name: "bad-method", + endpoint: "endpoint", + method: ";", + body: nil, + wantError: true, + }, + { + name: "bad-endpoint", + endpoint: ":endpoint", + method: "GET", + body: nil, + wantError: true, + }, + { + name: "bad-endpoint", + endpoint: ":endpoint", + method: "GET", + body: nil, + wantError: true, + }, + { + name: "post-body", + endpoint: "version/endpoint", + method: http.MethodPost, + body: []string{"a", "b", "c"}, + expectedBody: "[\"a\",\"b\",\"c\"]\n", + }, + { + name: "post-body", + endpoint: "version/endpoint", + method: http.MethodPost, + body: "no html & éscaping <", + expectedBody: "\"no html & éscaping <\"\n", + }, + { + name: "post-error", + endpoint: "version/endpoint", + method: http.MethodPost, + body: jsonMarshalError{}, + wantError: true, + }, + } { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + req, err := c.newRequest(tc.method, tc.endpoint, tc.body) + + if tc.wantError { + require.Error(t, err) + return + } + + require.NoError(t, err) + require.Equal(t, tc.method, req.Method) + require.Equal(t, baseURL+tc.endpoint, req.URL.String()) + if tc.expectedBody != "" { + body, err := ioutil.ReadAll(req.Body) + require.NoError(t, err) + require.Equal(t, tc.expectedBody, string(body)) + } + }) + } + }) + + t.Run("do", func(t *testing.T) { + for _, tc := range []struct { + name string + reqBody interface{} + expectedReqBody string + respBody string + expectedRespBody interface{} + wantError bool + status int + }{ + { + name: "no request nor response bodies", + }, + { + name: "request body without response body", + reqBody: "string", + expectedReqBody: "\"string\"\n", + }, + { + name: "request and response body", + reqBody: "request", + expectedReqBody: "\"request\"\n", + respBody: "\"response\"\n", + expectedRespBody: "response", + }, + { + name: "no request body and response body", + respBody: "\"response\"\n", + expectedRespBody: "response", + }, + { + name: "bad response json", + respBody: "\"oops", + wantError: true, + }, + { + name: "error status code", + status: http.StatusUnprocessableEntity, + wantError: true, + }, + { + name: "ok status code", + status: 200, + }, + } { + tc := tc + t.Run(tc.name, func(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if tc.status != 0 { + w.WriteHeader(tc.status) + } + if tc.expectedReqBody != "" { + require.Equal(t, "application/json", r.Header.Get("Content-Type")) + body, err := ioutil.ReadAll(r.Body) + require.NoError(t, err) + require.Equal(t, tc.expectedReqBody, string(body)) + } + _, _ = w.Write([]byte(tc.respBody)) + })) + defer srv.Close() + + c, err := newClient(srv.Client(), srv.URL) + + req, err := c.newRequest("GET", "endpoint", tc.reqBody) + require.NoError(t, err) + + var respBody interface{} + err = c.do(context.Background(), req, &respBody) + if tc.wantError { + require.Error(t, err) + return + } + require.NoError(t, err) + require.Equal(t, tc.expectedRespBody, respBody) + }) + } + }) + + t.Run("do-with-context", func(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) { + time.Sleep(time.Minute) + })) + defer srv.Close() + + c, err := newClient(srv.Client(), srv.URL) + require.NoError(t, err) + + req, err := c.newRequest("PUT", "endpoint", nil) + require.NoError(t, err) + + ctx, cancel := context.WithTimeout(context.Background(), time.Microsecond) + defer cancel() + err = c.do(ctx, req, nil) + require.Error(t, err) + }) + + t.Run("do-with-context", func(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {})) + defer srv.Close() + + c, err := newClient(srv.Client(), srv.URL) + require.NoError(t, err) + + req, err := c.newRequest("PUT", "endpoint", nil) + require.NoError(t, err) + + err = c.do(nil, req, nil) + require.Error(t, err) + }) +} + +type jsonMarshalError struct{} + +func (jsonMarshalError) UnmarshalJSON([]byte) error { return errors.New("oops") } +func (jsonMarshalError) MarshalJSON() ([]byte, error) { return nil, errors.New("oops") } diff --git a/internal/appsec/dyngo/instrumentation/httpinstr/http.go b/internal/appsec/dyngo/instrumentation/httpinstr/http.go new file mode 100644 index 0000000000..cdbb45fbef --- /dev/null +++ b/internal/appsec/dyngo/instrumentation/httpinstr/http.go @@ -0,0 +1,175 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016 Datadog, Inc. + +// Package httpinstr defines the HTTP operation that can be listened to using +// dyngo's operation instrumentation. It serves as an abstract representation +// of HTTP handler calls. +package httpinstr + +import ( + "net/http" + "net/url" + "os" + "reflect" + "strconv" + "strings" + + "gopkg.in/DataDog/dd-trace-go.v1/ddtrace" + "gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/dyngo" +) + +// Abstract HTTP handler operation definition. +type ( + // HandlerOperationArgs is the HTTP handler operation arguments. + HandlerOperationArgs struct { + Method string + Host string + RemoteAddr string + Path string + IsTLS bool + Span ddtrace.Span + + // RequestURI corresponds to the address `server.request.uri.raw` + RequestURI string + // Headers corresponds to the address `server.request.headers.no_cookies` + Headers map[string][]string + // Cookies corresponds to the address `server.request.cookies` + Cookies map[string][]string + // Query corresponds to the address `server.request.query` + Query url.Values + } + + // HandlerOperationRes is the HTTP handler operation results. + HandlerOperationRes struct { + // Status corresponds to the address `server.response.status` + Status int + } +) + +// enabled is true when appsec is enabled so that WrapHandler only wraps the +// handler when appsec is enabled. +// TODO(julio): remove this as soon as appsec becomes enabled by default +var enabled bool + +func init() { + enabled, _ = strconv.ParseBool(os.Getenv("DD_APPSEC_ENABLED")) +} + +// WrapHandler wraps the given HTTP handler with the abstract HTTP operation defined by HandlerOperationArgs and +// HandlerOperationRes. +func WrapHandler(handler http.Handler, span ddtrace.Span) http.Handler { + if !enabled { + span.SetTag("_dd.appsec.enabled", 0) + return handler + } + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + span.SetTag("_dd.appsec.enabled", 1) + span.SetTag("_dd.runtime_family", "go") + + headers := make(http.Header, len(r.Header)) + for k, v := range r.Header { + k := strings.ToLower(k) + if k == "cookie" { + // Do not include cookies in the request headers + continue + } + headers[k] = v + } + var cookies map[string][]string + if reqCookies := r.Cookies(); len(reqCookies) > 0 { + cookies = make(map[string][]string, len(reqCookies)) + for _, cookie := range reqCookies { + if cookie == nil { + continue + } + cookies[cookie.Name] = append(cookies[cookie.Name], cookie.Value) + } + } + host := r.Host + headers["host"] = []string{host} + op := StartOperation( + HandlerOperationArgs{ + Span: span, + IsTLS: r.TLS != nil, + Method: r.Method, + Host: r.Host, + Path: r.URL.Path, + RequestURI: r.RequestURI, + RemoteAddr: r.RemoteAddr, + Headers: headers, + Cookies: cookies, + // TODO(julio): avoid actively parsing the query string and move to a lazy monitoring of this value with + // the dynamic instrumentation of the Query() method. + Query: r.URL.Query(), + }, + nil, + ) + defer func() { + var status int + if mw, ok := w.(interface{ Status() int }); ok { + status = mw.Status() + } + op.Finish(HandlerOperationRes{Status: status}) + }() + handler.ServeHTTP(w, r) + }) +} + +// TODO(julio): create a go-generate tool to generate the types, vars and methods below + +// Operation type representing an HTTP operation. It must be created with +// StartOperation() and finished with its Finish(). +type Operation struct { + *dyngo.OperationImpl +} + +// StartOperation starts an HTTP handler operation, along with the given +// arguments and parent operation, and emits a start event up in the +// operation stack. When parent is nil, the operation is linked to the global +// root operation. +func StartOperation(args HandlerOperationArgs, parent dyngo.Operation) Operation { + return Operation{OperationImpl: dyngo.StartOperation(args, parent)} +} + +// Finish the HTTP handler operation, along with the given results, and emits a +// finish event up in the operation stack. +func (op Operation) Finish(res HandlerOperationRes) { + op.OperationImpl.Finish(res) +} + +// HTTP handler operation's start and finish event callback function types. +type ( + // OnHandlerOperationStart function type, called when an HTTP handler + // operation starts. + OnHandlerOperationStart func(dyngo.Operation, HandlerOperationArgs) + // OnHandlerOperationFinish function type, called when an HTTP handler + // operation finishes. + OnHandlerOperationFinish func(dyngo.Operation, HandlerOperationRes) +) + +var ( + handlerOperationArgsType = reflect.TypeOf((*HandlerOperationArgs)(nil)).Elem() + handlerOperationResType = reflect.TypeOf((*HandlerOperationRes)(nil)).Elem() +) + +// ListenedType returns the type a OnHandlerOperationStart event listener +// listens to, which is the HandlerOperationArgs type. +func (OnHandlerOperationStart) ListenedType() reflect.Type { return handlerOperationArgsType } + +// Call the underlying event listener function by performing the type-assertion +// on v whose type is the one returned by ListenedType(). +func (f OnHandlerOperationStart) Call(op dyngo.Operation, v interface{}) { + f(op, v.(HandlerOperationArgs)) +} + +// ListenedType returns the type a OnHandlerOperationFinish event listener +// listens to, which is the HandlerOperationRes type. +func (OnHandlerOperationFinish) ListenedType() reflect.Type { return handlerOperationResType } + +// Call the underlying event listener function by performing the type-assertion +// on v whose type is the one returned by ListenedType(). +func (f OnHandlerOperationFinish) Call(op dyngo.Operation, v interface{}) { + f(op, v.(HandlerOperationRes)) +} diff --git a/internal/appsec/dyngo/operation.go b/internal/appsec/dyngo/operation.go new file mode 100644 index 0000000000..af9a39c64c --- /dev/null +++ b/internal/appsec/dyngo/operation.go @@ -0,0 +1,238 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016 Datadog, Inc. + +package dyngo + +import ( + "reflect" + "sort" + "sync" + "sync/atomic" + + "gopkg.in/DataDog/dd-trace-go.v1/internal/log" +) + +// Operation interface type allowing to register event listeners to the +// operation. The event listeners will be automatically removed from the +// operation once it finishes so that it no longer can be called on finished +// operations. +type Operation interface { + // On allows to register an event listener to the operation. The event + // listener will be removed from the operation once it finishes. + On(EventListener) + + // Parent return the parent operation. It returns nil for the root + // operation. + Parent() Operation + + // emitEvent is a private method implemented by OperationImpl. + // We don't want to expose it to avoid allowing users to emit events + // themselves. + emitEvent(argsType reflect.Type, op Operation, v interface{}) + + // register the given event listeners and return the unregistration + // function allowing to remove the event listener from the operation. + register(...EventListener) UnregisterFunc +} + +// EventListener interface allowing to identify the Go type listened to and +// dispatch calls to the underlying event listener function. +type EventListener interface { + // ListenedType returns the Go type the event listener listens to. + ListenedType() reflect.Type + // Call the underlying event listener function. The type of the value v + // is the type the event listener listens to, according to the type + // returned by ListenedType(). + Call(op Operation, v interface{}) +} + +// UnregisterFunc is a function allowing to unregister from an operation the previously registered event listeners. +type UnregisterFunc func() + +var rootOperation = newOperation(nil) + +// Register global operation event listeners to listen to. +func Register(listeners ...EventListener) UnregisterFunc { + return rootOperation.register(listeners...) +} + +// OperationImpl structure allowing to subscribe to operation events and to navigate in the operation stack. Events +// bubble-up the operation stack, which allows listening to future events that might happen in the operation lifetime. +type OperationImpl struct { + parent Operation + eventRegister + + disabled bool + mu sync.RWMutex +} + +// StartOperation starts a new operation along with its arguments and emits a start event with the operation arguments. +func StartOperation(args interface{}, parent Operation) *OperationImpl { + if parent == nil { + parent = rootOperation + } + newOp := newOperation(parent) + argsType := reflect.TypeOf(args) + // Bubble-up the start event starting from the parent operation as you can't listen for your own start event + for op := parent; op != nil; op = op.Parent() { + op.emitEvent(argsType, newOp, args) + } + return newOp +} + +func newOperation(parent Operation) *OperationImpl { + return &OperationImpl{parent: parent} +} + +// Parent return the parent operation. It returns nil for the root operation. +func (o *OperationImpl) Parent() Operation { + return o.parent +} + +// Finish finishes the operation along with its results and emits a finish event with the operation results. +// The operation is then disabled and its event listeners removed. +func (o *OperationImpl) Finish(results interface{}) { + o.mu.RLock() + defer o.mu.RUnlock() + if o.disabled { + return + } + defer o.disable() + resType := reflect.TypeOf(results) + for op := Operation(o); op != nil; op = op.Parent() { + op.emitEvent(resType, o, results) + } +} + +func (o *OperationImpl) disable() { + o.disabled = true + o.eventRegister.clear() +} + +// Register allows to register the given event listeners to the operation. An unregistration function is returned +// allowing to unregister the event listeners from the operation. +func (o *OperationImpl) register(l ...EventListener) UnregisterFunc { + // eventRegisterIndex allows to lookup for the event listener in the event register. + type eventRegisterIndex struct { + key reflect.Type + id eventListenerID + } + o.mu.RLock() + defer o.mu.RUnlock() + if o.disabled { + return func() {} + } + indices := make([]eventRegisterIndex, len(l)) + for i, l := range l { + if l == nil { + continue + } + key := l.ListenedType() + id := o.eventRegister.add(key, l) + indices[i] = eventRegisterIndex{ + key: key, + id: id, + } + } + return func() { + for _, ix := range indices { + o.eventRegister.remove(ix.key, ix.id) + } + } +} + +// On registers the event listener. The difference with the Register() is that it doesn't return a function closure, +// which avoids unnecessary allocations +// For example: +// op.On(HTTPHandlerOperationStart(func (op *OperationImpl, args HTTPHandlerOperationArgs) { +// // ... +// })) +func (o *OperationImpl) On(l EventListener) { + o.mu.RLock() + defer o.mu.RUnlock() + if o.disabled { + return + } + o.eventRegister.add(l.ListenedType(), l) +} + +type ( + // eventRegister implements a thread-safe list of event listeners. + eventRegister struct { + mu sync.RWMutex + listeners eventListenerMap + } + + // eventListenerMap is the map of event listeners. The list of listeners are indexed by the operation argument or + // result type the event listener expects. + eventListenerMap map[reflect.Type][]eventListenerMapEntry + eventListenerMapEntry struct { + id eventListenerID + listener EventListener + } + + // eventListenerID is the unique ID of an event when registering it. It allows to find it back and remove it from + // the list of event listeners when unregistering it. + eventListenerID uint32 +) + +// lastID is the last event listener ID that was given to the latest event listener. +var lastID eventListenerID + +// nextID atomically increments lastID and returns the new event listener ID to use. +func nextID() eventListenerID { + return eventListenerID(atomic.AddUint32((*uint32)(&lastID), 1)) +} + +func (r *eventRegister) add(key reflect.Type, l EventListener) eventListenerID { + r.mu.Lock() + defer r.mu.Unlock() + if r.listeners == nil { + r.listeners = make(eventListenerMap) + } + // id is computed when the lock is exclusively taken so that we know listeners are added in incremental id order. + // This allows to use the optimized sort.Search() function to remove the entry. + id := nextID() + r.listeners[key] = append(r.listeners[key], eventListenerMapEntry{ + id: id, + listener: l, + }) + return id +} + +func (r *eventRegister) remove(key reflect.Type, id eventListenerID) { + r.mu.Lock() + defer r.mu.Unlock() + if r.listeners == nil { + return + } + listeners := r.listeners[key] + length := len(listeners) + i := sort.Search(length, func(i int) bool { + return listeners[i].id >= id + }) + if i < length && listeners[i].id == id { + r.listeners[key] = append(listeners[:i], listeners[i+1:]...) + } +} + +func (r *eventRegister) clear() { + r.mu.Lock() + defer r.mu.Unlock() + r.listeners = nil +} + +func (r *eventRegister) emitEvent(key reflect.Type, op Operation, v interface{}) { + defer func() { + if r := recover(); r != nil { + log.Error("appsec: recovered from an unexpected panic from an event listener: %+v", r) + } + }() + r.mu.RLock() + defer r.mu.RUnlock() + for _, e := range r.listeners[key] { + e.listener.Call(op, v) + } +} diff --git a/internal/appsec/dyngo/operation_test.go b/internal/appsec/dyngo/operation_test.go new file mode 100644 index 0000000000..93365ede99 --- /dev/null +++ b/internal/appsec/dyngo/operation_test.go @@ -0,0 +1,971 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016 Datadog, Inc. + +package dyngo_test + +import ( + "errors" + "fmt" + "io" + "net/http" + "net/url" + "reflect" + "strings" + "sync" + "sync/atomic" + "testing" + + "github.com/stretchr/testify/require" + + "gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/dyngo" +) + +// Dummy struct to mimic real-life operation stacks. +type ( + RootArgs struct{} + RootRes struct{} +) + +type ( + HTTPHandlerArgs struct { + URL *url.URL + Headers http.Header + } + HTTPHandlerRes struct{} + OnHTTPHandlerOperationStart func(dyngo.Operation, HTTPHandlerArgs) + OnHTTPHandlerOperationFinish func(dyngo.Operation, HTTPHandlerRes) +) + +func (f OnHTTPHandlerOperationStart) ListenedType() reflect.Type { + return reflect.TypeOf((*HTTPHandlerArgs)(nil)).Elem() +} +func (f OnHTTPHandlerOperationStart) Call(op dyngo.Operation, v interface{}) { + f(op, v.(HTTPHandlerArgs)) +} +func (f OnHTTPHandlerOperationFinish) ListenedType() reflect.Type { + return reflect.TypeOf((*HTTPHandlerRes)(nil)).Elem() +} +func (f OnHTTPHandlerOperationFinish) Call(op dyngo.Operation, v interface{}) { + f(op, v.(HTTPHandlerRes)) +} + +type ( + SQLQueryArgs struct { + Query string + } + SQLQueryRes struct { + Err error + } + OnSQLQueryOperationStart func(dyngo.Operation, SQLQueryArgs) + OnSQLQueryOperationFinish func(dyngo.Operation, SQLQueryRes) +) + +func (f OnSQLQueryOperationStart) ListenedType() reflect.Type { + return reflect.TypeOf((*SQLQueryArgs)(nil)).Elem() +} +func (f OnSQLQueryOperationStart) Call(op dyngo.Operation, v interface{}) { + f(op, v.(SQLQueryArgs)) +} +func (f OnSQLQueryOperationFinish) ListenedType() reflect.Type { + return reflect.TypeOf((*SQLQueryRes)(nil)).Elem() +} +func (f OnSQLQueryOperationFinish) Call(op dyngo.Operation, v interface{}) { + f(op, v.(SQLQueryRes)) +} + +type ( + GRPCHandlerArgs struct { + Msg interface{} + } + GRPCHandlerRes struct { + Res interface{} + } + OnGRPCHandlerOperationStart func(dyngo.Operation, GRPCHandlerArgs) + OnGRPCHandlerOperationFinish func(dyngo.Operation, GRPCHandlerRes) +) + +func (f OnGRPCHandlerOperationStart) ListenedType() reflect.Type { + return reflect.TypeOf((*GRPCHandlerArgs)(nil)).Elem() +} +func (f OnGRPCHandlerOperationStart) Call(op dyngo.Operation, v interface{}) { + f(op, v.(GRPCHandlerArgs)) +} +func (f OnGRPCHandlerOperationFinish) ListenedType() reflect.Type { + return reflect.TypeOf((*GRPCHandlerRes)(nil)).Elem() +} +func (f OnGRPCHandlerOperationFinish) Call(op dyngo.Operation, v interface{}) { + f(op, v.(GRPCHandlerRes)) +} + +type ( + JSONParserArgs struct { + Buf []byte + } + JSONParserRes struct { + Value interface{} + Err error + } + OnJSONParserOperationStart func(dyngo.Operation, JSONParserArgs) + OnJSONParserOperationFinish func(dyngo.Operation, JSONParserRes) +) + +func (f OnJSONParserOperationStart) ListenedType() reflect.Type { + return reflect.TypeOf((*JSONParserArgs)(nil)).Elem() +} +func (f OnJSONParserOperationStart) Call(op dyngo.Operation, v interface{}) { + f(op, v.(JSONParserArgs)) +} +func (f OnJSONParserOperationFinish) ListenedType() reflect.Type { + return reflect.TypeOf((*JSONParserRes)(nil)).Elem() +} +func (f OnJSONParserOperationFinish) Call(op dyngo.Operation, v interface{}) { + f(op, v.(JSONParserRes)) +} + +type ( + BodyReadArgs struct{} + BodyReadRes struct { + Buf []byte + Err error + } + OnBodyReadOperationStart func(dyngo.Operation, BodyReadArgs) + OnBodyReadOperationFinish func(dyngo.Operation, BodyReadRes) +) + +func (f OnBodyReadOperationStart) ListenedType() reflect.Type { + return reflect.TypeOf((*BodyReadArgs)(nil)).Elem() +} +func (f OnBodyReadOperationStart) Call(op dyngo.Operation, v interface{}) { + f(op, v.(BodyReadArgs)) +} +func (f OnBodyReadOperationFinish) ListenedType() reflect.Type { + return reflect.TypeOf((*BodyReadRes)(nil)).Elem() +} +func (f OnBodyReadOperationFinish) Call(op dyngo.Operation, v interface{}) { + f(op, v.(BodyReadRes)) +} + +type ( + MyOperationArgs struct{ n int } + MyOperationRes struct{ n int } + OnMyOperationStart func(dyngo.Operation, MyOperationArgs) + OnMyOperationFinish func(dyngo.Operation, MyOperationRes) +) + +func (f OnMyOperationStart) ListenedType() reflect.Type { + return reflect.TypeOf((*MyOperationArgs)(nil)).Elem() +} +func (f OnMyOperationStart) Call(op dyngo.Operation, v interface{}) { + f(op, v.(MyOperationArgs)) +} +func (f OnMyOperationFinish) ListenedType() reflect.Type { + return reflect.TypeOf((*MyOperationRes)(nil)).Elem() +} +func (f OnMyOperationFinish) Call(op dyngo.Operation, v interface{}) { + f(op, v.(MyOperationRes)) +} + +type ( + MyOperation2Args struct{} + MyOperation2Res struct{} + OnMyOperation2Start func(dyngo.Operation, MyOperation2Args) + OnMyOperation2Finish func(dyngo.Operation, MyOperation2Res) +) + +func (f OnMyOperation2Start) ListenedType() reflect.Type { + return reflect.TypeOf((*MyOperation2Args)(nil)).Elem() +} +func (f OnMyOperation2Start) Call(op dyngo.Operation, v interface{}) { + f(op, v.(MyOperation2Args)) +} +func (f OnMyOperation2Finish) ListenedType() reflect.Type { + return reflect.TypeOf((*MyOperation2Res)(nil)).Elem() +} +func (f OnMyOperation2Finish) Call(op dyngo.Operation, v interface{}) { + f(op, v.(MyOperation2Res)) +} + +type ( + MyOperation3Args struct{} + MyOperation3Res struct{} + OnMyOperation3Start func(dyngo.Operation, MyOperation3Args) + OnMyOperation3Finish func(dyngo.Operation, MyOperation3Res) +) + +func (f OnMyOperation3Start) ListenedType() reflect.Type { + return reflect.TypeOf((*MyOperation3Args)(nil)).Elem() +} +func (f OnMyOperation3Start) Call(op dyngo.Operation, v interface{}) { + f(op, v.(MyOperation3Args)) +} +func (f OnMyOperation3Finish) ListenedType() reflect.Type { + return reflect.TypeOf((*MyOperation3Res)(nil)).Elem() +} +func (f OnMyOperation3Finish) Call(op dyngo.Operation, v interface{}) { + f(op, v.(MyOperation3Res)) +} + +func TestUsage(t *testing.T) { + t.Run("operation-stacking", func(t *testing.T) { + // HTTP body read listener appending the read results to a buffer + rawBodyListener := func(called *int, buf *[]byte) dyngo.EventListener { + return OnHTTPHandlerOperationStart(func(op dyngo.Operation, _ HTTPHandlerArgs) { + op.On(OnBodyReadOperationFinish(func(op dyngo.Operation, res BodyReadRes) { + *called++ + *buf = append(*buf, res.Buf...) + })) + }) + } + + // Dummy waf looking for the string `attack` in HTTPHandlerArgs + wafListener := func(called *int, blocked *bool) dyngo.EventListener { + return OnHTTPHandlerOperationStart(func(op dyngo.Operation, args HTTPHandlerArgs) { + *called++ + + if strings.Contains(args.URL.RawQuery, "attack") { + *blocked = true + return + } + for _, values := range args.Headers { + for _, v := range values { + if strings.Contains(v, "attack") { + *blocked = true + return + } + } + } + }) + } + + jsonBodyValueListener := func(called *int, value *interface{}) dyngo.EventListener { + return OnHTTPHandlerOperationStart(func(op dyngo.Operation, _ HTTPHandlerArgs) { + op.On(OnJSONParserOperationStart(func(op dyngo.Operation, v JSONParserArgs) { + didBodyRead := false + + op.On(OnBodyReadOperationStart(func(_ dyngo.Operation, _ BodyReadArgs) { + didBodyRead = true + })) + + op.On(OnJSONParserOperationFinish(func(op dyngo.Operation, res JSONParserRes) { + *called++ + if !didBodyRead || res.Err != nil { + return + } + *value = res.Value + })) + })) + }) + } + + t.Run("operation-stacking", func(t *testing.T) { + // Run an operation stack that is monitored and not blocked by waf + root := dyngo.StartOperation(RootArgs{}, nil) + + var ( + WAFBlocked bool + WAFCalled int + ) + wafListener := wafListener(&WAFCalled, &WAFBlocked) + + var ( + RawBodyBuf []byte + RawBodyCalled int + ) + rawBodyListener := rawBodyListener(&RawBodyCalled, &RawBodyBuf) + + var ( + JSONBodyParserValue interface{} + JSONBodyParserCalled int + ) + jsonBodyValueListener := jsonBodyValueListener(&JSONBodyParserCalled, &JSONBodyParserValue) + + root.On(rawBodyListener) + root.On(wafListener) + root.On(jsonBodyValueListener) + + // Run the monitored stack of operations + operation( + root, + HTTPHandlerArgs{ + URL: &url.URL{RawQuery: "?v=ok"}, + Headers: http.Header{"header": []string{"value"}}}, + HTTPHandlerRes{}, + func(op dyngo.Operation) { + operation(op, JSONParserArgs{}, JSONParserRes{Value: []interface{}{"a", "json", "array"}}, func(op dyngo.Operation) { + operation(op, BodyReadArgs{}, BodyReadRes{Buf: []byte("my ")}, nil) + operation(op, BodyReadArgs{}, BodyReadRes{Buf: []byte("raw ")}, nil) + operation(op, BodyReadArgs{}, BodyReadRes{Buf: []byte("bo")}, nil) + operation(op, BodyReadArgs{}, BodyReadRes{Buf: []byte("dy"), Err: io.EOF}, nil) + }) + operation(op, SQLQueryArgs{}, SQLQueryRes{}, nil) + }, + ) + + // WAF callback called without blocking + require.False(t, WAFBlocked) + require.Equal(t, 1, WAFCalled) + + // The raw body listener has been called + require.Equal(t, []byte("my raw body"), RawBodyBuf) + require.Equal(t, 4, RawBodyCalled) + + // The json body value listener has been called + require.Equal(t, 1, JSONBodyParserCalled) + require.Equal(t, []interface{}{"a", "json", "array"}, JSONBodyParserValue) + }) + + t.Run("operation-stacking", func(t *testing.T) { + // Operation stack monitored and blocked by waf via the http operation monitoring + root := dyngo.StartOperation(RootArgs{}, nil) + + var ( + WAFBlocked bool + WAFCalled int + ) + wafListener := wafListener(&WAFCalled, &WAFBlocked) + + var ( + RawBodyBuf []byte + RawBodyCalled int + ) + rawBodyListener := rawBodyListener(&RawBodyCalled, &RawBodyBuf) + + var ( + JSONBodyParserValue interface{} + JSONBodyParserCalled int + ) + jsonBodyValueListener := jsonBodyValueListener(&JSONBodyParserCalled, &JSONBodyParserValue) + + root.On(rawBodyListener) + root.On(wafListener) + root.On(jsonBodyValueListener) + + // Run the monitored stack of operations + RawBodyBuf = nil + operation( + root, + HTTPHandlerArgs{ + URL: &url.URL{RawQuery: "?v=attack"}, + Headers: http.Header{"header": []string{"value"}}}, + HTTPHandlerRes{}, + func(op dyngo.Operation) { + operation(op, JSONParserArgs{}, JSONParserRes{Value: "a string", Err: errors.New("an error")}, func(op dyngo.Operation) { + operation(op, BodyReadArgs{}, BodyReadRes{Buf: []byte("another ")}, nil) + operation(op, BodyReadArgs{}, BodyReadRes{Buf: []byte("raw ")}, nil) + operation(op, BodyReadArgs{}, BodyReadRes{Buf: []byte("bo")}, nil) + operation(op, BodyReadArgs{}, BodyReadRes{Buf: []byte("dy"), Err: nil}, nil) + operation(op, BodyReadArgs{}, BodyReadRes{Buf: []byte(" value"), Err: io.EOF}, nil) + }) + + operation(op, SQLQueryArgs{}, SQLQueryRes{}, nil) + }, + ) + + // WAF callback called and blocked + require.True(t, WAFBlocked) + require.Equal(t, 1, WAFCalled) + + // The raw body listener has been called + require.Equal(t, 5, RawBodyCalled) + require.Equal(t, []byte("another raw body value"), RawBodyBuf) + + // The json body value listener has been called but no value due to a parser error + require.Equal(t, 1, JSONBodyParserCalled) + require.Equal(t, nil, JSONBodyParserValue) + }) + + t.Run("operation-stack", func(t *testing.T) { + // Operation stack not monitored + root := dyngo.StartOperation(RootArgs{}, nil) + + var ( + WAFBlocked bool + WAFCalled int + ) + wafListener := wafListener(&WAFCalled, &WAFBlocked) + + var ( + RawBodyBuf []byte + RawBodyCalled int + ) + rawBodyListener := rawBodyListener(&RawBodyCalled, &RawBodyBuf) + + var ( + JSONBodyParserValue interface{} + JSONBodyParserCalled int + ) + jsonBodyValueListener := jsonBodyValueListener(&JSONBodyParserCalled, &JSONBodyParserValue) + + root.On(rawBodyListener) + root.On(wafListener) + root.On(jsonBodyValueListener) + + // Run the monitored stack of operations + operation( + root, + GRPCHandlerArgs{}, GRPCHandlerRes{}, + func(op dyngo.Operation) { + operation(op, JSONParserArgs{}, JSONParserRes{Value: []interface{}{"a", "json", "array"}}, func(op dyngo.Operation) { + operation(op, BodyReadArgs{}, BodyReadRes{Buf: []byte("my ")}, nil) + operation(op, BodyReadArgs{}, BodyReadRes{Buf: []byte("raw ")}, nil) + operation(op, BodyReadArgs{}, BodyReadRes{Buf: []byte("bo")}, nil) + operation(op, BodyReadArgs{}, BodyReadRes{Buf: []byte("dy"), Err: io.EOF}, nil) + }) + operation(op, SQLQueryArgs{}, SQLQueryRes{}, nil) + }, + ) + + // WAF callback called without blocking + require.False(t, WAFBlocked) + require.Equal(t, 0, WAFCalled) + + // The raw body listener has been called + require.Nil(t, RawBodyBuf) + require.Equal(t, 0, RawBodyCalled) + + // The json body value listener has been called + require.Equal(t, 0, JSONBodyParserCalled) + require.Nil(t, JSONBodyParserValue) + }) + }) + + t.Run("recursive-operation", func(t *testing.T) { + root := dyngo.StartOperation(RootArgs{}, nil) + defer root.Finish(RootRes{}) + + called := 0 + root.On(OnHTTPHandlerOperationStart(func(dyngo.Operation, HTTPHandlerArgs) { + called++ + })) + + operation(root, HTTPHandlerArgs{}, HTTPHandlerRes{}, func(o dyngo.Operation) { + operation(o, HTTPHandlerArgs{}, HTTPHandlerRes{}, func(o dyngo.Operation) { + operation(o, HTTPHandlerArgs{}, HTTPHandlerRes{}, func(o dyngo.Operation) { + operation(o, HTTPHandlerArgs{}, HTTPHandlerRes{}, func(o dyngo.Operation) { + operation(o, HTTPHandlerArgs{}, HTTPHandlerRes{}, func(dyngo.Operation) { + }) + }) + }) + }) + }) + + require.Equal(t, 5, called) + }) + + t.Run("concurrency", func(t *testing.T) { + // root is the shared operation having concurrent accesses in this test + root := dyngo.StartOperation(RootArgs{}, nil) + defer root.Finish(RootRes{}) + + // Create nbGoroutines registering event listeners concurrently + nbGoroutines := 1000 + // The concurrency is maximized by using start barriers to sync the goroutine launches + var done, started, startBarrier sync.WaitGroup + + done.Add(nbGoroutines) + started.Add(nbGoroutines) + startBarrier.Add(1) + + var calls uint32 + for g := 0; g < nbGoroutines; g++ { + // Start a goroutine that registers its event listeners to root. + // This allows to test the thread-safety of the underlying list of listeners. + go func() { + started.Done() + startBarrier.Wait() + defer done.Done() + root.On(OnMyOperationStart(func(dyngo.Operation, MyOperationArgs) { atomic.AddUint32(&calls, 1) })) + root.On(OnMyOperationFinish(func(dyngo.Operation, MyOperationRes) { atomic.AddUint32(&calls, 1) })) + }() + } + + // Wait for all the goroutines to be started + started.Wait() + // Release the start barrier + startBarrier.Done() + // Wait for the goroutines to be done + done.Wait() + + // Create nbGoroutines emitting events concurrently + done.Add(nbGoroutines) + started.Add(nbGoroutines) + startBarrier.Add(1) + for g := 0; g < nbGoroutines; g++ { + // Start a goroutine that emits the events with a new operation. This allows to test the thread-safety of + // while emitting events. + go func() { + started.Done() + startBarrier.Wait() + defer done.Done() + op := dyngo.StartOperation(MyOperationArgs{}, root) + defer op.Finish(MyOperationRes{}) + }() + } + + // Wait for all the goroutines to be started + started.Wait() + // Release the start barrier + startBarrier.Done() + // Wait for the goroutines to be done + done.Wait() + + // The number of calls should be equal to the expected number of events + require.Equal(t, uint32(nbGoroutines*2*nbGoroutines), atomic.LoadUint32(&calls)) + }) + + t.Run("concurrency", func(t *testing.T) { + // Create nbGoroutines registering event listeners concurrently + nbGoroutines := 1000 + // The concurrency is maximized by using start barriers to sync the goroutine launches + var done, startBarrier sync.WaitGroup + + done.Add(nbGoroutines) + startBarrier.Add(nbGoroutines + 1) + + var calls uint32 + for g := 0; g < nbGoroutines; g++ { + // Start a goroutine that registers its event listeners to root, emits those events with a new operation, and + // finally unregisters them. This allows to test the thread-safety of the underlying list of listeners. + // To make the number of calls predictable, the event listener increases the number of calls only when it + // comes from the goroutine emitting the event. + go func(g int) { + startBarrier.Done() + startBarrier.Wait() + defer done.Done() + + unregister := dyngo.Register(OnMyOperationStart(func(_ dyngo.Operation, a MyOperationArgs) { + if a.n == g { + atomic.AddUint32(&calls, 1) + } + })) + defer unregister() + unregister = dyngo.Register(OnMyOperationFinish(func(_ dyngo.Operation, r MyOperationRes) { + if r.n == g { + atomic.AddUint32(&calls, 1) + } + })) + defer unregister() + + // Emit events by passing the goroutine number g + op := dyngo.StartOperation(MyOperationArgs{g}, nil) + defer op.Finish(MyOperationRes{g}) + }(g) + } + + // Wait for all the goroutines to be started + startBarrier.Done() + startBarrier.Wait() + // Wait for the goroutines to be done + done.Wait() + + // The number of calls should be equal to the expected number of events + require.Equal(t, uint32(nbGoroutines*2), calls) + }) +} + +func TestRegisterUnregister(t *testing.T) { + t.Run("single-listener", func(t *testing.T) { + var called int + unregister := dyngo.Register(OnMyOperationStart(func(dyngo.Operation, MyOperationArgs) { + called++ + })) + + op := dyngo.StartOperation(MyOperationArgs{}, nil) + require.Equal(t, 1, called) + op.Finish(MyOperationRes{}) + + unregister() + op = dyngo.StartOperation(MyOperationArgs{}, nil) + require.Equal(t, 1, called) + op.Finish(MyOperationRes{}) + + require.NotPanics(t, func() { + unregister() + }) + op = dyngo.StartOperation(MyOperationArgs{}, nil) + require.Equal(t, 1, called) + op.Finish(MyOperationRes{}) + }) + + t.Run("multiple-listeners", func(t *testing.T) { + var onStartCalled, onFinishCalled int + + unregister := dyngo.Register( + OnMyOperationStart(func(dyngo.Operation, MyOperationArgs) { + onStartCalled++ + }), + OnMyOperationFinish(func(dyngo.Operation, MyOperationRes) { + onFinishCalled++ + }), + ) + + operation(nil, MyOperationArgs{}, MyOperationRes{}, func(op dyngo.Operation) {}) + require.Equal(t, 1, onStartCalled) + require.Equal(t, 1, onFinishCalled) + + unregister() + operation(nil, MyOperationArgs{}, MyOperationRes{}, func(op dyngo.Operation) {}) + require.Equal(t, 1, onStartCalled) + require.Equal(t, 1, onFinishCalled) + + require.NotPanics(t, func() { + unregister() + }) + }) +} + +func operation(parent dyngo.Operation, args, res interface{}, child func(dyngo.Operation)) { + op := dyngo.StartOperation(args, parent) + defer op.Finish(res) + if child != nil { + child(op) + } +} + +func TestOperationEvents(t *testing.T) { + t.Run("start-event", func(t *testing.T) { + op1 := dyngo.StartOperation(MyOperationArgs{}, nil) + + var called int + op1.On(OnMyOperation2Start(func(dyngo.Operation, MyOperation2Args) { + called++ + })) + + op2 := dyngo.StartOperation(MyOperation2Args{}, op1) + op2.Finish(MyOperation2Res{}) + + // Called once + require.Equal(t, 1, called) + + op2 = dyngo.StartOperation(MyOperation2Args{}, op1) + op2.Finish(MyOperation2Res{}) + + // Called again + require.Equal(t, 2, called) + + // Finish the operation so that it gets disabled and its listeners removed + op1.Finish(MyOperationRes{}) + + op2 = dyngo.StartOperation(MyOperation2Args{}, op1) + op2.Finish(MyOperation2Res{}) + + // No longer called + require.Equal(t, 2, called) + }) + + t.Run("finish-event", func(t *testing.T) { + op1 := dyngo.StartOperation(MyOperationArgs{}, nil) + + var called int + op1.On(OnMyOperation2Finish(func(dyngo.Operation, MyOperation2Res) { + called++ + })) + + op2 := dyngo.StartOperation(MyOperation2Args{}, op1) + op2.Finish(MyOperation2Res{}) + // Called once + require.Equal(t, 1, called) + + op2 = dyngo.StartOperation(MyOperation2Args{}, op1) + op2.Finish(MyOperation2Res{}) + // Called again + require.Equal(t, 2, called) + + op3 := dyngo.StartOperation(MyOperation3Args{}, op2) + op3.Finish(MyOperation3Res{}) + // Not called + require.Equal(t, 2, called) + + op2 = dyngo.StartOperation(MyOperation2Args{}, op3) + op2.Finish(MyOperation2Res{}) + // Called again + require.Equal(t, 3, called) + + // Finish the operation so that it gets disabled and its listeners removed + op1.Finish(MyOperationRes{}) + + op2 = dyngo.StartOperation(MyOperation2Args{}, op3) + op2.Finish(MyOperation2Res{}) + // No longer called + require.Equal(t, 3, called) + + op2 = dyngo.StartOperation(MyOperation2Args{}, op2) + op2.Finish(MyOperation2Res{}) + // No longer called + require.Equal(t, 3, called) + }) + + t.Run("disabled-operation-registration", func(t *testing.T) { + var calls int + registerTo := func(op dyngo.Operation) { + op.On(OnMyOperation2Start(func(dyngo.Operation, MyOperation2Args) { + calls++ + })) + op.On(OnMyOperation2Finish(func(dyngo.Operation, MyOperation2Res) { + calls++ + })) + } + + // Start an operation and register event listeners to it. + // This step allows to test the listeners are called when the operation is alive + op := dyngo.StartOperation(MyOperationArgs{}, nil) + registerTo(op) + + // Trigger the registered events + op2 := dyngo.StartOperation(MyOperation2Args{}, op) + op2.Finish(MyOperation2Res{}) + // We should have 4 calls + require.Equal(t, 2, calls) + + // Finish the operation to disable it. Its event listeners should then be removed. + op.Finish(MyOperationRes{}) + + // Trigger the same events + op2 = dyngo.StartOperation(MyOperation2Args{}, op) + op2.Finish(MyOperation2Res{}) + // The number of calls should be unchanged + require.Equal(t, 2, calls) + + // Register again, but it shouldn't work because the operation is finished. + registerTo(op) + // Trigger the same events + op2 = dyngo.StartOperation(MyOperation2Args{}, op) + op2.Finish(MyOperation2Res{}) + // The number of calls should be unchanged + require.Equal(t, 2, calls) + }) + + t.Run("event-listener-panic", func(t *testing.T) { + t.Run("start", func(t *testing.T) { + op := dyngo.StartOperation(MyOperationArgs{}, nil) + defer op.Finish(MyOperationRes{}) + + // Panic on start + calls := 0 + op.On(OnMyOperationStart(func(dyngo.Operation, MyOperationArgs) { + // Call counter to check we actually call this listener + calls++ + panic(errors.New("oops")) + })) + // Start the operation triggering the event: it should not panic + require.NotPanics(t, func() { + op := dyngo.StartOperation(MyOperationArgs{}, op) + require.NotNil(t, op) + defer op.Finish(MyOperationRes{}) + require.Equal(t, calls, 1) + }) + }) + + t.Run("finish", func(t *testing.T) { + op := dyngo.StartOperation(MyOperationArgs{}, nil) + defer op.Finish(MyOperationRes{}) + // Panic on finish + calls := 0 + op.On(OnMyOperationFinish(func(dyngo.Operation, MyOperationRes) { + // Call counter to check we actually call this listener + calls++ + panic(errors.New("oops")) + })) + // Run the operation triggering the finish event: it should not panic + require.NotPanics(t, func() { + op := dyngo.StartOperation(MyOperationArgs{}, op) + require.NotNil(t, op) + op.Finish(MyOperationRes{}) + require.Equal(t, calls, 1) + }) + }) + }) +} + +func BenchmarkEvents(b *testing.B) { + b.Run("emitting", func(b *testing.B) { + // Benchmark the emission of events according to the operation stack length + for length := 1; length <= 64; length *= 2 { + b.Run(fmt.Sprintf("stack=%d", length), func(b *testing.B) { + root := dyngo.StartOperation(MyOperationArgs{}, nil) + defer root.Finish(MyOperationRes{}) + + op := root + for i := 0; i < length-1; i++ { + op = dyngo.StartOperation(MyOperationArgs{}, op) + defer op.Finish(MyOperationRes{}) + } + + b.Run("start event", func(b *testing.B) { + root.On(OnMyOperationStart(func(dyngo.Operation, MyOperationArgs) {})) + + b.ReportAllocs() + b.ResetTimer() + for n := 0; n < b.N; n++ { + dyngo.StartOperation(MyOperationArgs{}, op) + } + }) + + b.Run("start + finish events", func(b *testing.B) { + root.On(OnMyOperationFinish(func(dyngo.Operation, MyOperationRes) {})) + + b.ReportAllocs() + b.ResetTimer() + for n := 0; n < b.N; n++ { + leafOp := dyngo.StartOperation(MyOperationArgs{}, op) + leafOp.Finish(MyOperationRes{}) + } + }) + }) + } + }) + + b.Run("registering", func(b *testing.B) { + op := dyngo.StartOperation(MyOperationArgs{}, nil) + defer op.Finish(MyOperationRes{}) + + b.Run("start event", func(b *testing.B) { + b.ReportAllocs() + for n := 0; n < b.N; n++ { + op.On(OnMyOperationStart(func(dyngo.Operation, MyOperationArgs) {})) + } + }) + + b.Run("finish event", func(b *testing.B) { + b.ReportAllocs() + for n := 0; n < b.N; n++ { + op.On(OnMyOperationFinish(func(dyngo.Operation, MyOperationRes) {})) + } + }) + }) +} + +func BenchmarkGoAssumptions(b *testing.B) { + type ( + testS0 struct{} + testS1 struct{} + testS2 struct{} + testS3 struct{} + testS4 struct{} + ) + + // Compare map lookup times according to their key type. + // The selected implementation assumes using reflect.TypeOf(v).Name() doesn't allocate memory + // and is as good as "regular" string keys, whereas the use of reflect.Type keys is slower due + // to the underlying struct copy of the reflect struct type descriptor which has a lot of + // fields copied involved in the key comparison. + b.Run("map lookups", func(b *testing.B) { + b.Run("string keys", func(b *testing.B) { + m := map[string]int{} + key := "server.request.address.%d" + keys := make([]string, 5) + for i := 0; i < len(keys); i++ { + key := fmt.Sprintf(key, i) + keys[i] = key + m[key] = i + } + + b.ResetTimer() + b.ReportAllocs() + for n := 0; n < b.N; n++ { + _ = m[keys[n%len(keys)]] + } + }) + + getType := func(i int) reflect.Type { + i = i % 5 + switch i { + case 0: + return reflect.TypeOf(testS0{}) + case 1: + return reflect.TypeOf(testS1{}) + case 2: + return reflect.TypeOf(testS2{}) + case 3: + return reflect.TypeOf(testS3{}) + case 4: + return reflect.TypeOf(testS4{}) + } + panic("oops") + } + + b.Run("reflect.Type name keys", func(b *testing.B) { + m := map[string]int{} + for i := 0; i < 5; i++ { + m[getType(i).Name()] = i + } + + b.ReportAllocs() + b.ResetTimer() + for n := 0; n < b.N; n++ { + var k string + switch n % 5 { + case 0: + k = reflect.TypeOf(testS0{}).Name() + case 1: + k = reflect.TypeOf(testS1{}).Name() + case 2: + k = reflect.TypeOf(testS2{}).Name() + case 3: + k = reflect.TypeOf(testS3{}).Name() + case 4: + k = reflect.TypeOf(testS4{}).Name() + } + _ = m[k] + } + }) + + b.Run("reflect.Type keys", func(b *testing.B) { + m := map[reflect.Type]int{} + for i := 0; i < 5; i++ { + m[getType(i)] = i + } + + b.ReportAllocs() + b.ResetTimer() + for n := 0; n < b.N; n++ { + var k reflect.Type + switch n % 5 { + case 0: + k = reflect.TypeOf(testS0{}) + case 1: + k = reflect.TypeOf(testS1{}) + case 2: + k = reflect.TypeOf(testS2{}) + case 3: + k = reflect.TypeOf(testS3{}) + case 4: + k = reflect.TypeOf(testS4{}) + } + _ = m[k] + } + }) + + b.Run("custom type struct keys", func(b *testing.B) { + type typeDesc struct { + pkgPath, name string + } + m := map[typeDesc]int{} + for i := 0; i < 5; i++ { + typ := getType(i) + m[typeDesc{typ.PkgPath(), typ.Name()}] = i + } + + b.ReportAllocs() + b.ResetTimer() + for n := 0; n < b.N; n++ { + var k reflect.Type + switch n % 5 { + case 0: + k = reflect.TypeOf(testS0{}) + case 1: + k = reflect.TypeOf(testS1{}) + case 2: + k = reflect.TypeOf(testS2{}) + case 3: + k = reflect.TypeOf(testS3{}) + case 4: + k = reflect.TypeOf(testS4{}) + } + _ = m[typeDesc{k.PkgPath(), k.Name()}] + } + }) + }) +} diff --git a/internal/appsec/events.go b/internal/appsec/events.go new file mode 100644 index 0000000000..b3ca6bd349 --- /dev/null +++ b/internal/appsec/events.go @@ -0,0 +1,264 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016 Datadog, Inc. + +package appsec + +import ( + "strconv" + + "gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/dyngo/instrumentation/httpinstr" +) + +// securityEvent interface allowing to lazily serialize an event into an intake +// api struct actually sending it. Additional context can be optionally added to +// the security event using the following event wrappers. +type securityEvent interface { + toIntakeEvents() ([]*attackEvent, error) +} + +type ( + // httpContext is the security event context describing an HTTP handler. + // It includes information about its request and response. + httpContext struct { + Request httpRequestContext + Response httpResponseContext + } + + // httpRequestContext is the HTTP request context of an HTTP operation + // context. + httpRequestContext struct { + Method string + Host string + IsTLS bool + RequestURI string + RemoteAddr string + Path string + Headers map[string][]string + Query map[string][]string + } + + // httpResponseContext is the HTTP response context of an HTTP operation + // context. + httpResponseContext struct { + Status int + } +) + +type withHTTPContext struct { + securityEvent + ctx httpContext +} + +// withHTTPOperationContext adds the HTTP context to the event. +func withHTTPOperationContext(event securityEvent, args httpinstr.HandlerOperationArgs, res httpinstr.HandlerOperationRes) securityEvent { + return withHTTPContext{ + securityEvent: event, + ctx: httpContext{ + Request: httpRequestContext{ + Method: args.Method, + Host: args.Host, + IsTLS: args.IsTLS, + RequestURI: args.RequestURI, + Path: args.Path, + RemoteAddr: args.RemoteAddr, + Headers: args.Headers, + Query: args.Query, + }, + Response: httpResponseContext{ + Status: res.Status, + }, + }, + } +} + +// toIntakeEvent converts the current event with the HTTP context into an +// intake security event. +func (e withHTTPContext) toIntakeEvents() ([]*attackEvent, error) { + events, err := e.securityEvent.toIntakeEvents() + if err != nil { + return nil, err + } + reqContext := makeAttackContextHTTPRequest(e.ctx.Request) + resContext := makeAttackContextHTTPResponse(e.ctx.Response.Status) + httpContext := makeAttackContextHTTP(reqContext, resContext) + for _, event := range events { + event.Context.HTTP = httpContext + } + return events, nil +} + +// makeAttackContextHTTPRequest create the api.attackContextHTTPRequest payload +// from the given httpRequestContext. +func makeAttackContextHTTPRequest(req httpRequestContext) attackContextHTTPRequest { + host, portStr := splitHostPort(req.Host) + port, _ := strconv.Atoi(portStr) + remoteIP, remotePortStr := splitHostPort(req.RemoteAddr) + remotePort, _ := strconv.Atoi(remotePortStr) + var scheme string + if req.IsTLS { + scheme = "https" + } else { + scheme = "http" + } + url := makeHTTPURL(scheme, req.Host, req.Path) + headers := makeHTTPHeaders(req.Headers) + return attackContextHTTPRequest{ + Scheme: scheme, + Method: req.Method, + URL: url, + Host: host, + Port: port, + Path: req.Path, + RemoteIP: remoteIP, + RemotePort: remotePort, + Headers: headers, + Parameters: attackContextHTTPRequestParameters{Query: req.Query}, + } +} + +type spanContext struct { + securityEvent + traceID, spanID uint64 +} + +// withSpanContext adds the span context to the event. +func withSpanContext(event securityEvent, traceID, spanID uint64) securityEvent { + return spanContext{ + securityEvent: event, + traceID: traceID, + spanID: spanID, + } +} + +// ToIntakeEvent converts the current event with the span context into an +// intake security event. +func (ctx spanContext) toIntakeEvents() ([]*attackEvent, error) { + events, err := ctx.securityEvent.toIntakeEvents() + if err != nil { + return nil, err + } + traceID := strconv.FormatUint(ctx.traceID, 10) + spanID := strconv.FormatUint(ctx.spanID, 10) + traceContext := makeAttackContextTrace(traceID) + spanContext := makeAttackContextSpan(spanID) + for _, event := range events { + event.Context.Trace = traceContext + event.Context.Span = spanContext + } + return events, nil +} + +type serviceContext struct { + securityEvent + name, version, environment string +} + +// withServiceContext adds the service context to the event. +func withServiceContext(event securityEvent, name, version, environment string) securityEvent { + return serviceContext{ + securityEvent: event, + name: name, + version: version, + environment: environment, + } +} + +// ToIntakeEvent converts the current event with the service context into an +// intake security event. +func (ctx serviceContext) toIntakeEvents() ([]*attackEvent, error) { + events, err := ctx.securityEvent.toIntakeEvents() + if err != nil { + return nil, err + } + serviceContext := makeServiceContext(ctx.name, ctx.version, ctx.environment) + for _, event := range events { + event.Context.Service = serviceContext + } + return events, nil +} + +type tagsContext struct { + securityEvent + tags []string +} + +// withTagsContext adds the tags context to the event. +func withTagsContext(event securityEvent, tags []string) securityEvent { + return tagsContext{ + securityEvent: event, + tags: tags, + } +} + +// ToIntakeEvent converts the current event with the tags context into an +// intake security event. +func (ctx tagsContext) toIntakeEvents() ([]*attackEvent, error) { + events, err := ctx.securityEvent.toIntakeEvents() + if err != nil { + return nil, err + } + tagsContext := newAttackContextTags(ctx.tags) + for _, event := range events { + event.Context.Tags = tagsContext + } + return events, nil +} + +type tracerContext struct { + securityEvent + runtime, runtimeVersion, version string +} + +// withTracerContext adds the tracer context to the event. +func withTracerContext(event securityEvent, runtime, runtimeVersion, version string) securityEvent { + return tracerContext{ + securityEvent: event, + runtime: runtime, + runtimeVersion: runtimeVersion, + version: version, + } +} + +// ToIntakeEvent converts the current event with the tracer context into an +// intake security event. +func (ctx tracerContext) toIntakeEvents() ([]*attackEvent, error) { + events, err := ctx.securityEvent.toIntakeEvents() + if err != nil { + return nil, err + } + tracerContext := makeAttackContextTracer(ctx.version, ctx.runtime, ctx.runtimeVersion) + for _, event := range events { + event.Context.Tracer = tracerContext + } + return events, nil +} + +// withHostContext adds the running host context to the event. +func withHostContext(event securityEvent, hostname, osname string) securityEvent { + return hostContext{ + securityEvent: event, + hostname: hostname, + osname: osname, + } +} + +type hostContext struct { + securityEvent + hostname, osname string +} + +// ToIntakeEvent converts the current event with the host context into an intake +// security event. +func (ctx hostContext) toIntakeEvents() ([]*attackEvent, error) { + events, err := ctx.securityEvent.toIntakeEvents() + if err != nil { + return nil, err + } + hostContext := makeAttackContextHost(ctx.hostname, ctx.osname) + for _, event := range events { + event.Context.Host = hostContext + } + return events, nil +} diff --git a/internal/appsec/rule.go b/internal/appsec/rule.go new file mode 100644 index 0000000000..798d1d8390 --- /dev/null +++ b/internal/appsec/rule.go @@ -0,0 +1,13 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016 Datadog, Inc. + +//go:build appsec +// +build appsec + +package appsec + +// Static recommended AppSec rule +// Source: https://github.com/DataDog/appsec-event-rules/blob/main/v2/build/recommended.json +const staticRecommendedRule = "{\"version\":\"2.1\",\"rules\":[{\"id\":\"crs-913-110\",\"name\":\"Found request header associated with Acunetix security scanner\",\"tags\":{\"type\":\"security_scanner\",\"crs_id\":\"913110\",\"category\":\"attack_attempt\"},\"conditions\":[{\"parameters\":{\"inputs\":[{\"address\":\"server.request.headers.no_cookies\"}],\"list\":[\"acunetix-product\",\"(acunetix web vulnerability scanner\",\"acunetix-scanning-agreement\",\"acunetix-user-agreement\"]},\"operator\":\"phrase_match\"}],\"transformers\":[\"lowercase\"]},{\"id\":\"crs-913-120\",\"name\":\"Found request filename/argument associated with security scanner\",\"tags\":{\"type\":\"security_scanner\",\"crs_id\":\"913120\",\"category\":\"attack_attempt\"},\"conditions\":[{\"parameters\":{\"inputs\":[{\"address\":\"server.request.query\"},{\"address\":\"server.request.body\"},{\"address\":\"server.request.path_params\"}],\"list\":[\"/.adsensepostnottherenonobook\",\"/hello.html\",\"/actsensepostnottherenonotive\",\"/acunetix-wvs-test-for-some-inexistent-file\",\"/antidisestablishmentarianism\",\"/appscan_fingerprint/mac_address\",\"/arachni-\",\"/cybercop\",\"/nessus_is_probing_you_\",\"/nessustest\",\"/netsparker-\",\"/rfiinc.txt\",\"/thereisnowaythat-you-canbethere\",\"/w3af/remotefileinclude.html\",\"appscan_fingerprint\",\"w00tw00t.at.isc.sans.dfind\",\"w00tw00t.at.blackhats.romanian.anti-sec\"]},\"operator\":\"phrase_match\"}],\"transformers\":[\"lowercase\"]},{\"id\":\"crs-920-260\",\"name\":\"Unicode Full/Half Width Abuse Attack Attempt\",\"tags\":{\"type\":\"http_protocol_violation\",\"crs_id\":\"920260\",\"category\":\"attack_attempt\"},\"conditions\":[{\"parameters\":{\"inputs\":[{\"address\":\"server.request.uri.raw\"}],\"regex\":\"\\\\%u[fF]{2}[0-9a-fA-F]{2}\",\"options\":{\"case_sensitive\":true,\"min_length\":6}},\"operator\":\"match_regex\"}],\"transformers\":[]},{\"id\":\"crs-921-110\",\"name\":\"HTTP Request Smuggling Attack\",\"tags\":{\"type\":\"http_protocol_violation\",\"crs_id\":\"921110\",\"category\":\"attack_attempt\"},\"conditions\":[{\"parameters\":{\"inputs\":[{\"address\":\"server.request.query\"},{\"address\":\"server.request.body\"},{\"address\":\"server.request.path_params\"}],\"regex\":\"(?:get|post|head|options|connect|put|delete|trace|track|patch|propfind|propatch|mkcol|copy|move|lock|unlock)\\\\s+[^\\\\s]+\\\\s+http/\\\\d\",\"options\":{\"case_sensitive\":true,\"min_length\":12}},\"operator\":\"match_regex\"}],\"transformers\":[\"lowercase\"]},{\"id\":\"crs-921-140\",\"name\":\"HTTP Header Injection Attack via headers\",\"tags\":{\"type\":\"http_protocol_violation\",\"crs_id\":\"921140\",\"category\":\"attack_attempt\"},\"conditions\":[{\"parameters\":{\"inputs\":[{\"address\":\"server.request.headers.no_cookies\"}],\"regex\":\"[\\\\n\\\\r]\",\"options\":{\"case_sensitive\":true,\"min_length\":1}},\"operator\":\"match_regex\"}],\"transformers\":[]},{\"id\":\"crs-921-160\",\"name\":\"HTTP Header Injection Attack via payload (CR/LF and header-name detected)\",\"tags\":{\"type\":\"http_protocol_violation\",\"crs_id\":\"921160\",\"category\":\"attack_attempt\"},\"conditions\":[{\"parameters\":{\"inputs\":[{\"address\":\"server.request.query\"},{\"address\":\"server.request.path_params\"}],\"regex\":\"[\\\\n\\\\r]+(?:\\\\s|location|refresh|(?:set-)?cookie|(?:x-)?(?:forwarded-(?:for|host|server)|host|via|remote-ip|remote-addr|originating-IP))\\\\s*:\",\"options\":{\"case_sensitive\":true,\"min_length\":3}},\"operator\":\"match_regex\"}],\"transformers\":[\"lowercase\"]},{\"id\":\"crs-930-100\",\"name\":\"Obfuscated Path Traversal Attack (/../)\",\"tags\":{\"type\":\"lfi\",\"crs_id\":\"930100\",\"category\":\"attack_attempt\"},\"conditions\":[{\"parameters\":{\"inputs\":[{\"address\":\"server.request.uri.raw\"},{\"address\":\"server.request.headers.no_cookies\"}],\"regex\":\"(?:\\\\x5c|(?:%(?:c(?:0%(?:[2aq]f|5c|9v)|1%(?:[19p]c|8s|af))|2(?:5(?:c(?:0%25af|1%259c)|2f|5c)|%46|f)|(?:(?:f(?:8%8)?0%8|e)0%80%a|bg%q)f|%3(?:2(?:%(?:%6|4)6|F)|5%%63)|u(?:221[56]|002f|EFC8|F025)|1u|5c)|0x(?:2f|5c)|/))(?:%(?:(?:f(?:(?:c%80|8)%8)?0%8|e)0%80%ae|2(?:(?:5(?:c0%25a|2))?e|%45)|u(?:(?:002|ff0)e|2024)|%32(?:%(?:%6|4)5|E)|c0(?:%[256aef]e|\\\\.))|\\\\.(?:%0[01]|\\\\?)?|\\\\?\\\\.?|0x2e){2}(?:\\\\x5c|(?:%(?:c(?:0%(?:[2aq]f|5c|9v)|1%(?:[19p]c|8s|af))|2(?:5(?:c(?:0%25af|1%259c)|2f|5c)|%46|f)|(?:(?:f(?:8%8)?0%8|e)0%80%a|bg%q)f|%3(?:2(?:%(?:%6|4)6|F)|5%%63)|u(?:221[56]|002f|EFC8|F025)|1u|5c)|0x(?:2f|5c)|/))\",\"options\":{\"min_length\":4}},\"operator\":\"match_regex\"}],\"transformers\":[]},{\"id\":\"crs-930-110\",\"name\":\"Simple Path Traversal Attack (/../)\",\"tags\":{\"type\":\"lfi\",\"crs_id\":\"930110\",\"category\":\"attack_attempt\"},\"conditions\":[{\"parameters\":{\"inputs\":[{\"address\":\"server.request.uri.raw\"},{\"address\":\"server.request.headers.no_cookies\"}],\"regex\":\"(?:(?:^|[\\\\\\\\/])\\\\.\\\\.[\\\\\\\\/]|[\\\\\\\\/]\\\\.\\\\.(?:[\\\\\\\\/]|$))\",\"options\":{\"case_sensitive\":true,\"min_length\":3}},\"operator\":\"match_regex\"}],\"transformers\":[\"removeNulls\"]},{\"id\":\"crs-930-120\",\"name\":\"OS File Access Attempt\",\"tags\":{\"type\":\"lfi\",\"crs_id\":\"930120\",\"category\":\"attack_attempt\"},\"conditions\":[{\"parameters\":{\"inputs\":[{\"address\":\"server.request.cookies\"},{\"address\":\"server.request.query\"},{\"address\":\"server.request.body\"},{\"address\":\"server.request.path_params\"}],\"list\":[\".htaccess\",\".htdigest\",\".htpasswd\",\".addressbook\",\".aptitude/config\",\".bash_config\",\".bash_history\",\".bash_logout\",\".bash_profile\",\".bashrc\",\".cache/notify-osd.log\",\".config/odesk/odesk team.conf\",\".cshrc\",\".dockerignore\",\".drush/\",\".eslintignore\",\".fbcindex\",\".forward\",\".gitattributes\",\".gitconfig\",\".gnupg/\",\".hplip/hplip.conf\",\".ksh_history\",\".lesshst\",\".lftp/\",\".lhistory\",\".lldb-history\",\".local/share/mc/\",\".lynx_cookies\",\".my.cnf\",\".mysql_history\",\".nano_history\",\".node_repl_history\",\".pearrc\",\".php_history\",\".pinerc\",\".pki/\",\".proclog\",\".procmailrc\",\".psql_history\",\".python_history\",\".rediscli_history\",\".rhistory\",\".rhosts\",\".sh_history\",\".sqlite_history\",\".ssh/authorized_keys\",\".ssh/config\",\".ssh/id_dsa\",\".ssh/id_dsa.pub\",\".ssh/id_rsa\",\".ssh/id_rsa.pub\",\".ssh/identity\",\".ssh/identity.pub\",\".ssh/known_hosts\",\".subversion/auth\",\".subversion/config\",\".subversion/servers\",\".tconn/tconn.conf\",\".tcshrc\",\".vidalia/vidalia.conf\",\".viminfo\",\".vimrc\",\".www_acl\",\".wwwacl\",\".xauthority\",\".zhistory\",\".zshrc\",\".zsh_history\",\".nsconfig\",\"etc/redis.conf\",\"etc/redis-sentinel.conf\",\"etc/php.ini\",\"bin/php.ini\",\"etc/httpd/php.ini\",\"usr/lib/php.ini\",\"usr/lib/php/php.ini\",\"usr/local/etc/php.ini\",\"usr/local/lib/php.ini\",\"usr/local/php/lib/php.ini\",\"usr/local/php4/lib/php.ini\",\"usr/local/php5/lib/php.ini\",\"usr/local/apache/conf/php.ini\",\"etc/php4.4/fcgi/php.ini\",\"etc/php4/apache/php.ini\",\"etc/php4/apache2/php.ini\",\"etc/php5/apache/php.ini\",\"etc/php5/apache2/php.ini\",\"etc/php/php.ini\",\"etc/php/php4/php.ini\",\"etc/php/apache/php.ini\",\"etc/php/apache2/php.ini\",\"web/conf/php.ini\",\"usr/local/zend/etc/php.ini\",\"opt/xampp/etc/php.ini\",\"var/local/www/conf/php.ini\",\"etc/php/cgi/php.ini\",\"etc/php4/cgi/php.ini\",\"etc/php5/cgi/php.ini\",\"home2/bin/stable/apache/php.ini\",\"home/bin/stable/apache/php.ini\",\"etc/httpd/conf.d/php.conf\",\"php5/php.ini\",\"php4/php.ini\",\"php/php.ini\",\"windows/php.ini\",\"winnt/php.ini\",\"apache/php/php.ini\",\"xampp/apache/bin/php.ini\",\"netserver/bin/stable/apache/php.ini\",\"volumes/macintosh_hd1/usr/local/php/lib/php.ini\",\"etc/mono/1.0/machine.config\",\"etc/mono/2.0/machine.config\",\"etc/mono/2.0/web.config\",\"etc/mono/config\",\"usr/local/cpanel/logs/stats_log\",\"usr/local/cpanel/logs/access_log\",\"usr/local/cpanel/logs/error_log\",\"usr/local/cpanel/logs/license_log\",\"usr/local/cpanel/logs/login_log\",\"var/cpanel/cpanel.config\",\"var/log/sw-cp-server/error_log\",\"usr/local/psa/admin/logs/httpsd_access_log\",\"usr/local/psa/admin/logs/panel.log\",\"var/log/sso/sso.log\",\"usr/local/psa/admin/conf/php.ini\",\"etc/sw-cp-server/applications.d/plesk.conf\",\"usr/local/psa/admin/conf/site_isolation_settings.ini\",\"usr/local/sb/config\",\"etc/sw-cp-server/applications.d/00-sso-cpserver.conf\",\"etc/sso/sso_config.ini\",\"etc/mysql/conf.d/old_passwords.cnf\",\"var/log/mysql/mysql-bin.log\",\"var/log/mysql/mysql-bin.index\",\"var/log/mysql/data/mysql-bin.index\",\"var/log/mysql.log\",\"var/log/mysql.err\",\"var/log/mysqlderror.log\",\"var/log/mysql/mysql.log\",\"var/log/mysql/mysql-slow.log\",\"var/log/mysql-bin.index\",\"var/log/data/mysql-bin.index\",\"var/mysql.log\",\"var/mysql-bin.index\",\"var/data/mysql-bin.index\",\"program files/mysql/mysql server 5.0/data/{host}.err\",\"program files/mysql/mysql server 5.0/data/mysql.log\",\"program files/mysql/mysql server 5.0/data/mysql.err\",\"program files/mysql/mysql server 5.0/data/mysql-bin.log\",\"program files/mysql/mysql server 5.0/data/mysql-bin.index\",\"program files/mysql/data/{host}.err\",\"program files/mysql/data/mysql.log\",\"program files/mysql/data/mysql.err\",\"program files/mysql/data/mysql-bin.log\",\"program files/mysql/data/mysql-bin.index\",\"mysql/data/{host}.err\",\"mysql/data/mysql.log\",\"mysql/data/mysql.err\",\"mysql/data/mysql-bin.log\",\"mysql/data/mysql-bin.index\",\"usr/local/mysql/data/mysql.log\",\"usr/local/mysql/data/mysql.err\",\"usr/local/mysql/data/mysql-bin.log\",\"usr/local/mysql/data/mysql-slow.log\",\"usr/local/mysql/data/mysqlderror.log\",\"usr/local/mysql/data/{host}.err\",\"usr/local/mysql/data/mysql-bin.index\",\"var/lib/mysql/my.cnf\",\"etc/mysql/my.cnf\",\"etc/my.cnf\",\"program files/mysql/mysql server 5.0/my.ini\",\"program files/mysql/mysql server 5.0/my.cnf\",\"program files/mysql/my.ini\",\"program files/mysql/my.cnf\",\"mysql/my.ini\",\"mysql/my.cnf\",\"mysql/bin/my.ini\",\"var/postgresql/log/postgresql.log\",\"var/log/postgresql/postgresql.log\",\"var/log/postgres/pg_backup.log\",\"var/log/postgres/postgres.log\",\"var/log/postgresql.log\",\"var/log/pgsql/pgsql.log\",\"var/log/postgresql/postgresql-8.1-main.log\",\"var/log/postgresql/postgresql-8.3-main.log\",\"var/log/postgresql/postgresql-8.4-main.log\",\"var/log/postgresql/postgresql-9.0-main.log\",\"var/log/postgresql/postgresql-9.1-main.log\",\"var/log/pgsql8.log\",\"var/log/postgresql/postgres.log\",\"var/log/pgsql_log\",\"var/log/postgresql/main.log\",\"var/log/cron/var/log/postgres.log\",\"usr/internet/pgsql/data/postmaster.log\",\"usr/local/pgsql/data/postgresql.log\",\"usr/local/pgsql/data/pg_log\",\"postgresql/log/pgadmin.log\",\"var/lib/pgsql/data/postgresql.conf\",\"var/postgresql/db/postgresql.conf\",\"var/nm2/postgresql.conf\",\"usr/local/pgsql/data/postgresql.conf\",\"usr/local/pgsql/data/pg_hba.conf\",\"usr/internet/pgsql/data/pg_hba.conf\",\"usr/local/pgsql/data/passwd\",\"usr/local/pgsql/bin/pg_passwd\",\"etc/postgresql/postgresql.conf\",\"etc/postgresql/pg_hba.conf\",\"home/postgres/data/postgresql.conf\",\"home/postgres/data/pg_version\",\"home/postgres/data/pg_ident.conf\",\"home/postgres/data/pg_hba.conf\",\"program files/postgresql/8.3/data/pg_hba.conf\",\"program files/postgresql/8.3/data/pg_ident.conf\",\"program files/postgresql/8.3/data/postgresql.conf\",\"program files/postgresql/8.4/data/pg_hba.conf\",\"program files/postgresql/8.4/data/pg_ident.conf\",\"program files/postgresql/8.4/data/postgresql.conf\",\"program files/postgresql/9.0/data/pg_hba.conf\",\"program files/postgresql/9.0/data/pg_ident.conf\",\"program files/postgresql/9.0/data/postgresql.conf\",\"program files/postgresql/9.1/data/pg_hba.conf\",\"program files/postgresql/9.1/data/pg_ident.conf\",\"program files/postgresql/9.1/data/postgresql.conf\",\"wamp/logs/access.log\",\"wamp/logs/apache_error.log\",\"wamp/logs/genquery.log\",\"wamp/logs/mysql.log\",\"wamp/logs/slowquery.log\",\"wamp/bin/apache/apache2.2.22/logs/access.log\",\"wamp/bin/apache/apache2.2.22/logs/error.log\",\"wamp/bin/apache/apache2.2.21/logs/access.log\",\"wamp/bin/apache/apache2.2.21/logs/error.log\",\"wamp/bin/mysql/mysql5.5.24/data/mysql-bin.index\",\"wamp/bin/mysql/mysql5.5.16/data/mysql-bin.index\",\"wamp/bin/apache/apache2.2.21/conf/httpd.conf\",\"wamp/bin/apache/apache2.2.22/conf/httpd.conf\",\"wamp/bin/apache/apache2.2.21/wampserver.conf\",\"wamp/bin/apache/apache2.2.22/wampserver.conf\",\"wamp/bin/apache/apache2.2.22/conf/wampserver.conf\",\"wamp/bin/mysql/mysql5.5.24/my.ini\",\"wamp/bin/mysql/mysql5.5.24/wampserver.conf\",\"wamp/bin/mysql/mysql5.5.16/my.ini\",\"wamp/bin/mysql/mysql5.5.16/wampserver.conf\",\"wamp/bin/php/php5.3.8/php.ini\",\"wamp/bin/php/php5.4.3/php.ini\",\"xampp/apache/logs/access.log\",\"xampp/apache/logs/error.log\",\"xampp/mysql/data/mysql-bin.index\",\"xampp/mysql/data/mysql.err\",\"xampp/mysql/data/{host}.err\",\"xampp/sendmail/sendmail.log\",\"xampp/apache/conf/httpd.conf\",\"xampp/filezillaftp/filezilla server.xml\",\"xampp/mercurymail/mercury.ini\",\"xampp/php/php.ini\",\"xampp/phpmyadmin/config.inc.php\",\"xampp/sendmail/sendmail.ini\",\"xampp/webalizer/webalizer.conf\",\"opt/lampp/etc/httpd.conf\",\"xampp/htdocs/aca.txt\",\"xampp/htdocs/admin.php\",\"xampp/htdocs/leer.txt\",\"usr/local/apache/logs/audit_log\",\"usr/local/apache2/logs/audit_log\",\"logs/security_debug_log\",\"logs/security_log\",\"usr/local/apache/conf/modsec.conf\",\"usr/local/apache2/conf/modsec.conf\",\"winnt/system32/logfiles/msftpsvc\",\"winnt/system32/logfiles/msftpsvc1\",\"winnt/system32/logfiles/msftpsvc2\",\"windows/system32/logfiles/msftpsvc\",\"windows/system32/logfiles/msftpsvc1\",\"windows/system32/logfiles/msftpsvc2\",\"etc/logrotate.d/proftpd\",\"www/logs/proftpd.system.log\",\"var/log/proftpd\",\"var/log/proftpd/xferlog.legacy\",\"var/log/proftpd.access_log\",\"var/log/proftpd.xferlog\",\"etc/pam.d/proftpd\",\"etc/proftp.conf\",\"etc/protpd/proftpd.conf\",\"etc/vhcs2/proftpd/proftpd.conf\",\"etc/proftpd/modules.conf\",\"var/log/vsftpd.log\",\"etc/vsftpd.chroot_list\",\"etc/logrotate.d/vsftpd.log\",\"etc/vsftpd/vsftpd.conf\",\"etc/vsftpd.conf\",\"etc/chrootusers\",\"var/log/xferlog\",\"var/adm/log/xferlog\",\"etc/wu-ftpd/ftpaccess\",\"etc/wu-ftpd/ftphosts\",\"etc/wu-ftpd/ftpusers\",\"var/log/pure-ftpd/pure-ftpd.log\",\"logs/pure-ftpd.log\",\"var/log/pureftpd.log\",\"usr/sbin/pure-config.pl\",\"usr/etc/pure-ftpd.conf\",\"etc/pure-ftpd/pure-ftpd.conf\",\"usr/local/etc/pure-ftpd.conf\",\"usr/local/etc/pureftpd.pdb\",\"usr/local/pureftpd/etc/pureftpd.pdb\",\"usr/local/pureftpd/sbin/pure-config.pl\",\"usr/local/pureftpd/etc/pure-ftpd.conf\",\"etc/pure-ftpd.conf\",\"etc/pure-ftpd/pure-ftpd.pdb\",\"etc/pureftpd.pdb\",\"etc/pureftpd.passwd\",\"etc/pure-ftpd/pureftpd.pdb\",\"usr/ports/ftp/pure-ftpd/pure-ftpd.conf\",\"usr/ports/ftp/pure-ftpd/pureftpd.pdb\",\"usr/ports/ftp/pure-ftpd/pureftpd.passwd\",\"usr/ports/net/pure-ftpd/pure-ftpd.conf\",\"usr/ports/net/pure-ftpd/pureftpd.pdb\",\"usr/ports/net/pure-ftpd/pureftpd.passwd\",\"usr/pkgsrc/net/pureftpd/pure-ftpd.conf\",\"usr/pkgsrc/net/pureftpd/pureftpd.pdb\",\"usr/pkgsrc/net/pureftpd/pureftpd.passwd\",\"usr/ports/contrib/pure-ftpd/pure-ftpd.conf\",\"usr/ports/contrib/pure-ftpd/pureftpd.pdb\",\"usr/ports/contrib/pure-ftpd/pureftpd.passwd\",\"var/log/muddleftpd\",\"usr/sbin/mudlogd\",\"etc/muddleftpd/mudlog\",\"etc/muddleftpd.com\",\"etc/muddleftpd/mudlogd.conf\",\"etc/muddleftpd/muddleftpd.conf\",\"var/log/muddleftpd.conf\",\"usr/sbin/mudpasswd\",\"etc/muddleftpd/muddleftpd.passwd\",\"etc/muddleftpd/passwd\",\"var/log/ftp-proxy/ftp-proxy.log\",\"var/log/ftp-proxy\",\"var/log/ftplog\",\"etc/logrotate.d/ftp\",\"etc/ftpchroot\",\"etc/ftphosts\",\"etc/ftpusers\",\"var/log/exim_mainlog\",\"var/log/exim/mainlog\",\"var/log/maillog\",\"var/log/exim_paniclog\",\"var/log/exim/paniclog\",\"var/log/exim/rejectlog\",\"var/log/exim_rejectlog\",\"winnt/system32/logfiles/smtpsvc\",\"winnt/system32/logfiles/smtpsvc1\",\"winnt/system32/logfiles/smtpsvc2\",\"winnt/system32/logfiles/smtpsvc3\",\"winnt/system32/logfiles/smtpsvc4\",\"winnt/system32/logfiles/smtpsvc5\",\"windows/system32/logfiles/smtpsvc\",\"windows/system32/logfiles/smtpsvc1\",\"windows/system32/logfiles/smtpsvc2\",\"windows/system32/logfiles/smtpsvc3\",\"windows/system32/logfiles/smtpsvc4\",\"windows/system32/logfiles/smtpsvc5\",\"etc/osxhttpd/osxhttpd.conf\",\"system/library/webobjects/adaptors/apache2.2/apache.conf\",\"etc/apache2/sites-available/default\",\"etc/apache2/sites-available/default-ssl\",\"etc/apache2/sites-enabled/000-default\",\"etc/apache2/sites-enabled/default\",\"etc/apache2/apache2.conf\",\"etc/apache2/ports.conf\",\"usr/local/etc/apache/httpd.conf\",\"usr/pkg/etc/httpd/httpd.conf\",\"usr/pkg/etc/httpd/httpd-default.conf\",\"usr/pkg/etc/httpd/httpd-vhosts.conf\",\"etc/httpd/mod_php.conf\",\"etc/httpd/extra/httpd-ssl.conf\",\"etc/rc.d/rc.httpd\",\"usr/local/apache/conf/httpd.conf.default\",\"usr/local/apache/conf/access.conf\",\"usr/local/apache22/conf/httpd.conf\",\"usr/local/apache22/httpd.conf\",\"usr/local/etc/apache22/conf/httpd.conf\",\"usr/local/apps/apache22/conf/httpd.conf\",\"etc/apache22/conf/httpd.conf\",\"etc/apache22/httpd.conf\",\"opt/apache22/conf/httpd.conf\",\"usr/local/etc/apache2/vhosts.conf\",\"usr/local/apache/conf/vhosts.conf\",\"usr/local/apache2/conf/vhosts.conf\",\"usr/local/apache/conf/vhosts-custom.conf\",\"usr/local/apache2/conf/vhosts-custom.conf\",\"etc/apache/default-server.conf\",\"etc/apache2/default-server.conf\",\"usr/local/apache2/conf/extra/httpd-ssl.conf\",\"usr/local/apache2/conf/ssl.conf\",\"etc/httpd/conf.d\",\"usr/local/etc/apache22/httpd.conf\",\"usr/local/etc/apache2/httpd.conf\",\"etc/apache2/httpd2.conf\",\"etc/apache2/ssl-global.conf\",\"etc/apache2/vhosts.d/00_default_vhost.conf\",\"apache/conf/httpd.conf\",\"etc/apache/httpd.conf\",\"etc/httpd/conf\",\"http/httpd.conf\",\"usr/local/apache1.3/conf/httpd.conf\",\"usr/local/etc/httpd/conf\",\"var/apache/conf/httpd.conf\",\"var/www/conf\",\"www/apache/conf/httpd.conf\",\"www/conf/httpd.conf\",\"etc/init.d\",\"etc/apache/access.conf\",\"etc/rc.conf\",\"www/logs/freebsddiary-error.log\",\"www/logs/freebsddiary-access_log\",\"library/webserver/documents/index.html\",\"library/webserver/documents/index.htm\",\"library/webserver/documents/default.html\",\"library/webserver/documents/default.htm\",\"library/webserver/documents/index.php\",\"library/webserver/documents/default.php\",\"var/log/webmin/miniserv.log\",\"usr/local/etc/webmin/miniserv.conf\",\"etc/webmin/miniserv.conf\",\"usr/local/etc/webmin/miniserv.users\",\"etc/webmin/miniserv.users\",\"winnt/system32/logfiles/w3svc/inetsvn1.log\",\"winnt/system32/logfiles/w3svc1/inetsvn1.log\",\"winnt/system32/logfiles/w3svc2/inetsvn1.log\",\"winnt/system32/logfiles/w3svc3/inetsvn1.log\",\"windows/system32/logfiles/w3svc/inetsvn1.log\",\"windows/system32/logfiles/w3svc1/inetsvn1.log\",\"windows/system32/logfiles/w3svc2/inetsvn1.log\",\"windows/system32/logfiles/w3svc3/inetsvn1.log\",\"var/log/httpd/access_log\",\"var/log/httpd/error_log\",\"apache/logs/error.log\",\"apache/logs/access.log\",\"apache2/logs/error.log\",\"apache2/logs/access.log\",\"logs/error.log\",\"logs/access.log\",\"etc/httpd/logs/access_log\",\"etc/httpd/logs/access.log\",\"etc/httpd/logs/error_log\",\"etc/httpd/logs/error.log\",\"usr/local/apache/logs/access_log\",\"usr/local/apache/logs/access.log\",\"usr/local/apache/logs/error_log\",\"usr/local/apache/logs/error.log\",\"usr/local/apache2/logs/access_log\",\"usr/local/apache2/logs/access.log\",\"usr/local/apache2/logs/error_log\",\"usr/local/apache2/logs/error.log\",\"var/www/logs/access_log\",\"var/www/logs/access.log\",\"var/www/logs/error_log\",\"var/www/logs/error.log\",\"var/log/httpd/access.log\",\"var/log/httpd/error.log\",\"var/log/apache/access_log\",\"var/log/apache/access.log\",\"var/log/apache/error_log\",\"var/log/apache/error.log\",\"var/log/apache2/access_log\",\"var/log/apache2/access.log\",\"var/log/apache2/error_log\",\"var/log/apache2/error.log\",\"var/log/access_log\",\"var/log/access.log\",\"var/log/error_log\",\"var/log/error.log\",\"opt/lampp/logs/access_log\",\"opt/lampp/logs/error_log\",\"opt/xampp/logs/access_log\",\"opt/xampp/logs/error_log\",\"opt/lampp/logs/access.log\",\"opt/lampp/logs/error.log\",\"opt/xampp/logs/access.log\",\"opt/xampp/logs/error.log\",\"program files/apache group/apache/logs/access.log\",\"program files/apache group/apache/logs/error.log\",\"program files/apache software foundation/apache2.2/logs/error.log\",\"program files/apache software foundation/apache2.2/logs/access.log\",\"opt/apache/apache.conf\",\"opt/apache/conf/apache.conf\",\"opt/apache2/apache.conf\",\"opt/apache2/conf/apache.conf\",\"opt/httpd/apache.conf\",\"opt/httpd/conf/apache.conf\",\"etc/httpd/apache.conf\",\"etc/apache2/apache.conf\",\"etc/httpd/conf/apache.conf\",\"usr/local/apache/apache.conf\",\"usr/local/apache/conf/apache.conf\",\"usr/local/apache2/apache.conf\",\"usr/local/apache2/conf/apache.conf\",\"usr/local/php/apache.conf.php\",\"usr/local/php4/apache.conf.php\",\"usr/local/php5/apache.conf.php\",\"usr/local/php/apache.conf\",\"usr/local/php4/apache.conf\",\"usr/local/php5/apache.conf\",\"private/etc/httpd/apache.conf\",\"opt/apache/apache2.conf\",\"opt/apache/conf/apache2.conf\",\"opt/apache2/apache2.conf\",\"opt/apache2/conf/apache2.conf\",\"opt/httpd/apache2.conf\",\"opt/httpd/conf/apache2.conf\",\"etc/httpd/apache2.conf\",\"etc/httpd/conf/apache2.conf\",\"usr/local/apache/apache2.conf\",\"usr/local/apache/conf/apache2.conf\",\"usr/local/apache2/apache2.conf\",\"usr/local/apache2/conf/apache2.conf\",\"usr/local/php/apache2.conf.php\",\"usr/local/php4/apache2.conf.php\",\"usr/local/php5/apache2.conf.php\",\"usr/local/php/apache2.conf\",\"usr/local/php4/apache2.conf\",\"usr/local/php5/apache2.conf\",\"private/etc/httpd/apache2.conf\",\"usr/local/apache/conf/httpd.conf\",\"usr/local/apache2/conf/httpd.conf\",\"etc/httpd/conf/httpd.conf\",\"etc/apache/apache.conf\",\"etc/apache/conf/httpd.conf\",\"etc/apache2/httpd.conf\",\"usr/apache2/conf/httpd.conf\",\"usr/apache/conf/httpd.conf\",\"usr/local/etc/apache/conf/httpd.conf\",\"usr/local/apache/httpd.conf\",\"usr/local/apache2/httpd.conf\",\"usr/local/httpd/conf/httpd.conf\",\"usr/local/etc/apache2/conf/httpd.conf\",\"usr/local/etc/httpd/conf/httpd.conf\",\"usr/local/apps/apache2/conf/httpd.conf\",\"usr/local/apps/apache/conf/httpd.conf\",\"usr/local/php/httpd.conf.php\",\"usr/local/php4/httpd.conf.php\",\"usr/local/php5/httpd.conf.php\",\"usr/local/php/httpd.conf\",\"usr/local/php4/httpd.conf\",\"usr/local/php5/httpd.conf\",\"etc/apache2/conf/httpd.conf\",\"etc/http/conf/httpd.conf\",\"etc/httpd/httpd.conf\",\"etc/http/httpd.conf\",\"etc/httpd.conf\",\"opt/apache/conf/httpd.conf\",\"opt/apache2/conf/httpd.conf\",\"var/www/conf/httpd.conf\",\"private/etc/httpd/httpd.conf\",\"private/etc/httpd/httpd.conf.default\",\"etc/apache2/vhosts.d/default_vhost.include\",\"etc/apache2/conf.d/charset\",\"etc/apache2/conf.d/security\",\"etc/apache2/envvars\",\"etc/apache2/mods-available/autoindex.conf\",\"etc/apache2/mods-available/deflate.conf\",\"etc/apache2/mods-available/dir.conf\",\"etc/apache2/mods-available/mem_cache.conf\",\"etc/apache2/mods-available/mime.conf\",\"etc/apache2/mods-available/proxy.conf\",\"etc/apache2/mods-available/setenvif.conf\",\"etc/apache2/mods-available/ssl.conf\",\"etc/apache2/mods-enabled/alias.conf\",\"etc/apache2/mods-enabled/deflate.conf\",\"etc/apache2/mods-enabled/dir.conf\",\"etc/apache2/mods-enabled/mime.conf\",\"etc/apache2/mods-enabled/negotiation.conf\",\"etc/apache2/mods-enabled/php5.conf\",\"etc/apache2/mods-enabled/status.conf\",\"program files/apache group/apache/conf/httpd.conf\",\"program files/apache group/apache2/conf/httpd.conf\",\"program files/xampp/apache/conf/apache.conf\",\"program files/xampp/apache/conf/apache2.conf\",\"program files/xampp/apache/conf/httpd.conf\",\"program files/apache group/apache/apache.conf\",\"program files/apache group/apache/conf/apache.conf\",\"program files/apache group/apache2/conf/apache.conf\",\"program files/apache group/apache/apache2.conf\",\"program files/apache group/apache/conf/apache2.conf\",\"program files/apache group/apache2/conf/apache2.conf\",\"program files/apache software foundation/apache2.2/conf/httpd.conf\",\"volumes/macintosh_hd1/opt/httpd/conf/httpd.conf\",\"volumes/macintosh_hd1/opt/apache/conf/httpd.conf\",\"volumes/macintosh_hd1/opt/apache2/conf/httpd.conf\",\"volumes/macintosh_hd1/usr/local/php/httpd.conf.php\",\"volumes/macintosh_hd1/usr/local/php4/httpd.conf.php\",\"volumes/macintosh_hd1/usr/local/php5/httpd.conf.php\",\"volumes/webbackup/opt/apache2/conf/httpd.conf\",\"volumes/webbackup/private/etc/httpd/httpd.conf\",\"volumes/webbackup/private/etc/httpd/httpd.conf.default\",\"usr/local/etc/apache/vhosts.conf\",\"usr/local/jakarta/tomcat/conf/jakarta.conf\",\"usr/local/jakarta/tomcat/conf/server.xml\",\"usr/local/jakarta/tomcat/conf/context.xml\",\"usr/local/jakarta/tomcat/conf/workers.properties\",\"usr/local/jakarta/tomcat/conf/logging.properties\",\"usr/local/jakarta/dist/tomcat/conf/jakarta.conf\",\"usr/local/jakarta/dist/tomcat/conf/server.xml\",\"usr/local/jakarta/dist/tomcat/conf/context.xml\",\"usr/local/jakarta/dist/tomcat/conf/workers.properties\",\"usr/local/jakarta/dist/tomcat/conf/logging.properties\",\"usr/share/tomcat6/conf/server.xml\",\"usr/share/tomcat6/conf/context.xml\",\"usr/share/tomcat6/conf/workers.properties\",\"usr/share/tomcat6/conf/logging.properties\",\"var/log/tomcat6/catalina.out\",\"var/cpanel/tomcat.options\",\"usr/local/jakarta/tomcat/logs/catalina.out\",\"usr/local/jakarta/tomcat/logs/catalina.err\",\"opt/tomcat/logs/catalina.out\",\"opt/tomcat/logs/catalina.err\",\"usr/share/logs/catalina.out\",\"usr/share/logs/catalina.err\",\"usr/share/tomcat/logs/catalina.out\",\"usr/share/tomcat/logs/catalina.err\",\"usr/share/tomcat6/logs/catalina.out\",\"usr/share/tomcat6/logs/catalina.err\",\"usr/local/apache/logs/mod_jk.log\",\"usr/local/jakarta/tomcat/logs/mod_jk.log\",\"usr/local/jakarta/dist/tomcat/logs/mod_jk.log\",\"opt/[jboss]/server/default/conf/jboss-minimal.xml\",\"opt/[jboss]/server/default/conf/jboss-service.xml\",\"opt/[jboss]/server/default/conf/jndi.properties\",\"opt/[jboss]/server/default/conf/log4j.xml\",\"opt/[jboss]/server/default/conf/login-config.xml\",\"opt/[jboss]/server/default/conf/standardjaws.xml\",\"opt/[jboss]/server/default/conf/standardjboss.xml\",\"opt/[jboss]/server/default/conf/server.log.properties\",\"opt/[jboss]/server/default/deploy/jboss-logging.xml\",\"usr/local/[jboss]/server/default/conf/jboss-minimal.xml\",\"usr/local/[jboss]/server/default/conf/jboss-service.xml\",\"usr/local/[jboss]/server/default/conf/jndi.properties\",\"usr/local/[jboss]/server/default/conf/log4j.xml\",\"usr/local/[jboss]/server/default/conf/login-config.xml\",\"usr/local/[jboss]/server/default/conf/standardjaws.xml\",\"usr/local/[jboss]/server/default/conf/standardjboss.xml\",\"usr/local/[jboss]/server/default/conf/server.log.properties\",\"usr/local/[jboss]/server/default/deploy/jboss-logging.xml\",\"private/tmp/[jboss]/server/default/conf/jboss-minimal.xml\",\"private/tmp/[jboss]/server/default/conf/jboss-service.xml\",\"private/tmp/[jboss]/server/default/conf/jndi.properties\",\"private/tmp/[jboss]/server/default/conf/log4j.xml\",\"private/tmp/[jboss]/server/default/conf/login-config.xml\",\"private/tmp/[jboss]/server/default/conf/standardjaws.xml\",\"private/tmp/[jboss]/server/default/conf/standardjboss.xml\",\"private/tmp/[jboss]/server/default/conf/server.log.properties\",\"private/tmp/[jboss]/server/default/deploy/jboss-logging.xml\",\"tmp/[jboss]/server/default/conf/jboss-minimal.xml\",\"tmp/[jboss]/server/default/conf/jboss-service.xml\",\"tmp/[jboss]/server/default/conf/jndi.properties\",\"tmp/[jboss]/server/default/conf/log4j.xml\",\"tmp/[jboss]/server/default/conf/login-config.xml\",\"tmp/[jboss]/server/default/conf/standardjaws.xml\",\"tmp/[jboss]/server/default/conf/standardjboss.xml\",\"tmp/[jboss]/server/default/conf/server.log.properties\",\"tmp/[jboss]/server/default/deploy/jboss-logging.xml\",\"program files/[jboss]/server/default/conf/jboss-minimal.xml\",\"program files/[jboss]/server/default/conf/jboss-service.xml\",\"program files/[jboss]/server/default/conf/jndi.properties\",\"program files/[jboss]/server/default/conf/log4j.xml\",\"program files/[jboss]/server/default/conf/login-config.xml\",\"program files/[jboss]/server/default/conf/standardjaws.xml\",\"program files/[jboss]/server/default/conf/standardjboss.xml\",\"program files/[jboss]/server/default/conf/server.log.properties\",\"program files/[jboss]/server/default/deploy/jboss-logging.xml\",\"[jboss]/server/default/conf/jboss-minimal.xml\",\"[jboss]/server/default/conf/jboss-service.xml\",\"[jboss]/server/default/conf/jndi.properties\",\"[jboss]/server/default/conf/log4j.xml\",\"[jboss]/server/default/conf/login-config.xml\",\"[jboss]/server/default/conf/standardjaws.xml\",\"[jboss]/server/default/conf/standardjboss.xml\",\"[jboss]/server/default/conf/server.log.properties\",\"[jboss]/server/default/deploy/jboss-logging.xml\",\"opt/[jboss]/server/default/log/server.log\",\"opt/[jboss]/server/default/log/boot.log\",\"usr/local/[jboss]/server/default/log/server.log\",\"usr/local/[jboss]/server/default/log/boot.log\",\"private/tmp/[jboss]/server/default/log/server.log\",\"private/tmp/[jboss]/server/default/log/boot.log\",\"tmp/[jboss]/server/default/log/server.log\",\"tmp/[jboss]/server/default/log/boot.log\",\"program files/[jboss]/server/default/log/server.log\",\"program files/[jboss]/server/default/log/boot.log\",\"[jboss]/server/default/log/server.log\",\"[jboss]/server/default/log/boot.log\",\"var/log/lighttpd.error.log\",\"var/log/lighttpd.access.log\",\"var/lighttpd.log\",\"var/logs/access.log\",\"var/log/lighttpd/\",\"var/log/lighttpd/error.log\",\"var/log/lighttpd/access.www.log\",\"var/log/lighttpd/error.www.log\",\"var/log/lighttpd/access.log\",\"usr/local/apache2/logs/lighttpd.error.log\",\"usr/local/apache2/logs/lighttpd.log\",\"usr/local/apache/logs/lighttpd.error.log\",\"usr/local/apache/logs/lighttpd.log\",\"usr/local/lighttpd/log/lighttpd.error.log\",\"usr/local/lighttpd/log/access.log\",\"var/log/lighttpd/{domain}/access.log\",\"var/log/lighttpd/{domain}/error.log\",\"usr/home/user/var/log/lighttpd.error.log\",\"usr/home/user/var/log/apache.log\",\"home/user/lighttpd/lighttpd.conf\",\"usr/home/user/lighttpd/lighttpd.conf\",\"etc/lighttpd/lighthttpd.conf\",\"usr/local/etc/lighttpd.conf\",\"usr/local/lighttpd/conf/lighttpd.conf\",\"usr/local/etc/lighttpd.conf.new\",\"var/www/.lighttpdpassword\",\"var/log/nginx/access_log\",\"var/log/nginx/error_log\",\"var/log/nginx/access.log\",\"var/log/nginx/error.log\",\"var/log/nginx.access_log\",\"var/log/nginx.error_log\",\"logs/access_log\",\"logs/error_log\",\"etc/nginx/nginx.conf\",\"usr/local/etc/nginx/nginx.conf\",\"usr/local/nginx/conf/nginx.conf\",\"usr/local/zeus/web/global.cfg\",\"usr/local/zeus/web/log/errors\",\"opt/lsws/conf/httpd_conf.xml\",\"usr/local/lsws/conf/httpd_conf.xml\",\"opt/lsws/logs/error.log\",\"opt/lsws/logs/access.log\",\"usr/local/lsws/logs/error.log\",\"usr/local/logs/access.log\",\"usr/local/samba/lib/log.user\",\"usr/local/logs/samba.log\",\"var/log/samba/log.smbd\",\"var/log/samba/log.nmbd\",\"var/log/samba.log\",\"var/log/samba.log1\",\"var/log/samba.log2\",\"var/log/log.smb\",\"etc/samba/netlogon\",\"etc/smbpasswd\",\"etc/smb.conf\",\"etc/samba/dhcp.conf\",\"etc/samba/smb.conf\",\"etc/samba/samba.conf\",\"etc/samba/smb.conf.user\",\"etc/samba/smbpasswd\",\"etc/samba/smbusers\",\"etc/samba/private/smbpasswd\",\"usr/local/etc/smb.conf\",\"usr/local/samba/lib/smb.conf.user\",\"etc/dhcp3/dhclient.conf\",\"etc/dhcp3/dhcpd.conf\",\"etc/dhcp/dhclient.conf\",\"program files/vidalia bundle/polipo/polipo.conf\",\"etc/tor/tor-tsocks.conf\",\"etc/stunnel/stunnel.conf\",\"etc/tsocks.conf\",\"etc/tinyproxy/tinyproxy.conf\",\"etc/miredo-server.conf\",\"etc/miredo.conf\",\"etc/miredo/miredo-server.conf\",\"etc/miredo/miredo.conf\",\"etc/wicd/dhclient.conf.template.default\",\"etc/wicd/manager-settings.conf\",\"etc/wicd/wired-settings.conf\",\"etc/wicd/wireless-settings.conf\",\"var/log/ipfw.log\",\"var/log/ipfw\",\"var/log/ipfw/ipfw.log\",\"var/log/ipfw.today\",\"etc/ipfw.rules\",\"etc/ipfw.conf\",\"etc/firewall.rules\",\"winnt/system32/logfiles/firewall/pfirewall.log\",\"winnt/system32/logfiles/firewall/pfirewall.log.old\",\"windows/system32/logfiles/firewall/pfirewall.log\",\"windows/system32/logfiles/firewall/pfirewall.log.old\",\"etc/clamav/clamd.conf\",\"etc/clamav/freshclam.conf\",\"etc/x11/xorg.conf\",\"etc/x11/xorg.conf-vesa\",\"etc/x11/xorg.conf-vmware\",\"etc/x11/xorg.conf.beforevmwaretoolsinstall\",\"etc/x11/xorg.conf.orig\",\"etc/bluetooth/input.conf\",\"etc/bluetooth/main.conf\",\"etc/bluetooth/network.conf\",\"etc/bluetooth/rfcomm.conf\",\"proc/self/environ\",\"proc/self/mounts\",\"proc/self/stat\",\"proc/self/status\",\"proc/self/cmdline\",\"proc/self/fd/0\",\"proc/self/fd/1\",\"proc/self/fd/2\",\"proc/self/fd/3\",\"proc/self/fd/4\",\"proc/self/fd/5\",\"proc/self/fd/6\",\"proc/self/fd/7\",\"proc/self/fd/8\",\"proc/self/fd/9\",\"proc/self/fd/10\",\"proc/self/fd/11\",\"proc/self/fd/12\",\"proc/self/fd/13\",\"proc/self/fd/14\",\"proc/self/fd/15\",\"proc/version\",\"proc/devices\",\"proc/cpuinfo\",\"proc/meminfo\",\"proc/net/tcp\",\"proc/net/udp\",\"etc/bash_completion.d/debconf\",\"root/.bash_logout\",\"root/.bash_history\",\"root/.bash_config\",\"root/.bashrc\",\"etc/bash.bashrc\",\"var/adm/syslog\",\"var/adm/sulog\",\"var/adm/utmp\",\"var/adm/utmpx\",\"var/adm/wtmp\",\"var/adm/wtmpx\",\"var/adm/lastlog/username\",\"usr/spool/lp/log\",\"var/adm/lp/lpd-errs\",\"usr/lib/cron/log\",\"var/adm/loginlog\",\"var/adm/pacct\",\"var/adm/dtmp\",\"var/adm/acct/sum/loginlog\",\"var/adm/x0msgs\",\"var/adm/crash/vmcore\",\"var/adm/crash/unix\",\"etc/newsyslog.conf\",\"var/adm/qacct\",\"var/adm/ras/errlog\",\"var/adm/ras/bootlog\",\"var/adm/cron/log\",\"etc/utmp\",\"etc/security/lastlog\",\"etc/security/failedlogin\",\"usr/spool/mqueue/syslog\",\"var/adm/messages\",\"var/adm/aculogs\",\"var/adm/aculog\",\"var/adm/vold.log\",\"var/adm/log/asppp.log\",\"var/log/poplog\",\"var/log/authlog\",\"var/lp/logs/lpsched\",\"var/lp/logs/lpnet\",\"var/lp/logs/requests\",\"var/cron/log\",\"var/saf/_log\",\"var/saf/port/log\",\"var/log/news.all\",\"var/log/news/news.all\",\"var/log/news/news.crit\",\"var/log/news/news.err\",\"var/log/news/news.notice\",\"var/log/news/suck.err\",\"var/log/news/suck.notice\",\"var/log/messages\",\"var/log/messages.1\",\"var/log/user.log\",\"var/log/user.log.1\",\"var/log/auth.log\",\"var/log/pm-powersave.log\",\"var/log/xorg.0.log\",\"var/log/daemon.log\",\"var/log/daemon.log.1\",\"var/log/kern.log\",\"var/log/kern.log.1\",\"var/log/mail.err\",\"var/log/mail.info\",\"var/log/mail.warn\",\"var/log/ufw.log\",\"var/log/boot.log\",\"var/log/syslog\",\"var/log/syslog.1\",\"tmp/access.log\",\"etc/sensors.conf\",\"etc/sensors3.conf\",\"etc/host.conf\",\"etc/pam.conf\",\"etc/resolv.conf\",\"etc/apt/apt.conf\",\"etc/inetd.conf\",\"etc/syslog.conf\",\"etc/sysctl.conf\",\"etc/sysctl.d/10-console-messages.conf\",\"etc/sysctl.d/10-network-security.conf\",\"etc/sysctl.d/10-process-security.conf\",\"etc/sysctl.d/wine.sysctl.conf\",\"etc/security/access.conf\",\"etc/security/group.conf\",\"etc/security/limits.conf\",\"etc/security/namespace.conf\",\"etc/security/pam_env.conf\",\"etc/security/sepermit.conf\",\"etc/security/time.conf\",\"etc/ssh/sshd_config\",\"etc/adduser.conf\",\"etc/deluser.conf\",\"etc/avahi/avahi-daemon.conf\",\"etc/ca-certificates.conf\",\"etc/ca-certificates.conf.dpkg-old\",\"etc/casper.conf\",\"etc/chkrootkit.conf\",\"etc/debconf.conf\",\"etc/dns2tcpd.conf\",\"etc/e2fsck.conf\",\"etc/esound/esd.conf\",\"etc/etter.conf\",\"etc/fuse.conf\",\"etc/foremost.conf\",\"etc/hdparm.conf\",\"etc/kernel-img.conf\",\"etc/kernel-pkg.conf\",\"etc/ld.so.conf\",\"etc/ltrace.conf\",\"etc/mail/sendmail.conf\",\"etc/manpath.config\",\"etc/kbd/config\",\"etc/ldap/ldap.conf\",\"etc/logrotate.conf\",\"etc/mtools.conf\",\"etc/smi.conf\",\"etc/updatedb.conf\",\"etc/pulse/client.conf\",\"usr/share/adduser/adduser.conf\",\"etc/hostname\",\"etc/networks\",\"etc/timezone\",\"etc/modules\",\"etc/passwd\",\"etc/passwd~\",\"etc/passwd-\",\"etc/shadow\",\"etc/shadow~\",\"etc/shadow-\",\"etc/fstab\",\"etc/motd\",\"etc/hosts\",\"etc/group\",\"etc/group-\",\"etc/alias\",\"etc/crontab\",\"etc/crypttab\",\"etc/exports\",\"etc/mtab\",\"etc/hosts.allow\",\"etc/hosts.deny\",\"etc/os-release\",\"etc/password.master\",\"etc/profile\",\"etc/default/grub\",\"etc/resolvconf/update-libc.d/sendmail\",\"etc/inittab\",\"etc/issue\",\"etc/issue.net\",\"etc/login.defs\",\"etc/sudoers\",\"etc/sysconfig/network-scripts/ifcfg-eth0\",\"etc/redhat-release\",\"etc/debian_version\",\"etc/fedora-release\",\"etc/mandrake-release\",\"etc/slackware-release\",\"etc/suse-release\",\"etc/security/group\",\"etc/security/passwd\",\"etc/security/user\",\"etc/security/environ\",\"etc/security/limits\",\"etc/security/opasswd\",\"boot/grub/grub.cfg\",\"boot/grub/menu.lst\",\"root/.ksh_history\",\"root/.xauthority\",\"usr/lib/security/mkuser.default\",\"var/log/squirrelmail.log\",\"var/log/apache2/squirrelmail.log\",\"var/log/apache2/squirrelmail.err.log\",\"var/lib/squirrelmail/prefs/squirrelmail.log\",\"var/log/mail.log\",\"etc/squirrelmail/apache.conf\",\"etc/squirrelmail/config_local.php\",\"etc/squirrelmail/default_pref\",\"etc/squirrelmail/index.php\",\"etc/squirrelmail/config_default.php\",\"etc/squirrelmail/config.php\",\"etc/squirrelmail/filters_setup.php\",\"etc/squirrelmail/sqspell_config.php\",\"etc/squirrelmail/config/config.php\",\"etc/httpd/conf.d/squirrelmail.conf\",\"usr/share/squirrelmail/config/config.php\",\"private/etc/squirrelmail/config/config.php\",\"srv/www/htdos/squirrelmail/config/config.php\",\"var/www/squirrelmail/config/config.php\",\"var/www/html/squirrelmail/config/config.php\",\"var/www/html/squirrelmail-1.2.9/config/config.php\",\"usr/share/squirrelmail/plugins/squirrel_logger/setup.php\",\"usr/local/squirrelmail/www/readme\",\"windows/system32/drivers/etc/hosts\",\"windows/system32/drivers/etc/lmhosts.sam\",\"windows/system32/drivers/etc/networks\",\"windows/system32/drivers/etc/protocol\",\"windows/system32/drivers/etc/services\",\"/boot.ini\",\"windows/debug/netsetup.log\",\"windows/comsetup.log\",\"windows/repair/setup.log\",\"windows/setupact.log\",\"windows/setupapi.log\",\"windows/setuperr.log\",\"windows/updspapi.log\",\"windows/wmsetup.log\",\"windows/windowsupdate.log\",\"windows/odbc.ini\",\"usr/local/psa/admin/htdocs/domains/databases/phpmyadmin/libraries/config.default.php\",\"etc/apache2/conf.d/phpmyadmin.conf\",\"etc/phpmyadmin/config.inc.php\",\"etc/openldap/ldap.conf\",\"etc/cups/acroread.conf\",\"etc/cups/cupsd.conf\",\"etc/cups/cupsd.conf.default\",\"etc/cups/pdftops.conf\",\"etc/cups/printers.conf\",\"windows/system32/macromed/flash/flashinstall.log\",\"windows/system32/macromed/flash/install.log\",\"etc/cvs-cron.conf\",\"etc/cvs-pserver.conf\",\"etc/subversion/config\",\"etc/modprobe.d/vmware-tools.conf\",\"etc/updatedb.conf.beforevmwaretoolsinstall\",\"etc/vmware-tools/config\",\"etc/vmware-tools/tpvmlp.conf\",\"etc/vmware-tools/vmware-tools-libraries.conf\",\"var/log/vmware/hostd.log\",\"var/log/vmware/hostd-1.log\",\"wp-config.php\",\"wp-config.bak\",\"wp-config.old\",\"wp-config.temp\",\"wp-config.tmp\",\"wp-config.txt\",\"config.yml\",\"config_dev.yml\",\"config_prod.yml\",\"config_test.yml\",\"parameters.yml\",\"routing.yml\",\"security.yml\",\"services.yml\",\"sites/default/default.settings.php\",\"sites/default/settings.php\",\"sites/default/settings.local.php\",\"app/etc/local.xml\",\"sftp-config.json\",\"web.config\",\"includes/config.php\",\"includes/configure.php\",\"config.inc.php\",\"localsettings.php\",\"inc/config.php\",\"typo3conf/localconf.php\",\"config/app.php\",\"config/custom.php\",\"config/database.php\",\"/configuration.php\",\"/config.php\",\"var/mail/www-data\",\"etc/network/\",\"etc/init/\",\"inetpub/wwwroot/global.asa\",\"system32/inetsrv/config/applicationhost.config\",\"system32/inetsrv/config/administration.config\",\"system32/inetsrv/config/redirection.config\",\"system32/config/default\",\"system32/config/sam\",\"system32/config/system\",\"system32/config/software\",\"winnt/repair/sam._\",\"package.json\",\"package-lock.json\",\"gruntfile.js\",\"npm-debug.log\",\"ormconfig.json\",\"tsconfig.json\",\"webpack.config.js\",\"yarn.lock\"]},\"operator\":\"phrase_match\"}],\"transformers\":[\"lowercase\"]},{\"id\":\"crs-931-110\",\"name\":\"Possible Remote File Inclusion (RFI) Attack: Common RFI Vulnerable Parameter Name used w/URL Payload\",\"tags\":{\"type\":\"rfi\",\"crs_id\":\"931110\",\"category\":\"attack_attempt\"},\"conditions\":[{\"parameters\":{\"inputs\":[{\"address\":\"server.request.query\"}],\"regex\":\"(?:\\\\binclude\\\\s*\\\\([^)]*|mosConfig_absolute_path|_CONF\\\\[path\\\\]|_SERVER\\\\[DOCUMENT_ROOT\\\\]|GALLERY_BASEDIR|path\\\\[docroot\\\\]|appserv_root|config\\\\[root_dir\\\\])=(?:file|ftps?|https?)://\",\"options\":{\"min_length\":15}},\"operator\":\"match_regex\"}],\"transformers\":[]},{\"id\":\"crs-931-120\",\"name\":\"Possible Remote File Inclusion (RFI) Attack: URL Payload Used w/Trailing Question Mark Character (?)\",\"tags\":{\"type\":\"rfi\",\"crs_id\":\"931120\",\"category\":\"attack_attempt\"},\"conditions\":[{\"parameters\":{\"inputs\":[{\"address\":\"server.request.query\"},{\"address\":\"server.request.body\"},{\"address\":\"server.request.path_params\"}],\"regex\":\"^(?i:file|ftps?|https?).*?\\\\?+$\",\"options\":{\"case_sensitive\":true,\"min_length\":4}},\"operator\":\"match_regex\"}],\"transformers\":[]},{\"id\":\"crs-932-160\",\"name\":\"Remote Command Execution: Unix Shell Code Found\",\"tags\":{\"type\":\"command_injection\",\"crs_id\":\"932160\",\"category\":\"attack_attempt\"},\"conditions\":[{\"parameters\":{\"inputs\":[{\"address\":\"server.request.cookies\"},{\"address\":\"server.request.query\"},{\"address\":\"server.request.body\"},{\"address\":\"server.request.path_params\"}],\"list\":[\"${cdpath}\",\"${dirstack}\",\"${home}\",\"${hostname}\",\"${ifs}\",\"${oldpwd}\",\"${ostype}\",\"${path}\",\"${pwd}\",\"$cdpath\",\"$dirstack\",\"$home\",\"$hostname\",\"$ifs\",\"$oldpwd\",\"$ostype\",\"$path\",\"$pwd\",\"bin/bash\",\"bin/cat\",\"bin/csh\",\"bin/dash\",\"bin/du\",\"bin/echo\",\"bin/grep\",\"bin/less\",\"bin/ls\",\"bin/mknod\",\"bin/more\",\"bin/nc\",\"bin/ps\",\"bin/rbash\",\"bin/sh\",\"bin/sleep\",\"bin/su\",\"bin/tcsh\",\"bin/uname\",\"dev/fd/\",\"dev/null\",\"dev/stderr\",\"dev/stdin\",\"dev/stdout\",\"dev/tcp/\",\"dev/udp/\",\"dev/zero\",\"etc/group\",\"etc/master.passwd\",\"etc/passwd\",\"etc/pwd.db\",\"etc/shadow\",\"etc/shells\",\"etc/spwd.db\",\"proc/self/\",\"usr/bin/awk\",\"usr/bin/base64\",\"usr/bin/cat\",\"usr/bin/cc\",\"usr/bin/clang\",\"usr/bin/clang++\",\"usr/bin/curl\",\"usr/bin/diff\",\"usr/bin/env\",\"usr/bin/fetch\",\"usr/bin/file\",\"usr/bin/find\",\"usr/bin/ftp\",\"usr/bin/gawk\",\"usr/bin/gcc\",\"usr/bin/head\",\"usr/bin/hexdump\",\"usr/bin/id\",\"usr/bin/less\",\"usr/bin/ln\",\"usr/bin/mkfifo\",\"usr/bin/more\",\"usr/bin/nc\",\"usr/bin/ncat\",\"usr/bin/nice\",\"usr/bin/nmap\",\"usr/bin/perl\",\"usr/bin/php\",\"usr/bin/php5\",\"usr/bin/php7\",\"usr/bin/php-cgi\",\"usr/bin/printf\",\"usr/bin/psed\",\"usr/bin/python\",\"usr/bin/python2\",\"usr/bin/python3\",\"usr/bin/ruby\",\"usr/bin/sed\",\"usr/bin/socat\",\"usr/bin/tail\",\"usr/bin/tee\",\"usr/bin/telnet\",\"usr/bin/top\",\"usr/bin/uname\",\"usr/bin/wget\",\"usr/bin/who\",\"usr/bin/whoami\",\"usr/bin/xargs\",\"usr/bin/xxd\",\"usr/bin/yes\",\"usr/local/bin/bash\",\"usr/local/bin/curl\",\"usr/local/bin/ncat\",\"usr/local/bin/nmap\",\"usr/local/bin/perl\",\"usr/local/bin/php\",\"usr/local/bin/python\",\"usr/local/bin/python2\",\"usr/local/bin/python3\",\"usr/local/bin/rbash\",\"usr/local/bin/ruby\",\"usr/local/bin/wget\"]},\"operator\":\"phrase_match\"}],\"transformers\":[\"lowercase\"]},{\"id\":\"crs-932-171\",\"name\":\"Remote Command Execution: Shellshock (CVE-2014-6271)\",\"tags\":{\"type\":\"command_injection\",\"crs_id\":\"932171\",\"category\":\"attack_attempt\"},\"conditions\":[{\"parameters\":{\"inputs\":[{\"address\":\"server.request.query\"},{\"address\":\"server.request.body\"},{\"address\":\"server.request.path_params\"},{\"address\":\"server.request.headers.no_cookies\"}],\"regex\":\"^\\\\(\\\\s*\\\\)\\\\s+{\",\"options\":{\"case_sensitive\":true,\"min_length\":4}},\"operator\":\"match_regex\"}],\"transformers\":[]},{\"id\":\"crs-932-180\",\"name\":\"Restricted File Upload Attempt\",\"tags\":{\"type\":\"command_injection\",\"crs_id\":\"932180\",\"category\":\"attack_attempt\"},\"conditions\":[{\"parameters\":{\"inputs\":[{\"address\":\"server.request.headers.no_cookies\",\"key_path\":[\"x-filename\",\"x_filename\",\"x-file-name\"]}],\"list\":[\".htaccess\",\".htdigest\",\".htpasswd\",\"wp-config.php\",\"config.yml\",\"config_dev.yml\",\"config_prod.yml\",\"config_test.yml\",\"parameters.yml\",\"routing.yml\",\"security.yml\",\"services.yml\",\"default.settings.php\",\"settings.php\",\"settings.local.php\",\"local.xml\",\".env\"]},\"operator\":\"phrase_match\"}],\"transformers\":[\"lowercase\"]},{\"id\":\"crs-933-111\",\"name\":\"PHP Injection Attack: PHP Script File Upload Found\",\"tags\":{\"type\":\"unrestricted_file_upload\",\"crs_id\":\"933111\",\"category\":\"attack_attempt\"},\"conditions\":[{\"parameters\":{\"inputs\":[{\"address\":\"server.request.headers.no_cookies\",\"key_path\":[\"x-filename\",\"x_filename\",\"x.filename\",\"x-file-name\"]}],\"regex\":\".*\\\\.(?:php\\\\d*|phtml)\\\\..*$\",\"options\":{\"case_sensitive\":true,\"min_length\":5}},\"operator\":\"match_regex\"}],\"transformers\":[\"lowercase\"]},{\"id\":\"crs-933-130\",\"name\":\"PHP Injection Attack: Global Variables Found\",\"tags\":{\"type\":\"php_code_injection\",\"crs_id\":\"933130\",\"category\":\"attack_attempt\"},\"conditions\":[{\"parameters\":{\"inputs\":[{\"address\":\"server.request.cookies\"},{\"address\":\"server.request.query\"},{\"address\":\"server.request.body\"},{\"address\":\"server.request.path_params\"}],\"list\":[\"$globals\",\"$http_cookie_vars\",\"$http_env_vars\",\"$http_get_vars\",\"$http_post_files\",\"$http_post_vars\",\"$http_raw_post_data\",\"$http_request_vars\",\"$http_server_vars\",\"$_cookie\",\"$_env\",\"$_files\",\"$_get\",\"$_post\",\"$_request\",\"$_server\",\"$_session\",\"$argc\",\"$argv\"]},\"operator\":\"phrase_match\"}],\"transformers\":[\"lowercase\"]},{\"id\":\"crs-933-131\",\"name\":\"PHP Injection Attack: HTTP Headers Values Found\",\"tags\":{\"type\":\"php_code_injection\",\"crs_id\":\"933131\",\"category\":\"attack_attempt\"},\"conditions\":[{\"parameters\":{\"inputs\":[{\"address\":\"server.request.cookies\"},{\"address\":\"server.request.query\"},{\"address\":\"server.request.body\"},{\"address\":\"server.request.path_params\"}],\"regex\":\"(?:HTTP_(?:ACCEPT(?:_(?:ENCODING|LANGUAGE|CHARSET))?|(?:X_FORWARDED_FO|REFERE)R|(?:USER_AGEN|HOS)T|CONNECTION|KEEP_ALIVE)|PATH_(?:TRANSLATED|INFO)|ORIG_PATH_INFO|QUERY_STRING|REQUEST_URI|AUTH_TYPE)\",\"options\":{\"case_sensitive\":true,\"min_length\":9}},\"operator\":\"match_regex\"}],\"transformers\":[]},{\"id\":\"crs-933-140\",\"name\":\"PHP Injection Attack: I/O Stream Found\",\"tags\":{\"type\":\"php_code_injection\",\"crs_id\":\"933140\",\"category\":\"attack_attempt\"},\"conditions\":[{\"parameters\":{\"inputs\":[{\"address\":\"server.request.cookies\"},{\"address\":\"server.request.query\"},{\"address\":\"server.request.body\"},{\"address\":\"server.request.path_params\"}],\"regex\":\"php://(?:std(?:in|out|err)|(?:in|out)put|fd|memory|temp|filter)\",\"options\":{\"min_length\":8}},\"operator\":\"match_regex\"}],\"transformers\":[]},{\"id\":\"crs-933-150\",\"name\":\"PHP Injection Attack: High-Risk PHP Function Name Found\",\"tags\":{\"type\":\"php_code_injection\",\"crs_id\":\"933150\",\"category\":\"attack_attempt\"},\"conditions\":[{\"parameters\":{\"inputs\":[{\"address\":\"server.request.cookies\"},{\"address\":\"server.request.query\"},{\"address\":\"server.request.body\"},{\"address\":\"server.request.path_params\"}],\"list\":[\"__halt_compiler\",\"apache_child_terminate\",\"base64_decode\",\"bzdecompress\",\"call_user_func\",\"call_user_func_array\",\"call_user_method\",\"call_user_method_array\",\"convert_uudecode\",\"file_get_contents\",\"file_put_contents\",\"fsockopen\",\"get_class_methods\",\"get_class_vars\",\"get_defined_constants\",\"get_defined_functions\",\"get_defined_vars\",\"gzdecode\",\"gzinflate\",\"gzuncompress\",\"include_once\",\"invokeargs\",\"pcntl_exec\",\"pcntl_fork\",\"pfsockopen\",\"posix_getcwd\",\"posix_getpwuid\",\"posix_getuid\",\"posix_uname\",\"reflectionfunction\",\"require_once\",\"shell_exec\",\"str_rot13\",\"sys_get_temp_dir\",\"wp_remote_fopen\",\"wp_remote_get\",\"wp_remote_head\",\"wp_remote_post\",\"wp_remote_request\",\"wp_safe_remote_get\",\"wp_safe_remote_head\",\"wp_safe_remote_post\",\"wp_safe_remote_request\",\"zlib_decode\"]},\"operator\":\"phrase_match\"}],\"transformers\":[\"lowercase\"]},{\"id\":\"crs-933-160\",\"name\":\"PHP Injection Attack: High-Risk PHP Function Call Found\",\"tags\":{\"type\":\"php_code_injection\",\"crs_id\":\"933160\",\"category\":\"attack_attempt\"},\"conditions\":[{\"parameters\":{\"inputs\":[{\"address\":\"server.request.cookies\"},{\"address\":\"server.request.query\"},{\"address\":\"server.request.body\"},{\"address\":\"server.request.path_params\"}],\"regex\":\"\\\\b(?:s(?:e(?:t(?:_(?:e(?:xception|rror)_handler|magic_quotes_runtime|include_path)|defaultstub)|ssion_s(?:et_save_handler|tart))|qlite_(?:(?:(?:unbuffered|single|array)_)?query|create_(?:aggregate|function)|p?open|exec)|tr(?:eam_(?:context_create|socket_client)|ipc?slashes|rev)|implexml_load_(?:string|file)|ocket_c(?:onnect|reate)|h(?:ow_sourc|a1_fil)e|pl_autoload_register|ystem)|p(?:r(?:eg_(?:replace(?:_callback(?:_array)?)?|match(?:_all)?|split)|oc_(?:(?:terminat|clos|nic)e|get_status|open)|int_r)|o(?:six_(?:get(?:(?:e[gu]|g)id|login|pwnam)|mk(?:fifo|nod)|ttyname|kill)|pen)|hp(?:_(?:strip_whitespac|unam)e|version|info)|g_(?:(?:execut|prepar)e|connect|query)|a(?:rse_(?:ini_file|str)|ssthru)|utenv)|r(?:unkit_(?:function_(?:re(?:defin|nam)e|copy|add)|method_(?:re(?:defin|nam)e|copy|add)|constant_(?:redefine|add))|e(?:(?:gister_(?:shutdown|tick)|name)_function|ad(?:(?:gz)?file|_exif_data|dir))|awurl(?:de|en)code)|i(?:mage(?:createfrom(?:(?:jpe|pn)g|x[bp]m|wbmp|gif)|(?:jpe|pn)g|g(?:d2?|if)|2?wbmp|xbm)|s_(?:(?:(?:execut|write?|read)ab|fi)le|dir)|ni_(?:get(?:_all)?|set)|terator_apply|ptcembed)|g(?:et(?:_(?:c(?:urrent_use|fg_va)r|meta_tags)|my(?:[gpu]id|inode)|(?:lastmo|cw)d|imagesize|env)|z(?:(?:(?:defla|wri)t|encod|fil)e|compress|open|read)|lob)|a(?:rray_(?:u(?:intersect(?:_u?assoc)?|diff(?:_u?assoc)?)|intersect_u(?:assoc|key)|diff_u(?:assoc|key)|filter|reduce|map)|ssert(?:_options)?)|h(?:tml(?:specialchars(?:_decode)?|_entity_decode|entities)|(?:ash(?:_(?:update|hmac))?|ighlight)_file|e(?:ader_register_callback|x2bin))|f(?:i(?:le(?:(?:[acm]tim|inod)e|(?:_exist|perm)s|group)?|nfo_open)|tp_(?:nb_(?:ge|pu)|connec|ge|pu)t|(?:unction_exis|pu)ts|write|open)|o(?:b_(?:get_(?:c(?:ontents|lean)|flush)|end_(?:clean|flush)|clean|flush|start)|dbc_(?:result(?:_all)?|exec(?:ute)?|connect)|pendir)|m(?:b_(?:ereg(?:_(?:replace(?:_callback)?|match)|i(?:_replace)?)?|parse_str)|(?:ove_uploaded|d5)_file|ethod_exists|ysql_query|kdir)|e(?:x(?:if_(?:t(?:humbnail|agname)|imagetype|read_data)|ec)|scapeshell(?:arg|cmd)|rror_reporting|val)|c(?:url_(?:file_create|exec|init)|onvert_uuencode|reate_function|hr)|u(?:n(?:serialize|pack)|rl(?:de|en)code|[ak]?sort)|(?:json_(?:de|en)cod|debug_backtrac|tmpfil)e|b(?:(?:son_(?:de|en)|ase64_en)code|zopen)|var_dump)(?:\\\\s|/\\\\*.*\\\\*/|//.*|#.*)*\\\\(.*\\\\)\",\"options\":{\"min_length\":5}},\"operator\":\"match_regex\"}],\"transformers\":[]},{\"id\":\"crs-933-170\",\"name\":\"PHP Injection Attack: Serialized Object Injection\",\"tags\":{\"type\":\"php_code_injection\",\"crs_id\":\"933170\",\"category\":\"attack_attempt\"},\"conditions\":[{\"parameters\":{\"inputs\":[{\"address\":\"server.request.cookies\"},{\"address\":\"server.request.headers.no_cookies\"},{\"address\":\"server.request.query\"},{\"address\":\"server.request.body\"},{\"address\":\"server.request.path_params\"}],\"regex\":\"[oOcC]:\\\\d+:\\\\\\\".+?\\\\\\\":\\\\d+:{[\\\\W\\\\w]*}\",\"options\":{\"case_sensitive\":true,\"min_length\":12}},\"operator\":\"match_regex\"}],\"transformers\":[]},{\"id\":\"crs-933-200\",\"name\":\"PHP Injection Attack: Wrapper scheme detected\",\"tags\":{\"type\":\"php_code_injection\",\"crs_id\":\"933200\",\"category\":\"attack_attempt\"},\"conditions\":[{\"parameters\":{\"inputs\":[{\"address\":\"server.request.cookies\"},{\"address\":\"server.request.query\"},{\"address\":\"server.request.body\"},{\"address\":\"server.request.path_params\"}],\"regex\":\"(?i:zlib|glob|phar|ssh2|rar|ogg|expect|zip)://\",\"options\":{\"case_sensitive\":true,\"min_length\":6}},\"operator\":\"match_regex\"}],\"transformers\":[\"removeNulls\"]},{\"id\":\"crs-934-100\",\"name\":\"Node.js Injection Attack\",\"tags\":{\"type\":\"js_code_injection\",\"crs_id\":\"934100\",\"category\":\"attack_attempt\"},\"conditions\":[{\"parameters\":{\"inputs\":[{\"address\":\"server.request.cookies\"},{\"address\":\"server.request.query\"},{\"address\":\"server.request.body\"},{\"address\":\"server.request.path_params\"}],\"regex\":\"(?:(?:_(?:\\\\$\\\\$ND_FUNC\\\\$\\\\$_|_js_function)|(?:new\\\\s+Function|\\\\beval)\\\\s*\\\\(|String\\\\s*\\\\.\\\\s*fromCharCode|function\\\\s*\\\\(\\\\s*\\\\)\\\\s*{|this\\\\.constructor)|module\\\\.exports\\\\s*=)\",\"options\":{\"case_sensitive\":true,\"min_length\":5}},\"operator\":\"match_regex\"}],\"transformers\":[]},{\"id\":\"crs-941-100\",\"name\":\"XSS Attack Detected via libinjection\",\"tags\":{\"type\":\"xss\",\"crs_id\":\"941100\",\"category\":\"attack_attempt\"},\"conditions\":[{\"parameters\":{\"inputs\":[{\"address\":\"server.request.cookies\"},{\"address\":\"server.request.headers.no_cookies\",\"key_path\":[\"user-agent\",\"referer\"]},{\"address\":\"server.request.query\"},{\"address\":\"server.request.body\"},{\"address\":\"server.request.path_params\"}]},\"operator\":\"is_xss\"}],\"transformers\":[\"removeNulls\"]},{\"id\":\"crs-941-110\",\"name\":\"XSS Filter - Category 1: Script Tag Vector\",\"tags\":{\"type\":\"xss\",\"crs_id\":\"941110\",\"category\":\"attack_attempt\"},\"conditions\":[{\"parameters\":{\"inputs\":[{\"address\":\"server.request.cookies\"},{\"address\":\"server.request.headers.no_cookies\",\"key_path\":[\"user-agent\",\"referer\"]},{\"address\":\"server.request.query\"},{\"address\":\"server.request.body\"},{\"address\":\"server.request.path_params\"}],\"regex\":\"]*>[\\\\s\\\\S]*?\",\"options\":{\"min_length\":8}},\"operator\":\"match_regex\"}],\"transformers\":[\"removeNulls\"]},{\"id\":\"crs-941-120\",\"name\":\"XSS Filter - Category 2: Event Handler Vector\",\"tags\":{\"type\":\"xss\",\"crs_id\":\"941120\",\"category\":\"attack_attempt\"},\"conditions\":[{\"parameters\":{\"inputs\":[{\"address\":\"server.request.cookies\"},{\"address\":\"server.request.headers.no_cookies\",\"key_path\":[\"user-agent\",\"referer\"]},{\"address\":\"server.request.query\"},{\"address\":\"server.request.body\"},{\"address\":\"server.request.path_params\"}],\"regex\":\"[\\\\s\\\\\\\"'`;\\\\/0-9=\\\\x0B\\\\x09\\\\x0C\\\\x3B\\\\x2C\\\\x28\\\\x3B]on[a-zA-Z]{3,25}[\\\\s\\\\x0B\\\\x09\\\\x0C\\\\x3B\\\\x2C\\\\x28\\\\x3B]*?=[^=]\",\"options\":{\"min_length\":8}},\"operator\":\"match_regex\"}],\"transformers\":[\"removeNulls\"]},{\"id\":\"crs-941-140\",\"name\":\"XSS Filter - Category 4: Javascript URI Vector\",\"tags\":{\"type\":\"xss\",\"crs_id\":\"941140\",\"category\":\"attack_attempt\"},\"conditions\":[{\"parameters\":{\"inputs\":[{\"address\":\"server.request.cookies\"},{\"address\":\"server.request.headers.no_cookies\",\"key_path\":[\"user-agent\",\"referer\"]},{\"address\":\"server.request.query\"},{\"address\":\"server.request.body\"},{\"address\":\"server.request.path_params\"}],\"regex\":\"[a-z]+=(?:[^:=]+:.+;)*?[^:=]+:url\\\\(javascript\",\"options\":{\"min_length\":18}},\"operator\":\"match_regex\"}],\"transformers\":[\"removeNulls\"]},{\"id\":\"crs-941-180\",\"name\":\"Node-Validator Deny List Keywords\",\"tags\":{\"type\":\"xss\",\"crs_id\":\"941180\",\"category\":\"attack_attempt\"},\"conditions\":[{\"parameters\":{\"inputs\":[{\"address\":\"server.request.cookies\"},{\"address\":\"server.request.query\"},{\"address\":\"server.request.body\"},{\"address\":\"server.request.path_params\"}],\"list\":[\"document.cookie\",\"document.write\",\".parentnode\",\".innerhtml\",\"window.location\",\"-moz-binding\",\"]\",\"options\":{\"min_length\":8}},\"operator\":\"match_regex\"}],\"transformers\":[\"removeNulls\"]},{\"id\":\"crs-941-300\",\"name\":\"IE XSS Filters - Attack Detected via object tag\",\"tags\":{\"type\":\"xss\",\"crs_id\":\"941300\",\"category\":\"attack_attempt\"},\"conditions\":[{\"parameters\":{\"inputs\":[{\"address\":\"server.request.cookies\"},{\"address\":\"server.request.query\"},{\"address\":\"server.request.body\"},{\"address\":\"server.request.path_params\"}],\"regex\":\")|<.*\\\\+AD4-\",\"options\":{\"case_sensitive\":true,\"min_length\":6}},\"operator\":\"match_regex\"}],\"transformers\":[]},{\"id\":\"crs-941-360\",\"name\":\"JSFuck / Hieroglyphy obfuscation detected\",\"tags\":{\"type\":\"xss\",\"crs_id\":\"941360\",\"category\":\"attack_attempt\"},\"conditions\":[{\"parameters\":{\"inputs\":[{\"address\":\"server.request.cookies\"},{\"address\":\"server.request.query\"},{\"address\":\"server.request.body\"},{\"address\":\"server.request.path_params\"}],\"regex\":\"![!+ ]\\\\[\\\\]\",\"options\":{\"case_sensitive\":true,\"min_length\":4}},\"operator\":\"match_regex\"}],\"transformers\":[]},{\"id\":\"crs-942-100\",\"name\":\"SQL Injection Attack Detected via libinjection\",\"tags\":{\"type\":\"sqli\",\"crs_id\":\"942100\",\"category\":\"attack_attempt\"},\"conditions\":[{\"parameters\":{\"inputs\":[{\"address\":\"server.request.cookies\"},{\"address\":\"server.request.query\"},{\"address\":\"server.request.body\"},{\"address\":\"server.request.path_params\"}]},\"operator\":\"is_sqli\"}],\"transformers\":[\"removeNulls\"]},{\"id\":\"crs-942-140\",\"name\":\"SQL Injection Attack: Common DB Names Detected\",\"tags\":{\"type\":\"sqli\",\"crs_id\":\"942140\",\"category\":\"attack_attempt\"},\"conditions\":[{\"parameters\":{\"inputs\":[{\"address\":\"server.request.cookies\"},{\"address\":\"server.request.query\"},{\"address\":\"server.request.body\"},{\"address\":\"server.request.path_params\"}],\"regex\":\"\\\\b(?:(?:m(?:s(?:ys(?:ac(?:cess(?:objects|storage|xml)|es)|(?:relationship|object|querie)s|modules2?)|db)|aster\\\\.\\\\.sysdatabases|ysql\\\\.db)|pg_(?:catalog|toast)|information_schema|northwind|tempdb)\\\\b|s(?:(?:ys(?:\\\\.database_name|aux)|qlite(?:_temp)?_master)\\\\b|chema(?:_name\\\\b|\\\\W*\\\\())|d(?:atabas|b_nam)e\\\\W*\\\\()\",\"options\":{\"min_length\":4}},\"operator\":\"match_regex\"}],\"transformers\":[]},{\"id\":\"crs-942-160\",\"name\":\"Detects blind sqli tests using sleep() or benchmark()\",\"tags\":{\"type\":\"sqli\",\"crs_id\":\"942160\",\"category\":\"attack_attempt\"},\"conditions\":[{\"parameters\":{\"inputs\":[{\"address\":\"server.request.cookies\"},{\"address\":\"server.request.query\"},{\"address\":\"server.request.body\"},{\"address\":\"server.request.path_params\"}],\"regex\":\"(?i:sleep\\\\(\\\\s*?\\\\d*?\\\\s*?\\\\)|benchmark\\\\(.*?\\\\,.*?\\\\))\",\"options\":{\"case_sensitive\":true,\"min_length\":7}},\"operator\":\"match_regex\"}],\"transformers\":[]},{\"id\":\"crs-942-190\",\"name\":\"Detects MSSQL code execution and information gathering attempts\",\"tags\":{\"type\":\"sqli\",\"crs_id\":\"942190\",\"category\":\"attack_attempt\"},\"conditions\":[{\"parameters\":{\"inputs\":[{\"address\":\"server.request.cookies\"},{\"address\":\"server.request.query\"},{\"address\":\"server.request.body\"},{\"address\":\"server.request.path_params\"}],\"regex\":\"(?:\\\\b(?:(?:c(?:onnection_id|urrent_user)|database)\\\\s*?\\\\([^\\\\)]*?|u(?:nion(?:[\\\\w(?:\\\\s]*?select| select @)|ser\\\\s*?\\\\([^\\\\)]*?)|s(?:chema\\\\s*?\\\\([^\\\\)]*?|elect.*?\\\\w?user\\\\()|into[\\\\s+]+(?:dump|out)file\\\\s*?[\\\\\\\"'`]|from\\\\W+information_schema\\\\W|exec(?:ute)?\\\\s+master\\\\.)|[\\\\\\\"'`](?:;?\\\\s*?(?:union\\\\b\\\\s*?(?:(?:distin|sele)ct|all)|having|select)\\\\b\\\\s*?[^\\\\s]|\\\\s*?!\\\\s*?[\\\\\\\"'`\\\\w])|\\\\s*?exec(?:ute)?.*?\\\\Wxp_cmdshell|\\\\Wiif\\\\s*?\\\\()\",\"options\":{\"min_length\":3}},\"operator\":\"match_regex\"}],\"transformers\":[]},{\"id\":\"crs-942-220\",\"name\":\"Looking for integer overflow attacks, these are taken from skipfish, except 2.2.2250738585072011e-308 is the \\\\\\\"magic number\\\\\\\" crash\",\"tags\":{\"type\":\"sqli\",\"crs_id\":\"942220\",\"category\":\"attack_attempt\"},\"conditions\":[{\"parameters\":{\"inputs\":[{\"address\":\"server.request.cookies\"},{\"address\":\"server.request.query\"},{\"address\":\"server.request.body\"},{\"address\":\"server.request.path_params\"}],\"regex\":\"^(?i:-0000023456|4294967295|4294967296|2147483648|2147483647|0000012345|-2147483648|-2147483649|0000023456|2.2250738585072007e-308|2.2250738585072011e-308|1e309)$\",\"options\":{\"case_sensitive\":true,\"min_length\":5}},\"operator\":\"match_regex\"}],\"transformers\":[]},{\"id\":\"crs-942-240\",\"name\":\"Detects MySQL charset switch and MSSQL DoS attempts\",\"tags\":{\"type\":\"sqli\",\"crs_id\":\"942240\",\"category\":\"attack_attempt\"},\"conditions\":[{\"parameters\":{\"inputs\":[{\"address\":\"server.request.cookies\"},{\"address\":\"server.request.query\"},{\"address\":\"server.request.body\"},{\"address\":\"server.request.path_params\"}],\"regex\":\"(?:[\\\\\\\"'`](?:;*?\\\\s*?waitfor\\\\s+(?:delay|time)\\\\s+[\\\\\\\"'`]|;.*?:\\\\s*?goto)|alter\\\\s*?\\\\w+.*?cha(?:racte)?r\\\\s+set\\\\s+\\\\w+)\",\"options\":{\"min_length\":7}},\"operator\":\"match_regex\"}],\"transformers\":[]},{\"id\":\"crs-942-250\",\"name\":\"Detects MATCH AGAINST, MERGE and EXECUTE IMMEDIATE injections\",\"tags\":{\"type\":\"sqli\",\"crs_id\":\"942250\",\"category\":\"attack_attempt\"},\"conditions\":[{\"parameters\":{\"inputs\":[{\"address\":\"server.request.cookies\"},{\"address\":\"server.request.query\"},{\"address\":\"server.request.body\"},{\"address\":\"server.request.path_params\"}],\"regex\":\"(?i:merge.*?using\\\\s*?\\\\(|execute\\\\s*?immediate\\\\s*?[\\\\\\\"'`]|match\\\\s*?[\\\\w(?:),+-]+\\\\s*?against\\\\s*?\\\\()\",\"options\":{\"case_sensitive\":true,\"min_length\":11}},\"operator\":\"match_regex\"}],\"transformers\":[]},{\"id\":\"crs-942-270\",\"name\":\"Looking for basic sql injection. Common attack string for mysql, oracle and others\",\"tags\":{\"type\":\"sqli\",\"crs_id\":\"942270\",\"category\":\"attack_attempt\"},\"conditions\":[{\"parameters\":{\"inputs\":[{\"address\":\"server.request.cookies\"},{\"address\":\"server.request.query\"},{\"address\":\"server.request.body\"},{\"address\":\"server.request.path_params\"}],\"regex\":\"union.*?select.*?from\",\"options\":{\"min_length\":15}},\"operator\":\"match_regex\"}],\"transformers\":[]},{\"id\":\"crs-942-280\",\"name\":\"Detects Postgres pg_sleep injection, waitfor delay attacks and database shutdown attempts\",\"tags\":{\"type\":\"sqli\",\"crs_id\":\"942280\",\"category\":\"attack_attempt\"},\"conditions\":[{\"parameters\":{\"inputs\":[{\"address\":\"server.request.cookies\"},{\"address\":\"server.request.query\"},{\"address\":\"server.request.body\"},{\"address\":\"server.request.path_params\"}],\"regex\":\"(?:;\\\\s*?shutdown\\\\s*?(?:[#;{]|\\\\/\\\\*|--)|waitfor\\\\s*?delay\\\\s?[\\\\\\\"'`]+\\\\s?\\\\d|select\\\\s*?pg_sleep)\",\"options\":{\"min_length\":10}},\"operator\":\"match_regex\"}],\"transformers\":[]},{\"id\":\"crs-942-290\",\"name\":\"Finds basic MongoDB SQL injection attempts\",\"tags\":{\"type\":\"nosqli\",\"crs_id\":\"942290\",\"category\":\"attack_attempt\"},\"conditions\":[{\"parameters\":{\"inputs\":[{\"address\":\"server.request.cookies\"},{\"address\":\"server.request.query\"},{\"address\":\"server.request.body\"},{\"address\":\"server.request.path_params\"}],\"regex\":\"(?i:(?:\\\\[\\\\$(?:ne|eq|lte?|gte?|n?in|mod|all|size|exists|type|slice|x?or|div|like|between|and)\\\\]))\",\"options\":{\"case_sensitive\":true,\"min_length\":5}},\"operator\":\"match_regex\"}],\"transformers\":[]},{\"id\":\"crs-942-360\",\"name\":\"Detects concatenated basic SQL injection and SQLLFI attempts\",\"tags\":{\"type\":\"sqli\",\"crs_id\":\"942360\",\"category\":\"attack_attempt\"},\"conditions\":[{\"parameters\":{\"inputs\":[{\"address\":\"server.request.cookies\"},{\"address\":\"server.request.query\"},{\"address\":\"server.request.body\"},{\"address\":\"server.request.path_params\"}],\"regex\":\"(?:^[\\\\W\\\\d]+\\\\s*?(?:alter\\\\s*(?:a(?:(?:pplication\\\\s*rol|ggregat)e|s(?:ymmetric\\\\s*ke|sembl)y|u(?:thorization|dit)|vailability\\\\s*group)|c(?:r(?:yptographic\\\\s*provider|edential)|o(?:l(?:latio|um)|nversio)n|ertificate|luster)|s(?:e(?:rv(?:ice|er)|curity|quence|ssion|arch)|y(?:mmetric\\\\s*key|nonym)|togroup|chema)|m(?:a(?:s(?:ter\\\\s*key|k)|terialized)|e(?:ssage\\\\s*type|thod)|odule)|l(?:o(?:g(?:file\\\\s*group|in)|ckdown)|a(?:ngua|r)ge|ibrary)|t(?:(?:abl(?:espac)?|yp)e|r(?:igger|usted)|hreshold|ext)|p(?:a(?:rtition|ckage)|ro(?:cedur|fil)e|ermission)|d(?:i(?:mension|skgroup)|atabase|efault|omain)|r(?:o(?:l(?:lback|e)|ute)|e(?:sourc|mot)e)|f(?:u(?:lltext|nction)|lashback|oreign)|e(?:xte(?:nsion|rnal)|(?:ndpoi|ve)nt)|in(?:dex(?:type)?|memory|stance)|b(?:roker\\\\s*priority|ufferpool)|x(?:ml\\\\s*schema|srobject)|w(?:ork(?:load)?|rapper)|hi(?:erarchy|stogram)|o(?:perator|utline)|(?:nicknam|queu)e|us(?:age|er)|group|java|view)\\\\b|(?:(?:(?:trunc|cre)at|renam)e|d(?:e(?:lete|sc)|rop)|(?:inser|selec)t|load)\\\\s+\\\\w+|u(?:nion\\\\s*(?:(?:distin|sele)ct|all)\\\\b|pdate\\\\s+\\\\w+))|\\\\b(?:(?:(?:(?:trunc|cre|upd)at|renam)e|(?:inser|selec)t|de(?:lete|sc)|alter|load)\\\\s+(?:group_concat|load_file|char)\\\\b\\\\s*\\\\(?|end\\\\s*?\\\\);)|[\\\\\\\"'`\\\\w]\\\\s+as\\\\b\\\\s*[\\\\\\\"'`\\\\w]+\\\\s*\\\\bfrom|[\\\\s(?:]load_file\\\\s*?\\\\(|[\\\\\\\"'`]\\\\s+regexp\\\\W)\",\"options\":{\"min_length\":5}},\"operator\":\"match_regex\"}],\"transformers\":[]},{\"id\":\"crs-942-500\",\"name\":\"MySQL in-line comment detected\",\"tags\":{\"type\":\"sqli\",\"crs_id\":\"942500\",\"category\":\"attack_attempt\"},\"conditions\":[{\"parameters\":{\"inputs\":[{\"address\":\"server.request.cookies\"},{\"address\":\"server.request.query\"},{\"address\":\"server.request.body\"},{\"address\":\"server.request.path_params\"}],\"regex\":\"(?i:/\\\\*[!+](?:[\\\\w\\\\s=_\\\\-(?:)]+)?\\\\*/)\",\"options\":{\"case_sensitive\":true,\"min_length\":5}},\"operator\":\"match_regex\"}],\"transformers\":[]},{\"id\":\"crs-943-100\",\"name\":\"Possible Session Fixation Attack: Setting Cookie Values in HTML\",\"tags\":{\"type\":\"http_protocol_violation\",\"crs_id\":\"943100\",\"category\":\"attack_attempt\"},\"conditions\":[{\"parameters\":{\"inputs\":[{\"address\":\"server.request.cookies\"},{\"address\":\"server.request.query\"},{\"address\":\"server.request.body\"},{\"address\":\"server.request.path_params\"}],\"regex\":\"(?i:\\\\.cookie\\\\b.*?;\\\\W*?(?:expires|domain)\\\\W*?=|\\\\bhttp-equiv\\\\W+set-cookie\\\\b)\",\"options\":{\"case_sensitive\":true,\"min_length\":15}},\"operator\":\"match_regex\"}],\"transformers\":[]},{\"id\":\"crs-944-100\",\"name\":\"Remote Command Execution: Suspicious Java class detected\",\"tags\":{\"type\":\"java_code_injection\",\"crs_id\":\"944100\",\"category\":\"attack_attempt\"},\"conditions\":[{\"parameters\":{\"inputs\":[{\"address\":\"server.request.query\"},{\"address\":\"server.request.body\"},{\"address\":\"server.request.path_params\"},{\"address\":\"server.request.cookies\"},{\"address\":\"server.request.headers.no_cookies\"}],\"regex\":\"java\\\\.lang\\\\.(?:runtime|processbuilder)\",\"options\":{\"case_sensitive\":true,\"min_length\":17}},\"operator\":\"match_regex\"}],\"transformers\":[\"lowercase\"]},{\"id\":\"crs-944-110\",\"name\":\"Remote Command Execution: Java process spawn (CVE-2017-9805)\",\"tags\":{\"type\":\"java_code_injection\",\"crs_id\":\"944110\",\"category\":\"attack_attempt\"},\"conditions\":[{\"parameters\":{\"inputs\":[{\"address\":\"server.request.query\"},{\"address\":\"server.request.body\"},{\"address\":\"server.request.path_params\"},{\"address\":\"server.request.cookies\"},{\"address\":\"server.request.headers.no_cookies\"}],\"regex\":\"(?:runtime|processbuilder)\",\"options\":{\"case_sensitive\":true,\"min_length\":7}},\"operator\":\"match_regex\"},{\"parameters\":{\"inputs\":[{\"address\":\"server.request.query\"},{\"address\":\"server.request.body\"},{\"address\":\"server.request.path_params\"},{\"address\":\"server.request.cookies\"},{\"address\":\"server.request.headers.no_cookies\"}],\"regex\":\"(?:unmarshaller|base64data|java\\\\.)\",\"options\":{\"case_sensitive\":true,\"min_length\":5}},\"operator\":\"match_regex\"}],\"transformers\":[\"lowercase\"]},{\"id\":\"crs-944-130\",\"name\":\"Suspicious Java class detected\",\"tags\":{\"type\":\"java_code_injection\",\"crs_id\":\"944130\",\"category\":\"attack_attempt\"},\"conditions\":[{\"parameters\":{\"inputs\":[{\"address\":\"server.request.query\"},{\"address\":\"server.request.body\"},{\"address\":\"server.request.path_params\"},{\"address\":\"server.request.cookies\"},{\"address\":\"server.request.headers.no_cookies\"}],\"list\":[\"com.opensymphony.xwork2\",\"com.sun.org.apache\",\"java.io.bufferedinputstream\",\"java.io.bufferedreader\",\"java.io.bytearrayinputstream\",\"java.io.bytearrayoutputstream\",\"java.io.chararrayreader\",\"java.io.datainputstream\",\"java.io.file\",\"java.io.fileoutputstream\",\"java.io.filepermission\",\"java.io.filewriter\",\"java.io.filterinputstream\",\"java.io.filteroutputstream\",\"java.io.filterreader\",\"java.io.inputstream\",\"java.io.inputstreamreader\",\"java.io.linenumberreader\",\"java.io.objectoutputstream\",\"java.io.outputstream\",\"java.io.pipedoutputstream\",\"java.io.pipedreader\",\"java.io.printstream\",\"java.io.pushbackinputstream\",\"java.io.reader\",\"java.io.stringreader\",\"java.lang.class\",\"java.lang.integer\",\"java.lang.number\",\"java.lang.object\",\"java.lang.process\",\"java.lang.processbuilder\",\"java.lang.reflect\",\"java.lang.runtime\",\"java.lang.string\",\"java.lang.stringbuilder\",\"java.lang.system\",\"javax.script.scriptenginemanager\",\"org.apache.commons\",\"org.apache.struts\",\"org.apache.struts2\",\"org.omg.corba\",\"java.beans.xmldecode\"]},\"operator\":\"phrase_match\"}],\"transformers\":[\"lowercase\"]},{\"id\":\"sqr-000-001\",\"name\":\"SSRF: Try to access the credential manager of the main cloud services\",\"tags\":{\"type\":\"ssrf\",\"category\":\"attack_attempt\"},\"conditions\":[{\"parameters\":{\"inputs\":[{\"address\":\"server.request.query\"},{\"address\":\"server.request.body\"},{\"address\":\"server.request.path_params\"}],\"regex\":\"(?i)^\\\\W*((http|ftp)s?://)?\\\\W*((::f{4}:)?(169|(0x)?0*a9|0+251)\\\\.?(254|(0x)?0*fe|0+376)[0-9a-fx\\\\.:]+|metadata\\\\.google\\\\.internal|metadata\\\\.goog)\\\\W*/\",\"options\":{\"min_length\":4}},\"operator\":\"match_regex\"}],\"transformers\":[\"removeNulls\"]},{\"id\":\"sqr-000-002\",\"name\":\"Server-side Javascript injection: Try to detect obvious JS injection\",\"tags\":{\"type\":\"js_code_injection\",\"category\":\"attack_attempt\"},\"conditions\":[{\"parameters\":{\"inputs\":[{\"address\":\"server.request.query\"},{\"address\":\"server.request.body\"},{\"address\":\"server.request.path_params\"}],\"regex\":\"require\\\\(['\\\"][\\\\w\\\\.]+['\\\"]\\\\)|process\\\\.\\\\w+\\\\([\\\\w\\\\.]*\\\\)|\\\\.toString\\\\(\\\\)\",\"options\":{\"min_length\":4}},\"operator\":\"match_regex\"}],\"transformers\":[\"removeNulls\"]},{\"id\":\"sqr-000-007\",\"name\":\"NoSQL: Detect common exploitation strategy\",\"tags\":{\"type\":\"nosqli\",\"category\":\"attack_attempt\"},\"conditions\":[{\"parameters\":{\"inputs\":[{\"address\":\"server.request.headers.no_cookies\"}],\"regex\":\"\\\\$(eq|ne|lte?|gte?|n?in)\\\\b\"},\"operator\":\"match_regex\"}],\"transformers\":[]},{\"id\":\"sqr-000-008\",\"name\":\"Windows: Detect attempts to exfiltrate .ini files\",\"tags\":{\"type\":\"command_injection\",\"category\":\"attack_attempt\"},\"conditions\":[{\"parameters\":{\"inputs\":[{\"address\":\"server.request.query\"},{\"address\":\"server.request.body\"},{\"address\":\"server.request.path_params\"},{\"address\":\"server.request.headers.no_cookies\"}],\"regex\":\"(?i)[&|]\\\\s*type\\\\s+%\\\\w+%\\\\\\\\+\\\\w+\\\\.ini\\\\s*[&|]\"},\"operator\":\"match_regex\"}],\"transformers\":[]},{\"id\":\"sqr-000-009\",\"name\":\"Linux: Detect attempts to exfiltrate passwd files\",\"tags\":{\"type\":\"command_injection\",\"category\":\"attack_attempt\"},\"conditions\":[{\"parameters\":{\"inputs\":[{\"address\":\"server.request.query\"},{\"address\":\"server.request.body\"},{\"address\":\"server.request.path_params\"},{\"address\":\"server.request.headers.no_cookies\"}],\"regex\":\"(?i)[&|]\\\\s*cat\\\\s+\\\\/etc\\\\/[\\\\w\\\\.\\\\/]*passwd\\\\s*[&|]\"},\"operator\":\"match_regex\"}],\"transformers\":[]},{\"id\":\"sqr-000-010\",\"name\":\"Windows: Detect attempts to timeout a shell\",\"tags\":{\"type\":\"command_injection\",\"category\":\"attack_attempt\"},\"conditions\":[{\"parameters\":{\"inputs\":[{\"address\":\"server.request.query\"},{\"address\":\"server.request.body\"},{\"address\":\"server.request.path_params\"},{\"address\":\"server.request.headers.no_cookies\"}],\"regex\":\"(?i)[&|]\\\\s*timeout\\\\s+/t\\\\s+\\\\d+\\\\s*[&|]\"},\"operator\":\"match_regex\"}],\"transformers\":[]},{\"id\":\"sqr-000-011\",\"name\":\"SSRF: Try to access internal OMI service (CVE-2021-38647)\",\"tags\":{\"type\":\"ssrf\",\"category\":\"attack_attempt\"},\"conditions\":[{\"operator\":\"match_regex\",\"parameters\":{\"inputs\":[{\"address\":\"server.request.query\"},{\"address\":\"server.request.body\"},{\"address\":\"server.request.path_params\"}],\"regex\":\"http(s?):\\\\/\\\\/([A-Za-z0-9\\\\.\\\\-\\\\_]+|\\\\[[A-Fa-f0-9\\\\:]+\\\\]|):5986\\\\/wsman\",\"options\":{\"min_length\":4}}}],\"transformers\":[]},{\"id\":\"sqr-000-012\",\"name\":\"SSRF: Detect SSRF attempt on internal service\",\"tags\":{\"type\":\"ssrf\",\"category\":\"attack_attempt\"},\"conditions\":[{\"parameters\":{\"inputs\":[{\"address\":\"server.request.query\"},{\"address\":\"server.request.body\"},{\"address\":\"server.request.path_params\"}],\"regex\":\"^(jar:)?(http|https):\\\\/\\\\/([0-9oq]{1,5}\\\\.[0-9]{1,3}\\\\.[0-9]{1,3}\\\\.[0-9]{1,3}|[0-9]{1,10}|localhost)(:[0-9]{1,5})?(\\\\/.*|)$\"},\"operator\":\"match_regex\"}],\"transformers\":[\"lowercase\"]},{\"id\":\"sqr-000-013\",\"name\":\"SSRF: Detect SSRF attempts using IPv6 or octal/hexdecimal obfuscation\",\"tags\":{\"type\":\"ssrf\",\"category\":\"attack_attempt\"},\"conditions\":[{\"parameters\":{\"inputs\":[{\"address\":\"server.request.query\"},{\"address\":\"server.request.body\"},{\"address\":\"server.request.path_params\"}],\"regex\":\"^(jar:)?(http|https):\\\\/\\\\/((\\\\[)?[:0-9a-f\\\\.x]{2,}(\\\\])?)(:[0-9]{1,5})?(\\\\/.*)?$\"},\"operator\":\"match_regex\"}],\"transformers\":[\"lowercase\"]},{\"id\":\"sqr-000-014\",\"name\":\"SSRF: Detect SSRF domain redirection bypass\",\"tags\":{\"type\":\"ssrf\",\"category\":\"attack_attempt\"},\"conditions\":[{\"parameters\":{\"inputs\":[{\"address\":\"server.request.query\"},{\"address\":\"server.request.body\"},{\"address\":\"server.request.path_params\"},{\"address\":\"server.request.headers.no_cookies\"}],\"regex\":\"^(http|https):\\\\/\\\\/(.*burpcollaborator\\\\.net|localtest\\\\.me|mail\\\\.ebc\\\\.apple\\\\.com|bugbounty\\\\.dod\\\\.network|.*\\\\.[nx]ip\\\\.io)\"},\"operator\":\"match_regex\"}],\"transformers\":[\"lowercase\"]},{\"id\":\"sqr-000-015\",\"name\":\"SSRF: Detect SSRF attempt using non HTTP protocol\",\"tags\":{\"type\":\"ssrf\",\"category\":\"attack_attempt\"},\"conditions\":[{\"parameters\":{\"inputs\":[{\"address\":\"server.request.query\"},{\"address\":\"server.request.body\"},{\"address\":\"server.request.path_params\"},{\"address\":\"server.request.headers.no_cookies\"}],\"regex\":\"^(jar:)?((file|netdoc):\\\\/\\\\/[\\\\\\\\\\\\/]+|(dict|gopher|ldap|sftp|tftp):\\\\/\\\\/.*:[0-9]{1,5})\"},\"operator\":\"match_regex\"}],\"transformers\":[\"lowercase\"]},{\"id\":\"ua0-600-0xx\",\"name\":\"Joomla exploitation tool\",\"tags\":{\"type\":\"security_scanner\",\"category\":\"attack_attempt\"},\"conditions\":[{\"parameters\":{\"inputs\":[{\"address\":\"server.request.headers.no_cookies\",\"key_path\":[\"user-agent\"]}],\"regex\":\"JDatabaseDriverMysqli\"},\"operator\":\"match_regex\"}],\"transformers\":[]},{\"id\":\"ua0-600-10x\",\"name\":\"Nessus\",\"tags\":{\"type\":\"security_scanner\",\"category\":\"attack_attempt\"},\"conditions\":[{\"parameters\":{\"inputs\":[{\"address\":\"server.request.headers.no_cookies\",\"key_path\":[\"user-agent\"]}],\"regex\":\"(?i)^Nessus(/|([ :]+SOAP))\"},\"operator\":\"match_regex\"}],\"transformers\":[]},{\"id\":\"ua0-600-12x\",\"name\":\"Arachni\",\"tags\":{\"type\":\"security_scanner\",\"category\":\"attack_attempt\"},\"conditions\":[{\"parameters\":{\"inputs\":[{\"address\":\"server.request.headers.no_cookies\",\"key_path\":[\"user-agent\"]}],\"regex\":\"^Arachni\\\\/v\"},\"operator\":\"match_regex\"}],\"transformers\":[]},{\"id\":\"ua0-600-13x\",\"name\":\"Jorgee\",\"tags\":{\"type\":\"security_scanner\",\"category\":\"attack_attempt\"},\"conditions\":[{\"parameters\":{\"inputs\":[{\"address\":\"server.request.headers.no_cookies\",\"key_path\":[\"user-agent\"]}],\"regex\":\"(?i)\\\\bJorgee\\\\b\"},\"operator\":\"match_regex\"}],\"transformers\":[]},{\"id\":\"ua0-600-14x\",\"name\":\"Probely\",\"tags\":{\"type\":\"security_scanner\",\"category\":\"attack_attempt\"},\"conditions\":[{\"parameters\":{\"inputs\":[{\"address\":\"server.request.headers.no_cookies\",\"key_path\":[\"user-agent\"]}],\"regex\":\"(?i)\\\\bProbely\\\\b\"},\"operator\":\"match_regex\"}],\"transformers\":[]},{\"id\":\"ua0-600-15x\",\"name\":\"Metis\",\"tags\":{\"type\":\"security_scanner\",\"category\":\"attack_attempt\"},\"conditions\":[{\"parameters\":{\"inputs\":[{\"address\":\"server.request.headers.no_cookies\",\"key_path\":[\"user-agent\"]}],\"regex\":\"(?i)\\\\bmetis\\\\b\"},\"operator\":\"match_regex\"}],\"transformers\":[]},{\"id\":\"ua0-600-16x\",\"name\":\"SQL power injector\",\"tags\":{\"type\":\"security_scanner\",\"category\":\"attack_attempt\"},\"conditions\":[{\"parameters\":{\"inputs\":[{\"address\":\"server.request.headers.no_cookies\",\"key_path\":[\"user-agent\"]}],\"regex\":\"sql power injector\"},\"operator\":\"match_regex\"}],\"transformers\":[]},{\"id\":\"ua0-600-18x\",\"name\":\"N-Stealth\",\"tags\":{\"type\":\"security_scanner\",\"category\":\"attack_attempt\"},\"conditions\":[{\"parameters\":{\"inputs\":[{\"address\":\"server.request.headers.no_cookies\",\"key_path\":[\"user-agent\"]}],\"regex\":\"(?i)\\\\bn-stealth\\\\b\"},\"operator\":\"match_regex\"}],\"transformers\":[]},{\"id\":\"ua0-600-19x\",\"name\":\"Brutus\",\"tags\":{\"type\":\"security_scanner\",\"category\":\"attack_attempt\"},\"conditions\":[{\"parameters\":{\"inputs\":[{\"address\":\"server.request.headers.no_cookies\",\"key_path\":[\"user-agent\"]}],\"regex\":\"(?i)\\\\bbrutus\\\\b\"},\"operator\":\"match_regex\"}],\"transformers\":[]},{\"id\":\"ua0-600-1xx\",\"name\":\"Shellshock exploitation tool\",\"tags\":{\"type\":\"security_scanner\",\"category\":\"attack_attempt\"},\"conditions\":[{\"parameters\":{\"inputs\":[{\"address\":\"server.request.headers.no_cookies\",\"key_path\":[\"user-agent\"]}],\"regex\":\"\\\\(\\\\) \\\\{ :; *\\\\}\"},\"operator\":\"match_regex\"}],\"transformers\":[]},{\"id\":\"ua0-600-20x\",\"name\":\"Netsparker\",\"tags\":{\"type\":\"security_scanner\",\"category\":\"attack_attempt\"},\"conditions\":[{\"parameters\":{\"inputs\":[{\"address\":\"server.request.headers.no_cookies\",\"key_path\":[\"user-agent\"]}],\"regex\":\"(?i)(