@contrast/agent 4.12.2 → 4.13.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/esm.mjs +1 -32
  2. package/lib/assess/sinks/dynamo.js +65 -30
  3. package/lib/assess/static/read-findings-from-cache.js +40 -0
  4. package/lib/assess/technologies/index.js +12 -13
  5. package/lib/cli-rewriter/index.js +65 -6
  6. package/lib/core/config/options.js +6 -0
  7. package/lib/core/config/util.js +15 -33
  8. package/lib/core/exclusions/input.js +6 -1
  9. package/lib/core/express/index.js +2 -4
  10. package/lib/hooks/http.js +81 -81
  11. package/lib/hooks/require.js +1 -0
  12. package/lib/instrumentation.js +17 -0
  13. package/lib/protect/errors/handler-async-errors.js +66 -0
  14. package/lib/protect/input-analysis.js +7 -13
  15. package/lib/protect/listeners.js +27 -23
  16. package/lib/protect/rules/base-scanner/index.js +2 -2
  17. package/lib/protect/rules/bot-blocker/bot-blocker-rule.js +4 -2
  18. package/lib/protect/rules/cmd-injection/cmdinjection-rule.js +57 -2
  19. package/lib/protect/rules/cmd-injection-semantic-chained-commands/cmd-injection-semantic-chained-commands-rule.js +31 -2
  20. package/lib/protect/rules/cmd-injection-semantic-dangerous-paths/cmd-injection-semantic-dangerous-paths-rule.js +32 -2
  21. package/lib/protect/rules/index.js +42 -21
  22. package/lib/protect/rules/ip-denylist/ip-denylist-rule.js +2 -2
  23. package/lib/protect/rules/nosqli/nosql-injection-rule.js +103 -38
  24. package/lib/protect/rules/path-traversal/path-traversal-rule.js +3 -0
  25. package/lib/protect/rules/rule-factory.js +6 -6
  26. package/lib/protect/rules/signatures/signature.js +3 -0
  27. package/lib/protect/rules/sqli/sql-injection-rule.js +98 -5
  28. package/lib/protect/rules/sqli/sql-scanner/labels.json +0 -3
  29. package/lib/protect/rules/xss/reflected-xss-rule.js +3 -3
  30. package/lib/protect/sample-aggregator.js +65 -57
  31. package/lib/protect/service.js +709 -104
  32. package/lib/reporter/models/app-activity/sample.js +6 -0
  33. package/lib/reporter/ts-reporter.js +1 -1
  34. package/lib/util/get-file-type.js +47 -0
  35. package/package.json +5 -3
