@newrelic/security-agent 2.1.1 → 2.3.0

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,27 @@
1
+ ### v2.3.0 (2025-02-03)
2
+ #### Features
3
+ * Added Support for VM module
4
+ * IAST support for Next.js
5
+ * Support for Insecure settings i.e crypto, hash and random modules
6
+
7
+ #### Bug fixes
8
+ * Fix for special characters in ws header
9
+ * Fix for getting transaction in graphql instrumentation
10
+ * Fix for mongodb unit tests
11
+
12
+ #### Miscellaneous chores
13
+ * deps-dev: bump undici from v5.28.4 to v5.28.5
14
+ * Updated axios to v1.7.9
15
+
16
+ ### v2.2.0 (2024-12-18)
17
+ #### Features
18
+ * Support for express 5.x
19
+ * IAST support for GraphQL
20
+ * Added support for trustboundary security events
21
+
22
+ #### Bug fixes
23
+ * Fix for empty route in fastify
24
+
1
25
  ### v2.1.1 (2024-11-07)
2
26
  #### Bug fixes
3
27
  * Fix for assignment to logger constant
@@ -18,6 +18,7 @@ code, the source code can be found at [https://github.com/newrelic/csec-node-age
18
18
  * [check-disk-space](#check-disk-space)
19
19
  * [content-type](#content-type)
20
20
  * [cron](#cron)
21
+ * [docker-cli-js](#docker-cli-js)
21
22
  * [fast-safe-stringify](#fast-safe-stringify)
22
23
  * [find-package-json](#find-package-json)
23
24
  * [hash.js](#hashjs)
@@ -79,7 +80,7 @@ code, the source code can be found at [https://github.com/newrelic/csec-node-age
79
80
 
80
81
  ### axios
81
82
 
82
- This product includes source derived from [axios](https://github.com/axios/axios) ([v1.7.4](https://github.com/axios/axios/tree/v1.7.4)), distributed under the [MIT License](https://github.com/axios/axios/blob/v1.7.4/LICENSE):
83
+ This product includes source derived from [axios](https://github.com/axios/axios) ([v1.7.9](https://github.com/axios/axios/tree/v1.7.9)), distributed under the [MIT License](https://github.com/axios/axios/blob/v1.7.9/LICENSE):
83
84
 
84
85
  ```
85
86
  # Copyright (c) 2014-present Matt Zabriskie & Collaborators
@@ -165,6 +166,36 @@ The above copyright notice and this permission notice shall be included in all c
165
166
 
166
167
  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.
167
168
 
169
+ ```
170
+
171
+ ### docker-cli-js
172
+
173
+ This product includes source derived from [docker-cli-js](https://github.com/Quobject/docker-cli-js) ([v2.10.0](https://github.com/Quobject/docker-cli-js/tree/v2.10.0)), distributed under the [MIT License](https://github.com/Quobject/docker-cli-js/blob/v2.10.0/LICENSE):
174
+
175
+ ```
176
+ The MIT License (MIT)
177
+
178
+ Copyright (c) 2016 Matthias Ludwig (mludwig@quobject.io)
179
+
180
+ Permission is hereby granted, free of charge, to any person obtaining a copy
181
+ of this software and associated documentation files (the "Software"), to deal
182
+ in the Software without restriction, including without limitation the rights
183
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
184
+ copies of the Software, and to permit persons to whom the Software is
185
+ furnished to do so, subject to the following conditions:
186
+
187
+ The above copyright notice and this permission notice shall be included in
188
+ all copies or substantial portions of the Software.
189
+
190
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
191
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
192
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
193
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
194
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
195
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
196
+ THE SOFTWARE.
197
+
198
+
168
199
  ```
169
200
 
170
201
  ### fast-safe-stringify
@@ -259,7 +290,7 @@ THE SOFTWARE.
259
290
 
260
291
  ### https-proxy-agent
261
292
 
262
- This product includes source derived from [https-proxy-agent](https://github.com/TooTallNate/proxy-agents) ([v7.0.4](https://github.com/TooTallNate/proxy-agents/tree/v7.0.4)), distributed under the [MIT License](https://github.com/TooTallNate/proxy-agents/blob/v7.0.4/LICENSE):
293
+ This product includes source derived from [https-proxy-agent](https://github.com/TooTallNate/proxy-agents) ([v7.0.6](https://github.com/TooTallNate/proxy-agents/tree/v7.0.6)), distributed under the [MIT License](https://github.com/TooTallNate/proxy-agents/blob/v7.0.6/LICENSE):
263
294
 
264
295
  ```
265
296
  (The MIT License)
@@ -512,7 +543,7 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
512
543
 
513
544
  ### semver
514
545
 
515
- This product includes source derived from [semver](https://github.com/npm/node-semver) ([v7.5.4](https://github.com/npm/node-semver/tree/v7.5.4)), distributed under the [ISC License](https://github.com/npm/node-semver/blob/v7.5.4/LICENSE):
546
+ This product includes source derived from [semver](https://github.com/npm/node-semver) ([v7.6.3](https://github.com/npm/node-semver/tree/v7.6.3)), distributed under the [ISC License](https://github.com/npm/node-semver/blob/v7.6.3/LICENSE):
516
547
 
517
548
  ```
518
549
  The ISC License
@@ -1876,7 +1907,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
1876
1907
 
1877
1908
  ### mongodb-memory-server
1878
1909
 
1879
- This product includes source derived from [mongodb-memory-server](https://github.com/nodkz/mongodb-memory-server) ([v8.13.0](https://github.com/nodkz/mongodb-memory-server/tree/v8.13.0)), distributed under the [MIT License](https://github.com/nodkz/mongodb-memory-server/blob/v8.13.0/LICENSE.md):
1910
+ This product includes source derived from [mongodb-memory-server](https://github.com/nodkz/mongodb-memory-server) ([v8.11.2](https://github.com/nodkz/mongodb-memory-server/tree/v8.11.2)), distributed under the [MIT License](https://github.com/nodkz/mongodb-memory-server/blob/v8.11.2/LICENSE.md):
1880
1911
 
1881
1912
  ```
1882
1913
  The MIT License (MIT)
@@ -14,14 +14,15 @@ const EVENT_TYPE = {
14
14
  HTTP_REQUEST: 'HTTP_REQUEST',
15
15
  CODE_INJECTION: 'CODE_INJECTION',
16
16
  XXE: 'XXE',
17
- CIPHER: 'CIPHER',
18
17
  HASH: 'HASH',
19
18
  RANDOM: 'RANDOM',
20
19
  UNVALIDATED_REDIRECT: 'UNVALIDATED_REDIRECT',
21
20
  REFLECTED_XSS: 'REFLECTED_XSS',
22
21
  XPATH: 'XPATH',
23
22
  LDAP: 'LDAP',
24
- SECURE_COOKIE: 'SECURE_COOKIE'
23
+ SECURE_COOKIE: 'SECURE_COOKIE',
24
+ TRUSTBOUNDARY: 'TRUSTBOUNDARY',
25
+ CRYPTO: 'CRYPTO',
25
26
  }
26
27
  const EVENT_CATEGORY = {
27
28
  MYSQL: 'MYSQL',
@@ -42,7 +43,9 @@ const EVENT_CATEGORY = {
42
43
  REFLECTED_XSS: 'REFLECTED_XSS',
43
44
  XPATH: 'XPATH',
44
45
  LDAP: 'LDAP',
45
- SECURE_COOKIE: 'SECURE_COOKIE'
46
+ SECURE_COOKIE: 'SECURE_COOKIE',
47
+ TRUSTBOUNDARY: 'TRUSTBOUNDARY',
48
+ CIPHER: 'CIPHER',
46
49
  }
47
50
 
48
51
  module.exports = {
@@ -0,0 +1,145 @@
1
+ /*
2
+ * Copyright 2023 New Relic Corporation. All rights reserved.
3
+ * SPDX-License-Identifier: New Relic Software License v1.0
4
+ */
5
+
6
+ module.exports = initialize
7
+ const requestManager = require('../../core/request-manager');
8
+ const secUtils = require("../../core/sec-utils");
9
+ const API = require("../../../nr-security-api");
10
+ const securityMetaData = require('../../core/security-metadata');
11
+ const { EVENT_TYPE, EVENT_CATEGORY } = require('../../core/event-constants');
12
+ const { NR_CSEC_FUZZ_REQUEST_ID } = require('../../core/constants');
13
+ const logger = API.getLogger();
14
+ const NRLIB = 'newrelic/lib';
15
+ const SALIB = 'security-agent/lib'
16
+
17
+ /**
18
+ * Entry point of crypto module hook
19
+ * @param {*} shim
20
+ * @param {*} mod
21
+ * @param {*} moduleName
22
+ */
23
+ function initialize(shim, mod, moduleName) {
24
+ if (API && API.getNRAgent() && API.getNRAgent().config.security.exclude_from_iast_scan.iast_detection_category.insecure_settings) {
25
+ logger.warn('insecure_settings detection is disabled');
26
+ return;
27
+ }
28
+ logger.info('Instrumenting ' + moduleName)
29
+ cryptoCipherHooks(shim, mod, 'createCipheriv', moduleName);
30
+ cryptoHashHmacHooks(shim, mod, 'createHash', moduleName);
31
+ cryptoHashHmacHooks(shim, mod, 'createHmac', moduleName);
32
+ cryptoRandomHooks(shim, Math, 'random', "Math");
33
+ }
34
+ /**
35
+ * wrapper to hook crypto cipher methods
36
+ * @param {*} shim
37
+ * @param {*} mod
38
+ * @param {*} methodName
39
+ * @param {*} moduleName
40
+ */
41
+ function cryptoCipherHooks(shim, mod, methodName, moduleName) {
42
+ shim.wrap(mod, methodName, function makeWrapper(shim, fn) {
43
+ logger.debug(`Instrumenting ${moduleName}.${methodName}`);
44
+ return function wrapper() {
45
+ const interceptedArgs = [arguments[0]];
46
+ shim.interceptedArgs = interceptedArgs;
47
+ const request = requestManager.getRequest(shim);
48
+ if (request && API.getSecAgent() && API.getSecAgent().status && API.getSecAgent().status.getStatus() === 'active') {
49
+ const traceObject = secUtils.getTraceObject(shim);
50
+ const secMetadata = securityMetaData.getSecurityMetaData(request, interceptedArgs, traceObject, secUtils.getExecutionId(), EVENT_TYPE.CRYPTO, EVENT_CATEGORY.CIPHER)
51
+ if (secMetadata.traceObject && secMetadata.traceObject.stacktrace && (secMetadata.traceObject.stacktrace[0].includes(NRLIB) || secMetadata.traceObject.stacktrace[0].includes(SALIB))) {
52
+ //do nothing
53
+ }
54
+ else {
55
+ const secEvent = API.generateSecEvent(secMetadata);
56
+ this.secEvent = secEvent;
57
+ API.sendEvent(secEvent);
58
+ }
59
+ }
60
+ const result = fn.apply(this, arguments);
61
+
62
+ if (result && request && request.headers[NR_CSEC_FUZZ_REQUEST_ID]) {
63
+ API.generateExitEvent(this.secEvent);
64
+ delete this.secEvent
65
+ }
66
+ return result;
67
+ }
68
+ })
69
+ }
70
+
71
+
72
+ /**
73
+ * wrapper to hook crypto hash and mac methods
74
+ * @param {*} shim
75
+ * @param {*} mod
76
+ * @param {*} methodName
77
+ * @param {*} moduleName
78
+ */
79
+ function cryptoHashHmacHooks(shim, mod, methodName, moduleName) {
80
+ shim.wrap(mod, methodName, function makeWrapper(shim, fn) {
81
+ logger.debug(`Instrumenting ${moduleName}.${methodName}`);
82
+ return function wrapper() {
83
+ const interceptedArgs = [arguments[0]];
84
+ shim.interceptedArgs = interceptedArgs;
85
+ const request = requestManager.getRequest(shim);
86
+
87
+ if (request && arguments[0] !== 'sha256' && API.getSecAgent() && API.getSecAgent().status && API.getSecAgent().status.getStatus() === 'active') {
88
+ const traceObject = secUtils.getTraceObject(shim);
89
+ const secMetadata = securityMetaData.getSecurityMetaData(request, interceptedArgs, traceObject, secUtils.getExecutionId(), EVENT_TYPE.HASH, EVENT_CATEGORY.HASH)
90
+ if (secMetadata.traceObject && secMetadata.traceObject.stacktrace && (secMetadata.traceObject.stacktrace[0].includes(NRLIB) || secMetadata.traceObject.stacktrace[0].includes(SALIB))) {
91
+ //do nothing
92
+ }
93
+ else {
94
+ const secEvent = API.generateSecEvent(secMetadata);
95
+ this.secEvent = secEvent;
96
+ API.sendEvent(secEvent);
97
+ }
98
+ }
99
+ const result = fn.apply(this, arguments);
100
+ if (result && request && request.headers[NR_CSEC_FUZZ_REQUEST_ID] && arguments[0] !== 'sha256') {
101
+ API.generateExitEvent(this.secEvent);
102
+ delete this.secEvent
103
+ }
104
+ return result;
105
+ }
106
+ })
107
+ }
108
+ /**
109
+ * Wrapper for random hooks
110
+ * @param {*} shim
111
+ * @param {*} mod
112
+ * @param {*} methodName
113
+ * @param {*} moduleName
114
+ */
115
+ function cryptoRandomHooks(shim, mod, methodName, moduleName) {
116
+ shim.wrap(mod, methodName, function makeWrapper(shim, fn) {
117
+ logger.debug(`Instrumenting ${moduleName}.${methodName}`);
118
+ return function wrapper() {
119
+ const interceptedArgs = ["Math.random"];
120
+ shim.interceptedArgs = interceptedArgs;
121
+ const request = requestManager.getRequest(shim);
122
+ if (request && API.getSecAgent() && API.getSecAgent().status && API.getSecAgent().status.getStatus() === 'active') {
123
+ const traceObject = secUtils.getTraceObject(shim);
124
+ const secMetadata = securityMetaData.getSecurityMetaData(request, interceptedArgs, traceObject, secUtils.getExecutionId(), EVENT_TYPE.RANDOM, EVENT_CATEGORY.WEAKRANDOM)
125
+ if (secMetadata.traceObject && secMetadata.traceObject.stacktrace && secMetadata.traceObject.stacktrace[0] && (secMetadata.traceObject.stacktrace[0].includes(NRLIB) || secMetadata.traceObject.stacktrace[0].includes(SALIB))) {
126
+ //do nothing
127
+ }
128
+ else {
129
+ const secEvent = API.generateSecEvent(secMetadata);
130
+ this.secEvent = secEvent;
131
+ API.sendEvent(secEvent);
132
+ }
133
+
134
+ }
135
+ const result = fn.apply(this, arguments);
136
+ if (result && request && request.headers[NR_CSEC_FUZZ_REQUEST_ID]) {
137
+ API.generateExitEvent(this.secEvent);
138
+ delete this.secEvent
139
+ }
140
+ return result;
141
+ }
142
+ })
143
+ }
144
+
145
+
@@ -30,12 +30,14 @@ module.exports = function initialize(shim, express) {
30
30
 
31
31
  if (express.Router.use) {
32
32
  wrapExpress4(shim, express)
33
- }
33
+ }
34
+ else if (express.Router.prototype) {
35
+ wrapExpress5(shim, express)
36
+ }
34
37
  if(API && API.getNRAgent() && !API.getNRAgent().config.security.exclude_from_iast_scan.iast_detection_category.invalid_file_access){
35
38
  expressFileHook(shim, express && express.response, 'download')
36
39
  expressFileHook(shim, express && express.response, 'sendFile')
37
40
  }
38
-
39
41
  }
40
42
 
41
43
  /**
@@ -121,7 +123,7 @@ function extractParams(shim, req) {
121
123
  * @param {*} mod
122
124
  * @param {*} fun
123
125
  */
124
- function expressFileHook(shim, mod, fun){
126
+ function expressFileHook(shim, mod, fun) {
125
127
  shim.wrap(mod, fun, function makeFAWrapper(shim, fn) {
126
128
  if (!shim.isFunction(fn)) {
127
129
  return fn
@@ -149,4 +151,70 @@ function expressFileHook(shim, mod, fun){
149
151
  });
150
152
  }
151
153
 
154
+ /**
155
+ * Wrapper to hook express version 5.x route method
156
+ * @param {*} shim
157
+ * @param {*} express
158
+ */
159
+ function wrapExpress5(shim, express) {
160
+ shim.wrap(express.Router.prototype, 'route', function wrapRoute(shim, fn) {
161
+ if (!shim.isFunction(fn)) {
162
+ return fn
163
+ }
164
+ logger.debug('Instrumenting express.Router.prototype.route');
165
+ return function wrappedRoute() {
166
+ try {
167
+ const stakTrace = secUtils.traceElementForRoute();
168
+ const splittedStack = stakTrace[0].split(DOUBLE_DOLLAR);
169
+ const key = lodash.upperCase(splittedStack[1]) + ATTHERATE + arguments[0];
170
+ routeManager.setRoute(key, splittedStack[0]);
171
+ } catch (error) {
172
+
173
+ }
174
+ const route = fn.apply(this, arguments)
175
+ return route;
176
+ }
177
+ })
178
+
179
+ }
180
+
181
+ module.exports.wrapRouter = function wrapExpress5Router(shim, mod) {
182
+ let layer = shim.require('./lib/layer');
183
+ shim.wrap(layer.prototype, 'match', function wrapParam(shim, fn) {
184
+ if (!shim.isFunction(fn)) {
185
+ return fn
186
+ }
187
+ logger.debug('Instrumenting router.layer.match');
188
+ return function wrappedParam() {
189
+ const route = fn.apply(this, arguments);
190
+
191
+ try {
192
+ const uri = this.route.path;
193
+ const params = this.params;
194
+ const transaction = shim.tracer.getTransaction();
195
+ if (transaction) {
196
+ let request = requestManager.getRequestFromId(transaction.id);
197
+ if (params && request) {
198
+ Object.keys(params).forEach(function (key) {
199
+ if (params[key]) {
200
+ if (!request.parameterMap[key]) {
201
+ request.parameterMap[key] = new Array(params[key].toString());
202
+ requestManager.setRequest(transaction.id, request);
203
+ }
204
+ }
205
+ });
206
+ }
207
+ if (uri && request) {
208
+ request.uri = uri;
209
+ requestManager.setRequest(transaction.id, request);
210
+ }
211
+ }
212
+ } catch (error) {
213
+ logger.debug("Error while getting path params via router module", error);
214
+ }
215
+
216
+ return route
217
+ }
218
+ })
219
+ }
152
220
 
@@ -10,7 +10,6 @@ const { ATTHERATE, DOUBLE_DOLLAR } = require('../../core/constants');
10
10
  const lodash = require('lodash');
11
11
  const routeManager = require('../../core/route-manager');
12
12
  const semver = require('semver')
13
- const url = require('url');
14
13
  const API = require("../../../nr-security-api");
15
14
  const logger = API.getLogger();
16
15
 
@@ -119,12 +118,10 @@ function setupExports(original, wrappedExport) {
119
118
  */
120
119
  function extractParams(shim, req) {
121
120
  try {
122
- const reqURL = req.url || req.raw.url;
123
- const parsedURI = url.parse(reqURL).pathname;
124
121
  const transaction = shim.tracer.getTransaction();
125
122
  if (transaction) {
126
123
  let request = requestManager.getRequestFromId(transaction.id);
127
- if (req.params && segment && request) {
124
+ if (req.params && request) {
128
125
  Object.keys(req.params).forEach(function (key) {
129
126
  if (req.params[key]) {
130
127
  if (!request.parameterMap[key]) {
@@ -144,9 +141,9 @@ function extractParams(shim, req) {
144
141
  }
145
142
  });
146
143
  }
147
- let method = request.method.toUpperCase();
148
- if (parsedURI && request && requestManager.requestMap[method + ATTHERATE + parsedURI]) {
149
- request.uri = parsedURI;
144
+ const txInfo = lodash.get(req, Object.getOwnPropertySymbols(req).find(symbol => symbol.toString() === 'Symbol(fastify.context)'));
145
+ if (request && txInfo && txInfo.config && txInfo.config.url) {
146
+ request.uri = txInfo.config.url;
150
147
  requestManager.setRequest(transaction.id, request);
151
148
  }
152
149
  }
@@ -0,0 +1,75 @@
1
+ /*
2
+ * Copyright 2023 New Relic Corporation. All rights reserved.
3
+ * SPDX-License-Identifier: New Relic Software License v1.0
4
+ */
5
+
6
+ 'use strict'
7
+
8
+ /**
9
+ * Express middleware generates traces where middleware are considered siblings
10
+ * (ended on 'next' invocation) and not nested. Middlware are nested below the
11
+ * routers they are mounted to.
12
+ */
13
+
14
+ const requestManager = require("../../core/request-manager");
15
+ const { QUESTIONMARK } = require('../../core/constants');
16
+ const API = require("../../../nr-security-api");
17
+ const logger = API.getLogger();
18
+ const { APPLICATION_JSON } = require('../../core/constants');
19
+ let querystring = require('querystring');
20
+
21
+
22
+ module.exports = function initialize(shim, mod) {
23
+ logger.info("Instrumenting graphql");
24
+
25
+ shim.wrap(mod, 'execute', function wrapExecute(shim, fn) {
26
+ if (!shim.isFunction(fn)) {
27
+ return fn;
28
+ }
29
+ return function wrappedExecute() {
30
+ const transaction = shim.tracer.getTransaction();
31
+ try {
32
+ if (transaction) {
33
+ let request = requestManager.getRequestFromId(transaction.id);
34
+ let customDataType = {};
35
+ //check for query parms
36
+ if (request.headers['x-apollo-operation-name']) {
37
+ const queryString = request.url.split(QUESTIONMARK)[1];
38
+ const parsedQueryString = querystring.parse(queryString);
39
+ let allValues = Object.values(parsedQueryString);
40
+ for (let index = 0; index < allValues.length; index++) {
41
+ const element = allValues[index];
42
+ if (element.startsWith('query')) {
43
+ customDataType['*.query'] = 'GRAPHQL_QUERY';
44
+ }
45
+ if (element.startsWith('variables')) {
46
+ customDataType['*.variables'] = 'GRAPHQL_VARIABLE';
47
+ }
48
+ }
49
+ request.customDataType = customDataType;
50
+ requestManager.setRequest(transaction.id, request);
51
+ }
52
+
53
+ //check for body
54
+ if (request.headers['content-type'] === APPLICATION_JSON) {
55
+ const requestBody = JSON.parse(request.body);
56
+ if (requestBody.query) {
57
+ customDataType['*.query'] = 'GRAPHQL_QUERY';
58
+ }
59
+ if (requestBody.variables) {
60
+ customDataType['*.variables'] = 'GRAPHQL_VARIABLE';
61
+ }
62
+ request.customDataType = customDataType;
63
+ requestManager.setRequest(transaction.id, request);
64
+ }
65
+ }
66
+
67
+ } catch (error) {
68
+ logger.debug("Error while processing graphql http request", error);
69
+ }
70
+
71
+ return fn.apply(this, arguments);
72
+ }
73
+ })
74
+ }
75
+
@@ -5,7 +5,7 @@
5
5
 
6
6
  module.exports = initialize
7
7
  const requestManager = require('../../core/request-manager');
8
- const { NR_CSEC_FUZZ_REQUEST_ID, QUESTION_MARK, EMPTY_STRING, UTF8, CONTENT_TYPE, TEXT_HTML, APPLICATION_JSON, APPLICATION_XML, APPLICATION_XHTML, TEXT_PLAIN, APPLICATION_X_FORM_URLENCODED, MULTIPART_FORM_DATA, COMMA, SEMI_COLON, EQUAL, SECURE, HTTPONLY, SAMESITE, STRICT } = require('../../core/constants');
8
+ const { NR_CSEC_FUZZ_REQUEST_ID, QUESTION_MARK, EMPTY_STRING, UTF8, CONTENT_TYPE, TEXT_HTML, APPLICATION_JSON, APPLICATION_XML, APPLICATION_XHTML, TEXT_PLAIN, APPLICATION_X_FORM_URLENCODED, MULTIPART_FORM_DATA, COMMA, SEMI_COLON, EQUAL, SECURE, HTTPONLY, SAMESITE, STRICT, OBJECT } = require('../../core/constants');
9
9
  const ARRAY_TYPE = 'Array';
10
10
  const STRING_TYPE = 'string';
11
11
  const BUFFER_TYPE = 'Buffer';
@@ -446,6 +446,9 @@ function responseHook(resp, req, shim) {
446
446
  }
447
447
  try {
448
448
  secureCookieCheck(response, shim);
449
+ if (response && response.req && response.req.session) {
450
+ trustBoundaryCheck(response.req.session, shim)
451
+ }
449
452
  } catch (error) {
450
453
  logger.debug("Error while generating secure cookie event");
451
454
  }
@@ -456,8 +459,8 @@ function responseHook(resp, req, shim) {
456
459
  const policy = API.getPolicy();
457
460
  const dynamicScanningFlag = policy.data ? (policy.data.vulnerabilityScan?.enabled && policy.data.vulnerabilityScan.iastScan.enabled) : false;
458
461
  const type = response.getHeader(CONTENT_TYPE);
459
- let isUnsupportedType = isUnsupportedContentType(type);
460
-
462
+ let isUnsupportedType = isUnsupportedContentType(type, response);
463
+
461
464
  if (request && (construct || dynamicScanningFlag) && !isUnsupportedType) {
462
465
  const args = [];
463
466
  args.push(construct);
@@ -478,8 +481,11 @@ function responseHook(resp, req, shim) {
478
481
  * @param {*} conType
479
482
  * @returns
480
483
  */
481
- function isUnsupportedContentType(conType) {
484
+ function isUnsupportedContentType(conType, response) {
482
485
  try {
486
+ if(response.getHeaders()['etag']){
487
+ return false;
488
+ }
483
489
  if (!conType) {
484
490
  return true;
485
491
  }
@@ -531,7 +537,7 @@ function isUnsupportedContentType(conType) {
531
537
  function responseBodyCompute(response, args) {
532
538
  let encoding = UTF8;
533
539
  const type = response.getHeader(CONTENT_TYPE);
534
- let isUnsupportedType = isUnsupportedContentType(type);
540
+ let isUnsupportedType = isUnsupportedContentType(type, response);
535
541
  if (isUnsupportedType) {
536
542
  return;
537
543
  }
@@ -730,3 +736,32 @@ function truncateStringToBytes(inputString, maxBytes) {
730
736
  return truncatedString;
731
737
  }
732
738
 
739
+ /**
740
+ * Utility to generate trustboundary events.
741
+ * @param {*} sessionObject
742
+ * @param {*} shim
743
+ */
744
+ function trustBoundaryCheck(sessionObject, shim) {
745
+ try {
746
+ const request = requestManager.getRequest(shim);
747
+ let params = [];
748
+ Object.keys(sessionObject).forEach(key => {
749
+ if (sessionObject[key] && typeof sessionObject[key] != OBJECT) {
750
+ params.push(String(key));
751
+ params.push(String(sessionObject[key]));
752
+ }
753
+ });
754
+ if (!lodash.isEmpty(params)) {
755
+ const traceObject = secUtils.getTraceObject(shim);
756
+ const secMetadata = securityMetaData.getSecurityMetaData(request, params, traceObject, secUtils.getExecutionId(), EVENT_TYPE.TRUSTBOUNDARY, EVENT_CATEGORY.TRUSTBOUNDARY)
757
+ const secEvent = API.generateSecEvent(secMetadata);
758
+ API.sendEvent(secEvent);
759
+ }
760
+
761
+ } catch (error) {
762
+ logger.debug("Error while processing trustboundary", error);
763
+
764
+ }
765
+
766
+ }
767
+
@@ -0,0 +1,46 @@
1
+ module.exports = initialize;
2
+ const requestManager = require("../../core/request-manager");
3
+ const secUtils = require("../../core/sec-utils");
4
+ const API = require("../../../nr-security-api");
5
+ const securityMetaData = require('../../core/security-metadata');
6
+ const { NR_CSEC_FUZZ_REQUEST_ID } = require('../../core/constants');
7
+ const logger = API.getLogger();
8
+
9
+ function initialize(shim, vm, moduleName) {
10
+ logger.info('Instrumenting', moduleName);
11
+ vmHooks(shim, vm, 'runInNewContext', moduleName);
12
+ vmHooks(shim, vm, 'runInThisContext', moduleName);
13
+ vmHooks(shim, vm, 'runInContext', moduleName);
14
+ }
15
+
16
+ /**
17
+ * Wrapper to hook vm methods
18
+ * @param {*} shim
19
+ * @param {*} vm
20
+ * @param {*} method
21
+ * @param {*} moduleName
22
+ */
23
+ function vmHooks(shim, vm, method, moduleName) {
24
+ shim.wrap(vm, method, function makeWrapper(shim, fn) {
25
+ logger.debug(`Instrumenting ${moduleName}.${method}`);
26
+ return function wrapper() {
27
+ const interceptedArgs = [arguments[0]];
28
+ shim.interceptedArgs = interceptedArgs;
29
+ const request = requestManager.getRequest(shim);
30
+ if (request) {
31
+ const traceObject = secUtils.getTraceObject(shim);
32
+ const secMetadata = securityMetaData.getSecurityMetaData(request, interceptedArgs, traceObject, secUtils.getExecutionId(), "JAVASCRIPT_INJECTION", "JAVASCRIPT_INJECTION")
33
+ const secEvent = API.generateSecEvent(secMetadata);
34
+ this.secEvent = secEvent;
35
+ API.sendEvent(secEvent);
36
+ }
37
+
38
+ const result = fn.apply(this, arguments);
39
+ if (result && request && request.headers[NR_CSEC_FUZZ_REQUEST_ID]) {
40
+ API.generateExitEvent(this.secEvent);
41
+ delete this.secEvent
42
+ }
43
+ return result;
44
+ };
45
+ });
46
+ }
@@ -0,0 +1,93 @@
1
+ /*
2
+ * Copyright 2023 New Relic Corporation. All rights reserved.
3
+ * SPDX-License-Identifier: New Relic Pre-Release
4
+ */
5
+
6
+ 'use strict'
7
+ const requestManager = require("../../core/request-manager");
8
+ const API = require("../../../nr-security-api");
9
+ const logger = API.getLogger();
10
+
11
+ const semver = require('semver')
12
+
13
+ module.exports = function initialize(shim, nextjs) {
14
+ const nextVersion = shim.require('./package.json').version
15
+ logger.info("Instrumenting nextjs", nextVersion);
16
+
17
+ const nextServer = nextjs.default;
18
+ hookRunAPI(shim, nextServer.prototype, nextVersion);
19
+ }
20
+
21
+ /**
22
+ * Wrapper for Server.prototype.runApi
23
+ * @param {*} shim
24
+ * @param {*} mod
25
+ * @param {*} nextVersion
26
+ */
27
+ function hookRunAPI(shim, mod, nextVersion) {
28
+ shim.wrap(mod, 'runApi', function wrapRunApi(shim, originalFn) {
29
+ if (!shim.isFunction(originalFn)) {
30
+ return originalFn
31
+ }
32
+ logger.debug("Instrumenting Server.prototype.runApi");
33
+ return function wrappedRunApi() {
34
+ try {
35
+ const { page, params } = extractAttrs(arguments, nextVersion)
36
+ extractParams(shim, page, params);
37
+ } catch (error) {
38
+ logger.debug("Error while processing path paramters");
39
+ }
40
+
41
+ return originalFn.apply(this, arguments)
42
+ }
43
+ })
44
+ }
45
+
46
+ /**
47
+ * Extracts the page and params from an API request
48
+ *
49
+ * @param {object} args arguments to runApi
50
+ * @param {string} version next.js version
51
+ * @returns {object} { page, params }
52
+ */
53
+ function extractAttrs(args, version) {
54
+ let params
55
+ let page
56
+ if (semver.gte(version, '13.4.13')) {
57
+ const [, , , match] = args
58
+ page = match?.definition?.pathname
59
+ params = { ...match?.params }
60
+ } else {
61
+ ;[, , , params, page] = args
62
+ }
63
+
64
+ return { params, page }
65
+ }
66
+
67
+ /**
68
+ * Utility to extract path params
69
+ * @param {*} page
70
+ * @param {*} params
71
+ */
72
+ function extractParams(shim, page, params) {
73
+ try {
74
+ const transaction = shim.tracer.getTransaction();
75
+ if (transaction) {
76
+ let request = requestManager.getRequestFromId(transaction.id);
77
+ Object.keys(params).forEach(function (key) {
78
+ if (params[key]) {
79
+ if (!request.parameterMap[key]) {
80
+ request.parameterMap[key] = new Array(params[key].toString());
81
+ requestManager.setRequest(transaction.id, request);
82
+ }
83
+ }
84
+ });
85
+ if (request && page) {
86
+ request.uri = page;
87
+ requestManager.setRequest(transaction.id, request);
88
+ }
89
+ }
90
+ } catch (error) {
91
+ }
92
+ }
93
+
@@ -235,3 +235,46 @@ newrelic.instrument({
235
235
  }
236
236
  })
237
237
 
238
+ newrelic.instrument({
239
+ moduleName: 'graphql/execution/execute',
240
+ isEsm: true,
241
+ onRequire: require('./hooks/graphql/nr-graphql'),
242
+ onError: function intrumentErrorHandler(err) {
243
+ logger.error(err.message, err.stack)
244
+ }
245
+ })
246
+
247
+ newrelic.instrument({
248
+ moduleName: 'router',
249
+ isEsm: true,
250
+ onRequire: require('./hooks/express/nr-express').wrapRouter,
251
+ onError: function intrumentErrorHandler(err) {
252
+ logger.error(err.message, err.stack)
253
+ }
254
+ })
255
+
256
+ newrelic.instrument({
257
+ moduleName: 'crypto',
258
+ onRequire: require('./hooks/crypto/nr-crypto'),
259
+ onError: function intrumentErrorHandler(err) {
260
+ logger.error(err.message, err.stack)
261
+ }
262
+ })
263
+
264
+ newrelic.instrumentWebframework({
265
+ moduleName: 'next/dist/server/next-server',
266
+ isEsm: true,
267
+ onRequire: require('./hooks/nextjs/nr-next'),
268
+ onError: function intrumentErrorHandler(err) {
269
+ logger.error(err.message, err.stack)
270
+ }
271
+ })
272
+
273
+ newrelic.instrument({
274
+ moduleName: 'vm',
275
+ isEsm: true,
276
+ onRequire: require('./hooks/native/nr-vm'),
277
+ onError: function intrumentErrorHandler(err) {
278
+ logger.error(err.message, err.stack)
279
+ }
280
+ })
@@ -36,7 +36,8 @@ function AuthHeaders() {
36
36
  authHeaders['NR-ACCOUNT-ID'] = NRAgent.config.account_id;
37
37
  authHeaders['NR-LICENSE-KEY'] = NRAgent ? NRAgent.config.license_key : '';
38
38
  authHeaders["NR-CSEC-IAST-DATA-TRANSFER-MODE"] = "PULL";
39
- authHeaders['NR-CSEC-ENTITY-NAME'] = metadata['entity.name'] ? metadata['entity.name'] : '';
39
+ const entityName = metadata['entity.name'] ? metadata['entity.name'] : '';
40
+ authHeaders['NR-CSEC-ENTITY-NAME'] = encodeURIComponent(entityName);
40
41
  authHeaders['NR-CSEC-ENTITY-GUID'] = metadata['entity.guid'] ? metadata['entity.guid'] : '';
41
42
  let instanceCount = parseInt(NRAgent.config.security.scan_controllers.scan_instance_count);
42
43
  if (isNaN(instanceCount)) {
@@ -12,6 +12,7 @@ const API = require('../../../nr-security-api');
12
12
  const NRAgent = API.getNRAgent();
13
13
  const lodash = require('lodash');
14
14
  const commonUtils = require('./commonUtils');
15
+ const crypto = require('crypto');
15
16
  /**
16
17
  * BasicInfo Structure, which is applicatble to each
17
18
  * Event generated from agent.
@@ -99,7 +100,7 @@ function SecEvent(fileName, funcName, lineNumber, argument, eid, applicationInfo
99
100
  const dynamicScanning = vulnerabilityScan.enabled && vulnerabilityScan.iastScan.enabled;
100
101
  this.isIASTEnable = dynamicScanning;
101
102
  if (this.isIASTEnable) {
102
- this.id = eid + Math.floor(Math.random() * (100000 - 1) + 1);
103
+ this.id = eid + Math.floor(crypto.randomInt(100) * (100000 - 1) + 1);
103
104
  } else {
104
105
  this.id = eid;
105
106
  }
@@ -29,6 +29,10 @@ const initLogger = logs.getInitLogger();
29
29
 
30
30
  const NODE_SER = 'node-serialize/lib/serialize.js';
31
31
  const SECURE_COOKIE = 'SECURE_COOKIE';
32
+ const TRUSTBOUNDARY = 'TRUSTBOUNDARY';
33
+ const HASH = "HASH";
34
+ const CRYPTO = "CRYPTO";
35
+ const RANDOM = "RANDOM";
32
36
 
33
37
  const insecureSettingApiIdSet = new Set();
34
38
 
@@ -110,7 +114,7 @@ function generateSecEvent(securityMetadata) {
110
114
 
111
115
  let apiId = shaUtil.getSHA256ForData(traceObject.stacktrace.join('|') + uri);
112
116
  apiId = securityMetadata.eventType + HYPHEN + apiId;
113
- if (securityMetadata.eventType == SECURE_COOKIE) {
117
+ if (securityMetadata.eventType == SECURE_COOKIE || securityMetadata.eventType == CRYPTO || securityMetadata.eventType == HASH || securityMetadata.eventType == RANDOM) {
114
118
  if (insecureSettingApiIdSet.has(apiId)) {
115
119
  return;
116
120
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@newrelic/security-agent",
3
- "version": "2.1.1",
3
+ "version": "2.3.0",
4
4
  "description": "New Relic Security Agent for Node.js",
5
5
  "main": "index.js",
6
6
  "jsonVersion": "1.2.9",
@@ -39,10 +39,11 @@
39
39
  "url": "https://github.com/newrelic/csec-node-agent.git"
40
40
  },
41
41
  "dependencies": {
42
- "axios": "^1.7.4",
42
+ "axios": "^1.7.9",
43
43
  "check-disk-space": "^3.4.0",
44
44
  "content-type": "^1.0.5",
45
45
  "cron": "^3.1.7",
46
+ "docker-cli-js": "^2.10.0",
46
47
  "fast-safe-stringify": "^2.1.1",
47
48
  "find-package-json": "^1.2.0",
48
49
  "hash.js": "^1.1.7",