@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
@@ -21,29 +21,25 @@ module.exports = (core) => {
21
21
  const {
22
22
  depHooks,
23
23
  logger,
24
- scopes: { sources },
24
+ protect,
25
25
  protect: { inputAnalysis },
26
26
  } = core;
27
27
 
28
28
  function contrastNext(req, origNext, fnName) {
29
29
  return function next(origErr) {
30
- const sourceContext = sources.getStore()?.protect;
30
+ const sourceContext = protect.getSourceContext(fnName);
31
31
  let securityException;
32
32
 
33
- if (!sourceContext) {
34
- logger.debug(`source context not available in \`body-parser\`'s \`${fnName}\` hook`);
35
- } else {
36
- if (req.body && Object.keys(req.body).length) {
37
- sourceContext.parsedBody = req.body;
38
-
39
- try {
40
- inputAnalysis.handleParsedBody(sourceContext, req.body);
41
- } catch (err) {
42
- if (isSecurityException(err)) {
43
- securityException = err;
44
- } else {
45
- logger.error({ err }, 'Unexpected error during input analysis');
46
- }
33
+ if (sourceContext && req.body && Object.keys(req.body).length) {
34
+ sourceContext.parsedBody = req.body;
35
+
36
+ try {
37
+ inputAnalysis.handleParsedBody(sourceContext, req.body);
38
+ } catch (err) {
39
+ if (isSecurityException(err)) {
40
+ securityException = err;
41
+ } else {
42
+ logger.error({ err }, 'Unexpected error during input analysis');
47
43
  }
48
44
  }
49
45
  }
@@ -96,11 +92,7 @@ module.exports = (core) => {
96
92
  function contrastHooked(...args) {
97
93
  const parser = fn.original(...args);
98
94
  const hookedParser = function (req, res, next) {
99
- if (!req.body) {
100
- parser(req, res, next);
101
- } else {
102
- parser(req, res, contrastNext(req, next, fnName));
103
- }
95
+ parser(req, res, contrastNext(req, next, fnName));
104
96
  };
105
97
 
106
98
  Object.defineProperty(hookedParser, 'name', {
@@ -23,7 +23,7 @@ module.exports = (core) => {
23
23
  depHooks,
24
24
  patcher,
25
25
  logger,
26
- scopes: { sources },
26
+ protect,
27
27
  protect: { inputAnalysis },
28
28
  } = core;
29
29
 
@@ -40,23 +40,21 @@ module.exports = (core) => {
40
40
  const [req, , origNext] = data.args;
41
41
 
42
42
  function contrastNext(origErr) {
43
- const sourceContext = sources.getStore()?.protect;
43
+ const sourceContext = protect.getSourceContext('cookie-parser');
44
+
44
45
  let securityException;
45
46
 
46
- if (!sourceContext) {
47
- logger.debug('source context not available in `cookie-parser` hook');
48
- } else {
49
- if ((req.cookies && Object.keys(req.cookies).length) || (req.signedCookies && Object.keys(req.signedCookies).length)) {
50
- sourceContext.parsedCookies = { ...req.cookies, ...req.signedCookies };
47
+ if (sourceContext
48
+ && ((req.cookies && Object.keys(req.cookies).length) || (req.signedCookies && Object.keys(req.signedCookies).length))) {
49
+ sourceContext.parsedCookies = { ...req.cookies, ...req.signedCookies };
51
50
 
52
- try {
53
- inputAnalysis.handleCookies(sourceContext, sourceContext.parsedCookies);
54
- } catch (err) {
55
- if (isSecurityException(err)) {
56
- securityException = err;
57
- } else {
58
- logger.error({ err }, 'Unexpected error during input analysis');
59
- }
51
+ try {
52
+ inputAnalysis.handleCookies(sourceContext, sourceContext.parsedCookies);
53
+ } catch (err) {
54
+ if (isSecurityException(err)) {
55
+ securityException = err;
56
+ } else {
57
+ logger.error({ err }, 'Unexpected error during input analysis');
60
58
  }
61
59
  }
62
60
  }
@@ -28,7 +28,7 @@ module.exports = (core) => {
28
28
  depHooks,
29
29
  patcher,
30
30
  logger,
31
- scopes: { sources },
31
+ protect,
32
32
  protect: { inputAnalysis },
33
33
  } = core;
34
34
 
@@ -47,16 +47,13 @@ module.exports = (core) => {
47
47
  const [req, , origNext] = data.args;
48
48
 
49
49
  function contrastNext(origErr) {
50
- const sourceContext = sources.getStore()?.protect;
50
+ const sourceContext = protect.getSourceContext('Express.query');
51
51
  let securityException;
52
52
 
53
- if (!sourceContext) {
54
- logger.debug('source context not available in `Express.query` hook');
55
-
56
- // It is possible for the query to be already parsed by `qs`
57
- // which means that we've already handled/analyzed it.
58
- // So we check whether we already have the `parsedQuery` property in the context
59
- } else if (req.query && Object.keys(req.query).length && (!('parsedQuery' in sourceContext))) {
53
+ // It is possible for the query to be already parsed by `qs`
54
+ // which means that we've already handled/analyzed it.
55
+ // So we check whether we already have the `parsedQuery` property in the context
56
+ if (sourceContext && req.query && Object.keys(req.query).length && (!('parsedQuery' in sourceContext))) {
60
57
  sourceContext.parsedQuery = req.query;
61
58
 
62
59
  try {
@@ -87,11 +84,9 @@ module.exports = (core) => {
87
84
  patchType,
88
85
  pre(data) {
89
86
  const { obj: { params } } = data;
90
- const sourceContext = sources.getStore()?.protect;
87
+ const sourceContext = protect.getSourceContext('Express.Layer.handle_request');
91
88
 
92
- if (!sourceContext) {
93
- logger.debug('source context not available in `express.Layer.prototype.handle_request` hook');
94
- } else if (params && Object.keys(params).length) {
89
+ if (sourceContext && params && Object.keys(params).length) {
95
90
  sourceContext.parsedParams = params;
96
91
  inputAnalysis.handleUrlParams(sourceContext, params);
97
92
  }
@@ -28,7 +28,7 @@ module.exports = (core) => {
28
28
  depHooks,
29
29
  patcher,
30
30
  logger,
31
- scopes: { sources },
31
+ protect,
32
32
  protect: { inputAnalysis },
33
33
  } = core;
34
34
 
@@ -62,12 +62,11 @@ module.exports = (core) => {
62
62
  * @param {Function} done callback to signal the hook is finished.
63
63
  */
64
64
  function preValidationHook(request, reply, done) {
65
- const sourceContext = sources.getStore()?.protect;
65
+ const sourceContext = protect.getSourceContext('Fastify.preValidationHook');
66
+
66
67
  let securityException;
67
68
 
68
- if (!sourceContext) {
69
- logger.debug('source context not available in fastify prevalidation hook');
70
- } else {
69
+ if (sourceContext) {
71
70
  try {
72
71
  if (request.params) {
73
72
  sourceContext.parsedParams = request.params;
@@ -22,7 +22,7 @@ module.exports = (core) => {
22
22
  depHooks,
23
23
  patcher,
24
24
  logger,
25
- scopes: { sources },
25
+ protect,
26
26
  protect: { inputAnalysis },
27
27
  } = core;
28
28
 
@@ -36,12 +36,11 @@ module.exports = (core) => {
36
36
  const origCb = data.args[1];
37
37
 
38
38
  function hookedCb(...cbArgs) {
39
- const sourceContext = sources.getStore()?.protect;
39
+ const sourceContext = protect.getSourceContext('formidable');
40
+
40
41
  const [, fields, files] = cbArgs;
41
42
 
42
- if (!sourceContext) {
43
- logger.debug('source context not available in `formidable` hook');
44
- } else {
43
+ if (sourceContext) {
45
44
  if (fields) {
46
45
  sourceContext.parsedBody = fields;
47
46
  inputAnalysis.handleParsedBody(sourceContext, fields);
@@ -33,7 +33,6 @@ class HttpInstrumentation {
33
33
  this.config = core.config;
34
34
  this.logger = logger.child({ name: 'contrast:protect:input-analysis' });
35
35
  this.depHooks = core.depHooks;
36
- this.messages = core.messages;
37
36
  this.protect = core.protect;
38
37
  this.makeSourceContext = this.protect.makeSourceContext;
39
38
  this.maxBodySize = 16 * 1024 * 1024;
@@ -166,6 +165,7 @@ class HttpInstrumentation {
166
165
  const connectInputs = {
167
166
  headers: HttpInstrumentation.removeCookies(reqData.headers),
168
167
  uriPath: reqData.uriPath,
168
+ rawUrl: req.url,
169
169
  // TODO AGENT-203 - need to handle method-tampering rule.
170
170
  method: reqData.method,
171
171
  };
@@ -174,9 +174,18 @@ class HttpInstrumentation {
174
174
  if (reqData.standardUrlParsing) {
175
175
  connectInputs.queries = reqData.queries;
176
176
  }
177
+ if (inputAnalysis.virtualPatchesEvaluators?.length) {
178
+ store.protect.virtualPatchesEvaluators.push(...inputAnalysis.virtualPatchesEvaluators.map((e) => new Map(e)));
179
+ }
180
+ if (inputAnalysis.ipDenylist?.length) {
181
+ block = inputAnalysis.handleIpDenylist(store.protect, inputAnalysis.ipDenylist);
182
+ }
183
+ if (inputAnalysis.ipAllowlist?.length) {
184
+ const allowed = inputAnalysis.handleIpAllowlist(store.protect, inputAnalysis.ipAllowlist);
185
+ if (!block) Object.assign(store.protect, { allowed });
186
+ }
177
187
 
178
- block = inputAnalysis.handleConnect(store.protect, connectInputs);
179
-
188
+ block = block || inputAnalysis.handleConnect(store.protect, connectInputs);
180
189
  } catch (err) {
181
190
  this.logger.error({ err }, 'Error during input analysis');
182
191
  }
@@ -21,8 +21,7 @@ module.exports = (core) => {
21
21
  const {
22
22
  depHooks,
23
23
  patcher,
24
- logger,
25
- scopes: { sources },
24
+ protect,
26
25
  protect: { inputAnalysis },
27
26
  } = core;
28
27
 
@@ -39,15 +38,11 @@ module.exports = (core) => {
39
38
  const [ctx, origNext] = data.args;
40
39
 
41
40
  async function contrastNext(origErr) {
42
- const sourceContext = sources.getStore()?.protect;
41
+ const sourceContext = protect.getSourceContext('koa-body');
43
42
 
44
- if (!sourceContext) {
45
- logger.debug('source context not available in `koa-body` hook');
46
- } else {
47
- if (ctx.request.body && Object.keys(ctx.request.body).length) {
48
- sourceContext.parsedBody = ctx.request.body;
49
- inputAnalysis.handleParsedBody(sourceContext, ctx.request.body);
50
- }
43
+ if (sourceContext && ctx.request.body && Object.keys(ctx.request.body).length) {
44
+ sourceContext.parsedBody = ctx.request.body;
45
+ inputAnalysis.handleParsedBody(sourceContext, ctx.request.body);
51
46
  }
52
47
 
53
48
  await origNext(origErr);
@@ -21,8 +21,7 @@ module.exports = (core) => {
21
21
  const {
22
22
  depHooks,
23
23
  patcher,
24
- logger,
25
- scopes: { sources },
24
+ protect,
26
25
  protect: { inputAnalysis },
27
26
  } = core;
28
27
 
@@ -39,15 +38,12 @@ module.exports = (core) => {
39
38
  const [ctx, origNext] = data.args;
40
39
 
41
40
  async function contrastNext(origErr) {
42
- const sourceContext = sources.getStore()?.protect;
41
+ const sourceContext = protect.getSourceContext('koa-bodyparser');
43
42
 
44
- if (!sourceContext) {
45
- logger.debug('source context not available in `koa-bodyparser` hook');
46
- } else {
47
- if (ctx.request.body && Object.keys(ctx.request.body).length) {
48
- sourceContext.parsedBody = ctx.request.body;
49
- inputAnalysis.handleParsedBody(sourceContext, ctx.request.body);
50
- }
43
+
44
+ if (sourceContext && ctx.request.body && Object.keys(ctx.request.body).length) {
45
+ sourceContext.parsedBody = ctx.request.body;
46
+ inputAnalysis.handleParsedBody(sourceContext, ctx.request.body);
51
47
  }
52
48
 
53
49
  await origNext(origErr);
@@ -26,8 +26,7 @@ module.exports = (core) => {
26
26
  const {
27
27
  depHooks,
28
28
  patcher,
29
- logger,
30
- scopes: { sources },
29
+ protect,
31
30
  protect: { inputAnalysis },
32
31
  } = core;
33
32
 
@@ -38,11 +37,9 @@ module.exports = (core) => {
38
37
  depHooks.resolve({ name: 'koa', version: '>=2.3.0' }, (Koa) => {
39
38
  function contrastStartMiddleware(ctx, next) {
40
39
  if (ctx.query && Object.keys(ctx.query).length) {
41
- const sourceContext = sources.getStore()?.protect;
40
+ const sourceContext = protect.getSourceContext('Koa startMiddleware');
42
41
 
43
- if (!sourceContext) {
44
- logger.debug('source context not available in `qs` hook');
45
- } else if (!('parsedQuery' in sourceContext)) {
42
+ if (sourceContext && !('parsedQuery' in sourceContext)) {
46
43
  sourceContext.parsedQuery = ctx.query;
47
44
  inputAnalysis.handleQueryParams(sourceContext, ctx.query);
48
45
  }
@@ -76,15 +73,11 @@ module.exports = (core) => {
76
73
  name: `[${router}].layer.prototype`,
77
74
  patchType,
78
75
  post({ result }) {
79
- const sourceContext = sources.getStore()?.protect;
80
-
81
- if (!sourceContext) {
82
- logger.debug(`source context not available in \`[${router}].layer\` hook`);
83
- } else {
84
- if (Object.keys(result).length) {
85
- sourceContext.parsedParams = result;
86
- inputAnalysis.handleUrlParams(sourceContext, result);
87
- }
76
+ const sourceContext = protect.getSourceContext(`[${router}].layer`);
77
+
78
+ if (sourceContext && Object.keys(result).length) {
79
+ sourceContext.parsedParams = result;
80
+ inputAnalysis.handleUrlParams(sourceContext, result);
88
81
  }
89
82
  }
90
83
  });
@@ -107,15 +100,11 @@ module.exports = (core) => {
107
100
  const [ctx, origNext] = data.args;
108
101
 
109
102
  async function contrastNext(origErr) {
110
- const sourceContext = sources.getStore()?.protect;
111
-
112
- if (!sourceContext) {
113
- logger.debug('source context not available in `koa-cookie` hook');
114
- } else {
115
- if (ctx.cookie) {
116
- sourceContext.parsedCookies = ctx.cookie;
117
- inputAnalysis.handleCookies(sourceContext, ctx.cookie);
118
- }
103
+ const sourceContext = protect.getSourceContext('koa-cookie');
104
+
105
+ if (sourceContext && ctx.cookie) {
106
+ sourceContext.parsedCookies = ctx.cookie;
107
+ inputAnalysis.handleCookies(sourceContext, ctx.cookie);
119
108
  }
120
109
 
121
110
  await origNext(origErr);
@@ -23,7 +23,8 @@ module.exports = (core) => {
23
23
  depHooks,
24
24
  patcher,
25
25
  logger,
26
- scopes: { sources, wrap },
26
+ scopes: { wrap },
27
+ protect,
27
28
  protect: { inputAnalysis },
28
29
  } = core;
29
30
 
@@ -41,11 +42,9 @@ module.exports = (core) => {
41
42
 
42
43
  // We are getting the sourceContext here because in the time of calling
43
44
  // the contrastNext() method the context is lost
44
- const sourceContext = sources.getStore()?.protect;
45
- if (!sourceContext) {
46
- logger.debug('source context not available in `multer` hook');
47
- return;
48
- }
45
+ const sourceContext = protect.getSourceContext('multer');
46
+
47
+ if (!sourceContext) return;
49
48
 
50
49
  function contrastNext(origErr) {
51
50
  let securityException;
@@ -21,8 +21,7 @@ module.exports = (core) => {
21
21
  const {
22
22
  depHooks,
23
23
  patcher,
24
- logger,
25
- scopes: { sources },
24
+ protect,
26
25
  protect: { inputAnalysis },
27
26
  } = core;
28
27
 
@@ -34,16 +33,13 @@ module.exports = (core) => {
34
33
  patchType,
35
34
  post({ args, result }) {
36
35
  if (result && Object.keys(result).length) {
37
- const sourceContext = sources.getStore()?.protect;
36
+ const sourceContext = protect.getSourceContext('qs');
38
37
 
39
- if (!sourceContext) {
40
- logger.debug('source context not available in `qs` hook');
41
-
42
- // We need to run analysis for the `qs` result only when it's used as a query parser.
43
- // `qs` is used also for parsing bodies, but these cases we handle individually with
44
- // the respective library that's using it (e.g. `formidable`, `co-body`) because in
45
- // some cases its use is optional and we cannot rely on it.
46
- } else if (sourceContext.reqData?.queries === args[0]) {
38
+ // We need to run analysis for the `qs` result only when it's used as a query parser.
39
+ // `qs` is used also for parsing bodies, but these cases we handle individually with
40
+ // the respective library that's using it (e.g. `formidable`, `co-body`) because in
41
+ // some cases its use is optional and we cannot rely on it.
42
+ if (sourceContext && sourceContext.reqData?.queries === args[0]) {
47
43
  sourceContext.parsedQuery = result;
48
44
  inputAnalysis.handleQueryParams(sourceContext, result);
49
45
  }
@@ -21,12 +21,10 @@ module.exports = (core) => {
21
21
  const {
22
22
  depHooks,
23
23
  patcher,
24
- logger,
25
- scopes: { sources },
24
+ protect,
26
25
  protect: { inputAnalysis },
27
26
  } = core;
28
27
 
29
-
30
28
  // Patch `universal-cookie` package
31
29
  function install() {
32
30
  depHooks.resolve({ name: 'universal-cookie', file: 'cjs/utils.js' }, (uCookieUtils) => patcher.patch(uCookieUtils, 'parseCookies', {
@@ -34,11 +32,9 @@ module.exports = (core) => {
34
32
  patchType,
35
33
  post({ result }) {
36
34
  if (result && Object.keys(result).length) {
37
- const sourceContext = sources.getStore()?.protect;
35
+ const sourceContext = protect.getSourceContext('universal-cookie');
38
36
 
39
- if (!sourceContext) {
40
- logger.debug('source context not available in `universal-cookie` hook');
41
- } else {
37
+ if (sourceContext) {
42
38
  sourceContext.parsedCookies = result;
43
39
  inputAnalysis.handleCookies(sourceContext, result);
44
40
  }
@@ -0,0 +1,76 @@
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 { Event } = require('@contrast/common');
19
+ const address = require('ipaddr.js');
20
+
21
+ module.exports = (core) => {
22
+ const {
23
+ messages,
24
+ protect: { inputAnalysis },
25
+ } = core;
26
+
27
+ const ipAllowlist = inputAnalysis.ipAllowlist = [];
28
+ const ipDenylist = inputAnalysis.ipDenylist = [];
29
+
30
+ messages.on(Event.SERVER_SETTINGS_UPDATE, (serverUpdate) => {
31
+ const now = new Date().getTime();
32
+ const updatedIpAllowList = serverUpdate.features?.defend?.ipAllowlist.map((ipEntry) => ipEntryMap(ipEntry, now));
33
+ const updatedIpDenyList = serverUpdate.features?.defend?.ipDenylist.map((ipEntry) => ipEntryMap(ipEntry, now));
34
+
35
+ if (updatedIpAllowList) {
36
+ ipAllowlist.length = 0;
37
+ ipAllowlist.push(...updatedIpAllowList);
38
+ }
39
+ if (updatedIpDenyList) {
40
+ ipDenylist.length = 0;
41
+ ipDenylist.push(...updatedIpDenyList);
42
+ }
43
+ });
44
+ };
45
+
46
+ function ipEntryMap(ipEntry, startTime) {
47
+ const { ip, expires } = ipEntry;
48
+ let doesExpire, expiresAt, cidr;
49
+
50
+ if (expires) {
51
+ doesExpire = true;
52
+ expiresAt = startTime + expires;
53
+ } else {
54
+ doesExpire = false;
55
+ }
56
+
57
+ const slashIdx = ip.indexOf('/');
58
+ const isCIDR = slashIdx >= 0;
59
+ const ipInstance = isCIDR
60
+ ? address.process(ip.substr(0, slashIdx))
61
+ : address.process(ip);
62
+
63
+ const normalizedValue = ipInstance.toNormalizedString();
64
+
65
+ if (isCIDR) {
66
+ cidr = { range: address.parseCIDR(ip), kind: ipInstance.kind() };
67
+ }
68
+
69
+ return {
70
+ ...ipEntry,
71
+ doesExpire,
72
+ expiresAt,
73
+ normalizedValue,
74
+ cidr
75
+ };
76
+ }
@@ -0,0 +1,109 @@
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 { Event } = require('@contrast/common');
19
+
20
+ module.exports = (core) => {
21
+ const {
22
+ messages,
23
+ protect: { inputAnalysis },
24
+ } = core;
25
+
26
+ const virtualPatchesEvaluators = inputAnalysis.virtualPatchesEvaluators = [];
27
+
28
+ messages.on(Event.SERVER_SETTINGS_UPDATE, (serverUpdate) => {
29
+ const virtualPatches = serverUpdate.settings?.defend.virtualPatches;
30
+ if (virtualPatches) {
31
+ buildVPEvaluators(virtualPatches, virtualPatchesEvaluators);
32
+ }
33
+ });
34
+ };
35
+
36
+ function buildVPEvaluators(virtualPatches, evaluatorsArray) {
37
+ evaluatorsArray.length = 0;
38
+ for (const { headers, parameters, urls, uuid, name } of virtualPatches) {
39
+ const evaluators = new Map();
40
+
41
+ if (headers?.length) {
42
+ evaluators.set('HEADERS', (reqHeaders) => {
43
+ let result;
44
+ for (const { evaluation, name, value } of headers) {
45
+ const evalCheck = buildEvaluationCheck(evaluation);
46
+ const keyIndex = reqHeaders.indexOf(name.toLowerCase());
47
+
48
+ result = keyIndex !== -1 && evalCheck(reqHeaders[keyIndex + 1], value);
49
+ if (!result) break;
50
+ }
51
+
52
+ return result;
53
+ });
54
+ }
55
+
56
+ if (parameters?.length) {
57
+ evaluators.set('PARAMETERS', (reqParameters) => {
58
+ let result;
59
+ for (const { evaluation, name, value } of parameters) {
60
+ const evalCheck = buildEvaluationCheck(evaluation);
61
+
62
+ result = evalCheck(reqParameters[name], value);
63
+ if (!result) break;
64
+ }
65
+
66
+ return result;
67
+ });
68
+ }
69
+
70
+ if (urls?.length) {
71
+ evaluators.set('URLS', (reqUrl) => {
72
+ let result;
73
+ for (const { evaluation, value } of urls) {
74
+ const evalCheck = buildEvaluationCheck(evaluation);
75
+
76
+ result = evalCheck(reqUrl, value);
77
+ if (!result) break;
78
+ }
79
+
80
+ return result;
81
+ });
82
+ }
83
+
84
+ if (evaluators.size) {
85
+ evaluators.set('metadata', { name, uuid });
86
+ evaluatorsArray.push(evaluators);
87
+ }
88
+ }
89
+ }
90
+
91
+ function buildEvaluationCheck(evaluation) {
92
+ switch (evaluation) {
93
+ case 'MATCHES':
94
+ return (reqValue, matchedValue) => new RegExp(matchedValue, 'i').test(reqValue);
95
+ case 'EQUALS':
96
+ return (reqValue, matchedValue) => reqValue.toString() === matchedValue.toString();
97
+ case 'CONTAINS':
98
+ return (reqValue, matchedValue) => reqValue.includes(matchedValue);
99
+ case 'DOESNT_MATCH':
100
+ return (reqValue, matchedValue) => !new RegExp(matchedValue, 'i').test(reqValue);
101
+ // This is a typo but it is how it's passed from ContrastUI
102
+ case 'DOESNT_EQUALS':
103
+ return (reqValue, matchedValue) => reqValue.toString() !== matchedValue.toString();
104
+ case 'DOESNT_CONTAIN':
105
+ return (reqValue, matchedValue) => !reqValue.includes(matchedValue);
106
+ default:
107
+ return () => false;
108
+ }
109
+ }