@contrast/protect 1.57.0 → 1.59.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.
@@ -31,7 +31,7 @@ const {
31
31
  StringPrototypeSplit,
32
32
  }
33
33
  } = require('@contrast/common');
34
-
34
+ const { Core } = require('@contrast/core/lib/ioc/core');
35
35
  //
36
36
  // these rules are not implemented by agent-lib, but are being considered for
37
37
  // implementation:
@@ -95,678 +95,681 @@ const acceptedMethods = new Set([
95
95
  'version-control',
96
96
  ]);
97
97
 
98
- module.exports = function (core) {
99
- const {
100
- logger,
101
- protect: {
102
- agentLib,
103
- inputAnalysis,
104
- },
105
- config,
106
- } = core;
107
-
108
- const jsonInputTypes = {
109
- keyType: agentLib.InputType.JsonKey, inputType: agentLib.InputType.JsonValue
110
- };
111
-
112
- const parameterInputTypes = {
113
- keyType: agentLib.InputType.ParameterKey, inputType: agentLib.InputType.ParameterValue
114
- };
115
-
116
- // all handlers will be invoked with two arguments:
117
- // 1) sourceContext object containing:
118
- // - reqData, the abstract request object containing only what is needed
119
- // - protect, the protect context
120
- // - rules, exclusions, virtual patches (TS data). what was in effect for this
121
- // url *at the time the request was started*. these will not change.
122
- // - block(), function that executes block for this specific request
123
- // - findings{}, consolidated collection of findings returned by score functions
124
- // and augmented with disposition and additional information as needed.
125
- // 2) input or inputs specific to that handler
126
- //
127
- // exclusions
128
- // - url exclusions are applied at the level above the connect handler, if no rules
129
- // are applicable to a given URL then it's not processed in any way by handlers.
130
- // - input exclusions are applied **after** analysis. The rationale is that checking
131
- // inputs against rules 1) is very fast and 2) dramatically pares down the number
132
- // of exclusion checks that need to be made.
133
-
134
- /**
135
- * handleConnect()
136
- *
137
- * handle the inputs that are available when the HTTP 'request' event is emitted.
138
- *
139
- * this should *always* be the first handler called; it does setup that other
140
- * handlers require.
141
- *
142
- * the specific data that is available to be processed at this time:
143
- * - URI
144
- * - queries
145
- * - url params
146
- * - headers (should exclude cookies header)
147
- * - cookies (special handling of the header)
148
- *
149
- * but it really only makes sense to process:
150
- * - URI
151
- * - headers except cookies
152
- *
153
- * why? because the query string will (probably) be interpreted by the framework using
154
- * 'qs' and that could result in creating an array or an object instead of a simple text
155
- * value. and the cookies need to be parsed into individual cookies by the framework so
156
- * that our code doesn't have to guess at the format, encoding, and other permutations.
157
- * and for url params - only the framework knows that a portion of the url is a param.
158
- *
159
- * if it is known that 'qs' is not being used, then passing the query string in the
160
- * 'connectInputs' makes sense; a flag similar to 'contentType' can be set and it can be
161
- * used later to avoid calling 'handleQueryParams()'
162
- *
163
- * @param {Object} sourceContext { reqData, protect } that will be supplied to
164
- * all handlers and sinks for this request. It will always be supplied by the caller
165
- * to a handler; the handler is not aware of the implementation.
166
- * @param {Object} connectInputs each property is an input to be evaluated by this
167
- * handler.
168
- * @returns {undefined|[String]} undefined to permit else [mode, rule] to block.
169
- */
170
- inputAnalysis.handleConnect = function handleConnect(sourceContext, connectInputs) {
171
- const { policy: { rulesMask } } = sourceContext;
172
-
173
- inputAnalysis.handleVirtualPatches(
174
- sourceContext,
175
- { URLS: connectInputs.rawUrl, HEADERS: connectInputs.headers }
176
- );
177
-
178
- const findings = agentLib.scoreRequestConnect(rulesMask, connectInputs, preferWW);
179
- let block = mergeFindings(sourceContext, findings);
180
-
181
- if (!block) {
182
- block = inputAnalysis.handleMethodTampering(sourceContext, connectInputs);
183
- }
98
+ module.exports = Core.makeComponent({
99
+ name: 'protect.inputAnalysis.handlers',
100
+ factory(core) {
101
+ const {
102
+ logger,
103
+ protect: {
104
+ agentLib,
105
+ inputAnalysis,
106
+ },
107
+ config,
108
+ } = core;
109
+
110
+ const jsonInputTypes = {
111
+ keyType: agentLib.InputType.JsonKey, inputType: agentLib.InputType.JsonValue
112
+ };
184
113
 
185
- return block;
186
- };
187
-
188
- /**
189
- * handleQueryParams()
190
- *
191
- * If all values are strings there there is no need to re-evaluate them; that was
192
- * done on requestConnect. But some frameworks use packages like 'qs' to parse the
193
- * search params, which is non-standard and has many options including changing the
194
- * delimiters and the ability to create the equivalent of JSON from the search params
195
- * string. If any of the values are not strings then they need to be evaluated here;
196
- * these results replace the results generated at requestConnect.
197
- *
198
- * If it is known that 'qs' is being used then it is better not to have passed the
199
- * querystring to scoreRequestConnect().
200
- *
201
- * @param {Object} sourceContext
202
- * @param {Object} queryParams pojo {key: value, ...} for all query params/search params
203
- */
204
- inputAnalysis.handleQueryParams = function handleQueryParams(sourceContext, queryParams) {
205
- if (sourceContext.analyzedQuery) return;
206
- sourceContext.analyzedQuery = true;
207
-
208
- if (typeof queryParams !== 'object') {
209
- logger.debug({ queryParams }, 'handleQueryParams() called with non-object');
210
- return;
211
- }
114
+ const parameterInputTypes = {
115
+ keyType: agentLib.InputType.ParameterKey, inputType: agentLib.InputType.ParameterValue
116
+ };
212
117
 
213
- inputAnalysis.handleVirtualPatches(sourceContext, { PARAMETERS: queryParams });
118
+ // all handlers will be invoked with two arguments:
119
+ // 1) sourceContext object containing:
120
+ // - reqData, the abstract request object containing only what is needed
121
+ // - protect, the protect context
122
+ // - rules, exclusions, virtual patches (TS data). what was in effect for this
123
+ // url *at the time the request was started*. these will not change.
124
+ // - block(), function that executes block for this specific request
125
+ // - findings{}, consolidated collection of findings returned by score functions
126
+ // and augmented with disposition and additional information as needed.
127
+ // 2) input or inputs specific to that handler
128
+ //
129
+ // exclusions
130
+ // - url exclusions are applied at the level above the connect handler, if no rules
131
+ // are applicable to a given URL then it's not processed in any way by handlers.
132
+ // - input exclusions are applied **after** analysis. The rationale is that checking
133
+ // inputs against rules 1) is very fast and 2) dramatically pares down the number
134
+ // of exclusion checks that need to be made.
135
+
136
+ /**
137
+ * handleConnect()
138
+ *
139
+ * handle the inputs that are available when the HTTP 'request' event is emitted.
140
+ *
141
+ * this should *always* be the first handler called; it does setup that other
142
+ * handlers require.
143
+ *
144
+ * the specific data that is available to be processed at this time:
145
+ * - URI
146
+ * - queries
147
+ * - url params
148
+ * - headers (should exclude cookies header)
149
+ * - cookies (special handling of the header)
150
+ *
151
+ * but it really only makes sense to process:
152
+ * - URI
153
+ * - headers except cookies
154
+ *
155
+ * why? because the query string will (probably) be interpreted by the framework using
156
+ * 'qs' and that could result in creating an array or an object instead of a simple text
157
+ * value. and the cookies need to be parsed into individual cookies by the framework so
158
+ * that our code doesn't have to guess at the format, encoding, and other permutations.
159
+ * and for url params - only the framework knows that a portion of the url is a param.
160
+ *
161
+ * if it is known that 'qs' is not being used, then passing the query string in the
162
+ * 'connectInputs' makes sense; a flag similar to 'contentType' can be set and it can be
163
+ * used later to avoid calling 'handleQueryParams()'
164
+ *
165
+ * @param {Object} sourceContext { reqData, protect } that will be supplied to
166
+ * all handlers and sinks for this request. It will always be supplied by the caller
167
+ * to a handler; the handler is not aware of the implementation.
168
+ * @param {Object} connectInputs each property is an input to be evaluated by this
169
+ * handler.
170
+ * @returns {undefined|[String]} undefined to permit else [mode, rule] to block.
171
+ */
172
+ inputAnalysis.handleConnect = function handleConnect(sourceContext, connectInputs) {
173
+ const { policy: { rulesMask } } = sourceContext;
174
+
175
+ inputAnalysis.handleVirtualPatches(
176
+ sourceContext,
177
+ { URLS: connectInputs.rawUrl, HEADERS: connectInputs.headers }
178
+ );
214
179
 
215
- const block = commonObjectAnalyzer(sourceContext, queryParams, parameterInputTypes);
180
+ const findings = agentLib.scoreRequestConnect(rulesMask, connectInputs, preferWW);
181
+ let block = mergeFindings(sourceContext, findings);
216
182
 
217
- if (block) {
218
- core.protect.throwSecurityException(sourceContext);
219
- }
220
- };
221
-
222
- /**
223
- * handleUrlParams()
224
- *
225
- * Invoked when a framework emits URL params. It is similar to handling queryParams
226
- * except no finding for URL params can be present.
227
- *
228
- * @param {Object} sourceContext
229
- * @param {Object} urlParams pojo
230
- */
231
- inputAnalysis.handleUrlParams = function (sourceContext, urlParams) {
232
- if (sourceContext.analyzedUrlParams) return;
233
- sourceContext.analyzedUrlParams = true;
234
-
235
- if (typeof urlParams !== 'object') {
236
- logger.debug({ urlParams }, 'handleUrlParams() called with non-object');
237
- return;
238
- }
239
-
240
- inputAnalysis.handleVirtualPatches(sourceContext, { PARAMETERS: urlParams });
183
+ if (!block) {
184
+ block = inputAnalysis.handleMethodTampering(sourceContext, connectInputs);
185
+ }
241
186
 
242
- const { policy: { rulesMask } } = sourceContext;
243
- const resultsList = [];
244
- const { UrlParameter } = agentLib.InputType;
187
+ return block;
188
+ };
245
189
 
246
- traverseValues(urlParams, function (path, type, value) {
247
- // url param names are not checked.
248
- if (type !== 'Value') {
190
+ /**
191
+ * handleQueryParams()
192
+ *
193
+ * If all values are strings there there is no need to re-evaluate them; that was
194
+ * done on requestConnect. But some frameworks use packages like 'qs' to parse the
195
+ * search params, which is non-standard and has many options including changing the
196
+ * delimiters and the ability to create the equivalent of JSON from the search params
197
+ * string. If any of the values are not strings then they need to be evaluated here;
198
+ * these results replace the results generated at requestConnect.
199
+ *
200
+ * If it is known that 'qs' is being used then it is better not to have passed the
201
+ * querystring to scoreRequestConnect().
202
+ *
203
+ * @param {Object} sourceContext
204
+ * @param {Object} queryParams pojo {key: value, ...} for all query params/search params
205
+ */
206
+ inputAnalysis.handleQueryParams = function handleQueryParams(sourceContext, queryParams) {
207
+ if (sourceContext.analyzedQuery) return;
208
+ sourceContext.analyzedQuery = true;
209
+
210
+ if (typeof queryParams !== 'object') {
211
+ logger.debug({ queryParams }, 'handleQueryParams() called with non-object');
249
212
  return;
250
213
  }
251
214
 
252
- const items = agentLib.scoreAtom(rulesMask, value, UrlParameter, preferWW);
215
+ inputAnalysis.handleVirtualPatches(sourceContext, { PARAMETERS: queryParams });
253
216
 
254
- if (!items) {
255
- return;
256
- }
217
+ const block = commonObjectAnalyzer(sourceContext, queryParams, parameterInputTypes);
257
218
 
258
- for (const item of items) {
259
- resultsList.push({
260
- ruleId: item.ruleId,
261
- inputType: 'UrlParameter',
262
- path: ArrayPrototypeSlice.call(path),
263
- key: path.pop(), // there should always be at least the param name
264
- value,
265
- score: item.score,
266
- idsList: item.idsList
267
- });
219
+ if (block) {
220
+ core.protect.throwSecurityException(sourceContext);
268
221
  }
269
- });
222
+ };
270
223
 
271
- // if nothing was found then nothing needs to be done.
272
- if (resultsList.length === 0) {
273
- return;
274
- }
224
+ /**
225
+ * handleUrlParams()
226
+ *
227
+ * Invoked when a framework emits URL params. It is similar to handling queryParams
228
+ * except no finding for URL params can be present.
229
+ *
230
+ * @param {Object} sourceContext
231
+ * @param {Object} urlParams pojo
232
+ */
233
+ inputAnalysis.handleUrlParams = function(sourceContext, urlParams) {
234
+ if (sourceContext.analyzedUrlParams) return;
235
+ sourceContext.analyzedUrlParams = true;
236
+
237
+ if (typeof urlParams !== 'object') {
238
+ logger.debug({ urlParams }, 'handleUrlParams() called with non-object');
239
+ return;
240
+ }
275
241
 
276
- // something was found, so create "complete" findings.
277
- const urlParamsFindings = {
278
- trackRequest: true,
279
- securityException: undefined,
280
- resultsList,
281
- };
242
+ inputAnalysis.handleVirtualPatches(sourceContext, { PARAMETERS: urlParams });
282
243
 
283
- const block = mergeFindings(sourceContext, urlParamsFindings);
244
+ const { policy: { rulesMask } } = sourceContext;
245
+ const resultsList = [];
246
+ const { UrlParameter } = agentLib.InputType;
284
247
 
285
- if (block) {
286
- core.protect.throwSecurityException(sourceContext);
287
- }
288
- };
248
+ traverseValues(urlParams, function(path, type, value) {
249
+ // url param names are not checked.
250
+ if (type !== 'Value') {
251
+ return;
252
+ }
289
253
 
290
- /**
291
- * handleCookies()
292
- *
293
- * Invoked when a framework emits cookies. It is similar to handling queryParams except
294
- * no findings for cookies can be present.
295
- * @param {Object} sourceContext
296
- * @param {Object} cookies pojo
297
- */
298
- inputAnalysis.handleCookies = function (sourceContext, cookies) {
299
- if (sourceContext.analyzedCookies) return;
300
- sourceContext.analyzedCookies = true;
254
+ const items = agentLib.scoreAtom(rulesMask, value, UrlParameter, preferWW);
301
255
 
302
- inputAnalysis.handleVirtualPatches(sourceContext, { HEADERS: cookies });
256
+ if (!items) {
257
+ return;
258
+ }
303
259
 
304
- const { policy: { rulesMask } } = sourceContext;
260
+ for (const item of items) {
261
+ resultsList.push({
262
+ ruleId: item.ruleId,
263
+ inputType: 'UrlParameter',
264
+ path: ArrayPrototypeSlice.call(path),
265
+ key: path.pop(), // there should always be at least the param name
266
+ value,
267
+ score: item.score,
268
+ idsList: item.idsList
269
+ });
270
+ }
271
+ });
305
272
 
306
- const cookiesArr = Object.entries(cookies).reduce((acc, [key, value]) => {
307
- // things like booleans will cause agent-lib to throw
308
- if (isString(value)) acc.push(key, value);
309
- return acc;
310
- }, []);
273
+ // if nothing was found then nothing needs to be done.
274
+ if (resultsList.length === 0) {
275
+ return;
276
+ }
311
277
 
312
- const cookieFindings = agentLib.scoreRequestConnect(rulesMask, { cookies: cookiesArr }, preferWW);
278
+ // something was found, so create "complete" findings.
279
+ const urlParamsFindings = {
280
+ trackRequest: true,
281
+ securityException: undefined,
282
+ resultsList,
283
+ };
313
284
 
314
- const block = mergeFindings(sourceContext, cookieFindings);
285
+ const block = mergeFindings(sourceContext, urlParamsFindings);
315
286
 
316
- if (block) {
317
- core.protect.throwSecurityException(sourceContext);
318
- }
319
- };
320
-
321
- /**
322
- * handleParsedBody() is called with the body after a framework has parsed
323
- * the body. If the body was JSON then it may have already been scored by
324
- * handleRawBody(); if it was, bodyType was set to 'json' and this code only
325
- * converts the previous findings, using the parsed body.
326
- *
327
- * @param {Object} sourceContext
328
- * @param {Object} parsedBody
329
- */
330
- inputAnalysis.handleParsedBody = function (sourceContext, parsedBody) {
331
- if (sourceContext.analyzedBody) return;
332
-
333
- sourceContext.analyzedBody = true;
334
-
335
- if (typeof parsedBody !== 'object') {
336
- logger.debug({ parsedBody }, 'handleParsedBody() called with non-object');
337
- return;
338
- }
287
+ if (block) {
288
+ core.protect.throwSecurityException(sourceContext);
289
+ }
290
+ };
339
291
 
340
- inputAnalysis.handleVirtualPatches(sourceContext, { PARAMETERS: parsedBody });
292
+ /**
293
+ * handleCookies()
294
+ *
295
+ * Invoked when a framework emits cookies. It is similar to handling queryParams except
296
+ * no findings for cookies can be present.
297
+ * @param {Object} sourceContext
298
+ * @param {Object} cookies pojo
299
+ */
300
+ inputAnalysis.handleCookies = function(sourceContext, cookies) {
301
+ if (sourceContext.analyzedCookies) return;
302
+ sourceContext.analyzedCookies = true;
341
303
 
342
- let bodyType;
343
- let inputTypes;
344
- if (sourceContext.reqData.contentType.includes('/json')) {
345
- bodyType = 'json';
346
- inputTypes = jsonInputTypes;
347
- } else {
348
- bodyType = 'urlencoded';
349
- inputTypes = parameterInputTypes;
350
- }
304
+ inputAnalysis.handleVirtualPatches(sourceContext, { HEADERS: cookies });
351
305
 
352
- const block = commonObjectAnalyzer(sourceContext, parsedBody, inputTypes);
306
+ const { policy: { rulesMask } } = sourceContext;
353
307
 
354
- sourceContext.bodyType = bodyType;
308
+ const cookiesArr = Object.entries(cookies).reduce((acc, [key, value]) => {
309
+ // things like booleans will cause agent-lib to throw
310
+ if (isString(value)) acc.push(key, value);
311
+ return acc;
312
+ }, []);
355
313
 
356
- if (block) {
357
- core.protect.throwSecurityException(sourceContext);
358
- }
359
- };
314
+ const cookieFindings = agentLib.scoreRequestConnect(rulesMask, { cookies: cookiesArr }, preferWW);
360
315
 
361
- // was MULTIPART_NAME but maybe we should just call it what it is. it's kind
362
- // of a dumb rule anyway. but maybe some code actually uses the name provided.
363
- inputAnalysis.handleFileUploadName = function (sourceContext, names) {
364
- const type = agentLib.InputType.MultipartName;
365
- const { policy } = sourceContext;
366
- const resultsList = [];
316
+ const block = mergeFindings(sourceContext, cookieFindings);
367
317
 
368
- if (policy[Rule.UNSAFE_FILE_UPLOAD] === 'off') return;
318
+ if (block) {
319
+ core.protect.throwSecurityException(sourceContext);
320
+ }
321
+ };
369
322
 
370
- for (const name of names) {
371
- if (!isString(name)) {
372
- logger.debug({ filename: name }, 'handleFileUploadName() was called with non-string');
323
+ /**
324
+ * handleParsedBody() is called with the body after a framework has parsed
325
+ * the body. If the body was JSON then it may have already been scored by
326
+ * handleRawBody(); if it was, bodyType was set to 'json' and this code only
327
+ * converts the previous findings, using the parsed body.
328
+ *
329
+ * @param {Object} sourceContext
330
+ * @param {Object} parsedBody
331
+ */
332
+ inputAnalysis.handleParsedBody = function(sourceContext, parsedBody) {
333
+ if (sourceContext.analyzedBody) return;
334
+
335
+ sourceContext.analyzedBody = true;
336
+
337
+ if (typeof parsedBody !== 'object') {
338
+ logger.debug({ parsedBody }, 'handleParsedBody() called with non-object');
373
339
  return;
374
340
  }
375
341
 
376
- const items = agentLib.scoreAtom(policy.rulesMask, name, type);
342
+ inputAnalysis.handleVirtualPatches(sourceContext, { PARAMETERS: parsedBody });
377
343
 
378
- if (!items) {
379
- return;
380
- }
381
- for (const item of items) {
382
- resultsList.push({
383
- ruleId: item.ruleId,
384
- inputType: type,
385
- name: '',
386
- value: name,
387
- score: item.score,
388
- idsList: item.idsList,
389
- });
344
+ let bodyType;
345
+ let inputTypes;
346
+ if (sourceContext.reqData.contentType.includes('/json')) {
347
+ bodyType = 'json';
348
+ inputTypes = jsonInputTypes;
349
+ } else {
350
+ bodyType = 'urlencoded';
351
+ inputTypes = parameterInputTypes;
390
352
  }
391
- }
392
353
 
393
- // something was found, so create "complete" findings.
394
- const unsafeFilenameFindings = {
395
- trackRequest: true,
396
- securityException: undefined,
397
- resultsList,
354
+ const block = commonObjectAnalyzer(sourceContext, parsedBody, inputTypes);
355
+
356
+ sourceContext.bodyType = bodyType;
357
+
358
+ if (block) {
359
+ core.protect.throwSecurityException(sourceContext);
360
+ }
398
361
  };
399
362
 
400
- const block = mergeFindings(sourceContext, unsafeFilenameFindings);
363
+ // was MULTIPART_NAME but maybe we should just call it what it is. it's kind
364
+ // of a dumb rule anyway. but maybe some code actually uses the name provided.
365
+ inputAnalysis.handleFileUploadName = function(sourceContext, names) {
366
+ const type = agentLib.InputType.MultipartName;
367
+ const { policy } = sourceContext;
368
+ const resultsList = [];
401
369
 
402
- if (block) {
403
- core.protect.throwSecurityException(sourceContext);
404
- }
405
- };
370
+ if (policy[Rule.UNSAFE_FILE_UPLOAD] === 'off') return;
406
371
 
407
- inputAnalysis.handleVirtualPatches = function (sourceContext, requestInput) {
408
- const ruleId = Rule.VIRTUAL_PATCH;
372
+ for (const name of names) {
373
+ if (!isString(name)) {
374
+ logger.debug({ filename: name }, 'handleFileUploadName() was called with non-string');
375
+ return;
376
+ }
409
377
 
410
- if (!Object.keys(requestInput).filter(Boolean).length || !sourceContext?.virtualPatchesEvaluators.length) return;
378
+ const items = agentLib.scoreAtom(policy.rulesMask, name, type);
411
379
 
412
- for (const vpEvaluators of sourceContext.virtualPatchesEvaluators) {
413
- for (const key in requestInput) {
414
- const evaluator = vpEvaluators.get(key);
380
+ if (!items) {
381
+ return;
382
+ }
383
+ for (const item of items) {
384
+ resultsList.push({
385
+ ruleId: item.ruleId,
386
+ inputType: type,
387
+ name: '',
388
+ value: name,
389
+ score: item.score,
390
+ idsList: item.idsList,
391
+ });
392
+ }
393
+ }
394
+
395
+ // something was found, so create "complete" findings.
396
+ const unsafeFilenameFindings = {
397
+ trackRequest: true,
398
+ securityException: undefined,
399
+ resultsList,
400
+ };
415
401
 
416
- if (evaluator && requestInput[key] && evaluator(requestInput[key])) {
417
- vpEvaluators.delete(key);
418
- const { name, uuid } = vpEvaluators.get('metadata');
402
+ const block = mergeFindings(sourceContext, unsafeFilenameFindings);
419
403
 
420
- if (vpEvaluators.size === 1 && uuid) {
421
- if (!sourceContext.resultsMap[ruleId]) {
422
- sourceContext.resultsMap[ruleId] = [];
404
+ if (block) {
405
+ core.protect.throwSecurityException(sourceContext);
406
+ }
407
+ };
408
+
409
+ inputAnalysis.handleVirtualPatches = function(sourceContext, requestInput) {
410
+ const ruleId = Rule.VIRTUAL_PATCH;
411
+
412
+ if (!Object.keys(requestInput).filter(Boolean).length || !sourceContext?.virtualPatchesEvaluators.length) return;
413
+
414
+ for (const vpEvaluators of sourceContext.virtualPatchesEvaluators) {
415
+ for (const key in requestInput) {
416
+ const evaluator = vpEvaluators.get(key);
417
+
418
+ if (evaluator && requestInput[key] && evaluator(requestInput[key])) {
419
+ vpEvaluators.delete(key);
420
+ const { name, uuid } = vpEvaluators.get('metadata');
421
+
422
+ if (vpEvaluators.size === 1 && uuid) {
423
+ if (!sourceContext.resultsMap[ruleId]) {
424
+ sourceContext.resultsMap[ruleId] = [];
425
+ }
426
+ sourceContext.resultsMap[ruleId].push({
427
+ name,
428
+ uuid
429
+ });
430
+ sourceContext.securityException = ['block', ruleId];
431
+ core.protect.throwSecurityException(sourceContext);
423
432
  }
424
- sourceContext.resultsMap[ruleId].push({
425
- name,
426
- uuid
427
- });
428
- sourceContext.securityException = ['block', ruleId];
429
- core.protect.throwSecurityException(sourceContext);
430
433
  }
431
434
  }
432
435
  }
433
- }
434
- };
436
+ };
435
437
 
436
- inputAnalysis.handleIpAllowlist = function (sourceContext, ipAllowlist) {
437
- if (!sourceContext || !ipAllowlist.length) return;
438
+ inputAnalysis.handleIpAllowlist = function(sourceContext, ipAllowlist) {
439
+ if (!sourceContext || !ipAllowlist.length) return;
438
440
 
439
- const { ip: reqIp, headers: reqHeaders } = sourceContext.reqData;
441
+ const { ip: reqIp, headers: reqHeaders } = sourceContext.reqData;
440
442
 
441
- const match = ipListAnalysis(reqIp, reqHeaders, ipAllowlist);
443
+ const match = ipListAnalysis(reqIp, reqHeaders, ipAllowlist);
442
444
 
443
- if (match) {
444
- logger.info(match, 'Found a matching IP to an entry in ipAllow list');
445
- return true;
446
- }
447
- };
445
+ if (match) {
446
+ logger.info(match, 'Found a matching IP to an entry in ipAllow list');
447
+ return true;
448
+ }
449
+ };
450
+
451
+ inputAnalysis.handleIpDenylist = function(sourceContext, ipDenylist) {
452
+ const ruleId = Rule.IP_DENYLIST;
448
453
 
449
- inputAnalysis.handleIpDenylist = function (sourceContext, ipDenylist) {
450
- const ruleId = Rule.IP_DENYLIST;
454
+ if (!sourceContext || !ipDenylist.length) return;
451
455
 
452
- if (!sourceContext || !ipDenylist.length) return;
456
+ const { ip: reqIp, headers: reqHeaders } = sourceContext.reqData;
453
457
 
454
- const { ip: reqIp, headers: reqHeaders } = sourceContext.reqData;
458
+ const match = ipListAnalysis(reqIp, reqHeaders, ipDenylist);
455
459
 
456
- const match = ipListAnalysis(reqIp, reqHeaders, ipDenylist);
460
+ if (match) {
461
+ logger.info(match, 'Found a matching IP to an entry in ipDeny list');
462
+ if (!sourceContext.resultsMap[ruleId]) {
463
+ sourceContext.resultsMap[ruleId] = [];
464
+ }
457
465
 
458
- if (match) {
459
- logger.info(match, 'Found a matching IP to an entry in ipDeny list');
460
- if (!sourceContext.resultsMap[ruleId]) {
461
- sourceContext.resultsMap[ruleId] = [];
466
+ sourceContext.resultsMap[ruleId].push({
467
+ ip: match.matchedIp,
468
+ uuid: match.uuid,
469
+ });
470
+ return ['block', 'ip-denylist'];
462
471
  }
472
+ };
463
473
 
464
- sourceContext.resultsMap[ruleId].push({
465
- ip: match.matchedIp,
466
- uuid: match.uuid,
467
- });
468
- return ['block', 'ip-denylist'];
469
- }
470
- };
471
-
472
- inputAnalysis.handleMethodTampering = function (sourceContext, connectInputs) {
473
- const ruleId = Rule.METHOD_TAMPERING;
474
- const mode = sourceContext.policy[ruleId];
475
- if (mode !== OFF) {
476
- const { method } = connectInputs;
477
-
478
- if (!acceptedMethods.has(method)) {
479
- const result = {
480
- inputType: InputType.METHOD,
481
- key: 'method',
482
- value: method,
483
- blocked: false,
484
- exploitMetadata: null,
485
- };
486
-
487
- sourceContext.resultsMap[ruleId] = [result];
488
-
489
- if (BLOCKING_MODES.includes(mode)) {
490
- result.blocked = true;
491
- return sourceContext.securityException = ['block', ruleId];
474
+ inputAnalysis.handleMethodTampering = function(sourceContext, connectInputs) {
475
+ const ruleId = Rule.METHOD_TAMPERING;
476
+ const mode = sourceContext.policy[ruleId];
477
+ if (mode !== OFF) {
478
+ const { method } = connectInputs;
479
+
480
+ if (!acceptedMethods.has(method)) {
481
+ const result = {
482
+ inputType: InputType.METHOD,
483
+ key: 'method',
484
+ value: method,
485
+ blocked: false,
486
+ exploitMetadata: null,
487
+ };
488
+
489
+ sourceContext.resultsMap[ruleId] = [result];
490
+
491
+ if (BLOCKING_MODES.includes(mode)) {
492
+ result.blocked = true;
493
+ return sourceContext.securityException = ['block', ruleId];
494
+ }
492
495
  }
493
496
  }
494
- }
495
- };
496
-
497
- /**
498
- *
499
- * Invoked when the request is complete. Handles probe analysis (when configured) and
500
- * various other tasks needed prior to reporting.
501
- *
502
- * @param {Object} sourceContext
503
- */
504
- inputAnalysis.handleRequestEnd = function handleRequestEnd(sourceContext) {
505
- {
506
- // check status code to verify method-tampering exploitation
507
- const mtResult = sourceContext.resultsMap[Rule.METHOD_TAMPERING]?.[0];
508
- if (mtResult) {
509
- const { statusCode } = sourceContext.resData;
510
- if (statusCode !== 405 || statusCode !== 501) {
511
- mtResult.exploitMetadata = [{ statusCode }];
497
+ };
498
+
499
+ /**
500
+ *
501
+ * Invoked when the request is complete. Handles probe analysis (when configured) and
502
+ * various other tasks needed prior to reporting.
503
+ *
504
+ * @param {Object} sourceContext
505
+ */
506
+ inputAnalysis.handleRequestEnd = function handleRequestEnd(sourceContext) {
507
+ {
508
+ // check status code to verify method-tampering exploitation
509
+ const mtResult = sourceContext.resultsMap[Rule.METHOD_TAMPERING]?.[0];
510
+ if (mtResult) {
511
+ const { statusCode } = sourceContext.resData;
512
+ if (statusCode !== 405 || statusCode !== 501) {
513
+ mtResult.exploitMetadata = [{ statusCode }];
514
+ }
512
515
  }
513
516
  }
514
- }
515
517
 
516
- if (!config.protect.probe_analysis.enable) return;
517
-
518
- // Detecting probes
519
- const { resultsMap, policy: { rulesMask } } = sourceContext;
520
- const probesRules = [Rule.CMD_INJECTION, Rule.PATH_TRAVERSAL, Rule.SQL_INJECTION, Rule.XXE];
521
- const probes = {};
518
+ if (!config.protect.probe_analysis.enable) return;
519
+
520
+ // Detecting probes
521
+ const { resultsMap, policy: { rulesMask } } = sourceContext;
522
+ const probesRules = [Rule.CMD_INJECTION, Rule.PATH_TRAVERSAL, Rule.SQL_INJECTION, Rule.XXE];
523
+ const probes = {};
524
+
525
+ const findingsForScoreRequest = {
526
+ HeaderValue: {},
527
+ ParameterValue: {},
528
+ CookieValue: {},
529
+ };
530
+ const findingsForScoreAtom = {};
531
+ const valueToResultByRuleId = {};
532
+
533
+ Object.values(resultsMap).forEach(resultsByRuleId => {
534
+ resultsByRuleId.forEach(resultByRuleId => {
535
+ const {
536
+ ruleId,
537
+ exploitMetadata,
538
+ score,
539
+ value,
540
+ key,
541
+ inputType
542
+ } = resultByRuleId;
543
+
544
+ if (
545
+ !isMonitorMode(ruleId, sourceContext) ||
546
+ exploitMetadata.length > 0 ||
547
+ score >= 90 ||
548
+ !probesRules.some((rule) => rule === ruleId)
549
+ ) {
550
+ return;
551
+ }
522
552
 
523
- const findingsForScoreRequest = {
524
- HeaderValue: {},
525
- ParameterValue: {},
526
- CookieValue: {},
527
- };
528
- const findingsForScoreAtom = {};
529
- const valueToResultByRuleId = {};
530
-
531
- Object.values(resultsMap).forEach(resultsByRuleId => {
532
- resultsByRuleId.forEach(resultByRuleId => {
533
- const {
534
- ruleId,
535
- exploitMetadata,
536
- score,
537
- value,
538
- key,
539
- inputType
540
- } = resultByRuleId;
541
-
542
- if (
543
- !isMonitorMode(ruleId, sourceContext) ||
544
- exploitMetadata.length > 0 ||
545
- score >= 90 ||
546
- !probesRules.some((rule) => rule === ruleId)
547
- ) {
548
- return;
549
- }
553
+ const dataType = findingsForScoreRequest[inputType];
554
+ if (!dataType) {
555
+ if (!findingsForScoreAtom[value]) {
556
+ findingsForScoreAtom[value] = {};
557
+ }
550
558
 
551
- const dataType = findingsForScoreRequest[inputType];
552
- if (!dataType) {
553
- if (!findingsForScoreAtom[value]) {
554
- findingsForScoreAtom[value] = {};
559
+ findingsForScoreAtom[value][inputType] = resultByRuleId;
560
+ return;
555
561
  }
556
562
 
557
- findingsForScoreAtom[value][inputType] = resultByRuleId;
558
- return;
559
- }
560
-
561
- dataType[key] = value;
562
- valueToResultByRuleId[value] = resultByRuleId;
563
+ dataType[key] = value;
564
+ valueToResultByRuleId[value] = resultByRuleId;
565
+ });
563
566
  });
564
- });
565
-
566
- const { ParameterValue, HeaderValue, CookieValue } = findingsForScoreRequest;
567
-
568
- const results =
569
- agentLib.scoreRequestConnect(
570
- rulesMask,
571
- {
572
- queries: Object.entries(ParameterValue).flat(),
573
- headers: Object.entries(HeaderValue).flat(),
574
- cookies: Object.entries(CookieValue).flat(),
575
- },
576
- {
577
- preferWorthWatching: false,
578
- }
579
- ).resultsList || [];
580
567
 
581
- Object.entries(findingsForScoreAtom).forEach(([value, inputTypes]) => {
582
- Object.entries(inputTypes).forEach(([inputType, resultByRuleId]) =>
583
- (
584
- agentLib.scoreAtom(rulesMask, value, agentLib.InputType[inputType], {
568
+ const { ParameterValue, HeaderValue, CookieValue } = findingsForScoreRequest;
569
+
570
+ const results =
571
+ agentLib.scoreRequestConnect(
572
+ rulesMask,
573
+ {
574
+ queries: Object.entries(ParameterValue).flat(),
575
+ headers: Object.entries(HeaderValue).flat(),
576
+ cookies: Object.entries(CookieValue).flat(),
577
+ },
578
+ {
585
579
  preferWorthWatching: false,
586
- }) || []
587
- ).forEach(result => {
588
- results.push({ value, ...result });
589
- valueToResultByRuleId[value] = resultByRuleId;
590
- })
591
- );
592
- });
593
-
594
- results
595
- .filter(({ score, ruleId }) => score >= 90 && isMonitorMode(ruleId, sourceContext))
596
- .forEach((result) => {
597
- const resultByRuleId = valueToResultByRuleId[result.value];
598
- const probe = Object.assign({}, resultByRuleId, result, {
599
- mappedId: result.ruleId,
600
- });
601
- const key = ArrayPrototypeJoin.call([
602
- probe.ruleId,
603
- probe.inputType,
604
- ...probe.path,
605
- probe.value,
606
- ], '|');
607
- probes[key] = probe;
580
+ }
581
+ ).resultsList || [];
582
+
583
+ Object.entries(findingsForScoreAtom).forEach(([value, inputTypes]) => {
584
+ Object.entries(inputTypes).forEach(([inputType, resultByRuleId]) =>
585
+ (
586
+ agentLib.scoreAtom(rulesMask, value, agentLib.InputType[inputType], {
587
+ preferWorthWatching: false,
588
+ }) || []
589
+ ).forEach(result => {
590
+ results.push({ value, ...result });
591
+ valueToResultByRuleId[value] = resultByRuleId;
592
+ })
593
+ );
608
594
  });
609
595
 
610
- Object.values(probes).forEach(probe => {
611
- if (!resultsMap[probe.ruleId]) {
612
- resultsMap[probe.ruleId] = [];
613
- }
596
+ results
597
+ .filter(({ score, ruleId }) => score >= 90 && isMonitorMode(ruleId, sourceContext))
598
+ .forEach((result) => {
599
+ const resultByRuleId = valueToResultByRuleId[result.value];
600
+ const probe = Object.assign({}, resultByRuleId, result, {
601
+ mappedId: result.ruleId,
602
+ });
603
+ const key = ArrayPrototypeJoin.call([
604
+ probe.ruleId,
605
+ probe.inputType,
606
+ ...probe.path,
607
+ probe.value,
608
+ ], '|');
609
+ probes[key] = probe;
610
+ });
614
611
 
615
- resultsMap[probe.ruleId].push(probe);
616
- });
617
- };
618
-
619
- /**
620
- * commonObjectAnalyzer() walks an object supplied by the end-user and checks
621
- * it for vulnerabilities.
622
- *
623
- * This can cause the request to be blocked, depending on the mode and findings.
624
- *
625
- * @param {Object} sourceContext the sourceContext for the request
626
- * @param {Object} object the object to analyze. It could be from any input
627
- * source that can resolve to an object: x-www-form-urlencoded, JSON,
628
- * query params.
629
- * @param {Object} inputTypes is either jsonInputTypes or parameterInputTypes,
630
- * both are defined above. They specify the input types to be used when evaluating
631
- * the object.
632
- * @returns {Array | undefined} returns an array with block info if vulnerability was found.
633
- */
634
- function commonObjectAnalyzer(sourceContext, object, inputTypes) {
635
- const { policy: { rulesMask } } = sourceContext;
636
- if (!rulesMask) return;
637
-
638
- // use inputTypes to set params...
639
- const { keyType, inputType } = inputTypes;
640
- const inputTypeStr = inputTypes === jsonInputTypes ? 'Json' : 'Parameter';
641
- const resultsList = [];
642
-
643
- // it's possible to optimize this if qs (or a similar package) is not loaded
644
- // or if none of the values of queryParams are objects. a quick '.includes()'
645
- // could be used to determine that. if none are objects then traverseKeysAndValues()
646
- // wouldn't be used, just a simple "for (const key in queryParams) {...}" to
647
- // check each key and value associated with the key.
648
- //
649
- // otoh, it's a fair amount of additional logic and the gain is likely to be
650
- // small, so it probably only makes sense to check if qs (or similar) is actually
651
- // in use. a benchmark of "for (const key in queryParams) {...}" vs traverseKeysAndValues
652
- // should be created to see if, and in what cases, it makes sense.
653
- //
654
- // another day.
655
-
656
- /* eslint-disable-next-line complexity */
657
- traverseKeysAndValues(object, function (path, type, value) {
658
- let itemType;
659
- let isMongoQueryType;
660
- // this is a bit awkward now because nosql-injection-mongo is not integrated
661
- // into the scoreAtom() function (or the check_input() function it uses). as
662
- // a result, the two rules need to be checked independently and the results
663
- // merged, which is kind of a pia.
664
- // TODO AGENT-205
665
- if (type === 'Key') {
666
- itemType = keyType;
667
- if (rulesMask & agentLib.RuleType['nosql-injection-mongo']) {
668
- isMongoQueryType = agentLib.isMongoQueryType(value);
612
+ Object.values(probes).forEach(probe => {
613
+ if (!resultsMap[probe.ruleId]) {
614
+ resultsMap[probe.ruleId] = [];
669
615
  }
670
- } else {
671
- itemType = inputType;
672
- }
673
616
 
674
- let items = agentLib.scoreAtom(rulesMask, value, itemType, preferWW);
617
+ resultsMap[probe.ruleId].push(probe);
618
+ });
619
+ };
675
620
 
676
- if (!items && !isMongoQueryType) {
677
- return;
678
- }
679
- if (!items) {
680
- items = [];
681
- }
682
- let mongoPath;
683
- // if the key was a mongo query key, then add it to the items. it requires
684
- // that additional information is kept as well.
685
- if (isMongoQueryType) {
686
- const inputToCheck = getValueAtKey(object, path, value);
687
- // because scoreRequestConnect() returns the query type in the value, we
688
- // mimic it here (where scoreAtom() was used). the actual object/string
689
- // to match is stored as `inputToCheck`.
690
- const item = { ruleId: 'nosql-injection-mongo', score: 10, mongoContext: { inputToCheck } };
691
- items.push(item);
692
- }
693
- // make each item a complete Finding
694
- for (const item of items) {
695
- const result = {
696
- ruleId: item.ruleId,
697
- inputType: `${inputTypeStr}${type}`,
698
- path: mongoPath || ArrayPrototypeSlice.call(path),
699
- key: type === 'Key' ? value : path[path.length - 1],
700
- value,
701
- score: item.score,
702
- idsList: item.idsList || [],
703
- };
704
- if (item.mongoContext) {
705
- result.mongoContext = item.mongoContext;
621
+ /**
622
+ * commonObjectAnalyzer() walks an object supplied by the end-user and checks
623
+ * it for vulnerabilities.
624
+ *
625
+ * This can cause the request to be blocked, depending on the mode and findings.
626
+ *
627
+ * @param {Object} sourceContext the sourceContext for the request
628
+ * @param {Object} object the object to analyze. It could be from any input
629
+ * source that can resolve to an object: x-www-form-urlencoded, JSON,
630
+ * query params.
631
+ * @param {Object} inputTypes is either jsonInputTypes or parameterInputTypes,
632
+ * both are defined above. They specify the input types to be used when evaluating
633
+ * the object.
634
+ * @returns {Array | undefined} returns an array with block info if vulnerability was found.
635
+ */
636
+ function commonObjectAnalyzer(sourceContext, object, inputTypes) {
637
+ const { policy: { rulesMask } } = sourceContext;
638
+ if (!rulesMask) return;
639
+
640
+ // use inputTypes to set params...
641
+ const { keyType, inputType } = inputTypes;
642
+ const inputTypeStr = inputTypes === jsonInputTypes ? 'Json' : 'Parameter';
643
+ const resultsList = [];
644
+
645
+ // it's possible to optimize this if qs (or a similar package) is not loaded
646
+ // or if none of the values of queryParams are objects. a quick '.includes()'
647
+ // could be used to determine that. if none are objects then traverseKeysAndValues()
648
+ // wouldn't be used, just a simple "for (const key in queryParams) {...}" to
649
+ // check each key and value associated with the key.
650
+ //
651
+ // otoh, it's a fair amount of additional logic and the gain is likely to be
652
+ // small, so it probably only makes sense to check if qs (or similar) is actually
653
+ // in use. a benchmark of "for (const key in queryParams) {...}" vs traverseKeysAndValues
654
+ // should be created to see if, and in what cases, it makes sense.
655
+ //
656
+ // another day.
657
+
658
+ /* eslint-disable-next-line complexity */
659
+ traverseKeysAndValues(object, function(path, type, value) {
660
+ let itemType;
661
+ let isMongoQueryType;
662
+ // this is a bit awkward now because nosql-injection-mongo is not integrated
663
+ // into the scoreAtom() function (or the check_input() function it uses). as
664
+ // a result, the two rules need to be checked independently and the results
665
+ // merged, which is kind of a pia.
666
+ // TODO AGENT-205
667
+ if (type === 'Key') {
668
+ itemType = keyType;
669
+ if (rulesMask & agentLib.RuleType['nosql-injection-mongo']) {
670
+ isMongoQueryType = agentLib.isMongoQueryType(value);
671
+ }
672
+ } else {
673
+ itemType = inputType;
706
674
  }
707
- resultsList.push(result);
708
- }
709
- });
710
-
711
- // if nothing was found then nothing needs to be done.
712
- if (resultsList.length === 0) {
713
- return;
714
- }
715
675
 
716
- // something was found, so create "complete" findings.
717
- const findings = {
718
- trackRequest: true,
719
- securityException: undefined,
720
- resultsList,
721
- };
722
-
723
- return mergeFindings(sourceContext, findings);
724
- }
676
+ let items = agentLib.scoreAtom(rulesMask, value, itemType, preferWW);
725
677
 
726
- function ipListAnalysis(reqIp, reqHeaders, list) {
727
- const forwardedIps = [];
678
+ if (!items && !isMongoQueryType) {
679
+ return;
680
+ }
681
+ if (!items) {
682
+ items = [];
683
+ }
684
+ let mongoPath;
685
+ // if the key was a mongo query key, then add it to the items. it requires
686
+ // that additional information is kept as well.
687
+ if (isMongoQueryType) {
688
+ const inputToCheck = getValueAtKey(object, path, value);
689
+ // because scoreRequestConnect() returns the query type in the value, we
690
+ // mimic it here (where scoreAtom() was used). the actual object/string
691
+ // to match is stored as `inputToCheck`.
692
+ const item = { ruleId: 'nosql-injection-mongo', score: 10, mongoContext: { inputToCheck } };
693
+ items.push(item);
694
+ }
695
+ // make each item a complete Finding
696
+ for (const item of items) {
697
+ const result = {
698
+ ruleId: item.ruleId,
699
+ inputType: `${inputTypeStr}${type}`,
700
+ path: mongoPath || ArrayPrototypeSlice.call(path),
701
+ key: type === 'Key' ? value : path[path.length - 1],
702
+ value,
703
+ score: item.score,
704
+ idsList: item.idsList || [],
705
+ };
706
+ if (item.mongoContext) {
707
+ result.mongoContext = item.mongoContext;
708
+ }
709
+ resultsList.push(result);
710
+ }
711
+ });
728
712
 
729
- for (let i = 0; i < reqHeaders.length; i++) {
730
- if (reqHeaders[i] === 'x-forwarded-for') {
731
- const ipsFromHeaders = StringPrototypeSplit.call(reqHeaders[i + 1], /[,;]+/);
732
- forwardedIps.push(...ipsFromHeaders);
713
+ // if nothing was found then nothing needs to be done.
714
+ if (resultsList.length === 0) {
715
+ return;
733
716
  }
734
- }
735
717
 
736
- const ipsToCheck = [reqIp, ...forwardedIps];
737
- const now = new Date().getTime();
718
+ // something was found, so create "complete" findings.
719
+ const findings = {
720
+ trackRequest: true,
721
+ securityException: undefined,
722
+ resultsList,
723
+ };
738
724
 
739
- /* c8 ignore next 3 */
740
- if (!ipsToCheck.length) {
741
- return false;
725
+ return mergeFindings(sourceContext, findings);
742
726
  }
743
727
 
744
- for (const listEntry of list) {
745
- const { doesExpire, expiresAt } = listEntry;
746
- for (let i = 0; i < ipsToCheck.length; i++) {
747
- const currentIp = ipsToCheck[i];
728
+ function ipListAnalysis(reqIp, reqHeaders, list) {
729
+ const forwardedIps = [];
748
730
 
749
- // Ignore bad IP values.
750
- if (!address.isValid(currentIp)) {
751
- logger.warn('Unable to parse %s.', currentIp);
752
- continue;
731
+ for (let i = 0; i < reqHeaders.length; i++) {
732
+ if (reqHeaders[i] === 'x-forwarded-for') {
733
+ const ipsFromHeaders = StringPrototypeSplit.call(reqHeaders[i + 1], /[,;]+/);
734
+ forwardedIps.push(...ipsFromHeaders);
753
735
  }
754
- const expired = doesExpire ? expiresAt - now <= 0 : false;
736
+ }
755
737
 
756
- if (expired) {
757
- logger.info('IP expired: %s, %s', listEntry.name, listEntry.ip);
758
- continue;
759
- }
738
+ const ipsToCheck = [reqIp, ...forwardedIps];
739
+ const now = new Date().getTime();
740
+
741
+ /* c8 ignore next 3 */
742
+ if (!ipsToCheck.length) {
743
+ return false;
744
+ }
760
745
 
761
- const match = checkIpsMatch(listEntry, currentIp);
746
+ for (const listEntry of list) {
747
+ const { doesExpire, expiresAt } = listEntry;
748
+ for (let i = 0; i < ipsToCheck.length; i++) {
749
+ const currentIp = ipsToCheck[i];
762
750
 
763
- if (match) {
764
- return match;
751
+ // Ignore bad IP values.
752
+ if (!address.isValid(currentIp)) {
753
+ logger.warn('Unable to parse %s.', currentIp);
754
+ continue;
755
+ }
756
+ const expired = doesExpire ? expiresAt - now <= 0 : false;
757
+
758
+ if (expired) {
759
+ logger.info('IP expired: %s, %s', listEntry.name, listEntry.ip);
760
+ continue;
761
+ }
762
+
763
+ const match = checkIpsMatch(listEntry, currentIp);
764
+
765
+ if (match) {
766
+ return match;
767
+ }
765
768
  }
766
769
  }
767
770
  }
768
- }
769
- };
771
+ },
772
+ });
770
773
 
771
774
  /**
772
775
  * Reads the source context's policy and compares to result item to check whether to ignore it.