Skip to content
Closed
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
11 changes: 9 additions & 2 deletions ddtrace/appsec/_asm_request_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -400,8 +400,15 @@ def finalize_asm_env(env: ASM_Environment) -> None:
entry_span._set_attribute(APPSEC.EVENT_RULE_ERROR_COUNT, info.failed)
except Exception:
logger.debug("asm_context::finalize_asm_env::exception", extra=log_extra, exc_info=True)
if asm_config._rc_client_id is not None:
entry_span.set_tag(APPSEC.RC_CLIENT_ID, asm_config._rc_client_id)
if asm_config._rc_client_id_enabled:
from ddtrace.internal.remoteconfig.worker import remoteconfig_poller

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Remove the deferred Remote Config worker import

When an AppSec request is finalized with RC tagging enabled, this imports the worker inside the hot-path function to avoid an import-time dependency, leaving _asm_request_context structurally coupled to the RC worker and even adding a test that enforces the workaround. Extract or inject a lightweight current-client-ID accessor instead; repository policy explicitly forbids leaving deferred imports in place to conceal import-graph problems.

AGENTS.md reference: AGENTS.md:L20-L21

Useful? React with 馃憤聽/ 馃憥.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think Codex鈥檚 comment makes sense. WDYT @christophe-papazian?


# Fetch the current id from the RC client at span finalization. asm_config only
# tracks whether AppSec RC enabled tagging; mirroring the id there would go stale
# when runtime identity refresh rebuilds the RC client.
rc_client_id = remoteconfig_poller._client.id

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve the polling client ID in forked workers

In a forked worker, the runtime-ID callback renews RemoteConfigClient.id and drops the inherited native client (client.py:142-159), while RemoteConfigPoller.reset_at_fork() disables agent polling and consumes the parent's snapshots through inherited shared memory (worker.py:144-165). Reading the renewed local id here therefore tags worker spans with a client ID that has never polled or registered with the agent, so AppSec traces from common prefork deployments cannot be correlated with the RC client that supplied their configuration; retain/expose the origin poller's ID for shared-memory consumers instead.

Useful? React with 馃憤聽/ 馃憥.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think Codex鈥檚 comment makes sense. WDYT @P403n1x87?

if rc_client_id is not None:
entry_span.set_tag(APPSEC.RC_CLIENT_ID, rc_client_id)
waf_adresses = env.waf_addresses
req_headers = waf_adresses.get(SPAN_DATA_NAMES.REQUEST_HEADERS_NO_COOKIES, {})
if req_headers:
Expand Down
5 changes: 4 additions & 1 deletion ddtrace/appsec/_remoteconfiguration.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,13 @@ def enable_appsec_rc(callback: "AppSecCallback") -> None:

if asm_config._asm_enabled:
telemetry_writer.product_activated(TELEMETRY_APM_PRODUCT.APPSEC, True)
asm_config._rc_client_id = remoteconfig_poller._client.id

asm_config._rc_client_id_enabled = True


def disable_appsec_rc() -> None:
asm_config._rc_client_id_enabled = False

for product_name in APPSEC_PRODUCTS:
remoteconfig_poller.unregister_callback(product_name)
remoteconfig_poller.disable_product(product_name)
Expand Down
4 changes: 3 additions & 1 deletion ddtrace/internal/settings/asm.py
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,9 @@ class ASMConfig(DDConfig):
sys.platform.startswith("win") or sys.platform.startswith("cygwin")
)

_rc_client_id: Optional[str] = None
# Set by enable_appsec_rc()/disable_appsec_rc(); gates _dd.rc.client_id span tagging so it's
# only emitted while AppSec RC is actually enabled, not just whenever a live RC client exists.
_rc_client_id_enabled: bool = False

def __init__(self):
super().__init__()
Expand Down
14 changes: 14 additions & 0 deletions tests/appsec/appsec/test_asm_request_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,20 @@
config_asm = {"_asm_enabled": True}


@pytest.mark.parametrize("auto_enable_crashtracking", [False])
def test_import_does_not_load_remoteconfig_worker(run_python_code_in_subprocess, auto_enable_crashtracking):
code = """
import sys

import ddtrace.appsec._asm_request_context # noqa: F401

assert "ddtrace.internal.remoteconfig.worker" not in sys.modules
"""

_, stderr, status, _ = run_python_code_in_subprocess(code)
assert status == 0, stderr


def test_context_set_and_reset():
with asm_context(
ip_addr=_TEST_IP,
Expand Down
40 changes: 40 additions & 0 deletions tests/appsec/appsec/test_remoteconfiguration.py
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,46 @@ def test_rc_activation_validate_client_id(tracer, rc_poller, appsec_callback):
disable_appsec_rc()


def test_rc_client_id_tag_reflects_live_value_not_a_stale_cache(tracer, rc_poller, appsec_callback):
"""_dd.rc.client_id must be read live at span-tagging time, not cached once at RC enable time.

Otherwise the tag would go stale after e.g. an AWS Lambda MicroVM identity refresh
regenerates the real client id.
"""
from ddtrace.internal.remoteconfig.worker import remoteconfig_poller

with override_global_config(dict(_asm_enabled=True, _remote_config_enabled=True, api_version="v0.4")):
tracer.configure(appsec_enabled=True)
enable_appsec_rc(appsec_callback)

with mock.patch.object(remoteconfig_poller._client, "id", "client-id-one"):
with asm_context(tracer) as span:
set_http_meta(span, {}, raw_uri="http://example.com/", status_code="200")
assert span._local_root._get_str_attribute(APPSEC.RC_CLIENT_ID) == "client-id-one"

with mock.patch.object(remoteconfig_poller._client, "id", "client-id-two"):
with asm_context(tracer) as span:
set_http_meta(span, {}, raw_uri="http://example.com/", status_code="200")
assert span._local_root._get_str_attribute(APPSEC.RC_CLIENT_ID) == "client-id-two"
disable_appsec_rc()


def test_rc_client_id_tag_not_set_when_rc_disabled(tracer):
"""_dd.rc.client_id must not be tagged when AppSec RC was never enabled, even though a live
RC client (and id) exists process-wide -- otherwise every ASM-tracked span would get tagged
with an id from a Remote Config subscription AppSec never activated.
"""
from ddtrace.internal.remoteconfig.worker import remoteconfig_poller

with override_global_config(dict(_asm_enabled=True, api_version="v0.4")):
tracer.configure(appsec_enabled=True)

with mock.patch.object(remoteconfig_poller._client, "id", "client-id-one"):
with asm_context(tracer) as span:
set_http_meta(span, {}, raw_uri="http://example.com/", status_code="200")
assert span._local_root._get_str_attribute(APPSEC.RC_CLIENT_ID) is None


@pytest.mark.parametrize(
"env_rules, expected",
[
Expand Down
Loading