Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

internal/appsec: add new AppSec beta functionality and support for contrib/net/http #1007

Merged
merged 113 commits into from
Oct 20, 2021
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
113 commits
Select commit Hold shift + click to select a range
9867d8f
appsec/dyngo: add an instrumentation gateway draft
Julio-Guerra Jul 26, 2021
08e0cd7
appsec: waf prototype as an operation event listener
Julio-Guerra Jul 27, 2021
29f5a0a
appsec/waf: simply the WAF event listener
Julio-Guerra Jul 27, 2021
cf61756
contrib/net/http: integrate an http handler operation
Julio-Guerra Jul 27, 2021
d13939a
appsec/dyngo: make the event register map key type safe
Julio-Guerra Sep 1, 2021
db0d47e
appsec: milestone 0
Julio-Guerra Sep 15, 2021
16573e6
httputil: add HTTP handler operation definition
Julio-Guerra Sep 15, 2021
51d8095
ddtrace/tracer: AppSec integration
Julio-Guerra Sep 15, 2021
2259fa6
ddtrace/tracer: fix unsafe condition in appsec stopping
Julio-Guerra Sep 15, 2021
cbbd178
Move appsec into dd-trace-go module's internal package
Julio-Guerra Sep 17, 2021
0a132f0
tracer: pass app version and env to the appsec config
Julio-Guerra Sep 17, 2021
33eb8c5
tracer: fix appsec stopping
Julio-Guerra Sep 20, 2021
5258dd5
appsec: remove the global context
Julio-Guerra Sep 20, 2021
06480b3
tracer: start appsec before the tracer is made global
Julio-Guerra Sep 20, 2021
0d94e93
appsec,dyngo: add copyright headers
Julio-Guerra Sep 20, 2021
6824cad
go.mod: add new appsec dependencies
Julio-Guerra Sep 20, 2021
ceb207c
Merge remote-tracking branch 'origin/appsec' into julio-guerra-appsec…
Julio-Guerra Sep 20, 2021
35d1e35
contrib/net/http: fix example imports
Julio-Guerra Sep 20, 2021
335156f
internal/dyngo: simplify the unregistration API
Julio-Guerra Sep 20, 2021
48d4454
dyngo: add run time type-checking of operations and listeners
Julio-Guerra Sep 22, 2021
518795c
dyngo: code simplification
Julio-Guerra Sep 22, 2021
6594153
dyngo: port event listeners to static types
Julio-Guerra Sep 27, 2021
caa3036
appsec,dyngo: fix golint errors
Julio-Guerra Sep 28, 2021
910c41d
appsec: fix goimports errors
Julio-Guerra Sep 28, 2021
c58f1c8
dyngo: fix code duplication mistake
Julio-Guerra Sep 28, 2021
e39d05a
go.mod: update go-libsqreen to v0.7.3 fixing a macos build issue
Julio-Guerra Sep 28, 2021
3e4b189
ddtrace,appsec: simplify the appsec integration into ddtrace
Julio-Guerra Sep 28, 2021
c7fd172
appsec: remove no longer used logger interface
Julio-Guerra Sep 28, 2021
8a6dba0
dyngo: merge eventManager into Operation
Julio-Guerra Sep 28, 2021
4757b74
dyngo: fix missing comments
Julio-Guerra Sep 28, 2021
f4302b1
dyngo: add missing import
Julio-Guerra Sep 28, 2021
0d81f3e
dyngo: optimize listeners removal
Julio-Guerra Sep 28, 2021
27a664b
dyngo: add a concurrency test
Julio-Guerra Sep 29, 2021
6777607
contrib/httputil: listen for security events sooner
Julio-Guerra Sep 29, 2021
1d34682
appsec: simplify the integration into the tracer thanks to Start() an…
Julio-Guerra Sep 29, 2021
1f5b0e4
appsec/waf: simplify to the least amount of necessary things
Julio-Guerra Oct 1, 2021
c14d17e
appsec: always use the tracer hostname and service name configuration
Julio-Guerra Oct 1, 2021
75a067c
dyngo: recover from any listener panics
Julio-Guerra Oct 4, 2021
18d671a
dyngo: remove the data event
Julio-Guerra Oct 6, 2021
1f22e7e
dyngo: simplified and (mostly) statically typed API
Julio-Guerra Oct 6, 2021
de6ee79
appsec: simplify the waf event listener registration
Julio-Guerra Oct 6, 2021
bfd4c32
appsec/waf: add libddwaf v1.0.12 bindings
Julio-Guerra Oct 8, 2021
fce1f33
appsec/waf/bindings: fix macos -l ldflag
Julio-Guerra Oct 8, 2021
6290a22
appsec/waf/bindings: fix macos -L ldflag
Julio-Guerra Oct 8, 2021
faa976c
appsec/waf/bindings: fix timeout type on macos
Julio-Guerra Oct 8, 2021
2e5f0a9
appsec/intake/client: fix flaky test
Julio-Guerra Oct 8, 2021
f687f92
appsec/waf/bindings: fix macos type errors
Julio-Guerra Oct 8, 2021
ca16d2b
appsec/intake/client: fix test assertion
Julio-Guerra Oct 8, 2021
60c5bc7
appsec/waf/bindings: fix macos type errors
Julio-Guerra Oct 8, 2021
33c8221
appsec/intake/client: fix test assertion
Julio-Guerra Oct 8, 2021
93c3916
appsec/intake/client: remove flaky condition
Julio-Guerra Oct 8, 2021
856f1de
appsec/waf: encode byte slices as strings
Julio-Guerra Oct 10, 2021
717b7dd
appsec: add support for dd_appsec_rules
Julio-Guerra Oct 10, 2021
9ce7dc5
appsec: milestone 1 compliance
Julio-Guerra Oct 10, 2021
402a30f
appsec/intake/api: removed unnecessary blocked field from the sec event
Julio-Guerra Oct 11, 2021
11c77d7
appsec/intake/api: make private things private
Julio-Guerra Oct 11, 2021
1f4b374
dyngo: synplify the package organization and port the tests to the ne…
Julio-Guerra Oct 11, 2021
2b47dfc
dyngo/httpinstr: fix server.request.cookies value
Julio-Guerra Oct 12, 2021
3e5c271
appsec/intake/api: enforce static typing of events for now
Julio-Guerra Oct 12, 2021
95394f6
ddtrace/tracer: fix appsec http client and agent url
Julio-Guerra Oct 12, 2021
23265e9
contrib/httputils: expose the monitored response status code
Julio-Guerra Oct 12, 2021
7625a5a
appsec: disable appsec by default at compilation time
Julio-Guerra Oct 12, 2021
93e07f2
appsec/waf: dynamically get the list of addresses to use at run time
Julio-Guerra Oct 13, 2021
d52e5e5
repo: update the 3rd party license file
Julio-Guerra Oct 13, 2021
b117934
appsec/waf: remove the build tag without_ddwaf in favor of the new ap…
Julio-Guerra Oct 13, 2021
1e834ae
appsec: move to ioutil.ReadFile to support older go versions
Julio-Guerra Oct 13, 2021
a1df977
appsec: add debug logs on sendbatch
Julio-Guerra Oct 13, 2021
0fa57fa
appsec/waf: update libddwaf to v1.0.13 to use the latest recommended …
Julio-Guerra Oct 13, 2021
b6636b7
appsec: better logs
Julio-Guerra Oct 13, 2021
1bf3ebf
appsec: fix tags configuration
Julio-Guerra Oct 13, 2021
cf04894
appsec/intake/api: fix url query parameters type
Julio-Guerra Oct 13, 2021
0f2a70e
appsec/waf: move the cgo bindings in the waf package
Julio-Guerra Oct 14, 2021
a31a25b
appsec: fix the service entry span tags
Julio-Guerra Oct 14, 2021
fbb8ed2
appsec/waf: fix disabled build tags
Julio-Guerra Oct 14, 2021
ca9c12c
appsec/waf: fix disabled build tags
Julio-Guerra Oct 14, 2021
9ffa1a5
appsec: make the intake api package a leaf package
Julio-Guerra Oct 14, 2021
ee74c4f
ci: try changing the version to 2.1
Julio-Guerra Oct 14, 2021
593a4dd
ci: try fixing job name
Julio-Guerra Oct 14, 2021
efb668c
ci: try adding parameters to test-contrib
Julio-Guerra Oct 14, 2021
d382449
appsec: fix broken tests
Julio-Guerra Oct 14, 2021
00d235c
dyngo: fix concurrency test
Julio-Guerra Oct 14, 2021
6a3c50f
appsec/waf: fix missing libunwind symbols on the linux build
Julio-Guerra Oct 15, 2021
7fceb97
appsec/tests: derease execution time
Julio-Guerra Oct 15, 2021
8869086
appsec/tests: try avoid the timeout
Julio-Guerra Oct 15, 2021
a677017
appsec: overall package simplification
Julio-Guerra Oct 15, 2021
8b11e2b
tracer/config: add an http client getter
Julio-Guerra Oct 15, 2021
74013a1
circleci: add appsec tests
Julio-Guerra Oct 15, 2021
ea26d3c
tracer: disable appsec on TestTracerCleanStop
Julio-Guerra Oct 15, 2021
5716ebe
appsec,dyngo: adapt test names to the style guide
Julio-Guerra Oct 15, 2021
eca35ec
appsec/waf: fix case where no attacks were performed
Julio-Guerra Oct 18, 2021
d8c281b
appsec/intake/api: add required blocked field
Julio-Guerra Oct 18, 2021
04badb2
appsec: fix number of sent events debug log
Julio-Guerra Oct 18, 2021
49938d7
appsec: refactor into a flat package
Julio-Guerra Oct 18, 2021
259db1a
dyngo: move package into appsec/
Julio-Guerra Oct 18, 2021
c42784f
appsec: fix golint missing comments
Julio-Guerra Oct 18, 2021
00c1f6c
appsec/rule: update to the latest version of recommended rules
Julio-Guerra Oct 18, 2021
0fff92b
appsec,contrib/httputils: fix gofmt errors
Julio-Guerra Oct 18, 2021
3de60f3
appsec/waf: add a readme telling where the libs come from
Julio-Guerra Oct 18, 2021
9f9094e
circleci: try better job names
Julio-Guerra Oct 19, 2021
b88b13c
circleci: try better job names
Julio-Guerra Oct 19, 2021
c5ea49e
appsec/httpinstr: remove os.Getenv lookup in WrapHandler
Julio-Guerra Oct 19, 2021
56a06f9
ddtrace: disable appsec for tests checking the logs
Julio-Guerra Oct 19, 2021
fd40cbe
circleci: adapt dd_appsec_enabled env var to new job param
Julio-Guerra Oct 19, 2021
95191af
circleci: adapt dd_appsec_enabled env var to new job param
Julio-Guerra Oct 19, 2021
15dcd90
circleci: adapt dd_appsec_enabled env var to new job param
Julio-Guerra Oct 19, 2021
8e9d199
circleci: adapt dd_appsec_enabled env var to new job param
Julio-Guerra Oct 19, 2021
6c61a52
ci: run dd-trace-go tests
Julio-Guerra Oct 19, 2021
420cd91
circleci: run test-contrib and go1_12-build with appsec
Julio-Guerra Oct 19, 2021
d3d0a16
circleci: run test-contrib and go1_12-build with appsec
Julio-Guerra Oct 19, 2021
66300c8
appsec: fix manual keep span tag on security event
Julio-Guerra Oct 19, 2021
5960329
Merge remote-tracking branch 'origin/v1' into julio-guerra-appsec/waf
Julio-Guerra Oct 19, 2021
7e37f52
circleci: fix whitespaces
Julio-Guerra Oct 19, 2021
67924a4
ddtrace/tracer: increase TestWorker timeout to 2 seconds
Julio-Guerra Oct 20, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions contrib/internal/httputil/trace.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,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/tracer"
httpinstr "gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/instrumentation/http"
appsectypes "gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/types"
"gopkg.in/DataDog/dd-trace-go.v1/internal/dyngo"
)

