Skip to content

Commit 594af25

Browse files
committed
Add ato appsec events to Laminas
1 parent 78ecdb0 commit 594af25

File tree

15 files changed

+864
-4
lines changed

15 files changed

+864
-4
lines changed

src/DDTrace/Integrations/Laminas/LaminasIntegration.php

Lines changed: 278 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -582,10 +582,288 @@ static function ($This, $scope, $args) {
582582
}
583583
);
584584

585+
// Track login by hooking the storage write operation
586+
// This captures the full user object after it's stored
587+
hook_method(
588+
'Laminas\Authentication\Storage\Session',
589+
'write',
590+
null,
591+
static function ($This, $scope, $args, $returnValue) {
592+
if (!function_exists('\datadog\appsec\track_user_login_success_event_automated')) {
593+
return;
594+
}
595+
596+
// The first argument to write() is the identity
597+
$identity = isset($args[0]) ? $args[0] : null;
598+
if (!$identity) {
599+
return;
600+
}
601+
602+
// Skip if identity is just a string (initial write from authenticate())
603+
// We only want to track when the full user object is written by the controller
604+
if (is_string($identity)) {
605+
return;
606+
}
607+
608+
// Only track if this looks like a user object (has id property)
609+
$userId = self::getUserId($identity);
610+
if (!$userId) {
611+
return;
612+
}
613+
614+
$userLogin = self::getUserLogin($identity);
615+
$metadata = self::getUserMetadata($identity);
616+
617+
\datadog\appsec\track_user_login_success_event_automated(
618+
$userLogin,
619+
$userId,
620+
$metadata
621+
);
622+
}
623+
);
624+
625+
// Authentication tracking - Login failure
626+
install_hook(
627+
'Laminas\Authentication\AuthenticationService::authenticate',
628+
null,
629+
static function (HookData $hook) {
630+
$result = $hook->returned;
631+
632+
if (!$result instanceof \Laminas\Authentication\Result) {
633+
return;
634+
}
635+
636+
$code = $result->getCode();
637+
638+
// Only track failures
639+
if ($code === \Laminas\Authentication\Result::SUCCESS) {
640+
return;
641+
}
642+
643+
// Login failure
644+
if (!function_exists('\datadog\appsec\track_user_login_failure_event_automated')) {
645+
return;
646+
}
647+
648+
// Get the adapter from the hook arguments
649+
$adapter = isset($hook->args[0]) ? $hook->args[0] : null;
650+
$userLogin = null;
651+
652+
// Try to get the login from the adapter if it has a getIdentity method
653+
if ($adapter && method_exists($adapter, 'getIdentity')) {
654+
$userLogin = $adapter->getIdentity();
655+
}
656+
657+
$userExists = ($code === \Laminas\Authentication\Result::FAILURE_CREDENTIAL_INVALID);
658+
659+
\datadog\appsec\track_user_login_failure_event_automated(
660+
$userLogin,
661+
$userLogin,
662+
$userExists,
663+
[]
664+
);
665+
}
666+
);
667+
668+
// Track authenticated user on each request
669+
hook_method(
670+
'Laminas\Authentication\AuthenticationService',
671+
'hasIdentity',
672+
null,
673+
static function ($This, $scope, $args, $hasIdentity) {
674+
if (!$hasIdentity || !function_exists('\datadog\appsec\track_authenticated_user_event_automated')) {
675+
return;
676+
}
677+
678+
$identity = $This->getIdentity();
679+
if (!$identity) {
680+
return;
681+
}
682+
683+
$userId = self::getUserId($identity);
684+
\datadog\appsec\track_authenticated_user_event_automated($userId);
685+
}
686+
);
585687

586688
return Integration::LOADED;
587689
}
588690

