From b36eba18c4cf7464cc0af5d7b7da3501f29316bb Mon Sep 17 00:00:00 2001 From: Sumit Suthar Date: Fri, 5 May 2023 12:51:09 +0530 Subject: [PATCH 1/4] Support for cassandra db event generation --- .../core/event-constants.js | 1 + .../hooks/cassandra/nr-cassandra-driver.js | 117 ++++++++++++++++++ lib/instrumentation-security/index.js | 8 ++ 3 files changed, 126 insertions(+) create mode 100644 lib/instrumentation-security/hooks/cassandra/nr-cassandra-driver.js diff --git a/lib/instrumentation-security/core/event-constants.js b/lib/instrumentation-security/core/event-constants.js index d85932fd..ab27a64d 100644 --- a/lib/instrumentation-security/core/event-constants.js +++ b/lib/instrumentation-security/core/event-constants.js @@ -28,6 +28,7 @@ const EVENT_CATEGORY = { ORACLE: 'ORACLE', MONGO: 'MONGO', MSSQL: 'MSSQL', + CASSANDRA: 'CASSANDRA', SQLITE: 'SQLITE', FILE: 'FILE', SYS: 'SYSTEM', diff --git a/lib/instrumentation-security/hooks/cassandra/nr-cassandra-driver.js b/lib/instrumentation-security/hooks/cassandra/nr-cassandra-driver.js new file mode 100644 index 00000000..9a6a4f78 --- /dev/null +++ b/lib/instrumentation-security/hooks/cassandra/nr-cassandra-driver.js @@ -0,0 +1,117 @@ +/* + * Copyright 2020 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +const requestManager = require('../../core/request-manager'); +const semver = require('semver') +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'); + +module.exports = function initialize(shim, cassandra, moduleName) { + const cassandraVersion = shim.require('./package.json').version + logger.info('Instrumenting ' + moduleName, cassandraVersion) + const ClientProto = cassandra.Client.prototype + if (semver.satisfies(cassandraVersion, '>=4.4.0')) { + executeHook(shim, ClientProto, '_execute'); + } else { + executeHook(shim, ClientProto, '_innerExecute'); + } + executeHook(shim, ClientProto, 'batch'); +} + +/** + * Utility to create parameters from intercepted arguments + * @param {*} args + * @returns + */ +function extractQueryArgs(args) { + let query = args[0]; + let params = args[1]; + return { + query: query, + parameters: params + } +} + +/** + * Utility to create parameters from intercepted arguments + * @param {*} args + * @returns + */ +function findBatchQueryArg(args) { + const sql = (args[0] && args[0][0]) || '' + let query = sql.query || sql; + let params = []; + return { + query: query, + parameters: params + } +} + + +/** + * Wrapper to hook _execute, _innerExecute and batch method + * @param {*} shim + * @param {*} mod + * @param {*} method + */ +function executeHook(shim, mod, method) { + shim.wrap(mod, method, function makeQueryWrapper(shim, fn) { + return function queryWrapper() { + let parameters; + if (method === 'batch') { + parameters = findBatchQueryArg(arguments); + } + else { + parameters = extractQueryArgs(arguments); + } + shim.interceptedArgs = parameters; + const request = requestManager.getRequest(shim); + if (request) { + const traceObject = secUtils.getTraceObject(shim); + traceObject.sourceMethod = method; + const secMetadata = securityMetaData.getSecurityMetaData(request, parameters, traceObject, secUtils.getExecutionId(), EVENT_TYPE.DB_COMMAND, EVENT_CATEGORY.CASSANDRA) + const secEvent = API.generateSecEvent(secMetadata); + API.sendEvent(secEvent); + if (request.headers[NR_CSEC_FUZZ_REQUEST_ID]) { + callbackHook(shim, arguments, arguments.length - 1, secEvent); + } + } + return fn.apply(this, arguments); + }; + }); +} + +/** + * Callback hook to generate exit event + * @param {*} shim + * @param {*} mod + * @param {*} fun + * @param {*} secEvent + */ +function callbackHook(shim, mod, fun, secEvent) { + shim.secEvent = secEvent; + shim.wrap(mod, fun, function callbackWrapper(shim, fn) { + if (!shim.isFunction(fn)) { + return fn; + } + return function wrapper() { + if ((arguments[0] === null || arguments[0] === undefined) && shim.secEvent) { + API.generateExitEvent(shim.secEvent); + delete shim.secEvent; + } + return fn.apply(this, arguments); + } + }) +} + + + + diff --git a/lib/instrumentation-security/index.js b/lib/instrumentation-security/index.js index 4d331408..aaf8f94c 100644 --- a/lib/instrumentation-security/index.js +++ b/lib/instrumentation-security/index.js @@ -162,6 +162,14 @@ newrelic.instrumentWebframework({ } }) +newrelic.instrumentDatastore({ + moduleName: 'cassandra-driver', + onRequire: require('./hooks/cassandra/nr-cassandra-driver'), + onError: function intrumentErrorHandler(err) { + logger.error(err.message, err.stack) + } +}) + From b7b3c7b5c393e06daf411b6676fa1cc51bad8e78 Mon Sep 17 00:00:00 2001 From: Sumit Suthar Date: Fri, 11 Aug 2023 11:57:37 +0530 Subject: [PATCH 2/4] handling for null params --- .../hooks/cassandra/nr-cassandra-driver.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/instrumentation-security/hooks/cassandra/nr-cassandra-driver.js b/lib/instrumentation-security/hooks/cassandra/nr-cassandra-driver.js index 9a6a4f78..6e77e906 100644 --- a/lib/instrumentation-security/hooks/cassandra/nr-cassandra-driver.js +++ b/lib/instrumentation-security/hooks/cassandra/nr-cassandra-driver.js @@ -36,7 +36,7 @@ function extractQueryArgs(args) { let params = args[1]; return { query: query, - parameters: params + parameters: params ? params : [] } } From 4242a17b1462d5ba005dddd49859acd3bd9b5e39 Mon Sep 17 00:00:00 2001 From: Sumit Suthar Date: Mon, 14 Aug 2023 16:23:26 +0530 Subject: [PATCH 3/4] minor fix --- lib/instrumentation-security/core/constants.js | 1 + .../hooks/cassandra/nr-cassandra-driver.js | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/instrumentation-security/core/constants.js b/lib/instrumentation-security/core/constants.js index 8178510b..c08cd34b 100644 --- a/lib/instrumentation-security/core/constants.js +++ b/lib/instrumentation-security/core/constants.js @@ -61,5 +61,6 @@ module.exports = { UNDEFINED: 'undefined', SELF_FD_PATH: '/proc/self/fd/', NR_CSEC_FUZZ_REQUEST_ID: 'nr-csec-fuzz-request-id', + BATCH: 'batch' }; diff --git a/lib/instrumentation-security/hooks/cassandra/nr-cassandra-driver.js b/lib/instrumentation-security/hooks/cassandra/nr-cassandra-driver.js index 6e77e906..a45263f8 100644 --- a/lib/instrumentation-security/hooks/cassandra/nr-cassandra-driver.js +++ b/lib/instrumentation-security/hooks/cassandra/nr-cassandra-driver.js @@ -12,7 +12,7 @@ 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 { NR_CSEC_FUZZ_REQUEST_ID, EMPTY_STRING, BATCH } = require('../../core/constants'); module.exports = function initialize(shim, cassandra, moduleName) { const cassandraVersion = shim.require('./package.json').version @@ -46,7 +46,7 @@ function extractQueryArgs(args) { * @returns */ function findBatchQueryArg(args) { - const sql = (args[0] && args[0][0]) || '' + const sql = (args[0] && args[0][0]) || EMPTY_STRING; let query = sql.query || sql; let params = []; return { @@ -66,7 +66,7 @@ function executeHook(shim, mod, method) { shim.wrap(mod, method, function makeQueryWrapper(shim, fn) { return function queryWrapper() { let parameters; - if (method === 'batch') { + if (method === BATCH) { parameters = findBatchQueryArg(arguments); } else { From d1fc15d6206f2027e23f38a1df7ea68cd0bbf4a3 Mon Sep 17 00:00:00 2001 From: pratik-k2 <105849408+pratik-k2@users.noreply.github.com> Date: Mon, 9 Oct 2023 10:00:57 +0530 Subject: [PATCH 4/4] test: cassandra-driver unit tests cases (#104) Co-authored-by: Sumit Suthar --- package-lock.json | 35 ++++++++ package.json | 1 + .../nr-cassandra-driver.test.js | 86 +++++++++++++++++++ 3 files changed, 122 insertions(+) create mode 100644 test/instrumentation-security/nr-cassandra-driver.test.js diff --git a/package-lock.json b/package-lock.json index 7b5ad059..35baead4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5173,6 +5173,12 @@ "negotiator": "0.6.3" } }, + "adm-zip": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.10.tgz", + "integrity": "sha512-x0HvcHqVJNTPk/Bw8JbLWlWoo6Wwnsug0fnYYro1HBrjxZ3G7/AZk7Ahv8JwDe1uIcz8eBqvu86FuF1POiG7vQ==", + "dev": true + }, "agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", @@ -5598,6 +5604,35 @@ "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==" }, + "cassandra-driver": { + "version": "4.7.2", + "resolved": "https://registry.npmjs.org/cassandra-driver/-/cassandra-driver-4.7.2.tgz", + "integrity": "sha512-gwl1DeYvL8Wy3i1GDMzFtpUg5G473fU7EnHFZj7BUtdLB7loAfgZgB3zBhROc9fbaDSUDs6YwOPPojS5E1kbSA==", + "dev": true, + "requires": { + "@types/long": "~5.0.0", + "@types/node": ">=8", + "adm-zip": "~0.5.10", + "long": "~5.2.3" + }, + "dependencies": { + "@types/long": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/long/-/long-5.0.0.tgz", + "integrity": "sha512-eQs9RsucA/LNjnMoJvWG/nXa7Pot/RbBzilF/QRIU/xRl+0ApxrSUFsV5lmf01SvSlqMzJ7Zwxe440wmz2SJGA==", + "dev": true, + "requires": { + "long": "*" + } + }, + "long": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", + "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==", + "dev": true + } + } + }, "chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", diff --git a/package.json b/package.json index 3c9733aa..4e2b1f74 100755 --- a/package.json +++ b/package.json @@ -65,6 +65,7 @@ "@newrelic/test-utilities": "7.2.1", "@typescript-eslint/eslint-plugin": "^4.15.0", "@typescript-eslint/parser": "^4.15.0", + "cassandra-driver": "^4.7.2", "director": "^1.2.8", "eslint-config-prettier": "^8.3.0", "eslint-config-standard": "^16.0.3", diff --git a/test/instrumentation-security/nr-cassandra-driver.test.js b/test/instrumentation-security/nr-cassandra-driver.test.js new file mode 100644 index 00000000..4d6cfeb6 --- /dev/null +++ b/test/instrumentation-security/nr-cassandra-driver.test.js @@ -0,0 +1,86 @@ +/* + * 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 sinon = require('sinon'); +const CassandraDriver = require('cassandra-driver'); +const { Client, types } = CassandraDriver; + +const client = new Client({ + contactPoints: ['h1', 'h2'], + localDataCenter: 'datacenter1', + keyspace: 'ks1' +}); + +const _executeStub = sinon.stub(Client.prototype, '_execute'); +_executeStub.returns({ rows: [] }); + +test('cassandra-driver', (t) => { + t.autoend(); + let helper = null; + let initialize = null; + let shim = null; + let requireStub = null; + + t.beforeEach(() => { + helper = utils.TestAgent.makeInstrumented() + shim = helper.getShim(); + requireStub = sinon.stub(shim, 'require'); + requireStub.returns({ version: '4.5.6' }); + initialize = require('../../lib/instrumentation-security/hooks/cassandra/nr-cassandra-driver'); + initialize(shim, CassandraDriver, 'cassandra-driver'); + }) + + t.afterEach(() => { + helper && helper.unload() + }) + + t.test('should perform a successful query', async (t) => { + const expectedResult = { rows: [{ name: 'John', email: 'john@test.com' }] }; + _executeStub.resolves(expectedResult); + + const query = 'SELECT name, email FROM users WHERE key = ?'; + const result = await client.execute(query, ['someone']); + t.equal(result.rows[0].email, expectedResult.rows[0].email); + }); + + t.test('should perform a successful query when version < 4.4.0', async (t) => { + requireStub.returns({ version: '4.2.6' }); + initialize(shim, CassandraDriver, 'cassandra-driver'); + const expectedResult = { rows: [{ name: 'John', email: 'john@test.com' }] }; + _executeStub.resolves(expectedResult); + + const query = 'SELECT name, email FROM users WHERE key = ?'; + const result = await client.execute(query, ['someone']); + t.equal(result.rows[0].email, expectedResult.rows[0].email); + }); + + t.test('should perform a successful batch query', async (t) => { + _executeStub.restore(); + initialize(shim, CassandraDriver, 'cassandra-driver'); + const expectedResult = { rows: [{ name: 'John123', email: 'john@test.com' }] }; + sinon.stub(Client.prototype, 'batch').resolves(expectedResult); + + const queries = [ + { + query: 'SELECT name, email FROM users WHERE key = ?', + params: [1] + } + ]; + + const batch = { + queries: queries, + consistency: types.consistencies.quorum, + serialConsistency: types.consistencies.localSerial, + timestamp: Date.now() + }; + + const result = await client.batch(batch, { prepare: true }); + console.log(result) + t.equal(result.rows[0].email, expectedResult.rows[0].email); + }); +}) \ No newline at end of file