// TraceConfig defines the configuration for request tracing.
Expand Down Expand Up @@ -54,6 +57,34 @@ func TraceAndServe(h http.Handler, cfg *TraceConfig) {

cfg.ResponseWriter = wrapResponseWriter(cfg.ResponseWriter, span)

op := dyngo.StartOperation(
httpinstr.HandlerOperationArgs{
IsTLS: cfg.Request.TLS != nil,
Method: httpinstr.Method(cfg.Request.Method),
Host: httpinstr.Host(cfg.Request.Host),
RequestURI: httpinstr.RequestURI(cfg.Request.RequestURI),
RemoteAddr: httpinstr.RemoteAddr(cfg.Request.RemoteAddr),
Headers: httpinstr.Header(cfg.Request.Header),
UserAgent: httpinstr.UserAgent(cfg.Request.UserAgent()),
QueryValues: httpinstr.QueryValues(cfg.Request.URL.Query()),
},
)
op.OnData(func(_ *dyngo.Operation, e *appsectypes.SecurityEvent) {
Julio-Guerra marked this conversation as resolved.
Show resolved Hide resolved
// Keep this trace due to the security event
span.SetTag(ext.SamplingPriority, ext.ManualKeep)
Julio-Guerra marked this conversation as resolved.
Show resolved Hide resolved
// Add context to the event
spanCtx := span.Context()
// Add the APM context to the event
e.AddContext(appsectypes.SpanContext{
TraceID: spanCtx.TraceID(),
SpanID: spanCtx.SpanID(),
})
})
defer func() {
// TODO: get the status code out of the wrapped response write
op.Finish(httpinstr.HandlerOperationRes{})
}()

h.ServeHTTP(cfg.ResponseWriter, cfg.Request.WithContext(ctx))
}

