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: add tracer start option for appsec enablement #2966

Merged
merged 3 commits into from
Nov 6, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
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"`
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NB - This was previously broken; as the json package cannot do anything with non-exported fields...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch.

}
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 {
darccio marked this conversation as resolved.
Show resolved Hide resolved
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
Loading