@contrast/protect 1.4.0 → 1.6.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.
@@ -0,0 +1,106 @@
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
+ const { isSecurityException } = require('../../security-exception');
20
+
21
+ module.exports = (core) => {
22
+ const {
23
+ depHooks,
24
+ patcher,
25
+ logger,
26
+ protect,
27
+ protect: { inputAnalysis },
28
+ } = core;
29
+
30
+ /**
31
+ * registers a depHook for hapi module instrumentation
32
+ */
33
+ function install() {
34
+ depHooks.resolve(
35
+ { name: 'hapi', version: '>=18 <21' },
36
+ registerServerHandler
37
+ );
38
+ depHooks.resolve(
39
+ { name: '@hapi/hapi', version: '>=18 <21' },
40
+ registerServerHandler
41
+ );
42
+ }
43
+
44
+ const registerServerHandler = (hapi) => {
45
+ patcher.patch(hapi, 'server', {
46
+ name: 'hapi.server',
47
+ patchType,
48
+ post(data) {
49
+ const server = data.result;
50
+ if (server) {
51
+ instrumentServer(server);
52
+ } else {
53
+ logger.error('Hapi Server is called but there is no server instance!');
54
+ }
55
+ }
56
+ });
57
+ };
58
+
59
+ const instrumentServer = (server) => {
60
+ server.ext('onPreStart', function onPreStart(core) {
61
+ logger.info('hapi version %s', core.version);
62
+ });
63
+
64
+ server.ext('onPreHandler', function onPreHandler(req, h) {
65
+ const sourceContext = protect.getSourceContext('hapi.onPreHandler');
66
+
67
+ if (sourceContext) {
68
+ try {
69
+ if (req.params && Object.keys(req.params).length) {
70
+ sourceContext.parsedParams = req.params;
71
+ inputAnalysis.handleUrlParams(sourceContext, req.params);
72
+ }
73
+
74
+ if (req.cookies && Object.keys(req.cookies).length) {
75
+ sourceContext.parsedCookies = req.cookies;
76
+ inputAnalysis.handleCookies(sourceContext, req.cookies);
77
+ }
78
+
79
+ if (req.payload && Object.keys(req.payload).length) {
80
+ sourceContext.parsedBody = req.payload;
81
+ inputAnalysis.handleParsedBody(sourceContext, req.payload);
82
+ }
83
+
84
+ if (req.query && Object.keys(req.query).length) {
85
+ sourceContext.parsedQuery = req.query;
86
+ inputAnalysis.handleQueryParams(sourceContext, req.query);
87
+ }
88
+ } catch (err) {
89
+ if (isSecurityException(err)) {
90
+ throw err;
91
+ } else {
92
+ logger.error({ err }, 'Unexpected error during input analysis');
93
+ }
94
+ }
95
+ }
96
+
97
+ return h.continue;
98
+ });
99
+ };
100
+
101
+ const hapiInstrumentation = inputAnalysis.hapiInstrumentation = {
102
+ install
103
+ };
104
+
105
+ return hapiInstrumentation;
106
+ };
@@ -27,13 +27,13 @@ module.exports = function(core) {
27
27
  };
