@contrast/agent-bundle 5.45.1 → 5.46.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.
Files changed (56) hide show
  1. package/node_modules/@contrast/agent/package.json +10 -10
  2. package/node_modules/@contrast/agentify/package.json +14 -14
  3. package/node_modules/@contrast/architecture-components/package.json +4 -4
  4. package/node_modules/@contrast/assess/lib/dataflow/sources/handler.js +21 -24
  5. package/node_modules/@contrast/assess/lib/get-source-context.js +10 -21
  6. package/node_modules/@contrast/assess/lib/index.js +1 -1
  7. package/node_modules/@contrast/assess/lib/make-source-context.js +5 -10
  8. package/node_modules/@contrast/assess/lib/policy.js +400 -0
  9. package/node_modules/@contrast/assess/lib/response-scanning/handlers/index.js +10 -14
  10. package/node_modules/@contrast/assess/lib/session-configuration/handlers.js +1 -1
  11. package/node_modules/@contrast/assess/package.json +11 -11
  12. package/node_modules/@contrast/config/lib/options.js +8 -0
  13. package/node_modules/@contrast/config/package.json +2 -2
  14. package/node_modules/@contrast/core/package.json +4 -4
  15. package/node_modules/@contrast/deadzones/package.json +4 -4
  16. package/node_modules/@contrast/dep-hooks/package.json +3 -3
  17. package/node_modules/@contrast/esm-hooks/package.json +5 -5
  18. package/node_modules/@contrast/instrumentation/package.json +4 -4
  19. package/node_modules/@contrast/library-analysis/lib/install/library-reporting/dep.json +127 -127
  20. package/node_modules/@contrast/library-analysis/package.json +3 -3
  21. package/node_modules/@contrast/logger/package.json +2 -2
  22. package/node_modules/@contrast/metrics/package.json +5 -5
  23. package/node_modules/@contrast/patcher/package.json +2 -2
  24. package/node_modules/@contrast/protect/lib/input-analysis/handlers.js +1 -12
  25. package/node_modules/@contrast/protect/package.json +10 -10
  26. package/node_modules/@contrast/reporter/package.json +5 -5
  27. package/node_modules/@contrast/rewriter/package.json +4 -4
  28. package/node_modules/@contrast/route-coverage/package.json +7 -7
  29. package/node_modules/@contrast/scopes/package.json +5 -5
  30. package/node_modules/@contrast/sec-obs/package.json +8 -8
  31. package/node_modules/@contrast/sources/package.json +2 -2
  32. package/node_modules/@contrast/telemetry/package.json +4 -4
  33. package/node_modules/@types/node/README.md +1 -1
  34. package/node_modules/@types/node/assert/strict.d.ts +105 -2
  35. package/node_modules/@types/node/assert.d.ts +119 -95
  36. package/node_modules/@types/node/crypto.d.ts +117 -7
  37. package/node_modules/@types/node/events.d.ts +79 -33
  38. package/node_modules/@types/node/fs.d.ts +224 -0
  39. package/node_modules/@types/node/http.d.ts +28 -3
  40. package/node_modules/@types/node/package.json +3 -3
  41. package/node_modules/@types/node/test.d.ts +2 -23
  42. package/node_modules/@types/node/url.d.ts +6 -1
  43. package/node_modules/@types/node/util.d.ts +5 -0
  44. package/node_modules/@types/node/web-globals/events.d.ts +3 -0
  45. package/node_modules/@types/node/worker_threads.d.ts +33 -47
  46. package/node_modules/@types/node/zlib.d.ts +6 -0
  47. package/node_modules/undici-types/agent.d.ts +0 -4
  48. package/node_modules/undici-types/client.d.ts +0 -2
  49. package/node_modules/undici-types/dispatcher.d.ts +0 -6
  50. package/node_modules/undici-types/h2c-client.d.ts +0 -2
  51. package/node_modules/undici-types/index.d.ts +3 -1
  52. package/node_modules/undici-types/mock-interceptor.d.ts +0 -1
  53. package/node_modules/undici-types/package.json +1 -1
  54. package/node_modules/undici-types/snapshot-agent.d.ts +107 -0
  55. package/package.json +2 -2
  56. package/node_modules/@contrast/assess/lib/get-policy.js +0 -336
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contrast/agent",
3
- "version": "5.45.1",
3
+ "version": "5.46.0",
4
4
  "description": "Assess and Protect agents for Node.js",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "author": "Contrast Security <nodejs@contrastsecurity.com> (https://www.contrastsecurity.com)",
