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 9 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
67 changes: 67 additions & 0 deletions appsec/_test/appsec_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package _test_test
gbbr marked this conversation as resolved.
Show resolved Hide resolved

import (
"io/ioutil"
"net/http"
"net/http/httptest"
"os"
"strings"
"testing"

httptrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/net/http"
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"

"github.com/stretchr/testify/require"
)

// TestWAF is a simple validation test of the WAF protecting a net/http server. It only mockups the agent and tests that
// the WAF is properly detecting an XSS attempt and that the corresponding security event is being sent to the agent.
func TestWAF(t *testing.T) {
// Start the HTTP server acting as the agent
// Its handler counts the number of AppSec API requests and saves the latest event batch sent (only one expected).
var (
nbAppSecAPIRequests int
batch []byte
)
agent := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
if r.RequestURI == "/appsec/proxy/api/v2/appsecevts" {
nbAppSecAPIRequests++
var err error
batch, err = ioutil.ReadAll(r.Body)
require.NoError(t, err)
}
}))

// Start the tracer along with the fake agent HTTP server
os.Setenv("DD_APPSEC_ENABLED", "true")
tracer.Start(tracer.WithAgentAddr(strings.TrimPrefix(agent.URL, "http://")))

// Start and trace an HTTP server
mux := httptrace.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello World!\n"))
})
srv := httptest.NewServer(mux)
defer srv.Close()

// Send an XSS attack
req, err := http.NewRequest("POST", srv.URL+"/?attack=<script>alert()</script>", nil)
if err != nil {
panic(err)
}
res, err := srv.Client().Do(req)
require.NoError(t, err)

// Check that the handler was properly called
b, err := ioutil.ReadAll(res.Body)
require.NoError(t, err)
require.Equal(t, "Hello World!\n", string(b))

// Stop the tracer so that the AppSec events gets sent
tracer.Stop()

// Check that an XSS attack event was reported.
require.Equal(t, 1, nbAppSecAPIRequests)
require.True(t, strings.Contains(string(batch), "xss-blocking"))
}
13 changes: 13 additions & 0 deletions appsec/_test/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
module appsectest
Julio-Guerra marked this conversation as resolved.
Show resolved Hide resolved

go 1.16

replace (
github.com/DataDog/dd-trace-go/appsec => ../
gopkg.in/DataDog/dd-trace-go.v1 => ../../
)

require (
github.com/stretchr/testify v1.7.0
gopkg.in/DataDog/dd-trace-go.v1 v1.33.0
)
1,330 changes: 1,330 additions & 0 deletions appsec/_test/go.sum

Large diffs are not rendered by default.

196 changes: 196 additions & 0 deletions appsec/appsec.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
package appsec

import (
"context"
"fmt"
httpprotection "github.com/DataDog/dd-trace-go/appsec/internal/protection/http"
"net/http"
"os"
"path/filepath"
"runtime"
"sync"
"time"

"github.com/DataDog/dd-trace-go/appsec/dyngo"
"github.com/DataDog/dd-trace-go/appsec/internal/intake"
"github.com/DataDog/dd-trace-go/appsec/internal/intake/api"
appsectypes "github.com/DataDog/dd-trace-go/appsec/types"
)

type (
Config struct {
AgentURL string
Service ServiceConfig
Tags []string
Hostname string
MaxBatchLen int
MaxBatchStaleTime time.Duration
}

ServiceConfig struct {
Name, Version, Environment string
}
)

const (
defaultMaxBatchLen = 1024
defaultMaxBatchStaleTime = 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()
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(ctx context.Context) {
a.run(ctx)
}

func (a *Agent) Stop(gracefully bool) {
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(ctx context.Context) {
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: version,
},
appsectypes.HostContext{
Hostname: a.cfg.Hostname,
OS: fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH),
},
}

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

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

func eventBatchingLoop(ctx context.Context, client IntakeClient, eventChan <-chan *appsectypes.SecurityEvent, globalEventCtx []appsectypes.SecurityEventContext, cfg *Config) {
batch := make([]*appsectypes.SecurityEvent, 0, cfg.MaxBatchLen)
timer := time.NewTimer(time.Hour)
timer.Stop()

for {
select {
case <-ctx.Done():
// Return immediately as the context is now done so the http client cannot be used anymore
return

case event, ok := <-eventChan:
if event != nil {
batch = append(batch, event)
}
if !ok {
timer.Stop()
Julio-Guerra marked this conversation as resolved.
Show resolved Hide resolved
if len(batch) > 0 {
client.SendBatch(ctx, api.FromSecurityEvents(batch, globalEventCtx))
}
return
}

if l := len(batch); l == cfg.MaxBatchLen {
timer.Stop()
client.SendBatch(ctx, api.FromSecurityEvents(batch, globalEventCtx))
batch = batch[0:0]
} else if l == 1 {
timer = time.NewTimer(cfg.MaxBatchStaleTime)
}

case <-timer.C:
if len(batch) == 0 {
continue
}
client.SendBatch(ctx, api.FromSecurityEvents(batch, globalEventCtx))
batch = batch[0:0]
}
}
}

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