@contrast/assess 1.51.0 → 1.52.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.
|
@@ -22,6 +22,7 @@ const {
|
|
|
22
22
|
StringPrototypeMatchAll,
|
|
23
23
|
StringPrototypeReplace,
|
|
24
24
|
StringPrototypeReplaceAll,
|
|
25
|
+
StringPrototypeSubstring
|
|
25
26
|
}
|
|
26
27
|
} = require('@contrast/common');
|
|
27
28
|
const {
|
|
@@ -34,6 +35,7 @@ const { patchType } = require('../../common');
|
|
|
34
35
|
module.exports = function(core) {
|
|
35
36
|
const {
|
|
36
37
|
patcher,
|
|
38
|
+
scopes,
|
|
37
39
|
assess: {
|
|
38
40
|
getPropagatorContext,
|
|
39
41
|
eventFactory: { createPropagationEvent },
|
|
@@ -85,8 +87,7 @@ module.exports = function(core) {
|
|
|
85
87
|
// aren't valid, e.g., $0, $<name> when the pattern is a string.
|
|
86
88
|
replacement = String(data._replacement);
|
|
87
89
|
|
|
88
|
-
const matches =
|
|
89
|
-
let trackedReplacementsDone = 0;
|
|
90
|
+
const matches = StringPrototypeMatchAll.call(replacement, patternIsRE ? RE_REPS : STR_REPS);
|
|
90
91
|
|
|
91
92
|
for (const m of matches) {
|
|
92
93
|
let substitution;
|
|
@@ -113,7 +114,7 @@ module.exports = function(core) {
|
|
|
113
114
|
substitutionDone = true;
|
|
114
115
|
} else if (m[1][0] === '<') {
|
|
115
116
|
// named group
|
|
116
|
-
const groupName = m[1]
|
|
117
|
+
const groupName = StringPrototypeSubstring.call(m[1], 1, m[1].length - 1);
|
|
117
118
|
if (parsedArgs.groups[groupName] === undefined && groupName in parsedArgs.groups) {
|
|
118
119
|
// remove $<groupName> from the replacement pattern. N.B. this
|
|
119
120
|
// also could mess up if the replacement text containts text that
|
|
@@ -130,6 +131,7 @@ module.exports = function(core) {
|
|
|
130
131
|
//
|
|
131
132
|
// any idea what order $&'` should be in from a most-common to least-
|
|
132
133
|
// common perspective? nfi.
|
|
134
|
+
let substitutionTags;
|
|
133
135
|
if (!substitutionDone) {
|
|
134
136
|
if (m[1] === '$') {
|
|
135
137
|
// this could actually be tracked but in order to do this "right"
|
|
@@ -137,15 +139,20 @@ module.exports = function(core) {
|
|
|
137
139
|
// the second $ that is tracked. so punt, and just ignore it, as
|
|
138
140
|
// originally implemented.
|
|
139
141
|
substitution = '$';
|
|
142
|
+
data._replacementOffset -= 1;
|
|
140
143
|
} else if (m[1] === '&') {
|
|
141
144
|
// replace these '$&' in parsedArgs[0] (replacerArgs[0], i.e., the
|
|
142
145
|
// match). if tracked, handle it. do so by index, so we don't have to
|
|
143
146
|
// call replace again? e.g., string[m.index..m.index+2]
|
|
144
147
|
substitution = parsedArgs[0];
|
|
145
148
|
} else if (m[1] === '`') {
|
|
146
|
-
|
|
149
|
+
const info = tracker.getData(parsedArgs.input);
|
|
150
|
+
substitution = StringPrototypeSubstring.call(parsedArgs.input, 0, parsedArgs.index);
|
|
151
|
+
substitutionTags = createSubsetTags(info.tags, parsedArgs.index, substitution.length);
|
|
147
152
|
} else if (m[1] === "'") {
|
|
148
|
-
|
|
153
|
+
const info = tracker.getData(parsedArgs.input);
|
|
154
|
+
substitution = StringPrototypeSubstring.call(parsedArgs.input, parsedArgs.index + parsedArgs[0].length);
|
|
155
|
+
substitutionTags = createSubsetTags(info.tags, parsedArgs.index, substitution.length);
|
|
149
156
|
} // else {
|
|
150
157
|
// throw new Error('how can it have matched RE and gotten here?');
|
|
151
158
|
// }
|
|
@@ -154,15 +161,13 @@ module.exports = function(core) {
|
|
|
154
161
|
// is manipulating the output, even if it is just duplicating what
|
|
155
162
|
// might be tracked or untracked input. does that count?
|
|
156
163
|
}
|
|
157
|
-
|
|
158
|
-
if (
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
replacement = StringPrototypeReplace.call(replacement, m[0], substitution);
|
|
164
|
+
replacement = StringPrototypeReplace.call(replacement, m[0], substitution);
|
|
165
|
+
if (!substitutionTags && tracker.getData(substitution)) substitutionTags = tracker.getData(substitution)?.tags;
|
|
166
|
+
if (substitutionTags) {
|
|
167
|
+
data._replacementTags = createAppendTags(data._replacementTags || {}, substitutionTags, m.index + data._replacementOffset);
|
|
168
|
+
data._replacementOffset += (substitution.length - m[0].length);
|
|
163
169
|
}
|
|
164
170
|
}
|
|
165
|
-
|
|
166
171
|
}
|
|
167
172
|
|
|
168
173
|
// coerce the replacement to a string, e.g., null => 'null'
|
|
@@ -173,7 +178,7 @@ module.exports = function(core) {
|
|
|
173
178
|
data._history.add(data._replacementInfo);
|
|
174
179
|
}
|
|
175
180
|
|
|
176
|
-
return { replacement,
|
|
181
|
+
return { replacement, replacementTags: data._replacementTags || data._replacementInfo?.tags };
|
|
177
182
|
}
|
|
178
183
|
|
|
179
184
|
function getReplacer(data) {
|
|
@@ -183,14 +188,14 @@ module.exports = function(core) {
|
|
|
183
188
|
const { index, input } = parsedArgs;
|
|
184
189
|
|
|
185
190
|
const { _accumOffset, _accumTags } = data;
|
|
186
|
-
const { replacement,
|
|
191
|
+
const { replacement, replacementTags } = getReplacementInfo(data, args, parsedArgs);
|
|
187
192
|
|
|
188
193
|
const preTags = createSubsetTags(_accumTags, 0, _accumOffset + index);
|
|
189
194
|
const postTags = createSubsetTags(_accumTags, _accumOffset + index + match.length, input.length - index - match.length);
|
|
190
195
|
data._accumOffset += (replacement.length - match.length);
|
|
191
|
-
if (preTags || postTags ||
|
|
196
|
+
if (preTags || postTags || replacementTags) {
|
|
192
197
|
data._accumTags = createAppendTags(
|
|
193
|
-
createAppendTags(preTags,
|
|
198
|
+
createAppendTags(preTags, replacementTags, _accumOffset + index),
|
|
194
199
|
postTags,
|
|
195
200
|
data._accumOffset + index + match.length
|
|
196
201
|
);
|
|
@@ -204,12 +209,13 @@ module.exports = function(core) {
|
|
|
204
209
|
return core.assess.dataflow.propagation.stringInstrumentation.replace = {
|
|
205
210
|
install() {
|
|
206
211
|
const name = 'String.prototype.replace';
|
|
212
|
+
const store = { name, lock: true };
|
|
207
213
|
patcher.patch(String.prototype, 'replace', {
|
|
208
214
|
name,
|
|
209
215
|
patchType,
|
|
210
216
|
usePerf: 'sync',
|
|
211
|
-
|
|
212
|
-
if (!getPropagatorContext()) return;
|
|
217
|
+
around(next, data) {
|
|
218
|
+
if (!getPropagatorContext()) return next();
|
|
213
219
|
|
|
214
220
|
// setup state
|
|
215
221
|
data._objInfo = tracker.getData(data.obj);
|
|
@@ -218,22 +224,20 @@ module.exports = function(core) {
|
|
|
218
224
|
data._history = data._objInfo ? new Set([data._objInfo]) : new Set();
|
|
219
225
|
data._accumTags = data._objInfo?.tags || {};
|
|
220
226
|
data._accumOffset = 0;
|
|
227
|
+
data._replacementOffset = 0;
|
|
221
228
|
|
|
222
229
|
data.args[1] = getReplacer(data);
|
|
223
|
-
|
|
224
|
-
|
|
230
|
+
|
|
231
|
+
const result = !scopes.instrumentation.isLocked() ? scopes.instrumentation.run(store, next) : next();
|
|
232
|
+
|
|
225
233
|
if (
|
|
226
|
-
!
|
|
227
|
-
// todo: can we reuse this optimization in other propagators? e.g those performing substring-like operations
|
|
234
|
+
!result ||
|
|
228
235
|
!data._accumTags?.[UNTRUSTED] ||
|
|
229
|
-
!!tracker.getData(
|
|
230
|
-
|
|
236
|
+
!!tracker.getData(result) ||
|
|
237
|
+
data.obj === result
|
|
238
|
+
) return result;
|
|
231
239
|
|
|
232
|
-
|
|
233
|
-
return;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
const { obj, args: origArgs, result, hooked, orig } = data;
|
|
240
|
+
const { obj, args: origArgs, hooked, orig } = data;
|
|
237
241
|
const args = [];
|
|
238
242
|
if (tracker.getData(origArgs[0])) {
|
|
239
243
|
args.push({ tracked: true, value: origArgs[0] });
|
|
@@ -274,9 +278,7 @@ module.exports = function(core) {
|
|
|
274
278
|
|
|
275
279
|
const { extern } = tracker.track(result, event);
|
|
276
280
|
|
|
277
|
-
|
|
278
|
-
data.result = extern;
|
|
279
|
-
}
|
|
281
|
+
return extern;
|
|
280
282
|
}
|
|
281
283
|
});
|
|
282
284
|
},
|
package/lib/event-factory.js
CHANGED
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
|
|
16
16
|
'use strict';
|
|
17
17
|
|
|
18
|
-
const { InputType, empties, primordials: { StringPrototypeMatch } } = require('@contrast/common');
|
|
18
|
+
const { Event, InputType, empties, primordials: { StringPrototypeMatch } } = require('@contrast/common');
|
|
19
|
+
const { ConfigSource } = require('@contrast/config');
|
|
19
20
|
const { Core } = require('@contrast/core/lib/ioc/core');
|
|
20
21
|
const ANNOTATION_REGEX = /^(A|O|R|P|P\d+)$/;
|
|
21
22
|
const SOURCE_EVENT_MSG = 'Source event not created: %s';
|
|
@@ -33,6 +34,15 @@ module.exports = Core.makeComponent({
|
|
|
33
34
|
|
|
34
35
|
const eventFactory = core.assess.eventFactory = {};
|
|
35
36
|
|
|
37
|
+
function computeStacktracesConfig() {
|
|
38
|
+
const forceSink = config.getEffectiveValue('server.environment') === 'PRODUCTION' &&
|
|
39
|
+
config.getEffectiveSource('assess.stacktraces') === ConfigSource.DEFAULT_VALUE;
|
|
40
|
+
|
|
41
|
+
return forceSink ? 'SINK' : config.getEffectiveValue('assess.stacktraces');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
eventFactory.stacktraces = computeStacktracesConfig();
|
|
45
|
+
|
|
36
46
|
eventFactory.createdEvents = new WeakSet();
|
|
37
47
|
|
|
38
48
|
eventFactory.createSourceEvent = function(data = {}) {
|
|
@@ -91,7 +101,7 @@ module.exports = Core.makeComponent({
|
|
|
91
101
|
return null;
|
|
92
102
|
}
|
|
93
103
|
|
|
94
|
-
if (
|
|
104
|
+
if (eventFactory.stacktraces === 'ALL') {
|
|
95
105
|
data.stack = createSnapshot(data.stacktraceOpts)();
|
|
96
106
|
} else {
|
|
97
107
|
data.stack = empties.ARRAY;
|
|
@@ -133,7 +143,7 @@ module.exports = Core.makeComponent({
|
|
|
133
143
|
return null;
|
|
134
144
|
}
|
|
135
145
|
|
|
136
|
-
if (
|
|
146
|
+
if (eventFactory.stacktraces !== 'NONE') {
|
|
137
147
|
data.stack = createSnapshot(data.stacktraceOpts)();
|
|
138
148
|
} else {
|
|
139
149
|
data.stack = empties.ARRAY;
|
|
@@ -161,7 +171,7 @@ module.exports = Core.makeComponent({
|
|
|
161
171
|
logger.debug('malformed or missing sink event source field: %s', data.name);
|
|
162
172
|
return null;
|
|
163
173
|
}
|
|
164
|
-
if (
|
|
174
|
+
if (eventFactory.stacktraces !== 'NONE') {
|
|
165
175
|
data.stack = createSnapshot(data.stacktraceOpts)();
|
|
166
176
|
} else {
|
|
167
177
|
data.stack = empties.ARRAY;
|
|
@@ -202,7 +212,7 @@ module.exports = Core.makeComponent({
|
|
|
202
212
|
logger.debug('malformed or missing sink event source field: %s', data.name);
|
|
203
213
|
return null;
|
|
204
214
|
}
|
|
205
|
-
if (
|
|
215
|
+
if (eventFactory.stacktraces !== 'NONE') {
|
|
206
216
|
data.stack = createSnapshot(data.stacktraceOpts)();
|
|
207
217
|
} else {
|
|
208
218
|
data.stack = empties.ARRAY;
|
|
@@ -214,6 +224,12 @@ module.exports = Core.makeComponent({
|
|
|
214
224
|
return data;
|
|
215
225
|
};
|
|
216
226
|
|
|
227
|
+
|
|
228
|
+
// update computed config value if there are updates
|
|
229
|
+
core.messages.on(Event.SERVER_SETTINGS_UPDATE, () => {
|
|
230
|
+
eventFactory.stacktraces = computeStacktracesConfig();
|
|
231
|
+
});
|
|
232
|
+
|
|
217
233
|
return eventFactory;
|
|
218
234
|
}
|
|
219
235
|
});
|
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
// @ts-check
|
|
16
16
|
'use strict';
|
|
17
17
|
|
|
18
|
+
const { Core } = require('@contrast/core/lib/ioc/core');
|
|
19
|
+
|
|
18
20
|
/** @typedef {import('@contrast/assess').SourceContext} SourceContext */
|
|
19
21
|
|
|
20
22
|
/**
|
|
@@ -25,7 +27,12 @@
|
|
|
25
27
|
* assess: import('@contrast/assess').Assess;
|
|
26
28
|
* }} core
|
|
27
29
|
*/
|
|
28
|
-
module.exports =
|
|
30
|
+
module.exports = Core.makeComponent({
|
|
31
|
+
name: 'assess.getSourceContext',
|
|
32
|
+
factory
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
function factory(core) {
|
|
29
36
|
const {
|
|
30
37
|
config,
|
|
31
38
|
scopes: { sources, instrumentation },
|
|
@@ -109,4 +116,4 @@ module.exports = function(core) {
|
|
|
109
116
|
|
|
110
117
|
return ctx;
|
|
111
118
|
};
|
|
112
|
-
}
|
|
119
|
+
}
|
package/lib/index.js
CHANGED
|
@@ -63,7 +63,7 @@ module.exports = function assess(core) {
|
|
|
63
63
|
require('./get-policy')(core);
|
|
64
64
|
core.initComponentSync(require('./make-source-context'));
|
|
65
65
|
require('./rule-scopes')(core);
|
|
66
|
-
require('./get-source-context')
|
|
66
|
+
core.initComponentSync(require('./get-source-context'));
|
|
67
67
|
core.initComponentSync(require('./event-factory'));
|
|
68
68
|
|
|
69
69
|
// various Assess features
|
|
@@ -26,75 +26,76 @@ const { Core } = require('@contrast/core/lib/ioc/core');
|
|
|
26
26
|
*/
|
|
27
27
|
module.exports = Core.makeComponent({
|
|
28
28
|
name: 'assess.makeSourceContext',
|
|
29
|
-
factory
|
|
30
|
-
|
|
29
|
+
factory,
|
|
30
|
+
});
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
*/
|
|
35
|
-
return core.assess.makeSourceContext = function(sourceData) {
|
|
36
|
-
try {
|
|
32
|
+
function factory(core) {
|
|
33
|
+
const { assess, logger } = core;
|
|
37
34
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
35
|
+
/**
|
|
36
|
+
* @returns {import('@contrast/assess').SourceContext}
|
|
37
|
+
*/
|
|
38
|
+
return core.assess.makeSourceContext = function(sourceData) {
|
|
39
|
+
try {
|
|
43
40
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
41
|
+
const ctx = sourceData.store.assess = {
|
|
42
|
+
// default policy to `null` until it is set later below. this will cause
|
|
43
|
+
// all instrumentation to short-circuit, see `./get-source-context.js`.
|
|
44
|
+
policy: null,
|
|
45
|
+
};
|
|
47
46
|
|
|
48
|
-
|
|
49
|
-
|
|
47
|
+
if (!core.config.getEffectiveValue('assess.enable')) {
|
|
48
|
+
return ctx;
|
|
49
|
+
}
|
|
50
50
|
|
|
51
|
-
|
|
52
|
-
|
|
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 = {
|
|
64
|
-
method: req.method,
|
|
65
|
-
uriPath,
|
|
66
|
-
queries,
|
|
67
|
-
};
|
|
51
|
+
// todo: how to handle non-HTTP sources
|
|
52
|
+
const { incomingMessage: req } = sourceData;
|
|
68
53
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
54
|
+
// minimally process the request data for sampling and exclusions.
|
|
55
|
+
// more request fields will be appended in final result below.
|
|
56
|
+
let uriPath;
|
|
57
|
+
let queries;
|
|
58
|
+
const idx = req.url.indexOf('?');
|
|
59
|
+
if (idx >= 0) {
|
|
60
|
+
uriPath = StringPrototypeSlice.call(req.url, 0, idx);
|
|
61
|
+
queries = StringPrototypeSlice.call(req.url, idx + 1);
|
|
62
|
+
} else {
|
|
63
|
+
uriPath = req.url;
|
|
64
|
+
queries = '';
|
|
65
|
+
}
|
|
66
|
+
ctx.reqData = {
|
|
67
|
+
method: req.method,
|
|
68
|
+
uriPath,
|
|
69
|
+
queries,
|
|
70
|
+
};
|
|
72
71
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
72
|
+
// check whether sampling allows processing
|
|
73
|
+
ctx.sampleInfo = assess.sampler?.getSampleInfo(sourceData) ?? null;
|
|
74
|
+
if (ctx.sampleInfo?.canSample === false) return ctx;
|
|
76
75
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
ctx.reqData.httpVersion = req.httpVersion;
|
|
81
|
-
if (ctx.reqData.headers['content-type'])
|
|
82
|
-
ctx.reqData.contentType = StringPrototypeToLowerCase.call(ctx.reqData.headers['content-type']);
|
|
76
|
+
// set policy - can be returned as `null` if request is url-excluded.
|
|
77
|
+
ctx.policy = assess.getPolicy(ctx.reqData);
|
|
78
|
+
if (!ctx.policy) return ctx;
|
|
83
79
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
80
|
+
// build remaining reqData
|
|
81
|
+
ctx.reqData.headers = { ...req.headers }; // copy to avoid storing tracked values
|
|
82
|
+
ctx.reqData.ip = req.socket.remoteAddress;
|
|
83
|
+
ctx.reqData.httpVersion = req.httpVersion;
|
|
84
|
+
if (ctx.reqData.headers['content-type'])
|
|
85
|
+
ctx.reqData.contentType = StringPrototypeToLowerCase.call(ctx.reqData.headers['content-type']);
|
|
86
|
+
|
|
87
|
+
ctx.propagationEventsCount = 0;
|
|
88
|
+
ctx.sourceEventsCount = 0;
|
|
89
|
+
ctx.responseData = {};
|
|
90
|
+
ctx.ruleState = {};
|
|
91
|
+
|
|
92
|
+
return ctx;
|
|
93
|
+
} catch (err) {
|
|
94
|
+
logger.error(
|
|
95
|
+
{ err },
|
|
96
|
+
'unable to construct assess store. assess will be disabled for request.'
|
|
97
|
+
);
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contrast/assess",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.52.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)",
|
|
@@ -21,16 +21,16 @@
|
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"@contrast/common": "1.32.0",
|
|
24
|
-
"@contrast/config": "1.
|
|
25
|
-
"@contrast/core": "1.
|
|
26
|
-
"@contrast/dep-hooks": "1.
|
|
24
|
+
"@contrast/config": "1.45.0",
|
|
25
|
+
"@contrast/core": "1.50.0",
|
|
26
|
+
"@contrast/dep-hooks": "1.19.0",
|
|
27
27
|
"@contrast/distringuish": "^5.1.0",
|
|
28
|
-
"@contrast/instrumentation": "1.
|
|
29
|
-
"@contrast/logger": "1.
|
|
30
|
-
"@contrast/patcher": "1.
|
|
31
|
-
"@contrast/rewriter": "1.
|
|
32
|
-
"@contrast/route-coverage": "1.
|
|
33
|
-
"@contrast/scopes": "1.
|
|
28
|
+
"@contrast/instrumentation": "1.29.0",
|
|
29
|
+
"@contrast/logger": "1.23.0",
|
|
30
|
+
"@contrast/patcher": "1.22.0",
|
|
31
|
+
"@contrast/rewriter": "1.26.0",
|
|
32
|
+
"@contrast/route-coverage": "1.40.0",
|
|
33
|
+
"@contrast/scopes": "1.20.0",
|
|
34
34
|
"semver": "^7.6.0"
|
|
35
35
|
}
|
|
36
36
|
}
|