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)(