Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/instrumentation-security/core/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'

};
1 change: 1 addition & 0 deletions lib/instrumentation-security/core/event-constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const EVENT_CATEGORY = {
ORACLE: 'ORACLE',
MONGO: 'MONGO',
MSSQL: 'MSSQL',
CASSANDRA: 'CASSANDRA',
SQLITE: 'SQLITE',
FILE: 'FILE',
SYS: 'SYSTEM',
Expand Down
117 changes: 117 additions & 0 deletions lib/instrumentation-security/hooks/cassandra/nr-cassandra-driver.js
Original file line number Diff line number Diff line change
@@ -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, EMPTY_STRING, BATCH } = 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 ? params : []
}
}

/**
* Utility to create parameters from intercepted arguments
* @param {*} args
* @returns
*/
function findBatchQueryArg(args) {
const sql = (args[0] && args[0][0]) || EMPTY_STRING;
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);
}
})
}




7 changes: 7 additions & 0 deletions lib/instrumentation-security/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,13 @@ newrelic.instrumentWebframework({
}
})

newrelic.instrumentDatastore({
moduleName: 'cassandra-driver',
onRequire: require('./hooks/cassandra/nr-cassandra-driver'),
onError: function intrumentErrorHandler(err) {
logger.error(err.message, err.stack)
}
})



Expand Down
35 changes: 35 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
86 changes: 86 additions & 0 deletions test/instrumentation-security/nr-cassandra-driver.test.js
Original file line number Diff line number Diff line change
@@ -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);
});
})