Skip to content

Commit d7b40c9

Browse files
authored
chore(iast): improve iast logs [backport 3.1] (#12751)
Backport #12716 to 3.1. 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 8b3d819 commit d7b40c9

36 files changed

+263
-188
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
@@ -19,6 +19,8 @@
1919
from ddtrace.internal.packages import get_package_distributions
2020
from ddtrace.internal.utils.formats import asbool
2121

22+
from .._logs import iast_ast_debug_log
23+
from .._logs import iast_compiling_debug_log
2224
from .visitor import AstVisitor
2325

2426

@@ -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")):
@@ -602,7 +611,7 @@ def astpatch_module(module: ModuleType) -> Tuple[str, Optional[ast.Module]]:
602611
module_name=module_name,
603612
)
604613
if new_ast is None:
605-
log.debug("file not ast patched: %s", module_path)
614+
iast_compiling_debug_log(f"file not ast patched: {module_path}")
606615
return "", None
607616

608617
return module_path, new_ast

ddtrace/appsec/_iast/_handlers.py

Lines changed: 74 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
from ddtrace.internal.logger import get_logger
2020
from ddtrace.settings.asm import config as asm_config
2121

22+
from ._logs import iast_instrumentation_wrapt_debug_log
23+
from ._logs import iast_propagation_listener_log_log
2224
from ._taint_tracking._taint_objects import taint_pyobject
2325

2426

@@ -49,73 +51,78 @@ def _on_request_init(wrapped, instance, args, kwargs):
4951
source_origin=OriginType.PATH,
5052
)
5153
except Exception:
52-
log.debug("Unexpected exception while tainting pyobject", exc_info=True)
54+
iast_propagation_listener_log_log("Unexpected exception while tainting pyobject", exc_info=True)
5355

5456

5557
def _on_flask_patch(flask_version):
5658
if asm_config._iast_enabled:
57-
try_wrap_function_wrapper(
58-
"werkzeug.datastructures",
59-
"Headers.items",
60-
functools.partial(if_iast_taint_yield_tuple_for, (OriginType.HEADER_NAME, OriginType.HEADER)),
61-
)
59+
try:
60+
try_wrap_function_wrapper(
61+
"werkzeug.datastructures",
62+
"Headers.items",
63+
functools.partial(if_iast_taint_yield_tuple_for, (OriginType.HEADER_NAME, OriginType.HEADER)),
64+
)
6265

63-
try_wrap_function_wrapper(
64-
"werkzeug.datastructures",
65-
"EnvironHeaders.__getitem__",
66-
functools.partial(if_iast_taint_returned_object_for, OriginType.HEADER),
67-
)
68-
# Since werkzeug 3.1.0 get doesn't call to __getitem__
69-
try_wrap_function_wrapper(
70-
"werkzeug.datastructures",
71-
"EnvironHeaders.get",
72-
functools.partial(if_iast_taint_returned_object_for, OriginType.HEADER),
73-
)
74-
_set_metric_iast_instrumented_source(OriginType.HEADER_NAME)
75-
_set_metric_iast_instrumented_source(OriginType.HEADER)
66+
try_wrap_function_wrapper(
67+
"werkzeug.datastructures",
68+
"EnvironHeaders.__getitem__",
69+
functools.partial(if_iast_taint_returned_object_for, OriginType.HEADER),
70+
)
71+
# Since werkzeug 3.1.0 get doesn't call to __getitem__
72+
try_wrap_function_wrapper(
73+
"werkzeug.datastructures",
74+
"EnvironHeaders.get",
75+
functools.partial(if_iast_taint_returned_object_for, OriginType.HEADER),
76+
)
77+
_set_metric_iast_instrumented_source(OriginType.HEADER_NAME)
78+
_set_metric_iast_instrumented_source(OriginType.HEADER)
7679

77-
try_wrap_function_wrapper(
78-
"werkzeug.datastructures",
79-
"ImmutableMultiDict.__getitem__",
80-
functools.partial(if_iast_taint_returned_object_for, OriginType.PARAMETER),
81-
)
82-
_set_metric_iast_instrumented_source(OriginType.PARAMETER)
80+
try_wrap_function_wrapper(
81+
"werkzeug.datastructures",
82+
"ImmutableMultiDict.__getitem__",
83+
functools.partial(if_iast_taint_returned_object_for, OriginType.PARAMETER),
84+
)
85+
_set_metric_iast_instrumented_source(OriginType.PARAMETER)
8386

84-
if flask_version >= (2, 0, 0):
85-
# instance.query_string: raising an error on werkzeug/_internal.py "AttributeError: read only property"
86-
try_wrap_function_wrapper("werkzeug.wrappers.request", "Request.__init__", _on_request_init)
87+
if flask_version >= (2, 0, 0):
88+
# instance.query_string: raising an error on werkzeug/_internal.py "AttributeError: read only property"
89+
try_wrap_function_wrapper("werkzeug.wrappers.request", "Request.__init__", _on_request_init)
8790

