@contrast/agent 4.12.2 → 4.14.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 (66) hide show
  1. package/bootstrap.js +2 -3
  2. package/esm.mjs +9 -35
  3. package/lib/assess/membrane/debraner.js +0 -2
  4. package/lib/assess/membrane/index.js +1 -3
  5. package/lib/assess/models/tag-range/util.js +1 -2
  6. package/lib/assess/policy/propagators.json +13 -4
  7. package/lib/assess/policy/rules.json +42 -0
  8. package/lib/assess/policy/signatures.json +18 -0
  9. package/lib/assess/policy/util.js +3 -2
  10. package/lib/assess/propagators/JSON/stringify.js +6 -11
  11. package/lib/assess/propagators/ajv/conditionals.js +0 -3
  12. package/lib/assess/propagators/ajv/json-schema-type-evaluators.js +5 -4
  13. package/lib/assess/propagators/ajv/refs.js +1 -2
  14. package/lib/assess/propagators/ajv/schema-context.js +2 -3
  15. package/lib/assess/propagators/joi/any.js +1 -1
  16. package/lib/assess/propagators/joi/object.js +1 -1
  17. package/lib/assess/propagators/joi/string-base.js +16 -3
  18. package/lib/assess/propagators/mongoose/map.js +1 -1
  19. package/lib/assess/propagators/mongoose/mixed.js +1 -1
  20. package/lib/assess/propagators/mongoose/string.js +1 -1
  21. package/lib/assess/propagators/path/common.js +38 -29
  22. package/lib/assess/propagators/path/resolve.js +1 -0
  23. package/lib/assess/propagators/sequelize/utils.js +1 -2
  24. package/lib/assess/propagators/v8/init-hooks.js +0 -1
  25. package/lib/assess/sinks/dynamo.js +65 -30
  26. package/lib/assess/static/hardcoded.js +3 -3
  27. package/lib/assess/static/read-findings-from-cache.js +40 -0
  28. package/lib/assess/technologies/index.js +12 -13
  29. package/lib/cli-rewriter/index.js +65 -6
  30. package/lib/core/async-storage/hooks/mysql.js +57 -6
  31. package/lib/core/config/options.js +12 -6
  32. package/lib/core/config/util.js +15 -33
  33. package/lib/core/exclusions/input.js +6 -1
  34. package/lib/core/express/index.js +2 -4
  35. package/lib/core/logger/debug-logger.js +2 -2
  36. package/lib/core/stacktrace.js +2 -1
  37. package/lib/hooks/http.js +81 -81
  38. package/lib/hooks/require.js +1 -0
  39. package/lib/instrumentation.js +17 -0
  40. package/lib/protect/analysis/aho-corasick.js +1 -1
  41. package/lib/protect/errors/handler-async-errors.js +66 -0
  42. package/lib/protect/input-analysis.js +7 -13
  43. package/lib/protect/listeners.js +27 -23
  44. package/lib/protect/rules/base-scanner/index.js +2 -2
  45. package/lib/protect/rules/bot-blocker/bot-blocker-rule.js +4 -2
  46. package/lib/protect/rules/cmd-injection/cmdinjection-rule.js +57 -2
  47. package/lib/protect/rules/cmd-injection-semantic-chained-commands/cmd-injection-semantic-chained-commands-rule.js +31 -2
  48. package/lib/protect/rules/cmd-injection-semantic-dangerous-paths/cmd-injection-semantic-dangerous-paths-rule.js +32 -2
  49. package/lib/protect/rules/index.js +42 -21
  50. package/lib/protect/rules/ip-denylist/ip-denylist-rule.js +2 -2
  51. package/lib/protect/rules/nosqli/nosql-injection-rule.js +104 -39
  52. package/lib/protect/rules/path-traversal/path-traversal-rule.js +3 -0
  53. package/lib/protect/rules/rule-factory.js +6 -7
  54. package/lib/protect/rules/signatures/signature.js +3 -0
  55. package/lib/protect/rules/sqli/sql-injection-rule.js +98 -5
  56. package/lib/protect/rules/sqli/sql-scanner/labels.json +0 -3
  57. package/lib/protect/rules/xss/reflected-xss-rule.js +3 -3
  58. package/lib/protect/sample-aggregator.js +65 -57
  59. package/lib/protect/service.js +709 -104
  60. package/lib/reporter/models/app-activity/sample.js +6 -0
  61. package/lib/reporter/speedracer/unknown-connection-state.js +20 -32
  62. package/lib/reporter/translations/to-protobuf/settings/assess-features.js +4 -6
  63. package/lib/reporter/ts-reporter.js +1 -1
  64. package/lib/util/get-file-type.js +43 -0
  65. package/package.json +11 -11
  66. package/perf-logs.js +2 -5
