diff --git a/lib/instrumentation-security/core/event-constants.js b/lib/instrumentation-security/core/event-constants.js index 41c986b4..dbd91a0b 100644 --- a/lib/instrumentation-security/core/event-constants.js +++ b/lib/instrumentation-security/core/event-constants.js @@ -20,7 +20,8 @@ const EVENT_TYPE = { UNVALIDATED_REDIRECT: 'UNVALIDATED_REDIRECT', REFLECTED_XSS: 'REFLECTED_XSS', XPATH: 'XPATH', - LDAP: 'LDAP' + LDAP: 'LDAP', + CACHING_DATA_STORE: 'CACHING_DATA_STORE' } const EVENT_CATEGORY = { MYSQL: 'MYSQL', @@ -40,7 +41,8 @@ const EVENT_CATEGORY = { UNVALIDATED_REDIRECT: 'UNVALIDATED_REDIRECT', REFLECTED_XSS: 'REFLECTED_XSS', XPATH: 'XPATH', - LDAP: 'LDAP' + LDAP: 'LDAP', + MEMCACHED: 'MEMCACHED' } module.exports = { diff --git a/lib/instrumentation-security/hooks/memcached/nr-memcached.js b/lib/instrumentation-security/hooks/memcached/nr-memcached.js new file mode 100644 index 00000000..7cc9b1f4 --- /dev/null +++ b/lib/instrumentation-security/hooks/memcached/nr-memcached.js @@ -0,0 +1,104 @@ +/* + * Copyright 2023 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +const requestManager = require('../../core/request-manager'); +const secUtils = require('../../core/sec-utils'); +const API = require("../../../nr-security-api"); +const securityMetaData = require('../../core/security-metadata'); +const { EVENT_TYPE, EVENT_CATEGORY } = require('../../core/event-constants'); +const logger = API.getLogger(); +const { NR_CSEC_FUZZ_REQUEST_ID } = require('../../core/constants'); + +const OperationMap = { + 'set': 'write', + 'get': 'read', + 'gets': 'read', + 'replace': 'update', + 'add': 'write', + 'cas': 'write', + 'append': 'write', + 'prepend': 'write', + 'delete': 'delete', + 'touch': 'update', + 'incr': 'update', + 'decr': 'update', + 'version': 'read', + 'flush_all': 'delete', + 'stats': 'read', + 'stats settings': 'read', + 'stats slabs': 'read', + 'stats items': 'read', + 'stats cachedump': 'read' + +} + +module.exports = function initialize(shim, memcached, moduleName, additionalMod) { + logger.info('Instrumenting ' + moduleName) + let utils; + if (additionalMod) { + utils = additionalMod; + } + else { + utils = shim.require('./lib/utils'); + } + validateArgHook(shim, utils, 'validateArg'); +} + + +function prepareSecurityArgs(args) { + const obj = args[0]; + let parameters = Object.create(null); + try { + if (obj) { + parameters.mode = obj['type']; + let params = []; + params.push(obj['key'].toString()); + if (obj['value']) { + params.push(obj['value']); + } + parameters.arguments = params; + parameters.type = OperationMap[parameters.mode]? OperationMap[parameters.mode]: 'unknown'; + } + } catch (error) { + logger.debug("Error in preparing memcached parameters:", error); + } + + return parameters; +} + +/** + * Wrapper to hook validateArg method + * @param {*} shim + * @param {*} mod + * @param {*} method + */ +function validateArgHook(shim, mod, method) { + shim.wrap(mod, method, function makeQueryWrapper(shim, fn) { + return function queryWrapper() { + let params = prepareSecurityArgs(arguments); + shim.interceptedArgs = params; + const request = requestManager.getRequest(shim); + + if (request) { + const traceObject = secUtils.getTraceObject(shim); + traceObject.sourceMethod = method; + const secMetadata = securityMetaData.getSecurityMetaData(request, params, traceObject, secUtils.getExecutionId(), EVENT_TYPE.CACHING_DATA_STORE, EVENT_CATEGORY.MEMCACHED) + const secEvent = API.generateSecEvent(secMetadata); + this.secEvent = secEvent; + API.sendEvent(secEvent); + } + + const result = fn.apply(this, arguments); + if (result && request && request.headers[NR_CSEC_FUZZ_REQUEST_ID]) { + API.generateExitEvent(this.secEvent); + delete this.secEvent + } + return result; + }; + }); +} + diff --git a/lib/instrumentation-security/index.js b/lib/instrumentation-security/index.js index 2e471af1..36f66e0c 100644 --- a/lib/instrumentation-security/index.js +++ b/lib/instrumentation-security/index.js @@ -190,7 +190,14 @@ newrelic.instrumentWebframework({ } }) - +newrelic.instrumentDatastore({ + moduleName: 'memcached', + isEsm: true, + onRequire: require('./hooks/memcached/nr-memcached'), + onError: function intrumentErrorHandler(err) { + logger.error(err.message, err.stack) + } +}) diff --git a/package-lock.json b/package-lock.json index 94e6eeb4..b97ae6f3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -51,6 +51,7 @@ "eslint-plugin-promise": "^4.3.1", "eslint-plugin-sonarjs": "^0.15.0", "koa": "^2.14.1", + "memcached": "^2.2.2", "mongodb": "^4.13.0", "mongodb-memory-server": "^8.11.2", "mongodb2": "npm:mongodb@^2.2.36", @@ -7644,6 +7645,12 @@ "typedarray": "^0.0.6" } }, + "node_modules/connection-parse": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/connection-parse/-/connection-parse-0.0.7.tgz", + "integrity": "sha512-bTTG28diWg7R7/+qE5NZumwPbCiJOT8uPdZYu674brDjBWQctbaQbYlDKhalS+4i5HxIx+G8dZsnBHKzWpp01A==", + "dev": true + }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -9672,6 +9679,16 @@ "node": ">=8" } }, + "node_modules/hashring": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/hashring/-/hashring-3.2.0.tgz", + "integrity": "sha512-xCMovURClsQZ+TR30icCZj+34Fq1hs0y6YCASD6ZqdRfYRybb5Iadws2WS+w09mGM/kf9xyA5FCdJQGcgcraSA==", + "dev": true, + "dependencies": { + "connection-parse": "0.0.x", + "simple-lru-cache": "0.0.x" + } + }, "node_modules/hosted-git-info": { "version": "2.8.9", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", @@ -10474,6 +10491,15 @@ "node": ">=8" } }, + "node_modules/jackpot": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/jackpot/-/jackpot-0.0.6.tgz", + "integrity": "sha512-rbWXX+A9ooq03/dfavLg9OXQ8YB57Wa7PY5c4LfU3CgFpwEhhl3WyXTQVurkaT7zBM5I9SSOaiLyJ4I0DQmC0g==", + "dev": true, + "dependencies": { + "retry": "0.6.0" + } + }, "node_modules/jackspeak": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-1.4.2.tgz", @@ -11063,6 +11089,16 @@ "node": ">= 0.6" } }, + "node_modules/memcached": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/memcached/-/memcached-2.2.2.tgz", + "integrity": "sha512-lHwUmqkT9WdUUgRsAvquO4xsKXYaBd644Orz31tuth+w/BIfFNuJMWwsG7sa7H3XXytaNfPTZ5R/yOG3d9zJMA==", + "dev": true, + "dependencies": { + "hashring": "3.2.x", + "jackpot": ">=0.0.6" + } + }, "node_modules/memory-pager": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", @@ -13009,6 +13045,15 @@ "node": ">=4" } }, + "node_modules/retry": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.6.0.tgz", + "integrity": "sha512-RgncoxLF1GqwAzTZs/K2YpZkWrdIYbXsmesdomi+iPilSzjUyr/wzNIuteoTVaWokzdwZIJ9NHRNQa/RUiOB2g==", + "dev": true, + "engines": { + "node": "*" + } + }, "node_modules/reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -13286,6 +13331,12 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true }, + "node_modules/simple-lru-cache": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/simple-lru-cache/-/simple-lru-cache-0.0.2.tgz", + "integrity": "sha512-uEv/AFO0ADI7d99OHDmh1QfYzQk/izT1vCmu/riQfh7qjBVUUgRT87E5s5h7CxWCA/+YoZerykpEthzVrW3LIw==", + "dev": true + }, "node_modules/sinon": { "version": "15.2.0", "resolved": "https://registry.npmjs.org/sinon/-/sinon-15.2.0.tgz", @@ -22916,6 +22967,12 @@ "typedarray": "^0.0.6" } }, + "connection-parse": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/connection-parse/-/connection-parse-0.0.7.tgz", + "integrity": "sha512-bTTG28diWg7R7/+qE5NZumwPbCiJOT8uPdZYu674brDjBWQctbaQbYlDKhalS+4i5HxIx+G8dZsnBHKzWpp01A==", + "dev": true + }, "content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -24419,6 +24476,16 @@ } } }, + "hashring": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/hashring/-/hashring-3.2.0.tgz", + "integrity": "sha512-xCMovURClsQZ+TR30icCZj+34Fq1hs0y6YCASD6ZqdRfYRybb5Iadws2WS+w09mGM/kf9xyA5FCdJQGcgcraSA==", + "dev": true, + "requires": { + "connection-parse": "0.0.x", + "simple-lru-cache": "0.0.x" + } + }, "hosted-git-info": { "version": "2.8.9", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", @@ -25004,6 +25071,15 @@ "istanbul-lib-report": "^3.0.0" } }, + "jackpot": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/jackpot/-/jackpot-0.0.6.tgz", + "integrity": "sha512-rbWXX+A9ooq03/dfavLg9OXQ8YB57Wa7PY5c4LfU3CgFpwEhhl3WyXTQVurkaT7zBM5I9SSOaiLyJ4I0DQmC0g==", + "dev": true, + "requires": { + "retry": "0.6.0" + } + }, "jackspeak": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-1.4.2.tgz", @@ -25504,6 +25580,16 @@ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", "dev": true }, + "memcached": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/memcached/-/memcached-2.2.2.tgz", + "integrity": "sha512-lHwUmqkT9WdUUgRsAvquO4xsKXYaBd644Orz31tuth+w/BIfFNuJMWwsG7sa7H3XXytaNfPTZ5R/yOG3d9zJMA==", + "dev": true, + "requires": { + "hashring": "3.2.x", + "jackpot": ">=0.0.6" + } + }, "memory-pager": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", @@ -27057,6 +27143,12 @@ "integrity": "sha512-M0b3YWQs7R3Z917WRQy1HHA7Ba7D8hvZg6UE5mLykJxQVE2ju0IXbGlaHPPlkY+WN7wFP+wUMXmBFA0aV6vYGQ==", "dev": true }, + "retry": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.6.0.tgz", + "integrity": "sha512-RgncoxLF1GqwAzTZs/K2YpZkWrdIYbXsmesdomi+iPilSzjUyr/wzNIuteoTVaWokzdwZIJ9NHRNQa/RUiOB2g==", + "dev": true + }, "reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -27266,6 +27358,12 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true }, + "simple-lru-cache": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/simple-lru-cache/-/simple-lru-cache-0.0.2.tgz", + "integrity": "sha512-uEv/AFO0ADI7d99OHDmh1QfYzQk/izT1vCmu/riQfh7qjBVUUgRT87E5s5h7CxWCA/+YoZerykpEthzVrW3LIw==", + "dev": true + }, "sinon": { "version": "15.2.0", "resolved": "https://registry.npmjs.org/sinon/-/sinon-15.2.0.tgz", diff --git a/package.json b/package.json index 5797d957..430446b2 100644 --- a/package.json +++ b/package.json @@ -77,6 +77,7 @@ "eslint-plugin-promise": "^4.3.1", "eslint-plugin-sonarjs": "^0.15.0", "koa": "^2.14.1", + "memcached": "^2.2.2", "mongodb": "^4.13.0", "mongodb-memory-server": "^8.11.2", "mongodb2": "npm:mongodb@^2.2.36", diff --git a/test/instrumentation-security/nr-memcached.test.js b/test/instrumentation-security/nr-memcached.test.js new file mode 100755 index 00000000..290582cc --- /dev/null +++ b/test/instrumentation-security/nr-memcached.test.js @@ -0,0 +1,103 @@ +/* + * Copyright 2023 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: New Relic Pre-Release + */ + +'use strict' + +const test = require('tap').test; +const utils = require('@newrelic/test-utilities') +const cp = require('child_process'); +const Memcached = require('memcached'); +let memcached = new Memcached(); + +const dbSetup = async () => { + cp.execSync('docker rm -f csec_memcache && docker run --name csec_memcache -p 11212:11211 -d memcached && sleep 1'); + memcached = new Memcached("localhost:11212"); +} + +test('memcached', (t) => { + t.autoend(); + let helper = null; + let initialize = null; + let shim = null; + + t.before(async () => { + await dbSetup(); + }) + + t.beforeEach(() => { + helper = utils.TestAgent.makeInstrumented() + shim = helper.getShim(); + initialize = require('../../lib/instrumentation-security/hooks/memcached/nr-memcached'); + initialize(shim, Memcached, 'memcached', require('memcached/lib/utils')); + }) + + t.afterEach(() => { + helper && helper.unload() + }) + + t.test('set', (t) => { + memcached.set("hello", 1, 100, function (err, result) { + t.equal('set', shim.interceptedArgs.mode); + t.equal('hello', shim.interceptedArgs.arguments[0]); + t.end(); + }); + }) + + t.test('get', (t) => { + memcached.get("hello", function (err, result) { + t.equal('get', shim.interceptedArgs.mode); + t.equal('hello', shim.interceptedArgs.arguments[0]); + t.end(); + }); + }) + + t.test('get_multi', (t) => { + memcached.get(["hello", "hello_json"], function (err, result) { + t.equal('get', shim.interceptedArgs.mode); + t.equal(1, shim.interceptedArgs.arguments.length); + t.end(); + }); + }) + + t.test('delete', (t) => { + memcached.del('foo', function (err, result) { + t.equal('delete', shim.interceptedArgs.mode); + t.equal('foo', shim.interceptedArgs.arguments[0]); + t.end(); + }); + }) + + t.test('set_json', (t) => { + memcached.set("hello_json", { javascript: 'objects', are: ['no', 'problem', 4], nMemcached: true }, 10000, function (err, result) { + t.equal('set', shim.interceptedArgs.mode); + t.equal('hello_json', shim.interceptedArgs.arguments[0]); + t.end(); + }); + }) + + t.test('increment', (t) => { + memcached.increment( "hello", 1, function( err, result ){ + t.equal('incr', shim.interceptedArgs.mode); + t.equal('hello', shim.interceptedArgs.arguments[0]); + t.equal(1, shim.interceptedArgs.arguments[1]); + t.end(); + }); + }) + + t.test('decrement', (t) => { + memcached.decr( "hello", 1, function( err, result ){ + t.equal('decr', shim.interceptedArgs.mode); + t.equal('hello', shim.interceptedArgs.arguments[0]); + t.end(); + }); + }) + + t.teardown(() => { + cp.execSync('docker rm -f csec_memcache'); + }) + + +}) +