|
2 | 2 | import json |
3 | 3 | import os |
4 | 4 | import os.path |
| 5 | +from typing import Set |
5 | 6 | from typing import TYPE_CHECKING |
6 | 7 |
|
7 | 8 | import attr |
8 | 9 |
|
9 | | -import ddtrace |
10 | 10 | from ddtrace.appsec._ddwaf import DDWaf |
11 | 11 | from ddtrace.constants import MANUAL_KEEP_KEY |
| 12 | +from ddtrace.constants import ORIGIN_KEY |
| 13 | +from ddtrace.contrib.trace_utils import _normalize_tag_name |
12 | 14 | from ddtrace.ext import SpanTypes |
| 15 | +from ddtrace.internal import _context |
13 | 16 | from ddtrace.internal.logger import get_logger |
14 | 17 | from ddtrace.internal.processor import SpanProcessor |
15 | 18 |
|
16 | 19 |
|
17 | 20 | if TYPE_CHECKING: |
18 | | - from ddtrace import Span |
| 21 | + from typing import Dict |
| 22 | + |
| 23 | + from ddtrace.span import Span |
19 | 24 |
|
20 | 25 | ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) |
21 | 26 | DEFAULT_RULES = os.path.join(ROOT_DIR, "rules.json") |
22 | 27 |
|
23 | 28 | log = get_logger(__name__) |
24 | 29 |
|
25 | 30 |
|
| 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 | + |
26 | 36 | def get_rules(): |
| 37 | + # type: () -> str |
27 | 38 | return os.getenv("DD_APPSEC_RULES", default=DEFAULT_RULES) |
28 | 39 |
|
29 | 40 |
|
| 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 | + |
30 | 84 | @attr.s(eq=False) |
31 | 85 | class AppSecSpanProcessor(SpanProcessor): |
32 | 86 |
|
33 | 87 | rules = attr.ib(type=str, factory=get_rules) |
34 | 88 | _ddwaf = attr.ib(type=DDWaf, default=None) |
| 89 | + _addresses_to_keep = attr.ib(type=Set[str], factory=set) |
35 | 90 |
|
36 | 91 | @property |
37 | 92 | def enabled(self): |
@@ -67,26 +122,82 @@ def __attrs_post_init__(self): |
67 | 122 | # Partial of DDAS-0005-00 |
68 | 123 | log.warning("[DDAS-0005-00] WAF initialization failed") |
69 | 124 | 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) |
70 | 129 |
|
71 | 130 | def on_span_start(self, span): |
72 | 131 | # type: (Span) -> None |
73 | 132 | pass |
74 | 133 |
|
| 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 | + |
75 | 142 | def on_span_finish(self, span): |
76 | 143 | # type: (Span) -> None |
77 | 144 | if span.span_type != SpanTypes.WEB: |
78 | 145 | return |
79 | 146 | span.set_metric("_dd.appsec.enabled", 1.0) |
80 | 147 | 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 | + |
85 | 190 | log.debug("[DDAS-001-00] Executing AppSec In-App WAF with parameters: %s", data) |
86 | 191 | res = self._ddwaf.run(data) # res is a serialized json |
87 | 192 | 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]) |
88 | 195 | # Partial DDAS-011-00 |
89 | 196 | log.debug("[DDAS-011-00] AppSec In-App WAF returned: %s", res) |
90 | 197 | span._set_str_tag("appsec.event", "true") |
91 | 198 | 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. |
92 | 201 | 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