88-
_set_metric_iast_instrumented_source(OriginType.PATH)
89-
_set_metric_iast_instrumented_source(OriginType.QUERY)
91+
_set_metric_iast_instrumented_source(OriginType.PATH)
92+
_set_metric_iast_instrumented_source(OriginType.QUERY)
9093

91-
try_wrap_function_wrapper(
92-
"werkzeug.wrappers.request",
93-
"Request.get_data",
94-
functools.partial(_patched_dictionary, OriginType.BODY, OriginType.BODY),
95-
)
96-
try_wrap_function_wrapper(
97-
"werkzeug.wrappers.request",
98-
"Request.get_json",
99-
functools.partial(_patched_dictionary, OriginType.BODY, OriginType.BODY),
100-
)
94+
try_wrap_function_wrapper(
95+
"werkzeug.wrappers.request",
96+
"Request.get_data",
97+
functools.partial(_patched_dictionary, OriginType.BODY, OriginType.BODY),
98+
)
99+
try_wrap_function_wrapper(
100+
"werkzeug.wrappers.request",
101+
"Request.get_json",
102+
functools.partial(_patched_dictionary, OriginType.BODY, OriginType.BODY),
103+
)
101104

102-
_set_metric_iast_instrumented_source(OriginType.BODY)
105+
_set_metric_iast_instrumented_source(OriginType.BODY)
103106

104-
if flask_version < (2, 0, 0):
105-
_w(
106-
"werkzeug._internal",
107-
"_DictAccessorProperty.__get__",
108-
functools.partial(if_iast_taint_returned_object_for, OriginType.QUERY),
109-
)
110-
_set_metric_iast_instrumented_source(OriginType.QUERY)
107+
if flask_version < (2, 0, 0):
108+
_w(
109+
"werkzeug._internal",
110+
"_DictAccessorProperty.__get__",
111+
functools.partial(if_iast_taint_returned_object_for, OriginType.QUERY),
112+
)
113+
_set_metric_iast_instrumented_source(OriginType.QUERY)
111114

112-
# Instrumented on _ddtrace.appsec._asm_request_context._on_wrapped_view
113-
_set_metric_iast_instrumented_source(OriginType.PATH_PARAMETER)
115+
# Instrumented on _ddtrace.appsec._asm_request_context._on_wrapped_view
116+
_set_metric_iast_instrumented_source(OriginType.PATH_PARAMETER)
114117

115-
# Instrumented on _on_set_request_tags_iast
116-
_set_metric_iast_instrumented_source(OriginType.COOKIE_NAME)
117-
_set_metric_iast_instrumented_source(OriginType.COOKIE)
118-
_set_metric_iast_instrumented_source(OriginType.PARAMETER_NAME)
118+
# Instrumented on _on_set_request_tags_iast
119+
_set_metric_iast_instrumented_source(OriginType.COOKIE_NAME)
120+
_set_metric_iast_instrumented_source(OriginType.COOKIE)
121+
_set_metric_iast_instrumented_source(OriginType.PARAMETER_NAME)
122+
123+
iast_instrumentation_wrapt_debug_log("Patching flask correctly")
124+
except Exception:
125+
iast_instrumentation_wrapt_debug_log("Unexpected exception while patching Flask", exc_info=True)
119126

120127

121128
def _iast_on_wrapped_view(kwargs):
@@ -161,9 +168,9 @@ def _on_django_patch():
161168
_set_metric_iast_instrumented_source(OriginType.PARAMETER)
162169
_set_metric_iast_instrumented_source(OriginType.PARAMETER_NAME)
163170
_set_metric_iast_instrumented_source(OriginType.BODY)
164-
log.debug("[IAST] Patching Django correctly")
171+
iast_instrumentation_wrapt_debug_log("Patching Django correctly")
165172
except Exception:
166-
log.debug("[IAST] Unexpected exception while patch Django", exc_info=True)
173+
iast_instrumentation_wrapt_debug_log("Unexpected exception while patching Django", exc_info=True)
167174

168175

169176
def _on_django_func_wrapped(fn_args, fn_kwargs, first_arg_expected_type, *_):
@@ -203,7 +210,7 @@ def _on_django_func_wrapped(fn_args, fn_kwargs, first_arg_expected_type, *_):
203210
source_origin=OriginType.BODY,
204211
)
205212
except AttributeError:
206-
log.debug("IAST can't set attribute http_req.body", exc_info=True)
213+
iast_propagation_listener_log_log("IAST can't set attribute http_req.body", exc_info=True)
207214

