From 594af25f299948ebb736eb61933ba3d0195038d3 Mon Sep 17 00:00:00 2001 From: Alejandro Estringana Ruiz Date: Wed, 18 Mar 2026 16:52:33 +0100 Subject: [PATCH 1/6] Add ato appsec events to Laminas --- .../Laminas/LaminasIntegration.php | 278 ++++++++++++++++++ .../Laminas/Mvc/Latest/composer.json | 8 +- .../config/autoload/database.global.php | 25 ++ .../Application/config/module.config.php | 33 +++ .../src/Controller/LoginController.php | 126 ++++++++ .../src/Controller/LoginControllerFactory.php | 21 ++ .../Laminas/Mvc/Version_3_3/composer.json | 8 +- .../config/autoload/database.global.php | 25 ++ .../Mvc/Version_3_3/config/modules.config.php | 2 + .../Application/config/module.config.php | 33 +++ .../src/Controller/LoginController.php | 126 ++++++++ .../src/Controller/LoginControllerFactory.php | 21 ++ .../Mvc/AutomatedLoginEventsTestSuite.php | 126 ++++++++ .../Mvc/Latest/AutomatedLoginEventsTest.php | 18 ++ .../Mvc/V3_3/AutomatedLoginEventsTest.php | 18 ++ 15 files changed, 864 insertions(+), 4 deletions(-) create mode 100644 tests/Frameworks/Laminas/Mvc/Latest/config/autoload/database.global.php create mode 100644 tests/Frameworks/Laminas/Mvc/Latest/module/Application/src/Controller/LoginController.php create mode 100644 tests/Frameworks/Laminas/Mvc/Latest/module/Application/src/Controller/LoginControllerFactory.php create mode 100644 tests/Frameworks/Laminas/Mvc/Version_3_3/config/autoload/database.global.php create mode 100644 tests/Frameworks/Laminas/Mvc/Version_3_3/module/Application/src/Controller/LoginController.php create mode 100644 tests/Frameworks/Laminas/Mvc/Version_3_3/module/Application/src/Controller/LoginControllerFactory.php create mode 100644 tests/Integrations/Laminas/Mvc/AutomatedLoginEventsTestSuite.php create mode 100644 tests/Integrations/Laminas/Mvc/Latest/AutomatedLoginEventsTest.php create mode 100644 tests/Integrations/Laminas/Mvc/V3_3/AutomatedLoginEventsTest.php diff --git a/src/DDTrace/Integrations/Laminas/LaminasIntegration.php b/src/DDTrace/Integrations/Laminas/LaminasIntegration.php index 5db75ce2872..e730dbc4a6e 100644 --- a/src/DDTrace/Integrations/Laminas/LaminasIntegration.php +++ b/src/DDTrace/Integrations/Laminas/LaminasIntegration.php @@ -582,10 +582,288 @@ static function ($This, $scope, $args) { } ); + // Track login by hooking the storage write operation + // This captures the full user object after it's stored + hook_method( + 'Laminas\Authentication\Storage\Session', + 'write', + null, + static function ($This, $scope, $args, $returnValue) { + if (!function_exists('\datadog\appsec\track_user_login_success_event_automated')) { + return; + } + + // The first argument to write() is the identity + $identity = isset($args[0]) ? $args[0] : null; + if (!$identity) { + return; + } + + // Skip if identity is just a string (initial write from authenticate()) + // We only want to track when the full user object is written by the controller + if (is_string($identity)) { + return; + } + + // Only track if this looks like a user object (has id property) + $userId = self::getUserId($identity); + if (!$userId) { + return; + } + + $userLogin = self::getUserLogin($identity); + $metadata = self::getUserMetadata($identity); + + \datadog\appsec\track_user_login_success_event_automated( + $userLogin, + $userId, + $metadata + ); + } + ); + + // Authentication tracking - Login failure + install_hook( + 'Laminas\Authentication\AuthenticationService::authenticate', + null, + static function (HookData $hook) { + $result = $hook->returned; + + if (!$result instanceof \Laminas\Authentication\Result) { + return; + } + + $code = $result->getCode(); + + // Only track failures + if ($code === \Laminas\Authentication\Result::SUCCESS) { + return; + } + + // Login failure + if (!function_exists('\datadog\appsec\track_user_login_failure_event_automated')) { + return; + } + + // Get the adapter from the hook arguments + $adapter = isset($hook->args[0]) ? $hook->args[0] : null; + $userLogin = null; + + // Try to get the login from the adapter if it has a getIdentity method + if ($adapter && method_exists($adapter, 'getIdentity')) { + $userLogin = $adapter->getIdentity(); + } + + $userExists = ($code === \Laminas\Authentication\Result::FAILURE_CREDENTIAL_INVALID); + + \datadog\appsec\track_user_login_failure_event_automated( + $userLogin, + $userLogin, + $userExists, + [] + ); + } + ); + + // Track authenticated user on each request + hook_method( + 'Laminas\Authentication\AuthenticationService', + 'hasIdentity', + null, + static function ($This, $scope, $args, $hasIdentity) { + if (!$hasIdentity || !function_exists('\datadog\appsec\track_authenticated_user_event_automated')) { + return; + } + + $identity = $This->getIdentity(); + if (!$identity) { + return; + } + + $userId = self::getUserId($identity); + \datadog\appsec\track_authenticated_user_event_automated($userId); + } + ); return Integration::LOADED; } + /** + * Extract user ID from identity object + * + * @param mixed $identity + * @return string + */ + private static function getUserId($identity) + { + if (is_string($identity) || is_int($identity)) { + return (string)$identity; + } + + if (is_array($identity)) { + if (isset($identity['id'])) { + return (string)$identity['id']; + } + if (isset($identity['user_id'])) { + return (string)$identity['user_id']; + } + if (isset($identity['username'])) { + return $identity['username']; + } + if (isset($identity['email'])) { + return $identity['email']; + } + } + + if (is_object($identity)) { + // Try common property names + if (isset($identity->id)) { + return (string)$identity->id; + } + if (isset($identity->user_id)) { + return (string)$identity->user_id; + } + if (isset($identity->userId)) { + return (string)$identity->userId; + } + + // Try common getter methods + if (method_exists($identity, 'getId')) { + return (string)$identity->getId(); + } + if (method_exists($identity, 'getUserId')) { + return (string)$identity->getUserId(); + } + if (method_exists($identity, 'getUsername')) { + return $identity->getUsername(); + } + if (method_exists($identity, 'getEmail')) { + return $identity->getEmail(); + } + + // ArrayAccess support + if ($identity instanceof \ArrayAccess) { + if (isset($identity['id'])) { + return (string)$identity['id']; + } + if (isset($identity['user_id'])) { + return (string)$identity['user_id']; + } + if (isset($identity['username'])) { + return $identity['username']; + } + if (isset($identity['email'])) { + return $identity['email']; + } + } + } + + return ''; + } + + /** + * Extract user login (username/email) from identity object + * + * @param mixed $identity + * @return string|null + */ + private static function getUserLogin($identity) + { + if (is_string($identity)) { + return $identity; + } + + if (is_array($identity)) { + if (isset($identity['email'])) { + return $identity['email']; + } + if (isset($identity['username'])) { + return $identity['username']; + } + } + + if (is_object($identity)) { + // Try properties + if (isset($identity->email)) { + return $identity->email; + } + if (isset($identity->username)) { + return $identity->username; + } + + // Try getters + if (method_exists($identity, 'getEmail')) { + return $identity->getEmail(); + } + if (method_exists($identity, 'getUsername')) { + return $identity->getUsername(); + } + + // ArrayAccess support + if ($identity instanceof \ArrayAccess) { + if (isset($identity['email'])) { + return $identity['email']; + } + if (isset($identity['username'])) { + return $identity['username']; + } + } + } + + return null; + } + + /** + * Extract user metadata from identity object + * + * @param mixed $identity + * @return array + */ + private static function getUserMetadata($identity) + { + $metadata = []; + + if (is_array($identity)) { + if (isset($identity['name'])) { + $metadata['name'] = $identity['name']; + } + if (isset($identity['email'])) { + $metadata['email'] = $identity['email']; + } + return $metadata; + } + + if (is_object($identity)) { + // Try properties + if (isset($identity->name)) { + $metadata['name'] = $identity->name; + } + if (isset($identity->email)) { + $metadata['email'] = $identity->email; + } + + // Try getters + if (method_exists($identity, 'getName')) { + $metadata['name'] = $identity->getName(); + } + if (method_exists($identity, 'getEmail') && !isset($metadata['email'])) { + $metadata['email'] = $identity->getEmail(); + } + + // ArrayAccess support + if ($identity instanceof \ArrayAccess) { + if (isset($identity['name']) && !isset($metadata['name'])) { + $metadata['name'] = $identity['name']; + } + if (isset($identity['email']) && !isset($metadata['email'])) { + $metadata['email'] = $identity['email']; + } + } + } + + return $metadata; + } + public static function debugBacktraceToString(array $backtrace) { // (methods) # (line): ()\n diff --git a/tests/Frameworks/Laminas/Mvc/Latest/composer.json b/tests/Frameworks/Laminas/Mvc/Latest/composer.json index 84dfde3942e..66b3ecd087a 100644 --- a/tests/Frameworks/Laminas/Mvc/Latest/composer.json +++ b/tests/Frameworks/Laminas/Mvc/Latest/composer.json @@ -14,12 +14,16 @@ "laminas/laminas-component-installer": "^3.2", "laminas/laminas-development-mode": "^3.10", "laminas/laminas-skeleton-installer": "^1.2", - "laminas/laminas-mvc": "3.8.0" + "laminas/laminas-mvc": "3.8.0", + "laminas/laminas-authentication": "^2.9", + "laminas/laminas-db": "^2.13", + "laminas/laminas-session": "^2.10" }, "autoload": { "psr-4": { "Application\\": "module/Application/src/" - } + }, + "files": ["../../../../Appsec/Mock.php"] }, "autoload-dev": { "psr-4": { diff --git a/tests/Frameworks/Laminas/Mvc/Latest/config/autoload/database.global.php b/tests/Frameworks/Laminas/Mvc/Latest/config/autoload/database.global.php new file mode 100644 index 00000000000..9076427a9f8 --- /dev/null +++ b/tests/Frameworks/Laminas/Mvc/Latest/config/autoload/database.global.php @@ -0,0 +1,25 @@ + [ + 'driver' => 'Pdo', + 'dsn' => 'mysql:dbname=test;host=mysql-integration', + 'username' => 'test', + 'password' => 'test', + ], + 'service_manager' => [ + 'factories' => [ + Adapter::class => function ($container) { + return new Adapter($container->get('config')['db']); + }, + AuthenticationService::class => function ($container) { + $storage = new SessionStorage(); + return new AuthenticationService($storage); + }, + ], + ], +]; diff --git a/tests/Frameworks/Laminas/Mvc/Latest/module/Application/config/module.config.php b/tests/Frameworks/Laminas/Mvc/Latest/module/Application/config/module.config.php index 86768e77068..7f1c1b9201e 100644 --- a/tests/Frameworks/Laminas/Mvc/Latest/module/Application/config/module.config.php +++ b/tests/Frameworks/Laminas/Mvc/Latest/module/Application/config/module.config.php @@ -5,6 +5,8 @@ namespace Application; use Application\Controller\CommonSpecsController; +use Application\Controller\LoginController; +use Application\Controller\LoginControllerFactory; use Laminas\Router\Http\Literal; use Laminas\Router\Http\Segment; use Laminas\ServiceManager\Factory\InvokableFactory; @@ -61,6 +63,36 @@ 'action' => 'error', ], ] + ], + 'login_auth' => [ + 'type' => Literal::class, + 'options' => [ + 'route' => '/login/auth', + 'defaults' => [ + 'controller' => Controller\LoginController::class, + 'action' => 'auth', + ], + ] + ], + 'login_signup' => [ + 'type' => Literal::class, + 'options' => [ + 'route' => '/login/signup', + 'defaults' => [ + 'controller' => Controller\LoginController::class, + 'action' => 'signup', + ], + ] + ], + 'behind_auth' => [ + 'type' => Literal::class, + 'options' => [ + 'route' => '/behind_auth', + 'defaults' => [ + 'controller' => Controller\LoginController::class, + 'action' => 'behindAuth', + ], + ] ] ], ], @@ -68,6 +100,7 @@ 'factories' => [ Controller\IndexController::class => InvokableFactory::class, Controller\CommonSpecsController::class => InvokableFactory::class, + Controller\LoginController::class => LoginControllerFactory::class, ], ], 'view_manager' => [ diff --git a/tests/Frameworks/Laminas/Mvc/Latest/module/Application/src/Controller/LoginController.php b/tests/Frameworks/Laminas/Mvc/Latest/module/Application/src/Controller/LoginController.php new file mode 100644 index 00000000000..c2b79cfb22c --- /dev/null +++ b/tests/Frameworks/Laminas/Mvc/Latest/module/Application/src/Controller/LoginController.php @@ -0,0 +1,126 @@ +dbAdapter = $dbAdapter; + $this->authService = $authService; + } + + public function authAction() + { + $email = $this->params()->fromQuery('email'); + $password = $this->params()->fromQuery('password', 'password'); + + if (!$email) { + $response = $this->getResponse(); + $response->setStatusCode(400); + $response->setContent('Email is required'); + return $response; + } + + // Set up authentication adapter + $authAdapter = new CredentialTreatmentAdapter( + $this->dbAdapter, + 'users', + 'email', + 'password', + 'MD5(?)' + ); + + $authAdapter->setIdentity($email); + $authAdapter->setCredential($password); + + // Perform authentication + $result = $this->authService->authenticate($authAdapter); + + if ($result->isValid()) { + // Get the user data from the database + $userData = $authAdapter->getResultRowObject(null, 'password'); + + // Store user identity + $this->authService->getStorage()->write($userData); + + $response = $this->getResponse(); + $response->setStatusCode(200); + $response->setContent('Login successful'); + return $response; + } else { + $response = $this->getResponse(); + $response->setStatusCode(403); + $response->setContent('Invalid credentials'); + return $response; + } + } + + public function signupAction() + { + $email = $this->params()->fromQuery('email'); + $name = $this->params()->fromQuery('name'); + $password = $this->params()->fromQuery('password', 'password'); + + if (!$email || !$name || !$password) { + $response = $this->getResponse(); + $response->setStatusCode(400); + $response->setContent('Email, name, and password are required'); + return $response; + } + + // Insert new user + $connection = $this->dbAdapter->getDriver()->getConnection(); + $statement = $this->dbAdapter->query( + "INSERT INTO users (name, email, password) VALUES (?, ?, MD5(?))" + ); + $statement->execute([$name, $email, $password]); + + $userId = $this->dbAdapter->getDriver()->getLastGeneratedValue(); + + // Track signup event manually since we're not using an event system + if (function_exists('\datadog\appsec\track_user_signup_event_automated')) { + \datadog\appsec\track_user_signup_event_automated($email, (string)$userId, []); + } + + // Auto-login after signup + $userData = (object)[ + 'id' => $userId, + 'name' => $name, + 'email' => $email + ]; + $this->authService->getStorage()->write($userData); + + $response = $this->getResponse(); + $response->setStatusCode(200); + $response->setContent('Signup successful'); + return $response; + } + + public function behindAuthAction() + { + // Check if user is authenticated + if (!$this->authService->hasIdentity()) { + $response = $this->getResponse(); + $response->setStatusCode(401); + $response->setContent('Unauthorized'); + return $response; + } + + $response = $this->getResponse(); + $response->setStatusCode(200); + $response->setContent('page behind auth'); + return $response; + } +} diff --git a/tests/Frameworks/Laminas/Mvc/Latest/module/Application/src/Controller/LoginControllerFactory.php b/tests/Frameworks/Laminas/Mvc/Latest/module/Application/src/Controller/LoginControllerFactory.php new file mode 100644 index 00000000000..84f70258d8e --- /dev/null +++ b/tests/Frameworks/Laminas/Mvc/Latest/module/Application/src/Controller/LoginControllerFactory.php @@ -0,0 +1,21 @@ +get(Adapter::class); + $authService = $container->get(AuthenticationService::class); + + return new LoginController($dbAdapter, $authService); + } +} diff --git a/tests/Frameworks/Laminas/Mvc/Version_3_3/composer.json b/tests/Frameworks/Laminas/Mvc/Version_3_3/composer.json index f03478c8ec0..f292f7349ec 100644 --- a/tests/Frameworks/Laminas/Mvc/Version_3_3/composer.json +++ b/tests/Frameworks/Laminas/Mvc/Version_3_3/composer.json @@ -14,12 +14,16 @@ "laminas/laminas-component-installer": "^2.4", "laminas/laminas-development-mode": "^3.2", "laminas/laminas-skeleton-installer": "^0.6", - "laminas/laminas-mvc": "3.3.*" + "laminas/laminas-mvc": "3.3.*", + "laminas/laminas-authentication": "^2.9", + "laminas/laminas-db": "^2.13", + "laminas/laminas-session": "^2.10" }, "autoload": { "psr-4": { "Application\\": "module/Application/src/" - } + }, + "files": ["../../../../Appsec/Mock.php"] }, "autoload-dev": { "psr-4": { diff --git a/tests/Frameworks/Laminas/Mvc/Version_3_3/config/autoload/database.global.php b/tests/Frameworks/Laminas/Mvc/Version_3_3/config/autoload/database.global.php new file mode 100644 index 00000000000..9076427a9f8 --- /dev/null +++ b/tests/Frameworks/Laminas/Mvc/Version_3_3/config/autoload/database.global.php @@ -0,0 +1,25 @@ + [ + 'driver' => 'Pdo', + 'dsn' => 'mysql:dbname=test;host=mysql-integration', + 'username' => 'test', + 'password' => 'test', + ], + 'service_manager' => [ + 'factories' => [ + Adapter::class => function ($container) { + return new Adapter($container->get('config')['db']); + }, + AuthenticationService::class => function ($container) { + $storage = new SessionStorage(); + return new AuthenticationService($storage); + }, + ], + ], +]; diff --git a/tests/Frameworks/Laminas/Mvc/Version_3_3/config/modules.config.php b/tests/Frameworks/Laminas/Mvc/Version_3_3/config/modules.config.php index 1f94551ad32..6af7e9dac96 100644 --- a/tests/Frameworks/Laminas/Mvc/Version_3_3/config/modules.config.php +++ b/tests/Frameworks/Laminas/Mvc/Version_3_3/config/modules.config.php @@ -6,6 +6,8 @@ * This should be an array of module namespaces used in the application. */ return [ + 'Laminas\Session', + 'Laminas\Db', 'Laminas\Router', 'Laminas\Validator', 'Application', diff --git a/tests/Frameworks/Laminas/Mvc/Version_3_3/module/Application/config/module.config.php b/tests/Frameworks/Laminas/Mvc/Version_3_3/module/Application/config/module.config.php index 86768e77068..7f1c1b9201e 100644 --- a/tests/Frameworks/Laminas/Mvc/Version_3_3/module/Application/config/module.config.php +++ b/tests/Frameworks/Laminas/Mvc/Version_3_3/module/Application/config/module.config.php @@ -5,6 +5,8 @@ namespace Application; use Application\Controller\CommonSpecsController; +use Application\Controller\LoginController; +use Application\Controller\LoginControllerFactory; use Laminas\Router\Http\Literal; use Laminas\Router\Http\Segment; use Laminas\ServiceManager\Factory\InvokableFactory; @@ -61,6 +63,36 @@ 'action' => 'error', ], ] + ], + 'login_auth' => [ + 'type' => Literal::class, + 'options' => [ + 'route' => '/login/auth', + 'defaults' => [ + 'controller' => Controller\LoginController::class, + 'action' => 'auth', + ], + ] + ], + 'login_signup' => [ + 'type' => Literal::class, + 'options' => [ + 'route' => '/login/signup', + 'defaults' => [ + 'controller' => Controller\LoginController::class, + 'action' => 'signup', + ], + ] + ], + 'behind_auth' => [ + 'type' => Literal::class, + 'options' => [ + 'route' => '/behind_auth', + 'defaults' => [ + 'controller' => Controller\LoginController::class, + 'action' => 'behindAuth', + ], + ] ] ], ], @@ -68,6 +100,7 @@ 'factories' => [ Controller\IndexController::class => InvokableFactory::class, Controller\CommonSpecsController::class => InvokableFactory::class, + Controller\LoginController::class => LoginControllerFactory::class, ], ], 'view_manager' => [ diff --git a/tests/Frameworks/Laminas/Mvc/Version_3_3/module/Application/src/Controller/LoginController.php b/tests/Frameworks/Laminas/Mvc/Version_3_3/module/Application/src/Controller/LoginController.php new file mode 100644 index 00000000000..c2b79cfb22c --- /dev/null +++ b/tests/Frameworks/Laminas/Mvc/Version_3_3/module/Application/src/Controller/LoginController.php @@ -0,0 +1,126 @@ +dbAdapter = $dbAdapter; + $this->authService = $authService; + } + + public function authAction() + { + $email = $this->params()->fromQuery('email'); + $password = $this->params()->fromQuery('password', 'password'); + + if (!$email) { + $response = $this->getResponse(); + $response->setStatusCode(400); + $response->setContent('Email is required'); + return $response; + } + + // Set up authentication adapter + $authAdapter = new CredentialTreatmentAdapter( + $this->dbAdapter, + 'users', + 'email', + 'password', + 'MD5(?)' + ); + + $authAdapter->setIdentity($email); + $authAdapter->setCredential($password); + + // Perform authentication + $result = $this->authService->authenticate($authAdapter); + + if ($result->isValid()) { + // Get the user data from the database + $userData = $authAdapter->getResultRowObject(null, 'password'); + + // Store user identity + $this->authService->getStorage()->write($userData); + + $response = $this->getResponse(); + $response->setStatusCode(200); + $response->setContent('Login successful'); + return $response; + } else { + $response = $this->getResponse(); + $response->setStatusCode(403); + $response->setContent('Invalid credentials'); + return $response; + } + } + + public function signupAction() + { + $email = $this->params()->fromQuery('email'); + $name = $this->params()->fromQuery('name'); + $password = $this->params()->fromQuery('password', 'password'); + + if (!$email || !$name || !$password) { + $response = $this->getResponse(); + $response->setStatusCode(400); + $response->setContent('Email, name, and password are required'); + return $response; + } + + // Insert new user + $connection = $this->dbAdapter->getDriver()->getConnection(); + $statement = $this->dbAdapter->query( + "INSERT INTO users (name, email, password) VALUES (?, ?, MD5(?))" + ); + $statement->execute([$name, $email, $password]); + + $userId = $this->dbAdapter->getDriver()->getLastGeneratedValue(); + + // Track signup event manually since we're not using an event system + if (function_exists('\datadog\appsec\track_user_signup_event_automated')) { + \datadog\appsec\track_user_signup_event_automated($email, (string)$userId, []); + } + + // Auto-login after signup + $userData = (object)[ + 'id' => $userId, + 'name' => $name, + 'email' => $email + ]; + $this->authService->getStorage()->write($userData); + + $response = $this->getResponse(); + $response->setStatusCode(200); + $response->setContent('Signup successful'); + return $response; + } + + public function behindAuthAction() + { + // Check if user is authenticated + if (!$this->authService->hasIdentity()) { + $response = $this->getResponse(); + $response->setStatusCode(401); + $response->setContent('Unauthorized'); + return $response; + } + + $response = $this->getResponse(); + $response->setStatusCode(200); + $response->setContent('page behind auth'); + return $response; + } +} diff --git a/tests/Frameworks/Laminas/Mvc/Version_3_3/module/Application/src/Controller/LoginControllerFactory.php b/tests/Frameworks/Laminas/Mvc/Version_3_3/module/Application/src/Controller/LoginControllerFactory.php new file mode 100644 index 00000000000..84f70258d8e --- /dev/null +++ b/tests/Frameworks/Laminas/Mvc/Version_3_3/module/Application/src/Controller/LoginControllerFactory.php @@ -0,0 +1,21 @@ +get(Adapter::class); + $authService = $container->get(AuthenticationService::class); + + return new LoginController($dbAdapter, $authService); + } +} diff --git a/tests/Integrations/Laminas/Mvc/AutomatedLoginEventsTestSuite.php b/tests/Integrations/Laminas/Mvc/AutomatedLoginEventsTestSuite.php new file mode 100644 index 00000000000..5944c706905 --- /dev/null +++ b/tests/Integrations/Laminas/Mvc/AutomatedLoginEventsTestSuite.php @@ -0,0 +1,126 @@ +connection()->exec("DELETE from users where email LIKE 'test-user%'"); + AppsecStatus::getInstance()->setDefaults(); + } + + protected function login($email) + { + $this->call( + GetSpec::create('Login success event', '/login/auth?email='.$email) + ); + } + + protected function createUser($id, $name, $email) + { + //Password is password (MD5 hash) + $password = md5('password'); + $this->connection()->exec("insert into users (id, name, email, password) VALUES (".$id.", '".$name."', '".$email."', '".$password."')"); + } + + public function testUserLoginSuccessEvent() + { + $id = 1234; + $name = 'someName'; + $email = 'test-user@email.com'; + $this->createUser($id, $name, $email); + + $this->login($email); + + $events = AppsecStatus::getInstance()->getEvents(['track_user_login_success_event_automated']); + $this->assertEquals(1, count($events)); + $this->assertEquals($id, $events[0]['userId']); + $this->assertEquals($email, $events[0]['userLogin']); + $this->assertEquals($name, $events[0]['metadata']['name']); + $this->assertEquals($email, $events[0]['metadata']['email']); + } + + public function testUserLoginFailureEvent() + { + $email = 'test-user-non-existing@email.com'; + + $this->login($email); + + $events = AppsecStatus::getInstance()->getEvents(['track_user_login_failure_event_automated']); + $this->assertEquals(1, count($events)); + $this->assertEquals($email, $events[0]['userLogin']); + } + + public function testUserSignUp() + { + $email = 'test-user-new@email.com'; + $name = 'somename'; + $password = 'somepassword'; + + $this->call( + GetSpec::create('Signup', sprintf('/login/signup?email=%s&name=%s&password=%s', $email, $name, $password)) + ); + + $users = $this->connection()->query("SELECT * FROM users where email='".$email."'")->fetchAll(); + + $this->assertEquals(1, count($users)); + + $signUpEvent = AppsecStatus::getInstance()->getEvents(['track_user_signup_event_automated']); + + $this->assertEquals($users[0]['id'], $signUpEvent[0]['userId']); + $this->assertEquals($users[0]['email'], $signUpEvent[0]['userLogin']); + } + + public function testLoggedInCalls() + { + $this->enableSession(); + $id = 1234; + $name = 'someName'; + $email = 'test-user@email.com'; + $this->createUser($id, $name, $email); + + //First log in + $this->login($email); + + //Now we are logged in lets do another call + AppsecStatus::getInstance()->setDefaults(); //Remove all events + $this->call(GetSpec::create('Behind auth', '/behind_auth')); + + $loginEvents = AppsecStatus::getInstance()->getEvents([ + 'track_user_login_success_event_automated', + 'track_user_login_failure_event_automated', + 'track_user_signup_event_automated' + ]); + + $authenticatedEvents = AppsecStatus::getInstance()->getEvents([ + 'track_authenticated_user_event_automated' + ]); + + $this->assertEquals(0, count($loginEvents)); // Auth does not generate appsec events + $this->assertEquals(1, count($authenticatedEvents)); + $this->assertEquals($id, $authenticatedEvents[0]['userId']); + $this->disableSession(); + } +} diff --git a/tests/Integrations/Laminas/Mvc/Latest/AutomatedLoginEventsTest.php b/tests/Integrations/Laminas/Mvc/Latest/AutomatedLoginEventsTest.php new file mode 100644 index 00000000000..e6dd6931623 --- /dev/null +++ b/tests/Integrations/Laminas/Mvc/Latest/AutomatedLoginEventsTest.php @@ -0,0 +1,18 @@ + Date: Wed, 18 Mar 2026 17:37:27 +0100 Subject: [PATCH 2/6] Add path_params to laminas --- .../Laminas/LaminasIntegration.php | 10 +++++ .../Application/config/module.config.php | 14 ++++++ .../src/Controller/CommonSpecsController.php | 7 +++ .../Application/config/module.config.php | 14 ++++++ .../src/Controller/CommonSpecsController.php | 7 +++ .../Laminas/Mvc/Latest/PathParamsTest.php | 16 +++++++ .../Laminas/Mvc/PathParamsTestSuite.php | 44 +++++++++++++++++++ .../Laminas/Mvc/V3_3/PathParamsTest.php | 16 +++++++ 8 files changed, 128 insertions(+) create mode 100644 tests/Integrations/Laminas/Mvc/Latest/PathParamsTest.php create mode 100644 tests/Integrations/Laminas/Mvc/PathParamsTestSuite.php create mode 100644 tests/Integrations/Laminas/Mvc/V3_3/PathParamsTest.php diff --git a/src/DDTrace/Integrations/Laminas/LaminasIntegration.php b/src/DDTrace/Integrations/Laminas/LaminasIntegration.php index e730dbc4a6e..81fc0055eff 100644 --- a/src/DDTrace/Integrations/Laminas/LaminasIntegration.php +++ b/src/DDTrace/Integrations/Laminas/LaminasIntegration.php @@ -274,6 +274,16 @@ static function (SpanData $span) use ($controller, $action) { } $rootSpan->meta['laminas.route.name'] = $routeName; $rootSpan->meta['laminas.route.action'] = "$controller@$action"; + + // Push path params to appsec + if (function_exists('\datadog\appsec\push_addresses')) { + $params = $routeMatch->getParams(); + // Filter out the framework-specific params (controller, action) + $pathParams = array_diff_key($params, array_flip(['controller', 'action'])); + if (count($pathParams) > 0) { + \datadog\appsec\push_addresses(["server.request.path_params" => $pathParams]); + } + } } ); diff --git a/tests/Frameworks/Laminas/Mvc/Latest/module/Application/config/module.config.php b/tests/Frameworks/Laminas/Mvc/Latest/module/Application/config/module.config.php index 7f1c1b9201e..a3a494f6fa4 100644 --- a/tests/Frameworks/Laminas/Mvc/Latest/module/Application/config/module.config.php +++ b/tests/Frameworks/Laminas/Mvc/Latest/module/Application/config/module.config.php @@ -93,6 +93,20 @@ 'action' => 'behindAuth', ], ] + ], + 'dynamic_route' => [ + 'type' => Segment::class, + 'options' => [ + 'route' => '/dynamic_route[/:param01[/static[/:param02]]]', + 'constraints' => [ + 'param01' => '[a-zA-Z0-9_-]+', + 'param02' => '[a-zA-Z0-9_-]+', + ], + 'defaults' => [ + 'controller' => Controller\CommonSpecsController::class, + 'action' => 'dynamicRoute', + ], + ] ] ], ], diff --git a/tests/Frameworks/Laminas/Mvc/Latest/module/Application/src/Controller/CommonSpecsController.php b/tests/Frameworks/Laminas/Mvc/Latest/module/Application/src/Controller/CommonSpecsController.php index 8b84228fe8d..ef5daaaf47e 100644 --- a/tests/Frameworks/Laminas/Mvc/Latest/module/Application/src/Controller/CommonSpecsController.php +++ b/tests/Frameworks/Laminas/Mvc/Latest/module/Application/src/Controller/CommonSpecsController.php @@ -32,4 +32,11 @@ public function errorAction() { throw new \Exception('Controller error'); } + + public function dynamicRouteAction() + { + $response = new Response(); + $response->setContent('dynamicRoute'); + return $response; + } } diff --git a/tests/Frameworks/Laminas/Mvc/Version_3_3/module/Application/config/module.config.php b/tests/Frameworks/Laminas/Mvc/Version_3_3/module/Application/config/module.config.php index 7f1c1b9201e..a3a494f6fa4 100644 --- a/tests/Frameworks/Laminas/Mvc/Version_3_3/module/Application/config/module.config.php +++ b/tests/Frameworks/Laminas/Mvc/Version_3_3/module/Application/config/module.config.php @@ -93,6 +93,20 @@ 'action' => 'behindAuth', ], ] + ], + 'dynamic_route' => [ + 'type' => Segment::class, + 'options' => [ + 'route' => '/dynamic_route[/:param01[/static[/:param02]]]', + 'constraints' => [ + 'param01' => '[a-zA-Z0-9_-]+', + 'param02' => '[a-zA-Z0-9_-]+', + ], + 'defaults' => [ + 'controller' => Controller\CommonSpecsController::class, + 'action' => 'dynamicRoute', + ], + ] ] ], ], diff --git a/tests/Frameworks/Laminas/Mvc/Version_3_3/module/Application/src/Controller/CommonSpecsController.php b/tests/Frameworks/Laminas/Mvc/Version_3_3/module/Application/src/Controller/CommonSpecsController.php index 8b84228fe8d..ef5daaaf47e 100644 --- a/tests/Frameworks/Laminas/Mvc/Version_3_3/module/Application/src/Controller/CommonSpecsController.php +++ b/tests/Frameworks/Laminas/Mvc/Version_3_3/module/Application/src/Controller/CommonSpecsController.php @@ -32,4 +32,11 @@ public function errorAction() { throw new \Exception('Controller error'); } + + public function dynamicRouteAction() + { + $response = new Response(); + $response->setContent('dynamicRoute'); + return $response; + } } diff --git a/tests/Integrations/Laminas/Mvc/Latest/PathParamsTest.php b/tests/Integrations/Laminas/Mvc/Latest/PathParamsTest.php new file mode 100644 index 00000000000..0b3db9951cb --- /dev/null +++ b/tests/Integrations/Laminas/Mvc/Latest/PathParamsTest.php @@ -0,0 +1,16 @@ +call( + GetSpec::create('Call to dynamic route', "/dynamic_route/$param01/static/$param02") + ); + $events = AppsecStatus::getInstance()->getEvents(['push_addresses'], ['server.request.path_params']); + $this->assertEquals(1, count($events)); + $this->assertEquals($param01, $events[0][0]["server.request.path_params"]['param01']); + $this->assertEquals($param02, $events[0][0]["server.request.path_params"]['param02']); + } + + public function testDynamicRouteWithOptionalParametersNotGiven() + { + $param01 = 'first_param'; + $this->call( + GetSpec::create('Call to dynamic route', "/dynamic_route/$param01/static") + ); + $events = AppsecStatus::getInstance()->getEvents(['push_addresses'], ['server.request.path_params']); + $this->assertEquals(1, count($events)); + $this->assertCount(1, $events[0][0]["server.request.path_params"]); + $this->assertEquals($param01, $events[0][0]["server.request.path_params"]['param01']); + } + + public function testStaticRouteDoesNotGenerateEvent() + { + $this->call( + GetSpec::create('Call to static route', "/simple") + ); + $events = AppsecStatus::getInstance()->getEvents(['push_addresses'], ['server.request.path_params']); + $this->assertEquals(0, count($events)); + } +} diff --git a/tests/Integrations/Laminas/Mvc/V3_3/PathParamsTest.php b/tests/Integrations/Laminas/Mvc/V3_3/PathParamsTest.php new file mode 100644 index 00000000000..e570caf9bb0 --- /dev/null +++ b/tests/Integrations/Laminas/Mvc/V3_3/PathParamsTest.php @@ -0,0 +1,16 @@ + Date: Mon, 23 Mar 2026 13:18:42 +0100 Subject: [PATCH 3/6] Fix laminas tests --- .../Application/src/Controller/LoginControllerFactory.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Frameworks/Laminas/Mvc/Version_3_3/module/Application/src/Controller/LoginControllerFactory.php b/tests/Frameworks/Laminas/Mvc/Version_3_3/module/Application/src/Controller/LoginControllerFactory.php index 84f70258d8e..15a6026a388 100644 --- a/tests/Frameworks/Laminas/Mvc/Version_3_3/module/Application/src/Controller/LoginControllerFactory.php +++ b/tests/Frameworks/Laminas/Mvc/Version_3_3/module/Application/src/Controller/LoginControllerFactory.php @@ -6,8 +6,8 @@ use Laminas\Authentication\AuthenticationService; use Laminas\Db\Adapter\Adapter; +use Interop\Container\ContainerInterface; use Laminas\ServiceManager\Factory\FactoryInterface; -use Psr\Container\ContainerInterface; class LoginControllerFactory implements FactoryInterface { From 31222427b2054ebd2082ef7ae3802cbfc683f0d0 Mon Sep 17 00:00:00 2001 From: Alejandro Estringana Ruiz Date: Mon, 23 Mar 2026 15:22:41 +0100 Subject: [PATCH 4/6] Add http.route --- .../Laminas/LaminasIntegration.php | 142 ++++++++++++++++++ ..._test.test_scenario_get_return_string.json | 1 + ...test.test_scenario_get_with_exception.json | 1 + ...rios_test.test_scenario_get_with_view.json | 1 + ..._test.test_scenario_get_return_string.json | 1 + ...test.test_scenario_get_with_exception.json | 1 + ...rios_test.test_scenario_get_with_view.json | 1 + 7 files changed, 148 insertions(+) diff --git a/src/DDTrace/Integrations/Laminas/LaminasIntegration.php b/src/DDTrace/Integrations/Laminas/LaminasIntegration.php index 81fc0055eff..6ac827bcf72 100644 --- a/src/DDTrace/Integrations/Laminas/LaminasIntegration.php +++ b/src/DDTrace/Integrations/Laminas/LaminasIntegration.php @@ -275,6 +275,14 @@ static function (SpanData $span) use ($controller, $action) { $rootSpan->meta['laminas.route.name'] = $routeName; $rootSpan->meta['laminas.route.action'] = "$controller@$action"; + // http.route: developer-defined template (e.g. /dynamic_route[/:param01[/static[/:param02]]]) + // Use LaminasIntegration:: not self:: — non-static hook closure is bound to the route object; + // ddtrace resolves self to the instrumented class (e.g. TreeRouteStack). + $routeTemplate = LaminasIntegration::httpRouteTemplateFromMatchedRoute($this, $routeMatch); + if ($routeTemplate !== null) { + $rootSpan->meta[Tag::HTTP_ROUTE] = $routeTemplate; + } + // Push path params to appsec if (function_exists('\datadog\appsec\push_addresses')) { $params = $routeMatch->getParams(); @@ -874,6 +882,140 @@ private static function getUserMetadata($identity) return $metadata; } + /** + * Developer-defined route path for {@see Tag::HTTP_ROUTE}. Laminas requests do not carry this; + * MVC resolves on {@see \Laminas\Router\Http\TreeRouteStack} so {@code $this} is often the stack, + * not {@see \Laminas\Router\Http\Segment}. + * + * Must be public (not private): the {@code match()} hook closure runs with ddtrace caller scope + * {@see \Laminas\Router\Http\TreeRouteStack}, so private methods are not callable from there. + * + * @internal + * @param object $matchedRoute $this from {@see \Laminas\Router\RouteInterface::match()} + * @param RouteMatch|null $routeMatch return value of {@code match()} (needed for the route stack) + * @return string|null + */ + public static function httpRouteTemplateFromMatchedRoute($matchedRoute, $routeMatch = null) + { + if ($matchedRoute instanceof \Laminas\Router\Http\Literal) { + $rp = new \ReflectionProperty($matchedRoute, 'route'); + $rp->setAccessible(true); + + return (string) $rp->getValue($matchedRoute); + } + + if ($matchedRoute instanceof \Laminas\Router\Http\Segment) { + $rp = new \ReflectionProperty($matchedRoute, 'parts'); + $rp->setAccessible(true); + $parts = $rp->getValue($matchedRoute); + + return \is_array($parts) ? self::laminasSegmentPartsToRouteTemplate($parts) : null; + } + + if ( + $matchedRoute instanceof \Laminas\Router\Http\TreeRouteStack + && $routeMatch instanceof RouteMatch + ) { + $matchedName = $routeMatch->getMatchedRouteName(); + if ($matchedName === null || $matchedName === '') { + return null; + } + + return self::httpRouteTemplateFromNamedRouteStack($matchedRoute, (string) $matchedName); + } + + return null; + } + + /** + * @internal + * @param \Laminas\Router\Http\TreeRouteStack $stack + */ + public static function httpRouteTemplateFromNamedRouteStack($stack, string $matchedName): ?string + { + $segments = \explode('/', $matchedName, 2); + $route = $stack->getRoute($segments[0]); + if ($route === null) { + return null; + } + + $hasChild = isset($segments[1]); + + if ($route instanceof \Laminas\Router\Http\Part) { + $base = self::partRouteBaseTemplate($route); + $base = $base ?? ''; + if (!$hasChild) { + return $base !== '' ? $base : null; + } + $child = self::httpRouteTemplateFromNamedRouteStack($route, $segments[1]); + if ($child === null) { + return $base !== '' ? $base : null; + } + + return $base . $child; + } + + if ($route instanceof \Laminas\Router\Http\TreeRouteStack && $hasChild) { + return self::httpRouteTemplateFromNamedRouteStack($route, $segments[1]); + } + + if ($hasChild) { + return self::httpRouteTemplateFromMatchedRoute($route, null); + } + + return self::httpRouteTemplateFromMatchedRoute($route, null); + } + + /** + * Path template for the non-child segment of a {@see \Laminas\Router\Http\Part} route. + * + * @internal + */ + public static function partRouteBaseTemplate(\Laminas\Router\Http\Part $part): ?string + { + $rp = new \ReflectionProperty($part, 'route'); + $rp->setAccessible(true); + $baseRoute = $rp->getValue($part); + + return self::httpRouteTemplateFromMatchedRoute($baseRoute, null); + } + + /** + * Rebuilds the route string from {@see \Laminas\Router\Http\Segment} parsed parts (inverse of parseRouteDefinition). + * + * @internal + * @param array $parts + */ + public static function laminasSegmentPartsToRouteTemplate(array $parts): string + { + $buf = ''; + foreach ($parts as $part) { + if (!\is_array($part) || !isset($part[0])) { + continue; + } + switch ($part[0]) { + case 'literal': + $buf .= $part[1] ?? ''; + break; + case 'parameter': + $buf .= ':'; + $buf .= $part[1] ?? ''; + if (isset($part[2]) && $part[2] !== null && $part[2] !== '') { + $buf .= '{' . $part[2] . '}'; + } + break; + case 'optional': + $buf .= '[' . self::laminasSegmentPartsToRouteTemplate($part[1] ?? []) . ']'; + break; + case 'translated-literal': + $buf .= '{' . ($part[1] ?? '') . '}'; + break; + } + } + + return $buf; + } + public static function debugBacktraceToString(array $backtrace) { // (methods) # (line): ()\n diff --git a/tests/snapshots/tests.integrations.laminas.mvc.latest.common_scenarios_test.test_scenario_get_return_string.json b/tests/snapshots/tests.integrations.laminas.mvc.latest.common_scenarios_test.test_scenario_get_return_string.json index 8022cbe28f0..1bdb15085fc 100644 --- a/tests/snapshots/tests.integrations.laminas.mvc.latest.common_scenarios_test.test_scenario_get_return_string.json +++ b/tests/snapshots/tests.integrations.laminas.mvc.latest.common_scenarios_test.test_scenario_get_return_string.json @@ -11,6 +11,7 @@ "_dd.p.dm": "-0", "component": "laminas", "http.method": "GET", + "http.route": "/simple[/:key][/:pwd]", "laminas.route.name": "simple", "http.status_code": "200", "http.url": "http://localhost/simple?key=value&", diff --git a/tests/snapshots/tests.integrations.laminas.mvc.latest.common_scenarios_test.test_scenario_get_with_exception.json b/tests/snapshots/tests.integrations.laminas.mvc.latest.common_scenarios_test.test_scenario_get_with_exception.json index cca8a2d422d..378a177110c 100644 --- a/tests/snapshots/tests.integrations.laminas.mvc.latest.common_scenarios_test.test_scenario_get_with_exception.json +++ b/tests/snapshots/tests.integrations.laminas.mvc.latest.common_scenarios_test.test_scenario_get_with_exception.json @@ -15,6 +15,7 @@ "error.stack": "#0 {path}/tests/Frameworks/Laminas/Mvc/Latest/vendor/laminas/laminas-mvc/src/Controller/AbstractActionController.php(72): Application\\Controller\\CommonSpecsController->errorAction()\n#1 {path}/tests/Frameworks/Laminas/Mvc/Latest/vendor/laminas/laminas-eventmanager/src/EventManager.php(320): Laminas\\Mvc\\Controller\\AbstractActionController->onDispatch(Object(Laminas\\Mvc\\MvcEvent))\n#2 {path}/tests/Frameworks/Laminas/Mvc/Latest/vendor/laminas/laminas-eventmanager/src/EventManager.php(178): Laminas\\EventManager\\EventManager->triggerListeners(Object(Laminas\\Mvc\\MvcEvent), Object(Closure))\n#3 {path}/tests/Frameworks/Laminas/Mvc/Latest/vendor/laminas/laminas-mvc/src/Controller/AbstractController.php(105): Laminas\\EventManager\\EventManager->triggerEventUntil(Object(Closure), Object(Laminas\\Mvc\\MvcEvent))\n#4 {path}/tests/Frameworks/Laminas/Mvc/Latest/vendor/laminas/laminas-mvc/src/DispatchListener.php(117): Laminas\\Mvc\\Controller\\AbstractController->dispatch(Object(Laminas\\Http\\PhpEnvironment\\Request), Object(Laminas\\Http\\PhpEnvironment\\Response))\n#5 {path}/tests/Frameworks/Laminas/Mvc/Latest/vendor/laminas/laminas-eventmanager/src/EventManager.php(320): Laminas\\Mvc\\DispatchListener->onDispatch(Object(Laminas\\Mvc\\MvcEvent))\n#6 {path}/tests/Frameworks/Laminas/Mvc/Latest/vendor/laminas/laminas-eventmanager/src/EventManager.php(178): Laminas\\EventManager\\EventManager->triggerListeners(Object(Laminas\\Mvc\\MvcEvent), Object(Closure))\n#7 {path}/tests/Frameworks/Laminas/Mvc/Latest/vendor/laminas/laminas-mvc/src/Application.php(319): Laminas\\EventManager\\EventManager->triggerEventUntil(Object(Closure), Object(Laminas\\Mvc\\MvcEvent))\n#8 {path}/tests/Frameworks/Laminas/Mvc/Latest/public/index.php(37): Laminas\\Mvc\\Application->run()\n#9 {main}", "error.type": "Exception", "http.method": "GET", + "http.route": "/error[/:key][/:pwd]", "laminas.route.name": "error", "http.status_code": "500", "http.url": "http://localhost/error?key=value&", diff --git a/tests/snapshots/tests.integrations.laminas.mvc.latest.common_scenarios_test.test_scenario_get_with_view.json b/tests/snapshots/tests.integrations.laminas.mvc.latest.common_scenarios_test.test_scenario_get_with_view.json index 241060043b5..2d16a69aa4d 100644 --- a/tests/snapshots/tests.integrations.laminas.mvc.latest.common_scenarios_test.test_scenario_get_with_view.json +++ b/tests/snapshots/tests.integrations.laminas.mvc.latest.common_scenarios_test.test_scenario_get_with_view.json @@ -11,6 +11,7 @@ "_dd.p.dm": "-0", "component": "laminas", "http.method": "GET", + "http.route": "/simple_view[/:key][/:pwd]", "laminas.route.name": "simpleView", "http.status_code": "200", "http.url": "http://localhost/simple_view?key=value&", diff --git a/tests/snapshots/tests.integrations.laminas.mvc.v3_3.common_scenarios_test.test_scenario_get_return_string.json b/tests/snapshots/tests.integrations.laminas.mvc.v3_3.common_scenarios_test.test_scenario_get_return_string.json index fd62d76714b..1095152bcea 100644 --- a/tests/snapshots/tests.integrations.laminas.mvc.v3_3.common_scenarios_test.test_scenario_get_return_string.json +++ b/tests/snapshots/tests.integrations.laminas.mvc.v3_3.common_scenarios_test.test_scenario_get_return_string.json @@ -12,6 +12,7 @@ "_dd.p.tid": "6790b6e900000000", "component": "laminas", "http.method": "GET", + "http.route": "/simple[/:key][/:pwd]", "http.status_code": "200", "http.url": "http://localhost/simple?key=value&", "http.version": "1.1", diff --git a/tests/snapshots/tests.integrations.laminas.mvc.v3_3.common_scenarios_test.test_scenario_get_with_exception.json b/tests/snapshots/tests.integrations.laminas.mvc.v3_3.common_scenarios_test.test_scenario_get_with_exception.json index 788270c1451..b7e009b6638 100644 --- a/tests/snapshots/tests.integrations.laminas.mvc.v3_3.common_scenarios_test.test_scenario_get_with_exception.json +++ b/tests/snapshots/tests.integrations.laminas.mvc.v3_3.common_scenarios_test.test_scenario_get_with_exception.json @@ -16,6 +16,7 @@ "error.stack": "#0 {path}/tests/Frameworks/Laminas/Mvc/Version_3_3/vendor/laminas/laminas-mvc/src/Controller/AbstractActionController.php(71): Application\\Controller\\CommonSpecsController->errorAction()\n#1 {path}/tests/Frameworks/Laminas/Mvc/Version_3_3/vendor/laminas/laminas-eventmanager/src/EventManager.php(319): Laminas\\Mvc\\Controller\\AbstractActionController->onDispatch()\n#2 {path}/tests/Frameworks/Laminas/Mvc/Version_3_3/vendor/laminas/laminas-eventmanager/src/EventManager.php(179): Laminas\\EventManager\\EventManager->triggerListeners()\n#3 {path}/tests/Frameworks/Laminas/Mvc/Version_3_3/vendor/laminas/laminas-mvc/src/Controller/AbstractController.php(97): Laminas\\EventManager\\EventManager->triggerEventUntil()\n#4 {path}/tests/Frameworks/Laminas/Mvc/Version_3_3/vendor/laminas/laminas-mvc/src/DispatchListener.php(132): Laminas\\Mvc\\Controller\\AbstractController->dispatch()\n#5 {path}/tests/Frameworks/Laminas/Mvc/Version_3_3/vendor/laminas/laminas-eventmanager/src/EventManager.php(319): Laminas\\Mvc\\DispatchListener->onDispatch()\n#6 {path}/tests/Frameworks/Laminas/Mvc/Version_3_3/vendor/laminas/laminas-eventmanager/src/EventManager.php(179): Laminas\\EventManager\\EventManager->triggerListeners()\n#7 {path}/tests/Frameworks/Laminas/Mvc/Version_3_3/vendor/laminas/laminas-mvc/src/Application.php(325): Laminas\\EventManager\\EventManager->triggerEventUntil()\n#8 {path}/tests/Frameworks/Laminas/Mvc/Version_3_3/public/index.php(37): Laminas\\Mvc\\Application->run()\n#9 {main}", "error.type": "Exception", "http.method": "GET", + "http.route": "/error[/:key][/:pwd]", "http.status_code": "500", "http.url": "http://localhost/error?key=value&", "http.version": "1.1", diff --git a/tests/snapshots/tests.integrations.laminas.mvc.v3_3.common_scenarios_test.test_scenario_get_with_view.json b/tests/snapshots/tests.integrations.laminas.mvc.v3_3.common_scenarios_test.test_scenario_get_with_view.json index 2a6d6c8350b..901042e3de5 100644 --- a/tests/snapshots/tests.integrations.laminas.mvc.v3_3.common_scenarios_test.test_scenario_get_with_view.json +++ b/tests/snapshots/tests.integrations.laminas.mvc.v3_3.common_scenarios_test.test_scenario_get_with_view.json @@ -12,6 +12,7 @@ "_dd.p.tid": "6790b6ee00000000", "component": "laminas", "http.method": "GET", + "http.route": "/simple_view[/:key][/:pwd]", "http.status_code": "200", "http.url": "http://localhost/simple_view?key=value&", "http.version": "1.1", From f9ebd5934979a52e82bbfa0d6fbb6b1f06f99b74 Mon Sep 17 00:00:00 2001 From: Alejandro Estringana Ruiz Date: Mon, 23 Mar 2026 16:13:36 +0100 Subject: [PATCH 5/6] Add appsec integration test --- .../php/integration/Laminas33Tests.groovy | 112 ++++++++++++++++++ .../src/test/www/laminas33/.gitignore | 2 + .../src/test/www/laminas33/composer.json | 28 +++++ .../laminas33/config/application.config.php | 16 +++ .../config/autoload/database.global.php | 23 ++++ .../www/laminas33/config/autoload/global.php | 3 + .../test/www/laminas33/config/container.php | 12 ++ .../www/laminas33/config/modules.config.php | 10 ++ .../src/test/www/laminas33/initialize.sh | 26 ++++ .../Application/config/module.config.php | 84 +++++++++++++ .../src/Controller/DynamicPathController.php | 17 +++ .../src/Controller/IndexController.php | 16 +++ .../src/Controller/LoginController.php | 72 +++++++++++ .../src/Controller/LoginControllerFactory.php | 23 ++++ .../module/Application/src/Module.php | 15 +++ .../view/application/index/index.phtml | 6 + .../module/Application/view/error/404.phtml | 1 + .../module/Application/view/error/index.phtml | 1 + .../Application/view/layout/layout.phtml | 15 +++ .../src/test/www/laminas33/public/.htaccess | 10 ++ .../src/test/www/laminas33/public/index.php | 24 ++++ .../laminas33/public/outside_of_framework.php | 3 + 22 files changed, 519 insertions(+) create mode 100644 appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/Laminas33Tests.groovy create mode 100644 appsec/tests/integration/src/test/www/laminas33/.gitignore create mode 100644 appsec/tests/integration/src/test/www/laminas33/composer.json create mode 100644 appsec/tests/integration/src/test/www/laminas33/config/application.config.php create mode 100644 appsec/tests/integration/src/test/www/laminas33/config/autoload/database.global.php create mode 100644 appsec/tests/integration/src/test/www/laminas33/config/autoload/global.php create mode 100644 appsec/tests/integration/src/test/www/laminas33/config/container.php create mode 100644 appsec/tests/integration/src/test/www/laminas33/config/modules.config.php create mode 100755 appsec/tests/integration/src/test/www/laminas33/initialize.sh create mode 100644 appsec/tests/integration/src/test/www/laminas33/module/Application/config/module.config.php create mode 100644 appsec/tests/integration/src/test/www/laminas33/module/Application/src/Controller/DynamicPathController.php create mode 100644 appsec/tests/integration/src/test/www/laminas33/module/Application/src/Controller/IndexController.php create mode 100644 appsec/tests/integration/src/test/www/laminas33/module/Application/src/Controller/LoginController.php create mode 100644 appsec/tests/integration/src/test/www/laminas33/module/Application/src/Controller/LoginControllerFactory.php create mode 100644 appsec/tests/integration/src/test/www/laminas33/module/Application/src/Module.php create mode 100644 appsec/tests/integration/src/test/www/laminas33/module/Application/view/application/index/index.phtml create mode 100644 appsec/tests/integration/src/test/www/laminas33/module/Application/view/error/404.phtml create mode 100644 appsec/tests/integration/src/test/www/laminas33/module/Application/view/error/index.phtml create mode 100644 appsec/tests/integration/src/test/www/laminas33/module/Application/view/layout/layout.phtml create mode 100644 appsec/tests/integration/src/test/www/laminas33/public/.htaccess create mode 100644 appsec/tests/integration/src/test/www/laminas33/public/index.php create mode 100644 appsec/tests/integration/src/test/www/laminas33/public/outside_of_framework.php diff --git a/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/Laminas33Tests.groovy b/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/Laminas33Tests.groovy new file mode 100644 index 00000000000..e15fa8f33be --- /dev/null +++ b/appsec/tests/integration/src/test/groovy/com/datadog/appsec/php/integration/Laminas33Tests.groovy @@ -0,0 +1,112 @@ +package com.datadog.appsec.php.integration + +import com.datadog.appsec.php.docker.AppSecContainer +import com.datadog.appsec.php.docker.FailOnUnmatchedTraces +import com.datadog.appsec.php.docker.InspectContainerHelper +import com.datadog.appsec.php.model.Span +import com.datadog.appsec.php.model.Trace +import org.junit.jupiter.api.MethodOrderer +import org.junit.jupiter.api.Order +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestMethodOrder +import org.junit.jupiter.api.condition.EnabledIf +import org.testcontainers.junit.jupiter.Container +import org.testcontainers.junit.jupiter.Testcontainers + +import java.io.InputStream +import java.net.http.HttpRequest +import java.net.http.HttpResponse + +import static com.datadog.appsec.php.integration.TestParams.getPhpVersion +import static com.datadog.appsec.php.integration.TestParams.getVariant +import static java.net.http.HttpResponse.BodyHandlers.ofString + +@Testcontainers +@EnabledIf('isExpectedVersion') +@TestMethodOrder(MethodOrderer.OrderAnnotation) +class Laminas33Tests { + + /** + * Laminas MVC 3.3.x supports PHP 7.3–8.1 per composer constraints in www/laminas33. + */ + static boolean expectedVersion = + ['7.3', '7.4', '8.0', '8.1'].contains(getPhpVersion()) && !getVariant().contains('zts') + + AppSecContainer getContainer() { + getClass().CONTAINER + } + + @Container + @FailOnUnmatchedTraces + public static final AppSecContainer CONTAINER = + new AppSecContainer( + workVolume: this.name, + baseTag: 'apache2-mod-php', + phpVersion: getPhpVersion(), + phpVariant: getVariant(), + www: 'laminas33', + ) + + static void main(String[] args) { + InspectContainerHelper.run(CONTAINER) + } + + @Test + @Order(1) + void 'home request sets http route to literal slash'() { + Trace trace = container.traceFromRequest('/') { HttpResponse resp -> + assert resp.statusCode() == 200 + } + Span span = trace.first() + assert span.meta.'http.route' == '/' + } + + @Test + @Order(2) + void 'Login failure automated event'() { + Trace trace = container.traceFromRequest('/authenticate?email=nonExisiting@email.com') { + HttpResponse resp -> + assert resp.statusCode() == 403 + } + + Span span = trace.first() + assert span.meta.'appsec.events.users.login.failure.track' == 'true' + assert span.meta.'_dd.appsec.events.users.login.failure.auto.mode' == 'identification' + assert span.meta.'appsec.events.users.login.failure.usr.exists' == 'false' + assert span.meta.'http.route' == '/authenticate' + assert span.metrics._sampling_priority_v1 == 2.0d + } + + @Test + @Order(3) + void 'Login success automated event'() { + def trace = container.traceFromRequest('/authenticate?email=ciuser@example.com') { + HttpResponse resp -> + assert resp.statusCode() == 200 + } + + Span span = trace.first() + assert span.meta.'usr.id' == '1' + assert span.meta.'_dd.appsec.events.users.login.success.auto.mode' == 'identification' + assert span.meta.'appsec.events.users.login.success.track' == 'true' + assert span.meta.'http.route' == '/authenticate' + assert span.metrics._sampling_priority_v1 == 2.0d + } + + @Test + @Order(4) + void 'path params trigger WAF block and laminas http route template'() { + HttpRequest req = container.buildReq('/dynamic-path/someValue').GET().build() + def trace = container.traceFromRequest(req, ofString()) { HttpResponse re -> + assert re.statusCode() == 403 + assert re.body().toLowerCase().contains('blocked') + } + + Span span = trace.first() + assert span.metrics.'_dd.appsec.enabled' == 1.0d + assert span.metrics.'_dd.appsec.waf.duration' > 0.0d + assert span.meta.'_dd.appsec.event_rules.version' != '' + assert span.meta.'appsec.blocked' == 'true' + assert span.meta.'http.route' == '/dynamic-path[/:param01]' + } +} diff --git a/appsec/tests/integration/src/test/www/laminas33/.gitignore b/appsec/tests/integration/src/test/www/laminas33/.gitignore new file mode 100644 index 00000000000..548364011a8 --- /dev/null +++ b/appsec/tests/integration/src/test/www/laminas33/.gitignore @@ -0,0 +1,2 @@ +/vendor/ +/data/cache/ diff --git a/appsec/tests/integration/src/test/www/laminas33/composer.json b/appsec/tests/integration/src/test/www/laminas33/composer.json new file mode 100644 index 00000000000..5c0279cca79 --- /dev/null +++ b/appsec/tests/integration/src/test/www/laminas33/composer.json @@ -0,0 +1,28 @@ +{ + "name": "datadog/laminas-appsec-integration", + "description": "Laminas MVC 3.3 app for AppSec integration tests", + "type": "project", + "license": "BSD-3-Clause", + "require": { + "php": "^7.3 || ~8.0.0 || ~8.1.0", + "laminas/laminas-component-installer": "^2.4", + "laminas/laminas-development-mode": "^3.2", + "laminas/laminas-mvc": "3.3.*", + "laminas/laminas-authentication": "^2.9", + "laminas/laminas-db": "^2.13", + "laminas/laminas-session": "^2.10" + }, + "autoload": { + "psr-4": { + "Application\\": "module/Application/src/" + } + }, + "config": { + "allow-plugins": { + "laminas/laminas-component-installer": true + }, + "platform": { + "php": "7.3.33" + } + } +} diff --git a/appsec/tests/integration/src/test/www/laminas33/config/application.config.php b/appsec/tests/integration/src/test/www/laminas33/config/application.config.php new file mode 100644 index 00000000000..0091d20194d --- /dev/null +++ b/appsec/tests/integration/src/test/www/laminas33/config/application.config.php @@ -0,0 +1,16 @@ + require __DIR__ . '/modules.config.php', + 'module_listener_options' => [ + 'use_laminas_loader' => false, + 'config_glob_paths' => [ + realpath(__DIR__) . '/autoload/{{,*.}global,{,*.}local}.php', + ], + 'config_cache_enabled' => false, + 'config_cache_key' => 'application.config.cache', + 'module_map_cache_enabled' => false, + 'module_map_cache_key' => 'application.module.cache', + 'cache_dir' => 'data/cache/', + ], +]; diff --git a/appsec/tests/integration/src/test/www/laminas33/config/autoload/database.global.php b/appsec/tests/integration/src/test/www/laminas33/config/autoload/database.global.php new file mode 100644 index 00000000000..2c77d918427 --- /dev/null +++ b/appsec/tests/integration/src/test/www/laminas33/config/autoload/database.global.php @@ -0,0 +1,23 @@ + [ + 'driver' => 'Pdo', + 'dsn' => 'sqlite:/tmp/laminas_appsec.sqlite', + ], + 'service_manager' => [ + 'factories' => [ + Adapter::class => function ($container) { + return new Adapter($container->get('config')['db']); + }, + AuthenticationService::class => function ($container) { + $storage = new SessionStorage(); + return new AuthenticationService($storage); + }, + ], + ], +]; diff --git a/appsec/tests/integration/src/test/www/laminas33/config/autoload/global.php b/appsec/tests/integration/src/test/www/laminas33/config/autoload/global.php new file mode 100644 index 00000000000..0b67a5fe474 --- /dev/null +++ b/appsec/tests/integration/src/test/www/laminas33/config/autoload/global.php @@ -0,0 +1,3 @@ +getServiceManager(); diff --git a/appsec/tests/integration/src/test/www/laminas33/config/modules.config.php b/appsec/tests/integration/src/test/www/laminas33/config/modules.config.php new file mode 100644 index 00000000000..78a68bb98fe --- /dev/null +++ b/appsec/tests/integration/src/test/www/laminas33/config/modules.config.php @@ -0,0 +1,10 @@ +setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); +$pdo->exec("CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, email TEXT UNIQUE NOT NULL, password TEXT NOT NULL)"); +$hash = md5("password"); +$stmt = $pdo->prepare("INSERT INTO users (name, email, password) VALUES (?, ?, ?)"); +$stmt->execute(["Ci User", "ciuser@example.com", $hash]); +' + +chown www-data:www-data /tmp/laminas_appsec.sqlite +chown -R www-data:www-data /var/www/data +mkdir -p /tmp/logs/laminas +chown www-data:www-data /tmp/logs/laminas diff --git a/appsec/tests/integration/src/test/www/laminas33/module/Application/config/module.config.php b/appsec/tests/integration/src/test/www/laminas33/module/Application/config/module.config.php new file mode 100644 index 00000000000..0bb308b0327 --- /dev/null +++ b/appsec/tests/integration/src/test/www/laminas33/module/Application/config/module.config.php @@ -0,0 +1,84 @@ + [ + 'routes' => [ + 'home' => [ + 'type' => Literal::class, + 'options' => [ + 'route' => '/', + 'defaults' => [ + 'controller' => Controller\IndexController::class, + 'action' => 'index', + ], + ], + ], + 'authenticate' => [ + 'type' => Literal::class, + 'options' => [ + 'route' => '/authenticate', + 'defaults' => [ + 'controller' => Controller\LoginController::class, + 'action' => 'auth', + ], + ], + ], + 'register' => [ + 'type' => Literal::class, + 'options' => [ + 'route' => '/register', + 'defaults' => [ + 'controller' => Controller\LoginController::class, + 'action' => 'signup', + ], + ], + ], + 'dynamic_path' => [ + 'type' => Segment::class, + 'options' => [ + 'route' => '/dynamic-path[/:param01]', + 'constraints' => [ + 'param01' => '[a-zA-Z0-9_-]+', + ], + 'defaults' => [ + 'controller' => DynamicPathController::class, + 'action' => 'index', + ], + ], + ], + ], + ], + 'controllers' => [ + 'factories' => [ + Controller\LoginController::class => LoginControllerFactory::class, + Controller\IndexController::class => InvokableFactory::class, + DynamicPathController::class => InvokableFactory::class, + ], + ], + 'view_manager' => [ + 'display_not_found_reason' => true, + 'display_exceptions' => true, + 'doctype' => 'HTML5', + 'not_found_template' => 'error/404', + 'exception_template' => 'error/index', + 'template_map' => [ + 'layout/layout' => __DIR__ . '/../view/layout/layout.phtml', + 'application/index/index' => __DIR__ . '/../view/application/index/index.phtml', + 'error/404' => __DIR__ . '/../view/error/404.phtml', + 'error/index' => __DIR__ . '/../view/error/index.phtml', + ], + 'template_path_stack' => [ + __DIR__ . '/../view', + ], + ], +]; diff --git a/appsec/tests/integration/src/test/www/laminas33/module/Application/src/Controller/DynamicPathController.php b/appsec/tests/integration/src/test/www/laminas33/module/Application/src/Controller/DynamicPathController.php new file mode 100644 index 00000000000..347a8287c68 --- /dev/null +++ b/appsec/tests/integration/src/test/www/laminas33/module/Application/src/Controller/DynamicPathController.php @@ -0,0 +1,17 @@ +getResponse(); + $response->setContent('ok'); + return $response; + } +} diff --git a/appsec/tests/integration/src/test/www/laminas33/module/Application/src/Controller/IndexController.php b/appsec/tests/integration/src/test/www/laminas33/module/Application/src/Controller/IndexController.php new file mode 100644 index 00000000000..ebd21c3ca2c --- /dev/null +++ b/appsec/tests/integration/src/test/www/laminas33/module/Application/src/Controller/IndexController.php @@ -0,0 +1,16 @@ +dbAdapter = $dbAdapter; + $this->authService = $authService; + $this->usersTable = $usersTable; + } + + public function authAction() + { + $email = $this->params()->fromQuery('email'); + $password = $this->params()->fromQuery('password', 'password'); + + if (!$email) { + $response = $this->getResponse(); + $response->setStatusCode(400); + $response->setContent('Email is required'); + return $response; + } + + // SQLite-compatible: passwords stored as MD5 hex; compare with bound value + $authAdapter = new CredentialTreatmentAdapter( + $this->dbAdapter, + 'users', + 'email', + 'password', + '?' + ); + + $authAdapter->setIdentity($email); + $authAdapter->setCredential(md5($password)); + + $result = $this->authService->authenticate($authAdapter); + + if ($result->isValid()) { + $userData = $authAdapter->getResultRowObject(null, 'password'); + $this->authService->getStorage()->write($userData); + + $response = $this->getResponse(); + $response->setStatusCode(200); + $response->setContent('Login successful'); + return $response; + } + + $response = $this->getResponse(); + $response->setStatusCode(403); + $response->setContent('Invalid credentials'); + return $response; + } +} diff --git a/appsec/tests/integration/src/test/www/laminas33/module/Application/src/Controller/LoginControllerFactory.php b/appsec/tests/integration/src/test/www/laminas33/module/Application/src/Controller/LoginControllerFactory.php new file mode 100644 index 00000000000..2411dfc56b9 --- /dev/null +++ b/appsec/tests/integration/src/test/www/laminas33/module/Application/src/Controller/LoginControllerFactory.php @@ -0,0 +1,23 @@ +get(Adapter::class); + $authService = $container->get(AuthenticationService::class); + $usersTable = new TableGateway('users', $dbAdapter); + + return new LoginController($dbAdapter, $authService, $usersTable); + } +} diff --git a/appsec/tests/integration/src/test/www/laminas33/module/Application/src/Module.php b/appsec/tests/integration/src/test/www/laminas33/module/Application/src/Module.php new file mode 100644 index 00000000000..7bd91ba68d2 --- /dev/null +++ b/appsec/tests/integration/src/test/www/laminas33/module/Application/src/Module.php @@ -0,0 +1,15 @@ + +

