@contrast/assess 1.49.0 → 1.51.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.
@@ -15,13 +15,14 @@
15
15
 
16
16
  'use strict';
17
17
 
18
- const { primordials: { ArrayPrototypeJoin } } = require('@contrast/common');
18
+ const { primordials: { ArrayPrototypeJoin, RegExpPrototypeExec } } = require('@contrast/common');
19
19
  const { createSubsetTags, getAdjustedUntrackedValue } = require('../../../tag-utils');
20
20
  const { patchType } = require('../../common');
21
21
 
22
22
  module.exports = function(core) {
23
23
  const {
24
24
  patcher,
25
+ scopes,
25
26
  assess: {
26
27
  getPropagatorContext,
27
28
  eventFactory,
@@ -32,13 +33,22 @@ module.exports = function(core) {
32
33
  return core.assess.dataflow.propagation.stringInstrumentation.split = {
33
34
  install() {
34
35
  const name = 'String.prototype.split';
36
+ const store = { lock: true, name };
35
37
 
36
38
  patcher.patch(String.prototype, 'split', {
37
39
  name,
38
40
  patchType,
39
41
  usePerf: 'sync',
42
+ around(next, data) {
43
+ // prevent instrumenting call to `exec` instance method when splitter is RegExp
44
+ return data.args[0] instanceof RegExp && !scopes.instrumentation.isLocked() ?
45
+ scopes.instrumentation.run(store, next) :
46
+ next();
47
+ },
40
48
  post(data) {
41
49
  const { name, args: origArgs, obj, result, hooked, orig } = data;
50
+ const splitterIsRx = origArgs[0] instanceof RegExp;
51
+
42
52
  if (
43
53
  !obj ||
44
54
  !result ||
@@ -46,68 +56,115 @@ module.exports = function(core) {
46
56
  result.length === 0 ||
47
57
  typeof obj !== 'string' ||
48
58
  (origArgs.length === 1 && origArgs[0] == null) ||
49
- !getPropagatorContext()
59
+ !getPropagatorContext() ||
60
+ (!splitterIsRx && origArgs[0]?.[Symbol.split])
50
61
  ) return;
51
62
 
52
63
  const objInfo = tracker.getData(obj);
53
64
  if (!objInfo || obj === result[0]) return;
54
65
 
55
- const args = origArgs.map((arg) => {
56
- const argInfo = tracker.getData(arg);
57
- return argInfo ?
58
- { tracked: true, value: argInfo.value } :
59
- { tracked: false, value: `'${arg}'` };
60
- });
61
-
62
- const event = eventFactory.createPropagationEvent({
63
- name,
64
- moduleName: 'String',
65
- methodName: 'prototype.split',
66
- context: `'${objInfo.value}'.split(${ArrayPrototypeJoin.call(args.map(a => a.value))})`,
67
- history: [objInfo],
68
- object: {
69
- value: obj,
70
- tracked: true,
71
- },
72
- args,
73
- tags: {},
74
- result: {
75
- value: getAdjustedUntrackedValue(result),
76
- tracked: false
77
- },
78
- stacktraceOpts: {
79
- constructorOpt: hooked,
80
- prependFrames: [orig]
81
- },
82
- source: 'O',
83
- target: 'R'
84
- });
85
-
86
- if (!event) return;
87
-
88
- let idx = 0;
66
+ let readIdx = 0;
67
+ let splitterRxGlobal;
68
+ let sepOffset;
69
+ let rxMatch;
70
+ let _event;
71
+
72
+ // make single global regex to check for calculating sepOffsets (vs using origArgs[0])
73
+ if (origArgs[0] instanceof RegExp) splitterRxGlobal = new RegExp(origArgs[0], 'g');
74
+
89
75
  for (let i = 0; i < result.length; i++) {
90
- const res = result[i];
91
- const start = obj.indexOf(res, idx);
92
- idx += res.length;
93
- const objSubstr = obj.substring(start, start + res.length);
94
- const objSubstrInfo = tracker.getData(objSubstr);
95
- if (objSubstrInfo) {
96
- const tags = createSubsetTags(objInfo.tags, start, res.length);
97
-
98
- if (!tags) continue;
99
- const metadata = {
100
- ...event,
101
- result: { tracked: true, value: res },
102
- tags,
103
- };
104
- eventFactory.createdEvents.add(metadata);
105
- const { extern } = tracker.track(res, metadata);
106
-
107
- if (extern) {
108
- data.result[i] = extern;
76
+ let tags;
77
+ if (result[i].length) {
78
+ tags = createSubsetTags(objInfo.tags, readIdx, result[i].length);
79
+ }
80
+
81
+ if (tags) {
82
+ const metadata = makeEvent({
83
+ result: { tracked: true, value: result[i] },
84
+ tags: tags,
85
+ });
86
+
87
+ if (metadata) {
88
+ eventFactory.createdEvents.add(metadata);
89
+ const { extern } = tracker.track(result[i], metadata);
90
+ if (extern) data.result[i] = extern;
91
+ }
92
+ }
93
+
94
+ // increment read offset from element length
95
+ readIdx += result[i].length;
96
+
97
+ // calculate separator offset but don't increment until
98
+ // we check for regex capture groups in result below
99
+ if (splitterRxGlobal) {
100
+ rxMatch = RegExpPrototypeExec.call(splitterRxGlobal, obj);
101
+ if (rxMatch) sepOffset = rxMatch[0].length;
102
+ } else {
103
+ sepOffset = origArgs[0].length;
104
+ }
105
+
106
+ // handle if any regex matches are interleaved into the result
107
+ if (rxMatch) {
108
+ let groupIdx = rxMatch.length > 1 ? 1 : 0;
109
+
110
+ while (groupIdx && groupIdx < rxMatch.length) {
111
+ // move to next element in result
112
+ i++;
113
+
114
+ const tags = createSubsetTags(objInfo.tags, readIdx, rxMatch[groupIdx].length);
115
+ if (tags) {
116
+ const metadata = makeEvent({
117
+ result: { tracked: true, value: result[i] },
118
+ tags: tags,
119
+ });
120
+ eventFactory.createdEvents.add(metadata);
121
+ const { extern } = tracker.track(result[i], metadata);
122
+ if (extern) data.result[i] = extern;
123
+ }
124
+ // increment read offset using rxMatch[groupIdx].length instead of sepOffset
125
+ readIdx += rxMatch[groupIdx].length;
126
+ groupIdx++;
109
127
  }
128
+ } else {
129
+ readIdx += sepOffset;
130
+ }
131
+ }
132
+
133
+ // Defer base event creation until needed. All events will share same
134
+ // common base event data that gets cached per propagator run.
135
+ function makeEvent(mergeData) {
136
+ if (!_event) {
137
+ const args = origArgs.map((arg) => {
138
+ const argInfo = tracker.getData(arg);
139
+ return argInfo ?
140
+ { tracked: true, value: argInfo.value } :
141
+ { tracked: false, value: `'${arg}'` };
142
+ });
143
+ _event = eventFactory.createPropagationEvent({
144
+ name,
145
+ moduleName: 'String',
146
+ methodName: 'prototype.split',
147
+ context: `'${objInfo.value}'.split(${ArrayPrototypeJoin.call(args.map(a => a.value))})`,
148
+ history: [objInfo],
149
+ object: {
150
+ value: obj,
151
+ tracked: true,
152
+ },
153
+ args,
154
+ tags: {},
155
+ result: {
156
+ value: getAdjustedUntrackedValue(result),
157
+ tracked: false
158
+ },
159
+ stacktraceOpts: {
160
+ constructorOpt: hooked,
161
+ prependFrames: [orig]
162
+ },
163
+ source: 'O',
164
+ target: 'R'
165
+ });
110
166
  }
167
+ return !_event ? null : { ..._event, ...mergeData };
111
168
  }
112
169
  },
113
170
  });
@@ -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);
@@ -74,6 +74,9 @@ module.exports = function init(core) {
74
74
  name: 'express.middleware.init.expressInit',
75
75
  patchType,
76
76
  pre(data) {
77
+ // no need to patch is assess is off
78
+ if (!core.config.getEffectiveValue('assess.enable')) return;
79
+
77
80
  patcher.patch(data.args, '2', {
78
81
  name: 'express.middleware.init.expressInit.next',
79
82
  patchType,
@@ -16,200 +16,204 @@
16
16
  'use strict';
17
17
 
18
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 = {}) {
36
- if (!data.result?.value) {
37
- logger.debug(SOURCE_EVENT_MSG, `invalid result: ${data.name}`);
38
- return null;
39
- }
40
- if (!data.name) {
41
- logger.debug(SOURCE_EVENT_MSG, `invalid name: ${data.name}`);
42
- return null;
43
- }
44
- if (!(data.inputType in InputType)) {
45
- logger.debug(SOURCE_EVENT_MSG, `invalid inputType: ${data.name}`);
46
- return null;
47
- }
48
- if (!data.tags) {
49
- logger.debug(SOURCE_EVENT_MSG, `event has no tags: ${data.name}`);
50
- return null;
51
- }
52
- if (!data.stack || !Array.isArray(data.stack)) {
53
- logger.debug(SOURCE_EVENT_MSG, `invalid stack: ${data.name}`);
54
- return null;
55
- }
56
-
57
- data.time = Date.now();
58
- eventFactory.createdEvents.add(data);
59
-
60
- return data;
61
- };
62
-
63
- eventFactory.createPropagationEvent = function (data) {
64
- const sourceContext = sources.getStore()?.assess;
65
-
66
- if (!sourceContext) {
67
- logger.debug('No sourceContext found during Propagation event creation: %s', data.name);
68
- return null;
69
- }
70
- if (sourceContext.propagationEventsCount >= config.assess.max_propagation_events) {
71
- logger.debug('Maximum number of Propagation events reached. Event not created: %s', data.name);
72
- return null;
73
- }
74
- if (!data.name) {
75
- logger.debug(PROPAGATION_EVENT_MSG, `invalid name (${data.name})`);
76
- return null;
77
- }
78
- if (!data.history.length) {
79
- logger.debug(PROPAGATION_EVENT_MSG, `invalid history (${data.name})`);
80
- return null;
81
- }
82
- if (!data.source || !StringPrototypeMatch.call(data.source, ANNOTATION_REGEX)) {
83
- logger.debug(PROPAGATION_EVENT_MSG, `invalid source (${data.name})`);
84
- return null;
85
- }
86
- if (!data.target || !StringPrototypeMatch.call(data.target, ANNOTATION_REGEX)) {
87
- logger.debug(PROPAGATION_EVENT_MSG, `invalid target (${data.name})`);
88
- return null;
89
- }
90
-
91
- if (config.assess.stacktraces === 'ALL') {
92
- data.stack = createSnapshot(data.stacktraceOpts)();
93
- } else {
94
- data.stack = empties.ARRAY;
95
- }
96
-
97
- data.args ??= empties.ARRAY;
98
- data.addedTags ??= empties.ARRAY;
99
- data.history ??= empties.ARRAY;
100
- data.object ??= empties.UNTRACKED_VALUE_OBJ;
101
- data.result ??= empties.UNTRACKED_VALUE_OBJ;
102
- data.removedTags ??= empties.ARRAY;
103
- data.time = Date.now();
104
-
105
- eventFactory.createdEvents.add(data);
106
- sourceContext.propagationEventsCount++;
107
-
108
- return data;
109
- };
110
-
111
- eventFactory.createSinkEvent = function (data) {
112
- const sourceContext = sources.getStore()?.assess;
113
-
114
- if (!sourceContext) {
115
- logger.debug('no sourceContext found during sink event creation: %s', data.name);
116
- return null;
117
- }
118
- if (!data.name) {
119
- logger.debug('no sink event name: %s', data.name);
120
- return null;
121
- }
122
- if (!data.history.length) {
123
- logger.debug('empty history for sink event: %s', data.name);
124
- return null;
125
- }
126
- if (
127
- (!data.source || !StringPrototypeMatch.call(data.source, ANNOTATION_REGEX))
128
- ) {
129
- logger.debug('malformed or missing sink event source field: %s', data.name);
130
- return null;
131
- }
132
-
133
- if (config.assess.stacktraces !== 'NONE') {
134
- data.stack = createSnapshot(data.stacktraceOpts)();
135
- } else {
136
- data.stack = empties.ARRAY;
137
- }
138
-
139
- data.args ??= empties.ARRAY;
140
- data.history ??= empties.ARRAY;
141
- data.object ??= empties.ARRAY;
142
- data.result ??= empties.UNTRACKED_VALUE_OBJ;
143
- data.time = Date.now();
144
-
145
- eventFactory.createdEvents.add(data);
146
-
147
- return data;
148
- };
149
-
150
- eventFactory.createSessionEvent = function (data) {
151
- if (!data.name) {
152
- logger.debug('no sink event name: %s', data.name);
153
- return null;
154
- }
155
- if (
156
- (!data.source || !StringPrototypeMatch.call(data.source, ANNOTATION_REGEX))
157
- ) {
158
- logger.debug('malformed or missing sink event source field: %s', data.name);
159
- return null;
160
- }
161
- if (config.assess.stacktraces !== 'NONE') {
162
- data.stack = createSnapshot(data.stacktraceOpts)();
163
- } else {
164
- data.stack = empties.ARRAY;
165
- }
166
-
167
- data.args ??= empties.ARRAY;
168
- data.history ??= empties.ARRAY;
169
- data.obj ??= empties.UNTRACKED_VALUE_OBJ;
170
- data.result ??= empties.UNTRACKED_VALUE_OBJ;
171
- data.tags ??= empties.OBJECT;
172
- data.time = Date.now();
173
-
174
- eventFactory.createdEvents.add(data);
175
-
176
- return data;
177
- };
178
-
179
- /**
180
- * @param {{
181
- * context: string,
182
- * name: string,
183
- * moduleName: string,
184
- * methodName: string,
185
- * object: { value: any, tracked: boolean },
186
- * args: any[],
187
- * result: { value: vany, tracked: boolean },
188
- * source: string,
189
- * stacktraceOpts: { constructorOpt?: Function},
190
- * }} data
191
- * @returns {any}
192
- */
193
- eventFactory.createCryptoAnalysisEvent = function (data) {
194
- if (!data.name) {
195
- logger.debug('no sink event name: %s', data.name);
196
- return null;
197
- }
198
- if (!data.source || !StringPrototypeMatch.call(data.source, ANNOTATION_REGEX)) {
199
- logger.debug('malformed or missing sink event source field: %s', data.name);
200
- return null;
201
- }
202
- if (config.assess.stacktraces !== 'NONE') {
203
- data.stack = createSnapshot(data.stacktraceOpts)();
204
- } else {
205
- data.stack = empties.ARRAY;
206
- }
207
-
208
- data.time = Date.now();
209
- eventFactory.createdEvents.add(data);
210
-
211
- return data;
212
- };
213
-
214
- return eventFactory;
215
- };
24
+ module.exports = Core.makeComponent({
25
+ name: 'assess.eventFactory',
26
+ factory(core) {
27
+ const {
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;
64
+ };
65
+
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;
112
+ };
113
+
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;
151
+ };
152
+
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
+ };
181
+
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
+ };
216
+
217
+ return eventFactory;
218
+ }
219
+ });
package/lib/index.js CHANGED
@@ -17,6 +17,7 @@
17
17
 
18
18
  const { inspect } = require('util');
19
19
  const { callChildComponentMethodsSync } = require('@contrast/common');
20
+ const { ConfigSource } = require('@contrast/config');
20
21
 
21
22
  module.exports = function assess(core) {
22
23
  const {
@@ -27,7 +28,15 @@ module.exports = function assess(core) {
27
28
 
28
29
  const assess = core.assess = {
29
30
  install() {
30
- if (!config.getEffectiveValue('assess.enable')) {
31
+ // only force instrumentation if assess is explicitly enabled in local config
32
+ const forceInstrumentation =
33
+ config.preinstrument &&
34
+ config.getEffectiveSource('assess.enable') !== ConfigSource.DEFAULT_VALUE;
35
+
36
+ if (
37
+ !forceInstrumentation &&
38
+ !config.getEffectiveValue('assess.enable')
39
+ ) {
31
40
  core.logger.debug('assess is disabled, skipping installation');
32
41
  return;
33
42
  }
@@ -52,10 +61,10 @@ module.exports = function assess(core) {
52
61
  // ancillary tools used by different features
53
62
  require('./sampler')(core);
54
63
  require('./get-policy')(core);
55
- require('./make-source-context')(core);
64
+ core.initComponentSync(require('./make-source-context'));
56
65
  require('./rule-scopes')(core);
57
66
  require('./get-source-context')(core);
58
- require('./event-factory')(core);
67
+ core.initComponentSync(require('./event-factory'));
59
68
 
60
69
  // various Assess features
61
70
  require('./dataflow')(core);
@@ -16,75 +16,85 @@
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 {
35
37
 
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
- }
38
+ const ctx = sourceData.store.assess = {
39
+ // default policy to `null` until it is set later below. this will cause
40
+ // all instrumentation to short-circuit, see `./get-source-context.js`.
41
+ policy: null,
42
+ };
43
+
44
+ if (!core.config.getEffectiveValue('assess.enable')) {
45
+ return ctx;
46
+ }
48
47
 
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: {
48
+ // todo: how to handle non-HTTP sources
49
+ const { incomingMessage: req } = sourceData;
50
+
51
+ // minimally process the request data for sampling and exclusions.
52
+ // more request fields will be appended in final result below.
53
+ let uriPath;
54
+ let queries;
55
+ const idx = req.url.indexOf('?');
56
+ if (idx >= 0) {
57
+ uriPath = StringPrototypeSlice.call(req.url, 0, idx);
58
+ queries = StringPrototypeSlice.call(req.url, idx + 1);
59
+ } else {
60
+ uriPath = req.url;
61
+ queries = '';
62
+ }
63
+ ctx.reqData = {
54
64
  method: req.method,
55
65
  uriPath,
56
66
  queries,
57
- },
58
- };
67
+ };
59
68
 
60
- // check whether sampling allows processing
61
- ctx.sampleInfo = assess.sampler?.getSampleInfo(sourceData) ?? null;
62
- if (ctx.sampleInfo?.canSample === false) return ctx;
69
+ // check whether sampling allows processing
70
+ ctx.sampleInfo = assess.sampler?.getSampleInfo(sourceData) ?? null;
71
+ if (ctx.sampleInfo?.canSample === false) return ctx;
63
72
 
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;
73
+ // set policy - can be returned as `null` if request is url-excluded.
74
+ ctx.policy = assess.getPolicy(ctx.reqData);
75
+ if (!ctx.policy) return ctx;
67
76
 
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']);
77
+ // build remaining reqData
78
+ ctx.reqData.headers = { ...req.headers }; // copy to avoid storing tracked values
79
+ ctx.reqData.ip = req.socket.remoteAddress;
80
+ ctx.reqData.httpVersion = req.httpVersion;
81
+ if (ctx.reqData.headers['content-type'])
82
+ ctx.reqData.contentType = StringPrototypeToLowerCase.call(ctx.reqData.headers['content-type']);
74
83
 
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
- };
84
+ return {
85
+ ...ctx,
86
+ propagationEventsCount: 0,
87
+ sourceEventsCount: 0,
88
+ responseData: {},
89
+ ruleState: {},
90
+ };
91
+ } catch (err) {
92
+ logger.error(
93
+ { err },
94
+ 'unable to construct assess store. assess will be disabled for request.'
95
+ );
96
+ return null;
97
+ }
98
+ };
99
+ }
100
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contrast/assess",
3
- "version": "1.49.0",
3
+ "version": "1.51.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.31.0",
24
- "@contrast/config": "1.42.0",
25
- "@contrast/core": "1.47.0",
26
- "@contrast/dep-hooks": "1.16.0",
23
+ "@contrast/common": "1.32.0",
24
+ "@contrast/config": "1.44.0",
25
+ "@contrast/core": "1.49.0",
26
+ "@contrast/dep-hooks": "1.18.0",
27
27
  "@contrast/distringuish": "^5.1.0",
28
- "@contrast/instrumentation": "1.26.0",
29
- "@contrast/logger": "1.20.0",
30
- "@contrast/patcher": "1.19.0",
31
- "@contrast/rewriter": "1.23.0",
32
- "@contrast/route-coverage": "1.37.0",
33
- "@contrast/scopes": "1.17.0",
28
+ "@contrast/instrumentation": "1.28.0",
29
+ "@contrast/logger": "1.22.0",
30
+ "@contrast/patcher": "1.21.0",
31
+ "@contrast/rewriter": "1.25.0",
32
+ "@contrast/route-coverage": "1.39.0",
33
+ "@contrast/scopes": "1.19.0",
34
34
  "semver": "^7.6.0"
35
35
  }
36
36
  }