Skip to content

chore(aap): appsec.enabled metric #13822

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

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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
3 changes: 3 additions & 0 deletions ddtrace/_trace/tracer.py
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,7 @@ def configure(
context_provider: Optional[BaseContextProvider] = None,
compute_stats_enabled: Optional[bool] = None,
appsec_enabled: Optional[bool] = None,
appsec_enabled_origin: Optional[str] = "",
iast_enabled: Optional[bool] = None,
apm_tracing_disabled: Optional[bool] = None,
trace_processors: Optional[List[TraceProcessor]] = None,
Expand All @@ -351,6 +352,8 @@ def configure(

if appsec_enabled is not None:
asm_config._asm_enabled = appsec_enabled
if appsec_enabled_origin:
asm_config._asm_enabled_origin = appsec_enabled_origin # type: ignore[assignment]

if iast_enabled is not None:
asm_config._iast_enabled = iast_enabled
Expand Down
4 changes: 4 additions & 0 deletions ddtrace/appsec/_constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ class APPSEC(metaclass=Constant_Class):
APM_TRACING_ENV: Literal["DD_APM_TRACING_ENABLED"] = "DD_APM_TRACING_ENABLED"
RULE_FILE: Literal["DD_APPSEC_RULES"] = "DD_APPSEC_RULES"
ENABLED: Literal["_dd.appsec.enabled"] = "_dd.appsec.enabled"
ENABLED_ORIGIN_DEFAULT: Literal["default"] = "default"
ENABLED_ORIGIN_UNKNOWN: Literal["unknown"] = "unknown"
Comment on lines +55 to +56
Copy link
Contributor

Choose a reason for hiding this comment

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

What's the difference between default and unknown

ENABLED_ORIGIN_RC: Literal["remote_config"] = "remote_config"
ENABLED_ORIGIN_ENV: Literal["env_var"] = "env_var"
JSON: Literal["_dd.appsec.json"] = "_dd.appsec.json"
STRUCT: Literal["appsec"] = "appsec"
EVENT_RULE_VERSION: Literal["_dd.appsec.event_rules.version"] = "_dd.appsec.event_rules.version"
Expand Down
11 changes: 11 additions & 0 deletions ddtrace/appsec/_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@
from ddtrace.contrib.internal.trace_utils_base import _set_url_tag
from ddtrace.ext import http
from ddtrace.internal import core
from ddtrace.internal import telemetry
from ddtrace.internal.constants import RESPONSE_HEADERS
from ddtrace.internal.logger import get_logger
from ddtrace.internal.telemetry import TELEMETRY_NAMESPACE
from ddtrace.internal.utils import http as http_utils
from ddtrace.internal.utils.http import parse_form_multipart
from ddtrace.settings.asm import config as asm_config
Expand Down Expand Up @@ -374,7 +376,16 @@ def _on_start_response_blocked(ctx, flask_config, response_headers, status):
trace_utils.set_http_meta(ctx["req_span"], flask_config, status_code=status, response_headers=response_headers)


def _on_telemetry_periodic():
if asm_config._asm_enabled:
telemetry.telemetry_writer.add_gauge_metric(
TELEMETRY_NAMESPACE.APPSEC, "enabled", 2, (("origin", asm_config.asm_enabled_origin),)
)


def listen():
core.on("telemetry.periodic", _on_telemetry_periodic)

core.on("set_http_meta_for_asm", _on_set_http_meta)
core.on("flask.request_call_modifier", _on_request_span_modifier, "request_body")

Expand Down
3 changes: 2 additions & 1 deletion ddtrace/appsec/_remoteconfiguration.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

from ddtrace.appsec._capabilities import _asm_feature_is_required
from ddtrace.appsec._capabilities import _rc_capabilities
from ddtrace.appsec._constants import APPSEC
from ddtrace.appsec._constants import PRODUCTS
from ddtrace.internal.logger import get_logger
from ddtrace.internal.remoteconfig import Payload
Expand Down Expand Up @@ -165,7 +166,7 @@ def disable_asm(local_tracer: Tracer):

def enable_asm(local_tracer: Tracer):
if not asm_config._asm_enabled:
local_tracer.configure(appsec_enabled=True)
local_tracer.configure(appsec_enabled=True, appsec_enabled_origin=APPSEC.ENABLED_ORIGIN_RC)


def _preprocess_results_appsec_1click_activation(
Expand Down
7 changes: 7 additions & 0 deletions ddtrace/internal/telemetry/writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -622,10 +622,17 @@ def _generate_logs_event(self, logs):
log.debug("%s request payload", TELEMETRY_TYPE_LOGS)
self.add_event({"logs": list(logs)}, TELEMETRY_TYPE_LOGS)

def _dispatch(self):
# moved core here to avoid circular import
from ...internal import core

core.dispatch("telemetry.periodic")

def periodic(self, force_flush=False, shutting_down=False):
# ensure app_started is called at least once in case traces weren't flushed
self._app_started()
self._app_product_change()
self._dispatch()

namespace_metrics = self._namespace.flush(float(self.interval))
if namespace_metrics:
Expand Down
7 changes: 7 additions & 0 deletions ddtrace/settings/asm.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ def build_libddwaf_filename() -> str:

class ASMConfig(DDConfig):
_asm_enabled = DDConfig.var(bool, APPSEC_ENV, default=False)
_asm_enabled_origin = APPSEC.ENABLED_ORIGIN_UNKNOWN
_asm_static_rule_file = DDConfig.var(Optional[str], APPSEC.RULE_FILE, default=None)
# prevent empty string
if _asm_static_rule_file == "":
Expand Down Expand Up @@ -261,6 +262,12 @@ def __init__(self):
# Is one click available?
self._eval_asm_can_be_enabled()

@property
def asm_enabled_origin(self):
if APPSEC_ENV in os.environ:
return APPSEC.ENABLED_ORIGIN_ENV
return self._asm_enabled_origin

def reset(self):
"""For testing purposes, reset the configuration to its default values given current environment variables."""
self.__init__()
Expand Down
41 changes: 41 additions & 0 deletions tests/appsec/appsec/test_telemetry.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@
import pytest

import ddtrace.appsec._asm_request_context as asm_request_context
from ddtrace.appsec._constants import APPSEC
from ddtrace.appsec._ddwaf import version
import ddtrace.appsec._ddwaf.ddwaf_types
from ddtrace.appsec._deduplications import deduplication
from ddtrace.appsec._processor import AppSecSpanProcessor
from ddtrace.appsec._remoteconfiguration import enable_asm
from ddtrace.constants import APPSEC_ENV
from ddtrace.contrib.internal.trace_utils import set_http_meta
from ddtrace.ext import SpanTypes
from ddtrace.internal.telemetry.constants import TELEMETRY_NAMESPACE
Expand All @@ -18,6 +21,7 @@
import tests.appsec.rules as rules
from tests.appsec.utils import asm_context
from tests.appsec.utils import build_payload
from tests.utils import override_env
from tests.utils import override_global_config


Expand Down Expand Up @@ -258,3 +262,40 @@ def test_log_metric_error_ddwaf_update_deduplication_timelapse(telemetry_writer)
assert len(list_metrics_logs) == 1
finally:
deduplication._time_lapse = old_value


@pytest.mark.parametrize(
"environment,appsec_enabled,rc_enabled,expected_result,expected_origin",
(
({}, False, False, 0, ""),
({APPSEC_ENV: "true"}, True, False, 1, APPSEC.ENABLED_ORIGIN_ENV),
({}, True, False, 1, APPSEC.ENABLED_ORIGIN_UNKNOWN),
({}, True, True, 1, APPSEC.ENABLED_ORIGIN_UNKNOWN),
({}, False, True, 1, APPSEC.ENABLED_ORIGIN_RC),
({APPSEC_ENV: "true"}, False, True, 1, APPSEC.ENABLED_ORIGIN_ENV),
),
)
def test_appsec_enabled_metric(
environment, appsec_enabled, rc_enabled, expected_result, expected_origin, telemetry_writer, tracer
):
"""Test that an internal error is logged when the WAF returns an internal error."""
with override_env(environment), override_global_config(dict(_asm_enabled=appsec_enabled)):
tracer.configure(appsec_enabled=appsec_enabled)
AppSecSpanProcessor()
if rc_enabled:
enable_asm(tracer)

telemetry_writer._dispatch()

metrics_result = telemetry_writer._namespace.flush(0.1)
list_telemetry_metrics = metrics_result.get(TELEMETRY_TYPE_GENERATE_METRICS, {}).get(
TELEMETRY_NAMESPACE.APPSEC.value, {}
)
metrics = [m for m in list_telemetry_metrics if m["metric"] == "enabled"]
assert len(metrics) == expected_result, metrics
if expected_result > 0:
assert len(metrics[0]["tags"]) == 1
assert f"origin:{expected_origin}" in metrics[0]["tags"]

# Restore defaults
tracer.configure(appsec_enabled=appsec_enabled, appsec_enabled_origin=APPSEC.ENABLED_ORIGIN_UNKNOWN)
Loading