Skip to content

Commit

Permalink
add test_api_security_sampling for new sampling algorithm
Browse files Browse the repository at this point in the history
  • Loading branch information
christophe-papazian committed Feb 29, 2024
1 parent 9ac64b8 commit 566bafc
Show file tree
Hide file tree
Showing 8 changed files with 74 additions and 12 deletions.
2 changes: 1 addition & 1 deletion ddtrace/appsec/_api_security/api_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ def _schema_callback(self, env):
return

try:
if not self._should_collect_schema(env, root.context.sampling_priority):
if not self._should_collect_schema(env, env.span.context.sampling_priority):
return
except Exception:
log.warning("Failed to sample request for schema generation", exc_info=True)
Expand Down
2 changes: 1 addition & 1 deletion ddtrace/appsec/_remoteconfiguration.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,6 @@ def _add_rules_to_list(features: Mapping[str, Any], feature: str, message: str,
def _appsec_callback(features: Mapping[str, Any], test_tracer: Optional[Tracer] = None) -> None:
config = features.get("config", {})
_appsec_1click_activation(config, test_tracer)
_appsec_api_security_settings(config, test_tracer)
_appsec_rules_data(config, test_tracer)


Expand Down Expand Up @@ -235,6 +234,7 @@ def _appsec_1click_activation(features: Mapping[str, Any], test_tracer: Optional

def _appsec_api_security_settings(features: Mapping[str, Any], test_tracer: Optional[Tracer] = None) -> None:
"""
Deprecated
Update API Security settings from remote config
Actually: Update sample rate
"""
Expand Down
2 changes: 1 addition & 1 deletion ddtrace/appsec/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ def _appsec_rc_features_is_enabled() -> bool:


def _appsec_apisec_features_is_active() -> bool:
return asm_config._asm_enabled and asm_config._api_security_enabled and asm_config._api_security_sample_rate > 0.0
return asm_config._asm_enabled and asm_config._api_security_enabled


def _safe_userid(user_id):
Expand Down
2 changes: 1 addition & 1 deletion ddtrace/settings/asm.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ class ASMConfig(Env):
_user_model_email_field = Env.var(str, APPSEC.USER_MODEL_EMAIL_FIELD, default="")
_user_model_name_field = Env.var(str, APPSEC.USER_MODEL_NAME_FIELD, default="")
_api_security_enabled = Env.var(bool, API_SECURITY.ENV_VAR_ENABLED, default=True)
_api_security_sample_rate = Env.var(float, API_SECURITY.SAMPLE_RATE, validator=_validate_sample_rate, default=0.1)
_api_security_sample_rate = 0.0
_api_security_parse_response_body = Env.var(bool, API_SECURITY.PARSE_RESPONSE_BODY, default=True)
_asm_libddwaf = build_libddwaf_filename()
_asm_libddwaf_available = os.path.exists(_asm_libddwaf)
Expand Down
6 changes: 6 additions & 0 deletions tests/appsec/contrib_appsec/django_app/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from django.views.decorators.csrf import csrf_exempt

from ddtrace import tracer
import ddtrace.constants


# django.conf.urls.url was deprecated in django 3 and removed in django 4
Expand Down Expand Up @@ -40,6 +41,11 @@ def multi_view(request, param_int=0, param_str=""):
}
status = int(query_params.get("status", "200"))
headers_query = query_params.get("headers", "").split(",")
priority = query_params.get("priority", None)
if priority in ("keep", "drop"):
tracer.current_span().set_tag(
ddtrace.constants.MANUAL_KEEP_KEY if priority == "keep" else ddtrace.constants.MANUAL_DROP_KEY
)
response_headers = {}
for header in headers_query:
vk = header.split("=")
Expand Down
21 changes: 20 additions & 1 deletion tests/appsec/contrib_appsec/fastapi_app/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
from fastapi.responses import JSONResponse
from pydantic import BaseModel

from ddtrace import tracer
import ddtrace.constants


fake_secret_token = "DataDog"

Expand Down Expand Up @@ -51,6 +54,11 @@ async def multi_view(param_int: int, param_str: str, request: Request): # noqa:
}
status = int(query_params.get("status", "200"))
headers_query = query_params.get("headers", "").split(",")
priority = query_params.get("priority", None)
if priority in ("keep", "drop"):
tracer.current_span().set_tag(
ddtrace.constants.MANUAL_KEEP_KEY if priority == "keep" else ddtrace.constants.MANUAL_DROP_KEY
)
response_headers = {}
for header in headers_query:
vk = header.split("=")
Expand All @@ -71,7 +79,18 @@ async def multi_view_no_param(request: Request): # noqa: B008
"method": request.method,
}
status = int(query_params.get("status", "200"))
return JSONResponse(body, status_code=status)
headers_query = query_params.get("headers", "").split(",")
priority = query_params.get("priority", None)
if priority in ("keep", "drop"):
tracer.current_span().set_tag(
ddtrace.constants.MANUAL_KEEP_KEY if priority == "keep" else ddtrace.constants.MANUAL_DROP_KEY
)
response_headers = {}
for header in headers_query:
vk = header.split("=")
if len(vk) == 2:
response_headers[vk[0]] = vk[1]
return JSONResponse(body, status_code=status, headers=response_headers)