@@ -41,7 +41,10 @@ class PathTraversalRule extends Rule {
41
41
  INPUT_TYPES.URI,
42
42
  INPUT_TYPES.URL_PARAMETER
43
43
  ];
44
+
44
45
  this.applicableSinks = [SINK_TYPES.FILE_PATH];
46
+
47
+ this.usesLibInputAnalysis = true;
45
48
  }
46
49
 
47
50
  /**
@@ -27,7 +27,6 @@ const DEFAULT_SETTINGS = { serverFeatures: { defend: { enabled: false } } };
27
27
  * implemented, we add its constructor path to provide support.
28
28
  */
29
29
  const ctors = {
30
- /* eslint-disable prettier/prettier */
31
30
  // protect rules
32
31
  [RULES.CMD_INJECTION]: require('./cmd-injection/cmdinjection-rule'),
33
32
  [RULES.CMD_INJECTION_COMMAND_BACKDOORS]: require('./cmd-injection-command-backdoors/cmd-injection-command-backdoors-rule'),
@@ -46,15 +45,15 @@ const ctors = {
46
45
  [RULES.BOT_BLOCKER]: require('./bot-blocker/bot-blocker-rule'),
47
46
  [RULES.IP_DENYLIST]: require('./ip-denylist/ip-denylist-rule'),
48
47
  [RULES.VIRTUAL_PATCH]: require('./virtual-patch/virtual-patch-rule'),
49
- /* eslint-enable */
50
48
  };
51
49
 
52
50
  class ProtectRuleFactory {
53
- constructor({ featureSet = DEFAULT_SETTINGS, enabled }) {
51
+ constructor({ featureSet = DEFAULT_SETTINGS, enabled, agent }) {
54
52
  this.featureSet = featureSet;
55
53
  this.settings = {
56
54
  exceptions: []
57
55
  };
56
+ this.agent = agent;
58
57
  this.policies = {};
59
58
  this.signatures = new SignatureKit();
60
59
 
@@ -180,7 +179,7 @@ class ProtectRuleFactory {
180
179
  */
181
180
  IpDenylist() {
182
181
  const policy = this.policies[RULES.IP_DENYLIST];
183
- return new ctors[RULES.IP_DENYLIST](policy);
182
+ return new ctors[RULES.IP_DENYLIST](policy, this.agent);
184
183
  }
185
184
 
186
185
  /**
@@ -189,7 +188,7 @@ class ProtectRuleFactory {
189
188
  */
190
189
  BotBlocker() {
191
190
  const policy = this.policies[RULES.BOT_BLOCKER];
192
- return new ctors[RULES.BOT_BLOCKER](policy);
191
+ return new ctors[RULES.BOT_BLOCKER](policy, this.agent);
193
192
  }
194
193
 
195
194
  /**
@@ -198,7 +197,7 @@ class ProtectRuleFactory {
198
197
  */
199
198
  VirtualPatchRules() {
200
199
  return this.policies[RULES.VIRTUAL_PATCH].map(
201
- (policy) => new ctors[RULES.VIRTUAL_PATCH](policy)
200
+ (policy) => new ctors[RULES.VIRTUAL_PATCH](policy, this.agent)
202
201
  );
203
202
  }
204
203
 
@@ -224,7 +223,7 @@ class ProtectRuleFactory {
224
223
  policy.enable_rep = enableRep;
225
224
 
226
225
  if (ProtectRuleFactory.shouldBuildRule(policy)) {
227
- const rule = new ctors[id](policy);
226
+ const rule = new ctors[id](policy, this.agent);
228
227
  rule.signature = this.signatures.get(id);
229
228
  memo.push(rule);
230
229
  }
@@ -162,6 +162,9 @@ const createPatterns = (patternDefs) =>
162
162
  class Signature {
163
163
  constructor(definition) {
164
164
  this.name = definition.name;
165
+ // the following is undefined if the rule is not an input to the
166
+ // agent-lib score functions.
167
+ this.agentLibBit = definition.agentLibBit;
165
168
  this.keywordSearchers = normalizeScores(
166
169
  createKeywords(definition.keywordsList)
167
170
  );
@@ -15,7 +15,6 @@ Copyright: 2022 Contrast Security, Inc
15
15
  'use strict';
16
16
 
17
17
  const _ = require('lodash');
18
-
19
18
  const logger = require('../../../core/logger')('contrast:rules:protect');
20
19
  const { IMPORTANCE, INPUT_TYPES, SINK_TYPES } = require('../common');
21
20
  const isGenericComplicated = require('./generic-complicated');
@@ -38,8 +37,8 @@ const ScannerKit = new Map([
38
37
  ]);
39
38
 
40
39
  class SQLInjectionRule extends require('../') {
41
- constructor(policy) {
42
- super(policy);
40
+ constructor(policy, agent) {
41
+ super(policy, agent);
43
42
 
44
43
  this._scanners = new Map();
45
44
 
@@ -57,15 +56,29 @@ class SQLInjectionRule extends require('../') {
57
56
  INPUT_TYPES.QUERYSTRING,
58
57
  INPUT_TYPES.XML_VALUE,
59
58
  INPUT_TYPES.URI,
60
- INPUT_TYPES.URL_PARAMETER
59
+ INPUT_TYPES.URL_PARAMETER,
61
60
  ];
62
61
  this.applicableSinks = [SINK_TYPES.SQL_QUERY];
62
+
63
+ // if not using agentLib this constructor is done.
64
+ if (!agent.agentLib) {
65
+ return;
66
+ }
67
+
68
+ this.dbFlavors = {};
69
+ for (const flavor in agent.agentLib.DbType) {
70
+ this.dbFlavors[flavor.toLowerCase()] = this.agent.agentLib.DbType[flavor];
71
+ }
72
+ this.dbFlavorKeys = Object.keys(this.dbFlavors);
73
+
74
+ this.usesLibInputAnalysis = true;
63
75
  }
64
76
 
65
77
  /**
66
78
  * Evaluates the sink data by scanning the code string for injections
67
79
  * by collected sample inputs.
68
80
  * @param {SinkEvent} event Emitted by database drivers/wrappers/ORMs
81
+ * @param {Set(Sample)} applicableSamples Samples matching sql-injection criteria
69
82
  */
70
83
  evaluateAtSink({ event, applicableSamples }) {
71
84
  if (_.isEmpty(applicableSamples) || !event.data) {
@@ -88,9 +101,65 @@ class SQLInjectionRule extends require('../') {
88
101
  }
89
102
  }
90
103
 
104
+ /**
105
+ * Evaluates the sink data by scanning the code string for injections
106
+ * by collected sample inputs. Rather than using the node evaluators,
107
+ * this takes advantage of the agent-lib.
108
+ * @param {SinkEvent} event Emitted by database drivers/wrappers/ORMs
109
+ * @param {Sample[]} applicableSamples samples that apply to SQL injection.
110
+ */
111
+ // eslint-disable-next-line complexity
112
+ evaluateAtSinkForLib({ event, applicableSamples }) {
113
+ if (applicableSamples.size == 0 || !event.data) {
114
+ return;
115
+ }
116
+
117
+ const tag = this.dbFlavorKeys.find((name) => event.tags.has(name));
118
+ const dialect = this.dbFlavors[tag] || this.dbFlavors.mysql;
119
+
120
+ for (const sample of applicableSamples) {
121
+ let evalResult = null;
122
+ try {
123
+ const input = sample.input.value;
124
+ const sinkData = event.data;
125
+ const inputIndex = sinkData.indexOf(input);
126
+
127
+ if (inputIndex !== -1) {
128
+ evalResult = this.agent.agentLib.checkSqlInjectionSink(
129
+ inputIndex,
130
+ input.length,
131
+ dialect,
132
+ sinkData
133
+ );
134
+ if (evalResult) {
135
+ // capture the query for reporting purposes.
136
+ evalResult.query = sinkData;
137
+ } else if (inputIndex === 0 && input.length === sinkData.length) {
138
+ evalResult = {
139
+ startIndex: 0,
140
+ endIndex: input.length - 1,
141
+ overrunIndex: 0,
142
+ boundaryIndex: 0,
143
+ sinkData,
144
+ };
145
+ }
146
+ }
147
+ } catch (e) {
148
+ logger.info(`Failed to evaluate command-injection sink: ${e}`);
149
+ }
150
+
151
+ if (evalResult) {
152
+ this.appendAttackDetails(sample, evalResult);
153
+ sample.captureAppContext(event);
154
+ logger.warn(`EFFECTIVE - rule: ${this.id}, mode: ${this.mode}`);
155
+ this.blockRequest(sample);
156
+ }
157
+ }
158
+ }
159
+
91
160
  /**
92
161
  * In addition to using the traditional signature matching, this rule will
93
- * also check wether the input has other characteristics that warrant bumping
162
+ * also check whether the input has other characteristics that warrant bumping
94
163
  * an evaluation's importance from NONE -> WORTH_WATCHING.
95
164
  * @returns {}
96
165
  */
@@ -141,6 +210,30 @@ class SQLInjectionRule extends require('../') {
141
210
  };
142
211
  }
143
212
 
213
+ /**
214
+ * Builds details for Sql Injection Attack from agent-lib evaluation
215
+ * results.
216
+ * @param {Sample} sample The Sample for the attack
217
+ * @param {Object} findings The results of the sink analysis
218
+ * @returns {Object} The details
219
+ */
220
+ buildDetailsForLib(sample, finding) {
221
+ if (!finding) {
222
+ return null;
223
+ }
224
+
225
+ const { query } = finding;
226
+
227
+ return {
228
+ start: finding.startIndex,
229
+ end: finding.endIndex,
230
+ input: sample.input.toSerializable(),
231
+ boundaryOverrunIndex: finding.overrunIndex,
232
+ inputBoundaryIndex: finding.boundaryIndex,
233
+ query
234
+ };
235
+ }
236
+
144
237
  /**
145
238
  * Returns the appropriate scanner for the SQL dialect.
146
239
  * @param {String} id The id of the scanner
@@ -115,9 +115,6 @@
115
115
  ")" : "operator",
116
116
  "," : "operator",
117
117
  "*" : "operator",
118
- "=" : "operator",
119
- "^" : "operator",
120
- "!" : "operator",
121
118
 
122
119
  ";": "terminator"
123
120
  },
@@ -14,8 +14,6 @@ Copyright: 2022 Contrast Security, Inc
14
14
  */
15
15
  'use strict';
16
16
 
17
- const _ = require('lodash');
18
-
19
17
  const logger = require('../../../core/logger')('contrast:rules:protect');
20
18
  const { INPUT_TYPES, SINK_TYPES } = require('../common');
21
19
 
@@ -40,6 +38,8 @@ class ReflectedXssRule extends require('../') {
40
38
  INPUT_TYPES.URL_PARAMETER
41
39
  ];
42
40
  this.applicableSinks = [SINK_TYPES.RESPONSE_BODY];
41
+
42
+ this.usesLibInputAnalysis = true;
43
43
  }
44
44
  /**
45
45
  * Evaluates a SinkEvent contingent on <code>applicableSinks</code>.
@@ -48,7 +48,7 @@ class ReflectedXssRule extends require('../') {
48
48
  * @param {Set<Sample>} params.applicableSamples all (definite and ww) samples for xss
49
49
  */
50
50
  evaluateAtSink({ event, applicableSamples }) {
51
- if (!applicableSamples.size || !_.isString(event.data)) {
51
+ if (!applicableSamples.size) {
52
52
  return;
53
53
  }
54
54
 
@@ -20,7 +20,7 @@ Copyright: 2022 Contrast Security, Inc
20
20
 
21
21
  const _ = require('lodash');
22
22
 
23
- const POINTS_PATH = 'sample.assessment.results.points';
23
+ const { IMPORTANCE } = require('../constants');
24
24
 
25
25
  /**
26
26
  * Compares two findings and returns the one which is
@@ -38,31 +38,26 @@ const rankEffectiveness = (a, b) => {
38
38
  return null;
39
39
  }
40
40
 
41
- /* Otherwise, we want the one that is effective. */
42
- return _.find([a, b], (f) => f.sample.effective);
41
+ // effective is a boolean, so there is no "higher" effectiveness.
42
+ return a.sample.effective ? a : b;
43
43
  };
44
44
 
45
45
  /**
46
- * Compares two findings and returns the one which is
47
- * ranked higher in terms of assessment score. If their
48
- * assessment score is the same, <code>null</code> is
49
- * returned, signifying that a ranking order cannot be
50
- * determined for this criteria.
46
+ * Compares two findings and returns the one which has the highest
47
+ * score. If their scores are the same, return the first finding.
51
48
  * @param {object} a First finding
52
49
  * @param {object} b Second finding
53
- * @returns {object | null}
50
+ * @returns {object}
54
51
  */
55
- const rankPoints = (a, b) => {
56
- const p1 = _.get(a, POINTS_PATH);
57
- const p2 = _.get(b, POINTS_PATH);
52
+ const POINTS_PATH = 'sample.assessment.results.points';
58
53
 
59
- /* If both findings share point values, we can't rank them. */
60
- if (p1 === p2) {
61
- return null;
62
- }
54
+ function rankPoints(a, b) {
55
+ const pA = _.get(a, POINTS_PATH);
56
+ const pB = _.get(b, POINTS_PATH);
63
57
 
64
- return p1 < p2 ? b : a;
65
- };
58
+ // return a if a is greater than or equal to b
59
+ return pA < pB ? b : a;
60
+ }
66
61
 
67
62
  /**
68
63
  * Ranks two findings against effectiveness and score.
@@ -72,64 +67,77 @@ const rankPoints = (a, b) => {
72
67
  * @param {object} b Second finding
73
68
  * @returns {object} The highest-ranked finding
74
69
  */
75
- const rankFindings = (a, b) => rankEffectiveness(a, b) || rankPoints(a, b) || a;
70
+ const rankFindings = (a, b) => rankEffectiveness(a, b) || rankPoints(a, b);
76
71
 
77
72
  /**
78
- * Takes a collection of findings and organizes them into
79
- * a map having keys being unique input types and paths,
80
- * and values being unique findings pertaining to each key.
73
+ * Takes a collection of findings and organizes them into a map having keys
74
+ * being unique input types and paths, and values being unique findings pertaining
75
+ * to each key.
81
76
  * @param {object} memo The map being created
82
77
  * @param {object} finding Current finding in iteratee
83
78
  * @returns {object} Map of ranked findings per type/path
84
79
  */
85
- const rankIntoMap = (memo, finding) => {
80
+ function rankIntoMap(memo, finding) {
86
81
  const type = _.get(finding, 'sample.input.type', '_');
87
82
  const path = _.get(finding, 'sample.input.documentPath', '_');
88
83
  const name = _.get(finding, 'sample.input.name', '_');
89
84
 
90
- /* This is what defines uniqueness in reporting */
85
+ // this string is the key to the map
91
86
  const memoPath = `${type}.${path}.${name}`;
92
- const existing = memo[memoPath];
93
87
 
94
- memo[memoPath] = !existing ? finding : rankFindings(existing, finding);
88
+ const existing = memo[memoPath];
89
+ // if it exists, rank it against the new finding
90
+ memo[memoPath] = existing ? rankFindings(existing, finding) : finding;
95
91
 
92
+ // memo returned despite having side effects because this function is
93
+ // called using reduce.
96
94
  return memo;
97
- };
95
+ }
98
96
 
99
97
  /**
100
- * Functions for sample aggregation and ranking.
101
- */
102
- /**
103
- * From a collection of findings, will return a filtered
104
- * collection without low-scoring samples. Low-scoring
105
- * samples are not confirmed attacks. BUT, we DO want to
106
- * report on findings if they were effective, always.
107
- * @param {object[]} findings
108
- * @returns {object[]}
109
- */
110
- const filterLowScoring = (findings = []) =>
111
- _.filter(
112
- findings,
113
- (finding) =>
114
- // Virtual Patch findings do not have inputs but always create a WW sample.
115
- // Make sure they were effective.
116
- (!finding.sample.input && finding.sample.effective) ||
117
- /* Don't include low-scoring ineffective attacks. */
118
- !(!finding.sample.effective && !finding.sample.confirmedAttack)
119
- );
120
-
121
- /**
122
- * Accepts a collection of findings and returns a new
123
- * collection whose samples are unique with respect to
124
- * input type and path of the finding per input type. Any
125
- * duplicate findings for the same type and path will be
126
- * filtered to only include the one with the highest
127
- * points/effectiveness ranking.
98
+ * Accepts a collection of findings and returns a new collection whose samples
99
+ * are unique with respect to input type and path of the finding per input type.
100
+ * Any duplicate findings for the same type and path will be filtered to only
101
+ * include the one with the highest points/effectiveness ranking.
128
102
  * @param {object[]} findings The findings to aggregate.
103
+ * @param {function} wwFilter filter to check if a sample should be saved.
129
104
  * @returns {object[]}
105
+ *
106
+ * effective, but not blocked, becomes PROBED.
107
+ *
130
108
  */
131
- const aggregate = (findings = []) =>
132
- _.values(_.reduce(filterLowScoring(findings), rankIntoMap, {}));
109
+ function aggregate(findings, wwFilter) {
110
+ // separate the findings into effective and ineffective
111
+ // but marked as worth-watching by agent-lib. if not in
112
+ // one of the two lists, the finding is not relevant.
113
+ const effective = [];
114
+ const wwFindings = [];
115
+ // and the rest were not agent-lib worth-watching or effective
116
+ for (const finding of findings) {
117
+ const { sample } = finding;
118
+ if (sample.confirmedAttack || sample.effective) {
119
+ effective.push(finding);
120
+ } else if (finding.rule.agentLibBit && sample.assessment.results.importance === IMPORTANCE.WORTH_WATCHING) {
121
+ //console.log(finding.rule.id, s.results)
122
+ wwFindings.push(finding);
123
+ }
124
+ }
125
+
126
+ // do we need to run any inputs through the full scoring (not the abbreviated
127
+ // worth-watching scoring)?
128
+ if (wwFindings.length) {
129
+ for (const finding of wwFindings) {
130
+ if (wwFilter(finding)) {
131
+ finding.sample.assessment.results.importance = IMPORTANCE.DEFINITE;
132
+ // put this in the "effective list", even though it wasn't effective, so it
133
+ // will be reported as PROBED.
134
+ effective.push(finding);
135
+ }
136
+ }
137
+ }
138
+
139
+ return _.values(effective.reduce(rankIntoMap, {}));
140
+ }
133
141
 
134
142
  module.exports = {
135
143
  aggregate