@contrast/protect 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +12 -0
- package/README.md +9 -0
- package/lib/cli-rewriter.js +20 -0
- package/lib/error-handlers/constants.js +5 -0
- package/lib/error-handlers/index.js +13 -0
- package/lib/error-handlers/install/fastify3.js +88 -0
- package/lib/error-handlers/install/fastify3.test.js +142 -0
- package/lib/esm-loader.mjs +2 -0
- package/lib/esm-loader.test.mjs +11 -0
- package/lib/index.d.ts +36 -0
- package/lib/index.js +89 -0
- package/lib/index.test.js +32 -0
- package/lib/input-analysis/handlers.js +462 -0
- package/lib/input-analysis/handlers.test.js +898 -0
- package/lib/input-analysis/index.js +16 -0
- package/lib/input-analysis/index.test.js +28 -0
- package/lib/input-analysis/install/fastify3.js +79 -0
- package/lib/input-analysis/install/fastify3.test.js +71 -0
- package/lib/input-analysis/install/http.js +185 -0
- package/lib/input-analysis/install/http.test.js +315 -0
- package/lib/input-tracing/constants.js +5 -0
- package/lib/input-tracing/handlers/index.js +117 -0
- package/lib/input-tracing/handlers/index.test.js +395 -0
- package/lib/input-tracing/handlers/nosql-injection-mongo.js +48 -0
- package/lib/input-tracing/index.js +32 -0
- package/lib/input-tracing/install/README.md +1 -0
- package/lib/input-tracing/install/child-process.js +45 -0
- package/lib/input-tracing/install/child-process.test.js +112 -0
- package/lib/input-tracing/install/fs.js +107 -0
- package/lib/input-tracing/install/fs.test.js +118 -0
- package/lib/input-tracing/install/mysql.js +57 -0
- package/lib/input-tracing/install/mysql.test.js +108 -0
- package/lib/input-tracing/install/postgres.js +61 -0
- package/lib/input-tracing/install/postgres.test.js +125 -0
- package/lib/input-tracing/install/sequelize.js +51 -0
- package/lib/input-tracing/install/sequelize.test.js +79 -0
- package/lib/input-tracing/install/sqlite3.js +45 -0
- package/lib/input-tracing/install/sqlite3.test.js +88 -0
- package/lib/make-response-blocker.js +35 -0
- package/lib/make-response-blocker.test.js +88 -0
- package/lib/make-source-context.js +130 -0
- package/lib/make-source-context.test.js +298 -0
- package/lib/security-exception.js +12 -0
- package/lib/throw-security-exception.js +30 -0
- package/lib/throw-security-exception.test.js +50 -0
- package/lib/utils.js +88 -0
- package/lib/utils.test.js +40 -0
- package/package.json +32 -0
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { simpleTraverse } = require('../utils');
|
|
4
|
+
|
|
5
|
+
//
|
|
6
|
+
// these rules are not implemented by agent-lib, but are being considered for
|
|
7
|
+
// implementation:
|
|
8
|
+
// - semantic SQL injection: chaining, dangerous functions, suspicious unions, tautologies
|
|
9
|
+
// - malformed header
|
|
10
|
+
//
|
|
11
|
+
// the following rules are out-of-scope for agent-lib and should be implemented in
|
|
12
|
+
// the agent:
|
|
13
|
+
// - cmd-injection-backdoors (simple indexing)
|
|
14
|
+
// - expression-language-injection (java specific)
|
|
15
|
+
// - method-tampering
|
|
16
|
+
// - ReDOS (java specific)
|
|
17
|
+
// - untrusted-deserialization (agent/framework specific)
|
|
18
|
+
// - xxe (agent/framework specific)
|
|
19
|
+
//
|
|
20
|
+
// of the rules not implemented by agent-lib, only method-tampering and xxe check
|
|
21
|
+
// input and make a decision immediately (i.e., there is no sink). so these two
|
|
22
|
+
// rules will need to be implemented by the agent and called from these handlers.
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
// agent-lib treats each type of nosql database as a separate
|
|
26
|
+
// rule. they are all mapped to 'nosql-injection' in the agent.
|
|
27
|
+
// maybe this will need to change.
|
|
28
|
+
const agentLibRuleTypeToName = {
|
|
29
|
+
'nosql-injection-mongo': 'nosql-injection'
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const preferWW = { preferWorthWatching: true };
|
|
33
|
+
|
|
34
|
+
module.exports = function(core) {
|
|
35
|
+
const {
|
|
36
|
+
logger,
|
|
37
|
+
protect: {
|
|
38
|
+
agentLib,
|
|
39
|
+
inputAnalysis,
|
|
40
|
+
},
|
|
41
|
+
} = core;
|
|
42
|
+
|
|
43
|
+
// all handlers will be invoked with two arguments:
|
|
44
|
+
// 1) sourceContext object containing:
|
|
45
|
+
// - reqData, the abstract request object containing only what is needed
|
|
46
|
+
// - protect, the protect context
|
|
47
|
+
// - rules, exclusions, virtual patches (TS data). what was in effect for this
|
|
48
|
+
// url *at the time the request was started*. these will not change.
|
|
49
|
+
// - block(), function that executes block for this specific request
|
|
50
|
+
// - findings{}, consolidated collection of findings returned by score functions
|
|
51
|
+
// and augmented with disposition and additional information as needed.
|
|
52
|
+
// 2) input or inputs specific to that handler
|
|
53
|
+
//
|
|
54
|
+
// exclusions
|
|
55
|
+
// - url exclusions are applied at the level above the connect handler, if no rules
|
|
56
|
+
// are applicable to a given URL then it's not processed in any way by handlers.
|
|
57
|
+
// - input exclusions are applied **after** analysis. The rationale is that checking
|
|
58
|
+
// inputs against rules 1) is very fast and 2) dramatically pares down the number
|
|
59
|
+
// of exclusion checks that need to be made.
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* handleConnect()
|
|
63
|
+
*
|
|
64
|
+
* handle the inputs that are available when the HTTP 'request' event is emitted.
|
|
65
|
+
*
|
|
66
|
+
* this should *always* be the first handler called; it does setup that other
|
|
67
|
+
* handlers require.
|
|
68
|
+
*
|
|
69
|
+
* the specific data that is available to be processed at this time:
|
|
70
|
+
* - URI
|
|
71
|
+
* - queries
|
|
72
|
+
* - url params
|
|
73
|
+
* - headers (should exclude cookies header)
|
|
74
|
+
* - cookies (special handling of the header)
|
|
75
|
+
*
|
|
76
|
+
* but it really only makes sense to process:
|
|
77
|
+
* - URI
|
|
78
|
+
* - headers except cookies
|
|
79
|
+
*
|
|
80
|
+
* why? because the query string will (probably) be interpreted by the framework using
|
|
81
|
+
* 'qs' and that could result in creating an array or an object instead of a simple text
|
|
82
|
+
* value. and the cookies need to be parsed into individual cookies by the framework so
|
|
83
|
+
* that our code doesn't have to guess at the format, encoding, and other permutations.
|
|
84
|
+
* and for url params - only the framework knows that a portion of the url is a param.
|
|
85
|
+
*
|
|
86
|
+
* if it is known that 'qs' is not being used, then passing the query string in the
|
|
87
|
+
* 'connectInputs' makes sense; a flag similar to 'contentType' can be set and it can be
|
|
88
|
+
* used later to avoid calling 'handleQueryParams()'
|
|
89
|
+
*
|
|
90
|
+
* @param {Object} sourceContext { reqData, protect } that will be supplied to
|
|
91
|
+
* all handlers and sinks for this request. It will always be supplied by the caller
|
|
92
|
+
* to a handler; the handler is not aware of the implementation.
|
|
93
|
+
* @param {Object} connectInputs each property is an input to be evaluated by this
|
|
94
|
+
* handler.
|
|
95
|
+
* @returns {undefined|[String]} undefined to permit else [mode, rule] to block.
|
|
96
|
+
*/
|
|
97
|
+
inputAnalysis.handleConnect = function handleConnect(sourceContext, connectInputs) {
|
|
98
|
+
const { rules: { agentLibRules, agentLibRulesMask: mask } } = sourceContext;
|
|
99
|
+
|
|
100
|
+
// initialize findings to the basics
|
|
101
|
+
let block = undefined;
|
|
102
|
+
if (mask !== 0) {
|
|
103
|
+
const findings = agentLib.scoreRequestConnect(mask, connectInputs, preferWW);
|
|
104
|
+
block = mergeFindings(agentLibRules, sourceContext.findings, findings);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return block;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// this is called before the request goes away. this is where probe detection takes
|
|
111
|
+
// place. basically loop through findings.resultsList and, for all those that weren't
|
|
112
|
+
// blocked, run scoreAtom() with preferWorthWatching: false. for any that have a score
|
|
113
|
+
// >= 90 report them as probes.
|
|
114
|
+
inputAnalysis.handleRequestEnd = function handleRequestEnd(sourceContext) {
|
|
115
|
+
throw new Error('nyi', sourceContext);
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
const jsonInputTypes = {
|
|
120
|
+
keyType: agentLib.InputType.JsonKey, valueType: agentLib.InputType.JsonValue
|
|
121
|
+
};
|
|
122
|
+
const parameterInputTypes = {
|
|
123
|
+
keyType: agentLib.InputType.ParameterKey, valueType: agentLib.InputType.ParameterValue
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* handleQueryParams()
|
|
128
|
+
*
|
|
129
|
+
* If all values are strings there there is no need to re-evaluate them; that was
|
|
130
|
+
* done on requestConnect. But some frameworks use packages like 'qs' to parse the
|
|
131
|
+
* search params, which is non-standard and has many options including changing the
|
|
132
|
+
* delimiters and the ability to create the equivalent of JSON from the search params
|
|
133
|
+
* string. If any of the values are not strings then they need to be evaluated here;
|
|
134
|
+
* these results replace the results generated at requestConnect.
|
|
135
|
+
*
|
|
136
|
+
* If it is known that 'qs' is being used then it is better not to have passed the
|
|
137
|
+
* querystring to scoreRequestConnect().
|
|
138
|
+
*
|
|
139
|
+
* @param {Object} sourceContext
|
|
140
|
+
* @param {Object} queryParams pojo {key: value, ...} for all query params/search params
|
|
141
|
+
*/
|
|
142
|
+
inputAnalysis.handleQueryParams = function handleQueryParams(sourceContext, queryParams) {
|
|
143
|
+
if (typeof queryParams !== 'object') {
|
|
144
|
+
logger.debug({ queryParams }, 'handleQueryParams() called with non-object');
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
commonObjectAnalyzer(sourceContext, queryParams, parameterInputTypes);
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* handleUrlParams()
|
|
152
|
+
*
|
|
153
|
+
* Invoked when a framework emits URL params. It is similar to handling queryParams
|
|
154
|
+
* except no finding for URL params can be present.
|
|
155
|
+
*
|
|
156
|
+
* @param {Object} sourceContext
|
|
157
|
+
* @param {Object} urlParams pojo
|
|
158
|
+
*/
|
|
159
|
+
inputAnalysis.handleUrlParams = function(sourceContext, urlParams) {
|
|
160
|
+
if (typeof urlParams !== 'object') {
|
|
161
|
+
logger.debug({ urlParams }, 'handleUrlParams() called with non-object');
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const { rules, rules: { agentLibRulesMask: mask } } = sourceContext;
|
|
166
|
+
const resultsList = [];
|
|
167
|
+
const { UrlParameter } = agentLib.InputType;
|
|
168
|
+
|
|
169
|
+
simpleTraverse(urlParams, function(path, type, value) {
|
|
170
|
+
// url param names are not checked.
|
|
171
|
+
if (type !== 'Value') {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const items = agentLib.scoreAtom(mask, value, UrlParameter, preferWW);
|
|
175
|
+
if (!items) {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
for (const item of items) {
|
|
179
|
+
resultsList.push({
|
|
180
|
+
ruleId: item.ruleId,
|
|
181
|
+
inputType: 'UrlParameter',
|
|
182
|
+
path: path.slice(),
|
|
183
|
+
key: path.pop(), // there should always be at least the param name
|
|
184
|
+
value,
|
|
185
|
+
score: item.score,
|
|
186
|
+
idsList: [],
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// if nothing was found then nothing needs to be done.
|
|
192
|
+
if (resultsList.length === 0) {
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// something was found, so create "complete" findings.
|
|
197
|
+
const urlParamsFindings = {
|
|
198
|
+
trackRequest: true,
|
|
199
|
+
securityException: undefined,
|
|
200
|
+
resultsList,
|
|
201
|
+
};
|
|
202
|
+
const block = mergeFindings(rules.agentLibRules, sourceContext.findings, urlParamsFindings);
|
|
203
|
+
|
|
204
|
+
if (block) {
|
|
205
|
+
sourceContext.block(...block);
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* handleCookies()
|
|
211
|
+
*
|
|
212
|
+
* Invoked when a framework emits cookies. It is similar to handling queryParams except
|
|
213
|
+
* no findings for cookies can be present.
|
|
214
|
+
* @param {Object} sourceContext
|
|
215
|
+
* @param {Object} cookies pojo
|
|
216
|
+
*/
|
|
217
|
+
inputAnalysis.handleCookies = function(sourceContext, cookies) {
|
|
218
|
+
const cookiesArr = Object.entries(cookies).reduce((acc, [key, value]) => {
|
|
219
|
+
acc.push(key, value);
|
|
220
|
+
return acc;
|
|
221
|
+
}, []);
|
|
222
|
+
const { rules, rules: { agentLibRulesMask: mask } } = sourceContext;
|
|
223
|
+
const cookieFindings = agentLib.scoreRequestConnect(mask, { cookies: cookiesArr }, preferWW);
|
|
224
|
+
|
|
225
|
+
const block = mergeFindings(rules.agentLibRules, sourceContext.findings, cookieFindings);
|
|
226
|
+
|
|
227
|
+
if (block) {
|
|
228
|
+
sourceContext.block(...block);
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* handleParsedBody() is called with the body after a framework has parsed
|
|
234
|
+
* the body. If the body was JSON then it may have already been scored by
|
|
235
|
+
* handleRawBody(); if it was, bodyType was set to 'json' and this code only
|
|
236
|
+
* converts the previous findings, using the parsed body.
|
|
237
|
+
*
|
|
238
|
+
* @param {Object} sourceContext
|
|
239
|
+
* @param {Object} parsedBody
|
|
240
|
+
*/
|
|
241
|
+
inputAnalysis.handleParsedBody = function(sourceContext, parsedBody) {
|
|
242
|
+
if (typeof parsedBody !== 'object') {
|
|
243
|
+
logger.debug({ parsedBody }, 'handleParsedBody() called with non-object');
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
let bodyType;
|
|
248
|
+
let inputTypes;
|
|
249
|
+
if (sourceContext.reqData.contentType.includes('/json')) {
|
|
250
|
+
bodyType = 'json';
|
|
251
|
+
inputTypes = jsonInputTypes;
|
|
252
|
+
} else {
|
|
253
|
+
bodyType = 'urlencoded';
|
|
254
|
+
inputTypes = parameterInputTypes;
|
|
255
|
+
}
|
|
256
|
+
commonObjectAnalyzer(sourceContext, parsedBody, inputTypes);
|
|
257
|
+
sourceContext.findings.bodyType = bodyType;
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
// was MULTIPART_NAME but maybe we should just call it what it is. it's kind
|
|
261
|
+
// of a dumb rule anyway. but maybe some code actually uses the name provided.
|
|
262
|
+
inputAnalysis.handleFileUploadName = function(sourceContext, name) {
|
|
263
|
+
throw new Error('nyi', sourceContext, name);
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* commonObjectAnalyzer() walks an object supplied by the end-user and checks
|
|
269
|
+
* it for vulnerabilities.
|
|
270
|
+
*
|
|
271
|
+
* This can cause the request to be blocked, depending on the mode and findings.
|
|
272
|
+
*
|
|
273
|
+
* @param {Object} sourceContext the sourceContext for the request
|
|
274
|
+
* @param {Object} object the object to analyze. It could be from any input
|
|
275
|
+
* source that can resolve to an object: x-www-form-urlencoded, JSON,
|
|
276
|
+
* query params.
|
|
277
|
+
* @param {Object} inputTypes is either jsonInputTypes or parameterInputTypes,
|
|
278
|
+
* both are defined above. They specify the input types to be used when evaluating
|
|
279
|
+
* the object.
|
|
280
|
+
* @returns undefined
|
|
281
|
+
*/
|
|
282
|
+
function commonObjectAnalyzer(sourceContext, object, inputTypes) {
|
|
283
|
+
const { rules, rules: { agentLibRulesMask: mask } } = sourceContext;
|
|
284
|
+
// use inputTypes to set params...
|
|
285
|
+
const { keyType, valueType } = inputTypes;
|
|
286
|
+
const inputTypeStr = inputTypes === jsonInputTypes ? 'Json' : 'Parameter';
|
|
287
|
+
const { Where } = agentLib.MongoQueryType;
|
|
288
|
+
const resultsList = [];
|
|
289
|
+
|
|
290
|
+
// it's possible to optimize this if qs (or a similar package) is not loaded
|
|
291
|
+
// or if none of the values of queryParams are objects. a quick '.includes()'
|
|
292
|
+
// could be used to determine that. if none are objects then simpleTraverse()
|
|
293
|
+
// wouldn't be used, just a simple "for (const key in queryParams) {...}" to
|
|
294
|
+
// check each key and value associated with the key.
|
|
295
|
+
//
|
|
296
|
+
// otoh, it's a fair amount of additional logic and the gain is likely to be
|
|
297
|
+
// small, so it probably only makes sense to check if qs (or similar) is actually
|
|
298
|
+
// in use. a benchmark of "for (const key in queryParams) {...}" vs simpleTraverse
|
|
299
|
+
// should be created to see if, and in what cases, it makes sense.
|
|
300
|
+
//
|
|
301
|
+
// another day.
|
|
302
|
+
|
|
303
|
+
/* eslint-disable-next-line complexity */
|
|
304
|
+
simpleTraverse(object, function(path, type, value) {
|
|
305
|
+
let itemType;
|
|
306
|
+
let mongoQueryType;
|
|
307
|
+
// this is a bit awkward now because nosql-injection-mongo is not integrated
|
|
308
|
+
// into the scoreAtom() function (or the check_input() function it uses). as
|
|
309
|
+
// a result, the two rules need to be checked independently and the results
|
|
310
|
+
// merged, which is kind of a pia.
|
|
311
|
+
// TODO AGENT-205
|
|
312
|
+
if (type === 'Key') {
|
|
313
|
+
itemType = keyType;
|
|
314
|
+
if (mask & agentLib.RuleType['nosql-injection-mongo']) {
|
|
315
|
+
mongoQueryType = agentLib.getMongoQueryType(value);
|
|
316
|
+
}
|
|
317
|
+
} else {
|
|
318
|
+
itemType = valueType;
|
|
319
|
+
}
|
|
320
|
+
let items = agentLib.scoreAtom(mask, value, itemType, preferWW);
|
|
321
|
+
if (!items && !mongoQueryType) {
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
if (!items) {
|
|
325
|
+
items = [];
|
|
326
|
+
}
|
|
327
|
+
let mongoPath;
|
|
328
|
+
// if the key was a mongo query key, then add it to the items. it requires
|
|
329
|
+
// that additional information is kept as well.
|
|
330
|
+
if (mongoQueryType) {
|
|
331
|
+
const inputToCheck = getValueAtKey(object, path, value);
|
|
332
|
+
// because scoreRequestConnect() returns the query type in the value, we
|
|
333
|
+
// mimic it here (where scoreAtom() was used). the actual object/string
|
|
334
|
+
// to match is stored as `inputToCheck`.
|
|
335
|
+
const inputType = typeof inputToCheck;
|
|
336
|
+
if ((mongoQueryType <= Where && inputType === 'object') || (mongoQueryType >= Where && inputType === 'string')) {
|
|
337
|
+
// the query-type/input-type combination is valid. add a synthesized item.
|
|
338
|
+
const item = { ruleId: 'nosql-injection-mongo', score: 10, mongoContext: { inputToCheck } };
|
|
339
|
+
items.push(item);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
// make each item a complete Finding
|
|
343
|
+
for (const item of items) {
|
|
344
|
+
const result = {
|
|
345
|
+
ruleId: item.ruleId,
|
|
346
|
+
inputType: `${inputTypeStr}${type}`,
|
|
347
|
+
path: mongoPath || path.slice(),
|
|
348
|
+
key: type === 'Key' ? value : path[path.length - 1],
|
|
349
|
+
// mimic scoreRequestConnect() returning the query type as the value
|
|
350
|
+
value: mongoQueryType || value,
|
|
351
|
+
score: item.score,
|
|
352
|
+
idsList: [],
|
|
353
|
+
};
|
|
354
|
+
if (item.mongoContext) {
|
|
355
|
+
result.mongoContext = item.mongoContext;
|
|
356
|
+
}
|
|
357
|
+
resultsList.push(result);
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
// if nothing was found then nothing needs to be done.
|
|
362
|
+
if (resultsList.length === 0) {
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// something was found, so create "complete" findings.
|
|
367
|
+
const findings = {
|
|
368
|
+
trackRequest: true,
|
|
369
|
+
securityException: undefined,
|
|
370
|
+
resultsList,
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
const block = mergeFindings(rules.agentLibRules, sourceContext.findings, findings);
|
|
374
|
+
if (block) {
|
|
375
|
+
sourceContext.block(...block);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* merge new findings into the existing findings
|
|
382
|
+
*
|
|
383
|
+
* @param {Object} sourceContext sourceContext.findings is the existing findings
|
|
384
|
+
* @param {Object} newFindings the findings, in {trackRequest, resultsList} format.
|
|
385
|
+
* @returns {undefined|[String]} undefined to permit else [mode, rule] to block.
|
|
386
|
+
*/
|
|
387
|
+
function mergeFindings(rules, findings, newFindings) {
|
|
388
|
+
if (!newFindings.trackRequest) {
|
|
389
|
+
return findings.securityException;
|
|
390
|
+
}
|
|
391
|
+
normalizeFindings(rules, newFindings);
|
|
392
|
+
|
|
393
|
+
findings.trackRequest = findings.trackRequest || newFindings.trackRequest;
|
|
394
|
+
findings.securityException = findings.securityException || newFindings.securityException;
|
|
395
|
+
|
|
396
|
+
// merge them into a ruleId-indexed map (pojo)
|
|
397
|
+
for (const result of newFindings.resultsList) {
|
|
398
|
+
if (!findings.resultsMap[result.ruleId]) {
|
|
399
|
+
findings.resultsMap[result.ruleId] = [];
|
|
400
|
+
}
|
|
401
|
+
findings.resultsMap[result.ruleId].push(result);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
return findings.securityException;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
//
|
|
408
|
+
// add common fields to findings.
|
|
409
|
+
//
|
|
410
|
+
function normalizeFindings(rules, findings) {
|
|
411
|
+
// now both augment the rules and check to see if any require blocking
|
|
412
|
+
// at perimeter.
|
|
413
|
+
for (const r of findings.resultsList) {
|
|
414
|
+
// augment
|
|
415
|
+
// what additional augmentations are needed?
|
|
416
|
+
// the name/id might need to be mapped but keep the original so it's not lost
|
|
417
|
+
r.mappedId = agentLibRuleTypeToName[r.ruleId] || r.ruleId;
|
|
418
|
+
// this finding resulted in blocking, i.e., it is not a probe.
|
|
419
|
+
r.blocked = false;
|
|
420
|
+
|
|
421
|
+
// sink analysis will add findings here
|
|
422
|
+
r.details = [];
|
|
423
|
+
|
|
424
|
+
// apply exclusions here.
|
|
425
|
+
//
|
|
426
|
+
// apply exclusions after scoring inputs as it will require less work
|
|
427
|
+
// most of the time.
|
|
428
|
+
//
|
|
429
|
+
// the following might need to be changed. BAP is legacy behavior; beyond that,
|
|
430
|
+
// the only way a score >= 90 can come back is if there is no "worth-watching"
|
|
431
|
+
// option and that implies that there is no sink, so this is the only place at
|
|
432
|
+
// which the block can occur. so at a minimum 'block' should also result in a
|
|
433
|
+
// block.
|
|
434
|
+
const { mode } = rules[r.ruleId];
|
|
435
|
+
if (r.score >= 90 && ['block', 'block_at_perimeter'].includes(mode)) {
|
|
436
|
+
r.blocked = true;
|
|
437
|
+
findings.securityException = [mode, r.ruleId];
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* getValueAtKey() is used to fetch the object (expected) associated
|
|
444
|
+
* with the path of keys in obj. i say expected because this is only used
|
|
445
|
+
* for fetching the objects associated with a nosql vulnerability and those
|
|
446
|
+
* should always be objects.
|
|
447
|
+
*
|
|
448
|
+
* @param {Object} obj an object with keys
|
|
449
|
+
* @param {Array} path list of keys to walk through the object
|
|
450
|
+
* @param {String} lastKey the last key (it's not in path)
|
|
451
|
+
*
|
|
452
|
+
* @returns the value at end of walking path in obj
|
|
453
|
+
*/
|
|
454
|
+
function getValueAtKey(obj, path, key) {
|
|
455
|
+
for (const p of path) {
|
|
456
|
+
if (!(p in obj)) {
|
|
457
|
+
return undefined;
|
|
458
|
+
}
|
|
459
|
+
obj = obj[p];
|
|
460
|
+
}
|
|
461
|
+
return key in obj ? obj[key] : undefined;
|
|
462
|
+
}
|