Skip to content

Commit e33ac7c

Browse files
authored
chore(iast): improve iast logs [backport 2.21] (#12747)
Backport #12716 to 2.21. This refactor of IAST log messages includes: - most of the logs have prefixes but no normalized, such as "IAST: ...", "[IAST] ...", "IAST ...". - Create a hierarchy/namespaces of IAST features (instrumentation, metrics, propagation) and sub-levels: ``` iast::instrumentation:: iast::instrumentation::ast_patching::ast:: iast::instrumentation::ast_patching::compiling:: iast::instrumentation::wrapt:: iast::metrics::error:: iast::propagation::context:: iast::propagation::listener:: iast::propagation::sink_point:: iast::propagation::native:: iast::propagation::native::error:: ``` With all these levels, filtering and grouping messages becomes easier, both for use in dashboards and for testing A lot of logs have been added to analyze the IAST instrumentation process in depth This PR is a cherry-pick of one of the commits of this PR #12639 (cherry picked from commit bfeef47) ## 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 69249c2 commit e33ac7c

35 files changed

+251
-179
lines changed

ddtrace/appsec/_iast/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ def enable_iast_propagation():
9898

9999
get_package_distributions()
100100

101-
log.debug("IAST enabled")
101+
log.debug("iast::instrumentation::starting IAST")
102102
ModuleWatchdog.register_pre_exec_module_hook(_should_iast_patch, _exec_iast_patched_module)
103103
_iast_propagation_enabled = True
104104

ddtrace/appsec/_iast/_ast/ast_patching.py

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
from ddtrace.internal.packages import get_package_distributions
2121
from ddtrace.internal.utils.formats import asbool
2222

23+
from .._logs import iast_ast_debug_log
24+
from .._logs import iast_compiling_debug_log
2325
from .visitor import AstVisitor
2426

2527

@@ -478,28 +480,32 @@ def _should_iast_patch(module_name: Text) -> bool:
478480
# diff = max_allow - max_deny
479481
# return diff > 0 or (diff == 0 and not _in_python_stdlib_or_third_party(module_name))
480482
if _in_python_stdlib(module_name):
481-
log.debug("IAST: denying %s. it's in the _in_python_stdlib", module_name)
483+
iast_ast_debug_log(f"denying {module_name}. it's in the python_stdlib")
482484
return False
483485

484486
if _is_first_party(module_name):
487+
iast_ast_debug_log(f"allowing {module_name}. it's a first party module")
485488
return True
486489

487490
# else: third party. Check that is in the allow list and not in the deny list
488491
dotted_module_name = module_name.lower() + "."
489492

490493
# User allow or deny list set by env var have priority
491494
if _trie_has_prefix_for(_TRIE_USER_ALLOWLIST, dotted_module_name):
495+
iast_ast_debug_log(f"allowing {module_name}. it's in the USER_ALLOWLIST")
492496
return True
493497

494498
if _trie_has_prefix_for(_TRIE_USER_DENYLIST, dotted_module_name):
499+
iast_ast_debug_log(f"denying {module_name}. it's in the USER_DENYLIST")
495500
return False
496501

497502
if _trie_has_prefix_for(_TRIE_ALLOWLIST, dotted_module_name):
498503
if _trie_has_prefix_for(_TRIE_DENYLIST, dotted_module_name):
504+
iast_ast_debug_log(f"denying {module_name}. it's in the DENYLIST")
499505
return False
500-
log.debug("IAST: allowing %s. it's in the IAST_ALLOWLIST", module_name)
506+
iast_ast_debug_log(f"allowing {module_name}. it's in the ALLOWLIST")
501507
return True
502-
log.debug("IAST: denying %s. it's in the IAST_DENYLIST", module_name)
508+
iast_ast_debug_log(f"denying {module_name}. it's NOT in the ALLOWLIST")
503509
return False
504510

505511

@@ -557,17 +563,17 @@ def astpatch_module(module: ModuleType) -> Tuple[str, Optional[ast.Module]]:
557563

558564
module_origin = origin(module)
559565
if module_origin is None:
560-
log.debug("astpatch_source couldn't find the module: %s", module_name)
566+
iast_compiling_debug_log(f"could not find the module: {module_name}")
561567
return "", None
562568

563569
module_path = str(module_origin)
564570
try:
565571
if module_origin.stat().st_size == 0:
566572
# Don't patch empty files like __init__.py
567-
log.debug("empty file: %s", module_path)
573+
iast_compiling_debug_log(f"empty file: {module_path}")
568574
return "", None
569575
except OSError:
570-
log.debug("astpatch_source couldn't find the file: %s", module_path, exc_info=True)
576+
iast_compiling_debug_log(f"could not find the file: {module_path}", exc_info=True)
571577
return "", None
572578

573579
# Get the file extension, if it's dll, os, pyd, dyn, dynlib: return
@@ -577,19 +583,22 @@ def astpatch_module(module: ModuleType) -> Tuple[str, Optional[ast.Module]]:
577583

578584
if module_ext.lower() not in {".pyo", ".pyc", ".pyw", ".py"}:
579585
# Probably native or built-in module
580-
log.debug("extension not supported: %s for: %s", module_ext, module_path)
586+
iast_compiling_debug_log(f"Extension not supported: {module_ext} for: {module_path}")
581587
return "", None
582588

583589
with open(module_path, "r", encoding=get_encoding(module_path)) as source_file:
584590
try:
585591
source_text = source_file.read()
586592
except UnicodeDecodeError:
587-
log.debug("unicode decode error for file: %s", module_path, exc_info=True)
593+
iast_compiling_debug_log(f"Encode decode error for file: {module_path}", exc_info=True)
594+
return "", None
595+
except Exception:
596+
iast_compiling_debug_log(f"Unexpected read error: {module_path}", exc_info=True)
588597
return "", None
589598

590599
if len(source_text.strip()) == 0:
591600
# Don't patch empty files like __init__.py
592-
log.debug("empty file: %s", module_path)
601+
iast_compiling_debug_log(f"Empty file: {module_path}")
593602
return "", None
594603

595604
if not asbool(os.environ.get(IAST.ENV_NO_DIR_PATCH, "false")) and version_info > (3, 7):
@@ -603,7 +612,7 @@ def astpatch_module(module: ModuleType) -> Tuple[str, Optional[ast.Module]]:
603612
module_name=module_name,
604613
)
605614
if new_ast is None:
606-
log.debug("file not ast patched: %s", module_path)
615+
iast_compiling_debug_log(f"file not ast patched: {module_path}")
607616
return "", None
608617

