From 25bdde85eb41397619781db75c2835f61e37378d Mon Sep 17 00:00:00 2001 From: Alejandro Estringana Ruiz Date: Mon, 22 Jun 2026 17:10:53 +0200 Subject: [PATCH 01/15] Add Symfony app --- docs/execute/build.md | 2 +- manifests/php.yml | 42 + tests/appsec/test_automated_login_events.py | 4 +- utils/build/docker/php/symfony7x.Dockerfile | 60 ++ .../docker/php/weblogs/symfony7x/.apache.conf | 15 + .../php/weblogs/symfony7x/Acme/composer.json | 12 + .../php/weblogs/symfony7x/Acme/src/Acme.php | 7 + .../docker/php/weblogs/symfony7x/bin/console | 16 + .../php/weblogs/symfony7x/bin/init-db.php | 13 + .../php/weblogs/symfony7x/composer.json | 52 ++ .../php/weblogs/symfony7x/config/bundles.php | 6 + .../symfony7x/config/packages/framework.yaml | 16 + .../symfony7x/config/packages/security.yaml | 19 + .../php/weblogs/symfony7x/config/routes.yaml | 22 + .../weblogs/symfony7x/config/services.yaml | 9 + .../php/weblogs/symfony7x/public/.htaccess | 27 + .../php/weblogs/symfony7x/public/index.php | 20 + .../src/Controller/AppController.php | 836 ++++++++++++++++++ .../src/Controller/LoginController.php | 180 ++++ .../php/weblogs/symfony7x/src/Kernel.php | 11 + .../src/Security/AppAuthenticator.php | 35 + .../weblogs/symfony7x/src/Security/User.php | 53 ++ .../symfony7x/src/Security/UserProvider.php | 41 + .../php/weblogs/symfony7x/var/cache/.gitkeep | 0 24 files changed, 1495 insertions(+), 3 deletions(-) create mode 100644 utils/build/docker/php/symfony7x.Dockerfile create mode 100644 utils/build/docker/php/weblogs/symfony7x/.apache.conf create mode 100644 utils/build/docker/php/weblogs/symfony7x/Acme/composer.json create mode 100644 utils/build/docker/php/weblogs/symfony7x/Acme/src/Acme.php create mode 100755 utils/build/docker/php/weblogs/symfony7x/bin/console create mode 100644 utils/build/docker/php/weblogs/symfony7x/bin/init-db.php create mode 100644 utils/build/docker/php/weblogs/symfony7x/composer.json create mode 100644 utils/build/docker/php/weblogs/symfony7x/config/bundles.php create mode 100644 utils/build/docker/php/weblogs/symfony7x/config/packages/framework.yaml create mode 100644 utils/build/docker/php/weblogs/symfony7x/config/packages/security.yaml create mode 100644 utils/build/docker/php/weblogs/symfony7x/config/routes.yaml create mode 100644 utils/build/docker/php/weblogs/symfony7x/config/services.yaml create mode 100644 utils/build/docker/php/weblogs/symfony7x/public/.htaccess create mode 100644 utils/build/docker/php/weblogs/symfony7x/public/index.php create mode 100644 utils/build/docker/php/weblogs/symfony7x/src/Controller/AppController.php create mode 100644 utils/build/docker/php/weblogs/symfony7x/src/Controller/LoginController.php create mode 100644 utils/build/docker/php/weblogs/symfony7x/src/Kernel.php create mode 100644 utils/build/docker/php/weblogs/symfony7x/src/Security/AppAuthenticator.php create mode 100644 utils/build/docker/php/weblogs/symfony7x/src/Security/User.php create mode 100644 utils/build/docker/php/weblogs/symfony7x/src/Security/UserProvider.php create mode 100644 utils/build/docker/php/weblogs/symfony7x/var/cache/.gitkeep 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..1b48b0c0999 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 @@ -363,58 +367,73 @@ manifest: - weblog_declaration: "*": missing_feature (Basic auth not implemented) laravel11x: v1.8.0 + symfony7x: v1.8.0 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) laravel11x: v1.8.0 + symfony7x: v1.8.0 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) laravel11x: v1.8.0 + symfony7x: v1.8.0 tests/appsec/test_automated_login_events.py::Test_V3_Login_Events::test_login_wrong_password_failure_basic: - weblog_declaration: "*": missing_feature (Basic auth not implemented) laravel11x: v1.8.0 + symfony7x: v1.8.0 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) laravel11x: v1.8.0 + symfony7x: v1.8.0 tests/appsec/test_automated_login_events.py::Test_V3_Login_Events_Anon: v1.8.0 tests/appsec/test_automated_login_events.py::Test_V3_Login_Events_Anon::test_login_sdk_failure_basic: - weblog_declaration: "*": missing_feature (Basic auth not implemented) laravel11x: v1.8.0 + symfony7x: v1.8.0 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) laravel11x: v1.8.0 + symfony7x: v1.8.0 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) laravel11x: v1.8.0 + symfony7x: v1.8.0 tests/appsec/test_automated_login_events.py::Test_V3_Login_Events_Anon::test_login_wrong_password_failure_basic: - weblog_declaration: "*": missing_feature (Basic auth not implemented) laravel11x: v1.8.0 + symfony7x: v1.8.0 tests/appsec/test_automated_login_events.py::Test_V3_Login_Events_Anon::test_login_wrong_user_failure_basic: - weblog_declaration: "*": missing_feature (Basic auth not implemented) laravel11x: v1.8.0 + symfony7x: 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 @@ -424,6 +443,7 @@ manifest: 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 +634,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 +651,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 +661,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 +680,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 +704,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 +729,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 +743,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 +802,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 +835,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 +859,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 +1134,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 +1151,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) diff --git a/tests/appsec/test_automated_login_events.py b/tests/appsec/test_automated_login_events.py index d21e86e3af7..6d709d351ee 100644 --- a/tests/appsec/test_automated_login_events.py +++ b/tests/appsec/test_automated_login_events.py @@ -1155,10 +1155,10 @@ 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"] def login_failure_includes_usr_exists_meta() -> bool: 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..acc78646783 --- /dev/null +++ b/utils/build/docker/php/weblogs/symfony7x/bin/init-db.php @@ -0,0 +1,13 @@ +exec('CREATE TABLE IF NOT EXISTS users (id VARCHAR(255) PRIMARY KEY, username VARCHAR(255) UNIQUE NOT NULL, password VARCHAR(255) NOT NULL, name VARCHAR(255))'); + +$hash1 = password_hash('1234', PASSWORD_BCRYPT); +$db->exec("INSERT OR IGNORE INTO users (id, username, password, name) VALUES ('social-security-id', 'test', '$hash1', 'test')"); + +$hash2 = password_hash('1234', PASSWORD_BCRYPT); +$db->exec("INSERT OR IGNORE INTO users (id, username, password, name) VALUES ('591dc126-8431-4d0f-9509-b23318d3dce4', 'testuuid', '$hash2', 'testuuid')"); + +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..8a36c12b836 --- /dev/null +++ b/utils/build/docker/php/weblogs/symfony7x/composer.json @@ -0,0 +1,52 @@ +{ + "name": "php/symfony-weblog", + "type": "project", + "require": { + "php": ">=8.2", + "ext-ctype": "*", + "ext-iconv": "*", + "stripe/stripe-php": "^10.0", + "symfony/console": "^7.2", + "symfony/framework-bundle": "^7.2", + "symfony/security-bundle": "^7.2", + "symfony/process": "^7.2", + "symfony/yaml": "^7.2", + "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-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..380a489a080 --- /dev/null +++ b/utils/build/docker/php/weblogs/symfony7x/config/bundles.php @@ -0,0 +1,6 @@ + ['all' => true], + Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true], +]; 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..ef8b9959afc --- /dev/null +++ b/utils/build/docker/php/weblogs/symfony7x/config/packages/security.yaml @@ -0,0 +1,19 @@ +security: + password_hashers: + App\Security\User: + algorithm: bcrypt + + providers: + app_user_provider: + id: App\Security\UserProvider + + firewalls: + main: + pattern: ^/ + lazy: true + provider: app_user_provider + stateless: false + custom_authenticators: + - App\Security\AppAuthenticator + + 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..bfa08ab7870 --- /dev/null +++ b/utils/build/docker/php/weblogs/symfony7x/config/services.yaml @@ -0,0 +1,9 @@ +services: + _defaults: + autowire: true + autoconfigure: true + + App\: + resource: '../src/' + exclude: + - '../src/Kernel.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..674748c2ddf --- /dev/null +++ b/utils/build/docker/php/weblogs/symfony7x/src/Controller/AppController.php @@ -0,0 +1,836 @@ +getUser(); + if ($user instanceof \App\Security\User) { + if (function_exists('\datadog\appsec\internal\track_authenticated_user_event_automated')) { + \datadog\appsec\internal\track_authenticated_user_event_automated('custom', $user->getId()); + } elseif (function_exists('\datadog\appsec\track_authenticated_user_event_automated')) { + \datadog\appsec\track_authenticated_user_event_automated($user->getId()); + } + } + return new Response("Hello world!\n", 200, [ + 'Content-Type' => '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); + } + + $requestHeaders = []; + $responseHeaders = []; + + $ch = curl_init($url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_HEADER, false); + curl_setopt($ch, CURLINFO_HEADER_OUT, true); + curl_setopt($ch, CURLOPT_HEADERFUNCTION, function ($curl, $header) use (&$responseHeaders) { + $len = strlen($header); + $parts = explode(':', $header, 2); + if (count($parts) === 2) { + $responseHeaders[strtolower(trim($parts[0]))] = trim($parts[1]); + } + + return $len; + }); + $statusCode = 0; + curl_exec($ch); + $statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $rawRequestHeaders = curl_getinfo($ch, CURLINFO_HEADER_OUT); + if ($rawRequestHeaders) { + foreach (explode("\r\n", $rawRequestHeaders) as $line) { + if (strpos($line, ':') !== false) { + [$key, $value] = explode(':', $line, 2); + $requestHeaders[strtolower(trim($key))] = trim($value); + } + } + } + curl_close($ch); + + 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 + { + $file = $request->query->get('file', ''); + $content = @file_get_contents($file); + + return new Response($content === false ? '' : $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') { + $connection = new \PDO('mysql:dbname=mysql_dbname;host=mysqldb', 'mysqldb', 'mysqldb'); + $connection->query($query); + } elseif ($integration === 'pdo-pgsql') { + $connection = new \PDO('pgsql:dbname=system_tests_dbname;host=postgres;port=5433', 'system_tests_user', 'system_tests'); + $connection->query($query); + } elseif ($integration === 'mysqli') { + $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 !== '') { + $entry = json_encode(['message' => $msg]) . "\n"; + @file_put_contents($dir . '/helper.log', $entry, FILE_APPEND); + } + + 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('/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') { + $decoded = simplexml_load_string(stripslashes($request->getContent())); + $file = (string) ($decoded[0] ?? ''); + } else { + $file = urldecode($request->get('file', '')); + } + @file_get_contents($file); + + 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') { + $decoded = simplexml_load_string(stripslashes($request->getContent())); + $domain = (string) ($decoded[0] ?? ''); + } else { + $domain = urldecode($request->get('domain', '')); + } + @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 + { + @file_get_contents(urldecode($request->query->get('file1', ''))); + @file_get_contents(urldecode($request->query->get('file2', ''))); + @file_get_contents('../etc/passwd'); + + 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', ''); + + $mysqlOp = static function (string $op): void { + $db = new \PDO('mysql:dbname=mysql_dbname;host=mysqldb', 'mysqldb', 'mysqldb'); + switch ($op) { + case 'init': + $db->exec('CREATE TABLE IF NOT EXISTS demo(id INT NOT NULL, name VARCHAR(20) NOT NULL, age INT NOT NULL, PRIMARY KEY (id))'); + $db->exec("INSERT IGNORE INTO demo (id, name, age) VALUES (1, 'test', 16)"); + $db->exec("INSERT IGNORE INTO demo (id, name, age) VALUES (2, 'test2', 17)"); + $db->exec('DROP PROCEDURE IF EXISTS test_procedure'); + $db->exec('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': + $db->query('SELECT * from demo where id=1 or id IN (3, 4)'); + break; + case 'insert': + try { + $db->exec("insert into demo (id, name, age) values(3, 'test3', 163)"); + } catch (\PDOException $e) {} + break; + case 'update': + $db->exec('update demo set age=22 where id=1'); + break; + case 'delete': + $db->exec('delete from demo where id=2 or id=11111111'); + break; + case 'procedure': + $db->exec("call test_procedure(1, 'test')"); + break; + case 'select_error': + try { + $db->query('SELECT * from demosssss where id=1 or id=233333'); + } catch (\PDOException $e) {} + break; + } + }; + + $postgresOp = static function (string $op): void { + $db = new \PDO('pgsql:dbname=system_tests_dbname;host=postgres;port=5433', 'system_tests_user', 'system_tests'); + switch ($op) { + case 'init': + try { + $db->exec('CREATE TABLE demo(id INT NOT NULL, name VARCHAR(20) NOT NULL, age INT NOT NULL, PRIMARY KEY (id))'); + } catch (\PDOException $e) {} + try { + $db->exec("INSERT INTO demo (id, name, age) VALUES (1, 'test', 16)"); + } catch (\PDOException $e) {} + try { + $db->exec("INSERT INTO demo (id, name, age) VALUES (2, 'test2', 17)"); + } catch (\PDOException $e) {} + $db->exec("CREATE OR REPLACE PROCEDURE helloworld(id int, other varchar(10)) +LANGUAGE plpgsql +AS \$\$ +BEGIN + raise info 'Hello World'; +END; +\$\$"); + break; + case 'select': + $db->query('SELECT * from demo where id=1 or id IN (3, 4)'); + break; + case 'insert': + try { + $db->exec("insert into demo (id, name, age) values(3, 'test3', 163)"); + } catch (\PDOException $e) {} + break; + case 'update': + $db->exec("update demo set age=22 where name like '%tes%'"); + break; + case 'delete': + $db->exec('delete from demo where id=2 or id=11111111'); + break; + case 'procedure': + $db->exec("call helloworld(1, 'test')"); + break; + case 'select_error': + try { + $db->query('SELECT * from demosssssssss where id=1 or id=233333'); + } catch (\PDOException $e) {} + break; + } + }; + + if ($service === 'mysql') { + $mysqlOp($operation); + } elseif ($service === 'postgresql') { + $postgresOp($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 { + $pdo = new \PDO('mysql:dbname=mysql_dbname;host=mysqldb', 'mysqldb', 'mysqldb'); + $pdo->query('SELECT 1'); + } catch (\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': + $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': + 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': + 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': + 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(); + $responseHeaders = []; + $ch = curl_init($url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_HEADER, false); + curl_setopt($ch, CURLINFO_HEADER_OUT, true); + curl_setopt($ch, CURLOPT_HEADERFUNCTION, function ($curl, $header) use (&$responseHeaders) { + $len = strlen($header); + $parts = explode(':', $header, 2); + if (count($parts) === 2) { + $responseHeaders[strtolower(trim($parts[0]))] = trim($parts[1]); + } + + return $len; + }); + curl_exec($ch); + $statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $requestHeaders = []; + $rawRequestHeaders = curl_getinfo($ch, CURLINFO_HEADER_OUT); + if ($rawRequestHeaders) { + foreach (explode("\r\n", $rawRequestHeaders) as $line) { + if (strpos($line, ':') !== false) { + [$key, $value] = explode(':', $line, 2); + $requestHeaders[trim($key)] = trim($value); + } + } + } + curl_close($ch); + } 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]); + } + } + } + } + + $responseHeaders = []; + $ch = curl_init($url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_HEADER, false); + curl_setopt($ch, CURLINFO_HEADER_OUT, true); + curl_setopt($ch, CURLOPT_HEADERFUNCTION, function ($curl, $header) use (&$responseHeaders) { + $len = strlen($header); + $parts = explode(':', $header, 2); + if (count($parts) === 2) { + $responseHeaders[strtolower(trim($parts[0]))] = trim($parts[1]); + } + + return $len; + }); + curl_exec($ch); + $statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $requestHeaders = []; + $rawRequestHeaders = curl_getinfo($ch, CURLINFO_HEADER_OUT); + if ($rawRequestHeaders) { + foreach (explode("\r\n", $rawRequestHeaders) as $line) { + if (strpos($line, ':') !== false) { + [$key, $value] = explode(':', $line, 2); + $requestHeaders[trim($key)] = trim($value); + } + } + } + curl_close($ch); + + return new JsonResponse([ + 'url' => $url, + 'status_code' => $statusCode, + 'request_headers' => $requestHeaders, + 'response_headers' => $responseHeaders, + ]); + } + + #[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 + { + $ch = curl_init('http://127.0.0.1:7777/returnheaders'); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + $body = curl_exec($ch); + curl_close($ch); + + 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); + } + } +} 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..be589f51845 --- /dev/null +++ b/utils/build/docker/php/weblogs/symfony7x/src/Controller/LoginController.php @@ -0,0 +1,180 @@ + ['id' => 'new-user'], + ]; + + public function __construct( + private UserProvider $userProvider, + private UserPasswordHasherInterface $passwordHasher, + private Security $security, + ) {} + + #[Route('/login', name: 'login', methods: ['GET', 'POST'])] + public function login(Request $request): Response + { + $auth = $request->query->get('auth', ''); + + if ($auth === 'local') { + return $this->handleLocalLogin($request); + } + + if ($auth === 'basic') { + return $this->handleBasicLogin($request); + } + + return new Response('', 200); + } + + #[Route('/signup', name: 'signup', methods: ['POST'])] + public function signup(Request $request): Response + { + $username = $request->request->get('username', ''); + $password = $request->request->get('password', ''); + + $id = isset(self::$newUsers[$username]) ? self::$newUsers[$username]['id'] : hash('sha256', $username); + $hashedPassword = password_hash($password, PASSWORD_BCRYPT); + + $dbPath = $_ENV['SYMFONY_DB_PATH'] ?? '/tmp/symfony.db'; + $pdo = new \PDO("sqlite:$dbPath"); + $stmt = $pdo->prepare('INSERT OR REPLACE INTO users (id, username, password, name) VALUES (?, ?, ?, ?)'); + $stmt->execute([$id, $username, $hashedPassword, $username]); + + $user = new User($id, $username, $hashedPassword, $username); + $this->security->login($user, AppAuthenticator::class, 'main'); + $this->trackSignup($username, $id, []); + + return new Response('', 200); + } + + private function handleLocalLogin(Request $request): Response + { + if (!$request->isMethod('POST')) { + return new Response('', 200); + } + + $username = $request->request->get('username', ''); + $password = $request->request->get('password', ''); + $sdkEvent = $request->query->get('sdk_event', ''); + $sdkTrigger = $request->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' && $sdkEvent !== '') { + $this->callSdk($sdkEvent, $sdkUser, $sdkExists); + } + + $success = $this->attemptLogin($username, $password); + + if ($sdkTrigger === 'after' && $sdkEvent !== '') { + $this->callSdk($sdkEvent, $sdkUser, $sdkExists); + } + + if ($success) { + return new Response('', 200); + } + + return new Response('', 401); + } + + private function handleBasicLogin(Request $request): Response + { + $authHeader = $request->headers->get('Authorization', ''); + if (!str_starts_with($authHeader, 'Basic ')) { + return new Response('', 401); + } + + $decoded = base64_decode(substr($authHeader, 6)); + $parts = explode(':', $decoded, 2); + $username = $parts[0] ?? ''; + $password = $parts[1] ?? ''; + + $sdkEvent = $request->query->get('sdk_event', ''); + $sdkTrigger = $request->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' && $sdkEvent !== '') { + $this->callSdk($sdkEvent, $sdkUser, $sdkExists); + } + + $success = $this->attemptLogin($username, $password); + + if ($sdkTrigger === 'after' && $sdkEvent !== '') { + $this->callSdk($sdkEvent, $sdkUser, $sdkExists); + } + + return new Response('', $success ? 200 : 401); + } + + private function attemptLogin(string $username, string $password): bool + { + try { + $user = $this->userProvider->loadUserByIdentifier($username); + } catch (UserNotFoundException) { + $this->trackLoginFailure($username, '', false, []); + return false; + } + + if (!$this->passwordHasher->isPasswordValid($user, $password)) { + $this->trackLoginFailure($user->getUsername(), $user->getId(), true, []); + return false; + } + + $this->security->login($user, AppAuthenticator::class, 'main'); + $this->trackLoginSuccess($user->getUsername(), $user->getId(), []); + + return true; + } + + private function trackLoginSuccess(string $login, string $userId, array $meta): void + { + if (function_exists('\datadog\appsec\internal\track_user_login_success_event_automated')) { + \datadog\appsec\internal\track_user_login_success_event_automated('custom', $login, $userId, $meta); + } elseif (function_exists('\datadog\appsec\track_user_login_success_event_automated')) { + \datadog\appsec\track_user_login_success_event_automated($login, $userId, $meta); + } + } + + private function trackLoginFailure(string $login, string $userId, bool $exists, array $meta): void + { + if (function_exists('\datadog\appsec\internal\track_user_login_failure_event_automated')) { + \datadog\appsec\internal\track_user_login_failure_event_automated('custom', $login, $userId, $exists, $meta); + } elseif (function_exists('\datadog\appsec\track_user_login_failure_event_automated')) { + \datadog\appsec\track_user_login_failure_event_automated($login, $userId, $exists, $meta); + } + } + + private function trackSignup(string $login, string $userId, array $meta): void + { + if (function_exists('\datadog\appsec\internal\track_user_signup_event_automated')) { + \datadog\appsec\internal\track_user_signup_event_automated('custom', $login, $userId, $meta); + } elseif (function_exists('\datadog\appsec\track_user_signup_event_automated')) { + \datadog\appsec\track_user_signup_event_automated($login, $userId, $meta); + } + } + + private function callSdk(string $event, string $user, bool $exists): void + { + if ($event === 'success') { + \datadog\appsec\track_user_login_success_event($user, []); + } else { + \datadog\appsec\track_user_login_failure_event($user, $exists, []); + } + } +} 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 @@ +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/Security/UserProvider.php b/utils/build/docker/php/weblogs/symfony7x/src/Security/UserProvider.php new file mode 100644 index 00000000000..058b27af630 --- /dev/null +++ b/utils/build/docker/php/weblogs/symfony7x/src/Security/UserProvider.php @@ -0,0 +1,41 @@ +pdo = new \PDO("sqlite:$dbPath"); + } + + public function loadUserByIdentifier(string $identifier): UserInterface + { + $stmt = $this->pdo->prepare('SELECT * FROM users WHERE username = ?'); + $stmt->execute([$identifier]); + $row = $stmt->fetch(\PDO::FETCH_ASSOC); + + if (!$row) { + throw new UserNotFoundException(sprintf('User "%s" not found.', $identifier)); + } + + return User::fromDbRow($row); + } + + 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 From 5be569c5ced43b4431b44325410ebb7441ba2397 Mon Sep 17 00:00:00 2001 From: Alejandro Estringana Ruiz Date: Thu, 25 Jun 2026 11:27:06 +0200 Subject: [PATCH 02/15] Fix failing tests --- .../src/Controller/AppController.php | 91 ++++++++++++++++--- 1 file changed, 79 insertions(+), 12 deletions(-) diff --git a/utils/build/docker/php/weblogs/symfony7x/src/Controller/AppController.php b/utils/build/docker/php/weblogs/symfony7x/src/Controller/AppController.php index 674748c2ddf..db97abe5a68 100644 --- a/utils/build/docker/php/weblogs/symfony7x/src/Controller/AppController.php +++ b/utils/build/docker/php/weblogs/symfony7x/src/Controller/AppController.php @@ -331,7 +331,16 @@ public function logLibrary(Request $request): Response } $msg = $request->query->get('msg', ''); if ($msg !== '') { - $entry = json_encode(['message' => $msg]) . "\n"; + $record = ['message' => $msg]; + $rootSpan = \DDTrace\root_span(); + if ($rootSpan !== null) { + $record['dd.trace_id'] = \DDTrace\trace_id(); + $record['dd.span_id'] = (string) $rootSpan->id; + $record['dd.service'] = (string) (getenv('DD_SERVICE') ?: ''); + $record['dd.version'] = (string) (getenv('DD_VERSION') ?: ''); + $record['dd.env'] = (string) (getenv('DD_ENV') ?: ''); + } + $entry = json_encode($record) . "\n"; @file_put_contents($dir . '/helper.log', $entry, FILE_APPEND); } @@ -376,6 +385,52 @@ public function userLoginFailureEventV2(Request $request): Response 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', ''); + $stmt = null; + + if ($integration === 'pdo-pgsql') { + $con = new \PDO('pgsql:dbname=system_tests_dbname;host=postgres;port=5433', 'system_tests_user', 'system_tests'); + $stmt = $con->query('SELECT version()'); + } elseif ($integration === 'pdo-mysql') { + $con = new \PDO('mysql:dbname=mysql_dbname;host=mysqldb', 'mysqldb', 'mysqldb'); + $stmt = $con->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') { + $decoded = simplexml_load_string(stripslashes($request->getContent())); + $userId = (string) ($decoded[0] ?? ''); + } else { + $userId = urldecode($request->get('user_id', '')); + } + + // Use SQLite (always available) for RASP SQL injection detection + $dbPath = getenv('SYMFONY_DB_PATH') ?: '/tmp/symfony.db'; + $pdo = new \PDO("sqlite:$dbPath"); + // Intentionally unsafe query so ddtrace RASP can detect the injection + @$pdo->query("SELECT * FROM users WHERE id = '" . $userId . "'"); + + 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 { @@ -544,8 +599,11 @@ public function endpointFallback(Request $request): JsonResponse switch ($case) { case 'with_route': - $rootSpan->meta['http.route'] = '/users/{id}/profile'; - $rootSpan->meta['http.method'] = 'GET'; + // Use shutdown function so ddtrace Symfony integration cannot override + register_shutdown_function(static function () use ($rootSpan) { + $rootSpan->meta['http.route'] = '/users/{id}/profile'; + $rootSpan->meta['http.method'] = 'GET'; + }); return new JsonResponse([ 'status' => 'ok', @@ -554,9 +612,12 @@ public function endpointFallback(Request $request): JsonResponse ]); case 'with_endpoint': - unset($rootSpan->meta['http.route']); - $rootSpan->meta['http.endpoint'] = '/api/products/{param:int}'; - $rootSpan->meta['http.method'] = 'GET'; + // Use shutdown function so ddtrace Symfony integration cannot override + register_shutdown_function(static function () use ($rootSpan) { + unset($rootSpan->meta['http.route']); + $rootSpan->meta['http.endpoint'] = '/api/products/{param:int}'; + $rootSpan->meta['http.method'] = 'GET'; + }); return new JsonResponse([ 'status' => 'ok', @@ -565,9 +626,12 @@ public function endpointFallback(Request $request): JsonResponse ]); case '404': - unset($rootSpan->meta['http.route']); - $rootSpan->meta['http.endpoint'] = '/api/notfound/{param:int}'; - $rootSpan->meta['http.method'] = 'GET'; + // Use shutdown function so ddtrace Symfony integration cannot override + register_shutdown_function(static function () use ($rootSpan) { + unset($rootSpan->meta['http.route']); + $rootSpan->meta['http.endpoint'] = '/api/notfound/{param:int}'; + $rootSpan->meta['http.method'] = 'GET'; + }); return new JsonResponse([ 'status' => 'error', @@ -576,9 +640,12 @@ public function endpointFallback(Request $request): JsonResponse ], 404); case 'computed': - unset($rootSpan->meta['http.route']); - $rootSpan->meta['http.url'] = 'http://localhost:8080/endpoint_fallback_computed/users/123/orders/456'; - $rootSpan->meta['http.method'] = 'GET'; + // Use shutdown function so ddtrace Symfony integration cannot override + register_shutdown_function(static function () use ($rootSpan) { + 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', From 346944ec85df276ab657cc72269f1637c10a1f05 Mon Sep 17 00:00:00 2001 From: Alejandro Estringana Ruiz Date: Thu, 25 Jun 2026 12:26:41 +0200 Subject: [PATCH 03/15] Fix tests --- .../php/weblogs/symfony7x/src/Controller/AppController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/build/docker/php/weblogs/symfony7x/src/Controller/AppController.php b/utils/build/docker/php/weblogs/symfony7x/src/Controller/AppController.php index db97abe5a68..7bd0751b4a4 100644 --- a/utils/build/docker/php/weblogs/symfony7x/src/Controller/AppController.php +++ b/utils/build/docker/php/weblogs/symfony7x/src/Controller/AppController.php @@ -334,7 +334,7 @@ public function logLibrary(Request $request): Response $record = ['message' => $msg]; $rootSpan = \DDTrace\root_span(); if ($rootSpan !== null) { - $record['dd.trace_id'] = \DDTrace\trace_id(); + $record['dd.trace_id'] = \DDTrace\logs_correlation_trace_id(); $record['dd.span_id'] = (string) $rootSpan->id; $record['dd.service'] = (string) (getenv('DD_SERVICE') ?: ''); $record['dd.version'] = (string) (getenv('DD_VERSION') ?: ''); From 41946cd6ed52fbe8b0a42b7e2d50e1bbc7e4eb87 Mon Sep 17 00:00:00 2001 From: Alejandro Estringana Ruiz Date: Thu, 25 Jun 2026 16:50:30 +0200 Subject: [PATCH 04/15] Remove internal calls --- .../php/weblogs/symfony7x/composer.json | 2 + .../src/Controller/AppController.php | 178 ++++++------------ 2 files changed, 59 insertions(+), 121 deletions(-) diff --git a/utils/build/docker/php/weblogs/symfony7x/composer.json b/utils/build/docker/php/weblogs/symfony7x/composer.json index 8a36c12b836..6c66852c40e 100644 --- a/utils/build/docker/php/weblogs/symfony7x/composer.json +++ b/utils/build/docker/php/weblogs/symfony7x/composer.json @@ -9,8 +9,10 @@ "symfony/console": "^7.2", "symfony/framework-bundle": "^7.2", "symfony/security-bundle": "^7.2", + "symfony/http-client": "^7.2", "symfony/process": "^7.2", "symfony/yaml": "^7.2", + "monolog/monolog": "^3.0", "weblog/acme": "*" }, "repositories": [ diff --git a/utils/build/docker/php/weblogs/symfony7x/src/Controller/AppController.php b/utils/build/docker/php/weblogs/symfony7x/src/Controller/AppController.php index 7bd0751b4a4..86f0485c472 100644 --- a/utils/build/docker/php/weblogs/symfony7x/src/Controller/AppController.php +++ b/utils/build/docker/php/weblogs/symfony7x/src/Controller/AppController.php @@ -2,7 +2,10 @@ namespace App\Controller; +use Monolog\Handler\StreamHandler; +use Monolog\Logger; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\HttpClient\HttpClient; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -15,14 +18,8 @@ class AppController extends AbstractController #[Route('/', name: 'index', methods: ['GET'])] public function index(): Response { - $user = $this->getUser(); - if ($user instanceof \App\Security\User) { - if (function_exists('\datadog\appsec\internal\track_authenticated_user_event_automated')) { - \datadog\appsec\internal\track_authenticated_user_event_automated('custom', $user->getId()); - } elseif (function_exists('\datadog\appsec\track_authenticated_user_event_automated')) { - \datadog\appsec\track_authenticated_user_event_automated($user->getId()); - } - } + $this->getUser(); + return new Response("Hello world!\n", 200, [ 'Content-Type' => 'text/plain; charset=utf-8', 'Content-Length' => '13', @@ -70,35 +67,21 @@ public function makeDistantCall(Request $request): JsonResponse return new JsonResponse(['error' => 'url parameter required'], 400); } - $requestHeaders = []; - $responseHeaders = []; - - $ch = curl_init($url); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_HEADER, false); - curl_setopt($ch, CURLINFO_HEADER_OUT, true); - curl_setopt($ch, CURLOPT_HEADERFUNCTION, function ($curl, $header) use (&$responseHeaders) { - $len = strlen($header); - $parts = explode(':', $header, 2); - if (count($parts) === 2) { - $responseHeaders[strtolower(trim($parts[0]))] = trim($parts[1]); - } + $client = HttpClient::create(); + $response = $client->request('GET', $url); - return $len; - }); - $statusCode = 0; - curl_exec($ch); - $statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); - $rawRequestHeaders = curl_getinfo($ch, CURLINFO_HEADER_OUT); - if ($rawRequestHeaders) { - foreach (explode("\r\n", $rawRequestHeaders) as $line) { - if (strpos($line, ':') !== false) { - [$key, $value] = explode(':', $line, 2); - $requestHeaders[strtolower(trim($key))] = trim($value); - } + $statusCode = $response->getStatusCode(); + $responseHeaders = []; + foreach ($response->getHeaders(false) as $name => $values) { + $responseHeaders[$name] = implode(', ', $values); + } + $requestHeaders = []; + foreach (($response->getInfo('request_header') ?? '') ? explode("\r\n", $response->getInfo('request_header')) : [] as $line) { + if (str_contains($line, ':')) { + [$key, $value] = explode(':', $line, 2); + $requestHeaders[strtolower(trim($key))] = trim($value); } } - curl_close($ch); return new JsonResponse([ 'url' => $url, @@ -331,17 +314,9 @@ public function logLibrary(Request $request): Response } $msg = $request->query->get('msg', ''); if ($msg !== '') { - $record = ['message' => $msg]; - $rootSpan = \DDTrace\root_span(); - if ($rootSpan !== null) { - $record['dd.trace_id'] = \DDTrace\logs_correlation_trace_id(); - $record['dd.span_id'] = (string) $rootSpan->id; - $record['dd.service'] = (string) (getenv('DD_SERVICE') ?: ''); - $record['dd.version'] = (string) (getenv('DD_VERSION') ?: ''); - $record['dd.env'] = (string) (getenv('DD_ENV') ?: ''); - } - $entry = json_encode($record) . "\n"; - @file_put_contents($dir . '/helper.log', $entry, FILE_APPEND); + $logger = new Logger('system_tests'); + $logger->pushHandler(new StreamHandler($dir . '/helper.log', Logger::DEBUG)); + $logger->info('', ['message' => $msg]); } return new Response('', 200); @@ -599,11 +574,8 @@ public function endpointFallback(Request $request): JsonResponse switch ($case) { case 'with_route': - // Use shutdown function so ddtrace Symfony integration cannot override - register_shutdown_function(static function () use ($rootSpan) { - $rootSpan->meta['http.route'] = '/users/{id}/profile'; - $rootSpan->meta['http.method'] = 'GET'; - }); + $rootSpan->meta['http.route'] = '/users/{id}/profile'; + $rootSpan->meta['http.method'] = 'GET'; return new JsonResponse([ 'status' => 'ok', @@ -612,12 +584,9 @@ public function endpointFallback(Request $request): JsonResponse ]); case 'with_endpoint': - // Use shutdown function so ddtrace Symfony integration cannot override - register_shutdown_function(static function () use ($rootSpan) { - unset($rootSpan->meta['http.route']); - $rootSpan->meta['http.endpoint'] = '/api/products/{param:int}'; - $rootSpan->meta['http.method'] = 'GET'; - }); + unset($rootSpan->meta['http.route']); + $rootSpan->meta['http.endpoint'] = '/api/products/{param:int}'; + $rootSpan->meta['http.method'] = 'GET'; return new JsonResponse([ 'status' => 'ok', @@ -626,12 +595,9 @@ public function endpointFallback(Request $request): JsonResponse ]); case '404': - // Use shutdown function so ddtrace Symfony integration cannot override - register_shutdown_function(static function () use ($rootSpan) { - unset($rootSpan->meta['http.route']); - $rootSpan->meta['http.endpoint'] = '/api/notfound/{param:int}'; - $rootSpan->meta['http.method'] = 'GET'; - }); + unset($rootSpan->meta['http.route']); + $rootSpan->meta['http.endpoint'] = '/api/notfound/{param:int}'; + $rootSpan->meta['http.method'] = 'GET'; return new JsonResponse([ 'status' => 'error', @@ -640,12 +606,9 @@ public function endpointFallback(Request $request): JsonResponse ], 404); case 'computed': - // Use shutdown function so ddtrace Symfony integration cannot override - register_shutdown_function(static function () use ($rootSpan) { - unset($rootSpan->meta['http.route']); - $rootSpan->meta['http.url'] = 'http://localhost:8080/endpoint_fallback_computed/users/123/orders/456'; - $rootSpan->meta['http.method'] = 'GET'; - }); + 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', @@ -710,34 +673,21 @@ public function otelDropInBaggageApiOtel(Request $request): JsonResponse $scope = null; try { - $scope = $builder->build()->activate(); + $scope = $builder->build()->activate(); + $client = HttpClient::create(); + $response = $client->request('GET', $url); + $statusCode = $response->getStatusCode(); $responseHeaders = []; - $ch = curl_init($url); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_HEADER, false); - curl_setopt($ch, CURLINFO_HEADER_OUT, true); - curl_setopt($ch, CURLOPT_HEADERFUNCTION, function ($curl, $header) use (&$responseHeaders) { - $len = strlen($header); - $parts = explode(':', $header, 2); - if (count($parts) === 2) { - $responseHeaders[strtolower(trim($parts[0]))] = trim($parts[1]); - } - - return $len; - }); - curl_exec($ch); - $statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); - $requestHeaders = []; - $rawRequestHeaders = curl_getinfo($ch, CURLINFO_HEADER_OUT); - if ($rawRequestHeaders) { - foreach (explode("\r\n", $rawRequestHeaders) as $line) { - if (strpos($line, ':') !== false) { - [$key, $value] = explode(':', $line, 2); - $requestHeaders[trim($key)] = trim($value); - } + foreach ($response->getHeaders(false) as $name => $values) { + $responseHeaders[$name] = implode(', ', $values); + } + $requestHeaders = []; + foreach (($response->getInfo('request_header') ?? '') ? explode("\r\n", $response->getInfo('request_header')) : [] as $line) { + if (str_contains($line, ':')) { + [$key, $value] = explode(':', $line, 2); + $requestHeaders[strtolower(trim($key))] = trim($value); } } - curl_close($ch); } finally { if ($scope !== null) { $scope->detach(); @@ -780,33 +730,20 @@ public function otelDropInBaggageApiDatadog(Request $request): JsonResponse } } + $client = HttpClient::create(); + $response = $client->request('GET', $url); + $statusCode = $response->getStatusCode(); $responseHeaders = []; - $ch = curl_init($url); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_HEADER, false); - curl_setopt($ch, CURLINFO_HEADER_OUT, true); - curl_setopt($ch, CURLOPT_HEADERFUNCTION, function ($curl, $header) use (&$responseHeaders) { - $len = strlen($header); - $parts = explode(':', $header, 2); - if (count($parts) === 2) { - $responseHeaders[strtolower(trim($parts[0]))] = trim($parts[1]); - } - - return $len; - }); - curl_exec($ch); - $statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); - $requestHeaders = []; - $rawRequestHeaders = curl_getinfo($ch, CURLINFO_HEADER_OUT); - if ($rawRequestHeaders) { - foreach (explode("\r\n", $rawRequestHeaders) as $line) { - if (strpos($line, ':') !== false) { - [$key, $value] = explode(':', $line, 2); - $requestHeaders[trim($key)] = trim($value); - } + foreach ($response->getHeaders(false) as $name => $values) { + $responseHeaders[$name] = implode(', ', $values); + } + $requestHeaders = []; + foreach (($response->getInfo('request_header') ?? '') ? explode("\r\n", $response->getInfo('request_header')) : [] as $line) { + if (str_contains($line, ':')) { + [$key, $value] = explode(':', $line, 2); + $requestHeaders[strtolower(trim($key))] = trim($value); } } - curl_close($ch); return new JsonResponse([ 'url' => $url, @@ -830,10 +767,9 @@ public function returnHeaders(Request $request): JsonResponse #[Route('/requestdownstream', name: 'requestdownstream', methods: ['GET'])] public function requestDownstream(): Response { - $ch = curl_init('http://127.0.0.1:7777/returnheaders'); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - $body = curl_exec($ch); - curl_close($ch); + $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']); } From 752354e9864f89c866820079c1dee87198e0fc10 Mon Sep 17 00:00:00 2001 From: Alejandro Estringana Ruiz Date: Thu, 25 Jun 2026 17:12:14 +0200 Subject: [PATCH 05/15] Make symfony app more symfony approach --- .../php/weblogs/symfony7x/bin/init-db.php | 41 +++++++++--- .../php/weblogs/symfony7x/composer.json | 2 + .../php/weblogs/symfony7x/config/bundles.php | 1 + .../symfony7x/config/packages/doctrine.yaml | 17 +++++ .../symfony7x/config/packages/security.yaml | 2 +- .../src/Controller/LoginController.php | 49 ++------------ .../php/weblogs/symfony7x/src/Entity/User.php | 66 +++++++++++++++++++ .../src/Repository/UserRepository.php | 23 +++++++ .../weblogs/symfony7x/src/Security/User.php | 52 +-------------- .../symfony7x/src/Security/UserProvider.php | 21 +++--- 10 files changed, 161 insertions(+), 113 deletions(-) create mode 100644 utils/build/docker/php/weblogs/symfony7x/config/packages/doctrine.yaml create mode 100644 utils/build/docker/php/weblogs/symfony7x/src/Entity/User.php create mode 100644 utils/build/docker/php/weblogs/symfony7x/src/Repository/UserRepository.php diff --git a/utils/build/docker/php/weblogs/symfony7x/bin/init-db.php b/utils/build/docker/php/weblogs/symfony7x/bin/init-db.php index acc78646783..096423879af 100644 --- a/utils/build/docker/php/weblogs/symfony7x/bin/init-db.php +++ b/utils/build/docker/php/weblogs/symfony7x/bin/init-db.php @@ -1,13 +1,38 @@ +#!/usr/bin/env php exec('CREATE TABLE IF NOT EXISTS users (id VARCHAR(255) PRIMARY KEY, username VARCHAR(255) UNIQUE NOT NULL, password VARCHAR(255) NOT NULL, name VARCHAR(255))'); +use App\Entity\User; +use App\Kernel; +use Doctrine\ORM\Tools\SchemaTool; -$hash1 = password_hash('1234', PASSWORD_BCRYPT); -$db->exec("INSERT OR IGNORE INTO users (id, username, password, name) VALUES ('social-security-id', 'test', '$hash1', 'test')"); +require dirname(__DIR__).'/vendor/autoload.php'; -$hash2 = password_hash('1234', PASSWORD_BCRYPT); -$db->exec("INSERT OR IGNORE INTO users (id, username, password, name) VALUES ('591dc126-8431-4d0f-9509-b23318d3dce4', 'testuuid', '$hash2', 'testuuid')"); +$_SERVER['APP_ENV'] ??= 'prod'; +$_SERVER['APP_DEBUG'] ??= false; -echo "Database initialized at $dbPath\n"; +$kernel = new Kernel($_SERVER['APP_ENV'], (bool) $_SERVER['APP_DEBUG']); +$kernel->boot(); + +/** @var \Doctrine\ORM\EntityManagerInterface $em */ +$em = $kernel->getContainer()->get('doctrine.orm.entity_manager'); +$metadata = $em->getMetadataFactory()->getAllMetadata(); + +$schemaTool = new SchemaTool($em); +$schemaTool->updateSchema($metadata, true); + +$repo = $em->getRepository(User::class); +$seeds = [ + ['id' => 'social-security-id', 'username' => 'test', 'password' => '1234'], + ['id' => '591dc126-8431-4d0f-9509-b23318d3dce4', 'username' => 'testuuid', 'password' => '1234'], +]; + +foreach ($seeds as $seed) { + if ($repo->find($seed['id']) === null) { + $em->persist(new User($seed['id'], $seed['username'], password_hash($seed['password'], PASSWORD_BCRYPT), $seed['username'])); + } +} + +$em->flush(); +$kernel->shutdown(); + +echo "Database initialized at " . ($_SERVER['SYMFONY_DB_PATH'] ?? '/tmp/symfony.db') . "\n"; diff --git a/utils/build/docker/php/weblogs/symfony7x/composer.json b/utils/build/docker/php/weblogs/symfony7x/composer.json index 6c66852c40e..ff8eb376c48 100644 --- a/utils/build/docker/php/weblogs/symfony7x/composer.json +++ b/utils/build/docker/php/weblogs/symfony7x/composer.json @@ -5,6 +5,8 @@ "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", diff --git a/utils/build/docker/php/weblogs/symfony7x/config/bundles.php b/utils/build/docker/php/weblogs/symfony7x/config/bundles.php index 380a489a080..a7afe818200 100644 --- a/utils/build/docker/php/weblogs/symfony7x/config/bundles.php +++ b/utils/build/docker/php/weblogs/symfony7x/config/bundles.php @@ -1,6 +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..b5aa6829b92 --- /dev/null +++ b/utils/build/docker/php/weblogs/symfony7x/config/packages/doctrine.yaml @@ -0,0 +1,17 @@ +parameters: + default_sqlite_path: '/tmp/symfony.db' + +doctrine: + dbal: + driver: pdo_sqlite + path: '%env(default:default_sqlite_path:SYMFONY_DB_PATH)%' + 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/security.yaml b/utils/build/docker/php/weblogs/symfony7x/config/packages/security.yaml index ef8b9959afc..fe570eebb29 100644 --- a/utils/build/docker/php/weblogs/symfony7x/config/packages/security.yaml +++ b/utils/build/docker/php/weblogs/symfony7x/config/packages/security.yaml @@ -1,6 +1,6 @@ security: password_hashers: - App\Security\User: + App\Entity\User: algorithm: bcrypt providers: diff --git a/utils/build/docker/php/weblogs/symfony7x/src/Controller/LoginController.php b/utils/build/docker/php/weblogs/symfony7x/src/Controller/LoginController.php index be589f51845..df5739a4b28 100644 --- a/utils/build/docker/php/weblogs/symfony7x/src/Controller/LoginController.php +++ b/utils/build/docker/php/weblogs/symfony7x/src/Controller/LoginController.php @@ -2,8 +2,8 @@ namespace App\Controller; +use App\Entity\User; use App\Security\AppAuthenticator; -use App\Security\User; use App\Security\UserProvider; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\SecurityBundle\Security; @@ -44,20 +44,13 @@ public function login(Request $request): Response #[Route('/signup', name: 'signup', methods: ['POST'])] public function signup(Request $request): Response { - $username = $request->request->get('username', ''); - $password = $request->request->get('password', ''); + $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); - $id = isset(self::$newUsers[$username]) ? self::$newUsers[$username]['id'] : hash('sha256', $username); - $hashedPassword = password_hash($password, PASSWORD_BCRYPT); - - $dbPath = $_ENV['SYMFONY_DB_PATH'] ?? '/tmp/symfony.db'; - $pdo = new \PDO("sqlite:$dbPath"); - $stmt = $pdo->prepare('INSERT OR REPLACE INTO users (id, username, password, name) VALUES (?, ?, ?, ?)'); - $stmt->execute([$id, $username, $hashedPassword, $username]); - - $user = new User($id, $username, $hashedPassword, $username); + $user = $this->userProvider->createUser($id, $username, $hashedPassword, $username); $this->security->login($user, AppAuthenticator::class, 'main'); - $this->trackSignup($username, $id, []); return new Response('', 200); } @@ -127,48 +120,18 @@ private function attemptLogin(string $username, string $password): bool try { $user = $this->userProvider->loadUserByIdentifier($username); } catch (UserNotFoundException) { - $this->trackLoginFailure($username, '', false, []); return false; } if (!$this->passwordHasher->isPasswordValid($user, $password)) { - $this->trackLoginFailure($user->getUsername(), $user->getId(), true, []); return false; } $this->security->login($user, AppAuthenticator::class, 'main'); - $this->trackLoginSuccess($user->getUsername(), $user->getId(), []); return true; } - private function trackLoginSuccess(string $login, string $userId, array $meta): void - { - if (function_exists('\datadog\appsec\internal\track_user_login_success_event_automated')) { - \datadog\appsec\internal\track_user_login_success_event_automated('custom', $login, $userId, $meta); - } elseif (function_exists('\datadog\appsec\track_user_login_success_event_automated')) { - \datadog\appsec\track_user_login_success_event_automated($login, $userId, $meta); - } - } - - private function trackLoginFailure(string $login, string $userId, bool $exists, array $meta): void - { - if (function_exists('\datadog\appsec\internal\track_user_login_failure_event_automated')) { - \datadog\appsec\internal\track_user_login_failure_event_automated('custom', $login, $userId, $exists, $meta); - } elseif (function_exists('\datadog\appsec\track_user_login_failure_event_automated')) { - \datadog\appsec\track_user_login_failure_event_automated($login, $userId, $exists, $meta); - } - } - - private function trackSignup(string $login, string $userId, array $meta): void - { - if (function_exists('\datadog\appsec\internal\track_user_signup_event_automated')) { - \datadog\appsec\internal\track_user_signup_event_automated('custom', $login, $userId, $meta); - } elseif (function_exists('\datadog\appsec\track_user_signup_event_automated')) { - \datadog\appsec\track_user_signup_event_automated($login, $userId, $meta); - } - } - private function callSdk(string $event, string $user, bool $exists): void { if ($event === 'success') { 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/Repository/UserRepository.php b/utils/build/docker/php/weblogs/symfony7x/src/Repository/UserRepository.php new file mode 100644 index 00000000000..71128715235 --- /dev/null +++ b/utils/build/docker/php/weblogs/symfony7x/src/Repository/UserRepository.php @@ -0,0 +1,23 @@ +getEntityManager()->persist($user); + if ($flush) { + $this->getEntityManager()->flush(); + } + } +} diff --git a/utils/build/docker/php/weblogs/symfony7x/src/Security/User.php b/utils/build/docker/php/weblogs/symfony7x/src/Security/User.php index 801bf0b0f73..61239f56d1f 100644 --- a/utils/build/docker/php/weblogs/symfony7x/src/Security/User.php +++ b/utils/build/docker/php/weblogs/symfony7x/src/Security/User.php @@ -1,53 +1,3 @@ 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 {} -} +// Moved to App\Entity\User diff --git a/utils/build/docker/php/weblogs/symfony7x/src/Security/UserProvider.php b/utils/build/docker/php/weblogs/symfony7x/src/Security/UserProvider.php index 058b27af630..00c2bef5cd3 100644 --- a/utils/build/docker/php/weblogs/symfony7x/src/Security/UserProvider.php +++ b/utils/build/docker/php/weblogs/symfony7x/src/Security/UserProvider.php @@ -2,31 +2,32 @@ namespace App\Security; +use App\Entity\User; +use App\Repository\UserRepository; use Symfony\Component\Security\Core\Exception\UserNotFoundException; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\User\UserProviderInterface; class UserProvider implements UserProviderInterface { - private \PDO $pdo; + public function __construct(private UserRepository $userRepository) {} - public function __construct() + public function createUser(string $id, string $username, string $hashedPassword, string $name): User { - $dbPath = $_ENV['SYMFONY_DB_PATH'] ?? '/tmp/symfony.db'; - $this->pdo = new \PDO("sqlite:$dbPath"); + $user = new User($id, $username, $hashedPassword, $name); + $this->userRepository->save($user); + + return $user; } public function loadUserByIdentifier(string $identifier): UserInterface { - $stmt = $this->pdo->prepare('SELECT * FROM users WHERE username = ?'); - $stmt->execute([$identifier]); - $row = $stmt->fetch(\PDO::FETCH_ASSOC); - - if (!$row) { + $user = $this->userRepository->findOneBy(['username' => $identifier]); + if ($user === null) { throw new UserNotFoundException(sprintf('User "%s" not found.', $identifier)); } - return User::fromDbRow($row); + return $user; } public function refreshUser(UserInterface $user): UserInterface From ea853b3993d60e289a3cd837ec3c5a1d7f7e373f Mon Sep 17 00:00:00 2001 From: Alejandro Estringana Ruiz Date: Thu, 25 Jun 2026 17:50:57 +0200 Subject: [PATCH 06/15] Fix build --- .../php/weblogs/symfony7x/bin/init-db.php | 29 +++++++++---------- .../weblogs/symfony7x/config/services.yaml | 1 + 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/utils/build/docker/php/weblogs/symfony7x/bin/init-db.php b/utils/build/docker/php/weblogs/symfony7x/bin/init-db.php index 096423879af..8a19aac5584 100644 --- a/utils/build/docker/php/weblogs/symfony7x/bin/init-db.php +++ b/utils/build/docker/php/weblogs/symfony7x/bin/init-db.php @@ -2,37 +2,36 @@ boot(); +$config = ORMSetup::createAttributeMetadataConfiguration( + paths: [dirname(__DIR__) . '/src/Entity'], + isDevMode: false, +); -/** @var \Doctrine\ORM\EntityManagerInterface $em */ -$em = $kernel->getContainer()->get('doctrine.orm.entity_manager'); -$metadata = $em->getMetadataFactory()->getAllMetadata(); +$connection = DriverManager::getConnection(['driver' => 'pdo_sqlite', 'path' => $dbPath]); +$em = new EntityManager($connection, $config); -$schemaTool = new SchemaTool($em); -$schemaTool->updateSchema($metadata, true); +(new SchemaTool($em))->createSchema($em->getMetadataFactory()->getAllMetadata()); -$repo = $em->getRepository(User::class); $seeds = [ ['id' => 'social-security-id', 'username' => 'test', 'password' => '1234'], ['id' => '591dc126-8431-4d0f-9509-b23318d3dce4', 'username' => 'testuuid', 'password' => '1234'], ]; foreach ($seeds as $seed) { - if ($repo->find($seed['id']) === null) { + 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(); -$kernel->shutdown(); -echo "Database initialized at " . ($_SERVER['SYMFONY_DB_PATH'] ?? '/tmp/symfony.db') . "\n"; +echo "Database initialized at $dbPath\n"; diff --git a/utils/build/docker/php/weblogs/symfony7x/config/services.yaml b/utils/build/docker/php/weblogs/symfony7x/config/services.yaml index bfa08ab7870..4f47b4c2262 100644 --- a/utils/build/docker/php/weblogs/symfony7x/config/services.yaml +++ b/utils/build/docker/php/weblogs/symfony7x/config/services.yaml @@ -7,3 +7,4 @@ services: resource: '../src/' exclude: - '../src/Kernel.php' + - '../src/Security/User.php' From e9eacd143b80d48633abb637ae4e361a009315e7 Mon Sep 17 00:00:00 2001 From: Alejandro Estringana Ruiz Date: Fri, 26 Jun 2026 10:45:25 +0200 Subject: [PATCH 07/15] Fix tests --- manifests/php.yml | 5 +- .../src/Controller/AppController.php | 77 +++++++++++-------- .../src/Controller/LoginController.php | 21 +++++ 3 files changed, 70 insertions(+), 33 deletions(-) diff --git a/manifests/php.yml b/manifests/php.yml index 1b48b0c0999..1e63b87ddd1 100644 --- a/manifests/php.yml +++ b/manifests/php.yml @@ -436,7 +436,10 @@ manifest: symfony7x: 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 diff --git a/utils/build/docker/php/weblogs/symfony7x/src/Controller/AppController.php b/utils/build/docker/php/weblogs/symfony7x/src/Controller/AppController.php index 86f0485c472..c990ca329d7 100644 --- a/utils/build/docker/php/weblogs/symfony7x/src/Controller/AppController.php +++ b/utils/build/docker/php/weblogs/symfony7x/src/Controller/AppController.php @@ -75,13 +75,7 @@ public function makeDistantCall(Request $request): JsonResponse foreach ($response->getHeaders(false) as $name => $values) { $responseHeaders[$name] = implode(', ', $values); } - $requestHeaders = []; - foreach (($response->getInfo('request_header') ?? '') ? explode("\r\n", $response->getInfo('request_header')) : [] as $line) { - if (str_contains($line, ':')) { - [$key, $value] = explode(':', $line, 2); - $requestHeaders[strtolower(trim($key))] = trim($value); - } - } + $requestHeaders = $this->parseRequestHeadersFromDebug($response->getInfo('debug') ?? ''); return new JsonResponse([ 'url' => $url, @@ -574,8 +568,10 @@ public function endpointFallback(Request $request): JsonResponse switch ($case) { case 'with_route': - $rootSpan->meta['http.route'] = '/users/{id}/profile'; - $rootSpan->meta['http.method'] = 'GET'; + register_shutdown_function(function () use ($rootSpan): void { + $rootSpan->meta['http.route'] = '/users/{id}/profile'; + $rootSpan->meta['http.method'] = 'GET'; + }); return new JsonResponse([ 'status' => 'ok', @@ -584,9 +580,11 @@ public function endpointFallback(Request $request): JsonResponse ]); case 'with_endpoint': - unset($rootSpan->meta['http.route']); - $rootSpan->meta['http.endpoint'] = '/api/products/{param:int}'; - $rootSpan->meta['http.method'] = 'GET'; + 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', @@ -595,9 +593,11 @@ public function endpointFallback(Request $request): JsonResponse ]); case '404': - unset($rootSpan->meta['http.route']); - $rootSpan->meta['http.endpoint'] = '/api/notfound/{param:int}'; - $rootSpan->meta['http.method'] = 'GET'; + 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', @@ -606,9 +606,11 @@ public function endpointFallback(Request $request): JsonResponse ], 404); case 'computed': - unset($rootSpan->meta['http.route']); - $rootSpan->meta['http.url'] = 'http://localhost:8080/endpoint_fallback_computed/users/123/orders/456'; - $rootSpan->meta['http.method'] = 'GET'; + 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', @@ -681,13 +683,7 @@ public function otelDropInBaggageApiOtel(Request $request): JsonResponse foreach ($response->getHeaders(false) as $name => $values) { $responseHeaders[$name] = implode(', ', $values); } - $requestHeaders = []; - foreach (($response->getInfo('request_header') ?? '') ? explode("\r\n", $response->getInfo('request_header')) : [] as $line) { - if (str_contains($line, ':')) { - [$key, $value] = explode(':', $line, 2); - $requestHeaders[strtolower(trim($key))] = trim($value); - } - } + $requestHeaders = $this->parseRequestHeadersFromDebug($response->getInfo('debug') ?? ''); } finally { if ($scope !== null) { $scope->detach(); @@ -737,13 +733,7 @@ public function otelDropInBaggageApiDatadog(Request $request): JsonResponse foreach ($response->getHeaders(false) as $name => $values) { $responseHeaders[$name] = implode(', ', $values); } - $requestHeaders = []; - foreach (($response->getInfo('request_header') ?? '') ? explode("\r\n", $response->getInfo('request_header')) : [] as $line) { - if (str_contains($line, ':')) { - [$key, $value] = explode(':', $line, 2); - $requestHeaders[strtolower(trim($key))] = trim($value); - } - } + $requestHeaders = $this->parseRequestHeadersFromDebug($response->getInfo('debug') ?? ''); return new JsonResponse([ 'url' => $url, @@ -753,6 +743,29 @@ public function otelDropInBaggageApiDatadog(Request $request): JsonResponse ]); } + 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 { diff --git a/utils/build/docker/php/weblogs/symfony7x/src/Controller/LoginController.php b/utils/build/docker/php/weblogs/symfony7x/src/Controller/LoginController.php index df5739a4b28..d5ae37e7a8c 100644 --- a/utils/build/docker/php/weblogs/symfony7x/src/Controller/LoginController.php +++ b/utils/build/docker/php/weblogs/symfony7x/src/Controller/LoginController.php @@ -51,6 +51,7 @@ public function signup(Request $request): Response $user = $this->userProvider->createUser($id, $username, $hashedPassword, $username); $this->security->login($user, AppAuthenticator::class, 'main'); + $this->trackSignup($username, $user->getId(), []); return new Response('', 200); } @@ -120,14 +121,19 @@ private function attemptLogin(string $username, string $password): bool try { $user = $this->userProvider->loadUserByIdentifier($username); } catch (UserNotFoundException) { + $this->trackLoginFailure($username, '', false, []); + return false; } if (!$this->passwordHasher->isPasswordValid($user, $password)) { + $this->trackLoginFailure($username, $user->getId(), true, []); + return false; } $this->security->login($user, AppAuthenticator::class, 'main'); + $this->trackLoginSuccess($username, $user->getId(), []); return true; } @@ -140,4 +146,19 @@ private function callSdk(string $event, string $user, bool $exists): void \datadog\appsec\track_user_login_failure_event($user, $exists, []); } } + + private function trackLoginSuccess(string $login, string $userId, array $meta): void + { + \datadog\appsec\track_user_login_success_event_automated($login, $userId, $meta); + } + + private function trackLoginFailure(string $login, string $userId, bool $exists, array $meta): void + { + \datadog\appsec\track_user_login_failure_event_automated($login, $userId, $exists, $meta); + } + + private function trackSignup(string $login, string $userId, array $meta): void + { + \datadog\appsec\track_user_signup_event_automated($login, $userId, $meta); + } } From c0e235be39285933dcd315bb9bdf72fd5d69301a Mon Sep 17 00:00:00 2001 From: Alejandro Estringana Ruiz Date: Fri, 26 Jun 2026 17:31:03 +0200 Subject: [PATCH 08/15] Fix login --- .../src/Controller/LoginController.php | 133 ++---------------- .../src/Security/AppAuthenticator.php | 75 +++++++++- 2 files changed, 81 insertions(+), 127 deletions(-) diff --git a/utils/build/docker/php/weblogs/symfony7x/src/Controller/LoginController.php b/utils/build/docker/php/weblogs/symfony7x/src/Controller/LoginController.php index d5ae37e7a8c..4c68592d291 100644 --- a/utils/build/docker/php/weblogs/symfony7x/src/Controller/LoginController.php +++ b/utils/build/docker/php/weblogs/symfony7x/src/Controller/LoginController.php @@ -4,14 +4,14 @@ use App\Entity\User; use App\Security\AppAuthenticator; -use App\Security\UserProvider; +use Doctrine\ORM\EntityManagerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\SecurityBundle\Security; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; use Symfony\Component\Routing\Attribute\Route; -use Symfony\Component\Security\Core\Exception\UserNotFoundException; +use Symfony\Component\Security\Http\Authentication\AuthenticationUtils; class LoginController extends AbstractController { @@ -20,145 +20,32 @@ class LoginController extends AbstractController ]; public function __construct( - private UserProvider $userProvider, private UserPasswordHasherInterface $passwordHasher, private Security $security, ) {} #[Route('/login', name: 'login', methods: ['GET', 'POST'])] - public function login(Request $request): Response + public function login(AuthenticationUtils $authenticationUtils): Response { - $auth = $request->query->get('auth', ''); - - if ($auth === 'local') { - return $this->handleLocalLogin($request); - } - - if ($auth === 'basic') { - return $this->handleBasicLogin($request); - } - + // Authentication is handled by AppAuthenticator. + // This method is reached only for GET requests or when the authenticator does not support the request. return new Response('', 200); } #[Route('/signup', name: 'signup', methods: ['POST'])] - public function signup(Request $request): Response + 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 = $this->userProvider->createUser($id, $username, $hashedPassword, $username); - $this->security->login($user, AppAuthenticator::class, 'main'); - $this->trackSignup($username, $user->getId(), []); - - return new Response('', 200); - } - - private function handleLocalLogin(Request $request): Response - { - if (!$request->isMethod('POST')) { - return new Response('', 200); - } - - $username = $request->request->get('username', ''); - $password = $request->request->get('password', ''); - $sdkEvent = $request->query->get('sdk_event', ''); - $sdkTrigger = $request->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' && $sdkEvent !== '') { - $this->callSdk($sdkEvent, $sdkUser, $sdkExists); - } - - $success = $this->attemptLogin($username, $password); - - if ($sdkTrigger === 'after' && $sdkEvent !== '') { - $this->callSdk($sdkEvent, $sdkUser, $sdkExists); - } - - if ($success) { - return new Response('', 200); - } - - return new Response('', 401); - } - - private function handleBasicLogin(Request $request): Response - { - $authHeader = $request->headers->get('Authorization', ''); - if (!str_starts_with($authHeader, 'Basic ')) { - return new Response('', 401); - } - - $decoded = base64_decode(substr($authHeader, 6)); - $parts = explode(':', $decoded, 2); - $username = $parts[0] ?? ''; - $password = $parts[1] ?? ''; - - $sdkEvent = $request->query->get('sdk_event', ''); - $sdkTrigger = $request->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' && $sdkEvent !== '') { - $this->callSdk($sdkEvent, $sdkUser, $sdkExists); - } - - $success = $this->attemptLogin($username, $password); - - if ($sdkTrigger === 'after' && $sdkEvent !== '') { - $this->callSdk($sdkEvent, $sdkUser, $sdkExists); - } - - return new Response('', $success ? 200 : 401); - } - - private function attemptLogin(string $username, string $password): bool - { - try { - $user = $this->userProvider->loadUserByIdentifier($username); - } catch (UserNotFoundException) { - $this->trackLoginFailure($username, '', false, []); - - return false; - } - - if (!$this->passwordHasher->isPasswordValid($user, $password)) { - $this->trackLoginFailure($username, $user->getId(), true, []); - - return false; - } + $user = new User($id, $username, $hashedPassword, $username); + $entityManager->persist($user); + $entityManager->flush(); $this->security->login($user, AppAuthenticator::class, 'main'); - $this->trackLoginSuccess($username, $user->getId(), []); - return true; - } - - private function callSdk(string $event, string $user, bool $exists): void - { - if ($event === 'success') { - \datadog\appsec\track_user_login_success_event($user, []); - } else { - \datadog\appsec\track_user_login_failure_event($user, $exists, []); - } - } - - private function trackLoginSuccess(string $login, string $userId, array $meta): void - { - \datadog\appsec\track_user_login_success_event_automated($login, $userId, $meta); - } - - private function trackLoginFailure(string $login, string $userId, bool $exists, array $meta): void - { - \datadog\appsec\track_user_login_failure_event_automated($login, $userId, $exists, $meta); - } - - private function trackSignup(string $login, string $userId, array $meta): void - { - \datadog\appsec\track_user_signup_event_automated($login, $userId, $meta); + return new Response('', 200); } } diff --git a/utils/build/docker/php/weblogs/symfony7x/src/Security/AppAuthenticator.php b/utils/build/docker/php/weblogs/symfony7x/src/Security/AppAuthenticator.php index 720cc9feeb4..b21d3967a10 100644 --- a/utils/build/docker/php/weblogs/symfony7x/src/Security/AppAuthenticator.php +++ b/utils/build/docker/php/weblogs/symfony7x/src/Security/AppAuthenticator.php @@ -6,30 +6,97 @@ use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Exception\AuthenticationException; +use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException; use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator; use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; +use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials; use Symfony\Component\Security\Http\Authenticator\Passport\Passport; -use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport; class AppAuthenticator extends AbstractAuthenticator { public function supports(Request $request): ?bool { + if ($request->attributes->get('_route') !== 'login') { + return false; + } + + $auth = $request->query->get('auth', ''); + + if ($auth === 'local' && $request->isMethod('POST')) { + return true; + } + + if ($auth === 'basic') { + return true; + } + return false; } public function authenticate(Request $request): Passport { - throw new \LogicException('This authenticator should not be called directly.'); + $sdkTrigger = $request->query->get('sdk_trigger', ''); + $sdkEvent = $request->query->get('sdk_event', ''); + $sdkUser = $request->query->get('sdk_user', ''); + $sdkExists = filter_var($request->query->get('sdk_user_exists', 'false'), FILTER_VALIDATE_BOOLEAN); + + if ($sdkTrigger === 'before' && $sdkEvent !== '') { + $this->callSdk($sdkEvent, $sdkUser, $sdkExists); + } + + $auth = $request->query->get('auth', ''); + + if ($auth === 'basic') { + $authHeader = $request->headers->get('Authorization', ''); + if (!str_starts_with($authHeader, 'Basic ')) { + throw new CustomUserMessageAuthenticationException('Missing or invalid Authorization header'); + } + $decoded = base64_decode(substr($authHeader, 6)); + $parts = explode(':', $decoded, 2); + $username = $parts[0] ?? ''; + $password = $parts[1] ?? ''; + } else { + $username = $request->request->get('username', ''); + $password = $request->request->get('password', ''); + } + + return new Passport(new UserBadge($username), new PasswordCredentials($password)); } public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response { - return null; + $sdkTrigger = $request->query->get('sdk_trigger', ''); + $sdkEvent = $request->query->get('sdk_event', ''); + $sdkUser = $request->query->get('sdk_user', ''); + $sdkExists = filter_var($request->query->get('sdk_user_exists', 'false'), FILTER_VALIDATE_BOOLEAN); + + if ($sdkTrigger === 'after' && $sdkEvent !== '') { + $this->callSdk($sdkEvent, $sdkUser, $sdkExists); + } + + return new Response('', 200); } public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response { - return null; + $sdkTrigger = $request->query->get('sdk_trigger', ''); + $sdkEvent = $request->query->get('sdk_event', ''); + $sdkUser = $request->query->get('sdk_user', ''); + $sdkExists = filter_var($request->query->get('sdk_user_exists', 'false'), FILTER_VALIDATE_BOOLEAN); + + if ($sdkTrigger === 'after' && $sdkEvent !== '') { + $this->callSdk($sdkEvent, $sdkUser, $sdkExists); + } + + return new Response('', 401); + } + + private function callSdk(string $event, string $user, bool $exists): void + { + if ($event === 'success') { + \datadog\appsec\track_user_login_success_event($user, []); + } else { + \datadog\appsec\track_user_login_failure_event($user, $exists, []); + } } } From 9e89444e4b26d0d6a17ba958e80971fe7ac96c12 Mon Sep 17 00:00:00 2001 From: Alejandro Estringana Ruiz Date: Tue, 30 Jun 2026 16:11:51 +0200 Subject: [PATCH 09/15] Fix pipeline --- manifests/php.yml | 3 +++ .../symfony7x/src/Controller/AppController.php | 11 ++++++----- .../symfony7x/src/Controller/LoginController.php | 2 ++ .../symfony7x/src/Security/AppAuthenticator.php | 10 ++++++++++ 4 files changed, 21 insertions(+), 5 deletions(-) diff --git a/manifests/php.yml b/manifests/php.yml index 1e63b87ddd1..500f0c6dc38 100644 --- a/manifests/php.yml +++ b/manifests/php.yml @@ -443,6 +443,9 @@ manifest: 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 diff --git a/utils/build/docker/php/weblogs/symfony7x/src/Controller/AppController.php b/utils/build/docker/php/weblogs/symfony7x/src/Controller/AppController.php index c990ca329d7..646982f06e6 100644 --- a/utils/build/docker/php/weblogs/symfony7x/src/Controller/AppController.php +++ b/utils/build/docker/php/weblogs/symfony7x/src/Controller/AppController.php @@ -12,14 +12,16 @@ use Symfony\Component\Process\Exception\RuntimeException as ProcessRuntimeException; use Symfony\Component\Process\Process; use Symfony\Component\Routing\Attribute\Route; +use Symfony\Component\Security\Http\Attribute\IsGranted; class AppController extends AbstractController { + // PUBLIC_ACCESS allows anonymous access while still triggering AccessDecisionManager::decide, + // which lets the tracer auto-call track_authenticated_user_event_automated for authenticated users. + #[IsGranted('PUBLIC_ACCESS')] #[Route('/', name: 'index', methods: ['GET'])] public function index(): Response { - $this->getUser(); - return new Response("Hello world!\n", 200, [ 'Content-Type' => 'text/plain; charset=utf-8', 'Content-Length' => '13', @@ -67,9 +69,8 @@ public function makeDistantCall(Request $request): JsonResponse return new JsonResponse(['error' => 'url parameter required'], 400); } - $client = HttpClient::create(); - $response = $client->request('GET', $url); - + $client = HttpClient::create(); + $response = $client->request('GET', $url); $statusCode = $response->getStatusCode(); $responseHeaders = []; foreach ($response->getHeaders(false) as $name => $values) { diff --git a/utils/build/docker/php/weblogs/symfony7x/src/Controller/LoginController.php b/utils/build/docker/php/weblogs/symfony7x/src/Controller/LoginController.php index 4c68592d291..fef8843a615 100644 --- a/utils/build/docker/php/weblogs/symfony7x/src/Controller/LoginController.php +++ b/utils/build/docker/php/weblogs/symfony7x/src/Controller/LoginController.php @@ -42,6 +42,8 @@ public function signup(Request $request, EntityManagerInterface $entityManager): $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(); $this->security->login($user, AppAuthenticator::class, 'main'); diff --git a/utils/build/docker/php/weblogs/symfony7x/src/Security/AppAuthenticator.php b/utils/build/docker/php/weblogs/symfony7x/src/Security/AppAuthenticator.php index b21d3967a10..4435ffd2476 100644 --- a/utils/build/docker/php/weblogs/symfony7x/src/Security/AppAuthenticator.php +++ b/utils/build/docker/php/weblogs/symfony7x/src/Security/AppAuthenticator.php @@ -60,11 +60,19 @@ public function authenticate(Request $request): Passport $password = $request->request->get('password', ''); } + // Store username in session so the tracer's handleAuthenticationFailure hook can extract it + // via extractLoginFromAuthFailure Path C (_security.last_username) for wrong-password failures. + if ($request->hasSession()) { + $request->getSession()->set('_security.last_username', $username); + } + return new Passport(new UserBadge($username), new PasswordCredentials($password)); } public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response { + // Login success is tracked automatically by the tracer via + // AuthenticatorManager::handleAuthenticationSuccess hook. $sdkTrigger = $request->query->get('sdk_trigger', ''); $sdkEvent = $request->query->get('sdk_event', ''); $sdkUser = $request->query->get('sdk_user', ''); @@ -79,6 +87,8 @@ public function onAuthenticationSuccess(Request $request, TokenInterface $token, public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response { + // Login failure is tracked automatically by the tracer via + // AuthenticatorManager::handleAuthenticationFailure hook. $sdkTrigger = $request->query->get('sdk_trigger', ''); $sdkEvent = $request->query->get('sdk_event', ''); $sdkUser = $request->query->get('sdk_user', ''); From 4e373c060ffc980ca093c194b36e82467d880b1f Mon Sep 17 00:00:00 2001 From: Alejandro Estringana Ruiz Date: Wed, 1 Jul 2026 17:26:49 +0200 Subject: [PATCH 10/15] Fix some tests --- manifests/php.yml | 10 -- .../symfony7x/config/packages/security.yaml | 13 +- .../src/Controller/LoginController.php | 10 +- .../src/Security/AppAuthenticator.php | 112 ------------------ .../src/Security/LoginSuccessHandler.php | 25 ++++ 5 files changed, 37 insertions(+), 133 deletions(-) delete mode 100644 utils/build/docker/php/weblogs/symfony7x/src/Security/AppAuthenticator.php create mode 100644 utils/build/docker/php/weblogs/symfony7x/src/Security/LoginSuccessHandler.php diff --git a/manifests/php.yml b/manifests/php.yml index 500f0c6dc38..b51c2c8c96a 100644 --- a/manifests/php.yml +++ b/manifests/php.yml @@ -367,7 +367,6 @@ manifest: - weblog_declaration: "*": missing_feature (Basic auth not implemented) laravel11x: v1.8.0 - symfony7x: v1.8.0 tests/appsec/test_automated_login_events.py::Test_V3_Login_Events::test_login_sdk_failure_local: - weblog_declaration: laravel11x: v1.8.0 @@ -376,7 +375,6 @@ manifest: - weblog_declaration: "*": missing_feature (Basic auth not implemented) laravel11x: v1.8.0 - symfony7x: v1.8.0 tests/appsec/test_automated_login_events.py::Test_V3_Login_Events::test_login_sdk_success_local: - weblog_declaration: laravel11x: v1.8.0 @@ -385,12 +383,10 @@ manifest: - weblog_declaration: "*": missing_feature (Basic auth not implemented) laravel11x: v1.8.0 - symfony7x: v1.8.0 tests/appsec/test_automated_login_events.py::Test_V3_Login_Events::test_login_wrong_password_failure_basic: - weblog_declaration: "*": missing_feature (Basic auth not implemented) laravel11x: v1.8.0 - symfony7x: v1.8.0 tests/appsec/test_automated_login_events.py::Test_V3_Login_Events::test_login_wrong_password_failure_local: - weblog_declaration: laravel11x: v1.8.0 @@ -399,13 +395,11 @@ manifest: - weblog_declaration: "*": missing_feature (Basic auth not implemented) laravel11x: v1.8.0 - symfony7x: v1.8.0 tests/appsec/test_automated_login_events.py::Test_V3_Login_Events_Anon: v1.8.0 tests/appsec/test_automated_login_events.py::Test_V3_Login_Events_Anon::test_login_sdk_failure_basic: - weblog_declaration: "*": missing_feature (Basic auth not implemented) laravel11x: v1.8.0 - symfony7x: v1.8.0 tests/appsec/test_automated_login_events.py::Test_V3_Login_Events_Anon::test_login_sdk_failure_local: - weblog_declaration: laravel11x: v1.8.0 @@ -414,7 +408,6 @@ manifest: - weblog_declaration: "*": missing_feature (Basic auth not implemented) laravel11x: v1.8.0 - symfony7x: v1.8.0 tests/appsec/test_automated_login_events.py::Test_V3_Login_Events_Anon::test_login_sdk_success_local: - weblog_declaration: laravel11x: v1.8.0 @@ -423,17 +416,14 @@ manifest: - weblog_declaration: "*": missing_feature (Basic auth not implemented) laravel11x: v1.8.0 - symfony7x: v1.8.0 tests/appsec/test_automated_login_events.py::Test_V3_Login_Events_Anon::test_login_wrong_password_failure_basic: - weblog_declaration: "*": missing_feature (Basic auth not implemented) laravel11x: v1.8.0 - symfony7x: v1.8.0 tests/appsec/test_automated_login_events.py::Test_V3_Login_Events_Anon::test_login_wrong_user_failure_basic: - weblog_declaration: "*": missing_feature (Basic auth not implemented) laravel11x: v1.8.0 - symfony7x: 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: diff --git a/utils/build/docker/php/weblogs/symfony7x/config/packages/security.yaml b/utils/build/docker/php/weblogs/symfony7x/config/packages/security.yaml index fe570eebb29..85831e0e36f 100644 --- a/utils/build/docker/php/weblogs/symfony7x/config/packages/security.yaml +++ b/utils/build/docker/php/weblogs/symfony7x/config/packages/security.yaml @@ -5,15 +5,20 @@ security: providers: app_user_provider: - id: App\Security\UserProvider - + entity: + class: App\Entity\User + property: username firewalls: main: pattern: ^/ lazy: true provider: app_user_provider stateless: false - custom_authenticators: - - App\Security\AppAuthenticator + form_login: + login_path: login + check_path: login + username_parameter: username + password_parameter: password + success_handler: App\Security\LoginSuccessHandler access_control: [] diff --git a/utils/build/docker/php/weblogs/symfony7x/src/Controller/LoginController.php b/utils/build/docker/php/weblogs/symfony7x/src/Controller/LoginController.php index fef8843a615..69a45006e89 100644 --- a/utils/build/docker/php/weblogs/symfony7x/src/Controller/LoginController.php +++ b/utils/build/docker/php/weblogs/symfony7x/src/Controller/LoginController.php @@ -3,7 +3,6 @@ namespace App\Controller; use App\Entity\User; -use App\Security\AppAuthenticator; use Doctrine\ORM\EntityManagerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\SecurityBundle\Security; @@ -11,7 +10,6 @@ use Symfony\Component\HttpFoundation\Response; use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; use Symfony\Component\Routing\Attribute\Route; -use Symfony\Component\Security\Http\Authentication\AuthenticationUtils; class LoginController extends AbstractController { @@ -25,11 +23,9 @@ public function __construct( ) {} #[Route('/login', name: 'login', methods: ['GET', 'POST'])] - public function login(AuthenticationUtils $authenticationUtils): Response + public function login(): Response { - // Authentication is handled by AppAuthenticator. - // This method is reached only for GET requests or when the authenticator does not support the request. - return new Response('', 200); + return new Response('', Response::HTTP_OK); } #[Route('/signup', name: 'signup', methods: ['POST'])] @@ -46,7 +42,7 @@ public function signup(Request $request, EntityManagerInterface $entityManager): // Signup event is tracked automatically by the tracer via Doctrine\ORM\UnitOfWork::executeInserts hook. $entityManager->flush(); - $this->security->login($user, AppAuthenticator::class, 'main'); + $this->security->login($user); return new Response('', 200); } diff --git a/utils/build/docker/php/weblogs/symfony7x/src/Security/AppAuthenticator.php b/utils/build/docker/php/weblogs/symfony7x/src/Security/AppAuthenticator.php deleted file mode 100644 index 4435ffd2476..00000000000 --- a/utils/build/docker/php/weblogs/symfony7x/src/Security/AppAuthenticator.php +++ /dev/null @@ -1,112 +0,0 @@ -attributes->get('_route') !== 'login') { - return false; - } - - $auth = $request->query->get('auth', ''); - - if ($auth === 'local' && $request->isMethod('POST')) { - return true; - } - - if ($auth === 'basic') { - return true; - } - - return false; - } - - public function authenticate(Request $request): Passport - { - $sdkTrigger = $request->query->get('sdk_trigger', ''); - $sdkEvent = $request->query->get('sdk_event', ''); - $sdkUser = $request->query->get('sdk_user', ''); - $sdkExists = filter_var($request->query->get('sdk_user_exists', 'false'), FILTER_VALIDATE_BOOLEAN); - - if ($sdkTrigger === 'before' && $sdkEvent !== '') { - $this->callSdk($sdkEvent, $sdkUser, $sdkExists); - } - - $auth = $request->query->get('auth', ''); - - if ($auth === 'basic') { - $authHeader = $request->headers->get('Authorization', ''); - if (!str_starts_with($authHeader, 'Basic ')) { - throw new CustomUserMessageAuthenticationException('Missing or invalid Authorization header'); - } - $decoded = base64_decode(substr($authHeader, 6)); - $parts = explode(':', $decoded, 2); - $username = $parts[0] ?? ''; - $password = $parts[1] ?? ''; - } else { - $username = $request->request->get('username', ''); - $password = $request->request->get('password', ''); - } - - // Store username in session so the tracer's handleAuthenticationFailure hook can extract it - // via extractLoginFromAuthFailure Path C (_security.last_username) for wrong-password failures. - if ($request->hasSession()) { - $request->getSession()->set('_security.last_username', $username); - } - - return new Passport(new UserBadge($username), new PasswordCredentials($password)); - } - - public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response - { - // Login success is tracked automatically by the tracer via - // AuthenticatorManager::handleAuthenticationSuccess hook. - $sdkTrigger = $request->query->get('sdk_trigger', ''); - $sdkEvent = $request->query->get('sdk_event', ''); - $sdkUser = $request->query->get('sdk_user', ''); - $sdkExists = filter_var($request->query->get('sdk_user_exists', 'false'), FILTER_VALIDATE_BOOLEAN); - - if ($sdkTrigger === 'after' && $sdkEvent !== '') { - $this->callSdk($sdkEvent, $sdkUser, $sdkExists); - } - - return new Response('', 200); - } - - public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response - { - // Login failure is tracked automatically by the tracer via - // AuthenticatorManager::handleAuthenticationFailure hook. - $sdkTrigger = $request->query->get('sdk_trigger', ''); - $sdkEvent = $request->query->get('sdk_event', ''); - $sdkUser = $request->query->get('sdk_user', ''); - $sdkExists = filter_var($request->query->get('sdk_user_exists', 'false'), FILTER_VALIDATE_BOOLEAN); - - if ($sdkTrigger === 'after' && $sdkEvent !== '') { - $this->callSdk($sdkEvent, $sdkUser, $sdkExists); - } - - return new Response('', 401); - } - - private function callSdk(string $event, string $user, bool $exists): void - { - if ($event === 'success') { - \datadog\appsec\track_user_login_success_event($user, []); - } else { - \datadog\appsec\track_user_login_failure_event($user, $exists, []); - } - } -} 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..ad426870652 --- /dev/null +++ b/utils/build/docker/php/weblogs/symfony7x/src/Security/LoginSuccessHandler.php @@ -0,0 +1,25 @@ +duplicate(); + $subRequest->setMethod('GET'); + $subRequest->request->replace([]); + + return $this->httpKernel->handle($subRequest, HttpKernelInterface::SUB_REQUEST); + } +} From 20fb5bf9be154e14b95005c5565a9192947204c3 Mon Sep 17 00:00:00 2001 From: Alejandro Estringana Ruiz Date: Thu, 2 Jul 2026 10:37:04 +0200 Subject: [PATCH 11/15] Allow to mark weblogs as missing ids --- tests/appsec/test_automated_login_events.py | 29 ++++++++++++++------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/tests/appsec/test_automated_login_events.py b/tests/appsec/test_automated_login_events.py index 6d709d351ee..a4200be64aa 100644 --- a/tests/appsec/test_automated_login_events.py +++ b/tests/appsec/test_automated_login_events.py @@ -1160,6 +1160,9 @@ def validate_iden(meta: dict): # Weblog variants that omit usr.id on login failure (library-wide: libs_without_user_id_on_failure). 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: """True when this library and weblog emit usr.exists on login failure spans.""" @@ -1174,6 +1177,14 @@ 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 +1237,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 +1259,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 +1375,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 +1405,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" @@ -1532,7 +1543,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 +1563,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 +1679,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 +1707,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 @@ -1903,7 +1914,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 From 3b070b9dbec157199232f621b4ae8aded47b090b Mon Sep 17 00:00:00 2001 From: Alejandro Estringana Ruiz Date: Thu, 2 Jul 2026 10:42:43 +0200 Subject: [PATCH 12/15] Skip non appsec tests failing --- manifests/php.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/manifests/php.yml b/manifests/php.yml index b51c2c8c96a..e11c3c59831 100644 --- a/manifests/php.yml +++ b/manifests/php.yml @@ -1289,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: From a5f72f4c34a4fcc7bc9f6076a2df55351f19533a Mon Sep 17 00:00:00 2001 From: Alejandro Estringana Ruiz Date: Thu, 2 Jul 2026 11:42:32 +0200 Subject: [PATCH 13/15] Lint --- tests/appsec/test_automated_login_events.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/appsec/test_automated_login_events.py b/tests/appsec/test_automated_login_events.py index a4200be64aa..1d63c5f1f73 100644 --- a/tests/appsec/test_automated_login_events.py +++ b/tests/appsec/test_automated_login_events.py @@ -1179,10 +1179,7 @@ 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 - ) + 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") From df095e61a913af9de38593c2464b91920353c8c2 Mon Sep 17 00:00:00 2001 From: Alejandro Estringana Ruiz Date: Thu, 2 Jul 2026 13:15:50 +0200 Subject: [PATCH 14/15] Fix failures --- tests/appsec/test_automated_login_events.py | 4 +-- .../symfony7x/config/packages/security.yaml | 1 + .../src/Controller/LoginController.php | 4 --- .../src/Security/LoginFailureHandler.php | 28 +++++++++++++++++++ .../src/Security/LoginSuccessHandler.php | 11 ++++++++ 5 files changed, 42 insertions(+), 6 deletions(-) create mode 100644 utils/build/docker/php/weblogs/symfony7x/src/Security/LoginFailureHandler.php diff --git a/tests/appsec/test_automated_login_events.py b/tests/appsec/test_automated_login_events.py index 1d63c5f1f73..4b02fad47cb 100644 --- a/tests/appsec/test_automated_login_events.py +++ b/tests/appsec/test_automated_login_events.py @@ -1470,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" @@ -1772,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 diff --git a/utils/build/docker/php/weblogs/symfony7x/config/packages/security.yaml b/utils/build/docker/php/weblogs/symfony7x/config/packages/security.yaml index 85831e0e36f..def0cc5f4a4 100644 --- a/utils/build/docker/php/weblogs/symfony7x/config/packages/security.yaml +++ b/utils/build/docker/php/weblogs/symfony7x/config/packages/security.yaml @@ -20,5 +20,6 @@ security: 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/src/Controller/LoginController.php b/utils/build/docker/php/weblogs/symfony7x/src/Controller/LoginController.php index 69a45006e89..233a18a1232 100644 --- a/utils/build/docker/php/weblogs/symfony7x/src/Controller/LoginController.php +++ b/utils/build/docker/php/weblogs/symfony7x/src/Controller/LoginController.php @@ -5,7 +5,6 @@ use App\Entity\User; use Doctrine\ORM\EntityManagerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; -use Symfony\Bundle\SecurityBundle\Security; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; @@ -19,7 +18,6 @@ class LoginController extends AbstractController public function __construct( private UserPasswordHasherInterface $passwordHasher, - private Security $security, ) {} #[Route('/login', name: 'login', methods: ['GET', 'POST'])] @@ -42,8 +40,6 @@ public function signup(Request $request, EntityManagerInterface $entityManager): // Signup event is tracked automatically by the tracer via Doctrine\ORM\UnitOfWork::executeInserts hook. $entityManager->flush(); - $this->security->login($user); - return new Response('', 200); } } 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 index ad426870652..e87785a61d9 100644 --- a/utils/build/docker/php/weblogs/symfony7x/src/Security/LoginSuccessHandler.php +++ b/utils/build/docker/php/weblogs/symfony7x/src/Security/LoginSuccessHandler.php @@ -16,6 +16,17 @@ public function __construct( public function onAuthenticationSuccess(Request $request, TokenInterface $token): Response { + $sdkTrigger = $request->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([]); From faf23d9c78be31bf38f662f4b1010fe23f9fed2a Mon Sep 17 00:00:00 2001 From: Alejandro Estringana Ruiz Date: Fri, 3 Jul 2026 15:26:52 +0200 Subject: [PATCH 15/15] Make weblog more symfony approach --- .../php/weblogs/symfony7x/composer.json | 3 + .../symfony7x/config/packages/doctrine.yaml | 23 +- .../src/Controller/AppController.php | 265 ++++++++++-------- .../weblogs/symfony7x/src/Security/User.php | 3 - 4 files changed, 172 insertions(+), 122 deletions(-) delete mode 100644 utils/build/docker/php/weblogs/symfony7x/src/Security/User.php diff --git a/utils/build/docker/php/weblogs/symfony7x/composer.json b/utils/build/docker/php/weblogs/symfony7x/composer.json index ff8eb376c48..3491a81391d 100644 --- a/utils/build/docker/php/weblogs/symfony7x/composer.json +++ b/utils/build/docker/php/weblogs/symfony7x/composer.json @@ -11,6 +11,8 @@ "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", @@ -48,6 +50,7 @@ "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" } }, diff --git a/utils/build/docker/php/weblogs/symfony7x/config/packages/doctrine.yaml b/utils/build/docker/php/weblogs/symfony7x/config/packages/doctrine.yaml index b5aa6829b92..145bf09e1a1 100644 --- a/utils/build/docker/php/weblogs/symfony7x/config/packages/doctrine.yaml +++ b/utils/build/docker/php/weblogs/symfony7x/config/packages/doctrine.yaml @@ -3,8 +3,27 @@ parameters: doctrine: dbal: - driver: pdo_sqlite - path: '%env(default:default_sqlite_path:SYMFONY_DB_PATH)%' + 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 diff --git a/utils/build/docker/php/weblogs/symfony7x/src/Controller/AppController.php b/utils/build/docker/php/weblogs/symfony7x/src/Controller/AppController.php index 646982f06e6..86ac75c3318 100644 --- a/utils/build/docker/php/weblogs/symfony7x/src/Controller/AppController.php +++ b/utils/build/docker/php/weblogs/symfony7x/src/Controller/AppController.php @@ -2,9 +2,14 @@ namespace App\Controller; +use Doctrine\DBAL\Connection; use Monolog\Handler\StreamHandler; use Monolog\Logger; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\DependencyInjection\Attribute\Autowire; +use Symfony\Component\DomCrawler\Crawler; +use Symfony\Component\Filesystem\Exception\IOException; +use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\HttpClient\HttpClient; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; @@ -16,6 +21,15 @@ class AppController extends AbstractController { + public function __construct( + #[Autowire(service: 'doctrine.dbal.default_connection')] + private Connection $defaultConnection, + #[Autowire(service: 'doctrine.dbal.mysql_connection')] + private Connection $mysqlConnection, + #[Autowire(service: 'doctrine.dbal.postgres_connection')] + private Connection $postgresConnection, + ) {} + // PUBLIC_ACCESS allows anonymous access while still triggering AccessDecisionManager::decide, // which lets the tracer auto-call track_authenticated_user_event_automated for authenticated users. #[IsGranted('PUBLIC_ACCESS')] @@ -89,10 +103,13 @@ public function makeDistantCall(Request $request): JsonResponse #[Route('/read_file', name: 'read_file', methods: ['GET'])] public function readFile(Request $request): Response { - $file = $request->query->get('file', ''); - $content = @file_get_contents($file); + $filePath = $request->query->get('file', ''); + $content = ''; + try { + $content = (new Filesystem())->readFile($filePath); + } catch (IOException $e) {} - return new Response($content === false ? '' : $content, 200, ['Content-Type' => 'text/plain']); + return new Response($content, 200, ['Content-Type' => 'text/plain']); } #[Route('/users', name: 'users', methods: ['GET'])] @@ -287,12 +304,11 @@ public function dbm(Request $request): Response $query = 'SELECT version()'; if ($integration === 'pdo-mysql') { - $connection = new \PDO('mysql:dbname=mysql_dbname;host=mysqldb', 'mysqldb', 'mysqldb'); - $connection->query($query); + $this->mysqlConnection->executeQuery($query); } elseif ($integration === 'pdo-pgsql') { - $connection = new \PDO('pgsql:dbname=system_tests_dbname;host=postgres;port=5433', 'system_tests_user', 'system_tests'); - $connection->query($query); + $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); } @@ -359,18 +375,20 @@ public function userLoginFailureEventV2(Request $request): Response public function stubDbm(Request $request): JsonResponse { $integration = $request->query->get('integration', ''); - $stmt = null; + $captured = null; if ($integration === 'pdo-pgsql') { - $con = new \PDO('pgsql:dbname=system_tests_dbname;host=postgres;port=5433', 'system_tests_user', 'system_tests'); - $stmt = $con->query('SELECT version()'); + /** @var \PDO $nativePdo */ + $nativePdo = $this->postgresConnection->getNativeConnection(); + $stmt = $nativePdo->query('SELECT version()'); + $captured = $stmt instanceof \PDOStatement ? $stmt->queryString : null; } elseif ($integration === 'pdo-mysql') { - $con = new \PDO('mysql:dbname=mysql_dbname;host=mysqldb', 'mysqldb', 'mysqldb'); - $stmt = $con->query('SELECT version()'); + /** @var \PDO $nativePdo */ + $nativePdo = $this->mysqlConnection->getNativeConnection(); + $stmt = $nativePdo->query('SELECT version()'); + $captured = $stmt instanceof \PDOStatement ? $stmt->queryString : null; } - $captured = $stmt instanceof \PDOStatement ? $stmt->queryString : null; - return new JsonResponse([ 'status' => 'ok', 'dbm_comment' => $captured, @@ -386,17 +404,17 @@ public function raspSqli(Request $request): Response $decoded = json_decode($request->getContent(), true); $userId = $decoded['user_id'] ?? ''; } elseif ($contentType === 'application/xml') { - $decoded = simplexml_load_string(stripslashes($request->getContent())); - $userId = (string) ($decoded[0] ?? ''); + $crawler = new Crawler(); + $crawler->addXmlContent(stripslashes($request->getContent())); + $userId = $crawler->text(''); } else { $userId = urldecode($request->get('user_id', '')); } - // Use SQLite (always available) for RASP SQL injection detection - $dbPath = getenv('SYMFONY_DB_PATH') ?: '/tmp/symfony.db'; - $pdo = new \PDO("sqlite:$dbPath"); // Intentionally unsafe query so ddtrace RASP can detect the injection - @$pdo->query("SELECT * FROM users WHERE id = '" . $userId . "'"); + 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']); } @@ -410,12 +428,16 @@ public function raspLfi(Request $request): Response $decoded = json_decode($request->getContent(), true); $file = $decoded['file'] ?? ''; } elseif ($contentType === 'application/xml') { - $decoded = simplexml_load_string(stripslashes($request->getContent())); - $file = (string) ($decoded[0] ?? ''); + $crawler = new Crawler(); + $crawler->addXmlContent(stripslashes($request->getContent())); + $file = $crawler->text(''); } else { $file = urldecode($request->get('file', '')); } - @file_get_contents($file); + + try { + (new Filesystem())->readFile($file); + } catch (IOException $e) {} return new Response('Hello, LFI!', 200, ['Content-Type' => 'text/plain']); } @@ -429,11 +451,14 @@ public function raspSsrf(Request $request): Response $decoded = json_decode($request->getContent(), true); $domain = $decoded['domain'] ?? ''; } elseif ($contentType === 'application/xml') { - $decoded = simplexml_load_string(stripslashes($request->getContent())); - $domain = (string) ($decoded[0] ?? ''); + $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']); @@ -442,9 +467,16 @@ public function raspSsrf(Request $request): Response #[Route('/rasp/multiple', name: 'rasp_multiple', methods: ['GET'])] public function raspMultiple(Request $request): Response { - @file_get_contents(urldecode($request->query->get('file1', ''))); - @file_get_contents(urldecode($request->query->get('file2', ''))); - @file_get_contents('../etc/passwd'); + $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']); } @@ -455,94 +487,10 @@ public function db(Request $request): Response $service = $request->query->get('service', ''); $operation = $request->query->get('operation', ''); - $mysqlOp = static function (string $op): void { - $db = new \PDO('mysql:dbname=mysql_dbname;host=mysqldb', 'mysqldb', 'mysqldb'); - switch ($op) { - case 'init': - $db->exec('CREATE TABLE IF NOT EXISTS demo(id INT NOT NULL, name VARCHAR(20) NOT NULL, age INT NOT NULL, PRIMARY KEY (id))'); - $db->exec("INSERT IGNORE INTO demo (id, name, age) VALUES (1, 'test', 16)"); - $db->exec("INSERT IGNORE INTO demo (id, name, age) VALUES (2, 'test2', 17)"); - $db->exec('DROP PROCEDURE IF EXISTS test_procedure'); - $db->exec('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': - $db->query('SELECT * from demo where id=1 or id IN (3, 4)'); - break; - case 'insert': - try { - $db->exec("insert into demo (id, name, age) values(3, 'test3', 163)"); - } catch (\PDOException $e) {} - break; - case 'update': - $db->exec('update demo set age=22 where id=1'); - break; - case 'delete': - $db->exec('delete from demo where id=2 or id=11111111'); - break; - case 'procedure': - $db->exec("call test_procedure(1, 'test')"); - break; - case 'select_error': - try { - $db->query('SELECT * from demosssss where id=1 or id=233333'); - } catch (\PDOException $e) {} - break; - } - }; - - $postgresOp = static function (string $op): void { - $db = new \PDO('pgsql:dbname=system_tests_dbname;host=postgres;port=5433', 'system_tests_user', 'system_tests'); - switch ($op) { - case 'init': - try { - $db->exec('CREATE TABLE demo(id INT NOT NULL, name VARCHAR(20) NOT NULL, age INT NOT NULL, PRIMARY KEY (id))'); - } catch (\PDOException $e) {} - try { - $db->exec("INSERT INTO demo (id, name, age) VALUES (1, 'test', 16)"); - } catch (\PDOException $e) {} - try { - $db->exec("INSERT INTO demo (id, name, age) VALUES (2, 'test2', 17)"); - } catch (\PDOException $e) {} - $db->exec("CREATE OR REPLACE PROCEDURE helloworld(id int, other varchar(10)) -LANGUAGE plpgsql -AS \$\$ -BEGIN - raise info 'Hello World'; -END; -\$\$"); - break; - case 'select': - $db->query('SELECT * from demo where id=1 or id IN (3, 4)'); - break; - case 'insert': - try { - $db->exec("insert into demo (id, name, age) values(3, 'test3', 163)"); - } catch (\PDOException $e) {} - break; - case 'update': - $db->exec("update demo set age=22 where name like '%tes%'"); - break; - case 'delete': - $db->exec('delete from demo where id=2 or id=11111111'); - break; - case 'procedure': - $db->exec("call helloworld(1, 'test')"); - break; - case 'select_error': - try { - $db->query('SELECT * from demosssssssss where id=1 or id=233333'); - } catch (\PDOException $e) {} - break; - } - }; - if ($service === 'mysql') { - $mysqlOp($operation); + $this->mysqlOperation($operation); } elseif ($service === 'postgresql') { - $postgresOp($operation); + $this->postgresOperation($operation); } else { return new Response('Unsupported service: ' . htmlspecialchars($service), 400, ['Content-Type' => 'text/plain']); } @@ -554,9 +502,8 @@ public function db(Request $request): Response public function traceSql(): Response { try { - $pdo = new \PDO('mysql:dbname=mysql_dbname;host=mysqldb', 'mysqldb', 'mysqldb'); - $pdo->query('SELECT 1'); - } catch (\Exception $e) {} + $this->mysqlConnection->executeQuery('SELECT 1'); + } catch (\Doctrine\DBAL\Exception $e) {} return new Response('OK', 200, ['Content-Type' => 'text/plain']); } @@ -850,4 +797,88 @@ public function stripeCreatePaymentIntent(Request $request): JsonResponse 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/Security/User.php b/utils/build/docker/php/weblogs/symfony7x/src/Security/User.php deleted file mode 100644 index 61239f56d1f..00000000000 --- a/utils/build/docker/php/weblogs/symfony7x/src/Security/User.php +++ /dev/null @@ -1,3 +0,0 @@ -