Skip to content

Commit 7bfeeb0

Browse files
chore(aap): appsec span processor without import cycles (#14244)
This PR introduces: - remove of the `_appsec_processor` in the tracer. The appsec processor is now part of the list of regular processors. This fixes several problems. - change the logic order of how appsec is enabled and loaded This is removing 2 static import cycles, only 2 left (none with appsec). Context: Before that PR, the appsec product was loaded through the tracer, by creating an instance of AppSecSpanProcessor. After that PR, the appsec product or the remote config layer will register the AppSecSpanProcessor into the list of regular span processors of the tracer. **Bonus**: As the appsec processor is not reset anymore at fork time by the respawn of the tracer (because it does not depend on the tracer object anymore), it looks like it's also fixing the gap time we could have where appsec was back to factory setting at work respawn time. APPSEC-57505 ## 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 8fbd304 commit 7bfeeb0

File tree

15 files changed

+193
-146
lines changed

15 files changed

+193
-146
lines changed

.github/workflows/system-tests.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ jobs:
4040
persist-credentials: false
4141
repository: 'DataDog/system-tests'
4242
# Automatically managed, use scripts/update-system-tests-version to update
43-
ref: '798937b84f21558f301e841cf88c81ee961d2f34'
43+
ref: 'cc0f0a64853f98d0248b682d633d227fa41f4a6e'
4444

4545
- name: Checkout dd-trace-py
4646
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
@@ -90,7 +90,7 @@ jobs:
9090
persist-credentials: false
9191
repository: 'DataDog/system-tests'
9292
# Automatically managed, use scripts/update-system-tests-version to update
93-
ref: '798937b84f21558f301e841cf88c81ee961d2f34'
93+
ref: 'cc0f0a64853f98d0248b682d633d227fa41f4a6e'
9494

9595
- name: Build runner
9696
uses: ./.github/actions/install_runner
@@ -273,7 +273,7 @@ jobs:
273273
persist-credentials: false
274274
repository: 'DataDog/system-tests'
275275
# Automatically managed, use scripts/update-system-tests-version to update
276-
ref: '798937b84f21558f301e841cf88c81ee961d2f34'
276+
ref: 'cc0f0a64853f98d0248b682d633d227fa41f4a6e'
277277
- name: Checkout dd-trace-py
278278
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
279279
with:

ddtrace/_trace/tracer.py

Lines changed: 7 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,10 @@
77
import os
88
from os import getpid
99
from threading import RLock
10-
from typing import Any
1110
from typing import Callable
1211
from typing import Dict
1312
from typing import List
1413
from typing import Optional
15-
from typing import Tuple
1614
from typing import TypeVar
1715
from typing import Union
1816
from typing import cast
@@ -69,42 +67,20 @@
6967
AnyCallable = TypeVar("AnyCallable", bound=Callable)
7068

7169

72-
def _start_appsec_processor():
73-
try:
74-
from ddtrace.appsec._processor import AppSecSpanProcessor
75-
76-
return AppSecSpanProcessor()
77-
except Exception as e:
78-
# DDAS-001-01
79-
log.error(
80-
"[DDAS-001-01] "
81-
"AppSec could not start because of an unexpected error. No security activities will "
82-
"be collected. "
83-
"Please contact support at https://docs.datadoghq.com/help/ for help. Error details: "
84-
"\n%s",
85-
repr(e),
86-
)
87-
if config._raise:
88-
raise
89-
return None
90-
91-
9270
def _default_span_processors_factory(
9371
profiling_span_processor: EndpointCallCounterProcessor,
94-
) -> Tuple[List[SpanProcessor], Any]:
72+
) -> List[SpanProcessor]:
9573
"""Construct the default list of span processors to use."""
9674
span_processors: List[SpanProcessor] = []
9775
span_processors += [TopLevelSpanProcessor()]
9876

99-
appsec_processor = None
10077
if asm_config._asm_libddwaf_available:
10178
if asm_config._asm_enabled:
10279
if asm_config._api_security_enabled:
10380
from ddtrace.appsec._api_security.api_manager import APIManager
10481