609618
return module_path, new_ast

ddtrace/appsec/_iast/_handlers.py

Lines changed: 65 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
from ddtrace.settings.asm import config as asm_config
1919

2020
from ._iast_request_context import is_iast_request_enabled
21+
from ._logs import iast_instrumentation_wrapt_debug_log
22+
from ._logs import iast_propagation_listener_log_log
2123
from ._taint_tracking._taint_objects import taint_pyobject
2224

2325

@@ -70,68 +72,73 @@ def _on_request_init(wrapped, instance, args, kwargs):
7072
source_origin=OriginType.PATH,
7173
)
7274
except Exception:
73-
log.debug("Unexpected exception while tainting pyobject", exc_info=True)
75+
iast_propagation_listener_log_log("Unexpected exception while tainting pyobject", exc_info=True)
7476

7577

7678
def _on_flask_patch(flask_version):
7779
if asm_config._iast_enabled:
78-
try_wrap_function_wrapper(
79-
"werkzeug.datastructures",
80-
"Headers.items",
81-
functools.partial(if_iast_taint_yield_tuple_for, (OriginType.HEADER_NAME, OriginType.HEADER)),
82-
)
83-
_set_metric_iast_instrumented_source(OriginType.HEADER_NAME)
84-
_set_metric_iast_instrumented_source(OriginType.HEADER)
80+
try:
81+
try_wrap_function_wrapper(
82+
"werkzeug.datastructures",
83+
"Headers.items",
84+
functools.partial(if_iast_taint_yield_tuple_for, (OriginType.HEADER_NAME, OriginType.HEADER)),
85+
)
86+
_set_metric_iast_instrumented_source(OriginType.HEADER_NAME)
87+
_set_metric_iast_instrumented_source(OriginType.HEADER)
8588