208215
http_req.GET = taint_structure(http_req.GET, OriginType.PARAMETER_NAME, OriginType.PARAMETER)
209216
http_req.POST = taint_structure(http_req.POST, OriginType.PARAMETER_NAME, OriginType.BODY)
@@ -231,7 +238,9 @@ def _on_django_func_wrapped(fn_args, fn_kwargs, first_arg_expected_type, *_):
231238
v, source_name=k, source_value=v, source_origin=OriginType.PATH_PARAMETER
232239
)
233240
except Exception:
234-
log.debug("IAST: Unexpected exception while tainting path parameters", exc_info=True)
241+
iast_propagation_listener_log_log(
242+
"IAST: Unexpected exception while tainting path parameters", exc_info=True
243+
)
235244

236245

237246
def _custom_protobuf_getattribute(self, name):
@@ -292,7 +301,7 @@ def if_iast_taint_yield_tuple_for(origins, wrapped, instance, args, kwargs):
292301
)
293302
yield new_key, new_value
294303
except Exception:
295-
log.debug("Unexpected exception while tainting pyobject", exc_info=True)
304+
iast_propagation_listener_log_log("Unexpected exception while tainting pyobject", exc_info=True)
296305
else:
297306
for key, value in wrapped(*args, **kwargs):
298307
yield key, value
@@ -308,7 +317,7 @@ def if_iast_taint_returned_object_for(origin, wrapped, instance, args, kwargs):
308317
origin = OriginType.COOKIE
309318
return taint_pyobject(pyobject=value, source_name=name, source_value=value, source_origin=origin)
310319
except Exception:
311-
log.debug("Unexpected exception while tainting pyobject", exc_info=True)
320+
iast_propagation_listener_log_log("Unexpected exception while tainting pyobject", exc_info=True)
312321
return value
313322

314323

@@ -331,7 +340,7 @@ def if_iast_taint_starlette_datastructures(origin, wrapped, instance, args, kwar
331340
res.append(element)
332341
return res
333342
except Exception:
334-
log.debug("Unexpected exception while tainting pyobject", exc_info=True)
343+
iast_propagation_listener_log_log("Unexpected exception while tainting pyobject", exc_info=True)
335344
return value
336345

337346

@@ -458,7 +467,7 @@ def _on_django_finalize_response_pre(ctx, after_request_tags, request, response)
458467
content = response.content.decode("utf-8", errors="ignore")
459468
asm_check_stacktrace_leak(content)
460469
except Exception:
461-
log.debug("Unexpected exception checking for stacktrace leak", exc_info=True)
470+
iast_propagation_listener_log_log("Unexpected exception checking for stacktrace leak", exc_info=True)
462471

463472

464473
def _on_django_technical_500_response(request, response, exc_type, exc_value, tb):
@@ -472,7 +481,9 @@ def _on_django_technical_500_response(request, response, exc_type, exc_value, tb
472481
module = tb.tb_frame.f_globals.get("__name__", "")
473482
asm_report_stacktrace_leak_from_django_debug_page(exc_name, module)
474483
except Exception:
475-
log.debug("Unexpected exception checking for stacktrace leak on 500 response view", exc_info=True)
484+
iast_propagation_listener_log_log(
485+
"Unexpected exception checking for stacktrace leak on 500 response view", exc_info=True
486+
)
476487

477488

478489
def _on_flask_finalize_request_post(response, _):

ddtrace/appsec/_iast/_iast_request_context.py

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

8282

8383
def get_iast_reporter() -> Optional[IastSpanReporter]:
@@ -113,7 +113,7 @@ def set_iast_request_enabled(request_enabled) -> None:
113113
if env:
114114
env.request_enabled = request_enabled
115115
else:
116-
log.debug("[IAST] Trying to set IAST reporter but no context is present")
116+
log.debug("iast::propagation::context::Trying to set IAST reporter but no context is present")
117117

118118

119119
def _move_iast_data_to_root_span():
@@ -154,7 +154,9 @@ def _iast_end_request(ctx=None, span=None, *args, **kwargs):
154154
req_span = span
155155
else:
156156
req_span = ctx.get_item("req_span")
157-
157+
if req_span is None:
158+
log.debug("iast::propagation::context::Error finishing IAST context. There isn't a SPAN")
159+
return
158160
if asm_config._iast_enabled:
159161
existing_data = req_span.get_tag(IAST.JSON)
160162
if existing_data is None:
@@ -173,7 +175,7 @@ def _iast_end_request(ctx=None, span=None, *args, **kwargs):
173175
_create_and_attach_iast_report_to_span(req_span, existing_data, merge=True)
174176

175177
except Exception:
176-
log.debug("[IAST] Error finishing IAST context", exc_info=True)
178+
log.debug("iast::propagation::context::Error finishing IAST context", exc_info=True)
177179

178180

179181
def _iast_start_request(span=None, *args, **kwargs):
@@ -185,4 +187,4 @@ def _iast_start_request(span=None, *args, **kwargs):
185187
request_iast_enabled = True
186188
set_iast_request_enabled(request_iast_enabled)
187189
except Exception:
188-
log.debug("[IAST] Error starting IAST context", exc_info=True)
190+
log.debug("iast::propagation::context::Error starting IAST context", exc_info=True)

0 commit comments

Comments
 (0)