From ec10d19180a204d315afdc73494cfef9c2ecb4ff Mon Sep 17 00:00:00 2001 From: Hitesh Ahuja Date: Fri, 16 Aug 2024 14:19:32 -0700 Subject: [PATCH] feat(agent): initial prototype --- agent/fw_hooks.h | 1 + agent/php_execute.c | 228 +++++++++++++++++++++++++++++++++----------- axiom/nr_txn.h | 8 ++ 3 files changed, 182 insertions(+), 55 deletions(-) diff --git a/agent/fw_hooks.h b/agent/fw_hooks.h index dc04c657e..f66d18d72 100644 --- a/agent/fw_hooks.h +++ b/agent/fw_hooks.h @@ -46,6 +46,7 @@ extern void nr_fw_zend2_enable(TSRMLS_D); /* Libraries. */ extern void nr_aws_sdk_php_enable(); +extern void nr_composer_detected(); extern void nr_doctrine2_enable(TSRMLS_D); extern void nr_guzzle3_enable(TSRMLS_D); extern void nr_guzzle4_enable(TSRMLS_D); diff --git a/agent/php_execute.c b/agent/php_execute.c index 06f20ed28..c6f5a7f20 100644 --- a/agent/php_execute.c +++ b/agent/php_execute.c @@ -25,6 +25,7 @@ #include "util_metrics.h" #include "util_number_converter.h" #include "util_strings.h" +#include "util_syscalls.h" #include "util_url.h" #include "util_url.h" #include "util_metrics.h" @@ -476,7 +477,6 @@ typedef struct _nr_library_table_t { const char* library_name; const char* file_to_check; size_t file_to_check_len; - nr_composer_special_fn_t special; nr_library_enable_fn_t enable; } nr_library_table_t; @@ -486,28 +486,29 @@ typedef struct _nr_library_table_t { // clang-format: off static nr_library_table_t libraries[] = { /* AWS-SDK-PHP 3 */ - {"AWS-SDK-PHP", NR_PSTR("aws-sdk-php/src/awsclient.php"), 0, nr_aws_sdk_php_enable}, + {"AWS-SDK-PHP", NR_PSTR("aws-sdk-php/src/awsclient.php"), nr_aws_sdk_php_enable}, /* Doctrine < 2.18 */ - {"Doctrine 2", NR_PSTR("doctrine/orm/query.php"), 0, nr_doctrine2_enable}, + {"Doctrine 2", NR_PSTR("doctrine/orm/query.php"), nr_doctrine2_enable}, /* Doctrine 2.18 reworked the directory structure */ - {"Doctrine 2", NR_PSTR("doctrine/orm/src/query.php"), 0, nr_doctrine2_enable}, + {"Doctrine 2", NR_PSTR("doctrine/orm/src/query.php"), nr_doctrine2_enable}, - {"Guzzle 3", NR_PSTR("guzzle/http/client.php"), 0, nr_guzzle3_enable}, - {"Guzzle 4-5", NR_PSTR("hasemitterinterface.php"), 0, nr_guzzle4_enable}, - {"Guzzle 6", NR_PSTR("guzzle/src/functions_include.php"), 0, nr_guzzle6_enable}, + {"Guzzle 3", NR_PSTR("guzzle/http/client.php"), nr_guzzle3_enable}, + {"Guzzle 4-5", NR_PSTR("hasemitterinterface.php"), nr_guzzle4_enable}, + {"Guzzle 6", NR_PSTR("guzzle/src/functions_include.php"), nr_guzzle6_enable}, - {"MongoDB", NR_PSTR("mongodb/src/client.php"), 0, nr_mongodb_enable}, + {"MongoDB", NR_PSTR("mongodb/src/client.php"), nr_mongodb_enable}, /* * The first path is for Composer installs, the second is for * /usr/local/bin. */ - {"PHPUnit", NR_PSTR("phpunit/src/framework/test.php"), 0, nr_phpunit_enable}, - {"PHPUnit", NR_PSTR("phpunit/framework/test.php"), 0, nr_phpunit_enable}, + {"PHPUnit", NR_PSTR("phpunit/src/framework/test.php"), nr_phpunit_enable}, + {"PHPUnit", NR_PSTR("phpunit/framework/test.php"), nr_phpunit_enable}, - {"Predis", NR_PSTR("predis/src/client.php"), 0, nr_predis_enable}, - {"Predis", NR_PSTR("predis/client.php"), 0, nr_predis_enable}, + {"Predis", NR_PSTR("predis/src/client.php"), nr_predis_enable}, + {"Predis", NR_PSTR("predis/client.php"), nr_predis_enable}, + {"PHP_Package", NR_PSTR("vendor/autoload.php"), nr_composer_detected}, /* * Allow Zend Framework 1.x to be detected as a library as well as a @@ -515,14 +516,14 @@ static nr_library_table_t libraries[] = { * with other frameworks or even without a framework at all. This is * necessary for Magento in particular, which is built on ZF1. */ - {"Zend_Http", NR_PSTR("zend/http/client.php"), 0, nr_zend_http_enable}, + {"Zend_Http", NR_PSTR("zend/http/client.php"), nr_zend_http_enable}, /* * Allow Laminas Framework 3.x to be detected as a library as well as a * framework. This allows Laminas_Http_Client to be instrumented when used * with other frameworks or even without a framework at all. */ - {"Laminas_Http", NR_PSTR("laminas-http/src/client.php"), 0, nr_laminas_http_enable}, + {"Laminas_Http", NR_PSTR("laminas-http/src/client.php"), nr_laminas_http_enable}, /* * Other frameworks, detected only, but not specifically @@ -530,53 +531,53 @@ static nr_library_table_t libraries[] = { * detection of a supported framework or library later (since a transaction * can only have one framework). */ - {"Aura1", NR_PSTR("aura/framework/system.php"), 0, NULL}, - {"Aura2", NR_PSTR("aura/di/src/containerinterface.php"), 0, NULL}, - {"Aura3", NR_PSTR("aura/di/src/containerconfiginterface.php"), 0, NULL}, - {"CakePHP3", NR_PSTR("cakephp/src/core/functions.php"), 0, NULL}, - {"Fuel", NR_PSTR("fuel/core/classes/fuel.php"), 0, NULL}, - {"Lithium", NR_PSTR("lithium/core/libraries.php"), 0, NULL}, - {"Phpbb", NR_PSTR("phpbb/request/request.php"), 0, NULL}, - {"Phpixie2", NR_PSTR("phpixie/core/classes/phpixie/pixie.php"), 0, NULL}, - {"Phpixie3", NR_PSTR("phpixie/framework.php"), 0, NULL}, - {"React", NR_PSTR("react/event-loop/src/loopinterface.php"), 0, NULL}, - {"SilverStripe", NR_PSTR("injector/silverstripeinjectioncreator.php"), 0, NULL}, - {"SilverStripe4", NR_PSTR("silverstripeserviceconfigurationlocator.php"), 0, NULL}, - {"Typo3", NR_PSTR("classes/typo3/flow/core/bootstrap.php"), 0, NULL}, - {"Typo3", NR_PSTR("typo3/sysext/core/classes/core/bootstrap.php"), 0, NULL}, + {"Aura1", NR_PSTR("aura/framework/system.php"), NULL}, + {"Aura2", NR_PSTR("aura/di/src/containerinterface.php"), NULL}, + {"Aura3", NR_PSTR("aura/di/src/containerconfiginterface.php"), NULL}, + {"CakePHP3", NR_PSTR("cakephp/src/core/functions.php"), NULL}, + {"Fuel", NR_PSTR("fuel/core/classes/fuel.php"), NULL}, + {"Lithium", NR_PSTR("lithium/core/libraries.php"), NULL}, + {"Phpbb", NR_PSTR("phpbb/request/request.php"), NULL}, + {"Phpixie2", NR_PSTR("phpixie/core/classes/phpixie/pixie.php"), NULL}, + {"Phpixie3", NR_PSTR("phpixie/framework.php"), NULL}, + {"React", NR_PSTR("react/event-loop/src/loopinterface.php"), NULL}, + {"SilverStripe", NR_PSTR("injector/silverstripeinjectioncreator.php"), NULL}, + {"SilverStripe4", NR_PSTR("silverstripeserviceconfigurationlocator.php"), NULL}, + {"Typo3", NR_PSTR("classes/typo3/flow/core/bootstrap.php"), NULL}, + {"Typo3", NR_PSTR("typo3/sysext/core/classes/core/bootstrap.php"), NULL}, /* * Other CMS (content management systems), detected only, but * not specifically instrumented. */ - {"Moodle", NR_PSTR("moodlelib.php"), 0, NULL}, + {"Moodle", NR_PSTR("moodlelib.php"), NULL}, /* * It is likely that this will never be found, since the CodeIgniter.php * will get loaded first, and as such mark this transaction as belonging to * CodeIgniter, and not Expession Engine. */ - {"ExpressionEngine", NR_PSTR("system/expressionengine/config/config.php"), 0, NULL}, + {"ExpressionEngine", NR_PSTR("system/expressionengine/config/config.php"), NULL}, /* * ExpressionEngine 5, however, has a very obvious file we can look for. */ - {"ExpressionEngine5", NR_PSTR("expressionengine/boot/boot.php"), 0, NULL}, + {"ExpressionEngine5", NR_PSTR("expressionengine/boot/boot.php"), NULL}, /* * DokuWiki uses doku.php as an entry point, but has other files that are * loaded directly that this won't pick up. That's probably OK for * supportability metrics, but we'll add the most common name for the * configuration file as well just in case. */ - {"DokuWiki", NR_PSTR("doku.php"), 0, NULL}, - {"DokuWiki", NR_PSTR("conf/dokuwiki.php"), 0, NULL}, + {"DokuWiki", NR_PSTR("doku.php"), NULL}, + {"DokuWiki", NR_PSTR("conf/dokuwiki.php"), NULL}, /* * SugarCRM no longer has a community edition, so this likely only works * with older versions. */ - {"SugarCRM", NR_PSTR("sugarobjects/sugarconfig.php"), 0, NULL}, + {"SugarCRM", NR_PSTR("sugarobjects/sugarconfig.php"), NULL}, - {"Xoops", NR_PSTR("class/xoopsload.php"), 0, NULL}, - {"E107", NR_PSTR("e107_handlers/e107_class.php"), 0, NULL}, + {"Xoops", NR_PSTR("class/xoopsload.php"), NULL}, + {"E107", NR_PSTR("e107_handlers/e107_class.php"), NULL}, }; // clang-format: on @@ -585,15 +586,15 @@ static size_t num_libraries = sizeof(libraries) / sizeof(nr_library_table_t); // clang-format: off static nr_library_table_t logging_frameworks[] = { /* Monolog - Logging for PHP */ - {"Monolog", NR_PSTR("monolog/logger.php"), 0, nr_monolog_enable}, + {"Monolog", NR_PSTR("monolog/logger.php"), nr_monolog_enable}, /* Consolidation/Log - Logging for PHP */ - {"Consolidation/Log", NR_PSTR("consolidation/log/src/logger.php"), 0, NULL}, + {"Consolidation/Log", NR_PSTR("consolidation/log/src/logger.php"), NULL}, /* laminas-log - Logging for PHP */ - {"laminas-log", NR_PSTR("laminas-log/src/logger.php"), 0, NULL}, + {"laminas-log", NR_PSTR("laminas-log/src/logger.php"), NULL}, /* cakephp-log - Logging for PHP */ - {"cakephp-log", NR_PSTR("cakephp/log/log.php"), 0, NULL}, + {"cakephp-log", NR_PSTR("cakephp/log/log.php"), NULL}, /* Analog - Logging for PHP */ - {"Analog", NR_PSTR("analog/analog.php"), 0, NULL}, + {"Analog", NR_PSTR("analog/analog.php"), NULL}, }; // clang-format: on @@ -932,24 +933,137 @@ static void nr_execute_handle_library(const char* filename, } } -static void nr_execute_handle_composer(const char* filename, - const size_t filename_len TSRMLS_DC) { - size_t i; +void nr_composer_detected() { + if (NULL != NRPRG(txn) && !NRPRG(txn)->detection_status.composer_detected) { + const char* magic_file_1 + = "vendor/composer/autoload_real.php"; + const char* composer_file = "vendor/composer/installed.php"; + // the below should be using the absolute path + if (0 == nr_access(magic_file_1, F_OK | R_OK)) { + if (0 == nr_access(composer_file, F_OK | R_OK)) { + NRPRG(txn)->detection_status.composer_detected = 1; + NRPRG(txn)->detection_status.file_exists = 1; + NRPRG(txn)->detection_status.api_called = 0; + NRPRG(txn)->detection_status.inside_eval_string = 0; + } + } + } +} - for (i = 0; i < num_libraries; i++) { - if (nr_striendswith(STR_AND_LEN(filename), - STR_AND_LEN(libraries[i].file_to_check))) { - if (libraries[i].special) { - nr_composer_classification_t special - = libraries[i].special(filename TSRMLS_CC); - if (FILE_EXISTS == special) { - nr_fw_support_add_library_supportability_metric( - NRPRG(txn), libraries[i].library_name); - if (NULL != libraries[i].enable) { - libraries[i].enable(TSRMLS_C); +static void nr_get_composer_package_information() { + zval retval; + int result = -1; + + if (NRPRG(txn)->detection_status.inside_eval_string) { + return; + } + + char* check + = "" + "(function() {" + " try {" + " if (class_exists('Composer\\InstalledVersions')) {" + " if (method_exists('Composer\\InstalledVersions', " + " 'getInstalledPackages') && method_exists('Composer\\InstalledVersions', " + " 'getVersion')) {" + " return true;" + " } else {" + " return false;" + " }" + " } else {" + " return false;" + " }" + " } catch (Exception $e) {" + " return NULL;" + " }" + "})();"; + + char* getpackagename = + "" + "(function() {" + " try {" + " return Composer\\InstalledVersions::getInstalledPackages();" + " } catch (Exception $e) {" + " return NULL;" + " }" + "})();"; + + char* getversion = + "" + "(function() {" + " try {" + " return Composer\\InstalledVersions::getVersion(\"%s\");" + " } catch (Exception $e) {" + " return NULL;" + " }" + "})();"; + + NRPRG(txn)->detection_status.inside_eval_string = 1; + result = zend_eval_string(check, &retval, "check if class and methods exist" TSRMLS_CC); + NRPRG(txn)->detection_status.inside_eval_string = 0; + + if (result == SUCCESS) { + if (Z_TYPE(retval) == IS_NULL) { + if (NULL != NRPRG(txn)) { + NRPRG(txn)->detection_status.api_called = 1; + } + nrl_verbosedebug(NRL_TXN, "IS_NULL"); + return; + } else if (Z_TYPE(retval) == IS_FALSE) { + nrl_verbosedebug(NRL_TXN, "IS_FALSE"); + return; + } else if (Z_TYPE(retval) != IS_TRUE) { + nrl_verbosedebug(NRL_TXN, "NOT_TRUE"); + return; + } + } + zval_dtor(&retval); + NRPRG(txn)->detection_status.inside_eval_string = 1; + result = zend_eval_string(getpackagename, &retval, + "get installed packages by name" TSRMLS_CC); + NRPRG(txn)->detection_status.inside_eval_string = 0; + if (result == SUCCESS) { + if (Z_TYPE(retval) == IS_ARRAY) { + zval* value; + char* buf; + int result2; + zval retval2; + char* version = NULL; + (void)version; + ZEND_HASH_FOREACH_VAL(Z_ARRVAL(retval), value) { + if (Z_TYPE_P(value) == IS_STRING) { + buf = nr_formatf(getversion, Z_STRVAL_P(value)); + NRPRG(txn)->detection_status.inside_eval_string = 1; + result2 = zend_eval_string(buf, &retval2, "retrieve version for packages"); + NRPRG(txn)->detection_status.inside_eval_string = 0; + nr_free(buf); + if (SUCCESS == result2) { + if (nr_php_is_zval_valid_string(&retval2)) { + version = Z_STRVAL(retval2); + } } } + zval_dtor(&retval2); + nrl_verbosedebug(NRL_TXN, "package %s, version %s", + NRSAFESTR(Z_STRVAL_P(value)), NRSAFESTR(version)); } + ZEND_HASH_FOREACH_END(); + } else { + if (NULL != NRPRG(txn)) { + NRPRG(txn)->detection_status.api_called = 1; + } + zval_dtor(&retval); + return; + } + zval_dtor(&retval); + NRPRG(txn)->detection_status.api_called = 1; + } +} + +static void nr_execute_handle_composer() { + if (NRPRG(txn)->detection_status.file_exists) { + if (!NRPRG(txn)->detection_status.api_called) { + nr_get_composer_package_information(); } } } @@ -1060,6 +1174,10 @@ static void nr_php_execute_file(const zend_op_array* op_array, } nr_php_add_user_instrumentation(TSRMLS_C); + + if (NRPRG(txn)->detection_status.composer_detected && !NRPRG(txn)->detection_status.api_called) { + nr_execute_handle_composer(filename, filename_len); + } } /* diff --git a/axiom/nr_txn.h b/axiom/nr_txn.h index a6f942f66..0460e6ddf 100644 --- a/axiom/nr_txn.h +++ b/axiom/nr_txn.h @@ -203,6 +203,13 @@ typedef enum _nr_cpu_usage_t { NR_CPU_USAGE_COUNT = 2 } nr_cpu_usage_t; +typedef struct _nr_composer_info_t { + int composer_detected; + int api_called; + int file_exists; + int inside_eval_string; +} nr_composer_info_t; + /* * Possible transaction types, which go into the type bitfield in the nrtxn_t * struct. @@ -302,6 +309,7 @@ typedef struct _nrtxn_t { nr_distributed_trace_t* distributed_trace; /* distributed tracing metadata for the transaction */ nr_span_queue_t* span_queue; /* span queue when 8T is enabled */ + nr_composer_info_t detection_status; /* * flag to indicate if one time (per transaction) logging metrics