10582
APIManager.enable()
10683

107-
appsec_processor = _start_appsec_processor()
10884
else:
10985
# api_security_active will keep track of the service status of APIManager
11086
# we don't want to import the module if it was not started before due to
@@ -131,7 +107,7 @@ def _default_span_processors_factory(
131107

132108
span_processors.append(profiling_span_processor)
133109

134-
return span_processors, appsec_processor
110+
return span_processors
135111

136112

137113
class Tracer(object):
@@ -181,9 +157,7 @@ def __init__(self) -> None:
181157
config._trace_compute_stats = False
182158
# Direct link to the appsec processor
183159
self._endpoint_call_counter_span_processor = EndpointCallCounterProcessor()
184-
self._span_processors, self._appsec_processor = _default_span_processors_factory(
185-
self._endpoint_call_counter_span_processor
186-
)
160+
self._span_processors = _default_span_processors_factory(self._endpoint_call_counter_span_processor)
187161
self._span_aggregator = SpanAggregator(
188162
partial_flush_enabled=config._partial_flush_enabled,
189163
partial_flush_min_spans=config._partial_flush_min_spans,
@@ -430,7 +404,7 @@ def _recreate(
430404
appsec_enabled=appsec_enabled,
431405
reset_buffer=reset_buffer,
432406
)
433-
self._span_processors, self._appsec_processor = _default_span_processors_factory(
407+
self._span_processors = _default_span_processors_factory(
434408
self._endpoint_call_counter_span_processor,
435409
)
436410

@@ -604,9 +578,7 @@ def _start_span(
604578

605579
# Only call span processors if the tracer is enabled (even if APM opted out)
606580
if self.enabled or asm_config._apm_opt_out:
607-
for p in chain(
608-
self._span_processors, SpanProcessor.__processors__, [self._appsec_processor, self._span_aggregator]
609-
):
581+
for p in chain(self._span_processors, SpanProcessor.__processors__, [self._span_aggregator]):
610582
if p:
611583
p.on_span_start(span)
612584
core.dispatch("trace.span_start", (span,))
@@ -623,9 +595,7 @@ def _on_span_finish(self, span: Span) -> None:
623595

624596
# Only call span processors if the tracer is enabled (even if APM opted out)
625597
if self.enabled or asm_config._apm_opt_out:
626-
for p in chain(
627-
self._span_processors, SpanProcessor.__processors__, [self._appsec_processor, self._span_aggregator]
628-
):
598+
for p in chain(self._span_processors, SpanProcessor.__processors__, [self._span_aggregator]):
629599
if p:
630600
p.on_span_finish(span)
631601

@@ -924,9 +894,7 @@ def shutdown(self, timeout: Optional[float] = None) -> None:
924894
"""
925895
with self._shutdown_lock:
926896
# Thread safety: Ensures tracer is shutdown synchronously
927-
for processor in chain(
928-
self._span_processors, SpanProcessor.__processors__, [self._appsec_processor, self._span_aggregator]
929-
):
897+
for processor in chain(self._span_processors, SpanProcessor.__processors__, [self._span_aggregator]):
930898
if processor:
931899
processor.shutdown(timeout)
932900
self.enabled = False

ddtrace/appsec/_api_security/api_manager.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,12 +78,10 @@ def __init__(self) -> None:
7878
log.debug("%s initialized", self.__class__.__name__)
7979
self._hashtable: collections.OrderedDict[int, float] = collections.OrderedDict()
8080

81-
from ddtrace.appsec import _processor as appsec_processor
8281
import ddtrace.appsec._asm_request_context as _asm_request_context
8382
import ddtrace.appsec._metrics as _metrics
8483

8584
self._asm_context = _asm_request_context
86-
self._appsec_processor = appsec_processor
8785
self._metrics = _metrics
8886

8987
def _stop_service(self) -> None:

ddtrace/appsec/_capabilities.py

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
import base64
22
import enum
3-
from typing import Optional
43

5-
from ddtrace._trace import tracer
6-
from ddtrace.internal import core
74
from ddtrace.settings._config import config
85
from ddtrace.settings.asm import config as asm_config
96

@@ -65,13 +62,12 @@ def _asm_feature_is_required() -> bool:
6562
return (_FEATURE_REQUIRED & flags) != 0
6663

6764

68-
def _rc_capabilities(test_tracer: Optional[tracer.Tracer] = None) -> Flags:
69-
tracer = core.tracer if test_tracer is None else test_tracer
65+
def _rc_capabilities() -> Flags:
7066
value = Flags(0)
7167
if config._remote_config_enabled:
7268
if asm_config._asm_can_be_enabled:
7369
value |= Flags.ASM_ACTIVATION
74-
if tracer._appsec_processor and asm_config._asm_static_rule_file is None: # type: ignore
70+
if asm_config._asm_enabled and asm_config._asm_static_rule_file is None:
7571
value |= _ALL_ASM_BLOCKING
7672
if asm_config._ep_enabled:
7773
value |= _ALL_RASP
@@ -80,7 +76,7 @@ def _rc_capabilities(test_tracer: Optional[tracer.Tracer] = None) -> Flags:
8076
return value
8177

8278

83-
def _appsec_rc_capabilities(test_tracer: Optional[tracer.Tracer] = None) -> str:
79+
def _appsec_rc_capabilities() -> str:
8480
r"""return the bit representation of the composed capabilities in base64
8581
bit 0: Reserved
8682
bit 1: ASM 1-click Activation
@@ -96,5 +92,5 @@ def _appsec_rc_capabilities(test_tracer: Optional[tracer.Tracer] = None) -> str:
9692
...
9793
256 -> 100000000 -> b'\x01\x00' -> b'AQA='
9894
"""
99-
value = _rc_capabilities(test_tracer=test_tracer)
95+
value = _rc_capabilities()
10096
return base64.b64encode(value.to_bytes((value.bit_length() + 7) // 8, "big")).decode()

ddtrace/appsec/_common_module_patches.py

Lines changed: 22 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -75,16 +75,27 @@ def _must_block(actions: Iterable[str]) -> bool:
7575
return any(action in (WAF_ACTIONS.BLOCK_ACTION, WAF_ACTIONS.REDIRECT_ACTION) for action in actions)
7676

7777

78+
def _get_rasp_capability(capability: str) -> bool:
79+
"""Check if the RASP capability is enabled."""
80+
if asm_config._asm_enabled and asm_config._ep_enabled:
81+
from ddtrace.appsec._asm_request_context import in_asm_context
82+
83+
if not in_asm_context():
84+
return False
85+
86+
from ddtrace.appsec._processor import AppSecSpanProcessor
87+
88+
return AppSecSpanProcessor._instance is not None and getattr(
89+
AppSecSpanProcessor._instance, f"rasp_{capability}_enabled", False
90+
)
91+
return False
92+
93+
7894
def wrapped_open_CFDDB7ABBA9081B6(original_open_callable, instance, args, kwargs):
7995
"""
8096
wrapper for open file function
8197
"""
82-
if (
83-
asm_config._asm_enabled
84-
and asm_config._ep_enabled
85-
and core.tracer._appsec_processor is not None
86-
and core.tracer._appsec_processor.rasp_lfi_enabled
87-
):
98+
if _get_rasp_capability("lfi"):
8899
try:
89100
from ddtrace.appsec._asm_request_context import call_waf_callback
90101
from ddtrace.appsec._asm_request_context import in_asm_context
@@ -131,12 +142,7 @@ def wrapped_open_ED4CF71136E15EBF(original_open_callable, instance, args, kwargs
131142
# TODO: IAST SSRF sink to be added
132143
pass
133144

134-
if (
135-
asm_config._asm_enabled
136-
and asm_config._ep_enabled
137-
and core.tracer._appsec_processor is not None
138-
and core.tracer._appsec_processor.rasp_ssrf_enabled
139-
):
145+
if _get_rasp_capability("ssrf"):
140146
try:
141147
from ddtrace.appsec._asm_request_context import call_waf_callback
142148
from ddtrace.appsec._asm_request_context import in_asm_context
@@ -175,12 +181,7 @@ def wrapped_request_D8CB81E472AF98A2(original_request_callable, instance, args,
175181

176182
_iast_report_ssrf(original_request_callable, *args, **kwargs)
177183

178-
if (
179-
asm_config._asm_enabled
180-
and asm_config._ep_enabled
181-
and core.tracer._appsec_processor is not None
182-
and core.tracer._appsec_processor.rasp_ssrf_enabled
183-
):
184+
if _get_rasp_capability("ssrf"):
184185
try:
185186
from ddtrace.appsec._asm_request_context import call_waf_callback
186187
from ddtrace.appsec._asm_request_context import in_asm_context
@@ -211,12 +212,7 @@ def wrapped_system_5542593D237084A7(command: str) -> None:
211212
"""
212213
wrapper for os.system function
213214
"""
214-
if (
215-
asm_config._asm_enabled
216-
and asm_config._ep_enabled
217-
and core.tracer._appsec_processor is not None # type: ignore
218-
and core.tracer._appsec_processor.rasp_shi_enabled # type: ignore
219-
):
215+
if _get_rasp_capability("shi"):
220216
try:
221217
from ddtrace.appsec._asm_request_context import call_waf_callback
222218
from ddtrace.appsec._asm_request_context import in_asm_context
@@ -242,12 +238,7 @@ def popen_FD233052260D8B4D(arg_list: Union[List[str], str]) -> None:
242238
"""
243239
listener for subprocess.Popen class
244240
"""
245-
if (
246-
asm_config._asm_enabled
247-
and asm_config._ep_enabled
248-
and core.tracer._appsec_processor is not None # type: ignore
249-
and core.tracer._appsec_processor.rasp_cmdi_enabled # type: ignore
250-
):
241+
if _get_rasp_capability("cmdi"):
251242
try:
252243
from ddtrace.appsec._asm_request_context import call_waf_callback
253244
from ddtrace.appsec._asm_request_context import in_asm_context
@@ -287,12 +278,7 @@ def execute_4C9BAC8E228EB347(instrument_self, query, args, kwargs) -> None:
287278
parameters are ignored as they are properly handled by the dbapi without risk of injections
288279
"""
289280

290-
if (
291-
asm_config._asm_enabled
292-
and asm_config._ep_enabled
293-
and core.tracer._appsec_processor is not None # type: ignore
294-
and core.tracer._appsec_processor.rasp_sqli_enabled # type: ignore
295-
):
281+
if _get_rasp_capability("sqli"):
296282
try:
297283
from ddtrace.appsec._asm_request_context import call_waf_callback
298284
from ddtrace.appsec._asm_request_context import in_asm_context

ddtrace/appsec/_listeners.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,24 @@
1+
import sys
2+
13
from ddtrace.internal import core
4+
from ddtrace.settings.asm import config as asm_config
25

36

47
_APPSEC_TO_BE_LOADED = True
58

69

7-
def load_appsec():
10+
def _asm_switch_state() -> None:
11+
if asm_config._asm_enabled:
12+
from ddtrace.appsec._processor import AppSecSpanProcessor
13+
14+
AppSecSpanProcessor.enable()
15+
elif "ddtrace.appsec._processor" in sys.modules:
16+
from ddtrace.appsec._processor import AppSecSpanProcessor
17+
18+
AppSecSpanProcessor.disable()
19+
20+
21+
def load_appsec() -> None:
822
"""Lazily load the appsec module listeners."""
923
from ddtrace.appsec._asm_request_context import asm_listen
1024
from ddtrace.appsec._handlers import listen
@@ -15,7 +29,12 @@ def load_appsec():
1529
listen()
1630
trace_listen()
1731
asm_listen()
32+
core.on("asm.switch_state", _asm_switch_state)
1833
_APPSEC_TO_BE_LOADED = False
34+
if asm_config._asm_enabled:
35+
from ddtrace.appsec._processor import AppSecSpanProcessor
36+
37+
AppSecSpanProcessor.enable()
1938

2039

2140
def load_common_appsec_modules():

0 commit comments

Comments
 (0)