From 04be9d277f741fee905a240e8073edb8b03f907a Mon Sep 17 00:00:00 2001 From: Abhilash Date: Mon, 23 Feb 2026 17:20:12 +0530 Subject: [PATCH 1/4] feat: instrumented pg bind variables --- .../currencies/databases/pg/app.js | 13 ++++++++ .../currencies/databases/pg/test_base.js | 30 +++++++++++++++++++ .../tracing/instrumentation/databases/pg.js | 17 +++++++++++ 3 files changed, 60 insertions(+) diff --git a/packages/collector/test/integration/currencies/databases/pg/app.js b/packages/collector/test/integration/currencies/databases/pg/app.js index 2843101905..f31b5ff666 100644 --- a/packages/collector/test/integration/currencies/databases/pg/app.js +++ b/packages/collector/test/integration/currencies/databases/pg/app.js @@ -105,6 +105,19 @@ app.get('/parameterized-query', async (req, res) => { res.json({}); }); +app.get('/bind-variables-test', async (req, res) => { + // Test with string query and array parameters + await client.query('SELECT * FROM users WHERE name = $1 AND email = $2', ['testuser', 'test@example.com']); + + // Test with config object containing values + await pool.query({ + text: 'INSERT INTO users(name, email) VALUES($1, $2) RETURNING *', + values: ['bindtest', 'bindtest@example.com'] + }); + + res.json({ success: true }); +}); + app.get('/pool-string-insert', (req, res) => { const insert = 'INSERT INTO users(name, email) VALUES($1, $2) RETURNING *'; const values = ['beaker', 'beaker@muppets.com']; diff --git a/packages/collector/test/integration/currencies/databases/pg/test_base.js b/packages/collector/test/integration/currencies/databases/pg/test_base.js index b74fe336c5..17e142b255 100644 --- a/packages/collector/test/integration/currencies/databases/pg/test_base.js +++ b/packages/collector/test/integration/currencies/databases/pg/test_base.js @@ -65,6 +65,36 @@ module.exports = function (name, version, isLatest) { ) )); + it('must collect bind variables from parameterized queries', () => + controls + .sendRequest({ + method: 'GET', + path: '/bind-variables-test' + }) + .then(() => + retry(() => + agentControls.getSpans().then(spans => { + verifyHttpEntry(spans, '/bind-variables-test'); + + // Verify first query with string and array parameters + const selectQuery = getSpansByName(spans, 'postgres').find( + span => span.data.pg.stmt === 'SELECT * FROM users WHERE name = $1 AND email = $2' + ); + expect(selectQuery).to.exist; + expect(selectQuery.data.pg.bindValues).to.exist; + expect(selectQuery.data.pg.bindValues).to.deep.equal(['testuser', 'test@example.com']); + + // Verify second query with config object containing values + const insertQuery = getSpansByName(spans, 'postgres').find( + span => span.data.pg.stmt === 'INSERT INTO users(name, email) VALUES($1, $2) RETURNING *' + ); + expect(insertQuery).to.exist; + expect(insertQuery.data.pg.bindValues).to.exist; + expect(insertQuery.data.pg.bindValues).to.deep.equal(['bindtest', 'bindtest@example.com']); + }) + ) + )); + it('must trace pooled select now', () => controls .sendRequest({ diff --git a/packages/core/src/tracing/instrumentation/databases/pg.js b/packages/core/src/tracing/instrumentation/databases/pg.js index 839724eb02..7dae1adc22 100644 --- a/packages/core/src/tracing/instrumentation/databases/pg.js +++ b/packages/core/src/tracing/instrumentation/databases/pg.js @@ -58,6 +58,19 @@ function instrumentedQuery(ctx, originalQuery, argsForOriginalQuery) { kind: constants.EXIT }); span.stack = tracingUtil.getStackTrace(instrumentedQuery); + + // Extract bind variables/parameters + let bindValues; + if (typeof config === 'string') { + // Query is a string, parameters might be in argsForOriginalQuery[1] + if (argsForOriginalQuery.length > 1 && Array.isArray(argsForOriginalQuery[1])) { + bindValues = argsForOriginalQuery[1]; + } + } else if (config && config.values) { + // Query config object with values property + bindValues = config.values; + } + span.data.pg = { stmt: tracingUtil.shortenDatabaseStatement(typeof config === 'string' ? config : config.text), host, @@ -66,6 +79,10 @@ function instrumentedQuery(ctx, originalQuery, argsForOriginalQuery) { db }; + if (bindValues && bindValues.length > 0) { + span.data.pg.bindValues = bindValues; + } + let originalCallback; let callbackIndex = -1; for (let i = 1; i < argsForOriginalQuery.length; i++) { From 4ce1c18156e470d793f57a91de97be550b16a9f8 Mon Sep 17 00:00:00 2001 From: Abhilash Date: Wed, 4 Mar 2026 12:09:57 +0530 Subject: [PATCH 2/4] chore: update with partial hiding wip --- .../currencies/databases/pg/app.js | 92 +++++- .../currencies/databases/pg/test_base.js | 191 +++++++++++- .../tracing/instrumentation/databases/pg.js | 11 +- packages/core/src/tracing/tracingUtil.js | 146 +++++++++ .../tracingUtil_maskBindVariables_test.js | 285 ++++++++++++++++++ 5 files changed, 714 insertions(+), 11 deletions(-) create mode 100644 packages/core/test/tracing/tracingUtil_maskBindVariables_test.js diff --git a/packages/collector/test/integration/currencies/databases/pg/app.js b/packages/collector/test/integration/currencies/databases/pg/app.js index f31b5ff666..ab16dbb098 100644 --- a/packages/collector/test/integration/currencies/databases/pg/app.js +++ b/packages/collector/test/integration/currencies/databases/pg/app.js @@ -49,6 +49,22 @@ pool.query(createTableQuery, err => { } }); +// Create a stored procedure for testing +const createProcedureQuery = ` + CREATE OR REPLACE FUNCTION get_user_by_name(user_name VARCHAR) + RETURNS TABLE(id INT, name VARCHAR, email VARCHAR) AS $$ + BEGIN + RETURN QUERY SELECT users.id, users.name, users.email FROM users WHERE users.name = user_name; + END; + $$ LANGUAGE plpgsql; +`; + +pool.query(createProcedureQuery, err => { + if (err) { + log('Failed to create stored procedure', err); + } +}); + if (process.env.WITH_STDOUT) { app.use(morgan(`${logPrefix}:method :url :status`)); } @@ -107,7 +123,7 @@ app.get('/parameterized-query', async (req, res) => { app.get('/bind-variables-test', async (req, res) => { // Test with string query and array parameters - await client.query('SELECT * FROM users WHERE name = $1 AND email = $2', ['testuser', 'test@example.com']); + await client.query('SELECT * FROM users WHERE name = testuser AND email = test@example.com'); // Test with config object containing values await pool.query({ @@ -118,6 +134,80 @@ app.get('/bind-variables-test', async (req, res) => { res.json({ success: true }); }); +app.get('/stored-procedure-test', async (req, res) => { + // First insert a test user + await client.query('INSERT INTO users(name, email) VALUES($1, $2) ON CONFLICT DO NOTHING', [ + 'proceduretest', + 'procedure@example.com' + ]); + + // Call stored procedure with bind variable + const result = await client.query('SELECT * FROM get_user_by_name($1)', ['proceduretest']); + + res.json({ success: true, rows: result.rows }); +}); + +app.get('/all-data-types-test', async (req, res) => { + // Test with various data types to demonstrate masking + + // 1. String values + await client.query('SELECT $1::text as string_value', ['sensitive_password_123']); + + // 2. Number values (integer and float) + await client.query('SELECT $1::integer as int_value, $2::numeric as float_value', [42, 3.14159]); + + // 3. Boolean value + await client.query('SELECT $1::boolean as bool_value', [true]); + + // 4. null and undefined (null in SQL) + await client.query('SELECT $1 as null_value', [null]); + + // 5. Date object + await client.query('SELECT $1::timestamp as date_value', [new Date('2024-01-15T10:30:00Z')]); + + // 6. JSON object + await client.query('SELECT $1::jsonb as json_value', [ + JSON.stringify({ user: 'john', email: 'john@example.com', preferences: { theme: 'dark', notifications: true } }) + ]); + + // 7. Array (as JSON string for PostgreSQL) + await client.query('SELECT $1::jsonb as array_value', [JSON.stringify([1, 2, 3, 4, 5])]); + + // 8. Nested JSON with arrays + await client.query('SELECT $1::jsonb as nested_value', [ + JSON.stringify({ + users: [ + { id: 1, name: 'Alice', email: 'alice@example.com' }, + { id: 2, name: 'Bob', email: 'bob@example.com' } + ], + metadata: { created: '2024-01-01', version: 1 } + }) + ]); + + // 9. Buffer/Binary data (bytea in PostgreSQL) + const imageBuffer = Buffer.from([0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46]); // JPEG header + await client.query('SELECT $1::bytea as binary_value', [imageBuffer]); + + // 10. Large buffer (simulating file upload) + const largeBuffer = Buffer.alloc(1024); // 1KB buffer + await client.query('SELECT $1::bytea as large_binary', [largeBuffer]); + + // 11. Mixed types in single query + await client.query('SELECT $1::text, $2::integer, $3::boolean, $4::jsonb, $5::bytea', [ + 'user@example.com', + 12345, + false, + JSON.stringify({ key: 'value' }), + Buffer.from('secret') + ]); + + res.json({ + success: true, + message: 'All data types tested', + note: 'Check spans to see masked bind variables' + }); +}); + app.get('/pool-string-insert', (req, res) => { const insert = 'INSERT INTO users(name, email) VALUES($1, $2) RETURNING *'; const values = ['beaker', 'beaker@muppets.com']; diff --git a/packages/collector/test/integration/currencies/databases/pg/test_base.js b/packages/collector/test/integration/currencies/databases/pg/test_base.js index 17e142b255..5dcfb8525d 100644 --- a/packages/collector/test/integration/currencies/databases/pg/test_base.js +++ b/packages/collector/test/integration/currencies/databases/pg/test_base.js @@ -77,20 +77,201 @@ module.exports = function (name, version, isLatest) { verifyHttpEntry(spans, '/bind-variables-test'); // Verify first query with string and array parameters - const selectQuery = getSpansByName(spans, 'postgres').find( + let selectQuery = getSpansByName(spans, 'postgres'); + + console.log('SPAN SELECT QUERY: ', selectQuery[0].data); + + selectQuery = selectQuery.find( span => span.data.pg.stmt === 'SELECT * FROM users WHERE name = $1 AND email = $2' ); expect(selectQuery).to.exist; - expect(selectQuery.data.pg.bindValues).to.exist; - expect(selectQuery.data.pg.bindValues).to.deep.equal(['testuser', 'test@example.com']); + expect(selectQuery.data.pg.params).to.exist; + expect(selectQuery.data.pg.params).to.be.an('array'); + expect(selectQuery.data.pg.params).to.have.lengthOf(2); + // Verify values are masked (first 2 and last 2 chars visible, exact length preserved) + // 'testuser' (8 chars) -> 'te****er' + expect(selectQuery.data.pg.params[0]).to.equal('te****er'); + expect(selectQuery.data.pg.params[0]).to.have.lengthOf(8); + // 'test@example.com' (16 chars) -> 'te************om' + expect(selectQuery.data.pg.params[1]).to.equal('te************om'); + expect(selectQuery.data.pg.params[1]).to.have.lengthOf(16); // Verify second query with config object containing values const insertQuery = getSpansByName(spans, 'postgres').find( span => span.data.pg.stmt === 'INSERT INTO users(name, email) VALUES($1, $2) RETURNING *' ); expect(insertQuery).to.exist; - expect(insertQuery.data.pg.bindValues).to.exist; - expect(insertQuery.data.pg.bindValues).to.deep.equal(['bindtest', 'bindtest@example.com']); + expect(insertQuery.data.pg.params).to.exist; + expect(insertQuery.data.pg.params).to.be.an('array'); + expect(insertQuery.data.pg.params).to.have.lengthOf(2); + // Verify values are masked with exact length preserved + // 'bindtest' (8 chars) -> 'bi****st' + expect(insertQuery.data.pg.params[0]).to.equal('bi****st'); + expect(insertQuery.data.pg.params[0]).to.have.lengthOf(8); + // 'bindtest@example.com' (20 chars) -> 'bi****************om' + expect(insertQuery.data.pg.params[1]).to.equal('bi****************om'); + expect(insertQuery.data.pg.params[1]).to.have.lengthOf(20); + }) + ) + )); + + it('must collect bind variables when calling stored procedures', () => + controls + .sendRequest({ + method: 'GET', + path: '/stored-procedure-test' + }) + .then(() => + retry(() => + agentControls.getSpans().then(spans => { + verifyHttpEntry(spans, '/stored-procedure-test'); + + // Verify INSERT query with bind variables + const insertQuery = getSpansByName(spans, 'postgres').find( + span => span.data.pg.stmt && span.data.pg.stmt.includes('INSERT INTO users(name, email) VALUES($1, $2)') + ); + expect(insertQuery).to.exist; + expect(insertQuery.data.pg.params).to.exist; + expect(insertQuery.data.pg.params).to.be.an('array'); + expect(insertQuery.data.pg.params).to.have.lengthOf(2); + // Verify values are masked with exact length preserved + // 'proceduretest' (13 chars) -> 'pr*********st' + expect(insertQuery.data.pg.params[0]).to.equal('pr*********st'); + expect(insertQuery.data.pg.params[0]).to.have.lengthOf(13); + // 'procedure@example.com' (21 chars) -> 'pr*****************om' + expect(insertQuery.data.pg.params[1]).to.equal('pr*****************om'); + expect(insertQuery.data.pg.params[1]).to.have.lengthOf(21); + + // Verify stored procedure call with bind variable + const procedureCall = getSpansByName(spans, 'postgres').find( + span => span.data.pg.stmt === 'SELECT * FROM get_user_by_name($1)' + ); + expect(procedureCall).to.exist; + expect(procedureCall.data.pg.params).to.exist; + expect(procedureCall.data.pg.params).to.be.an('array'); + expect(procedureCall.data.pg.params).to.have.lengthOf(1); + // Verify value is masked with exact length preserved + // 'proceduretest' (13 chars) -> 'pr*********st' + expect(procedureCall.data.pg.params[0]).to.equal('pr*********st'); + expect(procedureCall.data.pg.params[0]).to.have.lengthOf(13); + }) + ) + )); + + it('must collect and mask all data types correctly', () => + controls + .sendRequest({ + method: 'GET', + path: '/all-data-types-test' + }) + .then(() => + retry(() => + agentControls.getSpans().then(spans => { + verifyHttpEntry(spans, '/all-data-types-test'); + const pgSpans = getSpansByName(spans, 'postgres'); + + // 1. String value test + const stringQuery = pgSpans.find(span => span.data.pg.stmt.includes('string_value')); + expect(stringQuery).to.exist; + expect(stringQuery.data.pg.params).to.exist; + // 'sensitive_password_123' (23 chars) -> 'se*******************23' + expect(stringQuery.data.pg.params[0]).to.equal('se******************23'); + expect(stringQuery.data.pg.params[0]).to.have.lengthOf(22); + + // 2. Number values test + const numberQuery = pgSpans.find(span => span.data.pg.stmt.includes('int_value')); + expect(numberQuery).to.exist; + expect(numberQuery.data.pg.params).to.have.lengthOf(2); + // 42 -> '**' + expect(numberQuery.data.pg.params[0]).to.equal('**'); + // 3.14159 -> '3.***59' + expect(numberQuery.data.pg.params[1]).to.equal('3.***59'); + + // 3. Boolean value test + const boolQuery = pgSpans.find(span => span.data.pg.stmt.includes('bool_value')); + expect(boolQuery).to.exist; + // true -> 't**e' + expect(boolQuery.data.pg.params[0]).to.equal('t**e'); + + // 4. Null value test + const nullQuery = pgSpans.find(span => span.data.pg.stmt.includes('null_value')); + expect(nullQuery).to.exist; + expect(nullQuery.data.pg.params[0]).to.equal(''); + + // 5. Date value test + const dateQuery = pgSpans.find(span => span.data.pg.stmt.includes('date_value')); + expect(dateQuery).to.exist; + // Date ISO string is masked + expect(dateQuery.data.pg.params[0]).to.match(/^20\*+0Z$/); + expect(dateQuery.data.pg.params[0]).to.have.lengthOf(24); + + // 6. JSON object test + const jsonQuery = pgSpans.find(span => span.data.pg.stmt.includes('json_value')); + expect(jsonQuery).to.exist; + // JSON is now masked with structure preserved + const parsedJson = JSON.parse(jsonQuery.data.pg.params[0]); + expect(parsedJson).to.have.property('u**r', 'j**n'); + expect(parsedJson).to.have.property('em**l', 'jo**************om'); + expect(parsedJson).to.have.property('pr********s'); + expect(parsedJson['pr********s']).to.have.property('th**e', 'd**k'); + expect(parsedJson['pr********s']).to.have.property('no*********ns', 't**e'); + + // 7. Array test + const arrayQuery = pgSpans.find(span => span.data.pg.stmt.includes('array_value')); + expect(arrayQuery).to.exist; + // Array is now masked with structure preserved + const parsedArray = JSON.parse(arrayQuery.data.pg.params[0]); + expect(parsedArray).to.be.an('array'); + expect(parsedArray).to.have.lengthOf(5); + expect(parsedArray[0]).to.equal('1'); + expect(parsedArray[1]).to.equal('2'); + expect(parsedArray[2]).to.equal('3'); + expect(parsedArray[3]).to.equal('4'); + expect(parsedArray[4]).to.equal('5'); + + // 8. Nested JSON test + const nestedQuery = pgSpans.find(span => span.data.pg.stmt.includes('nested_value')); + expect(nestedQuery).to.exist; + // Complex nested JSON is now masked with structure preserved + const parsedNested = JSON.parse(nestedQuery.data.pg.params[0]); + expect(parsedNested).to.have.property('us**s'); + expect(parsedNested['us**s']).to.be.an('array'); + expect(parsedNested['us**s']).to.have.lengthOf(2); + expect(parsedNested['us**s'][0]).to.have.property('*d', '1'); + expect(parsedNested['us**s'][0]).to.have.property('n**e', 'Al**e'); + expect(parsedNested['us**s'][0]).to.have.property('em**l', 'al**************om'); + expect(parsedNested).to.have.property('me*****a'); + expect(parsedNested['me*****a']).to.have.property('cr****d', '20********01'); + expect(parsedNested['me*****a']).to.have.property('ve****n', '1'); + + // 9. Buffer/Binary data test + const binaryQuery = pgSpans.find(span => span.data.pg.stmt.includes('binary_value')); + expect(binaryQuery).to.exist; + // Buffer shown as size + expect(binaryQuery.data.pg.params[0]).to.equal(''); + + // 10. Large buffer test + const largeBinaryQuery = pgSpans.find(span => span.data.pg.stmt.includes('large_binary')); + expect(largeBinaryQuery).to.exist; + expect(largeBinaryQuery.data.pg.params[0]).to.equal(''); + + // 11. Mixed types in single query + const mixedQuery = pgSpans.find(span => + span.data.pg.stmt.includes('SELECT $1::text, $2::integer, $3::boolean, $4::jsonb, $5::bytea') + ); + expect(mixedQuery).to.exist; + expect(mixedQuery.data.pg.params).to.have.lengthOf(5); + // String: 'user@example.com' -> 'us************om' + expect(mixedQuery.data.pg.params[0]).to.equal('us************om'); + // Number: 12345 -> '1***5' + expect(mixedQuery.data.pg.params[1]).to.equal('1***5'); + // Boolean: false -> 'f***e' + expect(mixedQuery.data.pg.params[2]).to.equal('f***e'); + // JSON: '{"key":"value"}' is now masked with structure preserved + const parsedMixed = JSON.parse(mixedQuery.data.pg.params[3]); + expect(parsedMixed).to.have.property('k*y', 'va**e'); + // Buffer: '' + expect(mixedQuery.data.pg.params[4]).to.equal(''); }) ) )); diff --git a/packages/core/src/tracing/instrumentation/databases/pg.js b/packages/core/src/tracing/instrumentation/databases/pg.js index 7dae1adc22..e5b1bbb28a 100644 --- a/packages/core/src/tracing/instrumentation/databases/pg.js +++ b/packages/core/src/tracing/instrumentation/databases/pg.js @@ -60,15 +60,15 @@ function instrumentedQuery(ctx, originalQuery, argsForOriginalQuery) { span.stack = tracingUtil.getStackTrace(instrumentedQuery); // Extract bind variables/parameters - let bindValues; + let params; if (typeof config === 'string') { // Query is a string, parameters might be in argsForOriginalQuery[1] if (argsForOriginalQuery.length > 1 && Array.isArray(argsForOriginalQuery[1])) { - bindValues = argsForOriginalQuery[1]; + params = argsForOriginalQuery[1]; } } else if (config && config.values) { // Query config object with values property - bindValues = config.values; + params = config.values; } span.data.pg = { @@ -79,8 +79,9 @@ function instrumentedQuery(ctx, originalQuery, argsForOriginalQuery) { db }; - if (bindValues && bindValues.length > 0) { - span.data.pg.bindValues = bindValues; + // Add masked bind parameters to span data if present + if (params && params.length > 0) { + span.data.pg.params = tracingUtil.maskBindVariables(params); } let originalCallback; diff --git a/packages/core/src/tracing/tracingUtil.js b/packages/core/src/tracing/tracingUtil.js index 57b713f6a3..af628cea00 100644 --- a/packages/core/src/tracing/tracingUtil.js +++ b/packages/core/src/tracing/tracingUtil.js @@ -414,3 +414,149 @@ exports.handleUnexpectedReturnValue = function handleUnexpectedReturnValue(retur return true; }; + +/** + * Masks a single bind variable value to protect sensitive data. + * Strategy: + * - Preserves exact length of original value for strings + * - For length <= 3: mask completely with asterisks + * - For length 4-5: show first and last character + * - For length > 5: show first 2 and last 2 characters + * - Special handling for objects, arrays, buffers, and other types + * - For JSON objects/arrays: masks individual values while preserving structure + * + * @param {*} value - The bind variable value to mask + * @returns {string} - The masked value + */ +function maskBindValue(value) { + // Handle null and undefined + if (value === null) { + return ''; + } + if (value === undefined) { + return ''; + } + + // Handle Buffer (binary data like images, files) + if (Buffer.isBuffer(value)) { + return ``; + } + + // Handle Date objects + if (value instanceof Date) { + const dateStr = value.toISOString(); + return maskString(dateStr); + } + + // Handle Arrays + if (Array.isArray(value)) { + try { + return maskJsonStructure(value); + } catch (e) { + return ''; + } + } + + // Handle Objects (including JSON) + if (typeof value === 'object') { + try { + return maskJsonStructure(value); + } catch (e) { + // Handle circular references or non-serializable objects + return ''; + } + } + + // Handle primitive types (string, number, boolean, bigint, symbol) + return maskString(String(value)); +} + +/** + * Masks JSON objects and arrays by masking individual values while preserving structure. + * Example: {"theme":"dark","notifications":true} -> {"t***e":"d**k","no*********ns":"t**e"} + * @param {*} obj - The object or array to mask + * @returns {string} - JSON string with masked values + */ +function maskJsonStructure(obj) { + // Check for circular references + const seen = new WeakSet(); + + /** + * @param {*} value + * @returns {*} + */ + function maskRecursive(value) { + // Handle primitives + if (value === null) return null; + if (value === undefined) return null; + if (typeof value === 'string') return maskString(value); + if (typeof value === 'number') return maskString(String(value)); + if (typeof value === 'boolean') return maskString(String(value)); + + // Handle objects and arrays + if (typeof value === 'object') { + // Check for circular reference + if (seen.has(value)) { + throw new Error('Circular reference detected'); + } + seen.add(value); + + if (Array.isArray(value)) { + return value.map(item => maskRecursive(item)); + } + + /** @type {Record} */ + const masked = {}; + Object.keys(value).forEach(key => { + // Mask both keys and values + const maskedKey = maskString(key); + masked[maskedKey] = maskRecursive(value[key]); + }); + return masked; + } + + return value; + } + + const masked = maskRecursive(obj); + return JSON.stringify(masked); +} + +/** + * Masks a string value preserving its exact length. + * @param {string} strValue - The string to mask + * @returns {string} - The masked string + */ +function maskString(strValue) { + const len = strValue.length; + + // For very short values (0-3 chars), mask completely + if (len <= 3) { + return '*'.repeat(len); + } + + // For length 4-5: show first and last character, mask the middle + if (len <= 5) { + const numAsterisks = len - 2; + return strValue[0] + '*'.repeat(numAsterisks) + strValue[len - 1]; + } + + // For length > 5: show first 2 and last 2 characters, mask the middle + const numAsterisks = len - 4; + return strValue.substring(0, 2) + '*'.repeat(numAsterisks) + strValue.substring(len - 2); +} + +/** + * Masks an array of bind variable values to protect sensitive data. + * Each value is masked individually with appropriate handling for different types. + * + * @param {Array<*>} params - Array of bind variable values + * @returns {Array} - Array of masked values + */ +exports.maskBindVariables = function maskBindVariables(params) { + if (!Array.isArray(params)) { + return params; + } + + return params.map(maskBindValue); +}; diff --git a/packages/core/test/tracing/tracingUtil_maskBindVariables_test.js b/packages/core/test/tracing/tracingUtil_maskBindVariables_test.js new file mode 100644 index 0000000000..b85ff6dd90 --- /dev/null +++ b/packages/core/test/tracing/tracingUtil_maskBindVariables_test.js @@ -0,0 +1,285 @@ +/* + * (c) Copyright IBM Corp. 2024 + */ + +'use strict'; + +const expect = require('chai').expect; +const tracingUtil = require('../../src/tracing/tracingUtil'); + +describe('tracing/tracingUtil', () => { + describe('maskBindVariables', () => { + it('should preserve exact length and show first 2 and last 2 chars for long values', () => { + const params = ['testuser', 'password123', 'email@example.com']; + const masked = tracingUtil.maskBindVariables(params); + + expect(masked).to.be.an('array'); + expect(masked).to.have.lengthOf(3); + // 'testuser' (8 chars) -> 'te****er' + expect(masked[0]).to.equal('te****er'); + expect(masked[0]).to.have.lengthOf(8); + // 'password123' (11 chars) -> 'pa*******23' + expect(masked[1]).to.equal('pa*******23'); + expect(masked[1]).to.have.lengthOf(11); + // 'email@example.com' (17 chars) -> 'em***********om' + expect(masked[2]).to.equal('em***********om'); + expect(masked[2]).to.have.lengthOf(17); + }); + + it('should show first and last char for length 4-5', () => { + const params = ['test', 'hello']; + const masked = tracingUtil.maskBindVariables(params); + + expect(masked).to.be.an('array'); + expect(masked).to.have.lengthOf(2); + // 'test' (4 chars) -> 't**t' + expect(masked[0]).to.equal('t**t'); + expect(masked[0]).to.have.lengthOf(4); + // 'hello' (5 chars) -> 'h***o' + expect(masked[1]).to.equal('h***o'); + expect(masked[1]).to.have.lengthOf(5); + }); + + it('should mask short strings completely preserving length', () => { + const params = ['ab', 'xyz', 'a']; + const masked = tracingUtil.maskBindVariables(params); + + expect(masked).to.be.an('array'); + expect(masked).to.have.lengthOf(3); + expect(masked[0]).to.equal('**'); + expect(masked[0]).to.have.lengthOf(2); + expect(masked[1]).to.equal('***'); + expect(masked[1]).to.have.lengthOf(3); + expect(masked[2]).to.equal('*'); + expect(masked[2]).to.have.lengthOf(1); + }); + + it('should handle null and undefined values', () => { + const params = [null, undefined, 'value']; + const masked = tracingUtil.maskBindVariables(params); + + expect(masked).to.be.an('array'); + expect(masked).to.have.lengthOf(3); + expect(masked[0]).to.equal(''); + expect(masked[1]).to.equal(''); + // 'value' (5 chars) -> 'v***e' + expect(masked[2]).to.equal('v***e'); + expect(masked[2]).to.have.lengthOf(5); + }); + + it('should convert numbers to strings and mask them', () => { + const params = [12345, 42, 999999999, 3.14159, -100]; + const masked = tracingUtil.maskBindVariables(params); + + expect(masked).to.be.an('array'); + expect(masked).to.have.lengthOf(5); + // '12345' (5 chars) -> '1***5' + expect(masked[0]).to.equal('1***5'); + expect(masked[0]).to.have.lengthOf(5); + // '42' (2 chars) -> '**' + expect(masked[1]).to.equal('**'); + expect(masked[1]).to.have.lengthOf(2); + // '999999999' (9 chars) -> '99*****99' + expect(masked[2]).to.equal('99*****99'); + expect(masked[2]).to.have.lengthOf(9); + // '3.14159' (7 chars) -> '3.***59' + expect(masked[3]).to.equal('3.***59'); + expect(masked[3]).to.have.lengthOf(7); + // '-100' (4 chars) -> '-**0' + expect(masked[4]).to.equal('-**0'); + expect(masked[4]).to.have.lengthOf(4); + }); + + it('should handle boolean values', () => { + const params = [true, false]; + const masked = tracingUtil.maskBindVariables(params); + + expect(masked).to.be.an('array'); + expect(masked).to.have.lengthOf(2); + // 'true' (4 chars) -> 't**e' + expect(masked[0]).to.equal('t**e'); + expect(masked[0]).to.have.lengthOf(4); + // 'false' (5 chars) -> 'f***e' + expect(masked[1]).to.equal('f***e'); + expect(masked[1]).to.have.lengthOf(5); + }); + + it('should handle Date objects', () => { + const date = new Date('2024-01-15T10:30:00.000Z'); + const params = [date]; + const masked = tracingUtil.maskBindVariables(params); + + expect(masked).to.be.an('array'); + expect(masked).to.have.lengthOf(1); + // ISO string is masked: '2024-01-15T10:30:00.000Z' (24 chars) -> '20******************00Z' + expect(masked[0]).to.match(/^20\*+0Z$/); + expect(masked[0]).to.have.lengthOf(24); + }); + + it('should handle JSON objects', () => { + const params = [ + { name: 'John', age: 30 }, + { email: 'test@example.com', active: true } + ]; + const masked = tracingUtil.maskBindVariables(params); + + expect(masked).to.be.an('array'); + expect(masked).to.have.lengthOf(2); + + // Parse to verify it's valid JSON and check structure + const parsed1 = JSON.parse(masked[0]); + expect(parsed1).to.have.property('n**e', 'J**n'); + expect(parsed1).to.have.property('a*e', '30'); + + const parsed2 = JSON.parse(masked[1]); + expect(parsed2).to.have.property('em**l', 'te**************om'); + expect(parsed2).to.have.property('ac***e', 't**e'); + }); + + it('should handle arrays', () => { + const params = [ + [1, 2, 3], + ['a', 'b', 'c'], + [{ id: 1 }, { id: 2 }] + ]; + const masked = tracingUtil.maskBindVariables(params); + + expect(masked).to.be.an('array'); + expect(masked).to.have.lengthOf(3); + + // Parse to verify it's valid JSON and check structure + const parsed1 = JSON.parse(masked[0]); + expect(parsed1).to.be.an('array'); + expect(parsed1).to.have.lengthOf(3); + expect(parsed1[0]).to.equal('1'); + expect(parsed1[1]).to.equal('2'); + expect(parsed1[2]).to.equal('3'); + + const parsed2 = JSON.parse(masked[1]); + expect(parsed2).to.be.an('array'); + expect(parsed2).to.have.lengthOf(3); + expect(parsed2[0]).to.equal('*'); + expect(parsed2[1]).to.equal('*'); + expect(parsed2[2]).to.equal('*'); + + const parsed3 = JSON.parse(masked[2]); + expect(parsed3).to.be.an('array'); + expect(parsed3).to.have.lengthOf(2); + expect(parsed3[0]).to.have.property('*d', '1'); + expect(parsed3[1]).to.have.property('*d', '2'); + }); + + it('should handle Buffer (binary data)', () => { + const imageBuffer = Buffer.from([0xff, 0xd8, 0xff, 0xe0]); // JPEG header + const params = [imageBuffer]; + const masked = tracingUtil.maskBindVariables(params); + + expect(masked).to.be.an('array'); + expect(masked).to.have.lengthOf(1); + expect(masked[0]).to.equal(''); + }); + + it('should handle large Buffer', () => { + const largeBuffer = Buffer.alloc(1024 * 1024); // 1MB + const params = [largeBuffer]; + const masked = tracingUtil.maskBindVariables(params); + + expect(masked).to.be.an('array'); + expect(masked).to.have.lengthOf(1); + expect(masked[0]).to.equal(''); + }); + + it('should handle circular references in objects', () => { + const obj = { name: 'test' }; + obj.self = obj; // Create circular reference + const params = [obj]; + const masked = tracingUtil.maskBindVariables(params); + + expect(masked).to.be.an('array'); + expect(masked).to.have.lengthOf(1); + expect(masked[0]).to.equal(''); + }); + + it('should handle circular references in arrays', () => { + const arr = [1, 2, 3]; + arr.push(arr); // Create circular reference + const params = [arr]; + const masked = tracingUtil.maskBindVariables(params); + + expect(masked).to.be.an('array'); + expect(masked).to.have.lengthOf(1); + expect(masked[0]).to.equal(''); + }); + + it('should handle BigInt values', () => { + // eslint-disable-next-line no-undef + const params = [BigInt(9007199254740991), BigInt(123)]; + const masked = tracingUtil.maskBindVariables(params); + + expect(masked).to.be.an('array'); + expect(masked).to.have.lengthOf(2); + // '9007199254740991' (16 chars) -> '90************91' + expect(masked[0]).to.equal('90************91'); + expect(masked[0]).to.have.lengthOf(16); + // '123' (3 chars) -> '***' + expect(masked[1]).to.equal('***'); + expect(masked[1]).to.have.lengthOf(3); + }); + + it('should handle empty array', () => { + const params = []; + const masked = tracingUtil.maskBindVariables(params); + + expect(masked).to.be.an('array'); + expect(masked).to.have.lengthOf(0); + }); + + it('should return non-array input as-is', () => { + const notAnArray = 'not an array'; + const result = tracingUtil.maskBindVariables(notAnArray); + + expect(result).to.equal(notAnArray); + }); + + it('should preserve exact length for very long values', () => { + const longValue = 'a'.repeat(100); + const params = [longValue]; + const masked = tracingUtil.maskBindVariables(params); + + expect(masked).to.be.an('array'); + expect(masked).to.have.lengthOf(1); + // First 2 chars + 96 asterisks + last 2 chars = 100 chars + expect(masked[0]).to.have.lengthOf(100); + expect(masked[0]).to.match(/^aa\*{96}aa$/); + }); + + it('should handle mixed data types', () => { + const params = [ + 'string', + 123, + true, + null, + undefined, + { key: 'value' }, + [1, 2, 3], + new Date('2024-01-01'), + Buffer.from('test') + ]; + const masked = tracingUtil.maskBindVariables(params); + + expect(masked).to.be.an('array'); + expect(masked).to.have.lengthOf(9); + expect(masked[0]).to.equal('st**ng'); // string + expect(masked[1]).to.equal('***'); // 123 + expect(masked[2]).to.equal('t**e'); // true + expect(masked[3]).to.equal(''); + expect(masked[4]).to.equal(''); + expect(masked[5]).to.match(/^\{"\*+e\}$/); // JSON object + expect(masked[6]).to.equal('[1***3]'); // array + expect(masked[7]).to.match(/^20\*+0Z$/); // Date + expect(masked[8]).to.equal(''); // Buffer + }); + }); +}); + +// Made with Bob From 212aeea029e0b7c07686048d18bad1f2899db480 Mon Sep 17 00:00:00 2001 From: Abhilash Date: Thu, 25 Jun 2026 11:57:54 +0530 Subject: [PATCH 3/4] chore: config parsing --- packages/core/src/config/index.js | 28 ++++++++- .../core/test/config/normalizeConfig_test.js | 59 +++++++++++++++++++ 2 files changed, 86 insertions(+), 1 deletion(-) diff --git a/packages/core/src/config/index.js b/packages/core/src/config/index.js index 3b862be2ef..9db1fdea2a 100644 --- a/packages/core/src/config/index.js +++ b/packages/core/src/config/index.js @@ -73,6 +73,7 @@ let currentConfig; * @property {boolean} [ignoreEndpointsDisableSuppression] * @property {boolean} [disableEOLEvents] * @property {globalStackTraceConfig} [global] + * @property {boolean} [captureBindVariables] */ /** @@ -160,7 +161,8 @@ let defaults = { }, ignoreEndpoints: {}, ignoreEndpointsDisableSuppression: false, - disableEOLEvents: false + disableEOLEvents: false, + captureBindVariables: false }, preloadOpentelemetry: false, secrets: { @@ -341,6 +343,7 @@ function normalizeTracingConfig({ userConfig = {}, defaultConfig = {}, finalConf normalizeIgnoreEndpoints({ userConfig, defaultConfig, finalConfig }); normalizeIgnoreEndpointsDisableSuppression({ userConfig, defaultConfig, finalConfig }); normalizeDisableEOLEvents({ userConfig, defaultConfig, finalConfig }); + normalizeCaptureBindVariables({ userConfig, defaultConfig, finalConfig }); } /** @@ -1074,6 +1077,29 @@ function normalizeDisableEOLEvents({ userConfig = {}, defaultConfig = {}, finalC }); } +/** + * @param {{ userConfig?: InstanaConfig|null, defaultConfig?: InstanaConfig, finalConfig?: InstanaConfig }} [options] + */ +function normalizeCaptureBindVariables({ userConfig = {}, defaultConfig = {}, finalConfig = {} } = {}) { + const { value, source } = util.resolve( + { + envValue: 'INSTANA_TRACING_BIND_VARIABLES', + inCodeValue: userConfig.tracing.captureBindVariables, + defaultValue: defaultConfig.tracing.captureBindVariables + }, + [validate.booleanValidator] + ); + + configStore.set('config.tracing.captureBindVariables', { source }); + finalConfig.tracing.captureBindVariables = value; + util.log({ + configPath: 'config.tracing.captureBindVariables', + source, + value, + envVarName: 'INSTANA_TRACING_BIND_VARIABLES' + }); +} + /** * @param {{ userConfig?: InstanaConfig|null, defaultConfig?: InstanaConfig, finalConfig?: InstanaConfig }} [options] */ diff --git a/packages/core/test/config/normalizeConfig_test.js b/packages/core/test/config/normalizeConfig_test.js index 5e27e2805a..c985b4c622 100644 --- a/packages/core/test/config/normalizeConfig_test.js +++ b/packages/core/test/config/normalizeConfig_test.js @@ -45,6 +45,7 @@ describe('config.normalizeConfig', () => { delete process.env.INSTANA_IGNORE_ENDPOINTS; delete process.env.INSTANA_IGNORE_ENDPOINTS_PATH; delete process.env.INSTANA_IGNORE_ENDPOINTS_DISABLE_SUPPRESSION; + delete process.env.INSTANA_TRACING_BIND_VARIABLES; } describe('default configuration', () => { @@ -2094,6 +2095,63 @@ describe('config.normalizeConfig', () => { }); }); + describe('captureBindVariables configuration', () => { + it('should default captureBindVariables to false', () => { + const config = coreConfig.normalize(); + expect(config.tracing.captureBindVariables).to.equal(false); + }); + + it('should use default (false) when neither env nor config is set', () => { + const config = coreConfig.normalize({}); + expect(config.tracing.captureBindVariables).to.be.false; + }); + + it('should enable captureBindVariables via config', () => { + const config = coreConfig.normalize({ userConfig: { tracing: { captureBindVariables: true } } }); + expect(config.tracing.captureBindVariables).to.equal(true); + }); + + it('should disable captureBindVariables via config', () => { + const config = coreConfig.normalize({ userConfig: { tracing: { captureBindVariables: false } } }); + expect(config.tracing.captureBindVariables).to.equal(false); + }); + + it('should enable captureBindVariables via INSTANA_TRACING_BIND_VARIABLES=true', () => { + process.env.INSTANA_TRACING_BIND_VARIABLES = 'true'; + const config = coreConfig.normalize(); + expect(config.tracing.captureBindVariables).to.equal(true); + }); + + it('should not enable captureBindVariables when INSTANA_TRACING_BIND_VARIABLES is not "true"', () => { + process.env.INSTANA_TRACING_BIND_VARIABLES = 'false'; + const config = coreConfig.normalize(); + expect(config.tracing.captureBindVariables).to.equal(false); + }); + + it('should default to false when INSTANA_TRACING_BIND_VARIABLES is set to an invalid value', () => { + process.env.INSTANA_TRACING_BIND_VARIABLES = 'invalid'; + const config = coreConfig.normalize(); + expect(config.tracing.captureBindVariables).to.equal(false); + }); + + it('should use config value when env is not set', () => { + const config = coreConfig.normalize({ userConfig: { tracing: { captureBindVariables: true } } }); + expect(config.tracing.captureBindVariables).to.be.true; + }); + + it('should give precedence to INSTANA_TRACING_BIND_VARIABLES env var over config', () => { + process.env.INSTANA_TRACING_BIND_VARIABLES = 'true'; + const config = coreConfig.normalize({ userConfig: { tracing: { captureBindVariables: false } } }); + expect(config.tracing.captureBindVariables).to.equal(true); + }); + + it('should give precedence to INSTANA_TRACING_BIND_VARIABLES=false over config=true', () => { + process.env.INSTANA_TRACING_BIND_VARIABLES = 'false'; + const config = coreConfig.normalize({ userConfig: { tracing: { captureBindVariables: true } } }); + expect(config.tracing.captureBindVariables).to.equal(false); + }); + }); + function checkDefaults(config) { expect(config).to.be.an('object'); @@ -2124,6 +2182,7 @@ describe('config.normalizeConfig', () => { expect(config.tracing.kafka.traceCorrelation).to.be.true; expect(config.tracing.useOpentelemetry).to.equal(true); expect(config.tracing.allowRootExitSpan).to.equal(false); + expect(config.tracing.captureBindVariables).to.equal(false); expect(config.preloadOpentelemetry).to.equal(false); From f301b4f84a92a5e6651d88c72eddf1bbdeb66c3a Mon Sep 17 00:00:00 2001 From: Abhilash Date: Thu, 25 Jun 2026 11:58:12 +0530 Subject: [PATCH 4/4] chore: updated based on config and cleanup masking --- .../currencies/databases/pg/app.js | 65 +--- .../currencies/databases/pg/app.mjs | 26 ++ .../currencies/databases/pg/test_base.js | 276 +++++------------ .../tracing/instrumentation/databases/pg.js | 32 +- packages/core/src/tracing/tracingUtil.js | 146 --------- .../tracingUtil_maskBindVariables_test.js | 285 ------------------ 6 files changed, 125 insertions(+), 705 deletions(-) delete mode 100644 packages/core/test/tracing/tracingUtil_maskBindVariables_test.js diff --git a/packages/collector/test/integration/currencies/databases/pg/app.js b/packages/collector/test/integration/currencies/databases/pg/app.js index ab16dbb098..796e23dd2e 100644 --- a/packages/collector/test/integration/currencies/databases/pg/app.js +++ b/packages/collector/test/integration/currencies/databases/pg/app.js @@ -122,8 +122,8 @@ app.get('/parameterized-query', async (req, res) => { }); app.get('/bind-variables-test', async (req, res) => { - // Test with string query and array parameters - await client.query('SELECT * FROM users WHERE name = testuser AND email = test@example.com'); + // Test with string query and positional array parameters + await client.query('SELECT * FROM users WHERE name = $1 AND email = $2', ['testuser', 'test@example.com']); // Test with config object containing values await pool.query({ @@ -147,67 +147,6 @@ app.get('/stored-procedure-test', async (req, res) => { res.json({ success: true, rows: result.rows }); }); -app.get('/all-data-types-test', async (req, res) => { - // Test with various data types to demonstrate masking - - // 1. String values - await client.query('SELECT $1::text as string_value', ['sensitive_password_123']); - - // 2. Number values (integer and float) - await client.query('SELECT $1::integer as int_value, $2::numeric as float_value', [42, 3.14159]); - - // 3. Boolean value - await client.query('SELECT $1::boolean as bool_value', [true]); - - // 4. null and undefined (null in SQL) - await client.query('SELECT $1 as null_value', [null]); - - // 5. Date object - await client.query('SELECT $1::timestamp as date_value', [new Date('2024-01-15T10:30:00Z')]); - - // 6. JSON object - await client.query('SELECT $1::jsonb as json_value', [ - JSON.stringify({ user: 'john', email: 'john@example.com', preferences: { theme: 'dark', notifications: true } }) - ]); - - // 7. Array (as JSON string for PostgreSQL) - await client.query('SELECT $1::jsonb as array_value', [JSON.stringify([1, 2, 3, 4, 5])]); - - // 8. Nested JSON with arrays - await client.query('SELECT $1::jsonb as nested_value', [ - JSON.stringify({ - users: [ - { id: 1, name: 'Alice', email: 'alice@example.com' }, - { id: 2, name: 'Bob', email: 'bob@example.com' } - ], - metadata: { created: '2024-01-01', version: 1 } - }) - ]); - - // 9. Buffer/Binary data (bytea in PostgreSQL) - const imageBuffer = Buffer.from([0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46]); // JPEG header - await client.query('SELECT $1::bytea as binary_value', [imageBuffer]); - - // 10. Large buffer (simulating file upload) - const largeBuffer = Buffer.alloc(1024); // 1KB buffer - await client.query('SELECT $1::bytea as large_binary', [largeBuffer]); - - // 11. Mixed types in single query - await client.query('SELECT $1::text, $2::integer, $3::boolean, $4::jsonb, $5::bytea', [ - 'user@example.com', - 12345, - false, - JSON.stringify({ key: 'value' }), - Buffer.from('secret') - ]); - - res.json({ - success: true, - message: 'All data types tested', - note: 'Check spans to see masked bind variables' - }); -}); - app.get('/pool-string-insert', (req, res) => { const insert = 'INSERT INTO users(name, email) VALUES($1, $2) RETURNING *'; const values = ['beaker', 'beaker@muppets.com']; diff --git a/packages/collector/test/integration/currencies/databases/pg/app.mjs b/packages/collector/test/integration/currencies/databases/pg/app.mjs index 774d060dec..c6556b50f8 100644 --- a/packages/collector/test/integration/currencies/databases/pg/app.mjs +++ b/packages/collector/test/integration/currencies/databases/pg/app.mjs @@ -104,6 +104,32 @@ app.get('/parameterized-query', async (req, res) => { res.json({}); }); +app.get('/bind-variables-test', async (req, res) => { + // Test with string query and positional array parameters + await client.query('SELECT * FROM users WHERE name = $1 AND email = $2', ['testuser', 'test@example.com']); + + // Test with config object containing values + await pool.query({ + text: 'INSERT INTO users(name, email) VALUES($1, $2) RETURNING *', + values: ['bindtest', 'bindtest@example.com'] + }); + + res.json({ success: true }); +}); + +app.get('/stored-procedure-test', async (req, res) => { + // First insert a test user + await client.query('INSERT INTO users(name, email) VALUES($1, $2) ON CONFLICT DO NOTHING', [ + 'proceduretest', + 'procedure@example.com' + ]); + + // Call stored procedure with bind variable + const result = await client.query('SELECT * FROM get_user_by_name($1)', ['proceduretest']); + + res.json({ success: true, rows: result.rows }); +}); + app.get('/pool-string-insert', (req, res) => { const insert = 'INSERT INTO users(name, email) VALUES($1, $2) RETURNING *'; const values = ['beaker', 'beaker@muppets.com']; diff --git a/packages/collector/test/integration/currencies/databases/pg/test_base.js b/packages/collector/test/integration/currencies/databases/pg/test_base.js index 5dcfb8525d..e7ed2ff5bb 100644 --- a/packages/collector/test/integration/currencies/databases/pg/test_base.js +++ b/packages/collector/test/integration/currencies/databases/pg/test_base.js @@ -65,7 +65,7 @@ module.exports = function (name, version, isLatest) { ) )); - it('must collect bind variables from parameterized queries', () => + it('must not capture bind variables by default', () => controls .sendRequest({ method: 'GET', @@ -75,206 +75,92 @@ module.exports = function (name, version, isLatest) { retry(() => agentControls.getSpans().then(spans => { verifyHttpEntry(spans, '/bind-variables-test'); - - // Verify first query with string and array parameters - let selectQuery = getSpansByName(spans, 'postgres'); - - console.log('SPAN SELECT QUERY: ', selectQuery[0].data); - - selectQuery = selectQuery.find( - span => span.data.pg.stmt === 'SELECT * FROM users WHERE name = $1 AND email = $2' - ); - expect(selectQuery).to.exist; - expect(selectQuery.data.pg.params).to.exist; - expect(selectQuery.data.pg.params).to.be.an('array'); - expect(selectQuery.data.pg.params).to.have.lengthOf(2); - // Verify values are masked (first 2 and last 2 chars visible, exact length preserved) - // 'testuser' (8 chars) -> 'te****er' - expect(selectQuery.data.pg.params[0]).to.equal('te****er'); - expect(selectQuery.data.pg.params[0]).to.have.lengthOf(8); - // 'test@example.com' (16 chars) -> 'te************om' - expect(selectQuery.data.pg.params[1]).to.equal('te************om'); - expect(selectQuery.data.pg.params[1]).to.have.lengthOf(16); - - // Verify second query with config object containing values - const insertQuery = getSpansByName(spans, 'postgres').find( - span => span.data.pg.stmt === 'INSERT INTO users(name, email) VALUES($1, $2) RETURNING *' - ); - expect(insertQuery).to.exist; - expect(insertQuery.data.pg.params).to.exist; - expect(insertQuery.data.pg.params).to.be.an('array'); - expect(insertQuery.data.pg.params).to.have.lengthOf(2); - // Verify values are masked with exact length preserved - // 'bindtest' (8 chars) -> 'bi****st' - expect(insertQuery.data.pg.params[0]).to.equal('bi****st'); - expect(insertQuery.data.pg.params[0]).to.have.lengthOf(8); - // 'bindtest@example.com' (20 chars) -> 'bi****************om' - expect(insertQuery.data.pg.params[1]).to.equal('bi****************om'); - expect(insertQuery.data.pg.params[1]).to.have.lengthOf(20); + const pgSpans = getSpansByName(spans, 'postgres'); + pgSpans.forEach(span => { + expect(span.data.pg.binds).to.not.exist; + }); }) ) )); - it('must collect bind variables when calling stored procedures', () => - controls - .sendRequest({ - method: 'GET', - path: '/stored-procedure-test' - }) - .then(() => - retry(() => - agentControls.getSpans().then(spans => { - verifyHttpEntry(spans, '/stored-procedure-test'); - - // Verify INSERT query with bind variables - const insertQuery = getSpansByName(spans, 'postgres').find( - span => span.data.pg.stmt && span.data.pg.stmt.includes('INSERT INTO users(name, email) VALUES($1, $2)') - ); - expect(insertQuery).to.exist; - expect(insertQuery.data.pg.params).to.exist; - expect(insertQuery.data.pg.params).to.be.an('array'); - expect(insertQuery.data.pg.params).to.have.lengthOf(2); - // Verify values are masked with exact length preserved - // 'proceduretest' (13 chars) -> 'pr*********st' - expect(insertQuery.data.pg.params[0]).to.equal('pr*********st'); - expect(insertQuery.data.pg.params[0]).to.have.lengthOf(13); - // 'procedure@example.com' (21 chars) -> 'pr*****************om' - expect(insertQuery.data.pg.params[1]).to.equal('pr*****************om'); - expect(insertQuery.data.pg.params[1]).to.have.lengthOf(21); - - // Verify stored procedure call with bind variable - const procedureCall = getSpansByName(spans, 'postgres').find( - span => span.data.pg.stmt === 'SELECT * FROM get_user_by_name($1)' - ); - expect(procedureCall).to.exist; - expect(procedureCall.data.pg.params).to.exist; - expect(procedureCall.data.pg.params).to.be.an('array'); - expect(procedureCall.data.pg.params).to.have.lengthOf(1); - // Verify value is masked with exact length preserved - // 'proceduretest' (13 chars) -> 'pr*********st' - expect(procedureCall.data.pg.params[0]).to.equal('pr*********st'); - expect(procedureCall.data.pg.params[0]).to.have.lengthOf(13); - }) - ) - )); + describe('with INSTANA_TRACING_BIND_VARIABLES=true', () => { + before(async () => { + await controls.stop(); + controls.env.INSTANA_TRACING_BIND_VARIABLES = 'true'; + await controls.startAndWaitForAgentConnection(5000, Date.now() + config.getTestTimeout()); + }); - it('must collect and mask all data types correctly', () => - controls - .sendRequest({ - method: 'GET', - path: '/all-data-types-test' - }) - .then(() => - retry(() => - agentControls.getSpans().then(spans => { - verifyHttpEntry(spans, '/all-data-types-test'); - const pgSpans = getSpansByName(spans, 'postgres'); + after(async () => { + await controls.stop(); + delete controls.env.INSTANA_TRACING_BIND_VARIABLES; + await controls.startAndWaitForAgentConnection(5000, Date.now() + config.getTestTimeout()); + }); - // 1. String value test - const stringQuery = pgSpans.find(span => span.data.pg.stmt.includes('string_value')); - expect(stringQuery).to.exist; - expect(stringQuery.data.pg.params).to.exist; - // 'sensitive_password_123' (23 chars) -> 'se*******************23' - expect(stringQuery.data.pg.params[0]).to.equal('se******************23'); - expect(stringQuery.data.pg.params[0]).to.have.lengthOf(22); - - // 2. Number values test - const numberQuery = pgSpans.find(span => span.data.pg.stmt.includes('int_value')); - expect(numberQuery).to.exist; - expect(numberQuery.data.pg.params).to.have.lengthOf(2); - // 42 -> '**' - expect(numberQuery.data.pg.params[0]).to.equal('**'); - // 3.14159 -> '3.***59' - expect(numberQuery.data.pg.params[1]).to.equal('3.***59'); - - // 3. Boolean value test - const boolQuery = pgSpans.find(span => span.data.pg.stmt.includes('bool_value')); - expect(boolQuery).to.exist; - // true -> 't**e' - expect(boolQuery.data.pg.params[0]).to.equal('t**e'); - - // 4. Null value test - const nullQuery = pgSpans.find(span => span.data.pg.stmt.includes('null_value')); - expect(nullQuery).to.exist; - expect(nullQuery.data.pg.params[0]).to.equal(''); - - // 5. Date value test - const dateQuery = pgSpans.find(span => span.data.pg.stmt.includes('date_value')); - expect(dateQuery).to.exist; - // Date ISO string is masked - expect(dateQuery.data.pg.params[0]).to.match(/^20\*+0Z$/); - expect(dateQuery.data.pg.params[0]).to.have.lengthOf(24); - - // 6. JSON object test - const jsonQuery = pgSpans.find(span => span.data.pg.stmt.includes('json_value')); - expect(jsonQuery).to.exist; - // JSON is now masked with structure preserved - const parsedJson = JSON.parse(jsonQuery.data.pg.params[0]); - expect(parsedJson).to.have.property('u**r', 'j**n'); - expect(parsedJson).to.have.property('em**l', 'jo**************om'); - expect(parsedJson).to.have.property('pr********s'); - expect(parsedJson['pr********s']).to.have.property('th**e', 'd**k'); - expect(parsedJson['pr********s']).to.have.property('no*********ns', 't**e'); - - // 7. Array test - const arrayQuery = pgSpans.find(span => span.data.pg.stmt.includes('array_value')); - expect(arrayQuery).to.exist; - // Array is now masked with structure preserved - const parsedArray = JSON.parse(arrayQuery.data.pg.params[0]); - expect(parsedArray).to.be.an('array'); - expect(parsedArray).to.have.lengthOf(5); - expect(parsedArray[0]).to.equal('1'); - expect(parsedArray[1]).to.equal('2'); - expect(parsedArray[2]).to.equal('3'); - expect(parsedArray[3]).to.equal('4'); - expect(parsedArray[4]).to.equal('5'); - - // 8. Nested JSON test - const nestedQuery = pgSpans.find(span => span.data.pg.stmt.includes('nested_value')); - expect(nestedQuery).to.exist; - // Complex nested JSON is now masked with structure preserved - const parsedNested = JSON.parse(nestedQuery.data.pg.params[0]); - expect(parsedNested).to.have.property('us**s'); - expect(parsedNested['us**s']).to.be.an('array'); - expect(parsedNested['us**s']).to.have.lengthOf(2); - expect(parsedNested['us**s'][0]).to.have.property('*d', '1'); - expect(parsedNested['us**s'][0]).to.have.property('n**e', 'Al**e'); - expect(parsedNested['us**s'][0]).to.have.property('em**l', 'al**************om'); - expect(parsedNested).to.have.property('me*****a'); - expect(parsedNested['me*****a']).to.have.property('cr****d', '20********01'); - expect(parsedNested['me*****a']).to.have.property('ve****n', '1'); - - // 9. Buffer/Binary data test - const binaryQuery = pgSpans.find(span => span.data.pg.stmt.includes('binary_value')); - expect(binaryQuery).to.exist; - // Buffer shown as size - expect(binaryQuery.data.pg.params[0]).to.equal(''); - - // 10. Large buffer test - const largeBinaryQuery = pgSpans.find(span => span.data.pg.stmt.includes('large_binary')); - expect(largeBinaryQuery).to.exist; - expect(largeBinaryQuery.data.pg.params[0]).to.equal(''); - - // 11. Mixed types in single query - const mixedQuery = pgSpans.find(span => - span.data.pg.stmt.includes('SELECT $1::text, $2::integer, $3::boolean, $4::jsonb, $5::bytea') - ); - expect(mixedQuery).to.exist; - expect(mixedQuery.data.pg.params).to.have.lengthOf(5); - // String: 'user@example.com' -> 'us************om' - expect(mixedQuery.data.pg.params[0]).to.equal('us************om'); - // Number: 12345 -> '1***5' - expect(mixedQuery.data.pg.params[1]).to.equal('1***5'); - // Boolean: false -> 'f***e' - expect(mixedQuery.data.pg.params[2]).to.equal('f***e'); - // JSON: '{"key":"value"}' is now masked with structure preserved - const parsedMixed = JSON.parse(mixedQuery.data.pg.params[3]); - expect(parsedMixed).to.have.property('k*y', 'va**e'); - // Buffer: '' - expect(mixedQuery.data.pg.params[4]).to.equal(''); - }) - ) - )); + it('must capture raw bind variables (string query + array params)', () => + controls + .sendRequest({ + method: 'GET', + path: '/bind-variables-test' + }) + .then(() => + retry(() => + agentControls.getSpans().then(spans => { + verifyHttpEntry(spans, '/bind-variables-test'); + + // string query with positional array params + const selectQuery = getSpansByName(spans, 'postgres').find( + span => span.data.pg.stmt === 'SELECT * FROM users WHERE name = $1 AND email = $2' + ); + expect(selectQuery).to.exist; + expect(selectQuery.data.pg.binds).to.be.an('array'); + expect(selectQuery.data.pg.binds).to.have.lengthOf(2); + expect(selectQuery.data.pg.binds[0]).to.equal('testuser'); + expect(selectQuery.data.pg.binds[1]).to.equal('test@example.com'); + + // config object with values property + const insertQuery = getSpansByName(spans, 'postgres').find( + span => span.data.pg.stmt === 'INSERT INTO users(name, email) VALUES($1, $2) RETURNING *' + ); + expect(insertQuery).to.exist; + expect(insertQuery.data.pg.binds).to.be.an('array'); + expect(insertQuery.data.pg.binds).to.have.lengthOf(2); + expect(insertQuery.data.pg.binds[0]).to.equal('bindtest'); + expect(insertQuery.data.pg.binds[1]).to.equal('bindtest@example.com'); + }) + ) + )); + + it('must capture raw bind variables when calling stored procedures', () => + controls + .sendRequest({ + method: 'GET', + path: '/stored-procedure-test' + }) + .then(() => + retry(() => + agentControls.getSpans().then(spans => { + verifyHttpEntry(spans, '/stored-procedure-test'); + + const insertQuery = getSpansByName(spans, 'postgres').find( + span => span.data.pg.stmt && span.data.pg.stmt.includes('INSERT INTO users(name, email) VALUES($1, $2)') + ); + expect(insertQuery).to.exist; + expect(insertQuery.data.pg.binds).to.be.an('array'); + expect(insertQuery.data.pg.binds).to.have.lengthOf(2); + expect(insertQuery.data.pg.binds[0]).to.equal('proceduretest'); + expect(insertQuery.data.pg.binds[1]).to.equal('procedure@example.com'); + + const procedureCall = getSpansByName(spans, 'postgres').find( + span => span.data.pg.stmt === 'SELECT * FROM get_user_by_name($1)' + ); + expect(procedureCall).to.exist; + expect(procedureCall.data.pg.binds).to.be.an('array'); + expect(procedureCall.data.pg.binds).to.have.lengthOf(1); + expect(procedureCall.data.pg.binds[0]).to.equal('proceduretest'); + }) + ) + )); + }); it('must trace pooled select now', () => controls diff --git a/packages/core/src/tracing/instrumentation/databases/pg.js b/packages/core/src/tracing/instrumentation/databases/pg.js index e5b1bbb28a..8a928b0574 100644 --- a/packages/core/src/tracing/instrumentation/databases/pg.js +++ b/packages/core/src/tracing/instrumentation/databases/pg.js @@ -13,11 +13,13 @@ const constants = require('../../constants'); const cls = require('../../cls'); let isActive = false; +let captureBindVariables = false; exports.spanName = 'postgres'; exports.batchable = true; -exports.init = function init() { +exports.init = function init(config) { + captureBindVariables = config && config.tracing && config.tracing.captureBindVariables === true; hook.onModuleLoad('pg', instrumentPg); }; @@ -59,18 +61,6 @@ function instrumentedQuery(ctx, originalQuery, argsForOriginalQuery) { }); span.stack = tracingUtil.getStackTrace(instrumentedQuery); - // Extract bind variables/parameters - let params; - if (typeof config === 'string') { - // Query is a string, parameters might be in argsForOriginalQuery[1] - if (argsForOriginalQuery.length > 1 && Array.isArray(argsForOriginalQuery[1])) { - params = argsForOriginalQuery[1]; - } - } else if (config && config.values) { - // Query config object with values property - params = config.values; - } - span.data.pg = { stmt: tracingUtil.shortenDatabaseStatement(typeof config === 'string' ? config : config.text), host, @@ -79,9 +69,19 @@ function instrumentedQuery(ctx, originalQuery, argsForOriginalQuery) { db }; - // Add masked bind parameters to span data if present - if (params && params.length > 0) { - span.data.pg.params = tracingUtil.maskBindVariables(params); + // Capture raw bind variables if enabled + if (captureBindVariables) { + let binds; + if (typeof config === 'string') { + if (argsForOriginalQuery.length > 1 && Array.isArray(argsForOriginalQuery[1])) { + binds = argsForOriginalQuery[1]; + } + } else if (config && config.values) { + binds = config.values; + } + if (binds && binds.length > 0) { + span.data.pg.binds = binds; + } } let originalCallback; diff --git a/packages/core/src/tracing/tracingUtil.js b/packages/core/src/tracing/tracingUtil.js index af628cea00..57b713f6a3 100644 --- a/packages/core/src/tracing/tracingUtil.js +++ b/packages/core/src/tracing/tracingUtil.js @@ -414,149 +414,3 @@ exports.handleUnexpectedReturnValue = function handleUnexpectedReturnValue(retur return true; }; - -/** - * Masks a single bind variable value to protect sensitive data. - * Strategy: - * - Preserves exact length of original value for strings - * - For length <= 3: mask completely with asterisks - * - For length 4-5: show first and last character - * - For length > 5: show first 2 and last 2 characters - * - Special handling for objects, arrays, buffers, and other types - * - For JSON objects/arrays: masks individual values while preserving structure - * - * @param {*} value - The bind variable value to mask - * @returns {string} - The masked value - */ -function maskBindValue(value) { - // Handle null and undefined - if (value === null) { - return ''; - } - if (value === undefined) { - return ''; - } - - // Handle Buffer (binary data like images, files) - if (Buffer.isBuffer(value)) { - return ``; - } - - // Handle Date objects - if (value instanceof Date) { - const dateStr = value.toISOString(); - return maskString(dateStr); - } - - // Handle Arrays - if (Array.isArray(value)) { - try { - return maskJsonStructure(value); - } catch (e) { - return ''; - } - } - - // Handle Objects (including JSON) - if (typeof value === 'object') { - try { - return maskJsonStructure(value); - } catch (e) { - // Handle circular references or non-serializable objects - return ''; - } - } - - // Handle primitive types (string, number, boolean, bigint, symbol) - return maskString(String(value)); -} - -/** - * Masks JSON objects and arrays by masking individual values while preserving structure. - * Example: {"theme":"dark","notifications":true} -> {"t***e":"d**k","no*********ns":"t**e"} - * @param {*} obj - The object or array to mask - * @returns {string} - JSON string with masked values - */ -function maskJsonStructure(obj) { - // Check for circular references - const seen = new WeakSet(); - - /** - * @param {*} value - * @returns {*} - */ - function maskRecursive(value) { - // Handle primitives - if (value === null) return null; - if (value === undefined) return null; - if (typeof value === 'string') return maskString(value); - if (typeof value === 'number') return maskString(String(value)); - if (typeof value === 'boolean') return maskString(String(value)); - - // Handle objects and arrays - if (typeof value === 'object') { - // Check for circular reference - if (seen.has(value)) { - throw new Error('Circular reference detected'); - } - seen.add(value); - - if (Array.isArray(value)) { - return value.map(item => maskRecursive(item)); - } - - /** @type {Record} */ - const masked = {}; - Object.keys(value).forEach(key => { - // Mask both keys and values - const maskedKey = maskString(key); - masked[maskedKey] = maskRecursive(value[key]); - }); - return masked; - } - - return value; - } - - const masked = maskRecursive(obj); - return JSON.stringify(masked); -} - -/** - * Masks a string value preserving its exact length. - * @param {string} strValue - The string to mask - * @returns {string} - The masked string - */ -function maskString(strValue) { - const len = strValue.length; - - // For very short values (0-3 chars), mask completely - if (len <= 3) { - return '*'.repeat(len); - } - - // For length 4-5: show first and last character, mask the middle - if (len <= 5) { - const numAsterisks = len - 2; - return strValue[0] + '*'.repeat(numAsterisks) + strValue[len - 1]; - } - - // For length > 5: show first 2 and last 2 characters, mask the middle - const numAsterisks = len - 4; - return strValue.substring(0, 2) + '*'.repeat(numAsterisks) + strValue.substring(len - 2); -} - -/** - * Masks an array of bind variable values to protect sensitive data. - * Each value is masked individually with appropriate handling for different types. - * - * @param {Array<*>} params - Array of bind variable values - * @returns {Array} - Array of masked values - */ -exports.maskBindVariables = function maskBindVariables(params) { - if (!Array.isArray(params)) { - return params; - } - - return params.map(maskBindValue); -}; diff --git a/packages/core/test/tracing/tracingUtil_maskBindVariables_test.js b/packages/core/test/tracing/tracingUtil_maskBindVariables_test.js deleted file mode 100644 index b85ff6dd90..0000000000 --- a/packages/core/test/tracing/tracingUtil_maskBindVariables_test.js +++ /dev/null @@ -1,285 +0,0 @@ -/* - * (c) Copyright IBM Corp. 2024 - */ - -'use strict'; - -const expect = require('chai').expect; -const tracingUtil = require('../../src/tracing/tracingUtil'); - -describe('tracing/tracingUtil', () => { - describe('maskBindVariables', () => { - it('should preserve exact length and show first 2 and last 2 chars for long values', () => { - const params = ['testuser', 'password123', 'email@example.com']; - const masked = tracingUtil.maskBindVariables(params); - - expect(masked).to.be.an('array'); - expect(masked).to.have.lengthOf(3); - // 'testuser' (8 chars) -> 'te****er' - expect(masked[0]).to.equal('te****er'); - expect(masked[0]).to.have.lengthOf(8); - // 'password123' (11 chars) -> 'pa*******23' - expect(masked[1]).to.equal('pa*******23'); - expect(masked[1]).to.have.lengthOf(11); - // 'email@example.com' (17 chars) -> 'em***********om' - expect(masked[2]).to.equal('em***********om'); - expect(masked[2]).to.have.lengthOf(17); - }); - - it('should show first and last char for length 4-5', () => { - const params = ['test', 'hello']; - const masked = tracingUtil.maskBindVariables(params); - - expect(masked).to.be.an('array'); - expect(masked).to.have.lengthOf(2); - // 'test' (4 chars) -> 't**t' - expect(masked[0]).to.equal('t**t'); - expect(masked[0]).to.have.lengthOf(4); - // 'hello' (5 chars) -> 'h***o' - expect(masked[1]).to.equal('h***o'); - expect(masked[1]).to.have.lengthOf(5); - }); - - it('should mask short strings completely preserving length', () => { - const params = ['ab', 'xyz', 'a']; - const masked = tracingUtil.maskBindVariables(params); - - expect(masked).to.be.an('array'); - expect(masked).to.have.lengthOf(3); - expect(masked[0]).to.equal('**'); - expect(masked[0]).to.have.lengthOf(2); - expect(masked[1]).to.equal('***'); - expect(masked[1]).to.have.lengthOf(3); - expect(masked[2]).to.equal('*'); - expect(masked[2]).to.have.lengthOf(1); - }); - - it('should handle null and undefined values', () => { - const params = [null, undefined, 'value']; - const masked = tracingUtil.maskBindVariables(params); - - expect(masked).to.be.an('array'); - expect(masked).to.have.lengthOf(3); - expect(masked[0]).to.equal(''); - expect(masked[1]).to.equal(''); - // 'value' (5 chars) -> 'v***e' - expect(masked[2]).to.equal('v***e'); - expect(masked[2]).to.have.lengthOf(5); - }); - - it('should convert numbers to strings and mask them', () => { - const params = [12345, 42, 999999999, 3.14159, -100]; - const masked = tracingUtil.maskBindVariables(params); - - expect(masked).to.be.an('array'); - expect(masked).to.have.lengthOf(5); - // '12345' (5 chars) -> '1***5' - expect(masked[0]).to.equal('1***5'); - expect(masked[0]).to.have.lengthOf(5); - // '42' (2 chars) -> '**' - expect(masked[1]).to.equal('**'); - expect(masked[1]).to.have.lengthOf(2); - // '999999999' (9 chars) -> '99*****99' - expect(masked[2]).to.equal('99*****99'); - expect(masked[2]).to.have.lengthOf(9); - // '3.14159' (7 chars) -> '3.***59' - expect(masked[3]).to.equal('3.***59'); - expect(masked[3]).to.have.lengthOf(7); - // '-100' (4 chars) -> '-**0' - expect(masked[4]).to.equal('-**0'); - expect(masked[4]).to.have.lengthOf(4); - }); - - it('should handle boolean values', () => { - const params = [true, false]; - const masked = tracingUtil.maskBindVariables(params); - - expect(masked).to.be.an('array'); - expect(masked).to.have.lengthOf(2); - // 'true' (4 chars) -> 't**e' - expect(masked[0]).to.equal('t**e'); - expect(masked[0]).to.have.lengthOf(4); - // 'false' (5 chars) -> 'f***e' - expect(masked[1]).to.equal('f***e'); - expect(masked[1]).to.have.lengthOf(5); - }); - - it('should handle Date objects', () => { - const date = new Date('2024-01-15T10:30:00.000Z'); - const params = [date]; - const masked = tracingUtil.maskBindVariables(params); - - expect(masked).to.be.an('array'); - expect(masked).to.have.lengthOf(1); - // ISO string is masked: '2024-01-15T10:30:00.000Z' (24 chars) -> '20******************00Z' - expect(masked[0]).to.match(/^20\*+0Z$/); - expect(masked[0]).to.have.lengthOf(24); - }); - - it('should handle JSON objects', () => { - const params = [ - { name: 'John', age: 30 }, - { email: 'test@example.com', active: true } - ]; - const masked = tracingUtil.maskBindVariables(params); - - expect(masked).to.be.an('array'); - expect(masked).to.have.lengthOf(2); - - // Parse to verify it's valid JSON and check structure - const parsed1 = JSON.parse(masked[0]); - expect(parsed1).to.have.property('n**e', 'J**n'); - expect(parsed1).to.have.property('a*e', '30'); - - const parsed2 = JSON.parse(masked[1]); - expect(parsed2).to.have.property('em**l', 'te**************om'); - expect(parsed2).to.have.property('ac***e', 't**e'); - }); - - it('should handle arrays', () => { - const params = [ - [1, 2, 3], - ['a', 'b', 'c'], - [{ id: 1 }, { id: 2 }] - ]; - const masked = tracingUtil.maskBindVariables(params); - - expect(masked).to.be.an('array'); - expect(masked).to.have.lengthOf(3); - - // Parse to verify it's valid JSON and check structure - const parsed1 = JSON.parse(masked[0]); - expect(parsed1).to.be.an('array'); - expect(parsed1).to.have.lengthOf(3); - expect(parsed1[0]).to.equal('1'); - expect(parsed1[1]).to.equal('2'); - expect(parsed1[2]).to.equal('3'); - - const parsed2 = JSON.parse(masked[1]); - expect(parsed2).to.be.an('array'); - expect(parsed2).to.have.lengthOf(3); - expect(parsed2[0]).to.equal('*'); - expect(parsed2[1]).to.equal('*'); - expect(parsed2[2]).to.equal('*'); - - const parsed3 = JSON.parse(masked[2]); - expect(parsed3).to.be.an('array'); - expect(parsed3).to.have.lengthOf(2); - expect(parsed3[0]).to.have.property('*d', '1'); - expect(parsed3[1]).to.have.property('*d', '2'); - }); - - it('should handle Buffer (binary data)', () => { - const imageBuffer = Buffer.from([0xff, 0xd8, 0xff, 0xe0]); // JPEG header - const params = [imageBuffer]; - const masked = tracingUtil.maskBindVariables(params); - - expect(masked).to.be.an('array'); - expect(masked).to.have.lengthOf(1); - expect(masked[0]).to.equal(''); - }); - - it('should handle large Buffer', () => { - const largeBuffer = Buffer.alloc(1024 * 1024); // 1MB - const params = [largeBuffer]; - const masked = tracingUtil.maskBindVariables(params); - - expect(masked).to.be.an('array'); - expect(masked).to.have.lengthOf(1); - expect(masked[0]).to.equal(''); - }); - - it('should handle circular references in objects', () => { - const obj = { name: 'test' }; - obj.self = obj; // Create circular reference - const params = [obj]; - const masked = tracingUtil.maskBindVariables(params); - - expect(masked).to.be.an('array'); - expect(masked).to.have.lengthOf(1); - expect(masked[0]).to.equal(''); - }); - - it('should handle circular references in arrays', () => { - const arr = [1, 2, 3]; - arr.push(arr); // Create circular reference - const params = [arr]; - const masked = tracingUtil.maskBindVariables(params); - - expect(masked).to.be.an('array'); - expect(masked).to.have.lengthOf(1); - expect(masked[0]).to.equal(''); - }); - - it('should handle BigInt values', () => { - // eslint-disable-next-line no-undef - const params = [BigInt(9007199254740991), BigInt(123)]; - const masked = tracingUtil.maskBindVariables(params); - - expect(masked).to.be.an('array'); - expect(masked).to.have.lengthOf(2); - // '9007199254740991' (16 chars) -> '90************91' - expect(masked[0]).to.equal('90************91'); - expect(masked[0]).to.have.lengthOf(16); - // '123' (3 chars) -> '***' - expect(masked[1]).to.equal('***'); - expect(masked[1]).to.have.lengthOf(3); - }); - - it('should handle empty array', () => { - const params = []; - const masked = tracingUtil.maskBindVariables(params); - - expect(masked).to.be.an('array'); - expect(masked).to.have.lengthOf(0); - }); - - it('should return non-array input as-is', () => { - const notAnArray = 'not an array'; - const result = tracingUtil.maskBindVariables(notAnArray); - - expect(result).to.equal(notAnArray); - }); - - it('should preserve exact length for very long values', () => { - const longValue = 'a'.repeat(100); - const params = [longValue]; - const masked = tracingUtil.maskBindVariables(params); - - expect(masked).to.be.an('array'); - expect(masked).to.have.lengthOf(1); - // First 2 chars + 96 asterisks + last 2 chars = 100 chars - expect(masked[0]).to.have.lengthOf(100); - expect(masked[0]).to.match(/^aa\*{96}aa$/); - }); - - it('should handle mixed data types', () => { - const params = [ - 'string', - 123, - true, - null, - undefined, - { key: 'value' }, - [1, 2, 3], - new Date('2024-01-01'), - Buffer.from('test') - ]; - const masked = tracingUtil.maskBindVariables(params); - - expect(masked).to.be.an('array'); - expect(masked).to.have.lengthOf(9); - expect(masked[0]).to.equal('st**ng'); // string - expect(masked[1]).to.equal('***'); // 123 - expect(masked[2]).to.equal('t**e'); // true - expect(masked[3]).to.equal(''); - expect(masked[4]).to.equal(''); - expect(masked[5]).to.match(/^\{"\*+e\}$/); // JSON object - expect(masked[6]).to.equal('[1***3]'); // array - expect(masked[7]).to.match(/^20\*+0Z$/); // Date - expect(masked[8]).to.equal(''); // Buffer - }); - }); -}); - -// Made with Bob