@@ -28,15 +28,15 @@
28
28
  "test": "bash ../scripts/test.sh"
29
29
  },
30
30
  "dependencies": {
31
- "@contrast/agentify": "1.57.0",
32
- "@contrast/architecture-components": "1.45.1",
33
- "@contrast/assess": "1.63.0",
31
+ "@contrast/agentify": "1.58.0",
32
+ "@contrast/architecture-components": "1.46.0",
33
+ "@contrast/assess": "1.64.0",
34
34
  "@contrast/common": "1.37.0",
35
- "@contrast/core": "1.57.1",
36
- "@contrast/library-analysis": "1.47.1",
37
- "@contrast/protect": "1.68.0",
38
- "@contrast/route-coverage": "1.49.1",
39
- "@contrast/sec-obs": "1.1.1",
40
- "@contrast/telemetry": "1.32.1"
35
+ "@contrast/core": "1.58.0",
36
+ "@contrast/library-analysis": "1.48.0",
37
+ "@contrast/protect": "1.69.0",
38
+ "@contrast/route-coverage": "1.50.0",
39
+ "@contrast/sec-obs": "1.2.0",
40
+ "@contrast/telemetry": "1.33.0"
41
41
  }
42
42
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contrast/agentify",
3
- "version": "1.57.0",
3
+ "version": "1.58.0",
4
4
  "description": "Configures Contrast agent services and instrumentation within an application",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "author": "Contrast Security <nodejs@contrastsecurity.com> (https://www.contrastsecurity.com)",
@@ -21,21 +21,21 @@
21
21
  },
22
22
  "dependencies": {
23
23
  "@contrast/common": "1.37.0",
24
- "@contrast/config": "1.52.1",
25
- "@contrast/core": "1.57.1",
26
- "@contrast/deadzones": "1.29.1",
27
- "@contrast/dep-hooks": "1.26.1",
28
- "@contrast/esm-hooks": "2.32.0",
24
+ "@contrast/config": "1.53.0",
25
+ "@contrast/core": "1.58.0",
26
+ "@contrast/deadzones": "1.30.0",
27
+ "@contrast/dep-hooks": "1.27.0",
28
+ "@contrast/esm-hooks": "2.33.0",
29
29
  "@contrast/find-package-json": "^1.1.0",
30
- "@contrast/instrumentation": "1.36.1",
31
- "@contrast/logger": "1.30.1",
32
- "@contrast/metrics": "1.34.1",
33
- "@contrast/patcher": "1.29.1",
30
+ "@contrast/instrumentation": "1.37.0",
31
+ "@contrast/logger": "1.31.0",
32
+ "@contrast/metrics": "1.35.0",
33
+ "@contrast/patcher": "1.30.0",
34
34
  "@contrast/perf": "1.4.0",
35
- "@contrast/reporter": "1.55.1",
36
- "@contrast/rewriter": "1.34.0",
37
- "@contrast/scopes": "1.27.1",
38
- "@contrast/sources": "1.3.1",
35
+ "@contrast/reporter": "1.56.0",
36
+ "@contrast/rewriter": "1.35.0",
37
+ "@contrast/scopes": "1.28.0",
38
+ "@contrast/sources": "1.4.0",
39
39
  "on-finished": "^2.4.1",
40
40
  "semver": "^7.6.0"
41
41
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contrast/architecture-components",
3
- "version": "1.45.1",
3
+ "version": "1.46.0",
4
4
  "description": "Detects external systems being connected to by applications.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "author": "Contrast Security <nodejs@contrastsecurity.com> (https://www.contrastsecurity.com)",
@@ -21,8 +21,8 @@
21
21
  },
22
22
  "dependencies": {
23
23
  "@contrast/common": "1.37.0",
24
- "@contrast/dep-hooks": "1.26.1",
25
- "@contrast/logger": "1.30.1",
26
- "@contrast/patcher": "1.29.1"
24
+ "@contrast/dep-hooks": "1.27.0",
25
+ "@contrast/logger": "1.31.0",
26
+ "@contrast/patcher": "1.30.0"
27
27
  }
28
28
  }