28
28
  class HttpInstrumentation {
29
29
  constructor(core) {
30
- const { logger } = core;
31
30
  this.messages = core.messages;
32
31
  this.scope = core.scopes.sources;
33
32
  this.config = core.config;
34
- this.logger = logger.child({ name: 'contrast:protect:input-analysis' });
33
+ this.logger = core.logger.child('contrast:protect:input-analysis');
35
34
  this.depHooks = core.depHooks;
36
35
  this.protect = core.protect;
36
+ this.patcher = core.patcher;
37
37
  this.makeSourceContext = this.protect.makeSourceContext;
38
38
  this.maxBodySize = 16 * 1024 * 1024;
39
39
  this.installed = false;
@@ -51,6 +51,7 @@ class HttpInstrumentation {
51
51
  this.installed = true;
52
52
  this.hookHttp();
53
53
  this.hookHttps();
54
+ this.hookHttp2();
54
55
  }
55
56
 
56
57
  uninstall() {
@@ -62,7 +63,7 @@ class HttpInstrumentation {
62
63
  */
63
64
  hookHttp() {
64
65
  this.logger.debug('hooking library: http');
65
- this.depHooks.resolve({ name: 'http' }, this.hookServer.bind(this));
66
+ this.depHooks.resolve({ name: 'http' }, (http) => this.hookServerEmit.call(this, http, 'httpServer'));
66
67
  }
67
68
 
68
69
  /**
@@ -70,36 +71,78 @@ class HttpInstrumentation {
70
71
  */
71
72
  hookHttps() {
72
73
  this.logger.debug('hooking library: https');
73
- this.depHooks.resolve({ name: 'https' }, this.hookServer.bind(this));
74
+ this.depHooks.resolve({ name: 'https' }, (https) => this.hookServerEmit.call(this, https, 'httpsServer'));
74
75
  }
75
76
 
76
77
  /**
77
- * Instruments the `Server` prototype from `http(s)`. This patches `emit` and
78
+ * Sets hooks to instrument `http2 Servers`.
79
+ */
80
+ hookHttp2() {
81
+ this.logger.debug('hooking library: http2');
82
+ // http2 library does not expose its Server class, so we need to hook the createServer function
83
+ this.depHooks.resolve({ name: 'http2' }, (http2) => this.hookCreateServer.call(this, http2, 'http2Server'));
84
+ this.depHooks.resolve({ name: 'http2' }, (http2) => this.hookCreateServer.call(this, http2, 'http2SecureServer', 'createSecureServer'));
85
+
86
+ this.logger.debug('hooking library: spdy');
87
+ this.depHooks.resolve({ name: 'spdy' }, (spdy) => this.hookServerEmit.call(this, spdy, 'spdyServer'));
88
+ }
89
+
90
+ /**
91
+ * Instruments the `Server` prototype from `http(s)` or spdy's http2 Server. This patches `emit` and
92
+ * invokes the protect service to do analysis when appropriate.
93
+ */
94
+ hookServerEmit(serverSource, sourceName) {
95
+ serverSource.Server.prototype = this.patcher.patch(serverSource.Server.prototype, 'emit', {
96
+ name: `${sourceName}.Server.prototype.emit`,
97
+ patchType: 'initiate-handling',
98
+ around: this.emitAroundHook.bind(this)
99
+ });
100
+ }
101
+
102
+ /**
103
+ * Instruments the `Http2Server` prototype which results from the http2.createServer/createSecureServer() call.
104
+ * This also patches `emit` and
78
105
  * invokes the protect service to do analysis when appropriate.
79
- *
80
- * @param {Object} xport The http(s) module export
81
106
  */
82
- hookServer(xport) {
107
+ hookCreateServer(serverSource, sourceName, constructorName = 'createServer') {
83
108
  const self = this;
84
109
 
85
- const {
86
- Server: {
87
- prototype: { emit }
88
- }
89
- } = xport;
110
+ return this.patcher.patch(serverSource, constructorName, {
111
+ name: sourceName,
112
+ patchType: 'initiate-handling',
113
+ post(data) {
114
+
115
+ const { result: server } = data;
116
+ const serverPrototype = server ? Object.getPrototypeOf(server) : null;
90
117
 
91
- xport.Server.prototype.emit = function(...args) {
92
- const [type] = args;
118
+ if (!serverPrototype) {
119
+ self.logger.error('Unable to patch server prototype, continue without instrumentation');
120
+ return;
121
+ }
93
122
 
94
- if (type !== 'request') {
95
- return emit.call(this, ...args);
123
+ self.patcher.patch(serverPrototype, 'emit', {
124
+ name: `${sourceName}.Server.prototype.emit`,
125
+ patchType: 'req-async-storage',
126
+ around: self.emitAroundHook.bind(self)
127
+ });
96
128
  }
129
+ });
130
+ }
97
131
 
98
- const context = { instance: this, method: emit, args };
99
- self.initiateRequestHandling(context);
132
+ /**
133
+ * The around hook for `emit` that
134
+ * invokes the protect service to do analysis when appropriate.
135
+ */
136
+ emitAroundHook(next, data) {
137
+ const [type] = data.args;
100
138
 
101
- return !!this._events[type];
102
- };
139
+ if (type !== 'request') {
140
+ return next();
141
+ }
142
+
143
+ const context = { instance: data.obj, method: next, args: data.args };
144
+ this.initiateRequestHandling(context);
145
+ return !!data.obj._events[type];
103
146
  }
104
147
 
105
148
  /**
@@ -117,15 +160,6 @@ class HttpInstrumentation {
117
160
  args: [, req, res]
118
161
  } = fnContext;
119
162
 
120
- // URL exclusions should be applied here. there is no point in doing any additional
121
- // work if the url is excluded for a particular rule, i.e., that rule should be removed
122
- // from the list of rules for this request. and if all rules are excluded for this url
123
- // then none of the following needs to be done.
124
- if (this.protect.rules.agentLibRulesMask === 0) {
125
- this.logger.debug('no agent-lib rules are enabled, not checking request');
126
- return;
127
- }
128
-
129
163
  let store;
130
164
  let block;
131
165
 
@@ -136,15 +170,20 @@ class HttpInstrumentation {
136
170
  // so that an async context is present.
137
171
  store = this.scope.getStore();
138
172
  // nothing can be done if async context is not available.
173
+
139
174
  if (!store) {
140
175
  this.logger.debug('cannot acquire store for initiateRequestHandling()');
176
+ setImmediate(() => method.call(instance, ...args));
141
177
  return;
142
178
  }
143
179
 
144
180
  store.protect = this.makeSourceContext(req, res);
145
181
  const { reqData } = store.protect;
146
182
 
147
- res.on('finish', () => messages.emit(Event.PROTECT, store));
183
+ res.on('finish', () => {
184
+ this.protect.inputAnalysis.handleRequestEnd(store.protect);
185
+ messages.emit(Event.PROTECT, store);
186
+ });
148
187
 
149
188
  // don't put inputs in the store; they are a param to each handler. findings
150
189
  // associated with inputs do go into the store. why not put the inputs
@@ -169,17 +208,21 @@ class HttpInstrumentation {
169
208
  // TODO AGENT-203 - need to handle method-tampering rule.
170
209
  method: reqData.method,
171
210
  };
211
+
172
212
  // only add queries if it's known that 'qs' or equivalent won't be used.
173
213
  /* c8 ignore next 3 */
174
214
  if (reqData.standardUrlParsing) {
175
215
  connectInputs.queries = reqData.queries;
176
216
  }
217
+
177
218
  if (inputAnalysis.virtualPatchesEvaluators?.length) {
178
219
  store.protect.virtualPatchesEvaluators.push(...inputAnalysis.virtualPatchesEvaluators.map((e) => new Map(e)));
179
220
  }
221
+
180
222
  if (inputAnalysis.ipDenylist?.length) {
181
223
  block = inputAnalysis.handleIpDenylist(store.protect, inputAnalysis.ipDenylist);
182
224
  }
225
+
183
226
  if (inputAnalysis.ipAllowlist?.length) {
184
227
  const allowed = inputAnalysis.handleIpAllowlist(store.protect, inputAnalysis.ipAllowlist);
185
228
  if (!block) Object.assign(store.protect, { allowed });
@@ -16,7 +16,12 @@
16
16
  'use strict';
17
17
 
18
18
  const util = require('util');
19
- const { BLOCKING_MODES, isString, simpleTraverse } = require('@contrast/common');
19
+ const {
20
+ ProtectRuleMode: { OFF },
21
+ BLOCKING_MODES,
22
+ isString,
23
+ simpleTraverse
24
+ } = require('@contrast/common');
20
25
 
21
26
  module.exports = function(core) {
22
27
  const { protect: { agentLib, inputTracing, throwSecurityException } } = core;
@@ -24,7 +29,7 @@ module.exports = function(core) {
24
29
  function handleFindings(sourceContext, sinkContext, ruleId, result, findings) {
25
30
  result.details.push({ sinkContext, findings });
26
31
 
27
- const { mode } = sourceContext.rules.agentLibRules[ruleId];
32
+ const mode = sourceContext.policy[ruleId];
28
33
 
29
34
  if (BLOCKING_MODES.includes(mode)) {
30
35
  result.blocked = true;
@@ -215,7 +220,7 @@ module.exports = function(core) {
215
220
  * @returns {AnalysisResult[]}
216
221
  */
217
222
  function getResultsByRuleId(ruleId, context) {
218
- if (context.rules.agentLibRules[ruleId].mode === 'off') {
223
+ if (context.policy[ruleId] === OFF) {
219
224
  return;
220
225
  }
221
226
  return context.findings.resultsMap[ruleId];
@@ -15,21 +15,15 @@
15
15
 
16
16
  'use strict';
17
17
 
18
- /**
19
- * INPUT TRACING is a STAGE of Protect.
20
- * The specification can be found here https://protect-spec.prod.dotnet.contsec.com/guide/input-tracing.html.
21
- *
22
- * To view other STAGES see https://protect-spec.prod.dotnet.contsec.com/guide/protect-types.html#protection-types
23
- * @param {object} core composed dependencies
24
- * @returns {object}
25
- */
18
+ const { installChildComponentsSync } = require('@contrast/common');
19
+
26
20
  module.exports = function(core) {
27
21
  const inputTracing = core.protect.inputTracing = {};
28
22
 
29
- // load the interfaces that will be used by input tracing instrumentation
23
+ // api
30
24
  require('./handlers')(core);
31
25
 
32
- // load the instrumentation installers
26
+ // instrumentation
33
27
  require('./install/child-process')(core);
34
28
  require('./install/fs')(core);
35
29
  require('./install/mongodb')(core);
@@ -38,17 +32,13 @@ module.exports = function(core) {
38
32
  require('./install/sequelize')(core);
39
33
  require('./install/sqlite3')(core);
40
34
  require('./install/http')(core);
35
+ require('./install/vm')(core);
36
+ require('./install/eval')(core);
37
+ require('./install/function')(core);
38
+ // TODO: NODE-2360 (oracledb)
41
39
 
42
40
  inputTracing.install = function() {
43
- inputTracing.cpInstrumentation.install();
44
- inputTracing.fsInstrumentation.install();
45
- inputTracing.mongodbInstrumentation.install();
46
- inputTracing.mysqlInstrumentation.install();
47
- inputTracing.postgresInstrumentation.install();
48
- inputTracing.sequelizeInstrumentation.install();
49
- inputTracing.sqlite3Instrumentation.install();
50
- inputTracing.httpInstrumentation.install();
51
- // TODO: NODE-2360 (2260?)
41
+ installChildComponentsSync(inputTracing);
52
42
  };
53
43
 
54
44
  return inputTracing;
@@ -54,6 +54,7 @@ module.exports = function(core) {
54
54
  core.protect.semanticAnalysis.handleCommandInjectionCommandBackdoors(sourceContext, sinkContext);
55
55
  core.protect.semanticAnalysis.handleCmdInjectionSemanticChainedCommands(sourceContext, sinkContext);
56
56
  core.protect.semanticAnalysis.handleCmdInjectionSemanticDangerous(sourceContext, sinkContext);
57
+ core.protect.semanticAnalysis.handlePathTraversalFileSecurityBypass(sourceContext, sinkContext);
57
58
  }
58
59
  });
59
60
  });
@@ -29,13 +29,13 @@ module.exports = function(core) {
29
29
  } = core;
30
30
 
31
31
  function install() {
32
- if (!global.ContrastMethods.__contrastEval) {
32
+ if (!global.ContrastMethods.eval) {
33
33
  logger.error('Cannot install `eval` instrumentation - Contrast method DNE');
34
34
  return;
35
35
  }
36
36
 
37
- patcher.patch(global.ContrastMethods, '__contrastEval', {
38
- name: 'global.ContrastMethods.__contrastEval',
37
+ patcher.patch(global.ContrastMethods, 'eval', {
38
+ name: 'global.ContrastMethods.eval',
39
39
  patchType,
40
40
  pre: ({ args, hooked, orig }) => {
41
41
  if (instrumentation.isLocked()) return;
@@ -106,6 +106,7 @@ module.exports = function(core) {
106
106
  { constructorOpt: hooked, prependFrames: [orig] }
107
107
  );
108
108
  inputTracing.handlePathTraversal(sourceContext, sinkContext);
109
+ core.protect.semanticAnalysis.handlePathTraversalFileSecurityBypass(sourceContext, sinkContext);
109
110
  }
110
111
  }
111
112
  });
@@ -0,0 +1,62 @@
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 { isString } = require('@contrast/common');
19
+ const { patchType } = require('../constants');
20
+
21
+ module.exports = function(core) {
22
+ const {
23
+ logger,
24
+ scopes: { instrumentation },
25
+ patcher,
26
+ captureStacktrace,
27
+ protect,
28
+ protect: { inputTracing }
29
+ } = core;
30
+
31
+ function install() {
32
+ if (!global.ContrastMethods.Function) {
33
+ logger.error('Cannot install `Function` instrumentation - Contrast method DNE');
34
+ return;
35
+ }
36
+
37
+ Object.assign(global.ContrastMethods, {
38
+ Function: patcher.patch(global.ContrastMethods.Function, {
39
+ name: 'global.ContrastMethods.Function',
40
+ patchType,
41
+ pre: ({ args, hooked, orig }) => {
42
+ if (instrumentation.isLocked()) return;
43
+
44
+ const sourceContext = protect.getSourceContext('Function');
45
+ const fnBody = args[args.length - 1];
46
+
47
+ if (!sourceContext || !fnBody || !isString(fnBody)) return;
48
+
49
+ const sinkContext = captureStacktrace(
50
+ { name: 'Function', value: fnBody },
51
+ { constructorOpt: hooked, prependFrames: [orig] }
52
+ );
53
+ inputTracing.ssjsInjection(sourceContext, sinkContext);
54
+ }
55
+ })
56
+ });
57
+ }
58
+
59
+ const functionInstrumentation = inputTracing.functionInstrumentation = { install };
60
+
61
+ return functionInstrumentation;
62
+ };
@@ -16,6 +16,7 @@
16
16
  'use strict';
17
17
 
18
18
  module.exports = function(core) {
19
+ const { protect } = core;
19
20
 
20
21
  function makeSourceContext(req, res) {
21
22
  // make the abstract request. it is an abstraction of a request that
@@ -79,9 +80,8 @@ module.exports = function(core) {
79
80
  // block closure captures res so it isn't exposed to beyond here
80
81
  block: core.protect.makeResponseBlocker(res),
81
82
 
82
- // this should be changed to capture only the rules applicable to this
83
- // particular request (if any route exclusions, etc.)
84
- rules: core.protect.rules,
83
+ policy: protect.getPolicy(),
84
+
85
85
  exclusions: [],
86
86
  virtualPatchesEvaluators: [],
87
87
 
@@ -98,54 +98,10 @@ module.exports = function(core) {
98
98
  semanticResultsMap: Object.create(null),
99
99
  serverFeaturesResultsMap: Object.create(null)
100
100
  },
101
-
102
- /*
103
- findings: {
104
- trackRequest: true,
105
- resultsList: [
106
- // Example 2
107
- {
108
- // return value from agent-lib
109
- value: 'kill -9 1',
110
- type: 'PARAMETER_VALUE',
111
- ruleId: 'cmd-injection',
112
- path: ['path', 'to', 'val'],
113
- // other data added during lifecycle
114
- // could we add these by mutating agent-lib return values?
115
- // What if there are multiple injections for the same value? The `details` value
116
- // could be an array in that case, or is this too complicated.
117
- blocked: false,
118
- details: [
119
- {
120
- context: {
121
- id: 'child_process.exec',
122
- get stack() {}, // lazy
123
- command: 'sudo kill -9 1',
124
- index: 5,
125
- }
126
- },
127
- {
128
- sinkId: 'child_process.exec',
129
- get stack() {}, // lazy
130
- command: 'sudo kill -9 1',
131
- index: 5,
132
- }]
133
- },
134
- ]
135
- }
136
- // (scoreAtom() returns only the ruleId and score because the caller supplied
137
- // the input and type; no key or path is known to scoreAtom(). code calling
138
- // scoreAtom() will need to augment the finding to match the above.)
139
- //
140
- // each finding is augmented with additional properties
141
- // - blocked: false // set to true if the finding causes the request to be blocked
142
- // - mappedId: ruleId // normalized ruleId, e.g., nosql-injection-mongo => nosql-injection
143
- // -
144
- // */
145
101
  };
146
102
 
147
103
  return protectStore;
148
104
  }
149
105
 
150
- core.protect.makeSourceContext = makeSourceContext;
106
+ return core.protect.makeSourceContext = makeSourceContext;
151
107
  };
package/lib/policy.js ADDED
@@ -0,0 +1,134 @@
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 {
19
+ Rule,
20
+ ProtectRuleMode: {
21
+ BLOCK_AT_PERIMETER,
22
+ BLOCK,
23
+ MONITOR,
24
+ OFF,
25
+ },
26
+ Rule: { BOT_BLOCKER },
27
+ Event: { SERVER_SETTINGS_UPDATE },
28
+ } = require('@contrast/common');
29
+
30
+
31
+ module.exports = function(core) {
32
+ const { config, logger, messages, protect } = core;
33
+ const policy = protect.policy = {};
34
+
35
+ function getModeFromConfig(ruleId) {
36
+ if (config.protect.disabled_rules.includes(ruleId)) {
37
+ return 'off';
38
+ }
39
+ return config.protect.rules?.[ruleId]?.mode;
40
+ }
41
+
42
+ /**
43
+ * Coerces ContrastUI mode names to match from common-agent-configuration
44
+ * @param {} remoteSetting
45
+ * @returns {string}
46
+ */
47
+ function readModeFromSetting(remoteSetting) {
48
+ switch (remoteSetting.mode) {
49
+ case 'OFF': return OFF;
50
+ case 'MONITORING': return MONITOR;
51
+ case 'BLOCKING': return remoteSetting.blockAtEntry ? BLOCK_AT_PERIMETER : BLOCK;
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Build out initial policy from configuration data
57
+ */
58
+ function initPolicy() {
59
+ for (const ruleId of Object.values(Rule)) {
60
+ policy[ruleId] = getModeFromConfig(ruleId) || OFF;
61
+ }
62
+ updateRulesMask();
63
+ }
64
+
65
+ /**
66
+ * When updates are given at runtime, we update our local rule policy while
67
+ * respecting rules of precedence.
68
+ * @param {[]} protectionRules
69
+ */
70
+ function updateFromProtectionRules(protectionRules) {
71
+ for (const remoteSetting of Object.values(protectionRules)) {
72
+ const { id: ruleId } = remoteSetting;
73
+
74
+ if (getModeFromConfig(ruleId)) {
75
+ continue;
76
+ }
77
+
78
+ policy[ruleId] = readModeFromSetting(remoteSetting);
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Rebuild rules mask based on which agent-lib rules are enabled
84
+ */
85
+ function updateRulesMask() {
86
+ let rulesMask = 0;
87
+ for (const [ruleId, mode] of Object.entries(policy)) {
88
+ if (protect.agentLib.RuleType[ruleId] && mode !== OFF) {
89
+ rulesMask = rulesMask | protect.agentLib.RuleType[ruleId];
90
+ }
91
+ }
92
+ policy.rulesMask = rulesMask;
93
+ }
94
+
95
+ /**
96
+ * This gets called by protect.makeSourceContext(). We return copy of policy to avoid
97
+ * inconsistent behavior if policy is updated during request handling.
98
+ */
99
+ function getPolicy() {
100
+ return { ...policy };
101
+ }
102
+
103
+ initPolicy();
104
+
105
+ messages.on(SERVER_SETTINGS_UPDATE, (remoteSettings) => {
106
+ let update;
107
+
108
+ const protectionRules = remoteSettings?.settings?.defend?.protectionRules;
109
+ if (protectionRules) {
110
+ updateFromProtectionRules(protectionRules);
111
+ update = 'application-settings';
112
+ }
113
+
114
+ if (remoteSettings?.features?.defend) {
115
+ const bbEnabled = remoteSettings.features.defend[BOT_BLOCKER];
116
+
117
+ if (
118
+ bbEnabled != null &&
119
+ !config.protect.disabled_rules.includes(BOT_BLOCKER) &&
120
+ !config.protect.rules?.[BOT_BLOCKER]?.mode
121
+ ) {
122
+ policy[BOT_BLOCKER] = bbEnabled ? BLOCK_AT_PERIMETER : OFF;
123
+ update = 'server-features';
124
+ }
125
+ }
126
+
127
+ if (update) {
128
+ updateRulesMask();
129
+ logger.info({ policy: protect.policy }, `protect policy updated from ${update}`);
130
+ }
131
+ });
132
+
133
+ return protect.getPolicy = getPolicy;
134
+ };