@contrast/assess 1.6.0 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/lib/dataflow/propagation/index.js +3 -0
  2. package/lib/dataflow/propagation/install/JSON/index.js +33 -0
  3. package/lib/dataflow/propagation/install/JSON/stringify.js +290 -0
  4. package/lib/dataflow/propagation/install/buffer.js +79 -0
  5. package/lib/dataflow/propagation/install/contrast-methods/string.js +5 -1
  6. package/lib/dataflow/propagation/install/encode-uri-component.js +5 -2
  7. package/lib/dataflow/propagation/install/pug-runtime-escape.js +5 -2
  8. package/lib/dataflow/propagation/install/sequelize.js +310 -0
  9. package/lib/dataflow/propagation/install/sql-template-strings.js +5 -4
  10. package/lib/dataflow/propagation/install/string/match.js +2 -2
  11. package/lib/dataflow/propagation/install/string/replace.js +9 -4
  12. package/lib/dataflow/sinks/common.js +10 -1
  13. package/lib/dataflow/sinks/index.js +30 -1
  14. package/lib/dataflow/sinks/install/express/index.js +29 -0
  15. package/lib/dataflow/sinks/install/express/unvalidated-redirect.js +134 -0
  16. package/lib/dataflow/sinks/install/fastify/unvalidated-redirect.js +96 -69
  17. package/lib/dataflow/sinks/install/http.js +20 -5
  18. package/lib/dataflow/sinks/install/koa/unvalidated-redirect.js +33 -9
  19. package/lib/dataflow/sinks/install/mongodb.js +297 -82
  20. package/lib/dataflow/sinks/install/mssql.js +9 -4
  21. package/lib/dataflow/sinks/install/mysql.js +20 -4
  22. package/lib/dataflow/sinks/install/postgres.js +25 -12
  23. package/lib/dataflow/sinks/install/sequelize.js +142 -0
  24. package/lib/dataflow/sinks/install/sqlite3.js +9 -4
  25. package/lib/dataflow/sources/handler.js +144 -26
  26. package/lib/dataflow/sources/index.js +6 -8
  27. package/lib/dataflow/sources/install/body-parser1.js +133 -0
  28. package/lib/dataflow/sources/install/cookie-parser1.js +101 -0
  29. package/lib/dataflow/sources/install/express/index.js +31 -0
  30. package/lib/dataflow/sources/install/express/params.js +81 -0
  31. package/lib/dataflow/sources/install/express/parsedUrl.js +87 -0
  32. package/lib/dataflow/sources/install/http.js +32 -18
  33. package/lib/dataflow/sources/install/querystring.js +75 -0
  34. package/lib/dataflow/tag-utils.js +68 -1
  35. package/package.json +3 -3
@@ -20,16 +20,18 @@ const {
20
20
  DataflowTag: {
21
21
  UNTRUSTED,
22
22
  ALPHANUM_SPACE_HYPHEN,
23
+ CUSTOM_VALIDATED,
23
24
  CUSTOM_VALIDATED_NOSQL_INJECTION,
24
25
  LIMITED_CHARS,
25
26
  STRING_TYPE_CHECKED,
26
27
  },
27
- Rule,
28
+ Rule: { NOSQL_INJECTION_MONGO },
28
29
  isNonEmptyObject,
29
- traverseValues
30
+ traverseValues,
31
+ isString
30
32
  } = require('@contrast/common');
31
33
  const utils = require('../../tag-utils');
32
- const { patchType } = require('../common');
34
+ const { patchType, filterSafeTags } = require('../common');
33
35
 