@app.get("/new_service/{service_name:str}/")
@app.post("/new_service/{service_name:str}/")
Expand Down
6 changes: 6 additions & 0 deletions tests/appsec/contrib_appsec/flask_app/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from flask import request

from ddtrace import tracer
import ddtrace.constants
from tests.webclient import PingFilter


Expand Down Expand Up @@ -36,6 +37,11 @@ def multi_view(param_int=0, param_str=""):
}
status = int(query_params.get("status", "200"))
headers_query = query_params.get("headers", "").split(",")
priority = query_params.get("priority", None)
if priority in ("keep", "drop"):
tracer.current_span().set_tag(
ddtrace.constants.MANUAL_KEEP_KEY if priority == "keep" else ddtrace.constants.MANUAL_DROP_KEY
)
response_headers = {}
for header in headers_query:
vk = header.split("=")
Expand Down
45 changes: 38 additions & 7 deletions tests/appsec/contrib_appsec/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -885,7 +885,6 @@ def test_nested_appsec_events(
({"User-Agent": "AllOK"}, False, False),
],
)
@pytest.mark.parametrize("sample_rate", [0.0, 1.0])
def test_api_security_schemas(
self,
interface: Interface,
Expand All @@ -897,16 +896,13 @@ def test_api_security_schemas(
headers,
event,
blocked,
sample_rate,
):
import base64
import gzip

from ddtrace.ext import http

with override_global_config(
dict(_asm_enabled=True, _api_security_enabled=apisec_enabled, _api_security_sample_rate=sample_rate)
):
with override_global_config(dict(_asm_enabled=True, _api_security_enabled=apisec_enabled)):
self.update_tracer(interface)
response = interface.client.post(
"/asm/324/huj/?x=1&y=2",
Expand All @@ -916,7 +912,6 @@ def test_api_security_schemas(
content_type="application/json",
)
assert asm_config._api_security_enabled == apisec_enabled
assert asm_config._api_security_sample_rate == sample_rate

assert self.status(response) == 403 if blocked else 200
assert get_tag(http.STATUS_CODE) == "403" if blocked else "200"
Expand All @@ -925,7 +920,7 @@ def test_api_security_schemas(
else:
assert get_triggers(root_span()) is None
value = get_tag(name)
if apisec_enabled and sample_rate:
if apisec_enabled:
assert value, name
api = json.loads(gzip.decompress(base64.b64decode(value)).decode())
assert api, name
Expand Down Expand Up @@ -976,6 +971,42 @@ def test_api_security_scanners(self, interface: Interface, get_tag, apisec_enabl
else:
assert value is None

@pytest.mark.parametrize("apisec_enabled", [True, False])
@pytest.mark.parametrize("priority", ["keep", "drop"])
def test_api_security_sampling(self, interface: Interface, get_tag, apisec_enabled, priority):
from ddtrace.ext import http

payload = {"mastercard": "5123456789123456"}
with override_global_config(dict(_asm_enabled=True, _api_security_enabled=apisec_enabled)):
self.update_tracer(interface)
response = interface.client.post(
f"/asm/?priority={priority}",
data=json.dumps(payload),
content_type="application/json",
)
assert self.status(response) == 200
assert get_tag(http.STATUS_CODE) == "200"
assert asm_config._api_security_enabled == apisec_enabled

value = get_tag("_dd.appsec.s.req.body")
if apisec_enabled and priority == "keep":
assert value
else:
assert value is None
# second request must be ignored
self.update_tracer(interface)
response = interface.client.post(
f"/asm/?priority={priority}",
data=json.dumps(payload),
content_type="application/json",
)
assert self.status(response) == 200
assert get_tag(http.STATUS_CODE) == "200"
assert asm_config._api_security_enabled == apisec_enabled

value = get_tag("_dd.appsec.s.req.body")
assert value is None

def test_request_invalid_rule_file(self, interface):
"""
When the rule file is invalid, the tracer should not crash or prevent normal behavior
Expand Down

0 comments on commit 566bafc

Please sign in to comment.