@contrast/protect 1.3.0 → 1.4.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.
Files changed (43) hide show
  1. package/lib/error-handlers/install/express4.js +2 -3
  2. package/lib/error-handlers/install/fastify3.js +2 -4
  3. package/lib/error-handlers/install/koa2.js +1 -2
  4. package/lib/{cli-rewriter.js → get-source-context.js} +13 -15
  5. package/lib/hardening/constants.js +20 -0
  6. package/lib/hardening/handlers.js +65 -0
  7. package/lib/hardening/index.js +29 -0
  8. package/lib/hardening/install/node-serialize0.js +59 -0
  9. package/lib/index.d.ts +3 -21
  10. package/lib/index.js +4 -0
  11. package/lib/input-analysis/handlers.js +169 -7
  12. package/lib/input-analysis/index.js +4 -0
  13. package/lib/input-analysis/install/body-parser1.js +13 -21
  14. package/lib/input-analysis/install/cookie-parser1.js +13 -15
  15. package/lib/input-analysis/install/express4.js +8 -13
  16. package/lib/input-analysis/install/fastify3.js +4 -5
  17. package/lib/input-analysis/install/formidable1.js +4 -5
  18. package/lib/input-analysis/install/http.js +12 -3
  19. package/lib/input-analysis/install/koa-body5.js +5 -10
  20. package/lib/input-analysis/install/koa-bodyparser4.js +6 -10
  21. package/lib/input-analysis/install/koa2.js +13 -24
  22. package/lib/input-analysis/install/multer1.js +5 -6
  23. package/lib/input-analysis/install/qs6.js +7 -11
  24. package/lib/input-analysis/install/universal-cookie4.js +3 -7
  25. package/lib/input-analysis/ip-analysis.js +76 -0
  26. package/lib/input-analysis/virtual-patches.js +109 -0
  27. package/lib/input-tracing/handlers/index.js +86 -22
  28. package/lib/input-tracing/index.js +10 -4
  29. package/lib/input-tracing/install/child-process.js +13 -7
  30. package/lib/input-tracing/install/eval.js +60 -0
  31. package/lib/input-tracing/install/fs.js +4 -2
  32. package/lib/input-tracing/install/http.js +63 -0
  33. package/lib/input-tracing/install/mongodb.js +20 -20
  34. package/lib/input-tracing/install/mysql.js +3 -2
  35. package/lib/input-tracing/install/postgres.js +5 -4
  36. package/lib/input-tracing/install/sequelize.js +7 -5
  37. package/lib/input-tracing/install/sqlite3.js +6 -4
  38. package/lib/input-tracing/install/vm.js +132 -0
  39. package/lib/make-source-context.js +4 -1
  40. package/lib/semantic-analysis/handlers.js +160 -0
  41. package/lib/semantic-analysis/index.js +38 -0
  42. package/package.json +7 -9
  43. package/lib/utils.js +0 -84
@@ -24,7 +24,6 @@ module.exports = function(core) {
24
24
  logger,
25
25
  depHooks,
26
26
  patcher,
27
- scopes: { sources },
28
27
  protect,
29
28
  } = core;
30
29
 