86-
try_wrap_function_wrapper(
87-
"werkzeug.datastructures",
88-
"ImmutableMultiDict.__getitem__",
89-
functools.partial(if_iast_taint_returned_object_for, OriginType.PARAMETER),
90-
)
91-
_set_metric_iast_instrumented_source(OriginType.PARAMETER)
89+
try_wrap_function_wrapper(
90+
"werkzeug.datastructures",
91+
"ImmutableMultiDict.__getitem__",
92+
functools.partial(if_iast_taint_returned_object_for, OriginType.PARAMETER),
93+
)
94+
_set_metric_iast_instrumented_source(OriginType.PARAMETER)
9295

93-
try_wrap_function_wrapper(
94-
"werkzeug.datastructures",
95-
"EnvironHeaders.__getitem__",
96-
functools.partial(if_iast_taint_returned_object_for, OriginType.HEADER),
97-
)
98-
_set_metric_iast_instrumented_source(OriginType.HEADER)
96+
try_wrap_function_wrapper(
97+
"werkzeug.datastructures",
98+
"EnvironHeaders.__getitem__",
99+
functools.partial(if_iast_taint_returned_object_for, OriginType.HEADER),
100+
)
101+
_set_metric_iast_instrumented_source(OriginType.HEADER)
99102

100-
if flask_version >= (2, 0, 0):
101-
# instance.query_string: raising an error on werkzeug/_internal.py "AttributeError: read only property"
102-
try_wrap_function_wrapper("werkzeug.wrappers.request", "Request.__init__", _on_request_init)
103+
if flask_version >= (2, 0, 0):
104+
# instance.query_string: raising an error on werkzeug/_internal.py "AttributeError: read only property"
105+
try_wrap_function_wrapper("werkzeug.wrappers.request", "Request.__init__", _on_request_init)
106+
107+
_set_metric_iast_instrumented_source(OriginType.PATH)
108+
_set_metric_iast_instrumented_source(OriginType.QUERY)
103109

104-
_set_metric_iast_instrumented_source(OriginType.PATH)
105-
_set_metric_iast_instrumented_source(OriginType.QUERY)
110+
# Instrumented on _ddtrace.appsec._asm_request_context._on_wrapped_view
111+
_set_metric_iast_instrumented_source(OriginType.PATH_PARAMETER)
106112

107-
# Instrumented on _ddtrace.appsec._asm_request_context._on_wrapped_view
108-
_set_metric_iast_instrumented_source(OriginType.PATH_PARAMETER)
113+
try_wrap_function_wrapper(
114+
"werkzeug.wrappers.request",
115+
"Request.get_data",
116+
functools.partial(_patched_dictionary, OriginType.BODY, OriginType.BODY),
117+
)
118+
try_wrap_function_wrapper(
119+
"werkzeug.wrappers.request",
120+
"Request.get_json",
121+
functools.partial(_patched_dictionary, OriginType.BODY, OriginType.BODY),
122+
)
109123

110-
try_wrap_function_wrapper(
111-
"werkzeug.wrappers.request",
112-
"Request.get_data",
113-
functools.partial(_patched_dictionary, OriginType.BODY, OriginType.BODY),
114-
)
115-
try_wrap_function_wrapper(
116-
"werkzeug.wrappers.request",
117-
"Request.get_json",
118-
functools.partial(_patched_dictionary, OriginType.BODY, OriginType.BODY),
119-
)
124+
_set_metric_iast_instrumented_source(OriginType.BODY)
120125

121-
_set_metric_iast_instrumented_source(OriginType.BODY)
126+
if flask_version < (2, 0, 0):
127+
_w(
128+
"werkzeug._internal",
129+
"_DictAccessorProperty.__get__",
130+
functools.partial(if_iast_taint_returned_object_for, OriginType.QUERY),
131+
)
132+
_set_metric_iast_instrumented_source(OriginType.QUERY)
122133

