diff --git a/Gemfile_test b/Gemfile_test index 5b8fdac8..495d3130 100644 --- a/Gemfile_test +++ b/Gemfile_test @@ -32,6 +32,11 @@ gem 'httpx' gem 'typhoeus' gem 'async-http' gem 'ethon' +if RUBY_VERSION < '2.5' + gem 'dalli', '=2.7.11' +else + gem 'dalli' +end if RUBY_ENGINE == 'jruby' gem 'activerecord-jdbc-adapter' gem 'jdbc-sqlite3' diff --git a/lib/newrelic_security/constants.rb b/lib/newrelic_security/constants.rb index 30c778cd..85d34574 100644 --- a/lib/newrelic_security/constants.rb +++ b/lib/newrelic_security/constants.rb @@ -26,6 +26,8 @@ module NewRelic::Security HTTP_REQUEST = 'HTTP_REQUEST' XPATH = 'XPATH' LDAP = 'LDAP' + CACHING_DATA_STORE = 'CACHING_DATA_STORE' + MEMCACHED = 'MEMCACHED' MONGO = 'MONGO' SQLITE = 'SQLITE' MYSQL = 'MYSQL' diff --git a/lib/newrelic_security/instrumentation-security/memcached/chain.rb b/lib/newrelic_security/instrumentation-security/memcached/chain.rb new file mode 100644 index 00000000..f1c1aeb0 --- /dev/null +++ b/lib/newrelic_security/instrumentation-security/memcached/chain.rb @@ -0,0 +1,25 @@ +module NewRelic::Security + module Instrumentation + module Dalli + module Client + module Chain + + def self.instrument! + ::Dalli::Client.class_eval do + include NewRelic::Security::Instrumentation::Dalli::Client + + alias_method :perform_without_security, :perform + + def perform(*all_args) + retval = nil + event = perform_on_enter(*all_args) { retval = perform_without_security(*all_args) } + perform_on_exit(event) { return retval } + end + + end + end + end + end + end + end +end \ No newline at end of file diff --git a/lib/newrelic_security/instrumentation-security/memcached/instrumentation.rb b/lib/newrelic_security/instrumentation-security/memcached/instrumentation.rb new file mode 100644 index 00000000..92c6b41d --- /dev/null +++ b/lib/newrelic_security/instrumentation-security/memcached/instrumentation.rb @@ -0,0 +1,49 @@ +require_relative 'prepend' +require_relative 'chain' + +module NewRelic::Security + module Instrumentation + module Dalli::Client + + READ_MODES = %i[get fetch].freeze + WRITE_MODES = %i[set].freeze + DELETE_MODES = %i[delete flush].freeze + UPDATE_MODES = %i[get append prepend incr decr touch replace].freeze + + def perform_on_enter(*all_args) + event = nil + NewRelic::Security::Agent.logger.debug "OnEnter : #{self.class}.#{__method__}" + hash = {} + hash[:type] = all_args[0] + hash[:arguments] = all_args[1..-1] + if READ_MODES.include?(all_args[0]) + hash[:mode] = :read + elsif WRITE_MODES.include?(all_args[0]) + hash[:mode] = :write + elsif DELETE_MODES.include?(all_args[0]) + hash[:mode] = :delete + elsif UPDATE_MODES.include?(all_args[0]) + hash[:mode] = :update + end + event = NewRelic::Security::Agent::Control::Collector.collect(CACHING_DATA_STORE, [hash], MEMCACHED) + rescue => exception + NewRelic::Security::Agent.logger.error "Exception in hook in #{self.class}.#{__method__}, #{exception.inspect}, #{exception.backtrace}" + ensure + yield + return event + end + + def perform_on_exit(event) + NewRelic::Security::Agent.logger.debug "OnExit : #{self.class}.#{__method__}" + NewRelic::Security::Agent::Utils.create_exit_event(event) + rescue => exception + NewRelic::Security::Agent.logger.error "Exception in hook in #{self.class}.#{__method__}, #{exception.inspect}, #{exception.backtrace}" + ensure + yield + end + + end + end +end + +NewRelic::Security::Instrumentation::InstrumentationLoader.install_instrumentation(:memcached, ::Dalli::Client, ::NewRelic::Security::Instrumentation::Dalli::Client) diff --git a/lib/newrelic_security/instrumentation-security/memcached/prepend.rb b/lib/newrelic_security/instrumentation-security/memcached/prepend.rb new file mode 100644 index 00000000..f7209b1a --- /dev/null +++ b/lib/newrelic_security/instrumentation-security/memcached/prepend.rb @@ -0,0 +1,18 @@ +module NewRelic::Security + module Instrumentation + module Dalli + module Client + module Prepend + include NewRelic::Security::Instrumentation::Dalli::Client + + def perform(*all_args) + retval = nil + event = perform_on_enter(*all_args) { retval = super } + perform_on_exit(event) { return retval } + end + + end + end + end + end +end \ No newline at end of file diff --git a/test/helpers/database_helper.rb b/test/helpers/database_helper.rb index 01702ed7..45b3311a 100644 --- a/test/helpers/database_helper.rb +++ b/test/helpers/database_helper.rb @@ -18,6 +18,9 @@ module Test POSTGRESQL_USER = 'postgres' POSTGRESQL_DATABASE = 'postgres' + MEMCACHED_HOST = 'localhost' + MEMCACHED_PORT = '11212' + module DatabaseHelper extend self @@ -110,6 +113,35 @@ def remove_postgresql_container end end + MEMCACHED_CONFIG = { + 'Image' => 'memcached:latest', + 'name' => 'memcached_test', + 'HostConfig' => { + 'PortBindings' => { + '11211/tcp' => [{ 'HostPort' => MEMCACHED_PORT }] + } + } + } + + def create_memcached_container + image = Docker::Image.create('fromImage' => 'memcached:latest') + image.refresh! + begin + Docker::Container.get('memcached_test').remove(force: true) + rescue + end + container = Docker::Container.create(MEMCACHED_CONFIG) + container.start + sleep 10 + end + + def remove_memcached_container + begin + Docker::Container.get('memcached_test').remove(force: true) + rescue + end + end + end end -end \ No newline at end of file +end diff --git a/test/newrelic_security/instrumentation-security/memcached/memcached_test.rb b/test/newrelic_security/instrumentation-security/memcached/memcached_test.rb new file mode 100644 index 00000000..317433e7 --- /dev/null +++ b/test/newrelic_security/instrumentation-security/memcached/memcached_test.rb @@ -0,0 +1,119 @@ +require 'dalli' +require_relative '../../../test_helper' +require 'newrelic_security/instrumentation-security/memcached/instrumentation' + +module NewRelic::Security + module Test + module Instrumentation + class TestMemcached < Minitest::Test + @@before_all_flag = false + + def setup + $event_list.clear() + NewRelic::Security::Agent::Control::HTTPContext.set_context({}) + unless @@before_all_flag + NewRelic::Security::Test::DatabaseHelper.create_memcached_container + @@before_all_flag = true + end + end + + def test_set_get_fetch_delete + cache = Dalli::Client.new("#{MEMCACHED_HOST}:#{MEMCACHED_PORT}") + $event_list.clear() + res = cache.set 'greet', 'hello', nil, :raw => true + args = [{:type=>:set, :arguments=>["greet", "hello", 0, 0, { :raw=>true }], :mode=>:write}] + expected_event = NewRelic::Security::Agent::Control::Event.new(CACHING_DATA_STORE, args, MEMCACHED) + assert_equal 1, NewRelic::Security::Agent::Control::Collector.get_event_count(CACHING_DATA_STORE) + assert_equal expected_event.caseType, $event_list[0].caseType + assert_equal expected_event.parameters, $event_list[0].parameters + assert_equal expected_event.eventCategory, $event_list[0].eventCategory + $event_list.clear() + + res = cache.get 'greet' + args = [{:type=>:get, :arguments=>["greet", nil], :mode=>:read}] + expected_event = NewRelic::Security::Agent::Control::Event.new(CACHING_DATA_STORE, args, MEMCACHED) + assert_equal 'hello', res + assert_equal 1, NewRelic::Security::Agent::Control::Collector.get_event_count(CACHING_DATA_STORE) + assert_equal expected_event.caseType, $event_list[0].caseType + assert_equal expected_event.parameters, $event_list[0].parameters + assert_equal expected_event.eventCategory, $event_list[0].eventCategory + $event_list.clear() + + res = cache.fetch 'greet' + args = [{:type=>:get, :arguments=>["greet", nil], :mode=>:read}] + expected_event = NewRelic::Security::Agent::Control::Event.new(CACHING_DATA_STORE, args, MEMCACHED) + assert_equal 'hello', res + assert_equal 1, NewRelic::Security::Agent::Control::Collector.get_event_count(CACHING_DATA_STORE) + assert_equal expected_event.caseType, $event_list[0].caseType + assert_equal expected_event.parameters, $event_list[0].parameters + assert_equal expected_event.eventCategory, $event_list[0].eventCategory + $event_list.clear() + + res = cache.delete 'greet' + args = [{:type=>:delete, :arguments=>["greet", 0], :mode=>:delete}] + expected_event = NewRelic::Security::Agent::Control::Event.new(CACHING_DATA_STORE, args, MEMCACHED) + assert_equal 1, NewRelic::Security::Agent::Control::Collector.get_event_count(CACHING_DATA_STORE) + assert_equal true, res + assert_equal expected_event.caseType, $event_list[0].caseType + assert_equal expected_event.parameters, $event_list[0].parameters + assert_equal expected_event.eventCategory, $event_list[0].eventCategory + $event_list.clear() + end + + def test_set_incr_get_flush + cache = Dalli::Client.new("#{MEMCACHED_HOST}:#{MEMCACHED_PORT}") + $event_list.clear() + res = cache.set 'counter', 0, nil, :raw => true + args = [{:type=>:set, :arguments=>["counter", 0, 0, 0, {:raw=>true}], :mode=>:write}] + expected_event = NewRelic::Security::Agent::Control::Event.new(CACHING_DATA_STORE, args, MEMCACHED) + assert_equal 1, NewRelic::Security::Agent::Control::Collector.get_event_count(CACHING_DATA_STORE) + assert_equal expected_event.caseType, $event_list[0].caseType + assert_equal expected_event.parameters, $event_list[0].parameters + assert_equal expected_event.eventCategory, $event_list[0].eventCategory + $event_list.clear() + + res = cache.incr 'counter', 1 + args = [{:type=>:incr, :arguments=>["counter", 1, 0, nil], :mode=>:update}] + expected_event = NewRelic::Security::Agent::Control::Event.new(CACHING_DATA_STORE, args, MEMCACHED) + assert_equal 1, res + assert_equal 1, NewRelic::Security::Agent::Control::Collector.get_event_count(CACHING_DATA_STORE) + assert_equal expected_event.caseType, $event_list[0].caseType + assert_equal expected_event.parameters, $event_list[0].parameters + assert_equal expected_event.eventCategory, $event_list[0].eventCategory + $event_list.clear() + + res = cache.get 'counter' + args = [{:type=>:get, :arguments=>["counter", nil], :mode=>:read}] + expected_event = NewRelic::Security::Agent::Control::Event.new(CACHING_DATA_STORE, args, MEMCACHED) + assert_equal "1", res + assert_equal 1, NewRelic::Security::Agent::Control::Collector.get_event_count(CACHING_DATA_STORE) + assert_equal expected_event.caseType, $event_list[0].caseType + assert_equal expected_event.parameters, $event_list[0].parameters + assert_equal expected_event.eventCategory, $event_list[0].eventCategory + $event_list.clear() + + res = cache.decr 'counter', 1 + args = [{:type=>:decr, :arguments=>["counter", 1, 0, nil], :mode=>:update}] + expected_event = NewRelic::Security::Agent::Control::Event.new(CACHING_DATA_STORE, args, MEMCACHED) + assert_equal 0, res + assert_equal 1, NewRelic::Security::Agent::Control::Collector.get_event_count(CACHING_DATA_STORE) + assert_equal expected_event.caseType, $event_list[0].caseType + assert_equal expected_event.parameters, $event_list[0].parameters + assert_equal expected_event.eventCategory, $event_list[0].eventCategory + $event_list.clear() + end + + def teardown + $event_list.clear() + NewRelic::Security::Agent::Control::HTTPContext.reset_context + end + + Minitest.after_run do + NewRelic::Security::Test::DatabaseHelper.remove_memcached_container + end + + end + end + end +end + \ No newline at end of file