691+
/**
692+
* Extract user ID from identity object
693+
*
694+
* @param mixed $identity
695+
* @return string
696+
*/
697+
private static function getUserId($identity)
698+
{
699+
if (is_string($identity) || is_int($identity)) {
700+
return (string)$identity;
701+
}
702+
703+
if (is_array($identity)) {
704+
if (isset($identity['id'])) {
705+
return (string)$identity['id'];
706+
}
707+
if (isset($identity['user_id'])) {
708+
return (string)$identity['user_id'];
709+
}
710+
if (isset($identity['username'])) {
711+
return $identity['username'];
712+
}
713+
if (isset($identity['email'])) {
714+
return $identity['email'];
715+
}
716+
}
717+
718+
if (is_object($identity)) {
719+
// Try common property names
720+
if (isset($identity->id)) {
721+
return (string)$identity->id;
722+
}
723+
if (isset($identity->user_id)) {
724+
return (string)$identity->user_id;
725+
}
726+
if (isset($identity->userId)) {
727+
return (string)$identity->userId;
728+
}
729+
730+
// Try common getter methods
731+
if (method_exists($identity, 'getId')) {
732+
return (string)$identity->getId();
733+
}
734+
if (method_exists($identity, 'getUserId')) {
735+
return (string)$identity->getUserId();
736+
}
737+
if (method_exists($identity, 'getUsername')) {
738+
return $identity->getUsername();
739+
}
740+
if (method_exists($identity, 'getEmail')) {
741+
return $identity->getEmail();
742+
}
743+
744+
// ArrayAccess support
745+
if ($identity instanceof \ArrayAccess) {
746+
if (isset($identity['id'])) {
747+
return (string)$identity['id'];
748+
}
749+
if (isset($identity['user_id'])) {
750+
return (string)$identity['user_id'];
751+
}
752+
if (isset($identity['username'])) {
753+
return $identity['username'];
754+
}
755+
if (isset($identity['email'])) {
756+
return $identity['email'];
757+
}
758+
}
759+
}
760+
761+
return '';
762+
}
763+
764+
/**
765+
* Extract user login (username/email) from identity object
766+
*
767+
* @param mixed $identity
768+
* @return string|null
769+
*/
770+
private static function getUserLogin($identity)
771+
{
772+
if (is_string($identity)) {
773+
return $identity;
774+
}
775+
776+
if (is_array($identity)) {
777+
if (isset($identity['email'])) {
778+
return $identity['email'];
779+
}
780+
if (isset($identity['username'])) {
781+
return $identity['username'];
782+
}
783+
}
784+
785+
if (is_object($identity)) {
786+
// Try properties
787+
if (isset($identity->email)) {
788+
return $identity->email;
789+
}
790+
if (isset($identity->username)) {
791+
return $identity->username;
792+
}
793+
794+
// Try getters
795+
if (method_exists($identity, 'getEmail')) {
796+
return $identity->getEmail();
797+
}
798+
if (method_exists($identity, 'getUsername')) {
799+
return $identity->getUsername();
800+
}
801+
802+
// ArrayAccess support
803+
if ($identity instanceof \ArrayAccess) {
804+
if (isset($identity['email'])) {
805+
return $identity['email'];
806+
}
807+
if (isset($identity['username'])) {
808+
return $identity['username'];
809+
}
810+
}
811+
}
812+
813+
return null;
814+
}
815+
816+
/**
817+
* Extract user metadata from identity object
818+
*
819+
* @param mixed $identity
820+
* @return array
821+
*/
822+
private static function getUserMetadata($identity)
823+
{
824+
$metadata = [];
825+
826+
if (is_array($identity)) {
827+
if (isset($identity['name'])) {
828+
$metadata['name'] = $identity['name'];
829+
}
830+
if (isset($identity['email'])) {
831+
$metadata['email'] = $identity['email'];
832+
}
833+
return $metadata;
834+
}
835+
836+
if (is_object($identity)) {
837+
// Try properties
838+
if (isset($identity->name)) {
839+
$metadata['name'] = $identity->name;
840+
}
841+
if (isset($identity->email)) {
842+
$metadata['email'] = $identity->email;
843+
}
844+
845+
// Try getters
846+
if (method_exists($identity, 'getName')) {
847+
$metadata['name'] = $identity->getName();
848+
}
849+
if (method_exists($identity, 'getEmail') && !isset($metadata['email'])) {
850+
$metadata['email'] = $identity->getEmail();
851+
}
852+
853+
// ArrayAccess support
854+
if ($identity instanceof \ArrayAccess) {
855+
if (isset($identity['name']) && !isset($metadata['name'])) {
856+
$metadata['name'] = $identity['name'];
857+
}
858+
if (isset($identity['email']) && !isset($metadata['email'])) {
859+
$metadata['email'] = $identity['email'];
860+
}
861+
}
862+
}
863+
864+
return $metadata;
865+
}
866+
589867
public static function debugBacktraceToString(array $backtrace)
590868
{
591869
// (methods) #<frame index> <file>(line): <class><type><function>()\n

tests/Frameworks/Laminas/Mvc/Latest/composer.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,16 @@
1414
"laminas/laminas-component-installer": "^3.2",
1515
"laminas/laminas-development-mode": "^3.10",
1616
"laminas/laminas-skeleton-installer": "^1.2",
17-
"laminas/laminas-mvc": "3.8.0"
17+
"laminas/laminas-mvc": "3.8.0",
18+
"laminas/laminas-authentication": "^2.9",
19+
"laminas/laminas-db": "^2.13",
20+
"laminas/laminas-session": "^2.10"
1821
},
1922
"autoload": {
2023
"psr-4": {
2124
"Application\\": "module/Application/src/"
22-
}
25+
},
26+
"files": ["../../../../Appsec/Mock.php"]
2327
},
2428
"autoload-dev": {
2529
"psr-4": {
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
use Laminas\Authentication\AuthenticationService;
4+
use Laminas\Authentication\Storage\Session as SessionStorage;
5+
use Laminas\Db\Adapter\Adapter;
6+
7+
return [
8+
'db' => [
9+
'driver' => 'Pdo',
10+
'dsn' => 'mysql:dbname=test;host=mysql-integration',
11+
'username' => 'test',
12+
'password' => 'test',
13+
],
14+
'service_manager' => [
15+
'factories' => [
16+
Adapter::class => function ($container) {
17+
return new Adapter($container->get('config')['db']);
18+
},
19+
AuthenticationService::class => function ($container) {
20+
$storage = new SessionStorage();
21+
return new AuthenticationService($storage);
22+
},
23+
],
24+
],
25+
];

tests/Frameworks/Laminas/Mvc/Latest/module/Application/config/module.config.php

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
namespace Application;
66

77
use Application\Controller\CommonSpecsController;
8+
use Application\Controller\LoginController;
9+
use Application\Controller\LoginControllerFactory;
810
use Laminas\Router\Http\Literal;
911
use Laminas\Router\Http\Segment;
1012
use Laminas\ServiceManager\Factory\InvokableFactory;
@@ -61,13 +63,44 @@
6163
'action' => 'error',
6264
],
6365
]
66+
],
67+
'login_auth' => [
68+
'type' => Literal::class,
69+
'options' => [
70+
'route' => '/login/auth',
71+
'defaults' => [
72+
'controller' => Controller\LoginController::class,
73+
'action' => 'auth',
74+
],
75+
]
76+
],
77+
'login_signup' => [
78+
'type' => Literal::class,
79+
'options' => [
80+
'route' => '/login/signup',
81+
'defaults' => [
82+
'controller' => Controller\LoginController::class,
83+
'action' => 'signup',
84+
],
85+
]
86+
],
87+
'behind_auth' => [
88+
'type' => Literal::class,
89+
'options' => [
90+
'route' => '/behind_auth',
91+
'defaults' => [
92+
'controller' => Controller\LoginController::class,
93+
'action' => 'behindAuth',
94+
],
95+
]
6496
]
6597
],
6698
],
6799
'controllers' => [
68100
'factories' => [
69101
Controller\IndexController::class => InvokableFactory::class,
70102
Controller\CommonSpecsController::class => InvokableFactory::class,
103+
Controller\LoginController::class => LoginControllerFactory::class,
71104
],
72105
],
73106
'view_manager' => [

0 commit comments

Comments
 (0)