123-
if flask_version < (2, 0, 0):
124-
_w(
125-
"werkzeug._internal",
126-
"_DictAccessorProperty.__get__",
127-
functools.partial(if_iast_taint_returned_object_for, OriginType.QUERY),
128-
)
129-
_set_metric_iast_instrumented_source(OriginType.QUERY)
134+
# Instrumented on _on_set_request_tags_iast
135+
_set_metric_iast_instrumented_source(OriginType.COOKIE_NAME)
136+
_set_metric_iast_instrumented_source(OriginType.COOKIE)
137+
_set_metric_iast_instrumented_source(OriginType.PARAMETER_NAME)
130138

131-
# Instrumented on _on_set_request_tags_iast
132-
_set_metric_iast_instrumented_source(OriginType.COOKIE_NAME)
133-
_set_metric_iast_instrumented_source(OriginType.COOKIE)
134-
_set_metric_iast_instrumented_source(OriginType.PARAMETER_NAME)
139+
iast_instrumentation_wrapt_debug_log("Patching flask correctly")
140+
except Exception:
141+
iast_instrumentation_wrapt_debug_log("Unexpected exception while patching Flask", exc_info=True)
135142

136143

137144
def _iast_on_wrapped_view(kwargs):
@@ -173,9 +180,9 @@ def _on_django_patch():
173180
functools.partial(if_iast_taint_returned_object_for, OriginType.PARAMETER),
174181
)
175182
)
176-
log.debug("[IAST] Patching Django correctly")
183+
iast_instrumentation_wrapt_debug_log("Patching Django correctly")
177184
except Exception:
178-
log.debug("[IAST] Unexpected exception while patch Django", exc_info=True)
185+
iast_instrumentation_wrapt_debug_log("Unexpected exception while patching Django", exc_info=True)
179186

180187

181188
def _on_django_func_wrapped(fn_args, fn_kwargs, first_arg_expected_type, *_):
@@ -215,7 +222,7 @@ def _on_django_func_wrapped(fn_args, fn_kwargs, first_arg_expected_type, *_):
215222
source_origin=OriginType.BODY,
216223
)
217224
except AttributeError:
218-
log.debug("IAST can't set attribute http_req.body", exc_info=True)
225+
iast_propagation_listener_log_log("IAST can't set attribute http_req.body", exc_info=True)
219226

220227
http_req.GET = taint_structure(http_req.GET, OriginType.PARAMETER_NAME, OriginType.PARAMETER)
221228
http_req.POST = taint_structure(http_req.POST, OriginType.PARAMETER_NAME, OriginType.BODY)
@@ -243,7 +250,9 @@ def _on_django_func_wrapped(fn_args, fn_kwargs, first_arg_expected_type, *_):
243250
v, source_name=k, source_value=v, source_origin=OriginType.PATH_PARAMETER
244251
)
245252
except Exception:
246-
log.debug("IAST: Unexpected exception while tainting path parameters", exc_info=True)
253+
iast_propagation_listener_log_log(
254+
"IAST: Unexpected exception while tainting path parameters", exc_info=True
255+
)
247256

248257

249258
def _custom_protobuf_getattribute(self, name):
@@ -304,7 +313,7 @@ def if_iast_taint_yield_tuple_for(origins, wrapped, instance, args, kwargs):
304313
)
305314
yield new_key, new_value
306315
except Exception:
307-
log.debug("Unexpected exception while tainting pyobject", exc_info=True)
316+
iast_propagation_listener_log_log("Unexpected exception while tainting pyobject", exc_info=True)
308317
else:
309318
for key, value in wrapped(*args, **kwargs):
310319
yield key, value
@@ -320,7 +329,7 @@ def if_iast_taint_returned_object_for(origin, wrapped, instance, args, kwargs):
320329
origin = OriginType.COOKIE
321330
return taint_pyobject(pyobject=value, source_name=name, source_value=value, source_origin=origin)
322331
except Exception:
323-
log.debug("Unexpected exception while tainting pyobject", exc_info=True)
332+
iast_propagation_listener_log_log("Unexpected exception while tainting pyobject", exc_info=True)
324333
return value
325334

326335

