Skip to content

Commit b187df1

Browse files
authored
chore(appsec): add support for more http data (#3193)
# Goal To run AppSec rules, we need to get some data from the incoming request. For instance, the parsed query string, the HTTP body or even the URL parameters from the framework. The goal of this PR is to provide a way to access data from instrumentations event when these are not available on spans. # Main change To reach this goal, `set_http_meta` is updated to allow more arguments related to the current HTTP request. These are stored in a new contextual API (which under the hood stores the data on the root span). The data is then retrieved on span finish in the AppSecSpanProcessor and run through ddwaf. ## Future work This PRs uses references to `tracer` and `span` to create a transaction/request store. This store is used to keep a state containing interesting data in term of AppSec. We use it at the end of the root span to run the in-app waf. In the future, this store will exist independently from `tracer` and/or `span` and the current state is a transition to a future mechanism. # Other Changes * We also need to collect a given list of header when an AppSec event is found * We mark the origin of the span collection to AppSec when there is an AppSec event and the previous origin was unset * We forward more request data to `set_http_meta` for Django, Flask and Pyramid * Update rules to latest version
1 parent be5a4e0 commit b187df1

24 files changed

+1299
-362
lines changed

ddtrace/appsec/CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ include(ExternalProject)
44

55
ExternalProject_Add(libddwaf
66
GIT_REPOSITORY https://github.com/DataDog/libddwaf.git
7-
GIT_TAG 5fe5f1b065bf9e7a1b58bfae2a1dcbb9f6ba4231
7+
GIT_TAG 627d8958d72440eb020e55ecb8772503796a73af
88
INSTALL_DIR ${CMAKE_SOURCE_DIR}
99
CMAKE_ARGS
1010
-DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE}

ddtrace/appsec/_ddwaf.pyx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ cdef class DDWaf(object):
227227
cdef ddwaf_object* rule_objects
228228
self._rules = _Wrapper(rules, max_objects=None)
229229
rule_objects = (<_Wrapper?>self._rules)._ptr;
230-
self._handle = ddwaf_init(rule_objects, NULL)
230+
self._handle = ddwaf_init(rule_objects, NULL, NULL)
231231
if <void *> self._handle == NULL:
232232
raise ValueError("invalid rules")
233233

ddtrace/appsec/_libddwaf.pxd

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,12 @@ cdef extern from "include/ddwaf.h":
2727
uint64_t nbEntries
2828
DDWAF_OBJ_TYPE type
2929

30+
ctypedef struct ddwaf_ruleset_info:
31+
uint16_t loaded
32+
uint16_t failed
33+
ddwaf_object errors
34+
const char *version
35+
3036
ctypedef struct ddwaf_handle:
3137
pass
3238

@@ -45,7 +51,7 @@ cdef extern from "include/ddwaf.h":
4551

4652
ctypedef void (*ddwaf_object_free_fn)(ddwaf_object *object);
4753

48-
ddwaf_handle ddwaf_init(const ddwaf_object* rules, const ddwaf_config* config);
54+
ddwaf_handle ddwaf_init(const ddwaf_object* rules, const ddwaf_config* config, ddwaf_ruleset_info *info);
4955
ddwaf_context ddwaf_context_init(const ddwaf_handle handle, ddwaf_object_free_fn obj_free);
5056
DDWAF_RET_CODE ddwaf_run(ddwaf_context context, ddwaf_object* data, ddwaf_result* result, uint64_t timeout);
5157
void ddwaf_context_destroy(ddwaf_context context);

ddtrace/appsec/processor.py

Lines changed: 117 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,36 +2,91 @@
22
import json
33
import os
44
import os.path
5+
from typing import Set
56
from typing import TYPE_CHECKING
67

78
import attr
89

9-
import ddtrace
1010
from ddtrace.appsec._ddwaf import DDWaf
1111
from ddtrace.constants import MANUAL_KEEP_KEY
12+
from ddtrace.constants import ORIGIN_KEY
13+
from ddtrace.contrib.trace_utils import _normalize_tag_name
1214
from ddtrace.ext import SpanTypes
15+
from ddtrace.internal import _context
1316
from ddtrace.internal.logger import get_logger
1417
from ddtrace.internal.processor import SpanProcessor
1518

1619

1720
if TYPE_CHECKING:
18-
from ddtrace import Span
21+
from typing import Dict
22+
23+
from ddtrace.span import Span
1924

2025
ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
2126
DEFAULT_RULES = os.path.join(ROOT_DIR, "rules.json")
2227

2328
log = get_logger(__name__)
2429

2530

31+
def _no_cookies(data):
32+
# type: (Dict[str, str]) -> Dict[str, str]
33+
return {key: value for key, value in data.items() if key.lower() not in ("cookie", "set-cookie")}
34+
35+
2636
def get_rules():
37+
# type: () -> str
2738
return os.getenv("DD_APPSEC_RULES", default=DEFAULT_RULES)
2839

2940

41+
class _Addresses(object):
42+
SERVER_REQUEST_BODY = "server.request.body"
43+
SERVER_REQUEST_QUERY = "server.request.query"
44+
SERVER_REQUEST_HEADERS_NO_COOKIES = "server.request.headers.no_cookies"
45+
SERVER_REQUEST_URI_RAW = "server.request.uri.raw"
46+
SERVER_REQUEST_METHOD = "server.request.method"
47+
SERVER_REQUEST_PATH_PARAMS = "server.request.path_params"
48+
SERVER_REQUEST_COOKIES = "server.request.cookies"
49+
SERVER_RESPONSE_STATUS = "server.response.status"
50+
SERVER_RESPONSE_HEADERS_NO_COOKIES = "server.response.headers.no_cookies"
51+
52+
53+
_COLLECTED_REQUEST_HEADERS = {
54+
"accept",
55+
"accept-encoding",
56+
"accept-language",
57+
"content-encoding",
58+
"content-language",
59+
"content-length",
60+
"content-type",
61+
"forwarded",
62+
"forwarded-for",
63+
"host",
64+
"true-client-ip",
65+
"user-agent",
66+
"via",
67+
"x-client-ip",
68+
"x-cluster-client-ip",
69+
"x-forwarded",
70+
"x-forwarded-for",
71+
"x-real-ip",
72+
}
73+
74+
_COLLECTED_HEADER_PREFIX = "http.request.headers."
75+
76+
77+
def _set_headers(span, headers):
78+
# type: (Span, Dict) -> None
79+
for k in headers:
80+
if k.lower() in _COLLECTED_REQUEST_HEADERS:
81+
span._set_str_tag(_normalize_tag_name("request", k), headers[k])
82+
83+
3084
@attr.s(eq=False)
3185
class AppSecSpanProcessor(SpanProcessor):
3286

3387
rules = attr.ib(type=str, factory=get_rules)
3488
_ddwaf = attr.ib(type=DDWaf, default=None)
89+
_addresses_to_keep = attr.ib(type=Set[str], factory=set)
3590

3691
@property
3792
def enabled(self):
@@ -67,26 +122,82 @@ def __attrs_post_init__(self):
67122
# Partial of DDAS-0005-00
68123
log.warning("[DDAS-0005-00] WAF initialization failed")
69124
raise
125+
for address in self._ddwaf.required_data:
126+
self._mark_needed(address)
127+
# we always need the request headers
128+
self._mark_needed(_Addresses.SERVER_REQUEST_HEADERS_NO_COOKIES)
70129

71130
def on_span_start(self, span):
72131
# type: (Span) -> None
73132
pass
74133

134+
def _mark_needed(self, address):
135+
# type: (str) -> None
136+
self._addresses_to_keep.add(address)
137+
138+
def _is_needed(self, address):
139+
# type: (str) -> bool
140+
return address in self._addresses_to_keep
141+
75142
def on_span_finish(self, span):
76143
# type: (Span) -> None
77144
if span.span_type != SpanTypes.WEB:
78145
return
79146
span.set_metric("_dd.appsec.enabled", 1.0)
80147
span._set_str_tag("_dd.runtime_family", "python")
81-
data = {
82-
"server.request.uri.raw": span.get_tag(ddtrace.ext.http.URL),
83-
"server.response.status": span.get_tag(ddtrace.ext.http.STATUS_CODE),
84-
}
148+
149+
data = {}
150+
if self._is_needed(_Addresses.SERVER_REQUEST_QUERY):
151+
request_query = _context.get_item("http.request.query", span=span)
152+
if request_query is not None:
153+
data[_Addresses.SERVER_REQUEST_QUERY] = request_query
154+
155+
if self._is_needed(_Addresses.SERVER_REQUEST_HEADERS_NO_COOKIES):
156+
request_headers = _context.get_item("http.request.headers", span=span)
157+
if request_headers is not None:
158+
data[_Addresses.SERVER_REQUEST_HEADERS_NO_COOKIES] = _no_cookies(request_headers)
159+
160+
if self._is_needed(_Addresses.SERVER_REQUEST_URI_RAW):
161+
uri = _context.get_item("http.request.uri", span=span)
162+
if uri is not None:
163+
data[_Addresses.SERVER_REQUEST_URI_RAW] = uri
164+
165+
if self._is_needed(_Addresses.SERVER_REQUEST_METHOD):
166+
request_method = _context.get_item("http.request.method", span=span)
167+
if request_method is not None:
168+
data[_Addresses.SERVER_REQUEST_METHOD] = request_method
169+
170+
if self._is_needed(_Addresses.SERVER_REQUEST_PATH_PARAMS):
171+
path_params = _context.get_item("http.request.path_params", span=span)
172+
if path_params is not None:
173+
data[_Addresses.SERVER_REQUEST_PATH_PARAMS] = path_params
174+
175+
if self._is_needed(_Addresses.SERVER_REQUEST_COOKIES):
176+
cookies = _context.get_item("http.request.cookies", span=span)
177+
if cookies is not None:
178+
data[_Addresses.SERVER_REQUEST_COOKIES] = cookies
179+
180+
if self._is_needed(_Addresses.SERVER_RESPONSE_STATUS):
181+
status = _context.get_item("http.response.status", span=span)
182+
if status is not None:
183+
data[_Addresses.SERVER_RESPONSE_STATUS] = status
184+
185+
if self._is_needed(_Addresses.SERVER_RESPONSE_HEADERS_NO_COOKIES):
186+
response_headers = _context.get_item("http.response.headers", span=span)
187+
if response_headers is not None:
188+
data[_Addresses.SERVER_RESPONSE_HEADERS_NO_COOKIES] = _no_cookies(response_headers)
189+
85190
log.debug("[DDAS-001-00] Executing AppSec In-App WAF with parameters: %s", data)
86191
res = self._ddwaf.run(data) # res is a serialized json
87192
if res is not None:
193+
if _Addresses.SERVER_REQUEST_HEADERS_NO_COOKIES in data:
194+
_set_headers(span, data[_Addresses.SERVER_REQUEST_HEADERS_NO_COOKIES])
88195
# Partial DDAS-011-00
89196
log.debug("[DDAS-011-00] AppSec In-App WAF returned: %s", res)
90197
span._set_str_tag("appsec.event", "true")
91198
span._set_str_tag("_dd.appsec.json", '{"triggers":%s}' % (res,))
199+
# Right now, we overwrite any value that could be already there. We need to reconsider when ASM/AppSec's
200+
# specs are updated.
92201
span.set_tag(MANUAL_KEEP_KEY)
202+
if span.get_tag(ORIGIN_KEY) is None:
203+
span._set_str_tag(ORIGIN_KEY, "appsec")

0 commit comments

Comments
 (0)