Skip to content

Commit f527956

Browse files
authored
chore(asm/appsec): add rate limiter and waf timeout (backport #3575) (#3634)
This is an automatic backport of pull request #3575 done by [Mergify](https://mergify.com). --- <details> <summary>Mergify commands and options</summary> <br /> More conditions and actions can be found in the [documentation](https://docs.mergify.com/). You can also trigger Mergify actions by commenting on this pull request: - `@Mergifyio refresh` will re-evaluate the rules - `@Mergifyio rebase` will rebase this PR on its base branch - `@Mergifyio update` will merge the base branch into this PR - `@Mergifyio backport <destination>` will backport this PR on `<destination>` branch Additionally, on Mergify [dashboard](https://dashboard.mergify.com/) you can: - look at your merge queues - generate the Mergify configuration with the config editor. Finally, you can contact us on https://mergify.com </details>
1 parent a457c0b commit f527956

File tree

2 files changed

+46
-1
lines changed

2 files changed

+46
-1
lines changed

ddtrace/appsec/processor.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from ddtrace.internal import _context
1616
from ddtrace.internal.logger import get_logger
1717
from ddtrace.internal.processor import SpanProcessor
18+
from ddtrace.internal.rate_limiter import RateLimiter
1819

1920

2021
if TYPE_CHECKING:
@@ -24,6 +25,8 @@
2425

2526
ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
2627
DEFAULT_RULES = os.path.join(ROOT_DIR, "rules.json")
28+
DEFAULT_TRACE_RATE_LIMIT = 100
29+
DEFAULT_WAF_TIMEOUT = 20 # ms
2730

2831
log = get_logger(__name__)
2932

@@ -81,12 +84,24 @@ def _set_headers(span, headers):
8184
span._set_str_tag(_normalize_tag_name("request", k), headers[k])
8285

8386

87+
def _get_rate_limiter():
88+
# type: () -> RateLimiter
89+
return RateLimiter(int(os.getenv("DD_APPSEC_TRACE_RATE_LIMIT", DEFAULT_TRACE_RATE_LIMIT)))
90+
91+
92+
def _get_waf_timeout():
93+
# type: () -> int
94+
return int(os.getenv("DD_APPSEC_WAF_TIMEOUT", DEFAULT_WAF_TIMEOUT))
95+
96+
8497
@attr.s(eq=False)
8598
class AppSecSpanProcessor(SpanProcessor):
8699

87100
rules = attr.ib(type=str, factory=get_rules)
88101
_ddwaf = attr.ib(type=DDWaf, default=None)
89102
_addresses_to_keep = attr.ib(type=Set[str], factory=set)
103+
_rate_limiter = attr.ib(type=RateLimiter, factory=_get_rate_limiter)
104+
_waf_timeout = attr.ib(type=int, factory=_get_waf_timeout)
90105

91106
@property
92107
def enabled(self):
@@ -188,8 +203,14 @@ def on_span_finish(self, span):
188203
data[_Addresses.SERVER_RESPONSE_HEADERS_NO_COOKIES] = _no_cookies(response_headers)
189204

190205
log.debug("[DDAS-001-00] Executing AppSec In-App WAF with parameters: %s", data)
191-
res = self._ddwaf.run(data) # res is a serialized json
206+
res = self._ddwaf.run(data, self._waf_timeout) # res is a serialized json
192207
if res is not None:
208+
# We run the rate limiter only if there is an attack, its goal is to limit the number of collected asm
209+
# events
210+
allowed = self._rate_limiter.is_allowed(span.start_ns)
211+
if not allowed:
212+
# TODO: add metric collection to keep an eye (when it's name is clarified)
213+
return
193214
if _Addresses.SERVER_REQUEST_HEADERS_NO_COOKIES in data:
194215
_set_headers(span, data[_Addresses.SERVER_REQUEST_HEADERS_NO_COOKIES])
195216
# Partial DDAS-011-00

tests/appsec/test_processor.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,3 +107,27 @@ def test_appsec_span_tags_snapshot(tracer):
107107
set_http_meta(span, {}, raw_uri="http://example.com/.git", status_code="404")
108108

109109
assert "triggers" in json.loads(span.get_tag("_dd.appsec.json"))
110+
111+
112+
def test_appsec_span_rate_limit(tracer):
113+
with override_env(dict(DD_APPSEC_TRACE_RATE_LIMIT="1")):
114+
_enable_appsec(tracer)
115+
116+
# we have 2 spans going through with a rate limit of 1: this is because the first span will update the rate
117+
# limiter last update timestamp. In other words, we need a first call to reset the rate limiter's clock
118+
# DEV: aligning rate limiter clock with this span (this
119+
# span will go through as it is linked to the init window)
120+
with tracer.trace("test", span_type=SpanTypes.WEB) as span1:
121+
set_http_meta(span1, {}, raw_uri="http://example.com/.git", status_code="404")
122+
123+
with tracer.trace("test", span_type=SpanTypes.WEB) as span2:
124+
set_http_meta(span2, {}, raw_uri="http://example.com/.git", status_code="404")
125+
span2.start_ns = span1.start_ns + 1
126+
127+
with tracer.trace("test", span_type=SpanTypes.WEB) as span3:
128+
set_http_meta(span3, {}, raw_uri="http://example.com/.git", status_code="404")
129+
span2.start_ns = span1.start_ns + 2
130+
131+
assert span1.get_tag("_dd.appsec.json") is not None
132+
assert span2.get_tag("_dd.appsec.json") is not None
133+
assert span3.get_tag("_dd.appsec.json") is None

0 commit comments

Comments
 (0)