@@ -45,24 +45,19 @@ module.exports = Core.makeComponent({
45
45
  const logger = core.logger.child({ name: 'contrast:sources' });
46
46
 
47
47
  sources.createTags = function createTags({ inputType, fieldName = '', value, tagNames }) {
48
- if (!value?.length) {
49
- return null;
50
- }
48
+ if (!value?.length) return null;
51
49
 
52
50
  const stop = value.length - 1;
53
- const tags = {
54
- [DataflowTag.UNTRUSTED]: [0, stop]
55
- };
51
+ const tags = { [DataflowTag.UNTRUSTED]: [0, stop] };
56
52
 
57
- if (tagNames) {
58
- for (const tag of tagNames) {
53
+ if (tagNames)
54
+ for (const tag of tagNames)
59
55
  tags[tag] = [0, stop];
60
- }
61
- }
62
56
 
63
- if (inputType === InputType.HEADER && StringPrototypeToLowerCase.call(fieldName) === 'referer') {
64
- tags[DataflowTag.HEADER] = [0, stop];
65
- }
57
+ if (
58
+ inputType === InputType.HEADER &&
59
+ StringPrototypeToLowerCase.call(fieldName) === 'referer'
60
+ ) tags[DataflowTag.HEADER] = [0, stop];
66
61
 
67
62
  return tags;
68
63
  };
@@ -89,14 +84,7 @@ module.exports = Core.makeComponent({
89
84
  return null;
90
85
  }
91
86
 
92
- // url exclusion
93
- if (!sourceContext.policy) {
94
- return null;
95
- }
96
-
97
- if (!context) {
98
- context = inputType;
99
- }
87
+ if (!context) context = inputType;
100
88
 
101
89
  const { policy: requestPolicy } = sourceContext;
102
90
  const max = config.assess.max_context_source_events;
@@ -111,7 +99,10 @@ module.exports = Core.makeComponent({
111
99
  }
112
100
 
113
101
  function createEvent({ fieldName, pathName, value, excludedRules }) {
114
- const tagNames = Array.from(excludedRules).map((ruleId) => `excluded:${ruleId}`);
102
+ let tagNames;
103
+ if (excludedRules) {
104
+ tagNames = Array.from(excludedRules).map((ruleId) => `excluded:${ruleId}`);
105
+ }
115
106
  // create the stacktrace once per call to .handle()
116
107
  stack || (stack = sources.createStacktrace(stacktraceOpts));
117
108
  return eventFactory.createSourceEvent({
@@ -127,7 +118,10 @@ module.exports = Core.makeComponent({
127
118
  }
128
119
 
129
120
  if (Buffer.isBuffer(data) && !tracker.getData(data)) {
130
- const { track, excludedRules } = requestPolicy.getInputPolicy(InputType.BODY);
121
+ const inputPolicy = requestPolicy.getInputPolicy(InputType.BODY);
122
+ const track = !!inputPolicy;
123
+ const excludedRules = inputPolicy?.constructor?.name == 'Set' ? inputPolicy : undefined;
124
+
131
125
  if (!track) {
132
126
  core.logger.debug({ inputType }, 'assess input exclusion disabled tracking');
133
127
  return;
@@ -149,7 +143,10 @@ module.exports = Core.makeComponent({
149
143
  return true;
150
144
  }
151
145
 
152
- const { track, excludedRules } = sourceContext.policy.getInputPolicy(inputType, fieldName);
146
+ const inputPolicy = sourceContext.policy.getInputPolicy(inputType, fieldName);
147
+ const track = !!inputPolicy;
148
+ const excludedRules = inputPolicy?.constructor?.name == 'Set' ? inputPolicy : undefined;
149
+
153
150
  if (!track) {
154
151
  core.logger.debug({ fieldName, inputType }, 'assess input exclusion disabling tracking');
155
152
  return;
@@ -53,20 +53,11 @@ function factory(core) {
53
53
  core.assess.getPropagatorContext = function getPropagatorContext() {
54
54
  if (instrumentation.isLocked()) return null;
55
55
 
56
- // the following logging used to be done by the caller, but has been moved
57
- // here as opposed to overloading `ctx.policy` with a special value so the
58
- // caller could determine whether no source context was available or the
59
- // request is being intentionally excluded. A negative of this is that the
60
- // function name is not available to be included in the log.
61
56
  const ctx = sources.getStore()?.assess;
62
- if (!ctx) return null;
63
-
64
57
  // there is a context, but if policy is null then assess is intentionally
65
58
  // disabled (i.e., url exclusion or the request is not sampled).
66
- if (!ctx.policy) {
67
- return null;
68
- }
69
-
59
+ if (!ctx?.policy || ctx?.policy.allowed) return null;
60
+ // event limits
70
61
  if (ctx.propagationEventsCount >= config.assess.max_propagation_events) return null;
71
62
 
72
63
  return ctx;
@@ -80,13 +71,13 @@ function factory(core) {
80
71
  if (instrumentation.isLocked()) return null;
81
72
 
82
73
  const ctx = sources.getStore()?.assess;
83
- if (!ctx) return null;
84
-
85
- if (!ctx.policy) {
86
- return null;
87
- }
74
+ if (!ctx?.policy || ctx.policy?.allowed) return null;
75
+ if (!ruleId) return ctx;
88
76
 
89
- if (ruleId && !ctx.policy?.enabledRules?.has?.(ruleId) || ruleScopes.isLocked(ruleId)) return null;
77
+ if (
78
+ !ctx.policy?.isRuleEnabled?.(ruleId) ||
79
+ ruleScopes.isLocked(ruleId)
80
+ ) return null;
90
81
 
91
82
  return ctx;
92
83
  };
@@ -105,10 +96,8 @@ function factory(core) {
105
96
  return null;
106
97
  }
107
98
 
108
- if (!ctx.policy) {
109
- return null;
110
- }
111
-
99
+ if (!ctx.policy || ctx.policy.allowed) return null;
100
+ // event limits
112
101
  if (ctx.sourceEventsCount >= config.assess.max_context_source_events) return null;
113
102
 
114
103
  return ctx;
@@ -60,7 +60,7 @@ module.exports = function assess(core) {
60
60
 
61
61
  // ancillary tools used by different features
62
62
  require('./sampler')(core);
63
- require('./get-policy')(core);
63
+ core.initComponentSync(require('./policy'));
64
64
  core.initComponentSync(require('./make-source-context'));
65
65
  require('./rule-scopes')(core);
66
66
  core.initComponentSync(require('./get-source-context'));
@@ -36,24 +36,19 @@ function factory(core) {
36
36
  * @returns {import('@contrast/assess').SourceContext}
37
37
  */
38
38
  return core.assess.makeSourceContext = function ({ store, incomingMessage: req }) {
39
-
40
39
  try {
41
40
  const ctx = 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
41
  policy: null,
45
42
  };
46
-
43
+ // if assess is disabled or not selected for sampling, the policy will
44
+ // be null (assess disabled) for lifetime of connection, despite UI updates.
47
45
  if (!core.config.getEffectiveValue('assess.enable')) return ctx;
48
-
49
- // check whether sampling allows processing
50
46
  ctx.sampleInfo = assess.sampler?.getSampleInfo(store.sourceInfo) ?? null;
51
47
  if (ctx.sampleInfo?.canSample === false) return ctx;
52
48
 
53
- // set policy - can be returned as `null` if request is url-excluded.
54
- ctx.policy = assess.getPolicy(store.sourceInfo);
55
- if (!ctx.policy) return ctx;
56
-
49
+ // assess-enabled policy from current effective config, but
50
+ // policy is dynamic and will respond to settings updates
51
+ ctx.policy = assess.policy.getRequestPolicy(store.sourceInfo);
57
52
  ctx.propagationEventsCount = 0;
58
53
  ctx.sourceEventsCount = 0;
59
54
  ctx.responseData = {};
@@ -0,0 +1,400 @@
1
+ /*
2
+ * Copyright: 2025 Contrast Security, Inc
3
+ * Contact: support@contrastsecurity.com
4
+ * License: Commercial
5
+
6
+ * NOTICE: This Software and the patented inventions embodied within may only be
7
+ * used as part of Contrast Security’s commercial offerings. Even though it is
8
+ * made available through public repositories, use of this Software is subject to
9
+ * the applicable End User Licensing Agreement found at
10
+ * https://www.contrastsecurity.com/enduser-terms-0317a or as otherwise agreed
11
+ * between Contrast Security and the End User. The Software may not be reverse
12
+ * engineered, modified, repackaged, sold, redistributed or otherwise used in a
13
+ * way not consistent with the End User License Agreement.
14
+ */
15
+
16
+ 'use strict';
17
+
18
+ const {
19
+ Event,
20
+ ExclusionType,
21
+ InputType,
22
+ Rule,
23
+ ResponseScanningRule,
24
+ SessionConfigurationRule,
25
+ set,
26
+ primordials: { ArrayPrototypeJoin, RegExpPrototypeTest }
27
+ } = require('@contrast/common');
28
+ const { Core } = require('@contrast/core/lib/ioc/core');
29
+
30
+ const ASSESS_RULES = Object.values({
31
+ ...Rule,
32
+ ...ResponseScanningRule,
33
+ ...SessionConfigurationRule,
34
+ });
35
+ const BROAD_INPUT_EXCLUSION_TYPES = [
36
+ ExclusionType.BODY,
37
+ ExclusionType.QUERYSTRING
38
+ ];
39
+ const NAMED_INPUT_EXCLUSION_TYPES = [
40
+ ExclusionType.COOKIE,
41
+ ExclusionType.HEADER,
42
+ ExclusionType.PARAMETER
43
+ ];
44
+ const BODY_TYPES = [
45
+ InputType.BODY,
46
+ InputType.JSON_VALUE,
47
+ InputType.JSON_ARRAYED_VALUE,
48
+ InputType.MULTIPART_CONTENT_TYPE,
49
+ InputType.MULTIPART_FIELD_NAME,
50
+ InputType.MULTIPART_NAME,
51
+ InputType.MULTIPART_VALUE,
52
+ ];
53
+ const COMPONENT_NAME = 'assess.policy';
54
+
55
+ class AssessPolicy {
56
+ /**
57
+ * @param {{
58
+ * config: import('@contrast/config').Config,
59
+ * logger: import('@contrast/logger').Logger,
60
+ * messages: import('@contrast/common').Messages,
61
+ * }} core
62
+ */
63
+ constructor(core) {
64
+ Object.defineProperty(this, 'core', { value: core });
65
+
66
+ this.version = Date.now();
67
+ this.disabledRules = new Set(core.config.getEffectiveValue('assess.rules.disabled_rules'));
68
+ this.exclusionMap = new Map([
69
+ [ExclusionType.BODY, []],
70
+ [ExclusionType.COOKIE, []],
71
+ [ExclusionType.HEADER, []],
72
+ [ExclusionType.PARAMETER, []],
73
+ [ExclusionType.QUERYSTRING, []],
74
+ [ExclusionType.URL, []],
75
+ ]);
76
+
77
+ core.messages.on(Event.SERVER_SETTINGS_UPDATE, (msg) => {
78
+ if (!msg.assess && !msg.exclusions) return;
79
+
80
+ this.version = Date.now();
81
+
82
+ if (msg.assess) {
83
+ const enabledRules = new Set();
84
+ this.disabledRules = new Set(core.config.getEffectiveValue('assess.rules.disabled_rules'));
85
+
86
+ for (const ruleId of ASSESS_RULES) {
87
+ const enable = msg.assess[ruleId]?.enable;
88
+ if (enable === false) {
89
+ this.disabledRules.add(ruleId);
90
+ // map to "sub-rules"
91
+ if (ruleId === Rule.NOSQL_INJECTION) this.disabledRules.add(Rule.NOSQL_INJECTION_MONGO);
92
+ } else if (enable === true) {
93
+ enabledRules.add(ruleId);
94
+ if (ruleId === Rule.NOSQL_INJECTION) enabledRules.add(Rule.NOSQL_INJECTION_MONGO);
95
+ }
96
+ }
97
+ this.core.logger.info({
98
+ enabledRules,
99
+ disabledRules: Array.from(this.disabledRules)
100
+ }, 'Assess policy rules updated');
101
+ }
102
+
103
+ if (msg.exclusions) {
104
+ for (const arr of this.exclusionMap.values()) arr.length = 0;
105
+
106
+ const rawDtmList = [
107
+ ...(msg?.exclusions?.input || []),
108
+ ...(msg?.exclusions?.url || []),
109
+ ].filter((exclusion) => exclusion?.modes?.includes?.('assess'));
110
+
111
+ // reset global exclusion state
112
+ for (const type of Object.values(ExclusionType)) {
113
+ this.exclusionMap.get(type).length = 0;
114
+ }
115
+
116
+ if (!rawDtmList.length) return;
117
+
118
+ for (const dtm of rawDtmList) {
119
+ // normalize different dtm types
120
+ dtm.type = dtm.type || 'URL';
121
+ const { type } = dtm;
122
+ const key = ExclusionType[type];
123
+ // defensive code against unanticipated DTM values
124
+ if (key) {
125
+ const Ctor = dtm.type === ExclusionType.URL ? UrlExclusion : InputExclusion;
126
+ this.exclusionMap.get(dtm.type).push(new Ctor(dtm));
127
+ }
128
+ }
129
+
130
+ this.core.logger.info({
131
+ exclusions: Object.fromEntries(this.exclusionMap)
132
+ }, 'Assess exclusions updated (%s total)', rawDtmList.length);
133
+ }
134
+ });
135
+ }
136
+
137
+ getRequestPolicy(sourceInfo) {
138
+ return new RequestPolicy(this.core, sourceInfo);
139
+ }
140
+ }
141
+
142
+ class RequestPolicy {
143
+ /**
144
+ * @param {{
145
+ * config: import('@contrast/config').Config,
146
+ * logger: import('@contrast/logger').Logger,
147
+ * messages: import('@contrast/common').Messages,
148
+ * }} core
149
+ * @param {import('@contrast/common').SourceInfo} sourceInfo
150
+ */
151
+ constructor(core, sourceInfo) {
152
+ Object.defineProperty(this, 'core', { value: core });
153
+ this.sourceInfo = sourceInfo;
154
+ this.init();
155
+ }
156
+
157
+ /**
158
+ * Used to (re)initialize the instance's exclusions, reading from current assess global policy.
159
+ */
160
+ init() {
161
+ const { core, sourceInfo } = this;
162
+ this.allowed = false;
163
+ this.version = core.assess.policy.version;
164
+ this.exclusions = {};
165
+
166
+ if (!core.config.getEffectiveValue('assess.enable')) {
167
+ this.allowed = true;
168
+ return;
169
+ }
170
+
171
+ // Evaluate URL exclusions.
172
+ // If one matches and applies to all rules, we set `allowed: true` which will
173
+ // disable assess for the request (via getSourceContext()). If specific rules are
174
+ // disabled, we remove them from the request policy's set of enabled rules.
175
+ for (const urlExclusion of this.core.assess.policy.exclusionMap.get(ExclusionType.URL)) {
176
+ if (urlExclusion.matchesUriPath(sourceInfo.uriPath)) {
177
+ if (!urlExclusion.rules?.size) {
178
+ core.logger.debug({
179
+ name: urlExclusion.name
180
+ }, 'All Assess rules have been disabled by URL exclusion');
181
+ this.allowed = true;
182
+ // no need to further process exclusions - request will be ignored
183
+ return;
184
+ } else {
185
+ // build as needed
186
+ if (!this.exclusions.disabledRules) this.exclusions.disabledRules = new Set();
187
+
188
+ for (const ruleId of urlExclusion.rules) {
189
+ this.exclusions.disabledRules.add(ruleId);
190
+ }
191
+ core.logger.debug({
192
+ name: urlExclusion.name,
193
+ rules: Array.from(urlExclusion.rules),
194
+ }, 'Assess rules disabled by URL exclusion');
195
+ }
196
+ }
197
+ }
198
+
199
+ // Process input exclusions that apply broadly: BODY, QUERYSTRING
200
+ for (const type of BROAD_INPUT_EXCLUSION_TYPES) {
201
+ for (const exclusion of core.assess.policy.exclusionMap.get(type)) {
202
+ if (exclusion.matchesUriPath(sourceInfo.uriPath)) {
203
+ // build as needed
204
+ if (!this.exclusions[type]) this.exclusions[type] = { track: true, excludedRules: new Set() };
205
+ const inputPolicy = this.exclusions[type];
206
+
207
+ if (exclusion.rules.size) {
208
+ for (const ruleId of exclusion.rules) {
209
+ inputPolicy.excludedRules.add(ruleId);
210
+ }
211
+ } else {
212
+ inputPolicy.track = false;
213
+ inputPolicy.excludedRules.clear();
214
+ break;
215
+ }
216
+ }
217
+ }
218
+ }
219
+
220
+ for (const type of NAMED_INPUT_EXCLUSION_TYPES) {
221
+ for (const exclusion of core.assess.policy.exclusionMap.get(type)) {
222
+ if (exclusion.matchesUriPath(sourceInfo.uriPath)) {
223
+ if (!this.exclusions[type]) this.exclusions[type] = [];
224
+ this.exclusions[type].push(exclusion);
225
+ }
226
+ }
227
+ }
228
+ }
229
+
230
+ /**
231
+ * Given input type and optional field name will give instructions on how
232
+ * to track based on global policy and various exclusions that may apply.
233
+ * @param {} inputType
234
+ * @param {} fieldName
235
+ * @returns {boolean|Set<string>} false - do not track
236
+ * true - track
237
+ * Set - track but add tags to exclude these rules
238
+ */
239
+ getInputPolicy(inputType, fieldName) {
240
+ if (this.version < this.core.assess.policy.version) this.init();
241
+ if (this.allowed) return false; // don't track - request ignored
242
+
243
+ let inputRuleExclusions;
244
+ let excludedRuleIds;
245
+
246
+ if (inputType === InputType.HEADER) {
247
+ inputRuleExclusions = this.exclusions[ExclusionType.HEADER];
248
+ } else if (inputType === InputType.QUERYSTRING) {
249
+ if (this.exclusions[ExclusionType.QUERYSTRING]?.track === false) {
250
+ return false;
251
+ } else {
252
+ if (this.exclusions[ExclusionType.QUERYSTRING]?.excludedRules)
253
+ excludedRuleIds = new Set(this.exclusions[ExclusionType.QUERYSTRING]?.excludedRules);
254
+ inputRuleExclusions = this.exclusions[ExclusionType.PARAMETER];
255
+ }
256
+ } else if (inputType === InputType.URL_PARAMETER) {
257
+ inputRuleExclusions = this.exclusions[ExclusionType.PARAMETER];
258
+ } else if ([
259
+ InputType.COOKIE_NAME,
260
+ InputType.COOKIE_VALUE
261
+ ].includes(inputType)) {
262
+ inputRuleExclusions = this.exclusions[ExclusionType.COOKIE];
263
+ } else if (BODY_TYPES.includes(inputType)) {
264
+ if (this.exclusions[ExclusionType.BODY]?.track === false) {
265
+ return false;
266
+ } else {
267
+ inputRuleExclusions = this.exclusions[ExclusionType.PARAMETER];
268
+ }
269
+ }
270
+
271
+ if (inputRuleExclusions) {
272
+ for (const exclusion of inputRuleExclusions) {
273
+ if (exclusion.matchesInputName(fieldName)) {
274
+ // disables some rules
275
+ if (exclusion.rules.size) {
276
+ for (const ruleId of exclusion.rules) {
277
+ if (!excludedRuleIds) excludedRuleIds = new Set();
278
+ excludedRuleIds.add(ruleId);
279
+ }
280
+ } else {
281
+ return false; // don't track - all rules disabled
282
+ }
283
+ }
284
+ }
285
+ }
286
+
287
+ if (this.exclusions.disabledRules || excludedRuleIds) {
288
+ // only URL Exclusions disabled these rules
289
+ if (!excludedRuleIds) return this.exclusions.disabledRules;
290
+ // only Input Exclusion disabled these
291
+ if (!this.exclusions.disabledRules) return excludedRuleIds;
292
+ // merge since URL Exclusions and Input Exclusions have disabled rules
293
+ return new Set([...this.exclusions.disabledRules, ...excludedRuleIds]);
294
+ }
295
+
296
+ return true;
297
+ }
298
+
299
+ isRuleEnabled(ruleId) {
300
+ if (this.version < this.core.assess.policy.version) this.init();
301
+
302
+ if (this.allowed) return false;
303
+
304
+ return (
305
+ !this.exclusions.disabledRules?.has?.(ruleId) &&
306
+ !this.core.assess.policy.disabledRules?.has?.(ruleId)
307
+ );
308
+ }
309
+ }
310
+
311
+ /**
312
+ * @typedef InputPolicy
313
+ * @property {boolean} track
314
+ * @property {Set<Rule>} excludedRules
315
+ */
316
+
317
+ class UrlExclusion {
318
+ constructor(dtm) {
319
+ this._urlRegex = null;
320
+ this._urls = new Set();
321
+ this.name = dtm.name;
322
+ this.type = ExclusionType[dtm.type];
323
+ this.rules = new Set(dtm.assess_rules);
324
+
325
+ if (dtm.urls.length) {
326
+ const regexSegments = [];
327
+ for (const url of dtm.urls) {
328
+ if (shouldBeRegExp(url)) {
329
+ regexSegments.push(url);
330
+ } else {
331
+ this._urls.add(url);
332
+ }
333
+ }
334
+ if (regexSegments.length) {
335
+ this._urlRegex = new RegExp(`^${ArrayPrototypeJoin.call(regexSegments, '|')}$`);
336
+ }
337
+ }
338
+ }
339
+
340
+ /**
341
+ * Checks whether the current URI path matches any of the exclusion's URL values.
342
+ * Exclusions that don't match for the current request will not be enabled. The
343
+ * interpretation of the DTM is that if its urls list is empty, then that means
344
+ * it should match all requestss (can be the case for input exclusions).
345
+ * @param {string} uriPath uri to check
346
+ * @returns {boolean}
347
+ */
348
+ matchesUriPath(uriPath) {
349
+ return (!this._urlRegex && !this._urls.size) ||
350
+ this._urls.has(uriPath) ||
351
+ !!this._urlRegex?.test?.(uriPath);
352
+ }
353
+ }
354
+
355
+ class InputExclusion extends UrlExclusion {
356
+ constructor(dtm) {
357
+ super(dtm);
358
+ this._inputNameRegex = null;
359
+ this._inputName = null;
360
+
361
+ // dtm.name value is null for BODY and QUERYSTRING types
362
+ if (dtm.name) {
363
+ if (shouldBeRegExp(dtm.name)) {
364
+ this._inputNameRegex = new RegExp(`^${dtm.name}$`);
365
+ } else {
366
+ this._inputName = dtm.name;
367
+ }
368
+ }
369
+ }
370
+
371
+ /**
372
+ * Checks if the provided name matches the value from the exclusion dtm.
373
+ * @param {string} name field name being evaluated
374
+ * @returns {boolean}
375
+ */
376
+ matchesInputName(name) {
377
+ // BODY and QUERYSTRING always match since they apply broadly
378
+ if (!this._inputName && !this._inputNameRegex) return true;
379
+ return this._inputNameRegex ? RegExpPrototypeTest.call(this._inputNameRegex, name) : this._inputName === name;
380
+ }
381
+ }
382
+
383
+ function shouldBeRegExp(str) {
384
+ return str.indexOf('*') > 0 ||
385
+ str.indexOf('.') > 0 ||
386
+ str.indexOf('+') > 0 ||
387
+ str.indexOf('?') > 0 ||
388
+ str.indexOf('\\') > 0;
389
+ }
390
+
391
+ module.exports = Core.makeComponent({
392
+ name: COMPONENT_NAME,
393
+ factory(core) {
394
+ const policy = new AssessPolicy(core);
395
+ set(core, COMPONENT_NAME, policy);
396
+ return policy;
397
+ },
398
+ });
399
+ module.exports.AssessPolicy = AssessPolicy;
400
+ module.exports.RequestPolicy = RequestPolicy;