package/lib/hooks/http.js CHANGED
@@ -75,93 +75,93 @@ const hookServer = (server, agent, id) => {
75
75
  const self = this;
76
76
  const [event, req, res] = args;
77
77
 
78
- if (event === 'request') {
79
- logger.debug('Received request %s, method %s', req.url, req.method);
80
- return scopes.runInRequestScope({
81
- agent,
82
- req,
83
- res,
84
- hookResponse(response, agent) {
85
- const { writeHead, end } = response;
86
- // XXX(ehden): we keep the original method available to call when handling
87
- // blocked requests because of MethodTampering or other rules whose sink
88
- // type is RESPONSE_STATUS and which can block at perimeter. We call the original
89
- // when we block to prevent recursing through blocks by setting our own 403.
90
- response[WRITE_HEAD] = writeHead;
91
- response[END] = end;
92
-
93
- /**
94
- * Patch writeHead to emit a sink event before calling writeHead
95
- *
96
- */
97
- patcher.patch(response, 'writeHead', {
98
- alwaysRun: true,
99
- name: 'write-head',
100
- patchType: PATCH_TYPES.PROTECT_SINK,
101
- pre(data) {
102
- const [statusCode, reason] = data.args;
103
- let [, , obj] = data.args;
104
- let contentType;
105
- if (typeof reason !== 'string') {
106
- obj = reason;
107
- }
108
- if (obj && typeof obj === 'object') {
109
- for (const [key, value] of Object.entries(obj)) {
110
- if (key.toLowerCase() === 'content-type') {
111
- contentType = value;
112
- }
78
+ if (event !== 'request') {
79
+ return emit.apply(self, args);
80
+ }
81
+
82
+ logger.debug('Received request %s, method %s', req.url, req.method);
83
+ return scopes.runInRequestScope({
84
+ agent,
85
+ req,
86
+ res,
87
+ hookResponse(response, agent) {
88
+ const { writeHead, end } = response;
89
+ // XXX(ehden): we keep the original method available to call when handling
90
+ // blocked requests because of MethodTampering or other rules whose sink
91
+ // type is RESPONSE_STATUS and which can block at perimeter. We call the original
92
+ // when we block to prevent recursing through blocks by setting our own 403.
93
+ response[WRITE_HEAD] = writeHead;
94
+ response[END] = end;
95
+
96
+ /**
97
+ * Patch writeHead to emit a sink event before calling writeHead
98
+ *
99
+ */
100
+ patcher.patch(response, 'writeHead', {
101
+ alwaysRun: true,
102
+ name: 'write-head',
103
+ patchType: PATCH_TYPES.PROTECT_SINK,
104
+ pre(data) {
105
+ const [statusCode, reason] = data.args;
106
+ let [, , obj] = data.args;
107
+ let contentType;
108
+ if (typeof reason !== 'string') {
109
+ obj = reason;
110
+ }
111
+ if (obj && typeof obj === 'object') {
112
+ for (const [key, value] of Object.entries(obj)) {
113
+ if (key.toLowerCase() === 'content-type') {
114
+ contentType = value;
113
115
  }
114
116
  }
115
- if (contentType) {
116
- AsyncStorage.set(KEYS.RESPONSE_CONTENT_TYPE, contentType);
117
- }
118
- emitSinkEvent(statusCode, SINK_TYPES.RESPONSE_STATUS, id, false);
119
117
  }
120
- });
121
-
122
- patcher.patch(response, 'setHeader', {
123
- alwaysRun: true,
124
- name: 'set-header',
125
- patchType: PATCH_TYPES.ASYNC_CONTEXT,
126
- pre(data) {
127
- const [name = '', value] = data.args;
128
- if (name.toLowerCase() === 'content-type' && value) {
129
- AsyncStorage.set(KEYS.RESPONSE_CONTENT_TYPE, value);
130
- }
118
+ if (contentType) {
119
+ AsyncStorage.set(KEYS.RESPONSE_CONTENT_TYPE, contentType);
120
+ }
121
+ emitSinkEvent(statusCode, SINK_TYPES.RESPONSE_STATUS, id, false);
122
+ }
123
+ });
124
+
125
+ patcher.patch(response, 'setHeader', {
126
+ alwaysRun: true,
127
+ name: 'set-header',
128
+ patchType: PATCH_TYPES.ASYNC_CONTEXT,
129
+ pre(data) {
130
+ const [name = '', value] = data.args;
131
+ if (name.toLowerCase() === 'content-type' && value) {
132
+ AsyncStorage.set(KEYS.RESPONSE_CONTENT_TYPE, value);
131
133
  }
132
- });
133
-
134
- // special HTTP logging for responses.
135
- if (agent.config.agent.logger.log_outbound_http) {
136
- ['end', 'writeHead', 'write'].forEach((method) => {
137
- logHook(response, method);
138
- });
139
134
  }
140
- },
141
- callback() {
142
- const ipEvent = createSourceEvent(
143
- req,
144
- req,
145
- res,
146
- 'socket.remoteAddress',
147
- INPUT_TYPES.IP,
148
- id
149
- );
150
- agentEmitter.emit('http.requestStart', req, res, ipEvent);
151
-
152
- emitSourceEvent(req, req, res, 'method', INPUT_TYPES.METHOD, id);
153
- emitSourceEvent(req, req, res, 'headers', INPUT_TYPES.HEADER, id);
154
- emitSourceEvent(req, req, res, 'url', INPUT_TYPES.URL, id);
155
-
156
- const rv = emit.apply(self, args);
157
-
158
- agentEmitter.emit('http.requestEnd', req, res);
159
- return rv;
135
+ });
136
+
137
+ // special HTTP logging for responses.
138
+ if (agent.config.agent.logger.log_outbound_http) {
139
+ ['end', 'writeHead', 'write'].forEach((method) => {
140
+ logHook(response, method);
141
+ });
160
142
  }
161
- });
162
- } else {
163
- return emit.apply(self, args);
164
- }
143
+ },
144
+ callback() {
145
+ const ipEvent = createSourceEvent(
146
+ req,
147
+ req,
148
+ res,
149
+ 'socket.remoteAddress',
150
+ INPUT_TYPES.IP,
151
+ id
152
+ );
153
+ agentEmitter.emit('http.requestStart', req, res, ipEvent);
154
+
155
+ emitSourceEvent(req, req, res, 'method', INPUT_TYPES.METHOD, id);
156
+ emitSourceEvent(req, req, res, 'headers', INPUT_TYPES.HEADER, id);
157
+ emitSourceEvent(req, req, res, 'url', INPUT_TYPES.URL, id);
158
+
159
+ const rv = emit.apply(self, args);
160
+
161
+ agentEmitter.emit('http.requestEnd', req, res);
162
+ return rv;
163
+ }
164
+ });
165
165
  };
166
166
  };
167
167
 
@@ -22,6 +22,7 @@ const logger = require('../core/logger')('hooks:require');
22
22
  class ModuleHook {
23
23
  constructor() {
24
24
  this.reqHook = new RequireHook(logger);
25
+ this.reqHook.install();
25
26
 
26
27
  // RequireHook takes care of hooking but we still need to patch require to
27
28
  // emit 'require' events
@@ -102,6 +102,7 @@ function assessModeFeatures(agent) {
102
102
  if (agent.isInAssessMode()) {
103
103
  logger.debug('initializing assess mode features');
104
104
  require('./assess/policy/init').init(agent);
105
+ require('./assess/static/read-findings-from-cache')(agent);
105
106
  require('./hooks/encoding')();
106
107
  require('./hooks/object-to-primitive')();
107
108
  require('./hooks/array')();
@@ -121,6 +122,22 @@ function protectModeFeatures({ agent, reporter }) {
121
122
  logger.debug('initializing protect mode features');
122
123
 
123
124
  require('./protect')();
125
+
126
+ // if it's native_input_analysis then use agent-lib
127
+ if (agent.config.agent.node.native_input_analysis) {
128
+ const lib = require('@contrast/agent-lib');
129
+ // needs the || '.' for testing...
130
+ const logDir = agent.config.agent.node.analysis_log_dir || '.';
131
+ const agentLib = new lib.Agent(
132
+ { enableLogging: true, logDir, logLevel: "INFO" }
133
+ );
134
+ // attach the constants so lib.Agent() isn't exposed.
135
+ for (const c in lib.constants) {
136
+ agentLib[c] = lib.constants[c];
137
+ }
138
+ agent.agentLib = agentLib;
139
+ }
140
+
124
141
  const protectService = require('./protect/listeners')(agent, reporter);
125
142
  const InputAnalysisSensor = require('./protect/input-analysis');
126
143
  require('./protect/sources')(agent);
@@ -0,0 +1,66 @@
1
+ /**
2
+ Copyright: 2022 Contrast Security, Inc
3
+ Contact: support@contrastsecurity.com
4
+ License: Commercial
5
+
6
+ NOTICE: This Software and the patented inventions embodied within may only be
7
+ used as part of Contrast Security’s commercial offerings. Even though it is
8
+ made available through public repositories, use of this Software is subject to
9
+ the applicable End User Licensing Agreement found at
10
+ https://www.contrastsecurity.com/enduser-terms-0317a or as otherwise agreed
11
+ between Contrast Security and the End User. The Software may not be reverse
12
+ engineered, modified, repackaged, sold, redistributed or otherwise used in a
13
+ way not consistent with the End User License Agreement.
14
+ */
15
+ 'use strict';
16
+
17
+ const { KEYS, AsyncStorage } = require('../../core/async-storage');
18
+ const agentEmitter = require('../../agent-emitter');
19
+ const isContrastError = require('../../util/is-contrast-error');
20
+ const process = require('process');
21
+
22
+ const handledErrors = new WeakSet();
23
+
24
+ module.exports.install = function() {
25
+ const originalOn = process.on;
26
+ process.on = function (name, ...args) {
27
+ if (name === 'unhandledRejection') {
28
+ return originalOn.call(
29
+ this,
30
+ name,
31
+ ...args.map(
32
+ (origHandler) =>
33
+ function (reason, promise) {
34
+ if (handledErrors.has(reason)) {
35
+ return; // skip
36
+ }
37
+ return origHandler(reason, promise);
38
+ },
39
+ ),
40
+ );
41
+ } else {
42
+ return originalOn.call(this, name, ...args);
43
+ }
44
+ };
45
+
46
+ process.on('unhandledRejection', function asyncErrorHandling(error) {
47
+ handlerAsyncErrors(error);
48
+ });
49
+
50
+ function handlerAsyncErrors(error) {
51
+ let handled = null;
52
+ if (isContrastError(error)) {
53
+ const res = AsyncStorage.fromException(error, KEYS.RES);
54
+ handled = agentEmitter.handleError(error, res);
55
+ }
56
+ if (handled) {
57
+ handledErrors.add(error);
58
+ } else {
59
+ console.warn(
60
+ `An Unhandled Rejection has been caught by the Contrast Security node-agent instrumentation. Error: ${error}`,
61
+ );
62
+ }
63
+ }
64
+ };
65
+
66
+
@@ -35,10 +35,7 @@ class InputAnalysisSensor {
35
35
  * for instrumenting both `http` and `https` modules when they load.
36
36
  */
37
37
  install() {
38
- if (
39
- this.installed ||
40
- !this.agent.config.agent.node.speedracer_input_analysis
41
- ) {
38
+ if (this.installed) {
42
39
  return;
43
40
  }
44
41
  this.installed = true;
@@ -140,7 +137,7 @@ class InputAnalysisSensor {
140
137
 
141
138
  let permit = true;
142
139
  try {
143
- const appContext = InputAnalysisSensor.getApplicationContext(method);
140
+ const appContext = InputAnalysisSensor.makeApplicationContext(method);
144
141
  permit = await this.service.analyzeRequest({
145
142
  meta,
146
143
  req,
@@ -249,10 +246,6 @@ class InputAnalysisSensor {
249
246
  return sizeInMb >= this.maxSize;
250
247
  }
251
248
 
252
- static isDone(args) {
253
- return args[0] === 'end';
254
- }
255
-
256
249
  /**
257
250
  * We defer calling the original req.emit of data chunks
258
251
  * and end until SR analyzes the request body.
@@ -266,7 +259,8 @@ class InputAnalysisSensor {
266
259
  */
267
260
  async processBodyAndEmit(meta, context, req, res) {
268
261
  const { args, method } = context;
269
- const done = InputAnalysisSensor.isDone(args);
262
+ // the request is done when the end event is emitted
263
+ const done = args[0] === 'end';
270
264
 
271
265
  let permit = true;
272
266
 
@@ -274,7 +268,7 @@ class InputAnalysisSensor {
274
268
  let appContext;
275
269
 
276
270
  if (done) {
277
- appContext = InputAnalysisSensor.getApplicationContext(method);
271
+ appContext = InputAnalysisSensor.makeApplicationContext(method);
278
272
 
279
273
  permit = await this.service.analyzeRequestStream({
280
274
  meta,
@@ -352,10 +346,10 @@ class InputAnalysisSensor {
352
346
  }
353
347
 
354
348
  /**
355
- * Gets the app context and sets the stack with
349
+ * Makes a new app context and sets the stack with
356
350
  * the proper request handler
357
351
  */
358
- static getApplicationContext(handle) {
352
+ static makeApplicationContext(handle) {
359
353
  const appContext = new ApplicationContext();
360
354
  appContext.setStack(handle);
361
355
  return appContext;
@@ -21,6 +21,7 @@ Copyright: 2022 Contrast Security, Inc
21
21
  */
22
22
 
23
23
  const _ = require('lodash');
24
+
24
25
  const Stream = require('stream');
25
26
  const parseurl = require('parseurl');
26
27
  const agentEmitter = require('../agent-emitter');
@@ -28,25 +29,28 @@ const ApplicationContext = require('./models/application-context');
28
29
  const Request = require('../reporter/models/request');
29
30
  const {
30
31
  AsyncStorage,
31
- KEYS: { SAMPLES, RULES, INPUT_EXCLUSIONS, RES, REQ, REQUEST }
32
+ KEYS: { SAMPLES, RULES, INPUT_EXCLUSIONS, RES, REQ, REQUEST },
32
33
  } = require('../core/async-storage');
33
34
  const Samples = require('./samples.js');
34
35
  const ProtectService = require('./service');
35
36
  const { INPUT_TYPES } = require('../constants');
37
+ const handlerAsyncErrors = require('./errors/handler-async-errors');
36
38
 
37
39
  module.exports = function protectEventListener(agent, reporter) {
38
40
  const service = new ProtectService(agent, reporter);
39
41
 
42
+ handlerAsyncErrors.install();
43
+
40
44
  agentEmitter.on('http.requestStart', (req, res, ipEvent) => {
41
- // We're expected to apply exclusions based only on the url path name,
42
- // not on the full path (which may include query string)
45
+ // apply exclusions based only on the url path name, not on the full
46
+ // path (which may include a query string).
43
47
  const { pathname: urlPath } = parseurl(req);
44
48
 
45
49
  const rules = service.getEnabledRules(urlPath, ipEvent);
46
50
  AsyncStorage.set(RULES, rules);
47
51
  AsyncStorage.set(
48
52
  INPUT_EXCLUSIONS,
49
- service.getEnabledInputExclusions(urlPath)
53
+ service.getEnabledInputExclusions(urlPath),
50
54
  );
51
55
  AsyncStorage.set(SAMPLES, new Samples());
52
56
  });
@@ -65,16 +69,20 @@ module.exports = function protectEventListener(agent, reporter) {
65
69
  }
66
70
 
67
71
  const ctxt = AsyncStorage.getContext();
68
- const rules = sourceRuleFilter(_.get(ctxt, RULES));
69
- const inputExclusions = _.get(ctxt, INPUT_EXCLUSIONS);
70
- const samples = _.get(ctxt, SAMPLES);
71
-
72
- enrichEvent(event, ctxt);
73
- if (agent.config.agent.node.speedracer_input_analysis) {
72
+ if (ctxt.defend) {
73
+ const { exclusions: inputExclusions, samples } = ctxt.defend;
74
+ const rules = sourceRuleFilter(ctxt.defend.rules);
75
+ enrichEvent(event, ctxt);
74
76
  enrichSamples(event, samples);
75
- }
76
77
 
77
- service.handleSourceEvent(event, rules, inputExclusions, samples);
78
+ if (!rules || rules.length === 0) {
79
+ return;
80
+ }
81
+ // it's ugly because this probably should have been here all along as opposed
82
+ // to "enriching" the event with data from ctxt.
83
+ event._ctxt = ctxt;
84
+ service.handleSourceEvent(event, rules, inputExclusions, samples);
85
+ }
78
86
  });
79
87
 
80
88
  agentEmitter.on('protect.sink', (event) => {
@@ -100,13 +108,13 @@ module.exports = function protectEventListener(agent, reporter) {
100
108
  * @param {Object} event the SourceEvent instance
101
109
  * @param {Set} storage async protect storage
102
110
  */
103
- function enrichEvent(event, storageCtxt = AsyncStorage.getContext()) {
111
+ function enrichEvent(event, storageCtxt) {
104
112
  if (storageCtxt) {
105
113
  [
106
114
  { key: '_incomingMessage', ctxtKey: REQ },
107
115
  { key: '_serverResponse', ctxtKey: RES },
108
116
  { key: 'request', ctxtKey: REQUEST },
109
- { key: 'response', ctxtKey: RES }
117
+ { key: 'response', ctxtKey: RES },
110
118
  ].forEach(({ key, ctxtKey }) => {
111
119
  if (!event[key]) {
112
120
  setKey({ storageCtxt, ctxtKey, event, key });
@@ -130,11 +138,8 @@ function enrichEvent(event, storageCtxt = AsyncStorage.getContext()) {
130
138
  * @param {SourceEvent} event
131
139
  * @param {StorageContext} ctxt
132
140
  */
133
- function enrichSamples(event, samples) {
134
- if (event.type !== INPUT_TYPES.URL_PARAMETER) {
135
- return;
136
- }
137
141
 
142
+ function enrichSamples(event, samples) {
138
143
  for (const key of Object.keys(event.data)) {
139
144
  for (const sample of samples.getAllByType(INPUT_TYPES.URL_PARAMETER)) {
140
145
  const decoded = decodeURIComponent(sample.input.name);
@@ -189,14 +194,13 @@ function isStream(data) {
189
194
  }
190
195
 
191
196
  /**
192
- * This filters out which rules should be involved in the handling of source
193
- * events. Under some conditions, like when we are using Speedracer for input
194
- * analysis, we only want a subset of active rules to respond to these events.
197
+ * return a function that filters out rules that speedracer or agent-lib
198
+ * handles.
195
199
  * @param {Object} config The agent configuration
196
- * @returns {Rule[]}
200
+ * @returns {Function} function that returns either filtered or unfiltered rules
197
201
  */
198
202
  function getSourceRuleFilter(config) {
199
- return config.agent.node.speedracer_input_analysis
203
+ return config.agent.node.speedracer_input_analysis && !config.agent.node.native_input_analysis
200
204
  ? (rules) => rules.filter((rule) => rule.inputClassification === false)
201
205
  : (rules) => rules;
202
206
  }
@@ -67,8 +67,8 @@ class BaseScanner {
67
67
  * 1. Parsing the query into a token sequence.
68
68
  * 2. Finding all stop/start indices of the input string within the query.
69
69
  * 3. Comparing these to the indices of the tokens in the sequence.
70
- * @param {String} substring A single input string.
71
- * @param {String} query The query to analize.
70
+ * @param {String} substring An input string.
71
+ * @param {String} query The query to analyze.
72
72
  * @returns {Object[]}
73
73
  */
74
74
  findInjection(substring, query) {
@@ -23,13 +23,15 @@ const { INPUT_TYPES, IMPORTANCE, PROTECTION_MODES } = require('../common');
23
23
  const USER_AGENT = 'user-agent';
24
24
 
25
25
  class BotBlockerRule extends Rule {
26
- constructor(policy = {}) {
27
- super(policy);
26
+ constructor(policy = {}, agent) {
27
+ super(policy, agent);
28
28
  this.id = 'bot-blocker';
29
29
  this.name = 'Bot Blocker';
30
30
  this.blockAtEntry = true;
31
31
  this.mode = PROTECTION_MODES.BLOCK_AT_PERIMETER;
32
32
  this.applicableInputs = [INPUT_TYPES.HEADER];
33
+
34
+ this.usesLibInputAnalysis = true;
33
35
  }
34
36
 
35
37
  /**
@@ -20,10 +20,11 @@ Copyright: 2022 Contrast Security, Inc
20
20
 
21
21
  const Rule = require('../');
22
22
  const { INPUT_TYPES, SINK_TYPES } = require('../common');
23
+ const logger = require('../../../core/logger')('contrast:rules:protect');
23
24
 
24
25
  class CMDInjectionRule extends Rule {
25
- constructor(policy) {
26
- super(policy);
26
+ constructor(policy, agent) {
27
+ super(policy, agent);
27
28
 
28
29
  this.id = 'cmd-injection';
29
30
  this.name = 'Command Injection';
@@ -42,6 +43,45 @@ class CMDInjectionRule extends Rule {
42
43
  INPUT_TYPES.URL_PARAMETER
43
44
  ];
44
45
  this.applicableSinks = [SINK_TYPES.COMMAND];
46
+
47
+ this.usesLibInputAnalysis = true;
48
+ }
49
+
50
+ /**
51
+ * Calls down to the agent analysis library for evaluation with the
52
+ * @param {Samples} applicableSamples Samples cache
53
+ * @param {Set} params.applicableSamples samples applicable to rule id
54
+ */
55
+ evaluateAtSinkForLib({ event, applicableSamples }) {
56
+ if (applicableSamples.size == 0 || !event.data) {
57
+ return;
58
+ }
59
+
60
+ for (const sample of applicableSamples) {
61
+ let evalResult = null;
62
+ try {
63
+ const input = sample.input.value;
64
+ const sinkData = event.data;
65
+ const inputIndex = sinkData.indexOf(input);
66
+
67
+ if (inputIndex !== -1) {
68
+ evalResult = this.agent.agentLib.checkCommandInjectionSink(
69
+ inputIndex,
70
+ input.length,
71
+ input
72
+ );
73
+ }
74
+ } catch (e) {
75
+ logger.info(`Failed to evaluate command-injection sink: ${e}`);
76
+ }
77
+
78
+ if (evalResult) {
79
+ this.appendAttackDetails(sample, evalResult);
80
+ sample.captureAppContext(event);
81
+ logger.warn(`EFFECTIVE - rule: ${this.id}, mode: ${this.mode}`);
82
+ this.blockRequest(sample);
83
+ }
84
+ }
45
85
  }
46
86
 
47
87
  /**
@@ -53,6 +93,21 @@ class CMDInjectionRule extends Rule {
53
93
  buildDetails(sample, findings) {
54
94
  return { command: findings };
55
95
  }
96
+
97
+ /**
98
+ * Builds the details for TS UI rendering based on agent lib findings.
99
+ * @param {Sample} sample relevant protect sample
100
+ * @param {Object} findings The results from the agent library
101
+ * @returns {Object}
102
+ */
103
+ buildDetailsForLib(sample, findings) {
104
+ return {
105
+ command: sample.input.value.substring(
106
+ findings.startIndex,
107
+ findings.endIndex
108
+ )
109
+ };
110
+ }
56
111
  }
57
112
 
58
113
  module.exports = CMDInjectionRule;
@@ -24,8 +24,8 @@ const UserInputFactory = require('../../../reporter/models/utils/user-input-fact
24
24
  const ChainedCommandScanner = require('./chained-command-scanner');
25
25
 
26
26
  class CMDInjectionSemanticChainedCommandsRule extends Rule {
27
- constructor(policy) {
28
- super(policy);
27
+ constructor(policy, agent) {
28
+ super(policy, agent);
29
29
  this.id = 'cmd-injection-semantic-chained-commands';
30
30
  this.name = 'Command Injection Chained Commands';
31
31
  this.applicableSinks = [SINK_TYPES.COMMAND];
@@ -72,6 +72,35 @@ class CMDInjectionSemanticChainedCommandsRule extends Rule {
72
72
  }
73
73
  }
74
74
 
75
+ /**
76
+ * Performs semantic analysis to determine if there are chained subcommands
77
+ * using agent-lib
78
+ * @param {ApplicationContext} event Sink event
79
+ * @param {Request} request Request model
80
+ * @param {Samples} samples Samples cache
81
+ */
82
+ evaluateAtSinkForLib({ event, request, samples }) {
83
+ const { data: command } = event;
84
+ const index = this.agent.agentLib.indexOfChaining(command);
85
+ if (index != -1) {
86
+ const sample = this.createAndSaveSample({
87
+ input: UserInputFactory.makeOne({ value: command }),
88
+ attributes: { effective: true },
89
+ classification: IMPORTANCE.DEFINITE,
90
+ event,
91
+ samples,
92
+ request
93
+ });
94
+
95
+ this.appendAttackDetails(sample, {
96
+ command,
97
+ findings: [CMD_INJECTION_SEMANTIC_TYPES.CHAINING]
98
+ });
99
+
100
+ this.blockRequest(sample);
101
+ }
102
+ }
103
+
75
104
  /**
76
105
  * Builds the details for TS UI rendering.
77
106
  * @param {Sample} sample N/a in this rule's case
@@ -24,8 +24,8 @@ const UserInputFactory = require('../../../reporter/models/utils/user-input-fact
24
24
  const DangerousPathsScanner = require('./dangerous-paths-scanner');
25
25
 
26
26
  class CMDInjectionSemanticDangerousPathsRule extends Rule {
27
- constructor(policy) {
28
- super(policy);
27
+ constructor(policy, agent) {
28
+ super(policy, agent);
29
29
  this.id = 'cmd-injection-semantic-dangerous-paths';
30
30
  this.name = 'Command Injection Dangerous Paths';
31
31
  this.applicableSinks = [SINK_TYPES.COMMAND];
@@ -66,6 +66,36 @@ class CMDInjectionSemanticDangerousPathsRule extends Rule {
66
66
  }
67
67
  }
68
68
 
69
+ /**
70
+ * Check for dangerous paths in the input command using the agent library.
71
+ * @param {ApplicationContext} event Sink event
72
+ * @param {Request} request Request model
73
+ * @param {Samples} samples Samples cache
74
+ */
75
+ evaluateAtSinkForLib({ event, request, samples }) {
76
+ const { data: command } = event;
77
+ const containsDangerousPath = this.agent.agentLib.containsDangerousPath(
78
+ command
79
+ );
80
+ if (containsDangerousPath) {
81
+ const sample = this.createAndSaveSample({
82
+ input: UserInputFactory.makeOne({ value: command }),
83
+ attributes: { effective: true },
84
+ classification: IMPORTANCE.DEFINITE,
85
+ event,
86
+ samples,
87
+ request
88
+ });
89
+
90
+ this.appendAttackDetails(sample, {
91
+ command,
92
+ findings: [CMD_INJECTION_SEMANTIC_TYPES.PATH_ARGUMENT]
93
+ });
94
+
95
+ this.blockRequest(sample);
96
+ }
97
+ }
98
+
69
99
  /**
70
100
  * Builds the details for TS UI rendering.
71
101
  * @param {Sample} sample N/a in this rule's case