@contrast/assess 1.50.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
  });
@@ -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,
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
  }
@@ -34,6 +34,17 @@ module.exports = Core.makeComponent({
34
34
  */
35
35
  return core.assess.makeSourceContext = function(sourceData) {
36
36
  try {
37
+
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
+ }
47
+
37
48
  // todo: how to handle non-HTTP sources
38
49
  const { incomingMessage: req } = sourceData;
39
50
 
@@ -49,16 +60,10 @@ module.exports = Core.makeComponent({
49
60
  uriPath = req.url;
50
61
  queries = '';
51
62
  }
52
-
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
- },
63
+ ctx.reqData = {
64
+ method: req.method,
65
+ uriPath,
66
+ queries,
62
67
  };
63
68
 
64
69
  // check whether sampling allows processing
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contrast/assess",
3
- "version": "1.50.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)",
@@ -21,16 +21,16 @@
21
21
  },
22
22
  "dependencies": {
23
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",
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.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",
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
  }