@@ -41,7 +40,7 @@ module.exports = function(core) {
41
40
  patchType,
42
41
  around(org, data) {
43
42
  const [err] = data.args;
44
- const sourceContext = sources.getStore()?.protect;
43
+ const sourceContext = protect.getSourceContext('finalHandler');
45
44
  const isSecurityException = SecurityException.isSecurityException(err);
46
45
 
47
46
  if (isSecurityException && sourceContext) {
@@ -67,7 +66,7 @@ module.exports = function(core) {
67
66
  patchType,
68
67
  around(org, data) {
69
68
  const [err] = data.args;
70
- const sourceContext = sources.getStore()?.protect;
69
+ const sourceContext = protect.getSourceContext('express.Layer.handle_error');
71
70
  const isSecurityException = SecurityException.isSecurityException(err);
72
71
 
73
72
  if (isSecurityException && sourceContext) {
@@ -20,10 +20,8 @@ const { patchType } = require('../constants');
20
20
 
21
21
  module.exports = function(core) {
22
22
  const {
23
- logger,
24
23
  depHooks,
25
24
  patcher,
26
- scopes: { sources },
27
25
  protect,
28
26
  } = core;
29
27
 
@@ -59,9 +57,9 @@ module.exports = function(core) {
59
57
  const normalHandler = fastify3ErrorHandler._userHandler || fastify3ErrorHandler.defaultErrorHandler;
60
58
 
61
59
  if (isSecurityException(err)) {
62
- const sourceContext = sources.getStore()?.protect;
60
+ const sourceContext = protect.getSourceContext('fastify3.errorHandler');
61
+
63
62
  if (!sourceContext) {
64
- logger.info('source context not found; unable to handle response');
65
63
  normalHandler.call(this, err, request, reply);
66
64
  } else {
67
65
  const blockInfo = sourceContext.findings.securityException;
@@ -24,7 +24,6 @@ module.exports = function(core) {
24
24
  logger,
25
25
  depHooks,
26
26
  patcher,
27
- scopes: { sources },
28
27
  protect,
29
28
  } = core;
30
29
 
@@ -42,7 +41,7 @@ module.exports = function(core) {
42
41
  patchType,
43
42
  around(org, data) {
44
43
  const [err] = data.args;
45
- const sourceContext = sources.getStore()?.protect;
44
+ const sourceContext = protect.getSourceContext('Koa.Application.handleRequest');
46
45
  const isSecurityException = SecurityException.isSecurityException(err);
47
46
 
48
47
  if (isSecurityException && sourceContext) {
@@ -1,5 +1,3 @@
1
- #!/usr/bin/env node
2
-
3
1
  /*
4
2
  * Copyright: 2022 Contrast Security, Inc
5
3
  * Contact: support@contrastsecurity.com
@@ -17,19 +15,19 @@
17
15
 
18
16
  'use strict';
19
17
 
20
- const { cliRewriter } = require('@contrast/core')();
18
+ module.exports = function(core) {
19
+ const { scopes: { sources }, logger } = core;
20
+
21
+ function getSourceContext(callPoint) {
22
+ const sourceContext = sources.getStore()?.protect;
21
23
 
22
- cliRewriter((deps) => {
23
- if (deps.config.protect.enable) {
24
- try {
25
- const protect = require('./index')(deps);
26
- protect.rewriting.install();
27
- } catch (err) {
28
- // TODO: something else
29
- throw err;
24
+ if (!sourceContext) {
25
+ logger.debug(`source context not available in ${callPoint}`);
26
+ return null;
30
27
  }
31
- } else {
32
- deps.logger.error('Configuration Error: mode \'Protect\' is not enabled');
33
- process.exit(1);
28
+
29
+ return sourceContext.allowed ? null : sourceContext;
34
30
  }
35
- });
31
+
32
+ core.protect.getSourceContext = getSourceContext;
33
+ };
@@ -0,0 +1,20 @@
1
+ /*
2
+ * Copyright: 2022 Contrast Security, Inc
3
+ * Contact: support@contrastsecurity.com
4
+ * License: Commercial
5
+
6
+ * NOTICE: This Software and the patented inventions embodied within may only be
7
+ * used as part of Contrast Security’s commercial offerings. Even though it is
8
+ * made available through public repositories, use of this Software is subject to
9
+ * the applicable End User Licensing Agreement found at
10
+ * https://www.contrastsecurity.com/enduser-terms-0317a or as otherwise agreed
11
+ * between Contrast Security and the End User. The Software may not be reverse
12
+ * engineered, modified, repackaged, sold, redistributed or otherwise used in a
13
+ * way not consistent with the End User License Agreement.
14
+ */
15
+
16
+ 'use strict';
17
+
18
+ module.exports = {
19
+ patchType: 'protect-hardening'
20
+ };
@@ -0,0 +1,65 @@
1
+ /*
2
+ * Copyright: 2022 Contrast Security, Inc
3
+ * Contact: support@contrastsecurity.com
4
+ * License: Commercial
5
+
6
+ * NOTICE: This Software and the patented inventions embodied within may only be
7
+ * used as part of Contrast Security’s commercial offerings. Even though it is
8
+ * made available through public repositories, use of this Software is subject to
9
+ * the applicable End User Licensing Agreement found at
10
+ * https://www.contrastsecurity.com/enduser-terms-0317a or as otherwise agreed
11
+ * between Contrast Security and the End User. The Software may not be reverse
12
+ * engineered, modified, repackaged, sold, redistributed or otherwise used in a
13
+ * way not consistent with the End User License Agreement.
14
+ */
15
+
16
+ 'use strict';
17
+
18
+ const { BLOCKING_MODES, isString } = require('@contrast/common');
19
+
20
+ const NODE_SERIALIZE_RCE_TOKEN = '_$$ND_FUNC$$_';
21
+
22
+ module.exports = function(core) {
23
+ const {
24
+ protect: {
25
+ hardening,
26
+ throwSecurityException,
27
+ }
28
+ } = core;
29
+
30
+ function getResults(sourceContext, ruleId) {
31
+ let results = sourceContext.findings.hardeningResultsMap[ruleId];
32
+ if (!results) {
33
+ results = sourceContext.findings.hardeningResultsMap[ruleId] = [];
34
+ }
35
+ return results;
36
+ }
37
+
38
+ hardening.handleUntrustedDeserialization = function(sourceContext, sinkContext) {
39
+ const ruleId = 'untrusted-deserialization';
40
+ const { mode } = sourceContext.rules.agentRules[ruleId];
41
+ const { name, value } = sinkContext;
42
+
43
+ if (mode === 'off') return;
44
+
45
+ if (name === 'node-serialize.unserialize') {
46
+ if (!isString(value) || !value.indexOf(NODE_SERIALIZE_RCE_TOKEN)) return;
47
+
48
+ const blocked = BLOCKING_MODES.includes(mode);
49
+ const results = getResults(sourceContext, ruleId);
50
+
51
+ results.push({
52
+ blocked,
53
+ findings: { deserializer: name, command: false },
54
+ sinkContext,
55
+ });
56
+
57
+ if (blocked) {
58
+ sourceContext.findings.securityException = [mode, ruleId];
59
+ throwSecurityException(sourceContext);
60
+ }
61
+ }
62
+ };
63
+
64
+ return hardening;
65
+ };
@@ -0,0 +1,29 @@
1
+ /*
2
+ * Copyright: 2022 Contrast Security, Inc
3
+ * Contact: support@contrastsecurity.com
4
+ * License: Commercial
5
+
6
+ * NOTICE: This Software and the patented inventions embodied within may only be
7
+ * used as part of Contrast Security’s commercial offerings. Even though it is
8
+ * made available through public repositories, use of this Software is subject to
9
+ * the applicable End User Licensing Agreement found at
10
+ * https://www.contrastsecurity.com/enduser-terms-0317a or as otherwise agreed
11
+ * between Contrast Security and the End User. The Software may not be reverse
12
+ * engineered, modified, repackaged, sold, redistributed or otherwise used in a
13
+ * way not consistent with the End User License Agreement.
14
+ */
15
+
16
+ 'use strict';
17
+
18
+ module.exports = function(core) {
19
+ const hardening = core.protect.hardening = {};
20
+
21
+ require('./handlers')(core);
22
+
23
+ require('./install/node-serialize0')(core);
24
+ hardening.install = function() {
25
+ hardening.nodeSerialize0Instrumentation.install();
26
+ };
27
+
28
+ return hardening;
29
+ };
@@ -0,0 +1,59 @@
1
+ /*
2
+ * Copyright: 2022 Contrast Security, Inc
3
+ * Contact: support@contrastsecurity.com
4
+ * License: Commercial
5
+
6
+ * NOTICE: This Software and the patented inventions embodied within may only be
7
+ * used as part of Contrast Security’s commercial offerings. Even though it is
8
+ * made available through public repositories, use of this Software is subject to
9
+ * the applicable End User Licensing Agreement found at
10
+ * https://www.contrastsecurity.com/enduser-terms-0317a or as otherwise agreed
11
+ * between Contrast Security and the End User. The Software may not be reverse
12
+ * engineered, modified, repackaged, sold, redistributed or otherwise used in a
13
+ * way not consistent with the End User License Agreement.
14
+ */
15
+
16
+ 'use strict';
17
+
18
+ const { patchType } = require('../constants');
19
+
20
+ module.exports = function(core) {
21
+ const {
22
+ depHooks,
23
+ patcher,
24
+ captureStacktrace,
25
+ protect,
26
+ protect: {
27
+ hardening
28
+ }
29
+ } = core;
30
+
31
+ function install() {
32
+ const name = 'node-serialize';
33
+ const method = 'unserialize';
34
+
35
+ depHooks.resolve(
36
+ { name, version: '<1.0.0' },
37
+ (nodeSerialize) => {
38
+ patcher.patch(nodeSerialize, method, {
39
+ name,
40
+ patchType,
41
+ pre({ args: [value], hooked, orig }) {
42
+ const sourceContext = protect.getSourceContext(`${name}.${method}`);
43
+
44
+ if (!sourceContext || !value) return;
45
+
46
+ const sinkContext = captureStacktrace(
47
+ { name: `${name}.${method}`, value },
48
+ { constructorOpt: hooked, prependFrames: [orig] },
49
+ );
50
+ hardening.handleUntrustedDeserialization(sourceContext, sinkContext);
51
+ },
52
+ });
53
+ });
54
+ }
55
+
56
+ return hardening.nodeSerialize0Instrumentation = {
57
+ install
58
+ };
59
+ };
package/lib/index.d.ts CHANGED
@@ -18,10 +18,9 @@ import { Core } from '@contrast/core';
18
18
  import { Logger } from '@contrast/logger';
19
19
  import { Sources } from '@contrast/scopes';
20
20
  import RequireHook from '@contrast/require-hook';
21
- import { RulesConfig, Rule, Messages, Result } from '@contrast/common';
21
+ import { RulesConfig, Messages, ReqData, ProtectMessage, Findings } from '@contrast/common';
22
22
  import { IncomingMessage, ServerResponse } from 'node:http';
23
23
  import { Config } from '@contrast/config';
24
- import { ProtectMessage } from '@contrast/common';
25
24
  import * as http from 'node:http';
26
25
  import * as https from 'node:https';
27
26
 
@@ -50,24 +49,6 @@ export class HttpInstrumentation {
50
49
  initiateRequestHandling(fnContext: { instance: any, method: any, args: any }): void; //TODO
51
50
  removeCookies(headers: string[]): string[];
52
51
  }
53
- export interface ReqData {
54
- method: string;
55
- headers: string[];
56
- uriPath: string;
57
- queries: string;
58
- contentType?: string;
59
- standardUrlParsing: boolean;
60
- ip: string;
61
- httpVersion: string,
62
- headers2: { [key: string]: Array<string> };
63
- }
64
-
65
- export interface Findings {
66
- trackRequest: boolean;
67
- securityException?: [mode: string, ruleId: string];
68
- bodyType?: 'json' | 'urlencoded';
69
- resultsMap: Record<Rule, Result[]>
70
- }
71
52
 
72
53
  export interface ProtectRequestStore {
73
54
  reqData: ReqData;
@@ -140,7 +121,8 @@ export interface Protect {
140
121
  sequelizeInstrumentation: {
141
122
  getQueryFromArgs: ([value]: any[]) => string | undefined,
142
123
  install: () => void
143
- }
124
+ },
125
+ httpInstrumentation: { install: () => void },
144
126
  install: () => void
145
127
  }
146
128
  errorHandlers: {
package/lib/index.js CHANGED
@@ -33,8 +33,11 @@ module.exports = function(core) {
33
33
  require('./throw-security-exception')(core);
34
34
  require('./make-response-blocker')(core);
35
35
  require('./make-source-context')(core);
36
+ require('./get-source-context')(core);
36
37
  require('./input-analysis')(core);
37
38
  require('./input-tracing')(core);
39
+ require('./hardening')(core);
40
+ require('./semantic-analysis')(core);
38
41
  require('./error-handlers')(core);
39
42
 
40
43
  const pkj = require('../package.json');
@@ -43,6 +46,7 @@ module.exports = function(core) {
43
46
  protect.install = function() {
44
47
  protect.inputAnalysis.install();
45
48
  protect.inputTracing.install();
49
+ protect.hardening.install();
46
50
  protect.errorHandlers.install();
47
51
  };
48
52
 
@@ -15,7 +15,8 @@
15
15
 
16
16
  'use strict';
17
17
 
18
- const { simpleTraverse } = require('../utils');
18
+ const { BLOCKING_MODES, simpleTraverse } = require('@contrast/common');
19
+ const address = require('ipaddr.js');
19
20
 
20
21
  //
21
22
  // these rules are not implemented by agent-lib, but are being considered for
@@ -110,8 +111,12 @@ module.exports = function(core) {
110
111
  * @returns {undefined|[String]} undefined to permit else [mode, rule] to block.
111
112
  */
112
113
  inputAnalysis.handleConnect = function handleConnect(sourceContext, connectInputs) {
114
+ if (!sourceContext || sourceContext.allowed) return;
115
+
113
116
  const { rules: { agentLibRules, agentLibRulesMask: mask } } = sourceContext;
114
117
 
118
+ inputAnalysis.handleVirtualPatches(sourceContext, { URLS: connectInputs.rawUrl, HEADERS: connectInputs.headers });
119
+
115
120
  // initialize findings to the basics
116
121
  let block = undefined;
117
122
  if (mask !== 0) {
@@ -130,12 +135,12 @@ module.exports = function(core) {
130
135
  throw new Error('nyi', sourceContext);
131
136
  };
132
137
 
133
-
134
138
  const jsonInputTypes = {
135
- keyType: agentLib.InputType.JsonKey, valueType: agentLib.InputType.JsonValue
139
+ keyType: agentLib.InputType.JsonKey, inputType: agentLib.InputType.JsonValue
136
140
  };
141
+
137
142
  const parameterInputTypes = {
138
- keyType: agentLib.InputType.ParameterKey, valueType: agentLib.InputType.ParameterValue
143
+ keyType: agentLib.InputType.ParameterKey, inputType: agentLib.InputType.ParameterValue
139
144
  };
140
145
 
141
146
  /**
@@ -155,10 +160,17 @@ module.exports = function(core) {
155
160
  * @param {Object} queryParams pojo {key: value, ...} for all query params/search params
156
161
  */
157
162
  inputAnalysis.handleQueryParams = function handleQueryParams(sourceContext, queryParams) {
163
+ if (sourceContext.analyzedQuery) return;
164
+ sourceContext.analyzedQuery = true;
165
+
158
166
  if (typeof queryParams !== 'object') {
159
167
  logger.debug({ queryParams }, 'handleQueryParams() called with non-object');
160
168
  return;
161
169
  }
170
+
171
+
172
+ inputAnalysis.handleVirtualPatches(sourceContext, { PARAMETERS: queryParams });
173
+
162
174
  const block = commonObjectAnalyzer(sourceContext, queryParams, parameterInputTypes);
163
175
 
164
176
  if (block) {
@@ -176,11 +188,16 @@ module.exports = function(core) {
176
188
  * @param {Object} urlParams pojo
177
189
  */
178
190
  inputAnalysis.handleUrlParams = function(sourceContext, urlParams) {
191
+ if (sourceContext.analyzedUrlParams) return;
192
+ sourceContext.analyzedUrlParams = true;
193
+
179
194
  if (typeof urlParams !== 'object') {
180
195
  logger.debug({ urlParams }, 'handleUrlParams() called with non-object');
181
196
  return;
182
197
  }
183
198
 
199
+ inputAnalysis.handleVirtualPatches(sourceContext, { PARAMETERS: urlParams });
200
+
184
201
  const { rules, rules: { agentLibRulesMask: mask } } = sourceContext;
185
202
  const resultsList = [];
186
203
  const { UrlParameter } = agentLib.InputType;
@@ -235,10 +252,16 @@ module.exports = function(core) {
235
252
  * @param {Object} cookies pojo
236
253
  */
237
254
  inputAnalysis.handleCookies = function(sourceContext, cookies) {
255
+ if (sourceContext.analyzedCookies) return;
256
+ sourceContext.analyzedCookies = true;
257
+
238
258
  const cookiesArr = Object.entries(cookies).reduce((acc, [key, value]) => {
239
259
  acc.push(key, value);
240
260
  return acc;
241
261
  }, []);
262
+
263
+ inputAnalysis.handleVirtualPatches(sourceContext, { HEADERS: cookies });
264
+
242
265
  const { rules, rules: { agentLibRulesMask: mask } } = sourceContext;
243
266
  const cookieFindings = agentLib.scoreRequestConnect(mask, { cookies: cookiesArr }, preferWW);
244
267
 
@@ -259,11 +282,16 @@ module.exports = function(core) {
259
282
  * @param {Object} parsedBody
260
283
  */
261
284
  inputAnalysis.handleParsedBody = function(sourceContext, parsedBody) {
285
+ if (sourceContext.analyzedBody) return;
286
+ sourceContext.analyzedBody = true;
287
+
262
288
  if (typeof parsedBody !== 'object') {
263
289
  logger.debug({ parsedBody }, 'handleParsedBody() called with non-object');
264
290
  return;
265
291
  }
266
292
 
293
+ inputAnalysis.handleVirtualPatches(sourceContext, { PARAMETERS: parsedBody });
294
+
267
295
  let bodyType;
268
296
  let inputTypes;
269
297
  if (sourceContext.reqData.contentType.includes('/json')) {
@@ -288,6 +316,70 @@ module.exports = function(core) {
288
316
  throw new Error('nyi', sourceContext, name);
289
317
  };
290
318
 
319
+ inputAnalysis.handleVirtualPatches = function(sourceContext, requestInput) {
320
+ const ruleId = 'virtual-patch';
321
+
322
+ if (!Object.keys(requestInput).filter(Boolean).length || !sourceContext?.virtualPatchesEvaluators.length) return;
323
+
324
+ for (const vpEvaluators of sourceContext.virtualPatchesEvaluators) {
325
+ for (const key in requestInput) {
326
+ const evaluator = vpEvaluators.get(key);
327
+
328
+ if (evaluator && requestInput[key] && evaluator(requestInput[key])) {
329
+ vpEvaluators.delete(key);
330
+ const { name, uuid } = vpEvaluators.get('metadata');
331
+
332
+ if (vpEvaluators.size === 1 && uuid) {
333
+ if (!sourceContext.findings.serverFeaturesResultsMap[ruleId]) {
334
+ sourceContext.findings.serverFeaturesResultsMap[ruleId] = [];
335
+ }
336
+ sourceContext.findings.serverFeaturesResultsMap[ruleId].push({
337
+ name,
338
+ uuid
339
+ });
340
+ sourceContext.findings.securityException = ['block', ruleId];
341
+ core.protect.throwSecurityException(sourceContext);
342
+ }
343
+ }
344
+ }
345
+ }
346
+ };
347
+
348
+ inputAnalysis.handleIpAllowlist = function(sourceContext, ipAllowlist) {
349
+ if (!sourceContext || !ipAllowlist.length) return;
350
+
351
+ const { ip: reqIp, headers: reqHeaders } = sourceContext.reqData;
352
+
353
+ const match = ipListAnalysis(reqIp, reqHeaders, ipAllowlist);
354
+
355
+ if (match) {
356
+ logger.info(match, 'Found a matching IP to an entry in ipAllow list');
357
+ return true;
358
+ }
359
+ };
360
+
361
+ inputAnalysis.handleIpDenylist = function(sourceContext, ipDenylist) {
362
+ const ruleId = 'ip-denylist';
363
+
364
+ if (!sourceContext || !ipDenylist.length) return;
365
+
366
+ const { ip: reqIp, headers: reqHeaders } = sourceContext.reqData;
367
+
368
+ const match = ipListAnalysis(reqIp, reqHeaders, ipDenylist);
369
+
370
+ if (match) {
371
+ logger.info(match, 'Found a matching IP to an entry in ipDeny list');
372
+ if (!sourceContext.findings.serverFeaturesResultsMap[ruleId]) {
373
+ sourceContext.findings.serverFeaturesResultsMap[ruleId] = [];
374
+ }
375
+
376
+ sourceContext.findings.serverFeaturesResultsMap[ruleId].push({
377
+ ip: match.matchedIp,
378
+ uuid: match.uuid,
379
+ });
380
+ return ['block', 'ip-denylist'];
381
+ }
382
+ };
291
383
 
292
384
  /**
293
385
  * commonObjectAnalyzer() walks an object supplied by the end-user and checks
@@ -307,7 +399,7 @@ module.exports = function(core) {
307
399
  function commonObjectAnalyzer(sourceContext, object, inputTypes) {
308
400
  const { rules, rules: { agentLibRulesMask: mask } } = sourceContext;
309
401
  // use inputTypes to set params...
310
- const { keyType, valueType } = inputTypes;
402
+ const { keyType, inputType } = inputTypes;
311
403
  const inputTypeStr = inputTypes === jsonInputTypes ? 'Json' : 'Parameter';
312
404
  const { Where } = agentLib.MongoQueryType;
313
405
  const resultsList = [];
@@ -340,7 +432,7 @@ module.exports = function(core) {
340
432
  mongoQueryType = agentLib.getMongoQueryType(value);
341
433
  }
342
434
  } else {
343
- itemType = valueType;
435
+ itemType = inputType;
344
436
  }
345
437
  let items = agentLib.scoreAtom(mask, value, itemType, preferWW);
346
438
  if (!items && !mongoQueryType) {
@@ -398,6 +490,50 @@ module.exports = function(core) {
398
490
 
399
491
  return mergeFindings(rules.agentLibRules, sourceContext.findings, findings);
400
492
  }
493
+
494
+ function ipListAnalysis(reqIp, reqHeaders, list) {
495
+ const forwardedIps = [];
496
+
497
+ for (let i = 0; i < reqHeaders.length; i++) {
498
+ if (reqHeaders[i] === 'x-forwarded-for') {
499
+ const ipsFromHeaders = reqHeaders[i + 1]?.split(/[,;]+/);
500
+ forwardedIps.push(...ipsFromHeaders);
501
+ }
502
+ }
503
+
504
+ const ipsToCheck = [reqIp, ...forwardedIps];
505
+ const now = new Date().getTime();
506
+
507
+ /* c8 ignore next 3 */
508
+ if (!ipsToCheck.length) {
509
+ return false;
510
+ }
511
+
512
+ for (const listEntry of list) {
513
+ const { doesExpire, expiresAt } = listEntry;
514
+ for (let i = 0; i < ipsToCheck.length; i++) {
515
+ const currentIp = ipsToCheck[i];
516
+
517
+ // Ignore bad IP values.
518
+ if (!address.isValid(currentIp)) {
519
+ logger.warn(`Unable to parse ${currentIp}.`);
520
+ continue;
521
+ }
522
+ const expired = doesExpire ? expiresAt - now <= 0 : false;
523
+
524
+ if (expired) {
525
+ logger.info(`IP expired: ${listEntry.name}, ${listEntry.ip}`);
526
+ continue;
527
+ }
528
+
529
+ const match = checkIpsMatch(listEntry, currentIp);
530
+
531
+ if (match) {
532
+ return match;
533
+ }
534
+ }
535
+ }
536
+ }
401
537
  };
402
538
 
403
539
  /**
@@ -455,13 +591,38 @@ function normalizeFindings(rules, findings) {
455
591
  // which the block can occur. so at a minimum 'block' should also result in a
456
592
  // block.
457
593
  const { mode } = rules[r.ruleId];
458
- if (r.score >= 90 && ['block', 'block_at_perimeter'].includes(mode)) {
594
+ if (r.score >= 90 && BLOCKING_MODES.includes(mode)) {
459
595
  r.blocked = true;
460
596
  findings.securityException = [mode, r.ruleId];
461
597
  }
462
598
  }
463
599
  }
464
600
 
601
+
602
+ function checkIpsMatch(listEntry, ip) {
603
+ const parsed = address.process(ip);
604
+
605
+ // Check if IP is in CIDR range,
606
+ if (listEntry.cidr) {
607
+ if (parsed.kind() !== listEntry.cidr.kind) {
608
+ return null;
609
+ }
610
+
611
+ if (parsed.match(listEntry.cidr.range)) {
612
+ return { ...listEntry, match: ip };
613
+ } else {
614
+ return null;
615
+ }
616
+ }
617
+
618
+ // or do a direct comparison
619
+ if (parsed.toNormalizedString() === listEntry.normalizedValue) {
620
+ return { ...listEntry, matchedIp: ip };
621
+ }
622
+
623
+ return null;
624
+ }
625
+
465
626
  /**
466
627
  * getValueAtKey() is used to fetch the object (expected) associated
467
628
  * with the path of keys in obj. i say expected because this is only used
@@ -476,6 +637,7 @@ function normalizeFindings(rules, findings) {
476
637
  */
477
638
  function getValueAtKey(obj, path, key) {
478
639
  for (const p of path) {
640
+ /* c8 ignore next 6 */
479
641
  if (!(p in obj)) {
480
642
  return undefined;
481
643
  }
@@ -39,6 +39,10 @@ module.exports = function(core) {
39
39
  require('./install/koa2')(core);
40
40
  require('./install/express4')(core);
41
41
 
42
+ // virtual patches
43
+ require('./virtual-patches')(core);
44
+ require('./ip-analysis')(core);
45
+
42
46
  inputAnalysis.install = function() {
43
47
  Object.values(inputAnalysis)
44
48
  .filter((property) => property.install)