@contrast/assess 1.58.2 → 1.59.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.
@@ -24,12 +24,35 @@ module.exports = function(core) {
24
24
  patcher,
25
25
  depHooks,
26
26
  assess: {
27
+ inspect,
27
28
  getPropagatorContext,
28
29
  eventFactory: { createPropagationEvent },
29
30
  dataflow: { tracker }
30
31
  }
31
32
  } = core;
32
33
 
34
+ function traverseObject(obj, result, tags, history, depth = 1) {
35
+ let i = 0;
36
+ for (const val of Object.values(obj)) {
37
+
38
+ if (typeof val === 'object' && depth <= 4) tags = traverseObject(val, result, tags, history, depth += 1);
39
+
40
+ const valInfo = tracker.getData(val);
41
+ if (!valInfo || depth > 4) break;
42
+
43
+ const currIdx = result.indexOf(val, i);
44
+ if (currIdx > -1) {
45
+ i = currIdx + val.length;
46
+ } else {
47
+ break;
48
+ }
49
+ tags = createAppendTags(tags, valInfo.tags, currIdx);
50
+ history.push({ ...valInfo });
51
+ }
52
+
53
+ return tags;
54
+ }
55
+
33
56
  return core.assess.dataflow.propagation.utilFormat = {
34
57
  install() {
35
58
  depHooks.resolve({ name: 'util', version: '*' }, (util) => {
@@ -57,13 +80,14 @@ module.exports = function(core) {
57
80
 
58
81
  for (i; i < args.length; i++) {
59
82
  let arg = args[i];
83
+ if (!arg) continue;
84
+
60
85
  const formatChar = formatChars[i - 1];
61
86
  if (formatChar) {
62
87
  switch (formatChar) {
63
88
  case 's':
64
89
  if (typeof arg === 'object') {
65
- // util.inspect instrumentation NYI
66
- arg = arg?.toString ? arg.toString() : util.inspect(arg, { depth: 0, colors: false, compact: 3 });
90
+ break; // handled below
67
91
  } else {
68
92
  arg = String(arg);
69
93
  }
@@ -77,36 +101,35 @@ module.exports = function(core) {
77
101
  arg = JSON.stringify(arg) ?? 'undefined';
78
102
  break;
79
103
  case 'o':
80
- // util.inspect instrumentation NYI
81
- arg = util.inspect(arg, { showHidden: true, showProxy: true });
82
- break;
104
+ break; // handled below
83
105
  case 'O':
84
- // util.inspect instrumentation NYI
85
- arg = util.inspect(arg);
86
- break;
106
+ break; // handled below
87
107
  case 'c':
88
108
  // c is ignored and skipped
89
109
  arg = '';
90
110
  break;
91
111
  }
92
112
  } else if (typeof arg !== 'string') {
93
- arg = util.inspect(arg);
113
+ arg = inspect(arg);
94
114
  }
95
115
 
96
- const argInfo = tracker.getData(arg);
97
- if (!argInfo) continue;
116
+ if (typeof arg === 'string') {
117
+ const argInfo = tracker.getData(arg);
118
+ if (!argInfo) continue;
98
119
 
99
- const currIdx = result.indexOf(arg, idx);
100
- if (currIdx > -1) {
101
- idx = currIdx + arg.length;
102
- } else {
103
- continue;
120
+ const currIdx = result.indexOf(arg, idx);
121
+ if (currIdx > -1) {
122
+ idx = currIdx + arg.length;
123
+ } else {
124
+ continue;
125
+ }
126
+ newTags = createAppendTags(newTags, argInfo.tags, currIdx);
127
+ history.push({ ...argInfo });
128
+ eventArgs.push({ value: argInfo ? argInfo.value : arg, tracked: !!argInfo });
129
+ } else if (typeof arg === 'object') {
130
+ newTags = traverseObject(arg, result, newTags, history);
131
+ eventArgs.push({ value: inspect(arg), tracked: false });
104
132
  }
105
-
106
- newTags = createAppendTags(newTags, argInfo.tags, currIdx);
107
-
108
- history.push({ ...argInfo });
109
- eventArgs.push({ value: argInfo ? argInfo.value : arg, tracked: !!argInfo });
110
133
  }
111
134
 
112
135
  const resultInfo = tracker.getData(result);
@@ -85,7 +85,7 @@ module.exports = function init(core) {
85
85
  },
86
86
  });
87
87
 
88
- sourceContext.parsedBody = !!Object.keys(_data).length;
88
+ sourceContext.parsedBody = !!(_data && Object.keys(_data).length);
89
89
  } catch (err) {
90
90
  logger.error({ err, funcKey: data.funcKey }, 'unable to handle source');
91
91
  }
@@ -23,6 +23,7 @@ module.exports = (core) => {
23
23
  depHooks,
24
24
  patcher,
25
25
  logger,
26
+ scopes,
26
27
  assess: {
27
28
  getSourceContext,
28
29
  dataflow: { sources }
@@ -51,7 +52,8 @@ module.exports = (core) => {
51
52
  }
52
53
 
53
54
  data.args[1] = async function contrastNext(origErr) {
54
- const inputType = sourceContext.reqData.headers?.['content-type']?.includes('/json')
55
+ const contentType = scopes.sources.getStore()?.sourceInfo?.contentType;
56
+ const inputType = contentType?.includes?.('/json')
55
57
  ? InputType.JSON_VALUE
56
58
  : typeof ctx.request.body == 'object'
57
59
  ? InputType.PARAMETER_VALUE
@@ -23,6 +23,7 @@ module.exports = (core) => {
23
23
  depHooks,
24
24
  patcher,
25
25
  logger,
26
+ scopes,
26
27
  assess: {
27
28
  getSourceContext,
28
29
  dataflow: { sources }
@@ -38,21 +39,20 @@ module.exports = (core) => {
38
39
  patchType,
39
40
  post({ args, hooked, orig, result, funcKey }) {
40
41
  const sourceContext = getSourceContext();
41
-
42
- if (!sourceContext) {
43
- return;
44
- }
42
+ if (!sourceContext) return;
45
43
 
46
44
  if (sourceContext.parsedQuery) {
47
45
  logger.trace({ inputType, funcKey }, 'values already tracked');
48
46
  return;
49
47
  }
50
48
 
49
+ const queries = scopes.sources.getStore()?.sourceInfo?.queries;
50
+
51
51
  // We need to run analysis for the `qs` result only when it's used as a query parser.
52
52
  // `qs` is used also for parsing bodies, but these cases we handle individually with
53
53
  // the respective library that's using it (e.g. `formidable`, `co-body`) because in
54
54
  // some cases its use is optional and we cannot rely on it.
55
- if (sourceContext.reqData?.queries === args[0]) {
55
+ if (queries === args[0]) {
56
56
  try {
57
57
  sources.handle({
58
58
  context: 'req.query',
@@ -24,6 +24,7 @@ module.exports = (core) => {
24
24
  depHooks,
25
25
  patcher,
26
26
  logger,
27
+ scopes,
27
28
  } = core;
28
29
 
29
30
  core.assess.dataflow.sources.querystringInstrumentation = {
@@ -46,7 +47,7 @@ module.exports = (core) => {
46
47
 
47
48
  // We only run analysis for the `querystring` result when it's used
48
49
  // as the framework's query parser
49
- if (sourceContext.reqData?.queries === args[0]) {
50
+ if (scopes.sources.getStore().sourceInfo?.queries === args[0]) {
50
51
  try {
51
52
  core.assess.dataflow.sources.handle({
52
53
  context: 'req.query',
package/lib/index.d.ts CHANGED
@@ -38,7 +38,6 @@ export interface Core extends _Core {
38
38
  }
39
39
 
40
40
  export interface SourceContext {
41
- reqData: object,
42
41
  responseData: {
43
42
  contentType: string,
44
43
  },
@@ -15,7 +15,6 @@
15
15
 
16
16
  'use strict';
17
17
 
18
- const { primordials: { StringPrototypeToLowerCase, StringPrototypeSlice } } = require('@contrast/common');
19
18
  const { Core } = require('@contrast/core/lib/ioc/core');
20
19
 
21
20
  /**
@@ -33,57 +32,28 @@ function factory(core) {
33
32
  const { assess, logger } = core;
34
33
 
35
34
  /**
35
+ * todo: how to handle non-HTTP sources
36
36
  * @returns {import('@contrast/assess').SourceContext}
37
37
  */
38
- return core.assess.makeSourceContext = function(sourceData) {
39
- try {
38
+ return core.assess.makeSourceContext = function ({ store, incomingMessage: req }) {
40
39
 
41
- const ctx = sourceData.store.assess = {
40
+ try {
41
+ const ctx = store.assess = {
42
42
  // default policy to `null` until it is set later below. this will cause
43
43
  // all instrumentation to short-circuit, see `./get-source-context.js`.
44
44
  policy: null,
45
45
  };
46
46
 
47
- if (!core.config.getEffectiveValue('assess.enable')) {
48
- return ctx;
49
- }
50
-
51
- // todo: how to handle non-HTTP sources
52
- const { incomingMessage: req } = sourceData;
53
-
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
- };
47
+ if (!core.config.getEffectiveValue('assess.enable')) return ctx;
71
48
 
72
49
  // check whether sampling allows processing
73
- ctx.sampleInfo = assess.sampler?.getSampleInfo(sourceData) ?? null;
50
+ ctx.sampleInfo = assess.sampler?.getSampleInfo(store.sourceInfo) ?? null;
74
51
  if (ctx.sampleInfo?.canSample === false) return ctx;
75
52
 
76
53
  // set policy - can be returned as `null` if request is url-excluded.
77
- ctx.policy = assess.getPolicy(ctx.reqData);
54
+ ctx.policy = assess.getPolicy(store.sourceInfo);
78
55
  if (!ctx.policy) return ctx;
79
56
 
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
57
  ctx.propagationEventsCount = 0;
88
58
  ctx.sourceEventsCount = 0;
89
59
  ctx.responseData = {};
@@ -31,23 +31,22 @@ class RouteAnalysisMonitor {
31
31
  }
32
32
 
33
33
  /**
34
- * @param {object} reqData
35
- * @param {string} reqData.uriPath
34
+ * @param {import('@contrast/common').SourceInfo} sourceInfo
35
+ * @param {string} sourceInfo.normalizedUri
36
36
  * @returns {AnalysisInfo}
37
37
  */
38
- getAnalysisInfo({ method, uriPath }) {
39
- const normalizedUrl = this._core.routeCoverage.uriPathToNormalizedUrl(uriPath);
38
+ getAnalysisInfo({ method, normalizedUri }) {
40
39
  const now = Date.now();
41
40
 
42
- if (normalizedUrl) {
43
- const key = `${method}:${normalizedUrl}`;
41
+ if (normalizedUri) {
42
+ const key = `${method}:${normalizedUri}`;
44
43
  let routeMeta = this._normalCache.get(key);
45
44
 
46
45
  // not in cache, not paused
47
46
  if (!routeMeta) {
48
47
  routeMeta = {
49
48
  pauseEnd: now + this._ttl,
50
- normalizedUrl,
49
+ normalizedUrl: normalizedUri,
51
50
  };
52
51
  this._normalCache.set(key, routeMeta);
53
52
 
@@ -64,8 +63,6 @@ class RouteAnalysisMonitor {
64
63
 
65
64
  // was in cache and still paused
66
65
  return { paused: true, ...routeMeta };
67
- } else {
68
- // todo - handle "dynamic" routes
69
66
  }
70
67
 
71
68
  return this._defaultAnalysisInfo;
@@ -105,7 +102,6 @@ class ProbabilisticSampler extends BaseSampler {
105
102
 
106
103
  getSampleInfo(sourceInfo) {
107
104
  const { baseline, base_probability } = this.opts;
108
- const { reqData } = sourceInfo.store.assess;
109
105
 
110
106
  if (this.baseline < baseline) {
111
107
  this.baseline++;
@@ -119,7 +115,7 @@ class ProbabilisticSampler extends BaseSampler {
119
115
 
120
116
  // check route monitoring before sampling
121
117
  if (canSample) {
122
- const routeInfo = this.routeMonitor?.getAnalysisInfo(reqData);
118
+ const routeInfo = this.routeMonitor?.getAnalysisInfo(sourceInfo);
123
119
 
124
120
  if (routeInfo) {
125
121
  // don't sample if analysis is paused
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contrast/assess",
3
- "version": "1.58.2",
3
+ "version": "1.59.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,18 @@
20
20
  "test": "bash ../scripts/test.sh"
21
21
  },
22
22
  "dependencies": {
23
- "@contrast/common": "1.34.2",
24
- "@contrast/config": "1.49.2",
25
- "@contrast/core": "1.54.2",
26
- "@contrast/dep-hooks": "1.23.2",
23
+ "@contrast/common": "1.35.0",
24
+ "@contrast/config": "1.50.0",
25
+ "@contrast/core": "1.55.0",
26
+ "@contrast/dep-hooks": "1.24.0",
27
27
  "@contrast/distringuish": "^5.1.0",
28
- "@contrast/instrumentation": "1.33.2",
29
- "@contrast/logger": "1.27.2",
30
- "@contrast/patcher": "1.26.2",
31
- "@contrast/rewriter": "1.30.2",
32
- "@contrast/route-coverage": "1.45.2",
33
- "@contrast/scopes": "1.24.2",
28
+ "@contrast/instrumentation": "1.34.0",
29
+ "@contrast/logger": "1.28.0",
30
+ "@contrast/patcher": "1.27.0",
31
+ "@contrast/rewriter": "1.31.0",
32
+ "@contrast/route-coverage": "1.46.0",
33
+ "@contrast/scopes": "1.25.0",
34
+ "@contrast/sources": "1.1.0",
34
35
  "semver": "^7.6.0"
35
36
  }
36
37
  }