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

appsec: setup ossec package and OpenOperation #2781

Merged
merged 7 commits into from
Aug 16, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
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
17 changes: 11 additions & 6 deletions internal/appsec/dyngo/operation.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,18 @@ package dyngo

import (
"context"
"gopkg.in/DataDog/dd-trace-go.v1/internal/orchestrion"
"sync"
"sync/atomic"

"go.uber.org/atomic"
"gopkg.in/DataDog/dd-trace-go.v1/internal/log"
"gopkg.in/DataDog/dd-trace-go.v1/internal/orchestrion"
)

// LogError is the function used to log errors in the dyngo package.
// This is required because we really want to be able to log errors from dyngo
// but the log package depend on too much packages that we want to instrument.
// So we need to do this to avoid dependency cycles.
var LogError = func(string, ...any) {}

// 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
Expand Down Expand Up @@ -179,7 +184,7 @@ func StartAndRegisterOperation[O Operation, E ArgOf[O]](ctx context.Context, op
// should call this function to ensure the operation is properly linked in the context tree.
func RegisterOperation(ctx context.Context, op Operation) context.Context {
op.unwrap().inContext = true
return context.WithValue(ctx, contextKey{}, op)
return orchestrion.CtxWithValue(ctx, contextKey{}, op)
}

// FinishOperation finishes the operation along with its results and emits a
Expand Down Expand Up @@ -317,7 +322,7 @@ func (b *dataBroadcaster) clear() {
func emitData[T any](b *dataBroadcaster, v T) {
defer func() {
if r := recover(); r != nil {
log.Error("appsec: recovered from an unexpected panic from an event listener: %+v", r)
LogError("appsec: recovered from an unexpected panic from an event listener: %+v", r)
}
}()
b.mu.RLock()
Expand Down Expand Up @@ -348,7 +353,7 @@ func (r *eventRegister) clear() {
func emitEvent[O Operation, T any](r *eventRegister, op O, v T) {
defer func() {
if r := recover(); r != nil {
log.Error("appsec: recovered from an unexpected panic from an event listener: %+v", r)
LogError("appsec: recovered from an unexpected panic from an event listener: %+v", r)
}
}()
r.mu.RLock()
Expand Down
41 changes: 41 additions & 0 deletions internal/appsec/emitter/ossec/lfi.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// 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 2024 Datadog, Inc.

package ossec

import (
"io/fs"

"gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/dyngo"
)

type (
// OpenOperation type embodies any kind of function calls that will result in a call to an open(2) syscall
OpenOperation struct {
RomainMuller marked this conversation as resolved.
Show resolved Hide resolved
dyngo.Operation
blockErr error
}

// OpenOperationArgs is the arguments for an open operation
OpenOperationArgs struct {
// Path is the path to the file to be opened
Path string
// Flags are the flags passed to the open(2) syscall
Flags int
// Perms are the permissions passed to the open(2) syscall if the creation of a file is required
Perms fs.FileMode
}

// OpenOperationRes is the result of an open operation
OpenOperationRes struct {
// File is the file descriptor returned by the open(2) syscall
File *any
// Err is the error returned by the function
Err *error
}
)

func (OpenOperationArgs) IsArgOf(*OpenOperation) {}
func (OpenOperationRes) IsResultOf(*OpenOperation) {}
10 changes: 9 additions & 1 deletion internal/appsec/listener/grpcsec/grpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/emitter/sharedsec"
"gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/listener"
"gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/listener/httpsec"
"gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/listener/ossec"
shared "gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/listener/sharedsec"
"gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/listener/sqlsec"
"gopkg.in/DataDog/dd-trace-go.v1/internal/log"
Expand All @@ -41,6 +42,9 @@ var supportedAddresses = listener.AddressSet{
httpsec.HTTPClientIPAddr: {},
httpsec.UserIDAddr: {},
httpsec.ServerIoNetURLAddr: {},
ossec.ServerIOFSFileAddr: {},
sqlsec.ServerDBStatementAddr: {},
sqlsec.ServerDBTypeAddr: {},
}

// Install registers the gRPC WAF Event Listener on the given root operation.
Expand Down Expand Up @@ -113,10 +117,14 @@ func (l *wafEventListener) onEvent(op *types.HandlerOperation, handlerArgs types
return
}

if l.isSecAddressListened(httpsec.ServerIoNetURLAddr) {
if httpsec.SSRFAddressesPresent(l.addresses) {
httpsec.RegisterRoundTripperListener(op, &op.SecurityEventsHolder, wafCtx, l.limiter)
}

if ossec.OSAddressesPresent(l.addresses) {
ossec.RegisterOpenListener(op, &op.SecurityEventsHolder, wafCtx, l.limiter)
}

if sqlsec.SQLAddressesPresent(l.addresses) {
sqlsec.RegisterSQLListener(op, &op.SecurityEventsHolder, wafCtx, l.limiter)
}
Expand Down
8 changes: 7 additions & 1 deletion internal/appsec/listener/httpsec/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/emitter/httpsec/types"
"gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/emitter/sharedsec"
"gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/listener"
"gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/listener/ossec"
shared "gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/listener/sharedsec"
"gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/listener/sqlsec"
"gopkg.in/DataDog/dd-trace-go.v1/internal/log"
Expand Down Expand Up @@ -55,6 +56,7 @@ var supportedAddresses = listener.AddressSet{
HTTPClientIPAddr: {},
UserIDAddr: {},
ServerIoNetURLAddr: {},
ossec.ServerIOFSFileAddr: {},
sqlsec.ServerDBStatementAddr: {},
sqlsec.ServerDBTypeAddr: {},
}
Expand Down Expand Up @@ -111,12 +113,16 @@ func (l *wafEventListener) onEvent(op *types.Operation, args types.HandlerOperat
return
}

if _, ok := l.addresses[ServerIoNetURLAddr]; ok {
if SSRFAddressesPresent(l.addresses) {
dyngo.On(op, shared.MakeWAFRunListener(&op.SecurityEventsHolder, wafCtx, l.limiter, func(args types.RoundTripOperationArgs) waf.RunAddressData {
return waf.RunAddressData{Ephemeral: map[string]any{ServerIoNetURLAddr: args.URL}}
}))
}

if ossec.OSAddressesPresent(l.addresses) {
ossec.RegisterOpenListener(op, &op.SecurityEventsHolder, wafCtx, l.limiter)
}

if sqlsec.SQLAddressesPresent(l.addresses) {
sqlsec.RegisterSQLListener(op, &op.SecurityEventsHolder, wafCtx, l.limiter)
}
Expand Down
6 changes: 6 additions & 0 deletions internal/appsec/listener/httpsec/roundtripper.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ package httpsec
import (
"gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/dyngo"
"gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/emitter/httpsec/types"
"gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/listener"
"gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/listener/sharedsec"
"gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/trace"

Expand All @@ -21,3 +22,8 @@ func RegisterRoundTripperListener(op dyngo.Operation, events *trace.SecurityEven
return waf.RunAddressData{Ephemeral: map[string]any{ServerIoNetURLAddr: args.URL}}
}))
}

func SSRFAddressesPresent(addresses listener.AddressSet) bool {
_, urlAddr := addresses[ServerIoNetURLAddr]
return urlAddr
}
44 changes: 44 additions & 0 deletions internal/appsec/listener/ossec/lfi.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// 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 2024 Datadog, Inc.

package ossec

import (
"github.com/DataDog/appsec-internal-go/limiter"
waf "github.com/DataDog/go-libddwaf/v3"

"gopkg.in/DataDog/dd-trace-go.v1/appsec/events"
"gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/dyngo"
"gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/emitter/ossec"
"gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/listener"
"gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/listener/sharedsec"
"gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/trace"
)

const (
ServerIOFSFileAddr = "server.io.fs.file"
)

func RegisterOpenListener(op dyngo.Operation, eventsHolder *trace.SecurityEventsHolder, wafCtx *waf.Context, limiter limiter.Limiter) {
runWAF := sharedsec.MakeWAFRunListener(eventsHolder, wafCtx, limiter, func(args ossec.OpenOperationArgs) waf.RunAddressData {
return waf.RunAddressData{Ephemeral: map[string]any{ServerIOFSFileAddr: args.Path}}
})

dyngo.On(op, func(op *ossec.OpenOperation, args ossec.OpenOperationArgs) {
dyngo.OnData(op, func(e *events.BlockingSecurityEvent) {
dyngo.OnFinish(op, func(op *ossec.OpenOperation, res ossec.OpenOperationRes) {
if res.Err != nil {
*res.Err = e
}
})
})
runWAF(op, args)
})
}

func OSAddressesPresent(addresses listener.AddressSet) bool {
_, fileAddr := addresses[ServerIOFSFileAddr]
return fileAddr
}
99 changes: 99 additions & 0 deletions internal/appsec/waf_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,28 +6,34 @@
package appsec_test

import (
"context"
"database/sql"
"encoding/json"
"fmt"
"io"
"io/fs"
"log"
"math/rand"
"net/http"
"net/http/httptest"
"net/url"
"os"
"strconv"
"strings"
"testing"

internal "github.com/DataDog/appsec-internal-go/appsec"
waf "github.com/DataDog/go-libddwaf/v3"

pAppsec "gopkg.in/DataDog/dd-trace-go.v1/appsec"
"gopkg.in/DataDog/dd-trace-go.v1/appsec/events"
sqltrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/database/sql"
httptrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/net/http"
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/mocktracer"
"gopkg.in/DataDog/dd-trace-go.v1/internal/appsec"
"gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/config"
"gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/dyngo"
"gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/emitter/ossec"
"gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/listener/httpsec"

_ "github.com/glebarez/go-sqlite"
Expand Down Expand Up @@ -642,7 +648,100 @@ func TestRASPSQLi(t *testing.T) {
})
}
}
}

func TestRASPLFI(t *testing.T) {
t.Setenv("DD_APPSEC_RULES", "testdata/rasp.json")
appsec.Start()
defer appsec.Stop()

if !appsec.RASPEnabled() {
t.Skip("RASP needs to be enabled for this test")
}

// Simulate what orchestrion does
WrappedOpen := func(ctx context.Context, path string, flags int) (file *os.File, err error) {
parent, _ := dyngo.FromContext(ctx)
op := &ossec.OpenOperation{
Operation: dyngo.NewOperation(parent),
}

dyngo.StartOperation(op, ossec.OpenOperationArgs{
Path: path,
Flags: flags,
Perms: fs.FileMode(0),
})

var x any = file
defer dyngo.FinishOperation(op, ossec.OpenOperationRes{
File: &x,
Err: &err,
})

return
}

mux := httptrace.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
// Subsequent spans inherit their parent from context.
path := r.URL.Query().Get("path")
block := r.URL.Query().Get("block")
if block == "true" {
_, err := WrappedOpen(r.Context(), path, os.O_RDONLY)
require.ErrorIs(t, err, &events.BlockingSecurityEvent{})
return
}

_, err := WrappedOpen(r.Context(), "/tmp/test", os.O_RDWR)
require.NoError(t, err)
w.WriteHeader(204)
})
srv := httptest.NewServer(mux)
defer srv.Close()

for _, tc := range []struct {
name string
path string
block bool
}{
{
name: "no-error",
path: "",
block: false,
},
{
name: "passwd",
path: "/etc/passwd",
block: true,
},
{
name: "shadow",
path: "/etc/shadow",
block: true,
},
} {
t.Run(tc.name, func(t *testing.T) {
mt := mocktracer.Start()
defer mt.Stop()

req, err := http.NewRequest("GET", srv.URL+"?path="+tc.path+"&block="+strconv.FormatBool(tc.block), nil)
require.NoError(t, err)
res, err := srv.Client().Do(req)
require.NoError(t, err)
defer res.Body.Close()

spans := mt.FinishedSpans()
require.Len(t, spans, 1)

if tc.block {
require.Equal(t, 403, res.StatusCode)
require.Contains(t, spans[0].Tag("_dd.appsec.json"), "rasp-930-100")
require.Contains(t, spans[0].Tags(), "_dd.stack")
} else {
require.Equal(t, 204, res.StatusCode)
}
})
}
}

// BenchmarkSampleWAFContext benchmarks the creation of a WAF context and running the WAF on a request/response pair
Expand Down
6 changes: 6 additions & 0 deletions internal/log/log.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"sync"
"time"

"gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/dyngo"
"gopkg.in/DataDog/dd-trace-go.v1/internal/version"
)

Expand Down Expand Up @@ -102,6 +103,11 @@ func init() {
if v := os.Getenv("DD_LOGGING_RATE"); v != "" {
setLoggingRate(v)
}

// This is required because we really want to be able to log errors from dyngo
// but the log package depend on too much packages that we want to instrument.
// So we need to do this to avoid dependency cycles.
dyngo.LogError = Error
}

func setLoggingRate(v string) {
Expand Down
Loading
Loading