@contrast/agentify 1.24.0 → 1.24.2

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.
@@ -18,8 +18,49 @@
18
18
  const IS_CLASS_REGEX = /^class\W/;
19
19
  const IS_FUNCTION_REGEX = /^(async\s+)?function\W/;
20
20
  const IS_RETURN_REGEX = /^return\W/;
21
- const METHOD_CONTEXT = 'function _contrast_';
22
- const FUNCTION_CONTEXT = 'const _contrast_fn = ';
21
+ const FUNCTION_DECL = 'function _contrast_';
22
+ const VARIABLE_DECL = 'const _contrast_fn = ';
23
+
24
+ /**
25
+ * Injects some context around a function's toString() to make it syntactically
26
+ * valid. The `swc` parser expects input to be a valid `Statement` like one of
27
+ * the following:
28
+ * ```js
29
+ * // (named) function declaration
30
+ * function f() {}
31
+ * // (named) class declaration
32
+ * class C {}
33
+ * // variable declaration with an (anonymous?) class or function expression
34
+ * const _contrast_fn = function () {};
35
+ * const _contrast_fn = () => {};
36
+ * const _contrast_fn = class {};
37
+ * ```
38
+ * @param {string} code original toString() return value
39
+ * @returns {string} the contextualized statement
40
+ */
41
+ function contextualize(code) {
42
+ // in case the class is anonymous we need to prepend a variable declaration.
43
+ if (IS_CLASS_REGEX.exec(code)) {
44
+ return `${VARIABLE_DECL}${code}`;
45
+ }
46
+
47
+ // if the function is a return, we don't need to add context.
48
+ if (!IS_RETURN_REGEX.exec(code)) {
49
+ const [orig] = code.split('{');
50
+ let lineOne = orig;
51
+
52
+ // When stringified, class methods look like normal function without the
53
+ // `function` keyword. We can normalize this by prepending `function`.
54
+ if (!IS_FUNCTION_REGEX.exec(lineOne) && lineOne.indexOf('=>') === -1) {
55
+ lineOne = `${FUNCTION_DECL}${lineOne}`;
56
+ }
57
+ lineOne = `${VARIABLE_DECL}${lineOne}`;
58
+
59
+ code = code.replace(orig, lineOne);
60
+ }
61
+
62
+ return code;
63
+ }
23
64
 
