Skip to content

Commit

Permalink
appsec: setup ossec package and OpenOperation (#2781)
Browse files Browse the repository at this point in the history
Signed-off-by: Eliott Bouhana <[email protected]>
  • Loading branch information
eliottness authored and darccio committed Aug 19, 2024
1 parent 002b6ff commit da02d30
Show file tree
Hide file tree
Showing 11 changed files with 259 additions and 16 deletions.
23 changes: 23 additions & 0 deletions .github/workflows/orchestrion.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
name: Orchestrion
on:
workflow_dispatch: # manually
pull_request:
merge_group:
push:
branches:
- release-v*

permissions: read-all

concurrency:
# Automatically cancel previous runs if a new one is triggered to conserve resources.
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}
cancel-in-progress: true

jobs:
test:
name: 'Run Tests'
uses: DataDog/orchestrion/.github/workflows/workflow_call.yml@main # we don't want to pin our own action
with:
dd-trace-go-ref: ${{ github.sha }}
runs-on: ubuntu-latest-16-cores
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 {
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[File any] struct {
// File is the file descriptor returned by the open(2) syscall
File *File
// Err is the error returned by the function
Err *error
}
)

func (OpenOperationArgs) IsArgOf(*OpenOperation) {}
func (OpenOperationRes[File]) 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
}
46 changes: 46 additions & 0 deletions internal/appsec/listener/ossec/lfi.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// 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 (
"os"

"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(_ *ossec.OpenOperation, res ossec.OpenOperationRes[*os.File]) {
if res.Err != nil {
*res.Err = e
}
})
})
runWAF(op, args)
})
}

func OSAddressesPresent(addresses listener.AddressSet) bool {
_, fileAddr := addresses[ServerIOFSFileAddr]
return fileAddr
}
98 changes: 98 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,99 @@ 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),
})

defer dyngo.FinishOperation(op, ossec.OpenOperationRes[*os.File]{
File: &file,
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
Loading

0 comments on commit da02d30

Please sign in to comment.