OK

diff --git a/appsec/tests/integration/src/test/www/laminas33/module/Application/view/error/404.phtml b/appsec/tests/integration/src/test/www/laminas33/module/Application/view/error/404.phtml new file mode 100644 index 00000000000..ffa6f66cc96 --- /dev/null +++ b/appsec/tests/integration/src/test/www/laminas33/module/Application/view/error/404.phtml @@ -0,0 +1 @@ +

404

diff --git a/appsec/tests/integration/src/test/www/laminas33/module/Application/view/error/index.phtml b/appsec/tests/integration/src/test/www/laminas33/module/Application/view/error/index.phtml new file mode 100644 index 00000000000..b6faa9b73b8 --- /dev/null +++ b/appsec/tests/integration/src/test/www/laminas33/module/Application/view/error/index.phtml @@ -0,0 +1 @@ +

Error

diff --git a/appsec/tests/integration/src/test/www/laminas33/module/Application/view/layout/layout.phtml b/appsec/tests/integration/src/test/www/laminas33/module/Application/view/layout/layout.phtml new file mode 100644 index 00000000000..b85c557edea --- /dev/null +++ b/appsec/tests/integration/src/test/www/laminas33/module/Application/view/layout/layout.phtml @@ -0,0 +1,15 @@ + +doctype() ?> + + + + headTitle('Laminas AppSec')->setSeparator(' - ')->setAutoEscape(false) ?> + + + content ?> + + diff --git a/appsec/tests/integration/src/test/www/laminas33/public/.htaccess b/appsec/tests/integration/src/test/www/laminas33/public/.htaccess new file mode 100644 index 00000000000..855519c0d15 --- /dev/null +++ b/appsec/tests/integration/src/test/www/laminas33/public/.htaccess @@ -0,0 +1,10 @@ +RewriteEngine On + +RewriteCond %{REQUEST_FILENAME} -s [OR] +RewriteCond %{REQUEST_FILENAME} -l [OR] +RewriteCond %{REQUEST_FILENAME} -d +RewriteRule ^.*$ - [L] + +RewriteCond %{REQUEST_URI}::$1 ^(/.+)/(.*)::\2$ +RewriteRule ^(.*) - [E=BASE:%1] +RewriteRule ^(.*)$ %{ENV:BASE}/index.php [L] diff --git a/appsec/tests/integration/src/test/www/laminas33/public/index.php b/appsec/tests/integration/src/test/www/laminas33/public/index.php new file mode 100644 index 00000000000..be14c77888e --- /dev/null +++ b/appsec/tests/integration/src/test/www/laminas33/public/index.php @@ -0,0 +1,24 @@ +get('Application')->run(); diff --git a/appsec/tests/integration/src/test/www/laminas33/public/outside_of_framework.php b/appsec/tests/integration/src/test/www/laminas33/public/outside_of_framework.php new file mode 100644 index 00000000000..b10c1607fd9 --- /dev/null +++ b/appsec/tests/integration/src/test/www/laminas33/public/outside_of_framework.php @@ -0,0 +1,3 @@ + Date: Tue, 24 Mar 2026 10:37:07 +0100 Subject: [PATCH 6/6] Update rest laminas snapshots --- ...laminas.api_tools.latest.rest_test.test_scenario_rest2xx.json | 1 + ...laminas.api_tools.latest.rest_test.test_scenario_rest4xx.json | 1 + ...laminas.api_tools.latest.rest_test.test_scenario_rest5xx.json | 1 + 3 files changed, 3 insertions(+) diff --git a/tests/snapshots/tests.integrations.laminas.api_tools.latest.rest_test.test_scenario_rest2xx.json b/tests/snapshots/tests.integrations.laminas.api_tools.latest.rest_test.test_scenario_rest2xx.json index 6d8de5d059c..781cebdb29d 100644 --- a/tests/snapshots/tests.integrations.laminas.api_tools.latest.rest_test.test_scenario_rest2xx.json +++ b/tests/snapshots/tests.integrations.laminas.api_tools.latest.rest_test.test_scenario_rest2xx.json @@ -11,6 +11,7 @@ "_dd.p.dm": "-0", "component": "laminas", "http.method": "POST", + "http.route": "[/v:version]/datadog-rest-service[/:datadog_rest_service_id]", "http.status_code": "201", "http.url": "http://localhost/datadog-rest-service", "http.version": "1.1", diff --git a/tests/snapshots/tests.integrations.laminas.api_tools.latest.rest_test.test_scenario_rest4xx.json b/tests/snapshots/tests.integrations.laminas.api_tools.latest.rest_test.test_scenario_rest4xx.json index 82abac8bc5f..bcd9fe56956 100644 --- a/tests/snapshots/tests.integrations.laminas.api_tools.latest.rest_test.test_scenario_rest4xx.json +++ b/tests/snapshots/tests.integrations.laminas.api_tools.latest.rest_test.test_scenario_rest4xx.json @@ -11,6 +11,7 @@ "_dd.p.dm": "-0", "component": "laminas", "http.method": "GET", + "http.route": "[/v:version]/datadog-rest-service[/:datadog_rest_service_id]", "http.status_code": "405", "http.url": "http://localhost/datadog-rest-service/1", "http.version": "1.1", diff --git a/tests/snapshots/tests.integrations.laminas.api_tools.latest.rest_test.test_scenario_rest5xx.json b/tests/snapshots/tests.integrations.laminas.api_tools.latest.rest_test.test_scenario_rest5xx.json index ceaf27c57dc..b12796c2391 100644 --- a/tests/snapshots/tests.integrations.laminas.api_tools.latest.rest_test.test_scenario_rest5xx.json +++ b/tests/snapshots/tests.integrations.laminas.api_tools.latest.rest_test.test_scenario_rest5xx.json @@ -15,6 +15,7 @@ "error.stack": "#0 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas-api-tools/api-tools-rest/src/AbstractResourceListener.php(182): DatadogApi\\V1\\Rest\\DatadogRestService\\DatadogRestServiceResource->fetch()\n#1 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas/laminas-eventmanager/src/EventManager.php(319): Laminas\\ApiTools\\Rest\\AbstractResourceListener->dispatch()\n#2 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas/laminas-eventmanager/src/EventManager.php(177): Laminas\\EventManager\\EventManager->triggerListeners()\n#3 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas-api-tools/api-tools-rest/src/Resource.php(544): Laminas\\EventManager\\EventManager->triggerEventUntil()\n#4 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas-api-tools/api-tools-rest/src/Resource.php(499): Laminas\\ApiTools\\Rest\\Resource->triggerEvent()\n#5 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas-api-tools/api-tools-rest/src/RestController.php(493): Laminas\\ApiTools\\Rest\\Resource->fetch()\n#6 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas/laminas-mvc/src/Controller/AbstractRestfulController.php(372): Laminas\\ApiTools\\Rest\\RestController->get()\n#7 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas-api-tools/api-tools-rest/src/RestController.php(335): Laminas\\Mvc\\Controller\\AbstractRestfulController->onDispatch()\n#8 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas/laminas-eventmanager/src/EventManager.php(319): Laminas\\ApiTools\\Rest\\RestController->onDispatch()\n#9 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas/laminas-eventmanager/src/EventManager.php(177): Laminas\\EventManager\\EventManager->triggerListeners()\n#10 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas/laminas-mvc/src/Controller/AbstractController.php(105): Laminas\\EventManager\\EventManager->triggerEventUntil()\n#11 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas/laminas-mvc/src/Controller/AbstractRestfulController.php(306): Laminas\\Mvc\\Controller\\AbstractController->dispatch()\n#12 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas/laminas-mvc/src/DispatchListener.php(117): Laminas\\Mvc\\Controller\\AbstractRestfulController->dispatch()\n#13 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas/laminas-eventmanager/src/EventManager.php(319): Laminas\\Mvc\\DispatchListener->onDispatch()\n#14 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas/laminas-eventmanager/src/EventManager.php(177): Laminas\\EventManager\\EventManager->triggerListeners()\n#15 {path}/tests/Frameworks/Laminas/ApiTools/Latest/vendor/laminas/laminas-mvc/src/Application.php(319): Laminas\\EventManager\\EventManager->triggerEventUntil()\n#16 {path}/tests/Frameworks/Laminas/ApiTools/Latest/public/index.php(60): Laminas\\Mvc\\Application->run()\n#17 {main}", "error.type": "Error", "http.method": "GET", + "http.route": "[/v:version]/datadog-rest-service[/:datadog_rest_service_id]", "http.status_code": "500", "http.url": "http://localhost/datadog-rest-service/42", "http.version": "1.1",