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