Skip to content

Commit

Permalink
feat(asm): enable exploit prevention (#9246)
Browse files Browse the repository at this point in the history
- Enable Exploit Prevention by default (opt out)
- Add span metrics tag for rasp duration and evaluations
- Improve unit tests for rasp to check for span metrics

## Checklist

- [x] Change(s) are motivated and described in the PR description
- [x] Testing strategy is described if automated tests are not included
in the PR
- [x] Risks are described (performance impact, potential for breakage,
maintainability)
- [x] Change is maintainable (easy to change, telemetry, documentation)
- [x] [Library release note
guidelines](https://ddtrace.readthedocs.io/en/stable/releasenotes.html)
are followed or label `changelog/no-changelog` is set
- [x] Documentation is included (in-code, generated user docs, [public
corp docs](https://github.com/DataDog/documentation/))
- [x] Backport labels are set (if
[applicable](https://ddtrace.readthedocs.io/en/latest/contributing.html#backporting))
- [x] If this PR changes the public interface, I've notified
`@DataDog/apm-tees`.

## Reviewer Checklist

- [ ] Title is accurate
- [ ] All changes are related to the pull request's stated goal
- [ ] Description motivates each change
- [ ] Avoids breaking
[API](https://ddtrace.readthedocs.io/en/stable/versioning.html#interfaces)
changes
- [ ] Testing strategy adequately addresses listed risks
- [ ] Change is maintainable (easy to change, telemetry, documentation)
- [ ] Release note makes sense to a user of the library
- [ ] 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)
  • Loading branch information
christophe-papazian authored May 14, 2024
1 parent 3e34d21 commit f682826
Show file tree
Hide file tree
Showing 9 changed files with 74 additions and 19 deletions.
37 changes: 33 additions & 4 deletions ddtrace/appsec/_asm_request_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from typing import List
from typing import Optional
from typing import Set
from typing import Union
from urllib import parse

from ddtrace._trace.span import Span
Expand Down Expand Up @@ -109,9 +110,15 @@ def unregister(span: Span) -> None:
env.must_call_globals = False


def update_span_metrics(span: Span, name: str, value: Union[float, int]) -> None:
span.set_metric(name, value + (span.get_metric(name) or 0.0))


def flush_waf_triggers(env: ASM_Environment) -> None:
if env.waf_triggers and env.span:
root_span = env.span._local_root or env.span
if not env.span:
return
root_span = env.span._local_root or env.span
if env.waf_triggers:
report_list = get_triggers(root_span)
if report_list is not None:
report_list.extend(env.waf_triggers)
Expand All @@ -122,6 +129,18 @@ def flush_waf_triggers(env: ASM_Environment) -> None:
else:
root_span.set_tag(APPSEC.JSON, json.dumps({"triggers": report_list}, separators=(",", ":")))
env.waf_triggers = []
telemetry_results = get_value(_TELEMETRY, _TELEMETRY_WAF_RESULTS)
if telemetry_results:
from ddtrace.appsec._metrics import DDWAF_VERSION

root_span.set_tag_str(APPSEC.WAF_VERSION, DDWAF_VERSION)
if telemetry_results["total_duration"]:
update_span_metrics(root_span, APPSEC.WAF_DURATION, telemetry_results["duration"])
update_span_metrics(root_span, APPSEC.WAF_DURATION_EXT, telemetry_results["total_duration"])
if telemetry_results["rasp"]["sum_eval"]:
update_span_metrics(root_span, APPSEC.RASP_DURATION, telemetry_results["rasp"]["duration"])
update_span_metrics(root_span, APPSEC.RASP_DURATION_EXT, telemetry_results["rasp"]["total_duration"])
update_span_metrics(root_span, APPSEC.RASP_RULE_EVAL, telemetry_results["rasp"]["sum_eval"])


GLOBAL_CALLBACKS[_CONTEXT_CALL] = [flush_waf_triggers]
Expand Down Expand Up @@ -150,8 +169,12 @@ def __init__(self):
"triggered": False,
"timeout": False,
"version": None,
"duration": 0.0,
"total_duration": 0.0,
"rasp": {
"called": False,
"sum_eval": 0,
"duration": 0.0,
"total_duration": 0.0,
"eval": {t: 0 for _, t in EXPLOIT_PREVENTION.TYPE},
"match": {t: 0 for _, t in EXPLOIT_PREVENTION.TYPE},
"timeout": {t: 0 for _, t in EXPLOIT_PREVENTION.TYPE},
Expand Down Expand Up @@ -344,6 +367,8 @@ def set_waf_telemetry_results(
is_blocked: bool,
is_timeout: bool,
rule_type: Optional[str],
duration: float,
total_duration: float,
) -> None:
result = get_value(_TELEMETRY, _TELEMETRY_WAF_RESULTS)
if result is not None:
Expand All @@ -354,12 +379,16 @@ def set_waf_telemetry_results(
result["timeout"] |= is_timeout
if rules_version is not None:
result["version"] = rules_version
result["duration"] += duration
result["total_duration"] += total_duration
else:
# Exploit Prevention telemetry
result["rasp"]["called"] = True
result["rasp"]["sum_eval"] += 1
result["rasp"]["eval"][rule_type] += 1
result["rasp"]["match"][rule_type] += int(is_triggered)
result["rasp"]["timeout"][rule_type] += int(is_timeout)
result["rasp"]["duration"] += duration
result["rasp"]["total_duration"] += total_duration


def get_waf_telemetry_results() -> Optional[Dict[str, Any]]:
Expand Down
3 changes: 3 additions & 0 deletions ddtrace/appsec/_constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ class APPSEC(metaclass=Constant_Class):
WAF_DURATION_EXT = "_dd.appsec.waf.duration_ext"
WAF_TIMEOUTS = "_dd.appsec.waf.timeouts"
WAF_VERSION = "_dd.appsec.waf.version"
RASP_DURATION = "_dd.appsec.rasp.duration"
RASP_DURATION_EXT = "_dd.appsec.rasp.duration_ext"
RASP_RULE_EVAL = "_dd.appsec.rasp.rule.eval"
ORIGIN_VALUE = "appsec"
CUSTOM_EVENT_PREFIX = "appsec.events"
USER_LOGIN_EVENT_PREFIX = "_dd.appsec.events.users.login"
Expand Down
2 changes: 1 addition & 1 deletion ddtrace/appsec/_metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ def _set_waf_request_metrics(*args):
tags=tags_request,
)
rasp = result["rasp"]
if rasp["called"]:
if rasp["sum_eval"]:
for t, n in [("eval", "rasp.rule.eval"), ("match", "rasp.rule.match"), ("timeout", "rasp.timeout")]:
for rule_type, value in rasp[t].items():
if value:
Expand Down
15 changes: 2 additions & 13 deletions ddtrace/appsec/_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,8 @@ def _waf_action(
bool(blocked),
waf_results.timeout,
rule_type,
waf_results.runtime,
waf_results.total_runtime,
)
if blocked:
core.set_item(WAF_CONTEXT_NAMES.BLOCKED, blocked, span=span)
Expand All @@ -357,21 +359,8 @@ def _waf_action(
span.set_tag_str(APPSEC.EVENT_RULE_ERRORS, errors)
log.debug("Error in ASM In-App WAF: %s", errors)
span.set_tag_str(APPSEC.EVENT_RULE_VERSION, info.version)
from ddtrace.appsec._ddwaf import version

span.set_tag_str(APPSEC.WAF_VERSION, version())

def update_metric(name, value):
old_value = span.get_metric(name)
if old_value is None:
old_value = 0.0
span.set_metric(name, value + old_value)

span.set_metric(APPSEC.EVENT_RULE_LOADED, info.loaded)
span.set_metric(APPSEC.EVENT_RULE_ERROR_COUNT, info.failed)
if waf_results.runtime:
update_metric(APPSEC.WAF_DURATION, waf_results.runtime)
update_metric(APPSEC.WAF_DURATION_EXT, waf_results.total_runtime)
except (JSONDecodeError, ValueError):
log.warning("Error parsing data ASM In-App WAF metrics report %s", info.errors)
except Exception:
Expand Down
2 changes: 1 addition & 1 deletion ddtrace/settings/asm.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ class ASMConfig(Env):
_deduplication_enabled = Env.var(bool, "_DD_APPSEC_DEDUPLICATION_ENABLED", default=True)

# default will be set to True once the feature is GA. For now it's always False
_ep_enabled = Env.var(bool, EXPLOIT_PREVENTION.EP_ENABLED, default=False)
_ep_enabled = Env.var(bool, EXPLOIT_PREVENTION.EP_ENABLED, default=True)
_ep_stack_trace_enabled = Env.var(bool, EXPLOIT_PREVENTION.STACK_TRACE_ENABLED, default=True)
# for max_stack_traces, 0 == unlimited
_ep_max_stack_traces = Env.var(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
features:
- |
ASM: This introduces full support for exploit prevention in the python tracer.
- LFI (via standard API open)
- SSRF (via standard API urllib or third party requests)
with monitoring and blocking feature, telemetry and span metrics reports.
5 changes: 5 additions & 0 deletions tests/appsec/contrib_appsec/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,11 @@ def get_tag(root_span):
yield lambda name: root_span().get_tag(name)


@pytest.fixture
def get_metric(root_span):
yield lambda name: root_span().get_metric(name)


def no_op(msg: str) -> None: # noqa: ARG001
"""Do nothing."""

Expand Down
7 changes: 7 additions & 0 deletions tests/appsec/contrib_appsec/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -1202,6 +1202,7 @@ def test_exploit_prevention(
interface,
root_span,
get_tag,
get_metric,
asm_enabled,
ep_enabled,
endpoint,
Expand All @@ -1215,6 +1216,7 @@ def test_exploit_prevention(

from ddtrace.appsec._common_module_patches import patch_common_modules
from ddtrace.appsec._common_module_patches import unpatch_common_modules
from ddtrace.appsec._constants import APPSEC
from ddtrace.appsec._metrics import DDWAF_VERSION
from ddtrace.contrib.requests import patch as patch_requests
from ddtrace.contrib.requests import unpatch as unpatch_requests
Expand Down Expand Up @@ -1260,6 +1262,11 @@ def test_exploit_prevention(
assert get_tag("rasp.request.done") is None
else:
assert get_tag("rasp.request.done") == endpoint
assert get_metric(APPSEC.RASP_DURATION) is not None
assert get_metric(APPSEC.RASP_DURATION_EXT) is not None
assert get_metric(APPSEC.RASP_RULE_EVAL) is not None
assert float(get_metric(APPSEC.RASP_DURATION_EXT)) >= float(get_metric(APPSEC.RASP_DURATION))
assert int(get_metric(APPSEC.RASP_RULE_EVAL)) > 0
else:
assert get_triggers(root_span()) is None
assert self.check_for_stack_trace(root_span) == []
Expand Down
15 changes: 15 additions & 0 deletions tests/contrib/django/test_django_appsec_snapshots.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ def daphne_client(django_asgi, additional_env=None):
"meta_struct",
"metrics._dd.appsec.waf.duration",
"metrics._dd.appsec.waf.duration_ext",
"metrics._dd.appsec.rasp.duration",
"metrics._dd.appsec.rasp.duration_ext",
"metrics._dd.appsec.rasp.rule.eval",
APPSEC_JSON_TAG,
]
)
Expand All @@ -93,6 +96,9 @@ def test_appsec_enabled():
"meta_struct",
"metrics._dd.appsec.waf.duration",
"metrics._dd.appsec.waf.duration_ext",
"metrics._dd.appsec.rasp.duration",
"metrics._dd.appsec.rasp.duration_ext",
"metrics._dd.appsec.rasp.rule.eval",
APPSEC_JSON_TAG,
]
)
Expand All @@ -113,6 +119,9 @@ def test_appsec_enabled_attack():
"meta_struct",
"metrics._dd.appsec.waf.duration",
"metrics._dd.appsec.waf.duration_ext",
"metrics._dd.appsec.rasp.duration",
"metrics._dd.appsec.rasp.duration_ext",
"metrics._dd.appsec.rasp.rule.eval",
APPSEC_JSON_TAG,
"metrics._dd.appsec.event_rules.loaded",
]
Expand Down Expand Up @@ -145,6 +154,9 @@ def test_request_ipblock_nomatch_200():
"meta_struct",
"metrics._dd.appsec.waf.duration",
"metrics._dd.appsec.waf.duration_ext",
"metrics._dd.appsec.rasp.duration",
"metrics._dd.appsec.rasp.duration_ext",
"metrics._dd.appsec.rasp.rule.eval",
"metrics._dd.appsec.event_rules.loaded",
]
)
Expand Down Expand Up @@ -182,6 +194,9 @@ def test_request_ipblock_match_403():
"meta_struct",
"metrics._dd.appsec.waf.duration",
"metrics._dd.appsec.waf.duration_ext",
"metrics._dd.appsec.rasp.duration",
"metrics._dd.appsec.rasp.duration_ext",
"metrics._dd.appsec.rasp.rule.eval",
"metrics._dd.appsec.event_rules.loaded",
]
)
Expand Down

0 comments on commit f682826

Please sign in to comment.