@newrelic/security-agent 2.1.1 → 2.2.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,12 @@
1
+ ### v2.2.0 (2024-12-18)
2
+ #### Features
3
+ * Support for express 5.x
4
+ * IAST support for GraphQL
5
+ * Added support for trustboundary security events
6
+
7
+ #### Bug fixes
8
+ * Fix for empty route in fastify
9
+
1
10
  ### v2.1.1 (2024-11-07)
2
11
  #### Bug fixes
3
12
  * Fix for assignment to logger constant
@@ -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,124 @@
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
+
15
+ /**
16
+ * Entry point of ldapjs and ldapts module hook
17
+ * @param {*} shim
18
+ * @param {*} mod
19
+ * @param {*} moduleName
20
+ */
21
+ function initialize(shim, mod, moduleName) {
22
+ logger.info('Instrumenting ' + moduleName)
23
+ cryptoCipherHooks(shim, mod, 'createCipheriv', moduleName);
24
+ cryptoHashHmacHooks(shim, mod, 'createHash', moduleName);
25
+ cryptoHashHmacHooks(shim, mod, 'createHmac', moduleName);
26
+ // cryptoRandomHooks(shim, mod, 'randomInt', moduleName);
27
+ // cryptoRandomHooks(shim, mod, 'randomBytes', moduleName);
28
+ cryptoRandomHooks(shim, Math, 'random', "Math");
29
+ }
30
+ /**
31
+ * wrapper to hook crypto cipher methods
32
+ * @param {*} shim
33
+ * @param {*} mod
34
+ * @param {*} methodName
35
+ * @param {*} moduleName
36
+ */
37
+ function cryptoCipherHooks(shim, mod, methodName, moduleName) {
38
+ shim.wrap(mod, methodName, function makeWrapper(shim, fn) {
39
+ logger.debug(`Instrumenting ${moduleName}.${methodName}`);
40
+ return function wrapper() {
41
+ const interceptedArgs = [arguments[0]];
42
+ shim.interceptedArgs = interceptedArgs;
43
+ const request = requestManager.getRequest(shim);
44
+ if (request) {
45
+ const traceObject = secUtils.getTraceObject(shim);
46
+ const secMetadata = securityMetaData.getSecurityMetaData(request, interceptedArgs, traceObject, secUtils.getExecutionId(), EVENT_TYPE.CRYPTO, EVENT_CATEGORY.CIPHER)
47
+ const secEvent = API.generateSecEvent(secMetadata);
48
+ this.secEvent = secEvent;
49
+ API.sendEvent(secEvent);
50
+ }
51
+ const result = fn.apply(this, arguments);
52
+
53
+ if (result && request && request.headers[NR_CSEC_FUZZ_REQUEST_ID]) {
54
+ API.generateExitEvent(this.secEvent);
55
+ delete this.secEvent
56
+ }
57
+ return result;
58
+ }
59
+ })
60
+ }
61
+
62
+
63
+ /**
64
+ * wrapper to hook crypto hash and mac methods
65
+ * @param {*} shim
66
+ * @param {*} mod
67
+ * @param {*} methodName
68
+ * @param {*} moduleName
69
+ */
70
+ function cryptoHashHmacHooks(shim, mod, methodName, moduleName) {
71
+ shim.wrap(mod, methodName, function makeWrapper(shim, fn) {
72
+ logger.debug(`Instrumenting ${moduleName}.${methodName}`);
73
+ return function wrapper() {
74
+ const interceptedArgs = [arguments[0]];
75
+ shim.interceptedArgs = interceptedArgs;
76
+ const request = requestManager.getRequest(shim);
77
+ if (request) {
78
+ const traceObject = secUtils.getTraceObject(shim);
79
+ const secMetadata = securityMetaData.getSecurityMetaData(request, interceptedArgs, traceObject, secUtils.getExecutionId(), EVENT_TYPE.HASH, EVENT_CATEGORY.HASH)
80
+ const secEvent = API.generateSecEvent(secMetadata);
81
+ this.secEvent = secEvent;
82
+ API.sendEvent(secEvent);
83
+ }
84
+ const result = fn.apply(this, arguments);
85
+ if (result && request && request.headers[NR_CSEC_FUZZ_REQUEST_ID]) {
86
+ API.generateExitEvent(this.secEvent);
87
+ delete this.secEvent
88
+ }
89
+ return result;
90
+ }
91
+ })
92
+ }
93
+ /**
94
+ * Wrapper for random hooks
95
+ * @param {*} shim
96
+ * @param {*} mod
97
+ * @param {*} methodName
98
+ * @param {*} moduleName
99
+ */
100
+ function cryptoRandomHooks(shim, mod, methodName, moduleName) {
101
+ shim.wrap(mod, methodName, function makeWrapper(shim, fn) {
102
+ logger.debug(`Instrumenting ${moduleName}.${methodName}`);
103
+ return function wrapper() {
104
+ const interceptedArgs = ["Math.random"];
105
+ shim.interceptedArgs = interceptedArgs;
106
+ const request = requestManager.getRequest(shim);
107
+ if (request && arguments[0]) {
108
+ const traceObject = secUtils.getTraceObject(shim);
109
+ const secMetadata = securityMetaData.getSecurityMetaData(request, interceptedArgs, traceObject, secUtils.getExecutionId(), EVENT_TYPE.RANDOM, EVENT_CATEGORY.WEAKRANDOM)
110
+ const secEvent = API.generateSecEvent(secMetadata);
111
+ this.secEvent = secEvent;
112
+ API.sendEvent(secEvent);
113
+ }
114
+ const result = fn.apply(this, arguments);
115
+ if (result && request && request.headers[NR_CSEC_FUZZ_REQUEST_ID]) {
116
+ API.generateExitEvent(this.secEvent);
117
+ delete this.secEvent
118
+ }
119
+ return result;
120
+ }
121
+ })
122
+ }
123
+
124
+
@@ -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 segment = shim.getActiveSegment();
31
+ try {
32
+ if (segment && segment.transaction) {
33
+ let request = requestManager.getRequestFromId(segment.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(segment.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(segment.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
  }
@@ -730,3 +733,32 @@ function truncateStringToBytes(inputString, maxBytes) {
730
733
  return truncatedString;
731
734
  }
732
735
 
736
+ /**
737
+ * Utility to generate trustboundary events.
738
+ * @param {*} sessionObject
739
+ * @param {*} shim
740
+ */
741
+ function trustBoundaryCheck(sessionObject, shim) {
742
+ try {
743
+ const request = requestManager.getRequest(shim);
744
+ let params = [];
745
+ Object.keys(sessionObject).forEach(key => {
746
+ if (sessionObject[key] && typeof sessionObject[key] != OBJECT) {
747
+ params.push(String(key));
748
+ params.push(String(sessionObject[key]));
749
+ }
750
+ });
751
+ if (!lodash.isEmpty(params)) {
752
+ const traceObject = secUtils.getTraceObject(shim);
753
+ const secMetadata = securityMetaData.getSecurityMetaData(request, params, traceObject, secUtils.getExecutionId(), EVENT_TYPE.TRUSTBOUNDARY, EVENT_CATEGORY.TRUSTBOUNDARY)
754
+ const secEvent = API.generateSecEvent(secMetadata);
755
+ API.sendEvent(secEvent);
756
+ }
757
+
758
+ } catch (error) {
759
+ logger.debug("Error while processing trustboundary", error);
760
+
761
+ }
762
+
763
+ }
764
+
@@ -235,3 +235,22 @@ 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
+
@@ -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.2.0",
4
4
  "description": "New Relic Security Agent for Node.js",
5
5
  "main": "index.js",
6
6
  "jsonVersion": "1.2.9",