Skip to content

Commit 57bc9cd

Browse files
chore(aap): add visibility for rc products (#13800)
- add a new private root span tag `_dd.appsec.rc_products` to track the number of paths currently active for each appsec remote config product - update unit tests to check for that tag - make sure appsec.enabled is on root span ## Checklist - [x] PR author has checked that all the criteria below are met - The PR description includes an overview of the change - The PR description articulates the motivation for the change - The change includes tests OR the PR description describes a testing strategy - The PR description notes risks associated with the change, if any - Newly-added code is easy to change - The change follows the [library release note guidelines](https://ddtrace.readthedocs.io/en/stable/releasenotes.html) - The change includes or references documentation updates if necessary - Backport labels are set (if [applicable](https://ddtrace.readthedocs.io/en/latest/contributing.html#backporting)) ## Reviewer Checklist - [x] Reviewer has checked that all the criteria below are met - Title is accurate - All changes are related to the pull request's stated goal - Avoids breaking [API](https://ddtrace.readthedocs.io/en/stable/versioning.html#interfaces) changes - Testing strategy adequately addresses listed risks - Newly-added code is easy to change - Release note makes sense to a user of the library - If necessary, author has acknowledged and discussed the performance implications of this PR as reported in the benchmarks PR comment - Backport labels are set in a manner that is consistent with the [release branch maintenance policy](https://ddtrace.readthedocs.io/en/latest/contributing.html#backporting)
1 parent 563f14b commit 57bc9cd

File tree

7 files changed

+34
-12
lines changed

7 files changed

+34
-12
lines changed

ddtrace/appsec/_asm_request_context.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ class ASM_Environment:
8181
It is contained into a ContextVar.
8282
"""
8383

84-
def __init__(self, span: Optional[Span] = None):
84+
def __init__(self, span: Optional[Span] = None, rc_products: str = ""):
8585
self.root = not in_asm_context()
8686
if self.root:
8787
core.add_suppress_exception(BlockingException)
@@ -105,6 +105,7 @@ def __init__(self, span: Optional[Span] = None):
105105
self.blocked: Optional[Dict[str, Any]] = None
106106
self.finalized: bool = False
107107
self.api_security_reported: int = 0
108+
self.rc_products: str = rc_products
108109

109110

110111
def _get_asm_context() -> Optional[ASM_Environment]:
@@ -260,6 +261,8 @@ def finalize_asm_env(env: ASM_Environment) -> None:
260261
res_headers = waf_adresses.get(SPAN_DATA_NAMES.RESPONSE_HEADERS_NO_COOKIES, {})
261262
if res_headers:
262263
_set_headers(root_span, res_headers, kind="response")
264+
if env.rc_products:
265+
root_span.set_tag_str(APPSEC.RC_PRODUCTS, env.rc_products)
263266

264267
core.discard_local_item(_ASM_CONTEXT)
265268

@@ -513,10 +516,10 @@ def store_waf_results_data(data) -> None:
513516
env.waf_triggers.extend(data)
514517

515518

516-
def start_context(span: Span):
519+
def start_context(span: Span, rc_products: str):
517520
if asm_config._asm_enabled:
518521
# it should only be called at start of a core context, when ASM_Env is not set yet
519-
core.set_item(_ASM_CONTEXT, ASM_Environment(span=span))
522+
core.set_item(_ASM_CONTEXT, ASM_Environment(span=span, rc_products=rc_products))
520523
asm_request_context_set(
521524
core.get_local_item("remote_addr"),
522525
core.get_local_item("headers"),

ddtrace/appsec/_constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ class APPSEC(metaclass=Constant_Class):
6666
RASP_DURATION_EXT: Literal["_dd.appsec.rasp.duration_ext"] = "_dd.appsec.rasp.duration_ext"
6767
RASP_RULE_EVAL: Literal["_dd.appsec.rasp.rule.eval"] = "_dd.appsec.rasp.rule.eval"
6868
RASP_TIMEOUTS: Literal["_dd.appsec.rasp.timeout"] = "_dd.appsec.rasp.timeout"
69+
RC_PRODUCTS: Literal["_dd.appsec.rc_products"] = "_dd.appsec.rc_products"
6970
TRUNCATION_STRING_LENGTH: Literal["_dd.appsec.truncated.string_length"] = "_dd.appsec.truncated.string_length"
7071
TRUNCATION_CONTAINER_SIZE: Literal["_dd.appsec.truncated.container_size"] = "_dd.appsec.truncated.container_size"
7172
TRUNCATION_CONTAINER_DEPTH: Literal["_dd.appsec.truncated.container_depth"] = "_dd.appsec.truncated.container_depth"

ddtrace/appsec/_ddwaf/waf.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from typing import List
66
from typing import Optional
77
from typing import Sequence
8+
from typing import Set
89
from typing import Tuple
910

1011
from ddtrace.appsec._constants import DEFAULT
@@ -77,6 +78,8 @@ def __init__(
7778
)
7879
self._default_ruleset = ruleset_map_object
7980
metrics.ddwaf_version = version()
81+
self._rc_products: Dict[str, Set[str]] = {}
82+
self._rc_products_str: str = ""
8083

8184
@property
8285
def required_data(self) -> List[str]:
@@ -115,9 +118,13 @@ def update_rules(
115118
ok = True
116119
for product, path in removals:
117120
ok &= py_remove_config(self._builder, path)
121+
self._rc_products.get(product, set()).discard(path)
118122
if product == "ASM_DD":
119123
self._asm_dd_cache.discard(path)
120124
for product, path, rules in updates:
125+
if product not in self._rc_products:
126+
self._rc_products[product] = set()
127+
self._rc_products[product].add(path)
121128
if product == "ASM_DD":
122129
if ASM_DD_DEFAULT in self._asm_dd_cache:
123130
# we need to remove the default ruleset before adding the new one
@@ -138,6 +145,7 @@ def update_rules(
138145
ddwaf_object_free(diagnostics)
139146
self._asm_dd_cache.add(ASM_DD_DEFAULT)
140147
new_handle = py_ddwaf_builder_build_instance(self._builder)
148+
self._rc_products_str = ",".join(f"{p}:{len(v)}" for p, v in self._rc_products.items() if v)
141149
if new_handle:
142150
self._handle = new_handle
143151
return ok
@@ -146,6 +154,7 @@ def _at_request_start(self) -> Optional[ddwaf_context_capsule]:
146154
ctx = None
147155
if self._handle:
148156
ctx = py_ddwaf_context_init(self._handle)
157+
ctx.rc_products = self._rc_products_str
149158
if not ctx:
150159
LOGGER.debug("DDWaf._at_request_start: failure to create the context.")
151160
return ctx

ddtrace/appsec/_ddwaf/waf_stubs.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ class ddwaf_context_capsule(Generic[T]):
4949
def __init__(self, ctx: Type[T], free_fn: Callable[[Type[T]], None]) -> None:
5050
self.ctx = ctx
5151
self.free_fn = free_fn
52+
self.rc_products: str = ""
5253

5354
def __del__(self):
5455
if self.ctx:

ddtrace/appsec/_processor.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -181,18 +181,18 @@ def on_span_start(self, span: Span) -> None:
181181
if span.span_type not in asm_config._asm_processed_span_types:
182182
return
183183

184-
_asm_request_context.start_context(span)
185-
186184
ctx = self._ddwaf._at_request_start()
185+
_asm_request_context.start_context(span, ctx.rc_products if ctx is not None else "")
187186
peer_ip = _asm_request_context.get_ip()
188187
headers = _asm_request_context.get_headers()
189188
headers_case_sensitive = _asm_request_context.get_headers_case_sensitive()
189+
root_span = span._local_root or span
190190

191-
span.set_metric(APPSEC.ENABLED, 1.0)
192-
span.set_tag_str(_RUNTIME_FAMILY, "python")
191+
root_span.set_metric(APPSEC.ENABLED, 1.0)
192+
root_span.set_tag_str(_RUNTIME_FAMILY, "python")
193193

194194
def waf_callable(custom_data=None, **kwargs):
195-
return self._waf_action(span._local_root or span, ctx, custom_data, **kwargs)
195+
return self._waf_action(root_span, ctx, custom_data, **kwargs)
196196

197197
_asm_request_context.set_waf_callback(waf_callable)
198198
_asm_request_context.add_context_callback(self.metrics._set_waf_request_metrics)

tests/appsec/appsec/test_processor.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,7 @@ def test_ip_update_rules_and_block(tracer):
258258

259259
assert get_waf_addresses("http.request.remote_ip") == rules._IP.BLOCKED
260260
assert is_blocked(span1)
261+
assert (span._local_root or span).get_tag(APPSEC.RC_PRODUCTS) == "ASM:1"
261262

262263

263264
def test_ip_update_rules_expired_no_block(tracer):
@@ -290,6 +291,7 @@ def test_ip_update_rules_expired_no_block(tracer):
290291

291292
assert get_waf_addresses("http.request.remote_ip") == rules._IP.BLOCKED
292293
assert is_blocked(span) is False
294+
assert (span._local_root or span).get_tag(APPSEC.RC_PRODUCTS) == "ASM:1"
293295

294296

295297
@snapshot(
@@ -658,7 +660,7 @@ def test_asm_context_registration(tracer):
658660
CUSTOM_RULE_METHOD = [
659661
(
660662
"ASM",
661-
"Datadog/1/ASM/data",
663+
"Datadog/3421/ASM/data",
662664
{
663665
"custom_rules": [
664666
{
@@ -743,11 +745,11 @@ def test_ephemeral_addresses(mock_run, persistent, ephemeral):
743745
from ddtrace.appsec._utils import _observator
744746
from ddtrace.trace import tracer
745747

746-
processor = AppSecSpanProcessor()
747-
processor._update_rules([], CUSTOM_RULE_METHOD)
748748
mock_run.return_value = DDWaf_result(0, [], {}, 0.0, 0.0, False, _observator(), {})
749749

750-
with asm_context(tracer=tracer, config=config_asm) as span:
750+
with asm_context(tracer=tracer, config=config_asm, rc_payload=CUSTOM_RULE_METHOD) as span:
751+
processor = tracer._appsec_processor
752+
assert processor
751753
# first call must send all data to the waf
752754
processor._waf_action(span, None, {persistent: {"key_1": "value_1"}, ephemeral: {"key_2": "value_2"}})
753755
assert mock_run.call_args[0][1] == {WAF_DATA_NAMES[persistent]: {"key_1": "value_1"}}
@@ -758,3 +760,4 @@ def test_ephemeral_addresses(mock_run, persistent, ephemeral):
758760
assert mock_run.call_args[1]["ephemeral_data"] == {
759761
WAF_DATA_NAMES[ephemeral]: {"key_2": "value_3"},
760762
}
763+
assert (span._local_root or span).get_tag(APPSEC.RC_PRODUCTS) == "ASM:1"

tests/appsec/utils.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ def asm_context(
5959
block_request_callable: typing.Optional[typing.Callable[[], bool]] = None,
6060
service: typing.Optional[str] = None,
6161
config=None,
62+
rc_payload=None,
6263
) -> typing.Iterator[Span]:
6364
with override_global_config(config) if config else contextlib.nullcontext():
6465
if tracer is None:
@@ -68,6 +69,10 @@ def asm_context(
6869
tracer._span_aggregator.writer._api_version = "v0.4"
6970
tracer._recreate()
7071
patch_for_waf_addresses()
72+
if rc_payload:
73+
processor = tracer._appsec_processor
74+
if processor:
75+
processor._update_rules([], rc_payload)
7176
with core.context_with_data(
7277
"test.asm",
7378
remote_addr=ip_addr,

0 commit comments

Comments
 (0)