Skip to content

Commit

Permalink
appsec: add tracer start option for appsec enablement (#2966)
Browse files Browse the repository at this point in the history
  • Loading branch information
RomainMuller authored Nov 6, 2024
1 parent 5dd43b0 commit d2882eb
Show file tree
Hide file tree
Showing 6 changed files with 172 additions and 31 deletions.
25 changes: 24 additions & 1 deletion ddtrace/tracer/option.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,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/internal"
appsecconfig "gopkg.in/DataDog/dd-trace-go.v1/internal/appsec/config"
"gopkg.in/DataDog/dd-trace-go.v1/internal/civisibility/constants"
"gopkg.in/DataDog/dd-trace-go.v1/internal/globalconfig"
"gopkg.in/DataDog/dd-trace-go.v1/internal/log"
Expand Down Expand Up @@ -114,6 +115,9 @@ type config struct {
// debug, when true, writes details to logs.
debug bool

// appsecStartOptions controls the options used when starting appsec features.
appsecStartOptions []appsecconfig.StartOption

// agent holds the capabilities of the agent and determines some
// of the behaviour of the tracer.
agent agentFeatures
Expand Down Expand Up @@ -660,7 +664,7 @@ func loadAgentFeatures(agentDisabled bool, agentURL *url.URL, httpClient *http.C
}
defer resp.Body.Close()
type agentConfig struct {
defaultEnv string `json:"default_env"`
DefaultEnv string `json:"default_env"`
}
type infoResponse struct {
Endpoints []string `json:"endpoints"`
Expand Down Expand Up @@ -762,6 +766,25 @@ func withNoopStats() StartOption {
}
}

// WithAppSecEnabled specifies whether AppSec features should be activated
// or not.
//
// By default, AppSec features are enabled if `DD_APPSEC_ENABLED` is set to a
// truthy value; and may be enabled by remote configuration if
// `DD_APPSEC_ENABLED` is not set at all.
//
// Using this option to explicitly disable appsec also prevents it from being
// remote activated.
func WithAppSecEnabled(enabled bool) StartOption {
mode := appsecconfig.ForcedOff
if enabled {
mode = appsecconfig.ForcedOn
}
return func(c *config) {
c.appsecStartOptions = append(c.appsecStartOptions, appsecconfig.WithEnablementMode(mode))
}
}

// WithFeatureFlags specifies a set of feature flags to enable. Please take into account
// that most, if not all features flags are considered to be experimental and result in
// unexpected bugs.
Expand Down
5 changes: 4 additions & 1 deletion ddtrace/tracer/tracer.go
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,10 @@ func Start(opts ...StartOption) {
// appsec.Start() may use the telemetry client to report activation, so it is
// important this happens _AFTER_ startTelemetry() has been called, so the
// client is appropriately configured.
appsec.Start(appsecConfig.WithRCConfig(cfg))
appsecopts := make([]appsecConfig.StartOption, 0, len(t.config.appsecStartOptions)+1)
appsecopts = append(appsecopts, t.config.appsecStartOptions...)
appsecopts = append(appsecopts, appsecConfig.WithRCConfig(cfg))
appsec.Start(appsecopts...)
_ = t.hostname() // Prime the hostname cache
}

Expand Down
34 changes: 20 additions & 14 deletions internal/appsec/appsec.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,32 +39,41 @@ func Start(opts ...config.StartOption) {
telemetry := newAppsecTelemetry()
defer telemetry.emit()

startConfig := config.NewStartConfig(opts...)

// AppSec can start either:
// 1. Manually thanks to DD_APPSEC_ENABLED
// 1. Manually thanks to DD_APPSEC_ENABLED (or via [config.WithEnablementMode])
// 2. Remotely when DD_APPSEC_ENABLED is undefined
// Note: DD_APPSEC_ENABLED=false takes precedence over remote configuration
// and enforces to have AppSec disabled.
enabled, set, err := config.IsEnabled()
mode, modeOrigin, err := startConfig.EnablementMode()
if err != nil {
logUnexpectedStartError(err)
return
}
if set {
telemetry.addEnvConfig("DD_APPSEC_ENABLED", enabled)

switch modeOrigin {
case config.OriginEnvVar:
telemetry.addEnvConfig("DD_APPSEC_ENABLED", mode == config.ForcedOn)
if mode == config.ForcedOff {
log.Debug("appsec: disabled by the configuration: set the environment variable DD_APPSEC_ENABLED to true to enable it")
return
}
case config.OriginExplicitOption:
telemetry.addCodeConfig("WithEnablementMode", mode)
}

// Check if AppSec is explicitly disabled
if set && !enabled {
log.Debug("appsec: disabled by the configuration: set the environment variable DD_APPSEC_ENABLED to true to enable it")
// In any case, if we're forced off, we no longer have any business here...
if mode == config.ForcedOff {
return
}

// Check whether libddwaf - required for Threats Detection - is ok or not
if ok, err := waf.Health(); !ok {
// We need to avoid logging an error to APM tracing users who don't necessarily intend to enable appsec
if set {
if mode == config.ForcedOn {
// DD_APPSEC_ENABLED is explicitly set so we log an error
log.Error("appsec: threats detection cannot be enabled for the following reasons: %vappsec: no security activities will be collected. Please contact support at https://docs.datadoghq.com/help/ for help.", err)
log.Error("appsec: threats detection cannot be enabled for the following reasons: %v\nappsec: no security activities will be collected. Please contact support at https://docs.datadoghq.com/help/ for help.", err)
} else {
// DD_APPSEC_ENABLED is not set so we cannot know what the intent is here, we must log a
// debug message instead to avoid showing an error to APM-tracing-only users.
Expand All @@ -74,14 +83,11 @@ func Start(opts ...config.StartOption) {
}

// From this point we know that AppSec is either enabled or can be enabled through remote config
cfg, err := config.NewConfig()
cfg, err := startConfig.NewConfig()
if err != nil {
logUnexpectedStartError(err)
return
}
for _, opt := range opts {
opt(cfg)
}
appsec := newAppSec(cfg)

// Start the remote configuration client
Expand All @@ -90,7 +96,7 @@ func Start(opts ...config.StartOption) {
log.Error("appsec: Remote config: disabled due to an instanciation error: %v", err)
}

if !set {
if mode == config.RCStandby {
// AppSec is not enforced by the env var and can be enabled through remote config
log.Debug("appsec: %s is not set, appsec won't start until activated through remote configuration", config.EnvEnabled)
if err := appsec.enableRemoteActivation(); err != nil {
Expand Down
93 changes: 80 additions & 13 deletions internal/appsec/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,78 @@ const (
)

// StartOption is used to customize the AppSec configuration when invoked with appsec.Start()
type StartOption func(c *Config)
type StartOption func(c *StartConfig)

type StartConfig struct {
// RC is the remote config client configuration to be used.
RC *remoteconfig.ClientConfig
// IsEnabled is a function that determines whether AppSec is enabled or not. When unset, the
// default [IsEnabled] function is used.
EnablementMode func() (EnablementMode, Origin, error)
}

type EnablementMode int8

const (
// ForcedOff is the mode where AppSec is forced to be disabled, not allowing remote activation.
ForcedOff EnablementMode = -1
// RCStandby is the mode where AppSec is in stand-by, waiting remote activation.
RCStandby EnablementMode = 0
// ForcedOn is the mode where AppSec is forced to be enabled.
ForcedOn EnablementMode = 1
)

type Origin uint8

const (
// OriginDefault is the origin of configuration values not explicitly set by the user in any way.
OriginDefault Origin = iota
// OriginEnvVar is the origin of configuration values set through environment variables.
OriginEnvVar
// OriginExplicitOption is the origin of configuration values set though explicit options in code.
OriginExplicitOption
)

func NewStartConfig(opts ...StartOption) *StartConfig {
c := &StartConfig{
EnablementMode: func() (mode EnablementMode, origin Origin, err error) {
enabled, set, err := IsEnabledByEnvironment()
if set {
origin = OriginEnvVar
if enabled {
mode = ForcedOn
} else {
mode = ForcedOff
}
} else {
origin = OriginDefault
mode = RCStandby
}
return mode, origin, err
},
}
for _, opt := range opts {
opt(c)
}
return c
}

// WithEnablementMode forces AppSec enablement, replacing the default initialization conditions
// implemented by [IsEnabledByEnvironment].
func WithEnablementMode(mode EnablementMode) StartOption {
return func(c *StartConfig) {
c.EnablementMode = func() (EnablementMode, Origin, error) {
return mode, OriginExplicitOption, nil
}
}
}

// WithRCConfig sets the AppSec remote config client configuration to the specified cfg
func WithRCConfig(cfg remoteconfig.ClientConfig) StartOption {
return func(c *StartConfig) {
c.RC = &cfg
}
}

// Config is the AppSec configuration.
type Config struct {
Expand Down Expand Up @@ -94,17 +165,12 @@ func (set AddressSet) AnyOf(anyOf ...string) bool {
return false
}

// WithRCConfig sets the AppSec remote config client configuration to the specified cfg
func WithRCConfig(cfg remoteconfig.ClientConfig) StartOption {
return func(c *Config) {
c.RC = &cfg
}
}

// IsEnabled returns true when appsec is enabled by the environment variable DD_APPSEC_ENABLED (as of strconv's boolean
// parsing rules). When false, it also returns whether the env var was actually set or not.
// In case of a parsing error, it returns a detailed error.
func IsEnabled() (enabled bool, set bool, err error) {
// IsEnabledByEnvironment returns true when appsec is enabled by the environment variable
// [EnvEnabled] being set to a truthy value, as well as whether the environment variable was set at
// all or not (so it is possible to distinguish between explicitly false, and false-by-default).
// If the [EnvEnabled] variable is set to a value that is not a valid boolean (according to
// [strconv.ParseBool]), it is considered false-y, and a detailed error is also returned.
func IsEnabledByEnvironment() (enabled bool, set bool, err error) {
return parseBoolEnvVar(EnvEnabled)
}

Expand All @@ -123,7 +189,7 @@ func parseBoolEnvVar(env string) (enabled bool, set bool, err error) {
}

// NewConfig returns a fresh appsec configuration read from the env
func NewConfig() (*Config, error) {
func (c *StartConfig) NewConfig() (*Config, error) {
rules, err := internal.RulesFromEnv()
if err != nil {
return nil, err
Expand All @@ -141,5 +207,6 @@ func NewConfig() (*Config, error) {
Obfuscator: internal.NewObfuscatorConfig(),
APISec: internal.NewAPISecConfig(),
RASP: internal.RASPEnabled(),
RC: c.RC,
}, nil
}
38 changes: 36 additions & 2 deletions internal/appsec/remoteconfig_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ package appsec
import (
"encoding/json"
"errors"
"fmt"
"os"
"reflect"
"slices"
Expand All @@ -30,7 +31,7 @@ func TestASMFeaturesCallback(t *testing.T) {
}
enabledPayload := []byte(`{"asm":{"enabled":true}}`)
disabledPayload := []byte(`{"asm":{"enabled":false}}`)
cfg, err := config.NewConfig()
cfg, err := config.NewStartConfig().NewConfig()
require.NoError(t, err)
a := newAppSec(cfg)
err = a.startRC()
Expand Down Expand Up @@ -410,13 +411,46 @@ func TestRemoteActivationScenarios(t *testing.T) {
require.False(t, found)
})

t.Run("WithEnablementMode(EnabledModeForcedOn)", func(t *testing.T) {
for _, envVal := range []string{"", "true", "false"} {
t.Run(fmt.Sprintf("DD_APPSEC_ENABLED=%s", envVal), func(t *testing.T) {
t.Setenv(config.EnvEnabled, envVal)

remoteconfig.Reset()
Start(config.WithEnablementMode(config.ForcedOn), config.WithRCConfig(remoteconfig.DefaultClientConfig()))
defer Stop()

require.True(t, Enabled())
found, err := remoteconfig.HasCapability(remoteconfig.ASMActivation)
require.NoError(t, err)
require.False(t, found)
found, err = remoteconfig.HasProduct(rc.ProductASMFeatures)
require.NoError(t, err)
require.False(t, found)
})
}
})

t.Run("DD_APPSEC_ENABLED=false", func(t *testing.T) {
t.Setenv(config.EnvEnabled, "false")
Start(config.WithRCConfig(remoteconfig.DefaultClientConfig()))
defer Stop()
require.Nil(t, activeAppSec)
require.False(t, Enabled())
})

t.Run("WithEnablementMode(EnabledModeForcedOff)", func(t *testing.T) {
for _, envVal := range []string{"", "true", "false"} {
t.Run(fmt.Sprintf("DD_APPSEC_ENABLED=%s", envVal), func(t *testing.T) {
t.Setenv(config.EnvEnabled, envVal)

Start(config.WithEnablementMode(config.ForcedOff), config.WithRCConfig(remoteconfig.DefaultClientConfig()))
defer Stop()
require.Nil(t, activeAppSec)
require.False(t, Enabled())
})
}
})
}

func TestCapabilitiesAndProducts(t *testing.T) {
Expand Down Expand Up @@ -829,7 +863,7 @@ func TestWafRCUpdate(t *testing.T) {
}

t.Run("toggle-blocking", func(t *testing.T) {
cfg, err := config.NewConfig()
cfg, err := config.NewStartConfig().NewConfig()
require.NoError(t, err)
wafHandle, err := waf.NewHandle(cfg.RulesManager.Latest, cfg.Obfuscator.KeyRegex, cfg.Obfuscator.ValueRegex)
require.NoError(t, err)
Expand Down
8 changes: 8 additions & 0 deletions internal/appsec/telemetry.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,14 @@ func (a *appsecTelemetry) addConfig(name string, value any) {
a.configs = append(a.configs, telemetry.Configuration{Name: name, Value: value})
}

// addCodeConfig adds a new configuration entry to this telemetry event.
func (a *appsecTelemetry) addCodeConfig(name string, value any) {
if a == nil {
return
}
a.configs = append(a.configs, telemetry.Configuration{Name: name, Value: value, Origin: telemetry.OriginCode})
}

// addEnvConfig adds a new envionment-sourced configuration entry to this event.
func (a *appsecTelemetry) addEnvConfig(name string, value any) {
if a == nil {
Expand Down

0 comments on commit d2882eb

Please sign in to comment.