@@ -343,7 +352,7 @@ def if_iast_taint_starlette_datastructures(origin, wrapped, instance, args, kwar
343352
res.append(element)
344353
return res
345354
except Exception:
346-
log.debug("Unexpected exception while tainting pyobject", exc_info=True)
355+
iast_propagation_listener_log_log("Unexpected exception while tainting pyobject", exc_info=True)
347356
return value
348357

349358

ddtrace/appsec/_iast/_iast_request_context.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ def set_iast_reporter(iast_reporter: IastSpanReporter) -> None:
7878
if env:
7979
env.iast_reporter = iast_reporter
8080
else:
81-
log.debug("[IAST] Trying to set IAST reporter but no context is present")
81+
log.debug("iast::propagation::context::Trying to set IAST reporter but no context is present")
8282

8383

8484
def get_iast_reporter() -> Optional[IastSpanReporter]:
@@ -101,7 +101,7 @@ def set_iast_request_enabled(request_enabled) -> None:
101101
if env:
102102
env.request_enabled = request_enabled
103103
else:
104-
log.debug("[IAST] Trying to set IAST reporter but no context is present")
104+
log.debug("iast::propagation::context::Trying to set IAST reporter but no context is present")
105105

106106

107107
def is_iast_request_enabled() -> bool:
@@ -149,7 +149,9 @@ def _iast_end_request(ctx=None, span=None, *args, **kwargs):
149149
req_span = span
150150
else:
151151
req_span = ctx.get_item("req_span")
152-
152+
if req_span is None:
153+
log.debug("iast::propagation::context::Error finishing IAST context. There isn't a SPAN")
154+
return
153155
if asm_config._iast_enabled:
154156
existing_data = req_span.get_tag(IAST.JSON)
155157
if existing_data is None:
@@ -168,7 +170,7 @@ def _iast_end_request(ctx=None, span=None, *args, **kwargs):
168170
_create_and_attach_iast_report_to_span(req_span, existing_data, merge=True)
169171

170172
except Exception:
171-
log.debug("[IAST] Error finishing IAST context", exc_info=True)
173+
log.debug("iast::propagation::context::Error finishing IAST context", exc_info=True)
172174

173175

174176
def _iast_start_request(span=None, *args, **kwargs):
@@ -180,4 +182,4 @@ def _iast_start_request(span=None, *args, **kwargs):
180182
request_iast_enabled = True
181183
set_iast_request_enabled(request_iast_enabled)
182184
except Exception:
183-
log.debug("[IAST] Error starting IAST context", exc_info=True)
185+
log.debug("iast::propagation::context::Error starting IAST context", exc_info=True)

ddtrace/appsec/_iast/_loader.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#!/usr/bin/env python3
22

3+
from ddtrace.appsec._iast._logs import iast_compiling_debug_log
34
from ddtrace.internal.logger import get_logger
45
from ddtrace.settings.asm import config as asm_config
56

@@ -18,24 +19,26 @@ def _exec_iast_patched_module(module_watchdog, module):
1819
try:
1920
module_path, patched_ast = astpatch_module(module)
2021
except Exception:
21-
log.debug("Unexpected exception while AST patching", exc_info=True)
22+
iast_compiling_debug_log("Unexpected exception while AST patching", exc_info=True)
2223
patched_ast = None
2324

2425
if patched_ast:
2526
try:
2627
# Patched source is compiled in order to execute it
2728
compiled_code = compile(patched_ast, module_path, "exec")
2829
except Exception:
29-
log.debug("Unexpected exception while compiling patched code", exc_info=True)
30+
iast_compiling_debug_log("Unexpected exception while compiling patched code", exc_info=True)
3031
compiled_code = None
3132

3233
if compiled_code:
34+
iast_compiling_debug_log(f"INSTRUMENTED CODE. executing {module_path}")
3335
# Patched source is executed instead of original module
3436
exec(compiled_code, module.__dict__) # nosec B102
3537
elif module_watchdog.loader is not None:
3638
try:
39+
iast_compiling_debug_log(f"DEFAULT CODE. executing {module}")
3740
module_watchdog.loader.exec_module(module)
3841
except ImportError:
39-
log.debug("Unexpected exception on import loader fallback", exc_info=True)
42+
iast_compiling_debug_log("Unexpected exception on import loader fallback", exc_info=True)
4043
else:
41-
log.debug("Module loader is not available, cannot execute module %s", module)
44+
iast_compiling_debug_log(f"Module loader is not available, cannot execute module {module}")

0 commit comments

Comments
 (0)