Expand Down
3 changes: 1 addition & 2 deletions contrib/net/http/example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,8 @@
package http_test

import (
"net/http"

httptrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/net/http"
"net/http"
Julio-Guerra marked this conversation as resolved.
Show resolved Hide resolved
)

func Example() {
Expand Down
48 changes: 48 additions & 0 deletions ddtrace/tracer/tracer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -78,6 +80,7 @@ type tracer struct {
// rules for applying a sampling rate to spans that match the designated service
// or operation name.
rulesSampling *rulesSampler
appsec *appsec.Agent
knusbaum marked this conversation as resolved.
Show resolved Hide resolved
}

const (
Expand Down Expand Up @@ -113,6 +116,7 @@ func Start(opts ...StartOption) {
if t.config.HasFeature("discovery") {
t.loadAgentFeatures()
}
startAppSec(t)
internal.SetGlobalTracer(t)
if t.config.logStartup {
logStartup(t)
Expand All @@ -121,6 +125,9 @@ func Start(opts ...StartOption) {

// Stop stops the started tracer. Subsequent calls are valid but become no-op.
func Stop() {
if v, ok := internal.GetGlobalTracer().(*tracer); ok && v.appsec != nil {
v.appsec.Stop(true)
}
Julio-Guerra marked this conversation as resolved.
Show resolved Hide resolved
internal.SetGlobalTracer(&internal.NoopTracer{})
log.Flush()
}
Expand Down Expand Up @@ -498,3 +505,44 @@ func (t *tracer) sample(span *span) {
}
t.prioritySampling.apply(span)
}

// Start AppSec when DD_APPSEC_ENABLED is true.
func startAppSec(t *tracer) {
if appsecEnabled := os.Getenv("DD_APPSEC_ENABLED"); appsecEnabled == "" {
return
} else if enabled, err := strconv.ParseBool(appsecEnabled); err != nil {
log.Error("appsec: could not parse DD_APPSEC_ENABLED value `%s` as a boolean value", appsecEnabled)
return
} else if !enabled {
return
}

appsecAgent, err := appsec.NewAgent(
t.config.httpClient,
appsecLogger{},
&appsec.Config{
Version: version.Tag,
AgentURL: fmt.Sprintf("http://%s/", t.config.agentAddr),
Julio-Guerra marked this conversation as resolved.
Show resolved Hide resolved
Service: appsec.ServiceConfig{
Name: t.config.serviceName,
Version: t.config.version,
Environment: t.config.env,
},
},
)
if err != nil {
log.Error("could not start appsec: %v", err)
}
appsecAgent.Start()
t.appsec = appsecAgent
}
Julio-Guerra marked this conversation as resolved.
Show resolved Hide resolved

type appsecLogger struct{}

func (appsecLogger) Warn(fmt string, v ...interface{}) {
log.Warn(fmt, v...)
}

func (appsecLogger) Error(fmt string, v ...interface{}) {
log.Error(fmt, v...)
}
Julio-Guerra marked this conversation as resolved.
Show resolved Hide resolved
221 changes: 221 additions & 0 deletions internal/appsec/appsec.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
// 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"
"fmt"
"net/http"
"os"
"path/filepath"
"runtime"
"sync"
"time"

"gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/internal/intake"
"gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/internal/intake/api"
httpprotection "gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/internal/protection/http"
appsectypes "gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/types"
"gopkg.in/DataDog/dd-trace-go.v1/internal/dyngo"
)

type (
Config struct {
Julio-Guerra marked this conversation as resolved.
Show resolved Hide resolved
AgentURL string
Service ServiceConfig
Tags []string
Hostname string
MaxBatchLen int
MaxBatchStaleTime time.Duration
Version string
}

ServiceConfig struct {
Name, Version, Environment string
}
)

// Default batching configuration values.
const (
defaultMaxBatchLen = 1024
defaultMaxBatchStaleTime = time.Second
)

// Default timeout of intake requests.
const defaultIntakeTimeout = 10 * time.Second

type Agent struct {
client *intake.Client
eventChan chan *appsectypes.SecurityEvent
wg sync.WaitGroup
cfg *Config
instrumentationIDs []dyngo.EventListenerID
}

type Logger interface {
Warn(string, ...interface{})
Error(string, ...interface{})
}

func NewAgent(client *http.Client, logger Logger, cfg *Config) (*Agent, error) {
intakeClient, err := intake.NewClient(client, cfg.AgentURL)
if err != nil {
return nil, err
}

if cfg.Hostname == "" {
hostname, err := os.Hostname()
if err != nil {
logger.Warn("unable to look up hostname: %v", err)
} else {
cfg.Hostname = hostname
}
}

if cfg.Service.Name == "" {
name, err := os.Executable()
Julio-Guerra marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
logger.Warn("unable to look up the executable name: %v", err)
} else {
cfg.Service.Name = filepath.Base(name)
}
}

if cfg.MaxBatchLen <= 0 {
cfg.MaxBatchLen = defaultMaxBatchLen
}

if cfg.MaxBatchStaleTime <= 0 {
cfg.MaxBatchStaleTime = defaultMaxBatchStaleTime
}

return &Agent{
eventChan: make(chan *appsectypes.SecurityEvent, 1000),
client: intakeClient,
cfg: cfg,
}, nil
}

func (a *Agent) Start() {
a.run()
}

func (a *Agent) Stop(gracefully bool) {
Julio-Guerra marked this conversation as resolved.
Show resolved Hide resolved
dyngo.Unregister(a.instrumentationIDs)
// Stop the batching goroutine
close(a.eventChan)
// Possibly wait for the goroutine to gracefully stop
if !gracefully {
return
}
a.wg.Wait()
}

func (a *Agent) run() {
a.instrumentationIDs = append(a.instrumentationIDs, httpprotection.Register()...)
a.instrumentationIDs = append(a.instrumentationIDs, a.listenSecurityEvents()...)

a.wg.Add(1)
go func() {
defer a.wg.Done()

globalEventCtx := []appsectypes.SecurityEventContext{
appsectypes.ServiceContext{
Name: a.cfg.Service.Name,
Version: a.cfg.Service.Version,
Environment: a.cfg.Service.Environment,
},
appsectypes.TagContext(a.cfg.Tags),
appsectypes.TracerContext{
Runtime: "go",
RuntimeVersion: runtime.Version(),
Version: a.cfg.Version,
},
appsectypes.HostContext{
Hostname: a.cfg.Hostname,
OS: fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH),
},
}

eventBatchingLoop(a.client, a.eventChan, globalEventCtx, a.cfg)
}()
}

type IntakeClient interface {
SendBatch(context.Context, api.EventBatch) error
}

func eventBatchingLoop(client IntakeClient, eventChan <-chan *appsectypes.SecurityEvent, globalEventCtx []appsectypes.SecurityEventContext, cfg *Config) {
// The batch of events
batch := make([]*appsectypes.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()
client.SendBatch(ctx, api.FromSecurityEvents(batch, globalEventCtx))
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 *Agent) listenSecurityEvents() []dyngo.EventListenerID {
return dyngo.Register(dyngo.InstrumentationDescriptor{
Title: "Attack Queue",
Instrumentation: dyngo.OperationInstrumentation{
EventListener: dyngo.OnDataEventListener(func(_ *dyngo.Operation, event *appsectypes.SecurityEvent) {
select {
case a.eventChan <- event:
default:
// TODO: add metrics on the nb of dropped events
}
}),
},
})

}
Loading