diff --git a/docs/execute/build.md b/docs/execute/build.md
index cd174d5e139..1faf8d22dca 100644
--- a/docs/execute/build.md
+++ b/docs/execute/build.md
@@ -43,7 +43,7 @@ Build images used for system tests.
+ Specific to the `GRAPHQL_APPSEC` scenario: `gqlgen`, `graph-gophers`, `graphql-go`
* For `java`: `spring-boot` (default),`akka-http`,`jersey-grizzly2`,`play`,`ratpack`,`resteasy-netty3`,`spring-boot-3-native`,`spring-boot-jetty`,`spring-boot-openliberty`,`spring-boot-payara`,`spring-boot-undertow`,`spring-boot-wildfly`,`uds-spring-boot`,`vertx3`,`vertx4`
* For `nodejs`: `express4` (default), `express4-typescript`, `express5`, `nextjs`, `fastify`
-* For `php`: `apache-mod-8.0` (default), `apache-mod-8.1`, `apache-mod-8.2`, `apache-mod-7.4`, `apache-mod-7.3`, `apache-mod-7.2`, `apache-mod-7.1`, `apache-mod-7.0`, `apache-mod-8.2-zts`, `apache-mod-8.1-zts`, `apache-mod-8.0-zts`, `apache-mod-7.4-zts`, `apache-mod-7.3-zts`, `apache-mod-7.2-zts`, `apache-mod-7.1-zts`, `apache-mod-7.0-zts`, `php-fpm-8.5`, `php-fpm-8.2`, `php-fpm-8.1`, `php-fpm-8.0`, `php-fpm-7.4`, `php-fpm-7.3`, `php-fpm-7.2`, `php-fpm-7.1`, `php-fpm-7.0`, `laravel11x`
+* For `php`: `apache-mod-8.0` (default), `apache-mod-8.1`, `apache-mod-8.2`, `apache-mod-7.4`, `apache-mod-7.3`, `apache-mod-7.2`, `apache-mod-7.1`, `apache-mod-7.0`, `apache-mod-8.2-zts`, `apache-mod-8.1-zts`, `apache-mod-8.0-zts`, `apache-mod-7.4-zts`, `apache-mod-7.3-zts`, `apache-mod-7.2-zts`, `apache-mod-7.1-zts`, `apache-mod-7.0-zts`, `php-fpm-8.5`, `php-fpm-8.2`, `php-fpm-8.1`, `php-fpm-8.0`, `php-fpm-7.4`, `php-fpm-7.3`, `php-fpm-7.2`, `php-fpm-7.1`, `php-fpm-7.0`, `laravel11x`, `symfony7x`
* For `python`: `flask-poc` (default), `fastapi`, `uwsgi-poc`, `django-poc`, `python3.12`
* For `ruby`: `rails70` (default), `rack`, `sinatra21`, and lot of other sinatra/rails versions
diff --git a/manifests/php.yml b/manifests/php.yml
index 3fe18e93032..e11c3c59831 100644
--- a/manifests/php.yml
+++ b/manifests/php.yml
@@ -98,6 +98,7 @@ manifest:
- weblog_declaration:
"*": v1.6.2
laravel11x: missing_feature (PHP tracer Laravel integration collects response body schema regardless of DD_API_SECURITY_PARSE_RESPONSE_BODY=false)
+ symfony7x: missing_feature (PHP tracer Symfony integration collects response body schema regardless of DD_API_SECURITY_PARSE_RESPONSE_BODY=false)
tests/appsec/api_security/test_schemas.py::Test_Schema_Response_Headers: v0.94.0
tests/appsec/api_security/test_schemas.py::Test_Schema_Response_on_Block: v1.11.0-dev
tests/appsec/api_security/test_schemas_auth.py: missing_feature
@@ -287,10 +288,12 @@ manifest:
- weblog_declaration:
"*": missing_feature
laravel11x: v1.6.2
+ symfony7x: v1.6.2
tests/appsec/rasp/test_sqli.py::Test_Sqli_Optional_SpanTags:
- weblog_declaration:
"*": missing_feature
laravel11x: v1.6.2
+ symfony7x: v1.6.2
tests/appsec/rasp/test_sqli.py::Test_Sqli_Rules_Version: v1.6.2
tests/appsec/rasp/test_sqli.py::Test_Sqli_StackTrace: missing_feature
tests/appsec/rasp/test_sqli.py::Test_Sqli_Telemetry: missing_feature
@@ -318,6 +321,7 @@ manifest:
? tests/appsec/test_asm_standalone.py::Test_APISecurityStandalone::test_no_appsec_upstream__no_asm_event__is_kept_with_priority_1__from_2
: - weblog_declaration:
laravel11x: missing_feature
+ symfony7x: missing_feature
tests/appsec/test_asm_standalone.py::Test_AppSecStandalone_NotEnabled: v1.6.2
tests/appsec/test_asm_standalone.py::Test_AppSecStandalone_UpstreamPropagation_V2: v1.8.0
tests/appsec/test_asm_standalone.py::Test_IastStandalone_UpstreamPropagation_V2: missing_feature
@@ -366,6 +370,7 @@ manifest:
tests/appsec/test_automated_login_events.py::Test_V3_Login_Events::test_login_sdk_failure_local:
- weblog_declaration:
laravel11x: v1.8.0
+ symfony7x: v1.8.0
tests/appsec/test_automated_login_events.py::Test_V3_Login_Events::test_login_sdk_success_basic:
- weblog_declaration:
"*": missing_feature (Basic auth not implemented)
@@ -373,6 +378,7 @@ manifest:
tests/appsec/test_automated_login_events.py::Test_V3_Login_Events::test_login_sdk_success_local:
- weblog_declaration:
laravel11x: v1.8.0
+ symfony7x: v1.8.0
tests/appsec/test_automated_login_events.py::Test_V3_Login_Events::test_login_success_basic:
- weblog_declaration:
"*": missing_feature (Basic auth not implemented)
@@ -384,6 +390,7 @@ manifest:
tests/appsec/test_automated_login_events.py::Test_V3_Login_Events::test_login_wrong_password_failure_local:
- weblog_declaration:
laravel11x: v1.8.0
+ symfony7x: v1.8.0
tests/appsec/test_automated_login_events.py::Test_V3_Login_Events::test_login_wrong_user_failure_basic:
- weblog_declaration:
"*": missing_feature (Basic auth not implemented)
@@ -396,6 +403,7 @@ manifest:
tests/appsec/test_automated_login_events.py::Test_V3_Login_Events_Anon::test_login_sdk_failure_local:
- weblog_declaration:
laravel11x: v1.8.0
+ symfony7x: v1.8.0
tests/appsec/test_automated_login_events.py::Test_V3_Login_Events_Anon::test_login_sdk_success_basic:
- weblog_declaration:
"*": missing_feature (Basic auth not implemented)
@@ -403,6 +411,7 @@ manifest:
tests/appsec/test_automated_login_events.py::Test_V3_Login_Events_Anon::test_login_sdk_success_local:
- weblog_declaration:
laravel11x: v1.8.0
+ symfony7x: v1.8.0
tests/appsec/test_automated_login_events.py::Test_V3_Login_Events_Anon::test_login_success_basic:
- weblog_declaration:
"*": missing_feature (Basic auth not implemented)
@@ -417,13 +426,20 @@ manifest:
laravel11x: v1.8.0
tests/appsec/test_automated_login_events.py::Test_V3_Login_Events_Blocking: v1.8.0
tests/appsec/test_automated_login_events.py::Test_V3_Login_Events_RC: v1.8.0
- tests/appsec/test_automated_payment_events.py: v1.17.0-dev
+ tests/appsec/test_automated_payment_events.py:
+ - weblog_declaration:
+ "*": missing_feature (Stripe endpoints not implemented)
+ symfony7x: v1.17.0-dev
tests/appsec/test_automated_user_and_session_tracking.py::Test_Automated_Session_Blocking: missing_feature
tests/appsec/test_automated_user_and_session_tracking.py::Test_Automated_User_Blocking: v1.8.0
tests/appsec/test_automated_user_and_session_tracking.py::Test_Automated_User_Tracking: v1.8.0
+ tests/appsec/test_automated_user_and_session_tracking.py::Test_Automated_User_Tracking::test_user_tracking_auto:
+ - weblog_declaration:
+ symfony7x: "missing_feature (APPSEC-68783: Symfony decide hook uses getUserIdentifier which returns username not database ID)"
tests/appsec/test_automated_user_and_session_tracking.py::Test_Automated_User_Tracking::test_user_tracking_sdk_overwrite:
- weblog_declaration:
laravel11x: missing_feature
+ symfony7x: missing_feature
tests/appsec/test_blocking_addresses.py::Test_BlockingGraphqlResolvers: missing_feature
tests/appsec/test_blocking_addresses.py::Test_Blocking_client_ip: v1.13.0+4663b2fa7c20c6920f347d059b57dc2a419cb7f7
tests/appsec/test_blocking_addresses.py::Test_Blocking_client_ip_with_K8_private_ip: v1.13.0+4663b2fa7c20c6920f347d059b57dc2a419cb7f7
@@ -614,6 +630,7 @@ manifest:
- weblog_declaration:
"*": v1.19.0
laravel11x: missing_feature (pending to be implemented on this weblog)
+ symfony7x: missing_feature (pending to be implemented on this weblog)
tests/debugger/test_debugger_code_origins.py::Test_Debugger_Code_Origins::test_code_origin_entry_present: irrelevant (HTTP entry spans in PHP-FPM/Apache are C-level; execute stack is empty at span close time so no user frames are captured)
tests/debugger/test_debugger_condition_errors.py::Test_Debugger_Invalid_Condition_DSL:
- weblog_declaration:
@@ -630,6 +647,7 @@ manifest:
- weblog_declaration:
"*": v1.19.0
laravel11x: missing_feature (pending to be implemented on this weblog)
+ symfony7x: missing_feature (pending to be implemented on this weblog)
tests/debugger/test_debugger_exception_replay.py::Test_Debugger_Exception_Replay::test_exception_replay_firsthit: missing_feature (Implemented only for dotnet)
tests/debugger/test_debugger_exception_replay.py::Test_Debugger_Exception_Replay::test_exception_replay_outofmemory: missing_feature (Implemented only for dotnet)
tests/debugger/test_debugger_exception_replay.py::Test_Debugger_Exception_Replay::test_exception_replay_recursion_20: irrelevant (The PHP weblog does not have non-user code, which could serve as boundary)
@@ -639,6 +657,7 @@ manifest:
- weblog_declaration:
"*": v1.19.0
laravel11x: missing_feature (pending to be implemented on this weblog)
+ symfony7x: missing_feature (pending to be implemented on this weblog)
? tests/debugger/test_debugger_expression_language.py::Test_Debugger_Expression_Language::test_expression_language_access_exception
: '>=1.16.0'
? tests/debugger/test_debugger_expression_language.py::Test_Debugger_Expression_Language::test_expression_language_comparison_operators
@@ -657,17 +676,20 @@ manifest:
- weblog_declaration:
"*": v1.20.0
laravel11x: missing_feature (pending to be implemented on this weblog)
+ symfony7x: missing_feature (pending to be implemented on this weblog)
tests/debugger/test_debugger_inproduct_enablement.py::Test_Debugger_InProduct_Enablement_Exception_Replay: missing_feature (blocked by exception replay throwable gap)
tests/debugger/test_debugger_pii.py::Test_Debugger_PII_Redaction:
- weblog_declaration:
"*": v1.19.0
laravel11x: missing_feature (pending to be implemented on this weblog)
+ symfony7x: missing_feature (pending to be implemented on this weblog)
tests/debugger/test_debugger_pii.py::Test_Debugger_PII_Redaction::test_pii_redaction_line_full: missing_feature (line probe infrastructure not implemented)
tests/debugger/test_debugger_pii.py::Test_Debugger_PII_Redaction_Excluded_Identifiers: missing_feature (DD_DYNAMIC_INSTRUMENTATION_REDACTION_EXCLUDED_IDENTIFIERS config key not implemented; only REDACTED_IDENTIFIERS exists which adds names, not removes them from the built-in list)
tests/debugger/test_debugger_probe_budgets.py::Test_Debugger_Probe_Budgets:
- weblog_declaration:
"*": v1.13.0+4663b2fa7c20c6920f347d059b57dc2a419cb7f7
laravel11x: missing_feature (pending to be implemented on this weblog)
+ symfony7x: missing_feature (pending to be implemented on this weblog)
tests/debugger/test_debugger_probe_budgets.py::Test_Debugger_Probe_Budgets::test_log_line_capture_expression_budgets: missing_feature
tests/debugger/test_debugger_probe_budgets.py::Test_Debugger_Probe_Budgets::test_span_probe_expression_budgets: irrelevant
tests/debugger/test_debugger_probe_snapshot.py::Test_Debugger_Line_Probe_Snaphots: missing_feature
@@ -678,15 +700,18 @@ manifest:
- weblog_declaration:
"*": v1.13.0+4663b2fa7c20c6920f347d059b57dc2a419cb7f7
laravel11x: missing_feature (pending to be implemented on this weblog)
+ symfony7x: missing_feature (pending to be implemented on this weblog)
tests/debugger/test_debugger_probe_snapshot.py::Test_Debugger_Method_Probe_Snaphots::test_mix_snapshot: missing_feature (requires line probe infrastructure not yet implemented)
tests/debugger/test_debugger_probe_snapshot.py::Test_Debugger_Method_Probe_Snaphots::test_span_decoration_method_snapshot:
- weblog_declaration:
"*": v1.13.0+4663b2fa7c20c6920f347d059b57dc2a419cb7f7
laravel11x: missing_feature (pending to be implemented on this weblog)
+ symfony7x: missing_feature (pending to be implemented on this weblog)
tests/debugger/test_debugger_probe_snapshot.py::Test_Debugger_Method_Probe_Snaphots_With_SCM:
- weblog_declaration:
"*": v1.13.0+4663b2fa7c20c6920f347d059b57dc2a419cb7f7
laravel11x: missing_feature (pending to be implemented on this weblog)
+ symfony7x: missing_feature (pending to be implemented on this weblog)
tests/debugger/test_debugger_probe_snapshot.py::Test_Debugger_Method_Probe_Snaphots_With_SCM::test_mix_snapshot: missing_feature (requires line probe infrastructure not yet implemented)
tests/debugger/test_debugger_probe_status.py::Test_Debugger_Line_Probe_Statuses: missing_feature (line probe infrastructure not implemented)
tests/debugger/test_debugger_probe_status.py::Test_Debugger_Method_Probe_Statuses:
@@ -700,6 +725,7 @@ manifest:
- weblog_declaration:
"*": v1.13.0+4663b2fa7c20c6920f347d059b57dc2a419cb7f7
laravel11x: missing_feature (pending to be implemented on this weblog)
+ symfony7x: missing_feature (pending to be implemented on this weblog)
tests/debugger/test_debugger_telemetry.py::Test_Debugger_Telemetry::test_telemetry_symdb: missing_feature (SymDb not implemented in PHP)
tests/docker_ssi/test_docker_ssi.py::TestDockerSSIFeatures::test_injection_metadata:
- declaration: missing_feature (Not implemented yet)
@@ -713,15 +739,18 @@ manifest:
- weblog_declaration:
"*": v1.21.0-dev
laravel11x: missing_feature (pending to be implemented on this weblog)
+ symfony7x: missing_feature (pending to be implemented on this weblog)
tests/ffe/test_exposures.py:
- weblog_declaration:
"*": v1.21.0-dev
laravel11x: missing_feature (pending to be implemented on this weblog)
+ symfony7x: missing_feature (pending to be implemented on this weblog)
tests/ffe/test_flag_eval_evp.py: missing_feature (FFL-2446)
tests/ffe/test_flag_eval_metrics.py:
- weblog_declaration:
"*": v1.21.0-dev
laravel11x: missing_feature (pending to be implemented on this weblog)
+ symfony7x: missing_feature (pending to be implemented on this weblog)
tests/integration_frameworks/llm/anthropic/test_anthropic_llmobs.py::TestAnthropicLlmObsMessages::test_create_error: bug (MLOB-1234)
tests/integrations/crossed_integrations/test_kafka.py::Test_Kafka: missing_feature
tests/integrations/crossed_integrations/test_kinesis.py::Test_Kinesis_PROPAGATION_VIA_MESSAGE_ATTRIBUTES: missing_feature
@@ -769,12 +798,14 @@ manifest:
- weblog_declaration:
"*": v0.91.1
laravel11x: missing_feature (pending to be implemented on this weblog)
+ symfony7x: missing_feature (pending to be implemented on this weblog)
tests/integrations/test_dbm.py::Test_Dbm_Comment_NodeJS_mysql2: irrelevant (These are nodejs only tests.)
tests/integrations/test_dbm.py::Test_Dbm_Comment_NodeJS_pg: irrelevant (These are nodejs only tests.)
tests/integrations/test_dbm.py::Test_Dbm_Comment_Postgres:
- weblog_declaration:
"*": v0.91.1
laravel11x: missing_feature (pending to be implemented on this weblog)
+ symfony7x: missing_feature (pending to be implemented on this weblog)
tests/integrations/test_dbm.py::Test_Dbm_Comment_Python_Aiomysql: irrelevant (These are python only tests.)
tests/integrations/test_dbm.py::Test_Dbm_Comment_Python_Asyncpg: irrelevant (These are python only tests.)
tests/integrations/test_dbm.py::Test_Dbm_Comment_Python_MysqlConnector: irrelevant (These are python only tests.)
@@ -800,14 +831,17 @@ manifest:
- weblog_declaration:
"*": v1.8.0
laravel11x: missing_feature (pending to be implemented on this weblog)
+ symfony7x: missing_feature (pending to be implemented on this weblog)
tests/integrations/test_inferred_proxy.py::Test_AWS_API_Gateway_Inferred_Span_Creation_With_Distributed_Context:
- weblog_declaration:
"*": v1.8.0
laravel11x: missing_feature (pending to be implemented on this weblog)
+ symfony7x: missing_feature (pending to be implemented on this weblog)
tests/integrations/test_inferred_proxy.py::Test_AWS_API_Gateway_Inferred_Span_Creation_With_Error:
- weblog_declaration:
"*": v1.8.0
laravel11x: missing_feature (pending to be implemented on this weblog)
+ symfony7x: missing_feature (pending to be implemented on this weblog)
tests/integrations/test_inferred_proxy.py::Test_AWS_API_Gateway_Inferred_Span_Creation_v2: missing_feature
tests/integrations/test_mongo.py::Test_Mongo:
- declaration: missing_feature (mongodb PHP extension not available in apache-mod containers)
@@ -821,6 +855,7 @@ manifest:
- weblog_declaration:
"*": v1.17.0
laravel11x: missing_feature (pending to be implemented on this weblog)
+ symfony7x: missing_feature (pending to be implemented on this weblog)
tests/k8s_lib_injection/test_k8s_lib_injection_appsec.py::TestK8sLibInjectionAppsecClusterEnabled: v1.16.0
tests/k8s_lib_injection/test_k8s_lib_injection_appsec.py::TestK8sLibInjectionAppsecDisabledByDefault: v1.16.0
tests/k8s_lib_injection/test_k8s_lib_injection_profiling.py::TestK8sLibInjectioProfilingClusterEnabled: v1.9.0
@@ -1095,11 +1130,13 @@ manifest:
tests/stats/test_stats.py::Test_Client_Stats::test_client_stats:
- weblog_declaration:
laravel11x: missing_feature (pending to be implemented on this weblog)
+ symfony7x: missing_feature (pending to be implemented on this weblog)
tests/stats/test_stats.py::Test_Client_Stats::test_disable: v1.17.0
tests/stats/test_stats.py::Test_Client_Stats::test_grpc_status_code: missing_feature (PHP does not support gRPC)
tests/stats/test_stats.py::Test_Client_Stats::test_is_trace_root:
- weblog_declaration:
laravel11x: missing_feature (pending to be implemented on this weblog)
+ symfony7x: missing_feature (pending to be implemented on this weblog)
tests/stats/test_stats.py::Test_Client_Stats::test_obfuscation: missing_feature
tests/stats/test_stats.py::Test_Client_Stats_Future_Obfuscation_Version: missing_feature
tests/stats/test_stats.py::Test_Client_Stats_Missing_Obfuscation_Version: missing_feature
@@ -1110,6 +1147,7 @@ manifest:
tests/test_baggage.py::Test_Baggage_Headers_Api_Datadog:
- weblog_declaration:
laravel11x: missing_feature (pending to be implemented on this weblog)
+ symfony7x: missing_feature (pending to be implemented on this weblog)
tests/test_baggage.py::Test_Baggage_Headers_Basic: incomplete_test_app (/make_distant_call endpoint is not correctly implemented)
tests/test_baggage.py::Test_Baggage_Headers_Malformed: incomplete_test_app (/make_distant_call endpoint is not correctly implemented)
tests/test_baggage.py::Test_Baggage_Headers_Malformed2: incomplete_test_app (/make_distant_call endpoint is not correctly implemented)
@@ -1251,6 +1289,9 @@ manifest:
tests/test_sampling_rates.py::Test_SamplingDecisions: v1.13.0+4663b2fa7c20c6920f347d059b57dc2a419cb7f7
tests/test_sampling_rates.py::Test_SamplingDeterminism: v1.7.2 # real version unknown
tests/test_sampling_rates.py::Test_SamplingRates: v1.7.2 # real version unknown
+ tests/test_scrubbing.py::Test_UrlField::test_main:
+ - weblog_declaration:
+ symfony7x: missing_feature (pending to be implemented on this weblog)
tests/test_scrubbing.py::Test_UrlQuery: v0.76.0
tests/test_semantic_conventions.py::Test_Meta::test_meta_component_tag: bug (APMAPI-920)
tests/test_semantic_conventions.py::Test_Meta::test_meta_http_url:
diff --git a/tests/appsec/test_automated_login_events.py b/tests/appsec/test_automated_login_events.py
index d21e86e3af7..4b02fad47cb 100644
--- a/tests/appsec/test_automated_login_events.py
+++ b/tests/appsec/test_automated_login_events.py
@@ -1155,10 +1155,13 @@ def validate_iden(meta: dict):
libs_without_user_id_on_failure = ["nodejs", "java"]
# Weblog variants that omit appsec.events.users.login.failure.usr.exists (library-wide skips use libs_without_user_exist).
-weblogs_without_user_exist = ["laravel11x"]
+weblogs_without_user_exist = ["laravel11x", "symfony7x"]
# Weblog variants that omit usr.id on login failure (library-wide: libs_without_user_id_on_failure).
-weblogs_without_user_id_on_failure = ["laravel11x"]
+weblogs_without_user_id_on_failure = ["laravel11x", "symfony7x"]
+
+# Weblog variants that omit usr.id on login success (library-wide: libs_without_user_id).
+weblogs_without_user_id = ["symfony7x"]
def login_failure_includes_usr_exists_meta() -> bool:
@@ -1174,6 +1177,11 @@ def login_failure_includes_usr_id_meta() -> bool:
)
+def login_success_includes_usr_id_meta() -> bool:
+ """True when this library and weblog emit usr.id on login success spans."""
+ return context.library not in libs_without_user_id and context.weblog_variant not in weblogs_without_user_id
+
+
@rfc("https://docs.google.com/document/d/1RT38U6dTTcB-8muiYV4-aVDCsT_XrliyakjtAPyjUpw")
@features.user_monitoring
@features.user_id_collection_modes
@@ -1226,7 +1234,7 @@ def test_login_success_local(self):
assert_boolean_meta_tag(meta, "appsec.events.users.login.success.track")
# optional (to review for each library)
- if context.library not in libs_without_user_id:
+ if login_success_includes_usr_id_meta():
assert meta["usr.id"] == "social-security-id"
assert meta["_dd.appsec.usr.id"] == "social-security-id"
@@ -1248,7 +1256,7 @@ def test_login_success_basic(self):
assert_boolean_meta_tag(meta, "appsec.events.users.login.success.track")
# optional (to review for each library)
- if context.library not in libs_without_user_id:
+ if login_success_includes_usr_id_meta():
assert meta["usr.id"] == "social-security-id"
assert meta["_dd.appsec.usr.id"] == "social-security-id"
@@ -1364,7 +1372,7 @@ def test_login_sdk_success_local(self):
assert_boolean_meta_tag(meta, "_dd.appsec.events.users.login.success.sdk")
# optional (to review for each library)
- if context.library not in libs_without_user_id:
+ if login_success_includes_usr_id_meta():
assert meta["usr.id"] == "sdkUser"
assert meta["_dd.appsec.usr.id"] == "social-security-id"
@@ -1394,7 +1402,7 @@ def test_login_sdk_success_basic(self):
assert_boolean_meta_tag(meta, "_dd.appsec.events.users.login.success.sdk")
# optional (to review for each library)
- if context.library not in libs_without_user_id:
+ if login_success_includes_usr_id_meta():
assert meta["usr.id"] == "sdkUser"
assert meta["_dd.appsec.usr.id"] == "social-security-id"
@@ -1462,7 +1470,7 @@ def test_signup_local(self):
assert_boolean_meta_tag(meta, "appsec.events.users.signup.track")
# optional (to review for each library)
- if context.library not in libs_without_user_id:
+ if login_success_includes_usr_id_meta():
assert meta["appsec.events.users.signup.usr.id"] == "new-user"
assert meta["_dd.appsec.usr.id"] == "new-user"
@@ -1532,7 +1540,7 @@ def test_login_success_local(self):
assert is_same_boolean(actual=meta["appsec.events.users.login.success.track"], expected="true")
# optional (to review for each library)
- if context.library not in libs_without_user_id:
+ if login_success_includes_usr_id_meta():
assert meta["usr.id"] == USER_HASH
assert meta["_dd.appsec.usr.id"] == USER_HASH
@@ -1552,7 +1560,7 @@ def test_login_success_basic(self):
assert is_same_boolean(actual=meta["appsec.events.users.login.success.track"], expected="true")
# optional (to review for each library)
- if context.library not in libs_without_user_id:
+ if login_success_includes_usr_id_meta():
assert meta["usr.id"] == USER_HASH
assert meta["_dd.appsec.usr.id"] == USER_HASH
@@ -1668,7 +1676,7 @@ def test_login_sdk_success_local(self):
assert is_same_boolean(actual=meta["_dd.appsec.events.users.login.success.sdk"], expected="true")
# optional (to review for each library)
- if context.library not in libs_without_user_id:
+ if login_success_includes_usr_id_meta():
assert meta["usr.id"] == "sdkUser"
assert meta["_dd.appsec.usr.id"] == USER_HASH
@@ -1696,7 +1704,7 @@ def test_login_sdk_success_basic(self):
assert is_same_boolean(actual=meta["_dd.appsec.events.users.login.success.sdk"], expected="true")
# optional (to review for each library)
- if context.library not in libs_without_user_id:
+ if login_success_includes_usr_id_meta():
assert meta["usr.id"] == "sdkUser"
assert meta["_dd.appsec.usr.id"] == USER_HASH
@@ -1764,7 +1772,7 @@ def test_signup_local(self):
assert is_same_boolean(actual=meta["appsec.events.users.signup.track"], expected="true")
# optional (to review for each library)
- if context.library not in libs_without_user_id:
+ if login_success_includes_usr_id_meta():
assert meta["appsec.events.users.signup.usr.id"] == NEW_USER_HASH
assert meta["_dd.appsec.usr.id"] == NEW_USER_HASH
@@ -1903,7 +1911,7 @@ def test_login_event_blocking_auto_id(self):
assert self.config_state_1.state == rc.ApplyState.ACKNOWLEDGED
assert self.config_state_2.state == rc.ApplyState.ACKNOWLEDGED
- if context.library not in libs_without_user_id:
+ if login_success_includes_usr_id_meta():
interfaces.library.assert_waf_attack(self.r_login_blocked, rule="block-user-id")
assert self.r_login_blocked.status_code == 403
diff --git a/utils/build/docker/php/symfony7x.Dockerfile b/utils/build/docker/php/symfony7x.Dockerfile
new file mode 100644
index 00000000000..6ef4175ee6b
--- /dev/null
+++ b/utils/build/docker/php/symfony7x.Dockerfile
@@ -0,0 +1,60 @@
+ARG PHP_VERSION=8.2
+ARG VARIANT=release
+
+FROM datadog/dd-appsec-php-ci:php-$PHP_VERSION-$VARIANT
+
+ENV PHP_VERSION=8.2
+ENV DD_TRACE_ENABLED=1
+ENV DD_TRACE_GENERATE_ROOT_SPAN=1
+ENV DD_TRACE_AGENT_FLUSH_AFTER_N_REQUESTS=0
+ENV DD_TRACE_HEADER_TAGS=user-agent
+
+EXPOSE 7777/tcp
+
+ADD binaries* /binaries/
+ADD utils/build/docker/php /tmp/php
+
+# Pre-create .env with APP_SECRET and APP_ENV
+RUN mkdir -p /var/www/html && \
+ echo "APP_ENV=prod" > /var/www/html/.env && \
+ php -r "echo 'APP_SECRET=' . bin2hex(random_bytes(16)) . PHP_EOL;" >> /var/www/html/.env && \
+ echo "APP_DEBUG=0" >> /var/www/html/.env && \
+ echo "SYMFONY_DB_PATH=/tmp/symfony.db" >> /var/www/html/.env
+
+RUN chmod +x /tmp/php/apache-mod/build.sh
+RUN /tmp/php/apache-mod/build.sh symfony7x
+
+# Use Symfony-specific Apache config (DocumentRoot public/, AllowOverride All)
+RUN cp /tmp/php/weblogs/symfony7x/.apache.conf /etc/apache2/mods-available/php.conf && \
+ sed -i "s/%PHP_MAJOR_VERSION//g" /etc/apache2/mods-available/php.conf && \
+ ln -sf /etc/apache2/mods-available/php.conf /etc/apache2/mods-enabled/php.conf
+
+# pdo_sqlite, dom, and tokenizer are needed by Symfony (mirrors laravel11x pattern; tokenizer for attribute routing)
+RUN printf "extension=dom.so\nextension=pdo_sqlite.so\nextension=tokenizer.so\n" >> /etc/php/php.ini
+
+# Create log directory before running PHP (php.ini writes error_log there)
+RUN mkdir -p /var/log/system-tests
+
+# Create SQLite database and seed test users (script is at /var/www/html/bin/init-db.php via build.sh)
+RUN SYMFONY_DB_PATH=/tmp/symfony.db php /var/www/html/bin/init-db.php && \
+ chmod 666 /tmp/symfony.db
+
+# Warm up Symfony cache
+RUN cd /var/www/html && APP_ENV=prod php bin/console cache:warmup
+
+# Install ddtrace (same pattern as apache-mod-X.Y.Dockerfiles)
+ADD utils/build/docker/php/common/install_ddtrace.sh /install_ddtrace.sh
+RUN /install_ddtrace.sh 1
+
+# Set writable permissions on var/ directory
+RUN chmod -R 775 /var/www/html/var && \
+ chown -R www-data:www-data /var/www/html/var
+
+RUN rm -rf /tmp/php/
+
+ADD utils/build/docker/php/apache-mod/entrypoint.sh /
+WORKDIR /binaries
+ENTRYPOINT []
+RUN echo "#!/bin/bash\ndumb-init /entrypoint.sh" > app.sh
+RUN chmod +x app.sh
+CMD [ "./app.sh" ]
diff --git a/utils/build/docker/php/weblogs/symfony7x/.apache.conf b/utils/build/docker/php/weblogs/symfony7x/.apache.conf
new file mode 100644
index 00000000000..5538bf820a5
--- /dev/null
+++ b/utils/build/docker/php/weblogs/symfony7x/.apache.conf
@@ -0,0 +1,15 @@
+
+
+ DocumentRoot /var/www/html/public
+ AllowEncodedSlashes NoDecode
+
+ AllowOverride All
+ Require all granted
+
+
+
+ SetHandler application/x-httpd-php
+ Require all granted
+
+ PHPIniDir "/etc/php/php.ini"
+
diff --git a/utils/build/docker/php/weblogs/symfony7x/Acme/composer.json b/utils/build/docker/php/weblogs/symfony7x/Acme/composer.json
new file mode 100644
index 00000000000..41c06c55248
--- /dev/null
+++ b/utils/build/docker/php/weblogs/symfony7x/Acme/composer.json
@@ -0,0 +1,12 @@
+{
+ "name": "weblog/acme",
+ "description": "Just a package to test the composer libraries are sent",
+ "type": "library",
+ "require": {},
+ "version": "1.0.0",
+ "autoload": {
+ "psr-4": {
+ "Acme\\": "src/"
+ }
+ }
+}
diff --git a/utils/build/docker/php/weblogs/symfony7x/Acme/src/Acme.php b/utils/build/docker/php/weblogs/symfony7x/Acme/src/Acme.php
new file mode 100644
index 00000000000..c44dcf88aa6
--- /dev/null
+++ b/utils/build/docker/php/weblogs/symfony7x/Acme/src/Acme.php
@@ -0,0 +1,7 @@
+getParameterOption(['--env', '-e'], $_SERVER['APP_ENV'] ?? 'prod', true);
+$debug = (bool) ($_SERVER['APP_DEBUG'] ?? ('prod' !== $env)) && !$input->hasParameterOption('--no-debug', true);
+
+$kernel = new Kernel($env, $debug);
+$app = new Application($kernel);
+$app->run($input);
diff --git a/utils/build/docker/php/weblogs/symfony7x/bin/init-db.php b/utils/build/docker/php/weblogs/symfony7x/bin/init-db.php
new file mode 100644
index 00000000000..8a19aac5584
--- /dev/null
+++ b/utils/build/docker/php/weblogs/symfony7x/bin/init-db.php
@@ -0,0 +1,37 @@
+#!/usr/bin/env php
+ 'pdo_sqlite', 'path' => $dbPath]);
+$em = new EntityManager($connection, $config);
+
+(new SchemaTool($em))->createSchema($em->getMetadataFactory()->getAllMetadata());
+
+$seeds = [
+ ['id' => 'social-security-id', 'username' => 'test', 'password' => '1234'],
+ ['id' => '591dc126-8431-4d0f-9509-b23318d3dce4', 'username' => 'testuuid', 'password' => '1234'],
+];
+
+foreach ($seeds as $seed) {
+ if ($em->find(User::class, $seed['id']) === null) {
+ $em->persist(new User($seed['id'], $seed['username'], password_hash($seed['password'], PASSWORD_BCRYPT), $seed['username']));
+ }
+}
+
+$em->flush();
+
+echo "Database initialized at $dbPath\n";
diff --git a/utils/build/docker/php/weblogs/symfony7x/composer.json b/utils/build/docker/php/weblogs/symfony7x/composer.json
new file mode 100644
index 00000000000..3491a81391d
--- /dev/null
+++ b/utils/build/docker/php/weblogs/symfony7x/composer.json
@@ -0,0 +1,59 @@
+{
+ "name": "php/symfony-weblog",
+ "type": "project",
+ "require": {
+ "php": ">=8.2",
+ "ext-ctype": "*",
+ "ext-iconv": "*",
+ "doctrine/doctrine-bundle": "^2.13",
+ "doctrine/orm": "^3.3",
+ "stripe/stripe-php": "^10.0",
+ "symfony/console": "^7.2",
+ "symfony/framework-bundle": "^7.2",
+ "symfony/security-bundle": "^7.2",
+ "symfony/dom-crawler": "^7.2",
+ "symfony/filesystem": "^7.2",
+ "symfony/http-client": "^7.2",
+ "symfony/process": "^7.2",
+ "symfony/yaml": "^7.2",
+ "monolog/monolog": "^3.0",
+ "weblog/acme": "*"
+ },
+ "repositories": [
+ {
+ "type": "path",
+ "url": "./Acme",
+ "options": {
+ "symlink": true
+ }
+ }
+ ],
+ "autoload": {
+ "psr-4": {
+ "App\\": "src/"
+ }
+ },
+ "config": {
+ "optimize-autoloader": true,
+ "preferred-install": "dist",
+ "sort-packages": true,
+ "allow-plugins": {
+ "symfony/flex": false
+ },
+ "platform": {
+ "ext-ctype": "8.2.0",
+ "ext-iconv": "8.2.0",
+ "ext-curl": "8.2.0",
+ "ext-json": "8.2.0",
+ "ext-mbstring": "8.2.0",
+ "ext-openssl": "8.2.0",
+ "ext-pdo": "8.2.0",
+ "ext-session": "8.2.0",
+ "ext-tokenizer": "8.2.0",
+ "ext-dom": "8.2.0",
+ "ext-xml": "8.2.0"
+ }
+ },
+ "minimum-stability": "stable",
+ "prefer-stable": true
+}
diff --git a/utils/build/docker/php/weblogs/symfony7x/config/bundles.php b/utils/build/docker/php/weblogs/symfony7x/config/bundles.php
new file mode 100644
index 00000000000..a7afe818200
--- /dev/null
+++ b/utils/build/docker/php/weblogs/symfony7x/config/bundles.php
@@ -0,0 +1,7 @@
+ ['all' => true],
+ Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true],
+ Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true],
+];
diff --git a/utils/build/docker/php/weblogs/symfony7x/config/packages/doctrine.yaml b/utils/build/docker/php/weblogs/symfony7x/config/packages/doctrine.yaml
new file mode 100644
index 00000000000..145bf09e1a1
--- /dev/null
+++ b/utils/build/docker/php/weblogs/symfony7x/config/packages/doctrine.yaml
@@ -0,0 +1,36 @@
+parameters:
+ default_sqlite_path: '/tmp/symfony.db'
+
+doctrine:
+ dbal:
+ default_connection: default
+ connections:
+ default:
+ driver: pdo_sqlite
+ path: '%env(default:default_sqlite_path:SYMFONY_DB_PATH)%'
+ server_version: '3.39'
+ mysql:
+ driver: pdo_mysql
+ host: mysqldb
+ dbname: mysql_dbname
+ user: mysqldb
+ password: mysqldb
+ server_version: '8.0'
+ postgres:
+ driver: pdo_pgsql
+ host: postgres
+ port: 5433
+ dbname: system_tests_dbname
+ user: system_tests_user
+ password: system_tests
+ server_version: '14.0'
+ orm:
+ auto_generate_proxy_classes: true
+ naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
+ mappings:
+ App:
+ is_bundle: false
+ type: attribute
+ dir: '%kernel.project_dir%/src/Entity'
+ prefix: 'App\Entity'
+ alias: App
diff --git a/utils/build/docker/php/weblogs/symfony7x/config/packages/framework.yaml b/utils/build/docker/php/weblogs/symfony7x/config/packages/framework.yaml
new file mode 100644
index 00000000000..d9b11a8807e
--- /dev/null
+++ b/utils/build/docker/php/weblogs/symfony7x/config/packages/framework.yaml
@@ -0,0 +1,16 @@
+framework:
+ secret: '%env(APP_SECRET)%'
+ http_method_override: false
+ handle_all_throwables: true
+
+ session:
+ handler_id: null
+ cookie_secure: auto
+ cookie_samesite: lax
+ storage_factory_id: session.storage.factory.native
+
+ php_errors:
+ log: true
+
+ router:
+ utf8: true
diff --git a/utils/build/docker/php/weblogs/symfony7x/config/packages/security.yaml b/utils/build/docker/php/weblogs/symfony7x/config/packages/security.yaml
new file mode 100644
index 00000000000..def0cc5f4a4
--- /dev/null
+++ b/utils/build/docker/php/weblogs/symfony7x/config/packages/security.yaml
@@ -0,0 +1,25 @@
+security:
+ password_hashers:
+ App\Entity\User:
+ algorithm: bcrypt
+
+ providers:
+ app_user_provider:
+ entity:
+ class: App\Entity\User
+ property: username
+ firewalls:
+ main:
+ pattern: ^/
+ lazy: true
+ provider: app_user_provider
+ stateless: false
+ form_login:
+ login_path: login
+ check_path: login
+ username_parameter: username
+ password_parameter: password
+ success_handler: App\Security\LoginSuccessHandler
+ failure_handler: App\Security\LoginFailureHandler
+
+ access_control: []
diff --git a/utils/build/docker/php/weblogs/symfony7x/config/routes.yaml b/utils/build/docker/php/weblogs/symfony7x/config/routes.yaml
new file mode 100644
index 00000000000..ea9db25594f
--- /dev/null
+++ b/utils/build/docker/php/weblogs/symfony7x/config/routes.yaml
@@ -0,0 +1,22 @@
+controllers:
+ resource:
+ path: ../src/Controller/
+ namespace: App\Controller
+ type: attribute
+
+# Override WAF routes to disable strict_slashes so /waf/ does not 301-redirect to /waf
+waf:
+ path: /waf
+ controller: App\Controller\AppController::waf
+ methods: [GET, POST]
+ options:
+ strict_slashes: false
+
+waf_path:
+ path: /waf/{path}
+ controller: App\Controller\AppController::wafPath
+ methods: [GET, POST]
+ requirements:
+ path: '.*'
+ options:
+ strict_slashes: false
diff --git a/utils/build/docker/php/weblogs/symfony7x/config/services.yaml b/utils/build/docker/php/weblogs/symfony7x/config/services.yaml
new file mode 100644
index 00000000000..4f47b4c2262
--- /dev/null
+++ b/utils/build/docker/php/weblogs/symfony7x/config/services.yaml
@@ -0,0 +1,10 @@
+services:
+ _defaults:
+ autowire: true
+ autoconfigure: true
+
+ App\:
+ resource: '../src/'
+ exclude:
+ - '../src/Kernel.php'
+ - '../src/Security/User.php'
diff --git a/utils/build/docker/php/weblogs/symfony7x/public/.htaccess b/utils/build/docker/php/weblogs/symfony7x/public/.htaccess
new file mode 100644
index 00000000000..2ce752e0db0
--- /dev/null
+++ b/utils/build/docker/php/weblogs/symfony7x/public/.htaccess
@@ -0,0 +1,27 @@
+DirectoryIndex index.php
+
+
+ Options -MultiViews
+
+
+
+ RewriteEngine On
+
+ RewriteCond %{REQUEST_URI}::$0 ^(/.+)/(.*)::\2$
+ RewriteRule .* - [E=BASE:%1]
+
+ RewriteCond %{HTTP:Authorization} .+
+ RewriteRule ^ - [E=HTTP_AUTHORIZATION:%0]
+
+ RewriteCond %{ENV:REDIRECT_STATUS} =""
+ RewriteRule ^index\.php(?:/(.*)|$) %{ENV:BASE}/$1 [R=301,L]
+
+ RewriteCond %{REQUEST_FILENAME} !-f
+ RewriteRule ^ %{ENV:BASE}/index.php [L]
+
+
+
+
+ RedirectMatch 307 ^/$ /index.php/
+
+
diff --git a/utils/build/docker/php/weblogs/symfony7x/public/index.php b/utils/build/docker/php/weblogs/symfony7x/public/index.php
new file mode 100644
index 00000000000..4c0649b3bcd
--- /dev/null
+++ b/utils/build/docker/php/weblogs/symfony7x/public/index.php
@@ -0,0 +1,20 @@
+ 1 && substr($_path, -1) === '/') {
+ $_SERVER['REQUEST_URI'] = rtrim($_path, '/') . ($_qpos !== false ? substr($_uri, $_qpos) : '');
+}
+
+$kernel = new Kernel($_SERVER['APP_ENV'] ?? 'prod', (bool) ($_SERVER['APP_DEBUG'] ?? false));
+$request = Request::createFromGlobals();
+$response = $kernel->handle($request);
+$response->send();
+$kernel->terminate($request, $response);
diff --git a/utils/build/docker/php/weblogs/symfony7x/src/Controller/AppController.php b/utils/build/docker/php/weblogs/symfony7x/src/Controller/AppController.php
new file mode 100644
index 00000000000..86ac75c3318
--- /dev/null
+++ b/utils/build/docker/php/weblogs/symfony7x/src/Controller/AppController.php
@@ -0,0 +1,884 @@
+ 'text/plain; charset=utf-8',
+ 'Content-Length' => '13',
+ ]);
+ }
+
+ #[Route('/stats-unique', name: 'stats_unique', methods: ['GET'])]
+ public function statsUnique(Request $request): Response
+ {
+ $code = (int) $request->query->get('code', 200);
+
+ return new Response('', $code);
+ }
+
+ #[Route('/sample_rate_route/{i}', name: 'sample_rate_route', methods: ['GET'])]
+ public function sampleRateRoute(): Response
+ {
+ return new Response('OK', 200, ['Content-Type' => 'text/plain']);
+ }
+
+ #[Route('/healthcheck', name: 'healthcheck', methods: ['GET'])]
+ public function healthcheck(): JsonResponse
+ {
+ $version = phpversion('ddtrace') ?: 'unknown';
+
+ return new JsonResponse([
+ 'status' => 'ok',
+ 'library' => ['name' => 'php', 'version' => $version],
+ ]);
+ }
+
+ #[Route('/status', name: 'status', methods: ['GET'])]
+ public function status(Request $request): Response
+ {
+ $code = intval($request->query->get('code', 200));
+
+ return new Response('', $code);
+ }
+
+ #[Route('/make_distant_call', name: 'make_distant_call', methods: ['GET'])]
+ public function makeDistantCall(Request $request): JsonResponse
+ {
+ $url = $request->query->get('url', '');
+ if ($url === '') {
+ return new JsonResponse(['error' => 'url parameter required'], 400);
+ }
+
+ $client = HttpClient::create();
+ $response = $client->request('GET', $url);
+ $statusCode = $response->getStatusCode();
+ $responseHeaders = [];
+ foreach ($response->getHeaders(false) as $name => $values) {
+ $responseHeaders[$name] = implode(', ', $values);
+ }
+ $requestHeaders = $this->parseRequestHeadersFromDebug($response->getInfo('debug') ?? '');
+
+ return new JsonResponse([
+ 'url' => $url,
+ 'status_code' => $statusCode,
+ 'request_headers' => $requestHeaders,
+ 'response_headers' => $responseHeaders,
+ ]);
+ }
+
+ #[Route('/read_file', name: 'read_file', methods: ['GET'])]
+ public function readFile(Request $request): Response
+ {
+ $filePath = $request->query->get('file', '');
+ $content = '';
+ try {
+ $content = (new Filesystem())->readFile($filePath);
+ } catch (IOException $e) {}
+
+ return new Response($content, 200, ['Content-Type' => 'text/plain']);
+ }
+
+ #[Route('/users', name: 'users', methods: ['GET'])]
+ public function users(Request $request): Response
+ {
+ $user = $request->query->get('user');
+
+ \DDTrace\set_user($user, [
+ 'name' => 'usr.name',
+ 'email' => 'usr.email',
+ 'session_id' => 'usr.session_id',
+ 'role' => 'usr.role',
+ 'scope' => 'usr.scope',
+ ]);
+
+ if ($user === 'sdkUser') {
+ \datadog\appsec\track_authenticated_user_event($user);
+ }
+
+ return new Response('OK', 200, ['Content-Type' => 'text/plain']);
+ }
+
+ #[Route('/identify', name: 'identify', methods: ['GET'])]
+ public function identify(): Response
+ {
+ \DDTrace\set_user('usr.id', [
+ 'name' => 'usr.name',
+ 'email' => 'usr.email',
+ 'session_id' => 'usr.session_id',
+ 'role' => 'usr.role',
+ 'scope' => 'usr.scope',
+ ]);
+
+ return new Response('', 200);
+ }
+
+ #[Route('/identify-propagate', name: 'identify_propagate', methods: ['GET'])]
+ public function identifyPropagate(): Response
+ {
+ \DDTrace\set_user('usr.id', [
+ 'name' => 'usr.name',
+ 'email' => 'usr.email',
+ 'session_id' => 'usr.session_id',
+ 'role' => 'usr.role',
+ 'scope' => 'usr.scope',
+ ], true);
+
+ return new Response('', 200);
+ }
+
+ #[Route('/waf', name: 'waf', methods: ['GET', 'POST'])]
+ public function waf(): Response
+ {
+ return new Response('Hello, WAF!', 200, ['Content-Type' => 'text/plain']);
+ }
+
+ #[Route('/waf/{path}', name: 'waf_path', requirements: ['path' => '.*'], methods: ['GET', 'POST'])]
+ public function wafPath(): Response
+ {
+ return new Response('Hello, WAF!', 200, ['Content-Type' => 'text/plain']);
+ }
+
+ #[Route('/headers', name: 'headers', methods: ['GET'])]
+ public function headers(): Response
+ {
+ return new Response('Hello, headers!', 200, [
+ 'Content-Type' => 'text/plain',
+ 'Content-Length' => '15',
+ 'Content-Language' => 'en-US',
+ ]);
+ }
+
+ #[Route('/session/new', name: 'session_new', methods: ['GET'])]
+ public function sessionNew(Request $request): Response
+ {
+ $session = $request->getSession();
+ $session->start();
+
+ return new Response($session->getId(), 200, ['Content-Type' => 'text/plain']);
+ }
+
+ #[Route('/user_login_success_event', name: 'user_login_success_event', methods: ['GET'])]
+ public function userLoginSuccessEvent(Request $request): Response
+ {
+ \datadog\appsec\track_user_login_success_event($request->query->get('event_user_id', 'system_tests_user'), [
+ 'metadata0' => 'value0',
+ 'metadata1' => 'value1',
+ ]);
+
+ return new Response('', 200);
+ }
+
+ #[Route('/user_login_failure_event', name: 'user_login_failure_event', methods: ['GET'])]
+ public function userLoginFailureEvent(Request $request): Response
+ {
+ $exists = $request->query->get('event_user_exists', 'true');
+ $existsBool = filter_var($exists, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
+ if ($existsBool === null) {
+ $existsBool = true;
+ }
+
+ \datadog\appsec\track_user_login_failure_event(
+ $request->query->get('event_user_id', 'system_tests_user'),
+ $existsBool,
+ [
+ 'metadata0' => 'value0',
+ 'metadata1' => 'value1',
+ ]
+ );
+
+ return new Response('', 200);
+ }
+
+ #[Route('/custom_event', name: 'custom_event', methods: ['GET'])]
+ public function customEvent(Request $request): Response
+ {
+ \datadog\appsec\track_custom_event($request->query->get('event_name', 'system_tests_event'), [
+ 'metadata0' => 'value0',
+ 'metadata1' => 'value1',
+ ]);
+
+ return new Response('', 200);
+ }
+
+ #[Route('/shell_execution', name: 'shell_execution', methods: ['POST'])]
+ public function shellExecution(Request $request): Response
+ {
+ $spec = json_decode($request->getContent(), true) ?: [];
+ $command = $spec['command'] ?? '';
+ $options = $spec['options'] ?? [];
+ $args = $spec['args'] ?? [];
+ $isShell = !empty($options['shell']);
+
+ if (is_string($args)) {
+ $args = preg_split('/\s+/', $args);
+ }
+ $c = array_merge([$command], $args);
+ if ($isShell) {
+ $c = implode(' ', $c);
+ }
+
+ try {
+ $process = $isShell ? Process::fromShellCommandline($c) : new Process($c);
+ $process->run();
+ } catch (ProcessRuntimeException) {
+ return new Response('Failed to open process', 500);
+ }
+
+ $out = "STDOUT:\n{$process->getOutput()}\nSTDERR:\n{$process->getErrorOutput()}\nexit code: {$process->getExitCode()}\n";
+
+ return new Response($out, 200, ['Content-Type' => 'text/plain']);
+ }
+
+ #[Route('/tag_value/{tag_value}/{status_code}', name: 'tag_value', methods: ['GET', 'POST', 'OPTIONS'])]
+ public function tagValue(string $tag_value, string $status_code, Request $request): Response
+ {
+ $responseCodeStr = strtok($status_code, '?') ?: $status_code;
+ if (!is_numeric($responseCodeStr)) {
+ return new Response('Error parsing uri', 400);
+ }
+ $responseCode = (int) $responseCodeStr;
+ \datadog\appsec\track_custom_event('system_tests_appsec_event', [
+ 'value' => $tag_value,
+ ]);
+ foreach ($request->query->all() as $key => $value) {
+ header(ucwords($key) . ': ' . $value);
+ }
+ $body = 'Value tagged';
+ $payloadPrefix = 'payload_in_response_body';
+ if (str_starts_with($tag_value, $payloadPrefix) && $request->isMethod('POST')) {
+ $parsed = $request->request->all() ?: (json_decode($request->getContent(), true) ?? []);
+
+ return new JsonResponse(['payload' => $parsed], $responseCode === 200 ? 200 : $responseCode);
+ }
+ if ($responseCode !== 200) {
+ return new Response($body, $responseCode, ['Content-Type' => 'text/plain']);
+ }
+
+ return new Response($body, 200, ['Content-Type' => 'text/plain']);
+ }
+
+ #[Route('/params/{param}', name: 'params', methods: ['GET'])]
+ public function params(): Response
+ {
+ return new Response('ok', 200, ['Content-Type' => 'text/plain']);
+ }
+
+ #[Route('/dbm', name: 'dbm', methods: ['GET'])]
+ public function dbm(Request $request): Response
+ {
+ $integration = $request->query->get('integration', '');
+ $query = 'SELECT version()';
+
+ if ($integration === 'pdo-mysql') {
+ $this->mysqlConnection->executeQuery($query);
+ } elseif ($integration === 'pdo-pgsql') {
+ $this->postgresConnection->executeQuery($query);
+ } elseif ($integration === 'mysqli') {
+ // Doctrine DBAL 3.x dropped the mysqli driver; raw mysqli is the only option here.
+ $connection = new \mysqli('mysqldb', 'mysqldb', 'mysqldb', 'mysql_dbname');
+ $connection->query($query);
+ }
+
+ return new Response('', 200);
+ }
+
+ #[Route('/log/library', name: 'log_library', methods: ['GET'])]
+ public function logLibrary(Request $request): Response
+ {
+ $dir = $_ENV['SYSTEM_TESTS_LOGS'] ?? '/var/log/system-tests';
+ if (!is_dir($dir)) {
+ @mkdir($dir, 0777, true);
+ }
+ $msg = $request->query->get('msg', '');
+ if ($msg !== '') {
+ $logger = new Logger('system_tests');
+ $logger->pushHandler(new StreamHandler($dir . '/helper.log', Logger::DEBUG));
+ $logger->info('', ['message' => $msg]);
+ }
+
+ return new Response('', 200);
+ }
+
+ #[Route('/user_login_success_event_v2', name: 'user_login_success_event_v2', methods: ['POST'])]
+ public function userLoginSuccessEventV2(Request $request): Response
+ {
+ $decoded = json_decode($request->getContent(), true) ?: [];
+ $login = $decoded['login'] ?? null;
+ $userId = $decoded['user_id'] ?? null;
+ $metadata = $decoded['metadata'] ?? null;
+
+ if ($login !== null && $userId !== null && $metadata !== null) {
+ \datadog\appsec\v2\track_user_login_success($login, $userId, $metadata);
+ } elseif ($login !== null && $userId !== null) {
+ \datadog\appsec\v2\track_user_login_success($login, $userId);
+ } else {
+ \datadog\appsec\v2\track_user_login_success($login);
+ }
+
+ return new Response('OK', 200, ['Content-Type' => 'text/plain']);
+ }
+
+ #[Route('/user_login_failure_event_v2', name: 'user_login_failure_event_v2', methods: ['POST'])]
+ public function userLoginFailureEventV2(Request $request): Response
+ {
+ $decoded = json_decode($request->getContent(), true) ?: [];
+ $login = $decoded['login'] ?? null;
+ $exists = isset($decoded['exists']) ? ($decoded['exists'] === 'true' || $decoded['exists'] === true) : null;
+ $metadata = $decoded['metadata'] ?? null;
+
+ if ($login !== null && $exists !== null && $metadata !== null) {
+ \datadog\appsec\v2\track_user_login_failure($login, $exists, $metadata);
+ } elseif ($login !== null && $exists !== null) {
+ \datadog\appsec\v2\track_user_login_failure($login, $exists);
+ } else {
+ \datadog\appsec\v2\track_user_login_failure($login);
+ }
+
+ return new Response('OK', 200, ['Content-Type' => 'text/plain']);
+ }
+
+ #[Route('/stub_dbm', name: 'stub_dbm', methods: ['GET'])]
+ public function stubDbm(Request $request): JsonResponse
+ {
+ $integration = $request->query->get('integration', '');
+ $captured = null;
+
+ if ($integration === 'pdo-pgsql') {
+ /** @var \PDO $nativePdo */
+ $nativePdo = $this->postgresConnection->getNativeConnection();
+ $stmt = $nativePdo->query('SELECT version()');
+ $captured = $stmt instanceof \PDOStatement ? $stmt->queryString : null;
+ } elseif ($integration === 'pdo-mysql') {
+ /** @var \PDO $nativePdo */
+ $nativePdo = $this->mysqlConnection->getNativeConnection();
+ $stmt = $nativePdo->query('SELECT version()');
+ $captured = $stmt instanceof \PDOStatement ? $stmt->queryString : null;
+ }
+
+ return new JsonResponse([
+ 'status' => 'ok',
+ 'dbm_comment' => $captured,
+ ]);
+ }
+
+ #[Route('/rasp/sqli', name: 'rasp_sqli', methods: ['GET', 'POST'])]
+ public function raspSqli(Request $request): Response
+ {
+ $userId = null;
+ $contentType = $request->headers->get('Content-Type', '');
+ if ($contentType === 'application/json') {
+ $decoded = json_decode($request->getContent(), true);
+ $userId = $decoded['user_id'] ?? '';
+ } elseif ($contentType === 'application/xml') {
+ $crawler = new Crawler();
+ $crawler->addXmlContent(stripslashes($request->getContent()));
+ $userId = $crawler->text('');
+ } else {
+ $userId = urldecode($request->get('user_id', ''));
+ }
+
+ // Intentionally unsafe query so ddtrace RASP can detect the injection
+ try {
+ $this->defaultConnection->executeQuery("SELECT * FROM users WHERE id = '" . $userId . "'");
+ } catch (\Doctrine\DBAL\Exception $e) {}
+
+ return new Response('Hello, SQLi!', 200, ['Content-Type' => 'text/plain']);
+ }
+
+ #[Route('/rasp/lfi', name: 'rasp_lfi', methods: ['GET', 'POST'])]
+ public function raspLfi(Request $request): Response
+ {
+ $file = null;
+ $contentType = $request->headers->get('Content-Type', '');
+ if ($contentType === 'application/json') {
+ $decoded = json_decode($request->getContent(), true);
+ $file = $decoded['file'] ?? '';
+ } elseif ($contentType === 'application/xml') {
+ $crawler = new Crawler();
+ $crawler->addXmlContent(stripslashes($request->getContent()));
+ $file = $crawler->text('');
+ } else {
+ $file = urldecode($request->get('file', ''));
+ }
+
+ try {
+ (new Filesystem())->readFile($file);
+ } catch (IOException $e) {}
+
+ return new Response('Hello, LFI!', 200, ['Content-Type' => 'text/plain']);
+ }
+
+ #[Route('/rasp/ssrf', name: 'rasp_ssrf', methods: ['GET', 'POST'])]
+ public function raspSsrf(Request $request): Response
+ {
+ $domain = null;
+ $contentType = $request->headers->get('Content-Type', '');
+ if ($contentType === 'application/json') {
+ $decoded = json_decode($request->getContent(), true);
+ $domain = $decoded['domain'] ?? '';
+ } elseif ($contentType === 'application/xml') {
+ $crawler = new Crawler();
+ $crawler->addXmlContent(stripslashes($request->getContent()));
+ $domain = $crawler->text('');
+ } else {
+ $domain = urldecode($request->get('domain', ''));
+ }
+
+ // RASP SSRF detection hooks into file_get_contents HTTP wrappers, not into curl/HttpClient
+ @file_get_contents('http://' . $domain);
+
+ return new Response('Hello, SSRF!', 200, ['Content-Type' => 'text/plain']);
+ }
+
+ #[Route('/rasp/multiple', name: 'rasp_multiple', methods: ['GET'])]
+ public function raspMultiple(Request $request): Response
+ {
+ $filesystem = new Filesystem();
+ foreach ([
+ urldecode($request->query->get('file1', '')),
+ urldecode($request->query->get('file2', '')),
+ '../etc/passwd',
+ ] as $file) {
+ try {
+ $filesystem->readFile($file);
+ } catch (IOException $e) {}
+ }
+
+ return new Response('Hello, multiple rasp!', 200, ['Content-Type' => 'text/plain']);
+ }
+
+ #[Route('/db', name: 'db', methods: ['GET'])]
+ public function db(Request $request): Response
+ {
+ $service = $request->query->get('service', '');
+ $operation = $request->query->get('operation', '');
+
+ if ($service === 'mysql') {
+ $this->mysqlOperation($operation);
+ } elseif ($service === 'postgresql') {
+ $this->postgresOperation($operation);
+ } else {
+ return new Response('Unsupported service: ' . htmlspecialchars($service), 400, ['Content-Type' => 'text/plain']);
+ }
+
+ return new Response('YEAH', 200, ['Content-Type' => 'text/plain']);
+ }
+
+ #[Route('/trace/sql', name: 'trace_sql', methods: ['GET'])]
+ public function traceSql(): Response
+ {
+ try {
+ $this->mysqlConnection->executeQuery('SELECT 1');
+ } catch (\Doctrine\DBAL\Exception $e) {}
+
+ return new Response('OK', 200, ['Content-Type' => 'text/plain']);
+ }
+
+ #[Route('/endpoint_fallback.php', name: 'endpoint_fallback', methods: ['GET'])]
+ public function endpointFallback(Request $request): JsonResponse
+ {
+ $rootSpan = \DDTrace\root_span();
+ $case = $request->query->get('case', 'unknown');
+
+ switch ($case) {
+ case 'with_route':
+ register_shutdown_function(function () use ($rootSpan): void {
+ $rootSpan->meta['http.route'] = '/users/{id}/profile';
+ $rootSpan->meta['http.method'] = 'GET';
+ });
+
+ return new JsonResponse([
+ 'status' => 'ok',
+ 'test_case' => 'with_route',
+ 'message' => 'http.route is set',
+ ]);
+
+ case 'with_endpoint':
+ register_shutdown_function(function () use ($rootSpan): void {
+ unset($rootSpan->meta['http.route']);
+ $rootSpan->meta['http.endpoint'] = '/api/products/{param:int}';
+ $rootSpan->meta['http.method'] = 'GET';
+ });
+
+ return new JsonResponse([
+ 'status' => 'ok',
+ 'test_case' => 'with_endpoint',
+ 'message' => 'http.endpoint is set, http.route is not',
+ ]);
+
+ case '404':
+ register_shutdown_function(function () use ($rootSpan): void {
+ unset($rootSpan->meta['http.route']);
+ $rootSpan->meta['http.endpoint'] = '/api/notfound/{param:int}';
+ $rootSpan->meta['http.method'] = 'GET';
+ });
+
+ return new JsonResponse([
+ 'status' => 'error',
+ 'test_case' => '404_with_endpoint',
+ 'message' => 'Not found - should not sample despite http.endpoint',
+ ], 404);
+
+ case 'computed':
+ register_shutdown_function(function () use ($rootSpan): void {
+ unset($rootSpan->meta['http.route']);
+ $rootSpan->meta['http.url'] = 'http://localhost:8080/endpoint_fallback_computed/users/123/orders/456';
+ $rootSpan->meta['http.method'] = 'GET';
+ });
+
+ return new JsonResponse([
+ 'status' => 'ok',
+ 'test_case' => 'computed_on_demand',
+ 'message' => 'Endpoint computed from URL',
+ 'has_endpoint_tag' => isset($rootSpan->meta['http.endpoint']),
+ ]);
+
+ default:
+ return new JsonResponse([
+ 'status' => 'error',
+ 'test_case' => 'unknown',
+ 'message' => 'Invalid case parameter. Valid values: with_route, with_endpoint, 404, computed',
+ ], 400);
+ }
+ }
+
+ #[Route('/api_security_sampling/{id}', name: 'api_security_sampling', methods: ['GET'])]
+ public function apiSecuritySampling(string $id): JsonResponse
+ {
+ return new JsonResponse(['status' => 200], 200);
+ }
+
+ #[Route('/api_security/sampling/{status}', name: 'api_security_sampling_status', methods: ['GET'])]
+ public function apiSecuritySamplingStatus(string $status): Response
+ {
+ $statusCode = intval($status);
+ if ($statusCode >= 200 && $statusCode <= 599) {
+ return new JsonResponse(['status' => $statusCode], $statusCode);
+ }
+
+ return new Response('Invalid status', 400, ['Content-Type' => 'text/plain']);
+ }
+
+ #[Route('/otel_drop_in_baggage_api_otel', name: 'otel_drop_in_baggage_api_otel', methods: ['GET'])]
+ public function otelDropInBaggageApiOtel(Request $request): JsonResponse
+ {
+ $url = $request->query->get('url');
+ if ($url === null) {
+ return new JsonResponse(['error' => 'Specify the url to call in the query string'], 400);
+ }
+
+ $baggage = \OpenTelemetry\API\Baggage\Baggage::getCurrent();
+ $builder = $baggage->toBuilder();
+
+ $baggageRemove = $request->query->get('baggage_remove');
+ if ($baggageRemove !== null) {
+ foreach (explode(',', $baggageRemove) as $key) {
+ $builder = $builder->remove(trim($key));
+ }
+ }
+
+ $baggageSet = $request->query->get('baggage_set');
+ if ($baggageSet !== null) {
+ foreach (explode(',', $baggageSet) as $item) {
+ $parts = explode('=', $item, 2);
+ if (count($parts) === 2) {
+ $builder = $builder->set(trim($parts[0]), trim($parts[1]));
+ }
+ }
+ }
+
+ $scope = null;
+ try {
+ $scope = $builder->build()->activate();
+ $client = HttpClient::create();
+ $response = $client->request('GET', $url);
+ $statusCode = $response->getStatusCode();
+ $responseHeaders = [];
+ foreach ($response->getHeaders(false) as $name => $values) {
+ $responseHeaders[$name] = implode(', ', $values);
+ }
+ $requestHeaders = $this->parseRequestHeadersFromDebug($response->getInfo('debug') ?? '');
+ } finally {
+ if ($scope !== null) {
+ $scope->detach();
+ }
+ }
+
+ return new JsonResponse([
+ 'url' => $url,
+ 'status_code' => $statusCode,
+ 'request_headers' => $requestHeaders,
+ 'response_headers' => $responseHeaders,
+ ]);
+ }
+
+ #[Route('/otel_drop_in_baggage_api_datadog', name: 'otel_drop_in_baggage_api_datadog', methods: ['GET'])]
+ public function otelDropInBaggageApiDatadog(Request $request): JsonResponse
+ {
+ $url = $request->query->get('url');
+ if ($url === null) {
+ return new JsonResponse(['error' => 'Specify the url to call in the query string'], 400);
+ }
+
+ $span = \DDTrace\root_span();
+ if ($span !== null) {
+ $baggageRemove = $request->query->get('baggage_remove');
+ if ($baggageRemove !== null) {
+ foreach (explode(',', $baggageRemove) as $key) {
+ unset($span->baggage[trim($key)]);
+ }
+ }
+
+ $baggageSet = $request->query->get('baggage_set');
+ if ($baggageSet !== null) {
+ foreach (explode(',', $baggageSet) as $item) {
+ $parts = explode('=', $item, 2);
+ if (count($parts) === 2) {
+ $span->baggage[trim($parts[0])] = trim($parts[1]);
+ }
+ }
+ }
+ }
+
+ $client = HttpClient::create();
+ $response = $client->request('GET', $url);
+ $statusCode = $response->getStatusCode();
+ $responseHeaders = [];
+ foreach ($response->getHeaders(false) as $name => $values) {
+ $responseHeaders[$name] = implode(', ', $values);
+ }
+ $requestHeaders = $this->parseRequestHeadersFromDebug($response->getInfo('debug') ?? '');
+
+ return new JsonResponse([
+ 'url' => $url,
+ 'status_code' => $statusCode,
+ 'request_headers' => $requestHeaders,
+ 'response_headers' => $responseHeaders,
+ ]);
+ }
+
+ private function parseRequestHeadersFromDebug(string $debug): array
+ {
+ $headers = [];
+ $inRequest = false;
+ foreach (explode("\n", $debug) as $line) {
+ $line = rtrim($line);
+ if (str_starts_with($line, '> ')) {
+ $inRequest = true;
+ $line = substr($line, 2);
+ } elseif ($inRequest && str_starts_with($line, '< ')) {
+ break;
+ } elseif ($inRequest && $line === '') {
+ break;
+ }
+ if ($inRequest && str_contains($line, ':')) {
+ [$key, $value] = explode(':', $line, 2);
+ $headers[strtolower(trim($key))] = trim($value);
+ }
+ }
+
+ return $headers;
+ }
+
+ #[Route('/returnheaders', name: 'returnheaders', methods: ['GET'])]
+ public function returnHeaders(Request $request): JsonResponse
+ {
+ $headers = [];
+ foreach ($request->headers->all() as $key => $values) {
+ $headers[$key] = implode(', ', $values);
+ }
+
+ return new JsonResponse($headers);
+ }
+
+ #[Route('/requestdownstream', name: 'requestdownstream', methods: ['GET'])]
+ public function requestDownstream(): Response
+ {
+ $client = HttpClient::create();
+ $response = $client->request('GET', 'http://127.0.0.1:7777/returnheaders');
+ $body = $response->getContent(false);
+
+ return new Response($body, 200, ['Content-Type' => 'application/json']);
+ }
+
+ #[Route('/load_dependency', name: 'load_dependency', methods: ['GET'])]
+ public function loadDependency(): Response
+ {
+ $acme = new \Acme\Acme();
+
+ return new Response('ok', 200, ['Content-Type' => 'text/plain']);
+ }
+
+ #[Route('/stripe/webhook', name: 'stripe_webhook', methods: ['POST'])]
+ public function stripeWebhook(Request $request): JsonResponse
+ {
+ try {
+ \Stripe\Stripe::setApiKey('sk_FAKE');
+ \Stripe\Stripe::$apiBase = 'http://internal_server:8089';
+
+ $payload = $request->getContent();
+ $sigHeader = $request->headers->get('Stripe-Signature', '');
+ $event = \Stripe\Webhook::constructEvent($payload, $sigHeader, 'whsec_FAKE');
+
+ return new JsonResponse($event->data->object->toArray());
+ } catch (\Stripe\Exception\SignatureVerificationException $e) {
+ return new JsonResponse(['error' => $e->getMessage()], 403);
+ } catch (\Exception $e) {
+ return new JsonResponse(['error' => $e->getMessage()], 403);
+ }
+ }
+
+ #[Route('/stripe/create_checkout_session', name: 'stripe_create_checkout_session', methods: ['POST'])]
+ public function stripeCreateCheckoutSession(Request $request): JsonResponse
+ {
+ try {
+ \Stripe\Stripe::setApiKey('sk_FAKE');
+ \Stripe\Stripe::$apiBase = 'http://internal_server:8089';
+
+ $data = json_decode($request->getContent(), true);
+ $result = \Stripe\Checkout\Session::create($data);
+
+ return new JsonResponse($result->toArray());
+ } catch (\Stripe\Exception\ApiErrorException $e) {
+ return new JsonResponse(['error' => $e->getMessage()], 500);
+ } catch (\Exception $e) {
+ return new JsonResponse(['error' => $e->getMessage()], 500);
+ }
+ }
+
+ #[Route('/stripe/create_payment_intent', name: 'stripe_create_payment_intent', methods: ['POST'])]
+ public function stripeCreatePaymentIntent(Request $request): JsonResponse
+ {
+ try {
+ \Stripe\Stripe::setApiKey('sk_FAKE');
+ \Stripe\Stripe::$apiBase = 'http://internal_server:8089';
+
+ $data = json_decode($request->getContent(), true);
+ $result = \Stripe\PaymentIntent::create($data);
+
+ return new JsonResponse($result->toArray());
+ } catch (\Stripe\Exception\ApiErrorException $e) {
+ return new JsonResponse(['error' => $e->getMessage()], 500);
+ } catch (\Exception $e) {
+ return new JsonResponse(['error' => $e->getMessage()], 500);
+ }
+ }
+
+ private function mysqlOperation(string $op): void
+ {
+ switch ($op) {
+ case 'init':
+ $this->mysqlConnection->executeStatement('CREATE TABLE IF NOT EXISTS demo(id INT NOT NULL, name VARCHAR(20) NOT NULL, age INT NOT NULL, PRIMARY KEY (id))');
+ $this->mysqlConnection->executeStatement("INSERT IGNORE INTO demo (id, name, age) VALUES (1, 'test', 16)");
+ $this->mysqlConnection->executeStatement("INSERT IGNORE INTO demo (id, name, age) VALUES (2, 'test2', 17)");
+ $this->mysqlConnection->executeStatement('DROP PROCEDURE IF EXISTS test_procedure');
+ $this->mysqlConnection->executeStatement('CREATE PROCEDURE test_procedure(IN test_id INT, IN other VARCHAR(20))
+BEGIN
+ SELECT demo.id, demo.name, demo.age FROM demo WHERE demo.id = test_id;
+END');
+ break;
+ case 'select':
+ $this->mysqlConnection->executeQuery('SELECT * from demo where id=1 or id IN (3, 4)');
+ break;
+ case 'insert':
+ try {
+ $this->mysqlConnection->executeStatement("insert into demo (id, name, age) values(3, 'test3', 163)");
+ } catch (\Doctrine\DBAL\Exception $e) {}
+ break;
+ case 'update':
+ $this->mysqlConnection->executeStatement('update demo set age=22 where id=1');
+ break;
+ case 'delete':
+ $this->mysqlConnection->executeStatement('delete from demo where id=2 or id=11111111');
+ break;
+ case 'procedure':
+ $this->mysqlConnection->executeStatement("call test_procedure(1, 'test')");
+ break;
+ case 'select_error':
+ try {
+ $this->mysqlConnection->executeQuery('SELECT * from demosssss where id=1 or id=233333');
+ } catch (\Doctrine\DBAL\Exception $e) {}
+ break;
+ }
+ }
+
+ private function postgresOperation(string $op): void
+ {
+ switch ($op) {
+ case 'init':
+ try {
+ $this->postgresConnection->executeStatement('CREATE TABLE demo(id INT NOT NULL, name VARCHAR(20) NOT NULL, age INT NOT NULL, PRIMARY KEY (id))');
+ } catch (\Doctrine\DBAL\Exception $e) {}
+ try {
+ $this->postgresConnection->executeStatement("INSERT INTO demo (id, name, age) VALUES (1, 'test', 16)");
+ } catch (\Doctrine\DBAL\Exception $e) {}
+ try {
+ $this->postgresConnection->executeStatement("INSERT INTO demo (id, name, age) VALUES (2, 'test2', 17)");
+ } catch (\Doctrine\DBAL\Exception $e) {}
+ $this->postgresConnection->executeStatement("CREATE OR REPLACE PROCEDURE helloworld(id int, other varchar(10))
+LANGUAGE plpgsql
+AS \$\$
+BEGIN
+ raise info 'Hello World';
+END;
+\$\$");
+ break;
+ case 'select':
+ $this->postgresConnection->executeQuery('SELECT * from demo where id=1 or id IN (3, 4)');
+ break;
+ case 'insert':
+ try {
+ $this->postgresConnection->executeStatement("insert into demo (id, name, age) values(3, 'test3', 163)");
+ } catch (\Doctrine\DBAL\Exception $e) {}
+ break;
+ case 'update':
+ $this->postgresConnection->executeStatement("update demo set age=22 where name like '%tes%'");
+ break;
+ case 'delete':
+ $this->postgresConnection->executeStatement('delete from demo where id=2 or id=11111111');
+ break;
+ case 'procedure':
+ $this->postgresConnection->executeStatement("call helloworld(1, 'test')");
+ break;
+ case 'select_error':
+ try {
+ $this->postgresConnection->executeQuery('SELECT * from demosssssssss where id=1 or id=233333');
+ } catch (\Doctrine\DBAL\Exception $e) {}
+ break;
+ }
+ }
+}
diff --git a/utils/build/docker/php/weblogs/symfony7x/src/Controller/LoginController.php b/utils/build/docker/php/weblogs/symfony7x/src/Controller/LoginController.php
new file mode 100644
index 00000000000..233a18a1232
--- /dev/null
+++ b/utils/build/docker/php/weblogs/symfony7x/src/Controller/LoginController.php
@@ -0,0 +1,45 @@
+ ['id' => 'new-user'],
+ ];
+
+ public function __construct(
+ private UserPasswordHasherInterface $passwordHasher,
+ ) {}
+
+ #[Route('/login', name: 'login', methods: ['GET', 'POST'])]
+ public function login(): Response
+ {
+ return new Response('', Response::HTTP_OK);
+ }
+
+ #[Route('/signup', name: 'signup', methods: ['POST'])]
+ public function signup(Request $request, EntityManagerInterface $entityManager): Response
+ {
+ $username = $request->request->get('username', '');
+ $password = $request->request->get('password', '');
+ $id = isset(self::$newUsers[$username]) ? self::$newUsers[$username]['id'] : hash('sha256', $username);
+ $hashedPassword = $this->passwordHasher->hashPassword(new User($id, $username, '', $username), $password);
+
+ $user = new User($id, $username, $hashedPassword, $username);
+ $entityManager->persist($user);
+
+ // Signup event is tracked automatically by the tracer via Doctrine\ORM\UnitOfWork::executeInserts hook.
+ $entityManager->flush();
+
+ return new Response('', 200);
+ }
+}
diff --git a/utils/build/docker/php/weblogs/symfony7x/src/Entity/User.php b/utils/build/docker/php/weblogs/symfony7x/src/Entity/User.php
new file mode 100644
index 00000000000..6a0d0614192
--- /dev/null
+++ b/utils/build/docker/php/weblogs/symfony7x/src/Entity/User.php
@@ -0,0 +1,66 @@
+id = $id;
+ $this->username = $username;
+ $this->password = $password;
+ $this->name = $name;
+ }
+
+ public function getId(): string
+ {
+ return $this->id;
+ }
+
+ public function getUsername(): string
+ {
+ return $this->username;
+ }
+
+ public function getUserIdentifier(): string
+ {
+ return $this->username;
+ }
+
+ public function getPassword(): string
+ {
+ return $this->password;
+ }
+
+ public function getName(): string
+ {
+ return $this->name ?? '';
+ }
+
+ public function getRoles(): array
+ {
+ return ['ROLE_USER'];
+ }
+
+ public function eraseCredentials(): void {}
+}
diff --git a/utils/build/docker/php/weblogs/symfony7x/src/Kernel.php b/utils/build/docker/php/weblogs/symfony7x/src/Kernel.php
new file mode 100644
index 00000000000..779cd1f2b12
--- /dev/null
+++ b/utils/build/docker/php/weblogs/symfony7x/src/Kernel.php
@@ -0,0 +1,11 @@
+getEntityManager()->persist($user);
+ if ($flush) {
+ $this->getEntityManager()->flush();
+ }
+ }
+}
diff --git a/utils/build/docker/php/weblogs/symfony7x/src/Security/LoginFailureHandler.php b/utils/build/docker/php/weblogs/symfony7x/src/Security/LoginFailureHandler.php
new file mode 100644
index 00000000000..803b36478c3
--- /dev/null
+++ b/utils/build/docker/php/weblogs/symfony7x/src/Security/LoginFailureHandler.php
@@ -0,0 +1,28 @@
+query->get('sdk_trigger', '');
+ $sdkUser = $request->query->get('sdk_user', '');
+ $sdkExists = filter_var($request->query->get('sdk_user_exists', 'false'), FILTER_VALIDATE_BOOLEAN);
+
+ if ($sdkTrigger === 'before' && $sdkUser !== '') {
+ \datadog\appsec\track_user_login_failure_event($sdkUser, $sdkExists, []);
+ }
+
+ if ($sdkTrigger === 'after' && $sdkUser !== '') {
+ \datadog\appsec\track_user_login_failure_event($sdkUser, $sdkExists, []);
+ }
+
+ return new Response('', 401);
+ }
+}
diff --git a/utils/build/docker/php/weblogs/symfony7x/src/Security/LoginSuccessHandler.php b/utils/build/docker/php/weblogs/symfony7x/src/Security/LoginSuccessHandler.php
new file mode 100644
index 00000000000..e87785a61d9
--- /dev/null
+++ b/utils/build/docker/php/weblogs/symfony7x/src/Security/LoginSuccessHandler.php
@@ -0,0 +1,36 @@
+query->get('sdk_trigger', '');
+ $sdkUser = $request->query->get('sdk_user', '');
+
+ if ($sdkTrigger === 'before' && $sdkUser !== '') {
+ \datadog\appsec\track_user_login_success_event($sdkUser, []);
+ }
+
+ if ($sdkTrigger === 'after' && $sdkUser !== '') {
+ \datadog\appsec\track_user_login_success_event($sdkUser, []);
+ }
+
+ $subRequest = $request->duplicate();
+ $subRequest->setMethod('GET');
+ $subRequest->request->replace([]);
+
+ return $this->httpKernel->handle($subRequest, HttpKernelInterface::SUB_REQUEST);
+ }
+}
diff --git a/utils/build/docker/php/weblogs/symfony7x/src/Security/UserProvider.php b/utils/build/docker/php/weblogs/symfony7x/src/Security/UserProvider.php
new file mode 100644
index 00000000000..00c2bef5cd3
--- /dev/null
+++ b/utils/build/docker/php/weblogs/symfony7x/src/Security/UserProvider.php
@@ -0,0 +1,42 @@
+userRepository->save($user);
+
+ return $user;
+ }
+
+ public function loadUserByIdentifier(string $identifier): UserInterface
+ {
+ $user = $this->userRepository->findOneBy(['username' => $identifier]);
+ if ($user === null) {
+ throw new UserNotFoundException(sprintf('User "%s" not found.', $identifier));
+ }
+
+ return $user;
+ }
+
+ public function refreshUser(UserInterface $user): UserInterface
+ {
+ return $this->loadUserByIdentifier($user->getUserIdentifier());
+ }
+
+ public function supportsClass(string $class): bool
+ {
+ return $class === User::class || is_subclass_of($class, User::class);
+ }
+}
diff --git a/utils/build/docker/php/weblogs/symfony7x/var/cache/.gitkeep b/utils/build/docker/php/weblogs/symfony7x/var/cache/.gitkeep
new file mode 100644
index 00000000000..e69de29bb2d