24
65
  module.exports = function (deps) {
25
66
  const {
@@ -36,58 +77,26 @@ module.exports = function (deps) {
36
77
  cache: new WeakMap(),
37
78
  });
38
79
 
39
- /**
40
- * Create some code context around a function to make it syntactically valid
41
- * the three general types of syntactic function definitions are:
42
- * - functions (declared with the function keyword)
43
- * - arrow functions
44
- * - class methods
45
- * @param {string} code the user code
46
- * @returns {string} the contextualized function
47
- */
48
- functionHooks.contextualizeFunction = (code) => {
49
- // Classes themselves are functions, so we need to exit early to avoid
50
- // clobbering the `class` keyword.
51
- if (IS_CLASS_REGEX.exec(code)) return code;
52
- const [orig] = code.split('{');
53
-
54
- let lineOne = orig;
55
-
56
- // if the function is a return, we don't need to add context.
57
- if (!IS_RETURN_REGEX.exec(lineOne)) {
58
- // When stringified, class methods look like normal function without the
59
- // `function` keyword. We can normalize this by prepending `function`.
60
- if (!IS_FUNCTION_REGEX.exec(lineOne) && lineOne.indexOf('=>') === -1) {
61
- lineOne = `${METHOD_CONTEXT}${lineOne}`;
62
- }
63
- lineOne = `${FUNCTION_CONTEXT}${lineOne}`;
64
-
65
- code = code.replace(orig, lineOne);
66
- }
67
-
68
- return code;
69
- };
70
-
71
80
  /**
72
81
  * Returns the rewritten function code. If an error occurs during rewriting,
73
82
  * we log the error and code information and return the original code string.
74
- * @param {string} code string value of the JavaScript function
75
- * @returns {string} the code rewritten to remove Contrast tokens
83
+ * @param {string} code original toString() return value
84
+ * @returns {string} the code rewritten to remove Contrast tokens
76
85
  */
77
- functionHooks.unwrite = function (code) {
86
+ function unwrite(code) {
78
87
  // cannot parse lone function expressions that are not part of an assignment.
79
88
  try {
80
- let unwritten = functionHooks.contextualizeFunction(code);
89
+ let unwritten = contextualize(code);
81
90
  unwritten = rewriter.unwriteSync(unwritten);
82
- unwritten = unwritten.replace(METHOD_CONTEXT, '');
83
- unwritten = unwritten.replace(FUNCTION_CONTEXT, '');
91
+ unwritten = unwritten.replace(FUNCTION_DECL, '');
92
+ unwritten = unwritten.replace(VARIABLE_DECL, '');
84
93
  unwritten = unwritten.replace(/;\s*$/, ''); // removes trailing semicolon/whitespace
85
94
  return unwritten;
86
95
  } catch (err) {
87
96
  logger.warn({ err, code }, 'Failed to unwrite function code');
88
97
  return code;
89
98
  }
90
- };
99
+ }
91
100
 
92
101
  functionHooks.install = function () {
93
102
  if (deps.functionHooks.installed) return;
@@ -113,7 +122,7 @@ module.exports = function (deps) {
113
122
  let result = code;
114
123
 
115
124
  if (code.indexOf('ContrastMethods') > -1) {
116
- const unwritten = functionHooks.unwrite(code);
125
+ const unwritten = unwrite(code);
117
126
  functionHooks.cache.set(data.obj, unwritten);
118
127
  result = unwritten;
119
128
  }
package/lib/index.js CHANGED
@@ -17,6 +17,10 @@
17
17
 
18
18
  const Module = require('module');
19
19
  const { IntentionalError } = require('@contrast/common');
20
+ const {
21
+ assertValidOpts,
22
+ preStartupValidation
23
+ } = require('./utils');
20
24
 
21
25
  const ERROR_MESSAGE = 'An error prevented the Contrast agent from installing. The application will be run without instrumentation.';
22
26
  /**
@@ -61,6 +65,9 @@ module.exports = function init(core = {}) {
61
65
  let _opts;
62
66
 
63
67
  core.agentify = async function agentify(callback, opts = {}) {
68
+ // options are hardcoded, so if this throws it's going to be in testing/dev situation
69
+ assertValidOpts(opts);
70
+
64
71
  _callback = callback;
65
72
  _opts = opts;
66
73
  _opts.type = _opts.type ?? 'cjs';
@@ -124,6 +131,9 @@ module.exports = function init(core = {}) {
124
131
  }
125
132
 
126
133
  try {
134
+ // check supported Node version and correct preload usage before anything else
135
+ preStartupValidation(core);
136
+
127
137
  require('@contrast/core/lib/messages')(core);
128
138
  require('@contrast/config')(core);
129
139
  if (core.config._errors?.length) {
@@ -131,7 +141,7 @@ module.exports = function init(core = {}) {
131
141
  }
132
142
  if (!core.config.enable) {
133
143
  const errorMessage = 'Contrast agent disabled by configuration (enable: false)';
134
- console.log(errorMessage);
144
+ console.info(errorMessage);
135
145
  throw new IntentionalError(errorMessage);
136
146
  }
137
147
 
@@ -172,7 +182,8 @@ module.exports = function init(core = {}) {
172
182
  if (core.logger) {
173
183
  core.logger.error({ err }, ERROR_MESSAGE);
174
184
  } else {
175
- console.error(new Error(ERROR_MESSAGE, { cause: err }));
185
+ console.error(err);
186
+ console.error(ERROR_MESSAGE);
176
187
  }
177
188
  }
178
189
  }
package/lib/utils.js ADDED
@@ -0,0 +1,153 @@
1
+ /*
2
+ * Copyright: 2024 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 path = require('path');
19
+ const process = require('process');
20
+ const semver = require('semver');
21
+ const { findPackageJsonSync } = require('@contrast/find-package-json');
22
+ const {
23
+ engines: {
24
+ node: nodeEngines,
25
+ }
26
+ } = require('../package.json');
27
+
28
+ /**
29
+ * Performs various startup validation checks. All checks throw errors when they
30
+ * fail so startup/installation stops.
31
+ * @param {string} core.nodeEngines
32
+ */
33
+ function preStartupValidation(core) {
34
+ assertSupportedNodeVersion(core.nodeEngines || nodeEngines);
35
+ assertSupportedPreloadUsage();
36
+ }
37
+
38
+ /**
39
+ * Checks current running version of Node against provided engines descriptor.
40
+ * If the Node version doesn't satisfy supported versions from engines, then
41
+ * we will throw an error to prevent instrumentation.
42
+ * @param {string} engines engines that must be satisfied
43
+ * @throws {Error}
44
+ */
45
+ function assertSupportedNodeVersion(engines) {
46
+ if (!semver.satisfies(process.version, engines)) {
47
+ let validRanges = '';
48
+ engines.split('||').forEach((range) => {
49
+ const minVersion = semver.minVersion(range).toString();
50
+ const maxVersion = range.split('<').pop()
51
+ .trim();
52
+ validRanges += `${minVersion} and ${maxVersion}, `;
53
+ });
54
+ throw new Error(`Contrast only officially supports Node LTS versions between ${validRanges}but detected ${process.version}.`);
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Checks that the correct preload flag is used given running node version.
60
+ * @throws {Error}
61
+ */
62
+ function assertSupportedPreloadUsage() {
63
+ const {
64
+ execArgv,
65
+ version,
66
+ env: { CSI_EXPOSE_CORE, NODE_OPTIONS },
67
+ } = process;
68
+
69
+ let msg;
70
+
71
+ // allow testing to ignore these restrictions
72
+ if (CSI_EXPOSE_CORE === 'no-validate') return;
73
+
74
+ // eslint-disable-next-line newline-per-chained-call
75
+ const [major, minor] = version.slice(1).split('.').map(Number);
76
+ const nodeOptionArgs = (NODE_OPTIONS || '').split(/\s+/).filter((v) => v);
77
+ const allArgsToCheck = [...execArgv, ...nodeOptionArgs];
78
+
79
+ let checkOccurred = false;
80
+
81
+ for (let i = 0; i < allArgsToCheck.length; i++) {
82
+ const preloadFlag = allArgsToCheck[i];
83
+ let preloadTarget = allArgsToCheck[i + 1];
84
+
85
+ if (!isLoader(preloadFlag)) continue;
86
+
87
+ if (preloadTarget && !preloadTarget.startsWith('@contrast/agent')) {
88
+ // if absolute path is provided read the package name to check if it's our agent
89
+ if (path.isAbsolute(preloadTarget)) {
90
+ try {
91
+ const { name } = require(findPackageJsonSync({ cwd: path.dirname(preloadTarget) }));
92
+ if (name !== '@contrast/agent') continue;
93
+ preloadTarget = name;
94
+ } catch (err) {
95
+ //
96
+ }
97
+ } else {
98
+ continue;
99
+ }
100
+ }
101
+
102
+ if (preloadTarget?.startsWith?.('@contrast/agent')) {
103
+ checkOccurred = true;
104
+
105
+ if (preloadFlag === '--import') {
106
+ if (major <= 18 && minor < 19) {
107
+ msg = 'Contrast requires that Node LTS versions >= 18.19.0 use the --import flag for ESM support';
108
+ }
109
+ } else {
110
+ // --loader or --experimental-loader
111
+ if (major >= 20 || (major === 18 && minor >= 19)) {
112
+ msg = 'Contrast requires that Node versions less than 18.19.0 use the --loader flag for ESM support';
113
+ }
114
+ }
115
+
116
+ // done since we've found us
117
+ break;
118
+ }
119
+ }
120
+
121
+ if (msg) throw new Error(msg);
122
+
123
+ // indicates we found ourselves
124
+ return checkOccurred;
125
+ }
126
+
127
+ function isLoader(arg) {
128
+ return arg == '--import' || arg == '--loader' || arg == '--experimental-loader';
129
+ }
130
+
131
+ /**
132
+ * Validates custom `installOrder` options. We currently need the reporter component
133
+ * to install first in order to onboard and get effective settings and mode values.
134
+ * @param {Object} opts The options object param
135
+ * @param {string[]} opts.installOrder array of component names to install in order
136
+ */
137
+ function assertValidOpts(opts) {
138
+ if (opts.noValidate) return;
139
+
140
+ if (opts.installOrder?.length) {
141
+ if (opts.installOrder[0] !== 'reporter') {
142
+ const msg = `The 'installOrder' option must include 'reporter' as first element. found '${opts.installOrder[0]}'`;
143
+ throw new Error(msg);
144
+ }
145
+ }
146
+ }
147
+
148
+ module.exports = {
149
+ assertValidOpts,
150
+ assertSupportedNodeVersion,
151
+ assertSupportedPreloadUsage,
152
+ preStartupValidation,
153
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contrast/agentify",
3
- "version": "1.24.0",
3
+ "version": "1.24.2",
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)",
@@ -11,24 +11,26 @@
11
11
  "types": "lib/index.d.ts",
12
12
  "engines": {
13
13
  "npm": ">=6.13.7 <7 || >= 8.3.1",
14
- "node": ">= 14.15.0"
14
+ "node": ">= 14.18.0"
15
15
  },
16
16
  "scripts": {
17
17
  "test": "../scripts/test.sh"
18
18
  },
19
19
  "dependencies": {
20
- "@contrast/common": "1.20.0",
21
- "@contrast/config": "1.27.0",
22
- "@contrast/core": "1.31.0",
23
- "@contrast/deadzones": "1.1.2",
24
- "@contrast/dep-hooks": "1.3.1",
25
- "@contrast/esm-hooks": "2.5.0",
26
- "@contrast/instrumentation": "1.7.0",
27
- "@contrast/logger": "1.8.0",
28
- "@contrast/metrics": "1.7.0",
29
- "@contrast/patcher": "1.7.1",
30
- "@contrast/reporter": "1.26.0",
31
- "@contrast/rewriter": "1.7.0",
32
- "@contrast/scopes": "1.4.0"
20
+ "@contrast/common": "1.20.1",
21
+ "@contrast/config": "1.27.1",
22
+ "@contrast/core": "1.31.2",
23
+ "@contrast/deadzones": "1.1.3",
24
+ "@contrast/dep-hooks": "1.3.2",
25
+ "@contrast/esm-hooks": "2.5.2",
26
+ "@contrast/find-package-json": "^1.1.0",
27
+ "@contrast/instrumentation": "1.7.1",
28
+ "@contrast/logger": "1.8.1",
29
+ "@contrast/metrics": "1.7.1",
30
+ "@contrast/patcher": "1.7.2",
31
+ "@contrast/reporter": "1.26.1",
32
+ "@contrast/rewriter": "1.7.2",
33
+ "@contrast/scopes": "1.4.1",
34
+ "semver": "^7.6.0"
33
35
  }
34
36
  }