34
36
  const collectionMethods = [
35
37
  'find',
@@ -45,10 +47,20 @@ const collectionMethods = [
45
47
  'updateMany',
46
48
  'deleteOne',
47
49
  'deleteMany',
50
+ 'aggregate',
51
+ 'mapReduce',
52
+ 'group'
48
53
  ];
54
+ const dbMethods = [
55
+ 'command',
56
+ 'eval',
57
+ 'aggregate'
58
+ ];
59
+ const methodsWithNestedCalls = ['findOne', 'eval', 'group'];
49
60
 
50
61
  const querySafeTags = [
51
62
  ALPHANUM_SPACE_HYPHEN,
63
+ CUSTOM_VALIDATED,
52
64
  CUSTOM_VALIDATED_NOSQL_INJECTION,
53
65
  LIMITED_CHARS,
54
66
  STRING_TYPE_CHECKED,
@@ -56,6 +68,7 @@ const querySafeTags = [
56
68
 
57
69
  module.exports = function(core) {
58
70
  const {
71
+ config,
59
72
  depHooks,
60
73
  logger,
61
74
  patcher,
@@ -63,7 +76,7 @@ module.exports = function(core) {
63
76
  assess: {
64
77
  dataflow: {
65
78
  tracker,
66
- sinks: { isVulnerable, reportFindings },
79
+ sinks: { isVulnerable, runInActiveSink, isLocked, reportFindings, reportSafePositive },
67
80
  eventFactory: { createSinkEvent }
68
81
  }
69
82
  }
@@ -72,22 +85,236 @@ module.exports = function(core) {
72
85
  const inspect = patcher.unwrap(util.inspect);
73
86
  const instr = core.assess.dataflow.sinks.mongodb = {};
74
87
 
75
- instr.getVulnerabilityInfo = function getVulnerabilityInfo(query) {
88
+ instr.getQueryVulnerabilityInfo = function getQueryVulnerabilityInfo(query) {
76
89
  let vulnInfo = null;
90
+ let reportSafe = null;
91
+
92
+ if (isString(query)) {
93
+ const strInfo = tracker.getData(query);
94
+ if (strInfo) {
95
+ if (isVulnerable(UNTRUSTED, querySafeTags, strInfo.tags)) {
96
+ vulnInfo = { strInfo };
97
+ } else {
98
+ reportSafe = { strInfo };
99
+ }
100
+ }
77
101
 
78
- if (!isNonEmptyObject(query)) return vulnInfo;
102
+ return { vulnInfo, reportSafe };
103
+ }
104
+
105
+ if (!isNonEmptyObject(query)) return { vulnInfo, reportSafe };
79
106
 
80
- traverseValues(query, (path, type, value) => {
107
+ traverseValues(query, (path, _type, value) => {
81
108
  const strInfo = tracker.getData(value);
82
- if (strInfo && isVulnerable(UNTRUSTED, querySafeTags, strInfo.tags)) {
83
- vulnInfo = { path, strInfo };
84
- return true; // halts traversal
109
+ if (strInfo) {
110
+ if (isVulnerable(UNTRUSTED, querySafeTags, strInfo.tags)) {
111
+ vulnInfo = { path: [...path], strInfo };
112
+ return true; // halts traversal
113
+ } else {
114
+ reportSafe = { path: [...path], strInfo };
115
+ }
85
116
  }
86
117
  });
87
118
 
88
- return vulnInfo;
119
+ return { vulnInfo, reportSafe };
89
120
  };
90
121
 
122
+ instr.getAggregateVulnerabilityInfo = function getAggregateVulnerabilityInfo(aggregation) {
123
+ let vulnInfo = null;
124
+ let reportSafe = null;
125
+
126
+ if (!isNonEmptyObject(aggregation)) return { vulnInfo, reportSafe };
127
+
128
+ traverseValues(aggregation, (path, _type, value) => {
129
+ const accumulatorFuncProps = [
130
+ 'init',
131
+ 'merge',
132
+ 'accumulate',
133
+ 'finalize'
134
+ ];
135
+ const lastIdx = path.length - 1;
136
+ if (
137
+ (path[lastIdx - 1] === '$function' && path[lastIdx] === 'body') ||
138
+ (path[lastIdx - 1] === '$accumulator' && accumulatorFuncProps.includes(path[lastIdx]))
139
+ ) {
140
+ const strInfo = tracker.getData(value);
141
+ if (strInfo) {
142
+ if (isVulnerable(UNTRUSTED, querySafeTags, strInfo.tags)) {
143
+ vulnInfo = { path: [...path], strInfo };
144
+ return true; // halts traversal
145
+ } else {
146
+ reportSafe = { path: [...path], strInfo };
147
+ }
148
+ }
149
+ }
150
+ });
151
+
152
+ return { vulnInfo, reportSafe };
153
+ };
154
+
155
+ instr.getMapReduceVulnerabilityInfo = function getMapReduceVulnerabilityInfo(argToCheck, argIdx) {
156
+ let vulnInfo = null;
157
+ let reportSafe = null;
158
+
159
+ if (argIdx !== 2 && isString(argToCheck)) {
160
+ const strInfo = tracker.getData(argToCheck);
161
+ if (strInfo) {
162
+ if (isVulnerable(UNTRUSTED, querySafeTags, strInfo.tags)) {
163
+ vulnInfo = { strInfo };
164
+ } else {
165
+ reportSafe = { strInfo };
166
+ }
167
+ }
168
+
169
+ return { vulnInfo, reportSafe };
170
+ }
171
+
172
+ if (!isNonEmptyObject(argToCheck)) return { vulnInfo, reportSafe };
173
+
174
+ traverseValues(argToCheck, (path, _type, value) => {
175
+ const vulnerableProps = [
176
+ 'query',
177
+ 'finalize',
178
+ ];
179
+ if (vulnerableProps.includes(path[0])) {
180
+ const strInfo = tracker.getData(value);
181
+ if (strInfo) {
182
+ if (isVulnerable(UNTRUSTED, querySafeTags, strInfo.tags)) {
183
+ vulnInfo = { path: [...path], strInfo };
184
+ return true; // halts traversal
185
+ } else {
186
+ reportSafe = { path: [...path], strInfo };
187
+ }
188
+ }
189
+ }
190
+ });
191
+
192
+ return { vulnInfo, reportSafe };
193
+ };
194
+
195
+ instr.getGroupVulnerabilityInfo = function getGroupVulnerabilityInfo(argToCheck, argIdx) {
196
+ let vulnInfo = null;
197
+ let reportSafe = null;
198
+
199
+ if (argIdx !== 1 && isString(argToCheck)) {
200
+ const strInfo = tracker.getData(argToCheck);
201
+ if (strInfo) {
202
+ if (isVulnerable(UNTRUSTED, querySafeTags, strInfo.tags)) {
203
+ vulnInfo = { strInfo };
204
+ } else {
205
+ reportSafe = { strInfo };
206
+ }
207
+ }
208
+
209
+ return { vulnInfo, reportSafe };
210
+ }
211
+
212
+ if (!isNonEmptyObject(argToCheck)) return { vulnInfo, reportSafe };
213
+
214
+ traverseValues(argToCheck, (path, _type, value) => {
215
+ const strInfo = tracker.getData(value);
216
+ if (strInfo) {
217
+ if (isVulnerable(UNTRUSTED, querySafeTags, strInfo.tags)) {
218
+ vulnInfo = { path: [...path], strInfo };
219
+ return true; // halts traversal
220
+ } else {
221
+ reportSafe = { path: [...path], strInfo };
222
+ }
223
+ }
224
+ });
225
+
226
+ return { vulnInfo, reportSafe };
227
+ };
228
+
229
+ function createAroundHook(entity, name, method, getInfoMethod, vulnerableArgIdxs) {
230
+ const argsIdxsToCheck = vulnerableArgIdxs || [0];
231
+ return function(next, data) {
232
+ const { obj, args: origArgs } = data;
233
+ const sourceCtx = sources.getStore()?.assess;
234
+
235
+ if (isLocked(NOSQL_INJECTION_MONGO) || instrumentation.isLocked() || !sourceCtx) {
236
+ return next();
237
+ }
238
+
239
+ let vulnInfo;
240
+ let reportSafe;
241
+ let vulnArgIdx;
242
+ const safeReports = [];
243
+
244
+ try {
245
+ for (const argIdx of argsIdxsToCheck) {
246
+ ({ vulnInfo, reportSafe } = getInfoMethod(origArgs[argIdx], argIdx));
247
+
248
+ if (vulnInfo) {
249
+ vulnArgIdx = argIdx;
250
+ break;
251
+ }
252
+ if (reportSafe) safeReports.push({ ...reportSafe, argIdx });
253
+ }
254
+
255
+ if (!vulnInfo) {
256
+ if (safeReports.length && config.assess.safe_positives.enable) {
257
+ const safeTags = safeReports.map((report) => filterSafeTags(querySafeTags, report.strInfo));
258
+ const strInfo = safeReports.map((report) => {
259
+ const tags = report.path ? getAdjustedQueryTags(report.path, report.strInfo, inspect(origArgs[report.argIdx], { depth: 4 })) : report.strInfo?.tags;
260
+
261
+ return {
262
+ value: inspect(origArgs[report.argIdx], { depth: 4 }),
263
+ tags
264
+ };
265
+ });
266
+
267
+ reportSafePositive({
268
+ name,
269
+ ruleId: NOSQL_INJECTION_MONGO,
270
+ safeTags: safeTags.length === 1 ? safeTags[0] : safeTags,
271
+ strInfo: strInfo.length === 1 ? strInfo[0] : strInfo
272
+ });
273
+ }
274
+
275
+ return methodsWithNestedCalls.includes(method) ? runInActiveSink(NOSQL_INJECTION_MONGO, async () => await next()) : next();
276
+ }
277
+
278
+ const { path, strInfo } = vulnInfo;
279
+ const objName = getObjectName(obj, entity);
280
+ const args = origArgs.map((arg, idx) => ({
281
+ value: isString(arg) ? arg : inspect(arg, { depth: 4 }),
282
+ tracked: idx === vulnArgIdx,
283
+ }));
284
+
285
+ const tags = path ? getAdjustedQueryTags(path, strInfo, args[vulnArgIdx].value) : strInfo?.tags;
286
+ const resultVal = args[args.length - 1].value.startsWith('[Function') ? '' : 'Promise';
287
+ const sinkEvent = createSinkEvent({
288
+ args,
289
+ context: `${objName}.${method}(${args.map((a) => a.value)})`,
290
+ history: [strInfo],
291
+ object: {
292
+ tracked: false,
293
+ value: `mongodb.${entity}`,
294
+ },
295
+ name,
296
+ result: {
297
+ tracked: false,
298
+ value: resultVal,
299
+ },
300
+ source: `P${vulnArgIdx}`,
301
+ stacktraceOpts: {
302
+ constructorOpt: data.hooked,
303
+ },
304
+ tags,
305
+ });
306
+
307
+ if (sinkEvent) {
308
+ reportFindings({ ruleId: NOSQL_INJECTION_MONGO, sinkEvent });
309
+ }
310
+ } catch (err) {
311
+ core.logger.error({ name, err }, 'assess sink analysis failed');
312
+ }
313
+
314
+ return methodsWithNestedCalls.includes(method) ? runInActiveSink(NOSQL_INJECTION_MONGO, async () => await next()) : next();
315
+ };
316
+ }
317
+
91
318
  instr.install = function() {
92
319
  depHooks.resolve({ name: 'mongodb' }, (mongodb, version) => {
93
320
  patchCollection(mongodb, version);
@@ -99,93 +326,77 @@ module.exports = function(core) {
99
326
 
100
327
  function patchCollection(mongodb, version) {
101
328
  for (const method of collectionMethods) {
102
-
103
329
  const proto = mongodb.Collection.prototype;
104
330
  const name = `mongodb.Collection.prototype.${method}`;
105
331
 
106
332
  if (!proto[method]) {
107
-
108
333
  logger.trace({ name, version }, 'method not found - skipping instrumentation');
109
334
  continue;
110
335
  }
111
336
 
112
- patcher.patch(proto, method, {
113
- name,
114
- patchType,
115
- around(next, data) {
116
- const { obj, args } = data;
117
- const sourceCtx = sources.getStore()?.assess;
118
-
119
- if (instrumentation.isLocked() || !sourceCtx) {
120
- return next();
121
- }
122
-
123
- const argIdx = 0;
124
- try {
125
- const vulnInfo = instr.getVulnerabilityInfo(args[argIdx]);
126
- if (vulnInfo) {
127
- const { path, strInfo } = vulnInfo;
128
- const objName = getObjectName(obj);
129
- const args = data.args.map((arg, idx) => ({
130
- value: inspect(arg),
131
- tracked: idx === argIdx,
132
- }));
133
-
134
- const tags = getAdjustedQueryTags(path, strInfo, args[argIdx].value);
135
- const resultVal = args[args.length - 1].value.startsWith('[Function') ? '' : 'Promise';
136
- const sinkEvent = createSinkEvent({
137
- args,
138
- context: `${objName}.${method}(${args.map((a) => a.value)})`,
139
- history: [strInfo],
140
- object: {
141
- tracked: false,
142
- value: 'mongodb.Collection',
143
- },
144
- name,
145
- result: {
146
- tracked: false,
147
- value: resultVal,
148
- },
149
- source: `P${argIdx}`,
150
- stacktraceOpts: {
151
- constructorOpt: data.hooked,
152
- },
153
- tags,
154
- });
155
-
156
- if (sinkEvent) {
157
- reportFindings({ ruleId: Rule.NOSQL_INJECTION_MONGO, sinkEvent });
158
- }
159
- }
160
- } catch (err) {
161
- core.logger.error({ name, err }, 'assess sink analysis failed');
162
- }
163
-
164
- if (method === 'findOne') {
165
- // `findOne` will call `find` so don't analyze in nested call
166
- const store = { name, lock: true };
167
- const ret = instrumentation.run(store, next);
168
- // but unlock for when callback args run or returned Promises resolve/reject
169
- store.lock = false;
170
- return ret;
171
- }
172
-
173
- return next();
174
- },
175
- });
337
+ if (method === 'aggregate') {
338
+ patcher.patch(proto, method, {
339
+ name,
340
+ patchType,
341
+ around: createAroundHook('Collection', name, method, instr.getAggregateVulnerabilityInfo)
342
+ });
343
+ } else if (method === 'mapReduce') {
344
+ patcher.patch(proto, method, {
345
+ name,
346
+ patchType,
347
+ around: createAroundHook('Collection', name, method, instr.getMapReduceVulnerabilityInfo, [0, 1, 2])
348
+ });
349
+ } else if (method === 'group') {
350
+ patcher.patch(proto, method, {
351
+ name,
352
+ patchType,
353
+ around: createAroundHook('Collection', name, method, instr.getGroupVulnerabilityInfo, [0, 1, 3])
354
+ });
355
+ } else {
356
+ patcher.patch(proto, method, {
357
+ name,
358
+ patchType,
359
+ around: createAroundHook('Collection', name, method, instr.getQueryVulnerabilityInfo)
360
+ });
361
+ }
176
362
  }
177
363
  }
178
364
 
179
-
180
365
  function patchDatabase(mongodb, version) {
181
- // todo
366
+ for (const method of dbMethods) {
367
+ const proto = mongodb.Db.prototype;
368
+ const name = `mongodb.Db.prototype.${method}`;
369
+
370
+ if (!proto[method]) {
371
+ logger.trace({ name, version }, 'method not found - skipping instrumentation');
372
+ continue;
373
+ }
374
+
375
+ if (method === 'aggregate') {
376
+ patcher.patch(proto, method, {
377
+ name,
378
+ patchType,
379
+ around: createAroundHook('Db', name, method, instr.getAggregateVulnerabilityInfo)
380
+ });
381
+ } else {
382
+ patcher.patch(proto, method, {
383
+ name,
384
+ patchType,
385
+ around: createAroundHook('Db', name, method, instr.getQueryVulnerabilityInfo)
386
+ });
387
+ }
388
+ }
182
389
  }
183
390
 
184
- function getObjectName(obj) {
391
+ function getObjectName(obj, entity) {
185
392
  let name = '';
186
393
  name += obj.s?.namespace?.db || 'db';
187
- name += '.';
188
- name += obj.s?.namespace?.collection || 'collection';
394
+
395
+ if (entity !== 'Db') {
396
+ name += '.';
397
+ name += obj.s?.namespace?.collection || 'collection';
398
+ }
399
+
189
400
  return name;
190
401
  }
191
402
 
@@ -193,6 +404,10 @@ module.exports = function(core) {
193
404
  const { tags } = strInfo;
194
405
  let idx = -1;
195
406
  for (const str of [...path, strInfo.value]) {
407
+ // This is the case with the `.aggregate` method
408
+ // where the argument is an array
409
+ if (str === 0) continue;
410
+
196
411
  idx = argString.indexOf(str, idx);
197
412
  if (idx == -1) {
198
413
  idx = -1;
@@ -17,7 +17,7 @@
17
17
 
18
18
  const {
19
19
  DataflowTag: { UNTRUSTED, SQL_ENCODED, LIMITED_CHARS, CUSTOM_VALIDATED, CUSTOM_ENCODED },
20
- Rule,
20
+ Rule: { SQL_INJECTION },
21
21
  isString
22
22
  } = require('@contrast/common');
23
23
  const { createModuleLabel } = require('../../propagation/common');
@@ -38,7 +38,7 @@ module.exports = function (core) {
38
38
  assess: {
39
39
  dataflow: {
40
40
  tracker,
41
- sinks: { isVulnerable, reportFindings },
41
+ sinks: { isVulnerable, isLocked, reportFindings },
42
42
  eventFactory: { createSinkEvent },
43
43
  },
44
44
  },
@@ -46,7 +46,12 @@ module.exports = function (core) {
46
46
 
47
47
  const pre = (name, obj, version) => (data) => {
48
48
  const store = sources.getStore()?.assess;
49
- if (!store || !data.args[0] || !isString(data.args[0])) return;
49
+ if (
50
+ !store ||
51
+ !data.args[0] ||
52
+ !isString(data.args[0]) ||
53
+ isLocked(SQL_INJECTION)
54
+ ) return;
50
55
 
51
56
  const strInfo = tracker.getData(data.args[0]);
52
57
  if (!strInfo || !isVulnerable(UNTRUSTED, safeTags, strInfo.tags)) {
@@ -75,7 +80,7 @@ module.exports = function (core) {
75
80
 
76
81
  if (event) {
77
82
  reportFindings({
78
- ruleId: Rule.SQL_INJECTION,
83
+ ruleId: SQL_INJECTION,
79
84
  sinkEvent: event,
80
85
  });
81
86
  }
@@ -16,7 +16,19 @@
16
16
  'use strict';
17
17
 
18
18
  const { patchType } = require('../common');
19
- const { Rule, isString, DataflowTag: { CUSTOM_ENCODED_SQL_INJECTION, CUSTOM_ENCODED, CUSTOM_VALIDATED_SQL_INJECTION, CUSTOM_VALIDATED, SQL_ENCODED, LIMITED_CHARS, UNTRUSTED } } = require('@contrast/common');
19
+ const {
20
+ Rule: { SQL_INJECTION },
21
+ isString,
22
+ DataflowTag: {
23
+ CUSTOM_ENCODED_SQL_INJECTION,
24
+ CUSTOM_ENCODED,
25
+ CUSTOM_VALIDATED_SQL_INJECTION,
26
+ CUSTOM_VALIDATED,
27
+ SQL_ENCODED,
28
+ LIMITED_CHARS,
29
+ UNTRUSTED
30
+ },
31
+ } = require('@contrast/common');
20
32
 
21
33
  const safeTags = [
22
34
  CUSTOM_ENCODED_SQL_INJECTION,
@@ -35,7 +47,7 @@ module.exports = function (core) {
35
47
  assess: {
36
48
  dataflow: {
37
49
  tracker,
38
- sinks: { isVulnerable, reportFindings },
50
+ sinks: { isVulnerable, isLocked, reportFindings },
39
51
  eventFactory: { createSinkEvent },
40
52
  },
41
53
  },
@@ -53,7 +65,11 @@ module.exports = function (core) {
53
65
 
54
66
  const pre = (module, file, obj) => (data) => {
55
67
  const store = sources.getStore()?.assess;
56
- if (!store || !data.args[0]) return;
68
+ if (
69
+ !store ||
70
+ !data.args[0] ||
71
+ isLocked(SQL_INJECTION)
72
+ ) return;
57
73
 
58
74
  const val = getValueFromArgs(data.args);
59
75
  if (!val) return;
@@ -85,7 +101,7 @@ module.exports = function (core) {
85
101
 
86
102
  if (event) {
87
103
  reportFindings({
88
- ruleId: Rule.SQL_INJECTION,
104
+ ruleId: SQL_INJECTION,
89
105
  sinkEvent: event,
90
106
  });
91
107
  }
@@ -18,20 +18,21 @@
18
18
  const util = require('util');
19
19
  const {
20
20
  DataflowTag: { UNTRUSTED, SQL_ENCODED, LIMITED_CHARS, CUSTOM_VALIDATED, CUSTOM_ENCODED },
21
- Rule,
21
+ Rule: { SQL_INJECTION: ruleId },
22
22
  isString
23
23
  } = require('@contrast/common');
24
- const { patchType } = require('../common');
24
+ const { filterSafeTags, patchType } = require('../common');
25
25
 
26
26
  module.exports = function (core) {
27
27
  const {
28
+ config,
28
29
  depHooks,
29
30
  patcher,
30
31
  scopes: { sources },
31
32
  assess: {
32
33
  dataflow: {
33
34
  tracker,
34
- sinks: { isVulnerable, reportFindings },
35
+ sinks: { isVulnerable, isLocked, reportFindings, reportSafePositive },
35
36
  eventFactory: { createSinkEvent },
36
37
  },
37
38
  },
@@ -48,15 +49,17 @@ module.exports = function (core) {
48
49
 
49
50
  const postgres = core.assess.dataflow.sinks.postgres = {};
50
51
 
51
- const preHook = (methodSignature, version, mod, obj) => (data) => {
52
+ const preHook = (methodSignature) => (data) => {
52
53
  const assessStore = sources.getStore()?.assess;
53
- if (!assessStore) return;
54
+ if (!assessStore || isLocked(ruleId)) return;
54
55
 
55
56
  const [arg0] = data.args;
56
57
  const query = arg0?.text || arg0;
57
58
  if (!query || !isString(query)) return;
58
59
 
59
60
  const strInfo = tracker.getData(query);
61
+ // todo: if the query isn't tracked but users are sending tracked strings in the values
62
+ // array (args[1]), then shouldn't we report a "safe-positive" event of some kind?
60
63
  if (!strInfo) return;
61
64
 
62
65
  const objValue = methodSignature.includes('native') ? 'pg.native.Client' : 'pg.Client';
@@ -88,10 +91,20 @@ module.exports = function (core) {
88
91
 
89
92
  if (event) {
90
93
  reportFindings({
91
- ruleId: Rule.SQL_INJECTION,
94
+ ruleId,
92
95
  sinkEvent: event,
93
96
  });
94
97
  }
98
+ } else if (config.assess.safe_positives.enable) {
99
+ reportSafePositive({
100
+ name: methodSignature,
101
+ ruleId,
102
+ safeTags: filterSafeTags(safeTags, strInfo),
103
+ strInfo: {
104
+ value: strInfo.value,
105
+ tags: strInfo.tags,
106
+ }
107
+ });
95
108
  }
96
109
  };
97
110
 
@@ -99,11 +112,11 @@ module.exports = function (core) {
99
112
  const pgClientQueryPatchName = 'pg.Client.prototype.query';
100
113
  depHooks.resolve(
101
114
  { name: 'pg', file: 'lib/client.js' },
102
- (client, version) => {
115
+ (client) => {
103
116
  patcher.patch(client.prototype, 'query', {
104
117
  name: pgClientQueryPatchName,
105
118
  patchType,
106
- pre: preHook('pg/lib/client.prototype.query', version, 'pg', 'Client'),
119
+ pre: preHook('pg/lib/client.prototype.query'),
107
120
  });
108
121
  },
109
122
  );
@@ -111,16 +124,16 @@ module.exports = function (core) {
111
124
  const pgNativeClientQueryPatchName = 'pg.native.Client.prototype.query';
112
125
  depHooks.resolve(
113
126
  { name: 'pg', file: 'lib/native/client.js' },
114
- (client, version) => {
127
+ (client) => {
115
128
  patcher.patch(client.prototype, 'query', {
116
129
  name: pgNativeClientQueryPatchName,
117
130
  patchType,
118
- pre: preHook('pg/lib/native/client.prototype.query', version, 'pg', 'native.Client'),
131
+ pre: preHook('pg/lib/native/client.prototype.query'),
119
132
  });
120
133
  },
121
134
  );
122
135
 
123
- depHooks.resolve({ name: 'pg-pool' }, (pool, version) => {
136
+ depHooks.resolve({ name: 'pg-pool' }, (pool) => {
124
137
  const name = 'pg-pool.Pool.prototype.query';
125
138
  patcher.patch(pool.prototype, 'query', {
126
139
  name,
@@ -137,7 +150,7 @@ module.exports = function (core) {
137
150
  return;
138
151
  }
139
152
 
140
- preHook(name, version, 'pg-pool', 'Pool')(data);
153
+ preHook(name)(data);
141
154
  },
142
155
  });
143
156
  });