@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.
- package/lib/function-hooks.js +51 -42
- package/lib/index.js +13 -2
- package/lib/utils.js +153 -0
- package/package.json +17 -15
package/lib/function-hooks.js
CHANGED
|
@@ -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
|
|
22
|
-
const
|
|
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
|
|
75
|
-
* @returns {string}
|
|
83
|
+
* @param {string} code original toString() return value
|
|
84
|
+
* @returns {string} the code rewritten to remove Contrast tokens
|
|
76
85
|
*/
|
|
77
|
-
|
|
86
|
+
function unwrite(code) {
|
|
78
87
|
// cannot parse lone function expressions that are not part of an assignment.
|
|
79
88
|
try {
|
|
80
|
-
let unwritten =
|
|
89
|
+
let unwritten = contextualize(code);
|
|
81
90
|
unwritten = rewriter.unwriteSync(unwritten);
|
|
82
|
-
unwritten = unwritten.replace(
|
|
83
|
-
unwritten = unwritten.replace(
|
|
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 =
|
|
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.
|
|
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(
|
|
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.
|
|
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.
|
|
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.
|
|
21
|
-
"@contrast/config": "1.27.
|
|
22
|
-
"@contrast/core": "1.31.
|
|
23
|
-
"@contrast/deadzones": "1.1.
|
|
24
|
-
"@contrast/dep-hooks": "1.3.
|
|
25
|
-
"@contrast/esm-hooks": "2.5.
|
|
26
|
-
"@contrast/
|
|
27
|
-
"@contrast/
|
|
28
|
-
"@contrast/
|
|
29
|
-
"@contrast/
|
|
30
|
-
"@contrast/
|
|
31
|
-
"@contrast/
|
|
32
|
-
"@contrast/
|
|
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
|
}
|