@newrelic/security-agent 3.0.3 → 3.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,20 @@
1
+ ### v3.0.5 (2026-10-29)
2
+ #### Bug fixes
3
+ * Subscribe to agent mongodb channels instead of wrapping prototypes (#375)
4
+ * Delegate wrapped mongodb methods to the prototype at call time (#376)
5
+
6
+ #### Miscellaneous chores
7
+ * bump undici to 7.30.1 and ws to 8.22.0 (#377)
8
+
9
+ ### v3.0.4 (2026-05-14)
10
+ #### Bug fixes
11
+ * remove dependency on uuid module (#357)
12
+ * update hapi instrumentation, remove shim.argsToArray method (#360)
13
+
14
+ #### Miscellaneous chores
15
+ * (deps-dev): bump protobufjs from 7.2.5 to 7.5.8 (#358)
16
+ * (deps-dev): bump fast-xml-parser and @aws-sdk/xml-builder (#356)
17
+
1
18
  ### v3.0.3 (2026-04-08)
2
19
  #### Miscellaneous chores
3
20
  * (deps-dev): bump koa from 3.0.3 to 3.1.2 (#330)
@@ -30,7 +30,6 @@ code, the source code can be found at [https://github.com/newrelic/csec-node-age
30
30
  * [undici](#undici)
31
31
  * [unescape-js](#unescape-js)
32
32
  * [unescape](#unescape)
33
- * [uuid](#uuid)
34
33
  * [ws](#ws)
35
34
 
36
35
  **[devDependencies](#devDependencies)**
@@ -465,23 +464,6 @@ THE SOFTWARE.
465
464
 
466
465
  ```
467
466
 
468
- ### uuid
469
-
470
- This product includes source derived from [uuid](https://github.com/uuidjs/uuid) ([v9.0.1](https://github.com/uuidjs/uuid/tree/v9.0.1)), distributed under the [MIT License](https://github.com/uuidjs/uuid/blob/v9.0.1/LICENSE.md):
471
-
472
- ```
473
- The MIT License (MIT)
474
-
475
- Copyright (c) 2010-2020 Robert Kieffer and other contributors
476
-
477
- Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
478
-
479
- The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
480
-
481
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
482
-
483
- ```
484
-
485
467
  ### ws
486
468
 
487
469
  This product includes source derived from [ws](https://github.com/websockets/ws) ([v8.18.0](https://github.com/websockets/ws/tree/v8.18.0)), distributed under the [MIT License](https://github.com/websockets/ws/blob/v8.18.0/LICENSE):
@@ -49,8 +49,7 @@ function serverPostConstructor(shim) {
49
49
  function wrapProtoRoute(shim, proto) {
50
50
  shim.wrap(proto, 'route', function wrapRoute(shim, original) {
51
51
  logger.debug(`Instrumenting ${proto}.route`);
52
- return function wrappedRoute() {
53
- const args = shim.argsToArray.apply(shim, arguments)
52
+ return function wrappedRoute(...args) {
54
53
  if (!shim.isObject(args[0])) {
55
54
  return original.apply(this, args)
56
55
  }
@@ -7,6 +7,7 @@
7
7
 
8
8
  /* eslint sonarjs/cognitive-complexity: ["error", 18] -- TODO: https://issues.newrelic.com/browse/NEWRELIC-5252 */
9
9
 
10
+ const diagnosticsChannel = require("diagnostics_channel");
10
11
  const { CURSOR_OPS, COLLECTION_OPS, DB_OPS } = require("./constants");
11
12
  const common = module.exports;
12
13
  const requestManager = require("../../core/request-manager");
@@ -18,6 +19,30 @@ const { EVENT_TYPE, EVENT_CATEGORY } = require('../../core/event-constants');
18
19
  const { NR_CSEC_FUZZ_REQUEST_ID } = require('../../core/constants');
19
20
  const logger = API.getLogger();
20
21
 
22
+ // Prefix for the newrelic agent's mongodb tracing channels. The agent publishes
23
+ // one tracing channel per instrumented mongodb class; subscribing to these lets
24
+ // us attach to constructed instances without reassigning prototype methods.
25
+ const AGENT_MONGODB_CHANNEL_PREFIX = "orchestrion:mongodb:";
26
+ // Marks an instance whose methods we have already wrapped, so repeated channel
27
+ // events for the same object do not double-wrap.
28
+ const CSEC_MONGO_INSTANCE_HOOKED = Symbol("csecMongoInstanceHooked");
29
+ // Tracks the active subscription handlers per agent channel. The mongodb hook
30
+ // can run more than once in a process (e.g. the newrelic agent being unloaded
31
+ // and re-instrumented), each time with a fresh `shim`. The tracing channel is
32
+ // process-global, so on each run we must remove the previous subscription
33
+ // (bound to the old `shim`) before adding a new one, otherwise we stack
34
+ // duplicate subscribers that serve a stale `shim`.
35
+ //
36
+ // This registry must live on `globalThis`, not in a module-scoped variable:
37
+ // this file's path contains "mongodb", so test/require-cache busters that evict
38
+ // modules matching "mongodb" would reload this module and reset a module-scoped
39
+ // Map — losing the handle needed to unsubscribe the still-live prior
40
+ // subscription. Keyed by full channel name -> handlers object.
41
+ const CHANNEL_SUBSCRIPTIONS_KEY = Symbol.for("newrelic.csec.mongodb.channelSubscriptions");
42
+ const channelSubscriptions =
43
+ globalThis[CHANNEL_SUBSCRIPTIONS_KEY] ||
44
+ (globalThis[CHANNEL_SUBSCRIPTIONS_KEY] = new Map());
45
+
21
46
  function methodChecker(data) {
22
47
  data = data.toLowerCase();
23
48
  if (data.includes("delete")) {
@@ -53,52 +78,200 @@ function payloadType(obj) {
53
78
 
54
79
  }
55
80
 
56
- function queryHook(shim, mod, method) {
57
- shim.wrap(mod, method, function makeQueryWrapper(shim, fn) {
58
- logger.debug(`Instrumenting mongodb.${method}`);
59
- return function queryWrapper() {
60
- const request = requestManager.getRequest(shim);
61
- if (CURSOR_OPS.includes(method)) {
62
- if (!this.mongo_sec_hooked && this.operation && this.operation.cmd) {
63
- const pType = payloadType(this.operation.cmd);
64
- const payloadData = {
65
- payloadType: pType,
66
- payload: this.operation.cmd,
67
- };
68
- shim.interceptedArgs = payloadData;
69
- }
70
- } else {
71
- this.mongo_sec_hooked = true;
72
-
73
- const pType = methodChecker(method);
81
+ /**
82
+ * Builds the security wrapper for a mongodb operation. The returned function
83
+ * reads the operation payload, generates the NoSQL security event, and (for
84
+ * fuzz requests) hooks the callback, then delegates to the original method.
85
+ *
86
+ * This is shared by both attachment strategies: the legacy prototype wrap
87
+ * (`queryHook`, used for mongodb < 4 where the newrelic agent does not provide
88
+ * subscriber based instrumentation) and the per-instance attachment
89
+ * (`instrumentInstance`, used for mongodb >= 4 so we do not reassign a
90
+ * prototype method already instrumented by the newrelic agent).
91
+ *
92
+ * @param {Shim} shim
93
+ * @param {string} method name of the operation being wrapped
94
+ * @param {Function} fn the original method
95
+ * @returns {Function} the wrapped method
96
+ */
97
+ function makeQueryWrapper(shim, method, fn) {
98
+ return function queryWrapper() {
99
+ const request = requestManager.getRequest(shim);
100
+ if (CURSOR_OPS.includes(method)) {
101
+ if (!this.mongo_sec_hooked && this.operation && this.operation.cmd) {
102
+ const pType = payloadType(this.operation.cmd);
74
103
  const payloadData = {
75
104
  payloadType: pType,
76
- payload: arguments,
105
+ payload: this.operation.cmd,
77
106
  };
78
107
  shim.interceptedArgs = payloadData;
79
-
80
108
  }
109
+ } else {
110
+ this.mongo_sec_hooked = true;
81
111
 
82
- try {
83
- if (request) {
84
- //generate event here
85
- const traceObject = secUtils.getTraceObject(shim);
86
- const secMetadata = securityMetaData.getSecurityMetaData(request, JSON.parse(JSON.stringify(shim.interceptedArgs)), traceObject, secUtils.getExecutionId(), EVENT_TYPE.NOSQL_DB_COMMAND, EVENT_CATEGORY.MONGO)
87
- const secEvent = API.generateSecEvent(secMetadata);
88
- API.sendEvent(secEvent);
89
- if (request.headers[NR_CSEC_FUZZ_REQUEST_ID]) {
90
- callbackHook(shim, arguments, arguments.length - 1, secEvent);
91
- }
112
+ const pType = methodChecker(method);
113
+ const payloadData = {
114
+ payloadType: pType,
115
+ payload: arguments,
116
+ };
117
+ shim.interceptedArgs = payloadData;
118
+
119
+ }
120
+
121
+ try {
122
+ if (request) {
123
+ //generate event here
124
+ const traceObject = secUtils.getTraceObject(shim);
125
+ const secMetadata = securityMetaData.getSecurityMetaData(request, JSON.parse(JSON.stringify(shim.interceptedArgs)), traceObject, secUtils.getExecutionId(), EVENT_TYPE.NOSQL_DB_COMMAND, EVENT_CATEGORY.MONGO)
126
+ const secEvent = API.generateSecEvent(secMetadata);
127
+ API.sendEvent(secEvent);
128
+ if (request.headers[NR_CSEC_FUZZ_REQUEST_ID]) {
129
+ callbackHook(shim, arguments, arguments.length - 1, secEvent);
92
130
  }
93
- } catch (error) {
94
131
  }
95
-
132
+ } catch (error) {
133
+ }
96
134
 
97
- return fn.apply(this, arguments);
98
- };
135
+
136
+ return fn.apply(this, arguments);
137
+ };
138
+ }
139
+
140
+ function queryHook(shim, mod, method) {
141
+ shim.wrap(mod, method, function wrapQuery(shim, fn) {
142
+ logger.debug(`Instrumenting mongodb.${method}`);
143
+ return makeQueryWrapper(shim, method, fn);
144
+ });
145
+ }
146
+
147
+ /**
148
+ * Attaches the security wrapper to a single mongodb instance (a `Collection`,
149
+ * cursor, etc.) as an own property, leaving the class prototype untouched.
150
+ *
151
+ * The newrelic agent (>= the release that instruments mongodb via
152
+ * `diagnostics_channel`) reassigns the driver prototype methods. If we also
153
+ * wrapped the prototype, the two reassignments would clobber one another and
154
+ * the agent's datastore instrumentation would be lost. Wrapping the instance
155
+ * sits below the prototype, so both coexist. See
156
+ * https://github.com/newrelic/csec-node-agent/issues/374.
157
+ *
158
+ * @param {Shim} shim
159
+ * @param {object} instance a constructed mongodb object (e.g. a `Collection`)
160
+ * @param {string[]} methods operation names to wrap when present on `instance`
161
+ */
162
+ function instrumentInstance(shim, instance, methods) {
163
+ if (!instance || instance[CSEC_MONGO_INSTANCE_HOOKED]) {
164
+ return;
165
+ }
166
+ // Guard so repeated channel events for the same instance do not re-wrap.
167
+ Object.defineProperty(instance, CSEC_MONGO_INSTANCE_HOOKED, {
168
+ value: true,
169
+ enumerable: false,
170
+ writable: true,
171
+ configurable: true
99
172
  });
173
+
174
+ const proto = Object.getPrototypeOf(instance);
175
+ for (let i = 0; i < methods.length; i++) {
176
+ const method = methods[i];
177
+ if (typeof instance[method] !== "function") {
178
+ continue;
179
+ }
180
+ logger.debug(`Instrumenting mongodb instance method ${method}`);
181
+ // Own property on the instance; does not touch the prototype. Delegate to
182
+ // the prototype method *lazily* (resolved at call time) rather than
183
+ // capturing it now: the newrelic agent wraps the prototype method on each
184
+ // (re-)instrumentation, so a value captured at construction time can go
185
+ // stale and bypass the agent's current wrapper. Reading it from the
186
+ // prototype at call time always runs whatever the agent currently has.
187
+ instance[method] = makeQueryWrapper(shim, method, function callProto() {
188
+ return proto[method].apply(this, arguments);
189
+ });
190
+ }
191
+ }
192
+
193
+ /**
194
+ * Subscribes to a newrelic agent mongodb construction tracing channel and
195
+ * attaches the security wrapper to each constructed instance. Returns false if
196
+ * the agent does not expose the channel (e.g. an older agent without
197
+ * subscriber based mongodb instrumentation), so callers can fall back to the
198
+ * legacy prototype wrapping.
199
+ *
200
+ * @param {Shim} shim
201
+ * @param {string} channelName the agent channel, e.g. `nr_collection`
202
+ * @param {string[]} methods operation names to wrap on each instance
203
+ * @returns {boolean} whether the channel subscription was established
204
+ */
205
+ function subscribeToAgentChannel(shim, channelName, methods) {
206
+ const fullName = `${AGENT_MONGODB_CHANNEL_PREFIX}${channelName}`;
207
+ try {
208
+ const channel = diagnosticsChannel.tracingChannel(fullName);
209
+ // Only ride the agent's channel when the agent is actually instrumenting
210
+ // mongodb on it (it subscribes its own handler via `start`). If nothing is
211
+ // publishing here (e.g. an older newrelic agent without subscriber based
212
+ // mongodb instrumentation, or the agent disabled), report failure so the
213
+ // caller falls back to the legacy prototype wrapping. There is no colliding
214
+ // agent instrumentation in that case, so prototype wrapping is safe.
215
+ if (channel.start.hasSubscribers !== true) {
216
+ return false;
217
+ }
218
+ // Remove any prior subscription for this channel. On re-instrumentation the
219
+ // previous handlers closed over a now-stale `shim`; replacing them keeps a
220
+ // single live subscription bound to the current `shim`.
221
+ const previous = channelSubscriptions.get(fullName);
222
+ if (previous) {
223
+ channel.unsubscribe(previous);
224
+ }
225
+ const handlers = {
226
+ end(event) {
227
+ try {
228
+ instrumentInstance(shim, event && event.self, methods);
229
+ } catch (error) {
230
+ logger.debug(`Error instrumenting mongodb instance via ${channelName}`, error);
231
+ }
232
+ }
233
+ };
234
+ channel.subscribe(handlers);
235
+ channelSubscriptions.set(fullName, handlers);
236
+ return true;
237
+ } catch (error) {
238
+ logger.debug(`Could not subscribe to agent channel ${channelName}`, error);
239
+ return false;
240
+ }
100
241
  }
101
242
 
243
+ /**
244
+ * Instruments cursor operations for mongodb >= 4 by subscribing to the newrelic
245
+ * agent's cursor construction channel and attaching to each instance. Falls
246
+ * back to the legacy prototype wrapping when the agent channel is unavailable.
247
+ *
248
+ * @param {Shim} shim
249
+ * @param {Cursor} [Cursor] cursor class, used only for the fallback path
250
+ */
251
+ common.instrumentCursorViaChannel = function instrumentCursorViaChannel(shim, Cursor) {
252
+ const methods = CURSOR_OPS.concat(["each"]);
253
+ const subscribed = subscribeToAgentChannel(shim, "nr_cursor", methods);
254
+ if (!subscribed) {
255
+ common.instrumentCursor(shim, Cursor);
256
+ }
257
+ };
258
+
259
+ /**
260
+ * Instruments collection operations for mongodb >= 4 by subscribing to the
261
+ * newrelic agent's collection construction channel and attaching to each
262
+ * instance. Falls back to the legacy prototype wrapping when the agent channel
263
+ * is unavailable.
264
+ *
265
+ * @param {Shim} shim
266
+ * @param {Collection} [Collection] collection class, used only for the fallback path
267
+ */
268
+ common.instrumentCollectionViaChannel = function instrumentCollectionViaChannel(shim, Collection) {
269
+ const subscribed = subscribeToAgentChannel(shim, "nr_collection", COLLECTION_OPS);
270
+ if (!subscribed) {
271
+ common.instrumentCollection(shim, Collection);
272
+ }
273
+ };
274
+
102
275
  /**
103
276
  * Instruments all methods from constants.CURSOR_OPS on a given
104
277
  * cursor class
@@ -6,12 +6,22 @@
6
6
 
7
7
  'use strict'
8
8
 
9
- const { instrumentCollection, instrumentCursor, instrumentDb } = require('./common')
9
+ const { instrumentCollectionViaChannel, instrumentCursorViaChannel, instrumentDb } = require('./common')
10
10
 
11
11
  module.exports = function instrument(shim, mongodb) {
12
- instrumentCursor(shim, mongodb.AbstractCursor)
13
- instrumentCursor(shim, mongodb.FindCursor)
14
- instrumentCursor(shim, mongodb.AggregationCursor)
15
- instrumentCollection(shim, mongodb.Collection)
12
+ // The newrelic agent instruments mongodb (>= 4) via diagnostics_channel and
13
+ // reassigns the driver prototype methods. Reassigning those same prototype
14
+ // methods here would clobber the agent's instrumentation (and vice versa), so
15
+ // we subscribe to the agent's construction channels and attach our security
16
+ // wrapper to each constructed instance instead. A single `nr_cursor`
17
+ // subscription covers AbstractCursor and its subclasses (FindCursor,
18
+ // AggregationCursor). See https://github.com/newrelic/csec-node-agent/issues/374.
19
+ //
20
+ // If the agent does not expose those channels (older agent without
21
+ // subscriber based mongodb instrumentation), these fall back to the legacy
22
+ // prototype wrapping, which is safe because there is no colliding agent
23
+ // instrumentation in that case.
24
+ instrumentCursorViaChannel(shim, mongodb.AbstractCursor)
25
+ instrumentCollectionViaChannel(shim, mongodb.Collection)
16
26
  instrumentDb(shim, mongodb.Db)
17
27
  }
@@ -5,7 +5,7 @@
5
5
 
6
6
  /* eslint-disable prefer-promise-reject-errors */
7
7
  const fs = require('fs');
8
- const uuidV = require('uuid');
8
+ const { randomUUID } = require('node:crypto');
9
9
  const logs = require('./logging');
10
10
  const logger = logs.getLogger();
11
11
  const OS = require('os');
@@ -45,7 +45,7 @@ const skip_detection_category_map = {
45
45
  * @returns UUID
46
46
  */
47
47
  function getUUID() {
48
- let uuid = process.env.applicationUUID || uuidV.v4();
48
+ let uuid = process.env.applicationUUID || randomUUID();
49
49
  process.env.applicationUUID = uuid;
50
50
  return uuid;
51
51
  }
@@ -11,8 +11,8 @@ if(process.platform == 'win32'){
11
11
  AGENT_DIR = path.join(__dirname, "..\\..\\..\\");
12
12
  }
13
13
 
14
- const uuid = require('uuid');
15
- process.env.applicationUUID = process.env.applicationUUID || uuid.v4();
14
+ const { randomUUID } = require('node:crypto');
15
+ process.env.applicationUUID = process.env.applicationUUID || randomUUID();
16
16
 
17
17
  module.exports = {
18
18
  AGENT_DIR,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@newrelic/security-agent",
3
- "version": "3.0.3",
3
+ "version": "3.0.5",
4
4
  "description": "New Relic Security Agent for Node.js",
5
5
  "main": "index.js",
6
6
  "jsonVersion": "1.2.9",
@@ -52,11 +52,10 @@
52
52
  "request-ip": "^3.3.0",
53
53
  "ringbufferjs": "^2.0.0",
54
54
  "semver": "^7.5.4",
55
- "undici": "^7.19.0",
55
+ "undici": "^7.30.1",
56
56
  "unescape": "^1.0.1",
57
57
  "unescape-js": "^1.1.4",
58
- "uuid": "^9.0.1",
59
- "ws": "^8.17.1"
58
+ "ws": "^8.22.0"
60
59
  },
61
60
  "devDependencies": {
62
61
  "@grpc/proto-loader": "^0.7.10",