@contrast/assess 1.48.0 → 1.50.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.
@@ -21,7 +21,6 @@ module.exports = function (core) {
21
21
  const {
22
22
  depHooks,
23
23
  patcher,
24
- assess: { getPropagatorContext }
25
24
  } = core;
26
25
 
27
26
  return core.assess.dataflow.propagation.fastifySend = {
@@ -33,9 +32,10 @@ module.exports = function (core) {
33
32
  usePerf: 'sync',
34
33
  pre(data) {
35
34
  const { args } = data;
36
-
37
- if (!getPropagatorContext()) return;
38
-
35
+ // (†) This is a minimal propagator that just untracks the argument. There are
36
+ // no propagation events generated and no expensive tag range computations. We
37
+ // don't get the propagator context before proceeding since it could lead to false
38
+ // positives e.g. if the number of propagation events exceeds the configured limit.
39
39
  const untrackedPath = StringPrototypeSlice.call(` ${args[0]}`, 1);
40
40
  args[0] = untrackedPath;
41
41
  },
@@ -49,9 +49,7 @@ module.exports = function (core) {
49
49
  usePerf: 'sync',
50
50
  pre(data) {
51
51
  const { args } = data;
52
-
53
- if (!getPropagatorContext()) return;
54
-
52
+ // (†)
55
53
  const untrackedPath = StringPrototypeSlice.call(` ${args[1]}`, 1);
56
54
  args[1] = untrackedPath;
57
55
  },
@@ -21,7 +21,6 @@ module.exports = function (core) {
21
21
  const {
22
22
  depHooks,
23
23
  patcher,
24
- assess: { getPropagatorContext }
25
24
  } = core;
26
25
 
27
26
  const send = {};
@@ -38,8 +37,10 @@ module.exports = function (core) {
38
37
  patchType,
39
38
  pre(data) {
40
39
  const { args } = data;
41
- if (!getPropagatorContext()) return;
42
-
40
+ // This is a minimal propagator that just untracks the argument. There are
41
+ // no propagation events generated and no expensive tag range computations. We
42
+ // don't get the propagator context before proceeding since it could lead to false
43
+ // positives e.g. if the number of propagation events exceeds the configured limit.
43
44
  const untrackedPath = StringPrototypeSlice.call(` ${args[0]}`, 1);
44
45
  args[0] = untrackedPath;
45
46
  },
@@ -32,30 +32,48 @@ module.exports = function(core) {
32
32
  return core.assess.dataflow.propagation.stringInstrumentation.concat = {
33
33
  install() {
34
34
  const name = 'String.prototype.concat';
35
+ const store = { name: `${name}:${patchType}`, lock: true };
35
36
 
36
37
  patcher.patch(String.prototype, 'concat', {
37
38
  name,
38
39
  patchType,
39
40
  usePerf: 'sync',
41
+ around(next, data) {
42
+ let hasArray = false;
43
+ for (let i = 0; i < data.args.length; i++) {
44
+ if (Array.isArray(data.args[i])) {
45
+ hasArray = true;
46
+ break;
47
+ }
48
+ }
49
+
50
+ // prevent propagation from happening in original concat method
51
+ // if arg is Array, since arg.join will be called on it.
52
+ if (hasArray && !core.scopes.instrumentation.isLocked())
53
+ return core.scopes.instrumentation.run(store, next);
54
+
55
+ return next();
56
+ },
40
57
  post(data) {
41
- const { obj, result, hooked, orig } = data;
42
- if (!result || !getPropagatorContext()) return;
58
+ if (!data.result || !getPropagatorContext()) return;
43
59
 
44
- const rInfo = tracker.getData(result);
60
+ const rInfo = tracker.getData(data.result);
45
61
  if (rInfo) {
46
62
  // this may happen w/ trackedStr.concat('') => trackedStr
47
63
  return;
48
64
  }
49
65
 
50
- const objInfo = tracker.getData(obj);
66
+ const objInfo = tracker.getData(data.obj);
51
67
  const history = objInfo ? new Set([objInfo]) : new Set();
52
- let globalOffset = typeof obj !== 'function' ? obj.length : 0;
68
+
69
+ let globalOffset = typeof data.obj !== 'function' ? data.obj.length : 0;
53
70
  const args = [];
54
71
  let tags = objInfo?.tags;
55
72
 
56
73
  for (const arg of data.args) {
74
+ const isArray = Array.isArray(arg);
75
+ // TODO NODE-3748: handle tag ranges when arg is an Array
57
76
  const strInfo = tracker.getData(arg);
58
-
59
77
  if (strInfo) {
60
78
  args.push({ tracked: true, value: arg });
61
79
  history.add(strInfo);
@@ -64,12 +82,12 @@ module.exports = function(core) {
64
82
  args.push({ tracked: false, value: getAdjustedUntrackedValue(arg) });
65
83
  }
66
84
 
67
- globalOffset += `${arg}`.length;
85
+ // if arg is an Array, then `${arg}` causes arg.join to get called and cause unwanted propagation
86
+ globalOffset += isArray ? ArrayPrototypeJoin.call(arg).length : `${arg}`.length;
68
87
  }
69
88
 
70
-
71
89
  if (history.size) {
72
- const objVal = objInfo ? `'${objInfo.value}'` : getAdjustedUntrackedValue(obj);
90
+ const objVal = objInfo ? `'${objInfo.value}'` : getAdjustedUntrackedValue(data.obj);
73
91
  const context = `${objVal}.concat(${ArrayPrototypeJoin.call(args.map((a) => a.value))})`;
74
92
  const event = createPropagationEvent({
75
93
  name,
@@ -77,11 +95,11 @@ module.exports = function(core) {
77
95
  methodName: 'prototype.concat',
78
96
  context,
79
97
  object: {
80
- value: objInfo?.value ?? getAdjustedUntrackedValue(obj),
98
+ value: objInfo?.value ?? getAdjustedUntrackedValue(data.obj),
81
99
  tracked: !!objInfo
82
100
  },
83
101
  result: {
84
- value: result,
102
+ value: data.result,
85
103
  tracked: true
86
104
  },
87
105
  args,
@@ -90,19 +108,19 @@ module.exports = function(core) {
90
108
  source: objInfo ? (history.size > 1 ? 'A' : 'O') : 'P',
91
109
  target: 'R',
92
110
  stacktraceOpts: {
93
- constructorOpt: hooked,
94
- prependFrames: [orig]
111
+ constructorOpt: data.hooked,
112
+ prependFrames: [data.orig]
95
113
  },
96
114
  });
97
115
 
98
116
  if (!event) return;
99
- const { extern } = tracker.track(result, event);
117
+ const { extern } = tracker.track(data.result, event);
100
118
 
101
119
  if (extern) {
102
120
  data.result = extern;
103
121
  }
104
122
  }
105
- },
123
+ }
106
124
  });
107
125
  },
108
126
  uninstall() {
@@ -15,7 +15,7 @@
15
15
 
16
16
  'use strict';
17
17
 
18
- const { isString, primordials: { StringPrototypeMatch, StringPrototypeToLowerCase } } = require('@contrast/common');
18
+ const { isString, primordials: { StringPrototypeMatch } } = require('@contrast/common');
19
19
  const { createAppendTags } = require('../../tag-utils');
20
20
  const { patchType } = require('../common');
21
21
 
@@ -47,7 +47,7 @@ module.exports = function(core) {
47
47
  let newTags = {};
48
48
  const history = [];
49
49
  const eventArgs = [];
50
- const formatChars = args[0].includes('%') ? StringPrototypeMatch.call(args[0], /[^%]+/g).map((x) => x[0]) : [];
50
+ const formatChars = args[0].includes('%') ? StringPrototypeMatch.call(args[0], /%[sdifjoOc]/g).map((x) => x[1]) : [];
51
51
  let i = 0;
52
52
 
53
53
  if (formatChars.length > 0) {
@@ -58,14 +58,44 @@ module.exports = function(core) {
58
58
  for (i; i < args.length; i++) {
59
59
  let arg = args[i];
60
60
  const formatChar = formatChars[i - 1];
61
- if (formatChar && typeof arg === 'object') {
62
-
63
- if (formatChar === 'j') {
64
- arg = JSON.stringify(arg);
65
- } else if (StringPrototypeToLowerCase.call(formatChar) === 'o') {
66
- // TO-DO: NODE-3235
61
+ if (formatChar) {
62
+ switch (formatChar) {
63
+ case 's':
64
+ if (typeof arg === 'object') {
65
+ // util.inspect instrumentation NYI
66
+ arg = arg?.toString ? arg.toString() : util.inspect(arg, { depth: 0, colors: false, compact: 3 });
67
+ } else {
68
+ arg = String(arg);
69
+ }
70
+ break;
71
+ case 'd':
72
+ case 'i':
73
+ case 'f':
74
+ // won't be tracked
75
+ break;
76
+ case 'j':
77
+ arg = JSON.stringify(arg) ?? 'undefined';
78
+ break;
79
+ case 'o':
80
+ // util.inspect instrumentation NYI
81
+ arg = util.inspect(arg, { showHidden: true, showProxy: true });
82
+ break;
83
+ case 'O':
84
+ // util.inspect instrumentation NYI
85
+ arg = util.inspect(arg);
86
+ break;
87
+ case 'c':
88
+ // c is ignored and skipped
89
+ arg = '';
90
+ break;
67
91
  }
92
+ } else if (typeof arg !== 'string') {
93
+ arg = util.inspect(arg);
68
94
  }
95
+
96
+ const argInfo = tracker.getData(arg);
97
+ if (!argInfo) continue;
98
+
69
99
  const currIdx = result.indexOf(arg, idx);
70
100
  if (currIdx > -1) {
71
101
  idx = currIdx + arg.length;
@@ -73,9 +103,6 @@ module.exports = function(core) {
73
103
  continue;
74
104
  }
75
105
 
76
- const argInfo = tracker.getData(arg);
77
- if (!argInfo) continue;
78
-
79
106
  newTags = createAppendTags(newTags, argInfo.tags, currIdx);
80
107
 
81
108
  history.push({ ...argInfo });
@@ -120,8 +147,7 @@ module.exports = function(core) {
120
147
  }
121
148
  }
122
149
  });
123
- }
124
- );
150
+ });
125
151
  },
126
152
  };
127
153
  };
@@ -24,178 +24,182 @@ const {
24
24
  StringPrototypeToLowerCase
25
25
  }
26
26
  } = require('@contrast/common');
27
+ const { Core } = require('@contrast/core/lib/ioc/core');
28
+
29
+ module.exports = Core.makeComponent({
30
+ name: 'assess.dataflow.sources.handle',
31
+ factory(core) {
32
+ const {
33
+ assess: {
34
+ eventFactory,
35
+ dataflow: {
36
+ sources,
37
+ tracker
38
+ }
39
+ },
40
+ config,
41
+ createSnapshot,
42
+ } = core;
27
43
 
28
- module.exports = function (core) {
29
- const {
30
- assess: {
31
- eventFactory,
32
- dataflow: {
33
- sources,
34
- tracker
35
- }
36
- },
37
- config,
38
- createSnapshot,
39
- } = core;
40
-
41
- const logger = core.logger.child({ name: 'contrast:sources' });
44
+ const logger = core.logger.child({ name: 'contrast:sources' });
42
45
 
43
- const emptyStack = Object.freeze([]);
46
+ const emptyStack = Object.freeze([]);
44
47
 
45
- sources.createTags = function createTags({ inputType, fieldName = '', value, tagNames }) {
46
- if (!value?.length) {
47
- return null;
48
- }
48
+ sources.createTags = function createTags({ inputType, fieldName = '', value, tagNames }) {
49
+ if (!value?.length) {
50
+ return null;
51
+ }
49
52
 
50
- const stop = value.length - 1;
51
- const tags = {
52
- [DataflowTag.UNTRUSTED]: [0, stop]
53
- };
53
+ const stop = value.length - 1;
54
+ const tags = {
55
+ [DataflowTag.UNTRUSTED]: [0, stop]
56
+ };
54
57
 
55
- if (tagNames) {
56
- for (const tag of tagNames) {
57
- tags[tag] = [0, stop];
58
+ if (tagNames) {
59
+ for (const tag of tagNames) {
60
+ tags[tag] = [0, stop];
61
+ }
58
62
  }
59
- }
60
63
 
61
- if (inputType === InputType.HEADER && StringPrototypeToLowerCase.call(fieldName) === 'referer') {
62
- tags[DataflowTag.HEADER] = [0, stop];
63
- }
64
+ if (inputType === InputType.HEADER && StringPrototypeToLowerCase.call(fieldName) === 'referer') {
65
+ tags[DataflowTag.HEADER] = [0, stop];
66
+ }
64
67
 
65
- return tags;
66
- };
67
-
68
- sources.createStacktrace = function (stacktraceOpts) {
69
- return config.assess.stacktraces === 'NONE' || config.assess.stacktraces === 'SINK'
70
- ? emptyStack
71
- : createSnapshot(stacktraceOpts)();
72
- };
73
-
74
- sources.handle = function ({
75
- context,
76
- keys,
77
- name,
78
- inputType = InputType.UNKNOWN,
79
- stacktraceOpts,
80
- data,
81
- sourceContext,
82
- }) {
83
- if (!data) return;
84
-
85
- if (!sourceContext) {
86
- logger.trace({ inputType, sourceName: name }, 'skipping assess source handling - no request context');
87
- return null;
88
- }
68
+ return tags;
69
+ };
89
70
 
90
- // url exclusion
91
- if (!sourceContext.policy) {
92
- return null;
93
- }
71
+ sources.createStacktrace = function(stacktraceOpts) {
72
+ return config.assess.stacktraces === 'NONE' || config.assess.stacktraces === 'SINK'
73
+ ? emptyStack
74
+ : createSnapshot(stacktraceOpts)();
75
+ };
94
76
 
95
- if (!context) {
96
- context = inputType;
97
- }
77
+ sources.handle = function({
78
+ context,
79
+ keys,
80
+ name,
81
+ inputType = InputType.UNKNOWN,
82
+ stacktraceOpts,
83
+ data,
84
+ sourceContext,
85
+ }) {
86
+ if (!data) return;
87
+
88
+ if (!sourceContext) {
89
+ logger.trace({ inputType, sourceName: name }, 'skipping assess source handling - no request context');
90
+ return null;
91
+ }
98
92
 
99
- const { policy: requestPolicy } = sourceContext;
100
- const max = config.assess.max_context_source_events;
101
- let _data = data;
102
- let stack;
93
+ // url exclusion
94
+ if (!sourceContext.policy) {
95
+ return null;
96
+ }
103
97
 
104
- if (keys) {
105
- _data = {};
106
- for (const key of keys) {
107
- _data[key] = data[key];
98
+ if (!context) {
99
+ context = inputType;
108
100
  }
109
- }
110
101
 
111
- function createEvent({ fieldName, pathName, value, excludedRules }) {
112
- const tagNames = Array.from(excludedRules).map((ruleId) => `excluded:${ruleId}`);
113
- // create the stacktrace once per call to .handle()
114
- stack || (stack = sources.createStacktrace(stacktraceOpts));
115
- return eventFactory.createSourceEvent({
116
- context: `${context}.${pathName}`,
117
- name,
118
- fieldName,
119
- pathName,
120
- stack,
121
- inputType,
122
- tags: sources.createTags({ inputType, fieldName, value, tagNames }),
123
- result: { tracked: true, value },
124
- });
125
- }
102
+ const { policy: requestPolicy } = sourceContext;
103
+ const max = config.assess.max_context_source_events;
104
+ let _data = data;
105
+ let stack;
126
106
 
127
- if (Buffer.isBuffer(data) && !tracker.getData(data)) {
128
- const { track, excludedRules } = requestPolicy.getInputPolicy(InputType.BODY);
129
- if (!track) {
130
- core.logger.debug({ inputType }, 'assess input exclusion disabled tracking');
131
- return;
107
+ if (keys) {
108
+ _data = {};
109
+ for (const key of keys) {
110
+ _data[key] = data[key];
111
+ }
132
112
  }
133
113
 
134
- const event = createEvent({ pathName: 'body', value: data, fieldName: '', excludedRules });
135
- if (event) {
136
- tracker.track(data, event);
114
+ function createEvent({ fieldName, pathName, value, excludedRules }) {
115
+ const tagNames = Array.from(excludedRules).map((ruleId) => `excluded:${ruleId}`);
116
+ // create the stacktrace once per call to .handle()
117
+ stack || (stack = sources.createStacktrace(stacktraceOpts));
118
+ return eventFactory.createSourceEvent({
119
+ context: `${context}.${pathName}`,
120
+ name,
121
+ fieldName,
122
+ pathName,
123
+ stack,
124
+ inputType,
125
+ tags: sources.createTags({ inputType, fieldName, value, tagNames }),
126
+ result: { tracked: true, value },
127
+ });
137
128
  }
138
129
 
139
- return;
140
- }
141
-
142
- traverse(_data, (path, fieldName, value, obj) => {
143
- const pathName = ArrayPrototypeJoin.call(path, '.');
130
+ if (Buffer.isBuffer(data) && !tracker.getData(data)) {
131
+ const { track, excludedRules } = requestPolicy.getInputPolicy(InputType.BODY);
132
+ if (!track) {
133
+ core.logger.debug({ inputType }, 'assess input exclusion disabled tracking');
134
+ return;
135
+ }
144
136
 
145
- if (sourceContext.sourceEventsCount >= max) {
146
- logger.trace({ inputType, sourceName: name }, 'exiting assess source handling - %s max events exceeded', max);
147
- return true;
148
- }
137
+ const event = createEvent({ pathName: 'body', value: data, fieldName: '', excludedRules });
138
+ if (event) {
139
+ tracker.track(data, event);
140
+ }
149
141
 
150
- const { track, excludedRules } = sourceContext.policy.getInputPolicy(inputType, fieldName);
151
- if (!track) {
152
- core.logger.debug({ fieldName, inputType }, 'assess input exclusion disabling tracking');
153
142
  return;
154
143
  }
155
144
 
156
- if (isString(value) && value.length) {
157
- const strInfo = tracker.getData(value);
158
- if (strInfo) {
159
- // TODO: confirm this "layering-on" approach is what we want
160
- // when a value is already tracked, the handler will "re-track" the value with new source
161
- // event metadata. without this step tracker would complain about value already being tracked.
162
- // alternatively we could treat this more like a propagation event and update existing metadata.
163
- value = strInfo.value;
145
+ traverse(_data, (path, fieldName, value, obj) => {
146
+ const pathName = ArrayPrototypeJoin.call(path, '.');
147
+
148
+ if (sourceContext.sourceEventsCount >= max) {
149
+ logger.trace({ inputType, sourceName: name }, 'exiting assess source handling - %s max events exceeded', max);
150
+ return true;
164
151
  }
165
- const event = createEvent({ pathName, value, fieldName, excludedRules });
166
- if (!event) {
167
- logger.warn({ inputType, sourceName: name, pathName, value }, 'unable to create source event');
152
+
153
+ const { track, excludedRules } = sourceContext.policy.getInputPolicy(inputType, fieldName);
154
+ if (!track) {
155
+ core.logger.debug({ fieldName, inputType }, 'assess input exclusion disabling tracking');
168
156
  return;
169
157
  }
170
158
 
171
- const { extern } = tracker.track(value, event);
172
- if (extern) {
173
- logger.trace({ extern, fieldName, sourceName: name, inputType }, 'tracked');
174
- obj[fieldName] = extern;
175
-
176
- sourceContext.sourceEventsCount++;
159
+ if (isString(value) && value.length) {
160
+ const strInfo = tracker.getData(value);
161
+ if (strInfo) {
162
+ // TODO: confirm this "layering-on" approach is what we want
163
+ // when a value is already tracked, the handler will "re-track" the value with new source
164
+ // event metadata. without this step tracker would complain about value already being tracked.
165
+ // alternatively we could treat this more like a propagation event and update existing metadata.
166
+ value = strInfo.value;
167
+ }
168
+ const event = createEvent({ pathName, value, fieldName, excludedRules });
169
+ if (!event) {
170
+ logger.warn({ inputType, sourceName: name, pathName, value }, 'unable to create source event');
171
+ return;
172
+ }
173
+
174
+ const { extern } = tracker.track(value, event);
175
+ if (extern) {
176
+ logger.trace({ extern, fieldName, sourceName: name, inputType }, 'tracked');
177
+ obj[fieldName] = extern;
178
+
179
+ sourceContext.sourceEventsCount++;
180
+ }
181
+ } else if (Buffer.isBuffer(value) && !tracker.getData(value)) {
182
+ const event = createEvent({ pathName, value, fieldName, excludedRules });
183
+ if (event) {
184
+ tracker.track(value, event);
185
+ } else {
186
+ logger.warn({ inputType, sourceName: name, pathName, value }, 'unable to create source event');
187
+ }
177
188
  }
178
- } else if (Buffer.isBuffer(value) && !tracker.getData(value)) {
179
- const event = createEvent({ pathName, value, fieldName, excludedRules });
180
- if (event) {
181
- tracker.track(value, event);
182
- } else {
183
- logger.warn({ inputType, sourceName: name, pathName, value }, 'unable to create source event');
184
- }
185
- }
186
- });
189
+ });
187
190
 
188
- if (keys) {
189
- for (const key of keys) {
190
- data[key] = _data[key];
191
+ if (keys) {
192
+ for (const key of keys) {
193
+ data[key] = _data[key];
194
+ }
191
195
  }
192
- }
193
196
 
194
- return data;
195
- };
197
+ return data;
198
+ };
196
199
 
197
- return sources;
198
- };
200
+ return sources;
201
+ }
202
+ });
199
203
 
200
204
  /**
201
205
  * A custom traversal function for handling source value tracking efficiently.
@@ -20,8 +20,7 @@ const { callChildComponentMethodsSync } = require('@contrast/common');
20
20
  module.exports = function (core) {
21
21
  const sources = core.assess.dataflow.sources = {};
22
22
 
23
- require('./handler')(core);
24
-
23
+ core.initComponentSync(require('./handler'));
25
24
  require('./install/express')(core);
26
25
  require('./install/fastify')(core);
27
26
  require('./install/hapi')(core);
@@ -14,7 +14,7 @@
14
14
  */
15
15
  'use strict';
16
16
 
17
- const { primordials: { StringPrototypeSplit } } = require('@contrast/common');
17
+ const { empties, primordials: { StringPrototypeSplit } } = require('@contrast/common');
18
18
 
19
19
  //
20
20
  // This module implements tag range manipulation functions. There are generally
@@ -295,7 +295,7 @@ function createMergedTags(firstTags, secondTags) {
295
295
 
296
296
  function mergeCore(firstTagRanges, secondTagRanges) {
297
297
  if (!firstTagRanges && !secondTagRanges) {
298
- return [];
298
+ return empties.ARRAY;
299
299
  } else if (!firstTagRanges) {
300
300
  return [...secondTagRanges];
301
301
  } else if (!secondTagRanges) {
@@ -463,7 +463,7 @@ function createAdjustedQueryTags(path, tags, value, argString) {
463
463
  break;
464
464
  }
465
465
  }
466
- return idx >= 0 ? createAppendTags([], tags, idx) : { ...tags };
466
+ return idx >= 0 ? createAppendTags(empties.OBJECT, tags, idx) : { ...tags };
467
467
  }
468
468
 
469
469
  /**
@@ -15,309 +15,205 @@
15
15
 
16
16
  'use strict';
17
17
 
18
- const { InputType, primordials: { StringPrototypeMatch } } = require('@contrast/common');
18
+ const { InputType, empties, primordials: { StringPrototypeMatch } } = require('@contrast/common');
19
+ const { Core } = require('@contrast/core/lib/ioc/core');
19
20
  const ANNOTATION_REGEX = /^(A|O|R|P|P\d+)$/;
20
21
  const SOURCE_EVENT_MSG = 'Source event not created: %s';
21
22
  const PROPAGATION_EVENT_MSG = 'Propagation event not created: %s';
22
23
 
23
- module.exports = function (core) {
24
- const {
25
- createSnapshot,
26
- config,
27
- logger,
28
- scopes: { sources },
29
- } = core;
30
-
31
- const eventFactory = core.assess.eventFactory = {};
32
-
33
- eventFactory.createdEvents = new WeakSet();
34
-
35
- eventFactory.createSourceEvent = function (data = {}) {
24
+ module.exports = Core.makeComponent({
25
+ name: 'assess.eventFactory',
26
+ factory(core) {
36
27
  const {
37
- name,
38
- result = { value: null, tracked: false },
39
- tags,
40
- inputType,
41
- stack,
42
- } = data;
43
-
44
- if (!result.value) {
45
- logger.debug({ name }, SOURCE_EVENT_MSG, 'invalid result');
46
- return null;
47
- }
48
-
49
- if (!name) {
50
- logger.debug({ name }, SOURCE_EVENT_MSG, 'invalid name');
51
- return null;
52
- }
53
-
54
- if (!(inputType in InputType)) {
55
- logger.debug({ name }, SOURCE_EVENT_MSG, 'invalid inputType');
56
- return null;
57
- }
58
-
59
- if (!tags) {
60
- logger.debug({ name }, SOURCE_EVENT_MSG, 'event has no tags');
61
- return null;
62
- }
63
-
64
-
65
- if (!stack || !Array.isArray(stack)) {
66
- logger.debug({ name }, SOURCE_EVENT_MSG, 'invalid stack');
67
- return null;
68
- }
69
-
70
- data.time = Date.now();
71
- eventFactory.createdEvents.add(data);
72
-
73
- return data;
74
- };
75
-
76
- eventFactory.createPropagationEvent = function (data) {
77
- const {
78
- name = '',
79
- moduleName,
80
- methodName,
81
- history = [],
82
- object = { value: null, tracked: false },
83
- args = [],
84
- context,
85
- result = { value: null, tracked: false },
86
- tags = {},
87
- addedTags = [],
88
- removedTags = [],
89
- source,
90
- target,
91
- stacktraceOpts
92
- } = data;
93
-
94
- const sourceContext = sources.getStore()?.assess;
95
-
96
- if (!sourceContext) {
97
- logger.debug({ name }, 'No sourceContext found during Propagation event creation');
98
- return null;
99
- }
100
-
101
- if (sourceContext.propagationEventsCount >= config.assess.max_propagation_events) {
102
- logger.debug({ name }, 'Maximum number of Propagation events reached. Event not created');
103
- return null;
104
- }
105
-
106
- if (!name) {
107
- logger.debug({ name }, PROPAGATION_EVENT_MSG, 'invalid name');
108
- return null;
109
- }
110
-
111
- if (!history.length) {
112
- logger.debug({ name }, PROPAGATION_EVENT_MSG, 'invalid history');
113
- return null;
114
- }
115
-
116
- if (!source || !StringPrototypeMatch.call(source, ANNOTATION_REGEX)) {
117
- logger.debug({ name }, PROPAGATION_EVENT_MSG, 'invalid source');
118
- return null;
119
- }
120
-
121
- if (!target || !StringPrototypeMatch.call(target, ANNOTATION_REGEX)) {
122
- logger.debug({ name }, PROPAGATION_EVENT_MSG, 'invalid target');
123
- return null;
124
- }
125
-
126
- let stack;
127
- if (config.assess.stacktraces === 'ALL') {
128
- stack = createSnapshot(stacktraceOpts)();
129
- } else {
130
- stack = [];
131
- }
132
-
133
- const event = {
134
- addedTags,
135
- args,
136
- context,
137
- history,
138
- name,
139
- moduleName,
140
- methodName,
141
- object,
142
- removedTags,
143
- result,
144
- source,
145
- stack,
146
- tags,
147
- target,
148
- time: Date.now(),
28
+ createSnapshot,
29
+ config,
30
+ logger,
31
+ scopes: { sources },
32
+ } = core;
33
+
34
+ const eventFactory = core.assess.eventFactory = {};
35
+
36
+ eventFactory.createdEvents = new WeakSet();
37
+
38
+ eventFactory.createSourceEvent = function(data = {}) {
39
+ if (!data.result?.value) {
40
+ logger.debug(SOURCE_EVENT_MSG, `invalid result: ${data.name}`);
41
+ return null;
42
+ }
43
+ if (!data.name) {
44
+ logger.debug(SOURCE_EVENT_MSG, `invalid name: ${data.name}`);
45
+ return null;
46
+ }
47
+ if (!(data.inputType in InputType)) {
48
+ logger.debug(SOURCE_EVENT_MSG, `invalid inputType: ${data.name}`);
49
+ return null;
50
+ }
51
+ if (!data.tags) {
52
+ logger.debug(SOURCE_EVENT_MSG, `event has no tags: ${data.name}`);
53
+ return null;
54
+ }
55
+ if (!data.stack || !Array.isArray(data.stack)) {
56
+ logger.debug(SOURCE_EVENT_MSG, `invalid stack: ${data.name}`);
57
+ return null;
58
+ }
59
+
60
+ data.time = Date.now();
61
+ eventFactory.createdEvents.add(data);
62
+
63
+ return data;
149
64
  };
150
65
 
151
- eventFactory.createdEvents.add(event);
152
- sourceContext.propagationEventsCount++;
153
-
154
- return event;
155
- };
156
-
157
- eventFactory.createSinkEvent = function (data) {
158
- const {
159
- context,
160
- name = '',
161
- moduleName,
162
- methodName,
163
- history = [],
164
- object = { value: null, tracked: false },
165
- args = [],
166
- result = { value: null, tracked: false },
167
- tags = {},
168
- source,
169
- stacktraceOpts
170
- } = data;
171
-
172
- const sourceContext = sources.getStore()?.assess;
173
- if (!sourceContext) {
174
- logger.debug({ name }, 'no sourceContext found during sink event creation');
175
- return null;
176
- }
177
- if (!name) {
178
- logger.debug({ name }, 'no sink event name');
179
- return null;
180
- }
181
- if (!history.length) {
182
- logger.debug({ name }, 'empty history for sink event');
183
- return null;
184
- }
185
- if (
186
- (!source || !StringPrototypeMatch.call(source, ANNOTATION_REGEX))
187
- ) {
188
- logger.debug({ name }, 'malformed or missing sink event source field');
189
- return null;
190
- }
191
-
192
- let stack;
193
- if (config.assess.stacktraces !== 'NONE') {
194
- stack = createSnapshot(stacktraceOpts)();
195
- } else {
196
- stack = [];
197
- }
198
-
199
- const event = {
200
- args,
201
- context,
202
- history,
203
- name,
204
- moduleName,
205
- methodName,
206
- object,
207
- result,
208
- source,
209
- stack,
210
- tags,
211
- time: Date.now(),
66
+ eventFactory.createPropagationEvent = function(data) {
67
+ const sourceContext = sources.getStore()?.assess;
68
+
69
+ if (!sourceContext) {
70
+ logger.debug('No sourceContext found during Propagation event creation: %s', data.name);
71
+ return null;
72
+ }
73
+ if (sourceContext.propagationEventsCount >= config.assess.max_propagation_events) {
74
+ logger.debug('Maximum number of Propagation events reached. Event not created: %s', data.name);
75
+ return null;
76
+ }
77
+ if (!data.name) {
78
+ logger.debug(PROPAGATION_EVENT_MSG, `invalid name (${data.name})`);
79
+ return null;
80
+ }
81
+ if (!data.history.length) {
82
+ logger.debug(PROPAGATION_EVENT_MSG, `invalid history (${data.name})`);
83
+ return null;
84
+ }
85
+ if (!data.source || !StringPrototypeMatch.call(data.source, ANNOTATION_REGEX)) {
86
+ logger.debug(PROPAGATION_EVENT_MSG, `invalid source (${data.name})`);
87
+ return null;
88
+ }
89
+ if (!data.target || !StringPrototypeMatch.call(data.target, ANNOTATION_REGEX)) {
90
+ logger.debug(PROPAGATION_EVENT_MSG, `invalid target (${data.name})`);
91
+ return null;
92
+ }
93
+
94
+ if (config.assess.stacktraces === 'ALL') {
95
+ data.stack = createSnapshot(data.stacktraceOpts)();
96
+ } else {
97
+ data.stack = empties.ARRAY;
98
+ }
99
+
100
+ data.args ??= empties.ARRAY;
101
+ data.addedTags ??= empties.ARRAY;
102
+ data.history ??= empties.ARRAY;
103
+ data.object ??= empties.UNTRACKED_VALUE_OBJ;
104
+ data.result ??= empties.UNTRACKED_VALUE_OBJ;
105
+ data.removedTags ??= empties.ARRAY;
106
+ data.time = Date.now();
107
+
108
+ eventFactory.createdEvents.add(data);
109
+ sourceContext.propagationEventsCount++;
110
+
111
+ return data;
212
112
  };
213
113
 
214
- eventFactory.createdEvents.add(event);
215
-
216
- return event;
217
- };
218
-
219
- eventFactory.createSessionEvent = function (data) {
220
- const {
221
- context,
222
- name = '',
223
- moduleName,
224
- methodName,
225
- object = { value: null, tracked: false },
226
- args = [],
227
- result = { value: null, tracked: false },
228
- source,
229
- stacktraceOpts,
230
- framework,
231
- options
232
- } = data;
233
-
234
- if (!name) {
235
- logger.debug({ name }, 'no sink event name');
236
- return null;
237
- }
238
-
239
- if (
240
- (!source || !StringPrototypeMatch.call(source, ANNOTATION_REGEX))
241
- ) {
242
- logger.debug({ name }, 'malformed or missing sink event source field');
243
- return null;
244
- }
245
-
246
- let stack;
247
- if (config.assess.stacktraces !== 'NONE') {
248
- stack = createSnapshot(stacktraceOpts)();
249
- } else {
250
- stack = [];
251
- }
252
-
253
- const event = {
254
- args,
255
- context,
256
- history: [],
257
- name,
258
- moduleName,
259
- methodName,
260
- object,
261
- result,
262
- source,
263
- stack,
264
- tags: {},
265
- time: Date.now(),
266
- framework,
267
- options,
114
+ eventFactory.createSinkEvent = function(data) {
115
+ const sourceContext = sources.getStore()?.assess;
116
+
117
+ if (!sourceContext) {
118
+ logger.debug('no sourceContext found during sink event creation: %s', data.name);
119
+ return null;
120
+ }
121
+ if (!data.name) {
122
+ logger.debug('no sink event name: %s', data.name);
123
+ return null;
124
+ }
125
+ if (!data.history.length) {
126
+ logger.debug('empty history for sink event: %s', data.name);
127
+ return null;
128
+ }
129
+ if (
130
+ (!data.source || !StringPrototypeMatch.call(data.source, ANNOTATION_REGEX))
131
+ ) {
132
+ logger.debug('malformed or missing sink event source field: %s', data.name);
133
+ return null;
134
+ }
135
+
136
+ if (config.assess.stacktraces !== 'NONE') {
137
+ data.stack = createSnapshot(data.stacktraceOpts)();
138
+ } else {
139
+ data.stack = empties.ARRAY;
140
+ }
141
+
142
+ data.args ??= empties.ARRAY;
143
+ data.history ??= empties.ARRAY;
144
+ data.object ??= empties.ARRAY;
145
+ data.result ??= empties.UNTRACKED_VALUE_OBJ;
146
+ data.time = Date.now();
147
+
148
+ eventFactory.createdEvents.add(data);
149
+
150
+ return data;
268
151
  };
269
152
 
270
- eventFactory.createdEvents.add(event);
271
-
272
- return event;
273
- };
274
-
275
-
276
- /**
277
- * @param {{
278
- * context: string,
279
- * name: string,
280
- * moduleName: string,
281
- * methodName: string,
282
- * object: { value: any, tracked: boolean },
283
- * args: any[],
284
- * result: { value: vany, tracked: boolean },
285
- * source: string,
286
- * stacktraceOpts: { constructorOpt?: Function},
287
- * }} data
288
- * @returns {any}
289
- */
290
- eventFactory.createCryptoAnalysisEvent = function (data) {
291
- const {
292
- name = '',
293
- source,
294
- stacktraceOpts,
295
- } = data;
296
-
297
- if (!name) {
298
- logger.debug({ name }, 'no sink event name');
299
- return null;
300
- }
301
-
302
- if (!source || !StringPrototypeMatch.call(source, ANNOTATION_REGEX)) {
303
- logger.debug({ name }, 'malformed or missing sink event source field');
304
- return null;
305
- }
306
-
307
- let stack;
308
- if (config.assess.stacktraces !== 'NONE') {
309
- stack = createSnapshot(stacktraceOpts)();
310
- } else {
311
- stack = [];
312
- }
313
-
314
- data.stack = stack;
315
- data.time = Date.now();
316
-
317
- eventFactory.createdEvents.add(data);
153
+ eventFactory.createSessionEvent = function(data) {
154
+ if (!data.name) {
155
+ logger.debug('no sink event name: %s', data.name);
156
+ return null;
157
+ }
158
+ if (
159
+ (!data.source || !StringPrototypeMatch.call(data.source, ANNOTATION_REGEX))
160
+ ) {
161
+ logger.debug('malformed or missing sink event source field: %s', data.name);
162
+ return null;
163
+ }
164
+ if (config.assess.stacktraces !== 'NONE') {
165
+ data.stack = createSnapshot(data.stacktraceOpts)();
166
+ } else {
167
+ data.stack = empties.ARRAY;
168
+ }
169
+
170
+ data.args ??= empties.ARRAY;
171
+ data.history ??= empties.ARRAY;
172
+ data.obj ??= empties.UNTRACKED_VALUE_OBJ;
173
+ data.result ??= empties.UNTRACKED_VALUE_OBJ;
174
+ data.tags ??= empties.OBJECT;
175
+ data.time = Date.now();
176
+
177
+ eventFactory.createdEvents.add(data);
178
+
179
+ return data;
180
+ };
318
181
 
319
- return data;
320
- };
182
+ /**
183
+ * @param {{
184
+ * context: string,
185
+ * name: string,
186
+ * moduleName: string,
187
+ * methodName: string,
188
+ * object: { value: any, tracked: boolean },
189
+ * args: any[],
190
+ * result: { value: vany, tracked: boolean },
191
+ * source: string,
192
+ * stacktraceOpts: { constructorOpt?: Function},
193
+ * }} data
194
+ * @returns {any}
195
+ */
196
+ eventFactory.createCryptoAnalysisEvent = function(data) {
197
+ if (!data.name) {
198
+ logger.debug('no sink event name: %s', data.name);
199
+ return null;
200
+ }
201
+ if (!data.source || !StringPrototypeMatch.call(data.source, ANNOTATION_REGEX)) {
202
+ logger.debug('malformed or missing sink event source field: %s', data.name);
203
+ return null;
204
+ }
205
+ if (config.assess.stacktraces !== 'NONE') {
206
+ data.stack = createSnapshot(data.stacktraceOpts)();
207
+ } else {
208
+ data.stack = empties.ARRAY;
209
+ }
210
+
211
+ data.time = Date.now();
212
+ eventFactory.createdEvents.add(data);
213
+
214
+ return data;
215
+ };
321
216
 
322
- return eventFactory;
323
- };
217
+ return eventFactory;
218
+ }
219
+ });
package/lib/index.js CHANGED
@@ -52,10 +52,10 @@ module.exports = function assess(core) {
52
52
  // ancillary tools used by different features
53
53
  require('./sampler')(core);
54
54
  require('./get-policy')(core);
55
- require('./make-source-context')(core);
55
+ core.initComponentSync(require('./make-source-context'));
56
56
  require('./rule-scopes')(core);
57
57
  require('./get-source-context')(core);
58
- require('./event-factory')(core);
58
+ core.initComponentSync(require('./event-factory'));
59
59
 
60
60
  // various Assess features
61
61
  require('./dataflow')(core);
@@ -16,75 +16,80 @@
16
16
  'use strict';
17
17
 
18
18
  const { primordials: { StringPrototypeToLowerCase, StringPrototypeSlice } } = require('@contrast/common');
19
+ const { Core } = require('@contrast/core/lib/ioc/core');
20
+
19
21
  /**
20
22
  * @param {{
21
23
  * assess: import('@contrast/assess').Assess,
22
24
  * logger: import('@contrast/logger').Logger,
23
25
  * }} core
24
26
  */
25
- module.exports = function(core) {
26
- const { assess, logger } = core;
27
+ module.exports = Core.makeComponent({
28
+ name: 'assess.makeSourceContext',
29
+ factory(core) {
30
+ const { assess, logger } = core;
27
31
 
28
- /**
29
- * @returns {import('@contrast/assess').SourceContext}
30
- */
31
- return core.assess.makeSourceContext = function (sourceData) {
32
- try {
33
- // todo: how to handle non-HTTP sources
34
- const { incomingMessage: req } = sourceData;
32
+ /**
33
+ * @returns {import('@contrast/assess').SourceContext}
34
+ */
35
+ return core.assess.makeSourceContext = function(sourceData) {
36
+ try {
37
+ // todo: how to handle non-HTTP sources
38
+ const { incomingMessage: req } = sourceData;
35
39
 
36
- // minimally process the request data for sampling and exclusions.
37
- // more request fields will be appended in final result below.
38
- let uriPath;
39
- let queries;
40
- const idx = req.url.indexOf('?');
41
- if (idx >= 0) {
42
- uriPath = StringPrototypeSlice.call(req.url, 0, idx);
43
- queries = StringPrototypeSlice.call(req.url, idx + 1);
44
- } else {
45
- uriPath = req.url;
46
- queries = '';
47
- }
40
+ // minimally process the request data for sampling and exclusions.
41
+ // more request fields will be appended in final result below.
42
+ let uriPath;
43
+ let queries;
44
+ const idx = req.url.indexOf('?');
45
+ if (idx >= 0) {
46
+ uriPath = StringPrototypeSlice.call(req.url, 0, idx);
47
+ queries = StringPrototypeSlice.call(req.url, idx + 1);
48
+ } else {
49
+ uriPath = req.url;
50
+ queries = '';
51
+ }
48
52
 
49
- const ctx = sourceData.store.assess = {
50
- // default policy to `null` until it is set later below. this will cause
51
- // all instrumentation to short-circuit, see `./get-source-context.js`.
52
- policy: null,
53
- reqData: {
54
- method: req.method,
55
- uriPath,
56
- queries,
57
- },
58
- };
53
+ const ctx = sourceData.store.assess = {
54
+ // default policy to `null` until it is set later below. this will cause
55
+ // all instrumentation to short-circuit, see `./get-source-context.js`.
56
+ policy: null,
57
+ reqData: {
58
+ method: req.method,
59
+ uriPath,
60
+ queries,
61
+ },
62
+ };
59
63
 
60
- // check whether sampling allows processing
61
- ctx.sampleInfo = assess.sampler?.getSampleInfo(sourceData) ?? null;
62
- if (ctx.sampleInfo?.canSample === false) return ctx;
64
+ // check whether sampling allows processing
65
+ ctx.sampleInfo = assess.sampler?.getSampleInfo(sourceData) ?? null;
66
+ if (ctx.sampleInfo?.canSample === false) return ctx;
63
67
 
64
- // set policy - can be returned as `null` if request is url-excluded.
65
- ctx.policy = assess.getPolicy(ctx.reqData);
66
- if (!ctx.policy) return ctx;
68
+ // set policy - can be returned as `null` if request is url-excluded.
69
+ ctx.policy = assess.getPolicy(ctx.reqData);
70
+ if (!ctx.policy) return ctx;
67
71
 
68
- // build remaining reqData
69
- ctx.reqData.headers = { ...req.headers }; // copy to avoid storing tracked values
70
- ctx.reqData.ip = req.socket.remoteAddress;
71
- ctx.reqData.httpVersion = req.httpVersion;
72
- if (ctx.reqData.headers['content-type'])
73
- ctx.reqData.contentType = StringPrototypeToLowerCase.call(ctx.reqData.headers['content-type']);
72
+ // build remaining reqData
73
+ ctx.reqData.headers = { ...req.headers }; // copy to avoid storing tracked values
74
+ ctx.reqData.ip = req.socket.remoteAddress;
75
+ ctx.reqData.httpVersion = req.httpVersion;
76
+ if (ctx.reqData.headers['content-type'])
77
+ ctx.reqData.contentType = StringPrototypeToLowerCase.call(ctx.reqData.headers['content-type']);
74
78
 
75
- return {
76
- ...ctx,
77
- propagationEventsCount: 0,
78
- sourceEventsCount: 0,
79
- responseData: {},
80
- ruleState: {},
81
- };
82
- } catch (err) {
83
- logger.error(
84
- { err },
85
- 'unable to construct assess store. assess will be disabled for request.'
86
- );
87
- return null;
88
- }
89
- };
90
- };
79
+ return {
80
+ ...ctx,
81
+ propagationEventsCount: 0,
82
+ sourceEventsCount: 0,
83
+ responseData: {},
84
+ ruleState: {},
85
+ };
86
+ } catch (err) {
87
+ logger.error(
88
+ { err },
89
+ 'unable to construct assess store. assess will be disabled for request.'
90
+ );
91
+ return null;
92
+ }
93
+ };
94
+ }
95
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contrast/assess",
3
- "version": "1.48.0",
3
+ "version": "1.50.0",
4
4
  "description": "Contrast service providing framework-agnostic Assess support",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "author": "Contrast Security <nodejs@contrastsecurity.com> (https://www.contrastsecurity.com)",
@@ -20,17 +20,17 @@
20
20
  "test": "../scripts/test.sh"
21
21
  },
22
22
  "dependencies": {
23
- "@contrast/common": "1.30.0",
24
- "@contrast/config": "1.41.0",
25
- "@contrast/core": "1.46.0",
26
- "@contrast/dep-hooks": "1.15.0",
23
+ "@contrast/common": "1.32.0",
24
+ "@contrast/config": "1.43.0",
25
+ "@contrast/core": "1.48.0",
26
+ "@contrast/dep-hooks": "1.17.0",
27
27
  "@contrast/distringuish": "^5.1.0",
28
- "@contrast/instrumentation": "1.25.0",
29
- "@contrast/logger": "1.19.0",
30
- "@contrast/patcher": "1.18.0",
31
- "@contrast/rewriter": "1.22.0",
32
- "@contrast/route-coverage": "1.36.0",
33
- "@contrast/scopes": "1.16.0",
28
+ "@contrast/instrumentation": "1.27.0",
29
+ "@contrast/logger": "1.21.0",
30
+ "@contrast/patcher": "1.20.0",
31
+ "@contrast/rewriter": "1.24.0",
32
+ "@contrast/route-coverage": "1.38.0",
33
+ "@contrast/scopes": "1.18.0",
34
34
  "semver": "^7.6.0"
35
35
  }
36
36
  }