@contrast/agent 5.0.0-beta.9 → 5.0.1

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/LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright: 2023 Contrast Security, Inc
1
+ Copyright: 2024 Contrast Security, Inc
2
2
  Contact: support@contrastsecurity.com
3
3
  License: Commercial
4
4
 
package/README.md ADDED
@@ -0,0 +1,151 @@
1
+ # Contrast Security Node.js Agent
2
+
3
+ This package will enable instrumentation of your Node.js application for
4
+ security analysis and runtime protection by [Contrast Security](https://www.contrastsecurity.com).
5
+
6
+ Unlike legacy application security testing solutions, Contrast produces accurate
7
+ results without dependence on application security experts. Accuracy comes from
8
+ Contrast's patented Deep Security Instrumentation technology, which integrates
9
+ the most effective elements of Interactive (IAST), Static (SAST), and Dynamic
10
+ (DAST) application security testing technology, software composition analysis
11
+ (SCA), and configuration analysis, and delivers them directly to applications.
12
+
13
+ Contrast produces a continuous stream of accurate vulnerability and compliance
14
+ risk information whenever and wherever software is run. Development, QA and
15
+ Security teams get results as they develop and test software, enabling them to
16
+ find and fix security flaws early in the software lifecycle, when they are
17
+ easiest and cheapest to remediate.
18
+
19
+ ## New in version 5
20
+
21
+ - The agent no longer ships or operates with the `contrast-service` "sidecar" executables. This allows for a drastically smaller download and simplified deployments.
22
+
23
+ - Framework support includes `express`, `koa`, and `fastify`, with others soon to come.
24
+
25
+ - The agent does not respond to any command-line configuration flags. Configuration options can be set using environment variables and/or `contrast_security.yaml` file. If you were previously using the agent's `-c` CLI option to set the location of your configuration file, you can use `CONTRAST_CONFIG_PATH` environment variable instead. See more about configuration [below](#configuration).
26
+
27
+ - Structured logging.
28
+
29
+ - Ablility to run Assess and Protect modes concurrently.
30
+
31
+
32
+ ## Getting Started
33
+
34
+ Existing Contrast Node.js agent users should install and update the Contrast
35
+ Node.js agent from [npm](https://www.npmjs.com/). The Contrast Node.js agent follows semantic
36
+ versioning (`major.minor.patch`).
37
+
38
+ An API key, provided by Contrast Security, is required for the agent to function.
39
+
40
+ Ensure you have installed the latest LTS (Long Term Support) version of [Node.js](http://nodejs.org/)
41
+
42
+ To install from npm using the command line (run from the app root directory):
43
+
44
+ ```sh
45
+ $ npm install @contrast/agent
46
+ ```
47
+
48
+ ## Usage
49
+
50
+ ### [CommonJS (CJS) Applications](https://nodejs.org/docs/latest-v12.x/api/modules.html)
51
+
52
+ CommonJS is the original Node.js module system. CJS modules are loaded with the
53
+ `const module = require('module')` syntax.
54
+
55
+ When instrumenting an application written this way, without EJS modules or ESM dependencies, use the following
56
+ method to start the application.
57
+
58
+ ```sh
59
+ node -r @contrast/agent app-main.js [app arguments]
60
+ ```
61
+
62
+ ### [ECMAScript (ESM) Applications](https://nodejs.org/docs/latest-v12.x/api/esm.html#esm_ecmascript_modules)
63
+
64
+ ECMAScript modules are the _new_ official standard format to package JavaScript
65
+ code for reuse. ES Modules are loaded with the `import module from 'module'`
66
+ syntax.
67
+
68
+ When instrumenting an application that utilizes ECMAScript Modules, use the
69
+ following methods to start the application. This is the appropriate method for
70
+ instrumenting an application that uses CJS, ESM, or a combination of both.
71
+
72
+ - Node versions `>=16.17.0` and `<18.19.0`:
73
+
74
+ ```sh
75
+ node --loader @contrast/agent app-main.mjs [app arguments]
76
+ ```
77
+
78
+ - Node versions `>=18.19.0` and `<20.0.0`, or versions `>=20.6.0`:
79
+
80
+ ```sh
81
+ node --import @contrast/agent app-main.mjs [app arguments]
82
+ ```
83
+
84
+
85
+ ### Configuration
86
+
87
+ #### File Locations
88
+
89
+ The agent will look for the `contrast_security.yaml` configuration file in the following locations and order:
90
+
91
+ 1. Within the processes current working directory, that is `${process.cwd()}/contrast_security.yaml`.
92
+
93
+ 1. OS-specific configuration directories.
94
+
95
+ - Unix and MacOS systems:
96
+
97
+ - `/etc/contrast/node/contrast_security.yaml`, _then_
98
+
99
+ - `/etc/contrast/contrast_security.yaml`
100
+
101
+ - Win32 systems:
102
+
103
+ - `${process.env.ProgramData}\contrast\node\contrast_security.yaml`, _then_
104
+
105
+ - `${process.env.ProgramData}\contrast\contrast_security.yaml`
106
+
107
+ 1. Unix home directory.
108
+
109
+ - `~/.config/contrast/node/contrast_security.yaml`, _then_
110
+
111
+ - `~/.config/contrast/contrast_security.yaml`
112
+
113
+ > Note: The optional `/node/` path segment is useful in cases where you want to organize configuration files by agent language:
114
+ > ```
115
+ > /etc
116
+ > /contrast
117
+ > /node/contrast_security.yaml
118
+ > /java/contrast_security.yaml
119
+ > /python/contrast_security.yaml
120
+ > ```
121
+
122
+ You can also specify the location of the configuration file with the `CONTRAST_CONFIG_PATH` environment variable:
123
+
124
+ ```sh
125
+ CONTRAST_CONFIG_PATH=/path/to/config.yaml node -r @contrast/agent app-main.js
126
+ ```
127
+
128
+ > Note: If `process.env.CONTRAST_CONFIG_PATH` set, the agent will look at that location _only_. If there is an issue reading the configuration file from this location the agent will not look in the standard locations described above, but instead do the following:
129
+ > 1. Halt instrumentation
130
+ > 2. Communicate an error
131
+ > 3. Run the application as if without Contrast
132
+
133
+ #### Minimum Configuration Option Requirements
134
+
135
+ The agent requires a minimum set of configuration options be set. They are described below as YAML.
136
+
137
+ ```yaml
138
+ api:
139
+ # Organization's API key
140
+ api_key: dCBvm46uEJAUV2musNFb357SnvqYrlq1
141
+ # Contrast user account service key
142
+ service_key: PZU499KK3YD4X2DT
143
+ # Contrast user account ID (In most cases, this is your login ID)
144
+ user_name: agent_d228a527-130c-18cc-93b8-20096136ba0b@UserOrg
145
+ # Address to the Contrast backend. This will vary.
146
+ url: https://app.contrastsecurity.com
147
+ ```
148
+
149
+ Visit https://agent.config.contrastsecurity.com/ to use our online tool for building your YAML file interactively.
150
+
151
+ For detailed installation and configuration instructions, see the [Node.js Agent documentation](https://docs.contrastsecurity.com/en/install-node-js.html).
@@ -0,0 +1,55 @@
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
+ // check to see if we're running with the correct flag (--loader or --import)
17
+ // for the right version of node. we do not function correctly when --loader
18
+ // is used for node >= 20 or when --import is used for node < 20.
19
+ //
20
+ export default function checkImportVsLoaderVsNodeVersion() {
21
+ // allow testing to ignore these restrictions
22
+ const noValidate = process.env.CSI_EXPOSE_CORE === 'no-validate';
23
+ let flag;
24
+
25
+ const { execArgv, version } = process;
26
+ // eslint-disable-next-line newline-per-chained-call
27
+ const [major, minor] = version.slice(1).split('.').map(Number);
28
+ for (let i = 0; i < execArgv.length; i++) {
29
+ const loader = execArgv[i];
30
+ const agent = execArgv[i + 1];
31
+ if (['--import', '--loader', '--experimental-loader'].includes(loader) && agent?.startsWith('@contrast/agent')) {
32
+ flag = loader;
33
+ if (noValidate) {
34
+ return { flag, msg: '' };
35
+ }
36
+ if (loader === '--import') {
37
+ // loader is --import
38
+ if (major === 18 && minor >= 19 || major >= 20) {
39
+ return { flag, msg: '' };
40
+ }
41
+ if (major <= 18 && minor < 19) {
42
+ return { flag, msg: 'Contrast requires node versions less than 18.19.0 use the --loader flag for ESM support' };
43
+ }
44
+ } else {
45
+ // loader is either --loader or --experimental-loader (same thing)
46
+ if (major >= 20 || major === 18 && minor >= 19) {
47
+ return { flag, msg: 'Contrast requires node versions >= 18.19.0 use the --import flag for ESM support' };
48
+ }
49
+ }
50
+ break;
51
+ }
52
+ }
53
+ // i think we only get here if flag === '--loader' and node < 20
54
+ return { flag, msg: '' };
55
+ }
@@ -0,0 +1,262 @@
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
+ import Module from 'node:module';
16
+ import { readFile as rf } from 'node:fs/promises';
17
+ import W from 'node:worker_threads';
18
+
19
+ const logLoad = 1;
20
+ const logResolve = 2;
21
+ const logRequireAll = 4;
22
+ const logApplication = 8; // eslint-disable-line no-unused-vars
23
+ let log = 0;
24
+ if (process.env.CSI_HOOKS_LOG) {
25
+ log = +process.env.CSI_HOOKS_LOG;
26
+ }
27
+
28
+ const [major, minor] = process.versions.node.split('.').map(it => +it);
29
+ const isLT16_12 = major < 16 || (major === 16 && minor < 12);
30
+
31
+ // import.meta.resolve('node-fetch') v20 became synchronous. not all that useful for now because
32
+ // of the significant break in behavior. but the function is great - it resolves a specifier to the
33
+ // file that would be loaded.
34
+ // file:///home/bruce/github/csi/rasp-v3/node_modules/node-fetch/src/index.js
35
+
36
+ const { default: core } = await import('./initialize.mjs');
37
+ const { esmHooks: { mappings, fixPath, getFileType } } = core;
38
+
39
+ const initialize = Module.register && async function(data) {
40
+ if (data?.port) {
41
+ data.port.on('message', _msg => {
42
+ // we don't currently send messages to the loader thread but when we do,
43
+ // this is where they will show up.
44
+ //console.log('ESM HOOK -> INIT -> MESSAGE', _msg);
45
+ });
46
+ data.port.postMessage({ type: 'keep-alive', data: 'hello from esm-hooks' });
47
+ data.port.unref();
48
+ // this is running in the loader thread. save thread info because it can't
49
+ // be set from the main thread.
50
+ if (W.isMainThread) {
51
+ throw new Error('initialize() called from main thread.');
52
+ }
53
+ core.threadInfo.isMainThread = false;
54
+ core.threadInfo.threadId = W.threadId;
55
+ core.threadInfo.port = data.port;
56
+ // the loader thread's post() sends via the port.
57
+ core.threadInfo.post = (type, data) => {
58
+ core.threadInfo.port.postMessage({ type, data });
59
+ };
60
+ }
61
+
62
+ log && console.log('ESM HOOK -> INIT');
63
+ // it's not clear to me why Module is present when initialize() is being
64
+ // executed in the loader thread. but it is. (code here originally loaded
65
+ // Module via createRequire(), but that is not necessary.)
66
+ const originalRequire = Module.prototype.require;
67
+ const originalCompile = Module.prototype._compile;
68
+ const originalExtensions = Module._extensions['.js'];
69
+ const originalLoad = Module._load;
70
+
71
+ // Module needs to be patched in the loader thread too. initialize() runs in
72
+ // that context.
73
+ Module.prototype.require = function(moduleId) {
74
+ (log & logRequireAll) && console.log(`CJS(init ${W.threadId}) -> require(${moduleId})`);
75
+ return originalRequire.call(this, moduleId);
76
+ };
77
+
78
+ Module.prototype._compile = function(code, filename) {
79
+ (log & logRequireAll) && console.log(`CJS(init ${W.threadId}) -> _compile(${filename})`);
80
+ return originalCompile.call(this, code, filename);
81
+ };
82
+
83
+ Module._extensions['.js'] = function(module, filename) {
84
+ (log & logRequireAll) && console.log(`CJS(init ${W.threadId}) -> _extensions[".js"]: ${filename}`);
85
+ return originalExtensions.call(this, module, filename);
86
+ };
87
+
88
+ Module._load = function(request, parent, isMain) {
89
+ (log & logLoad) && console.log(`CJS(init ${W.threadId}) -> _load(${request})`);
90
+ return originalLoad.call(this, request, parent, isMain);
91
+ };
92
+ };
93
+
94
+ const resolve = async function(specifier, context, nextResolve) {
95
+ (log & logResolve) && console.log(`ESM HOOK(${W.threadId}) -> RESOLVE -> ${specifier}`);
96
+ let isFlaggedToPatch = false;
97
+ if (context.parentURL) {
98
+ isFlaggedToPatch = context.parentURL.endsWith('csi-flag=', context.parentURL.length - 1);
99
+ }
100
+
101
+ // this needs to be generalized so it uses the execArgv value. the idea is to
102
+ // capture when the application actually starts. is this needed? i don't think so
103
+ // because the loaders aren't instantiated until esm-loader.mjs returns/register()s
104
+ // the hooks. i am leaving the comment here as a breadcrumb in case we need to
105
+ // capture the application start time. but we could 1) look at process.argv, 2)
106
+ // find the first argument, and 3) notice when that's loaded and capture that
107
+ // "now we are in the user's application".
108
+ //
109
+ //if (false) debugger;
110
+ //if (log & logApplication) {
111
+ // const RE = /test-integration-servers\/express.m?js(\?.+)?$/;
112
+ // if (specifier.match(RE) || context.parentURL?.match(RE)) {
113
+ // console.log(`ESM HOOK(${W.threadId}) -> RESOLVE -> ${specifier} from ${context.parentURL}`);
114
+ // }
115
+ //}
116
+
117
+ // this works for most of what we need to patch because they are not esm-native
118
+ // modules. but for esm-native modules, e.g., node-fetch, this won't work. they
119
+ // will need to be patched in the loader thread.
120
+ //
121
+ // We could walk up the parent chain to see if we should interpret this specifier
122
+ // as a module or commonjs, but it seems more straightforward to just always
123
+ // redirect. walking up the parent chain would let us avoid redirecting commonjs
124
+ // files. we could also call nextResolve() and let node tell us type of the file
125
+ // is but that would potentially create problems when other hooks, e.g., datadog,
126
+ // are in place.
127
+ //
128
+ // also we could check to see if this module's already been patched and skip this.
129
+ // not sure of that though; it might get loaded as a module, not a commonjs file,
130
+ // and that would be a problem. so when we start patching esm-native modules, we'll
131
+ // need a second "already patched" weakmap.
132
+ if (!isFlaggedToPatch && specifier in mappings) {
133
+ // eslint-disable-next-line prefer-const
134
+ let { url, format, target } = mappings[specifier];
135
+ // set flag to module or commonjs. i'm not sure this is needed but am keeping it
136
+ // in place until we have to implement esm-native module rewrites/wrapping. this
137
+ // is the point at which resolve() communicates to load().
138
+ //
139
+ // builtin's are probably the most likely to be loaded, so they're first.
140
+ // some tweaks might be needed when we start to patch esm-native modules
141
+ // in esm-native-code:
142
+ // https://nodejs.org/docs/latest-v20.x/api/esm.html#builtin-modules
143
+ // https://nodejs.org/docs/latest-v20.x/api/module.html#modulesyncbuiltinesmexports
144
+ if (target === 'builtin') {
145
+ url = `${url.href}?csi-flag=m`;
146
+ format = 'module';
147
+ } else if (target === 'commonjs') {
148
+ url = `${url.href}?csi-flag=c`;
149
+ format = getFileType(url) || format || 'commonjs';
150
+ } else if (target === 'module') {
151
+ url = `${url.href}?csi-flag=m`;
152
+ format = getFileType(url) || format || 'module';
153
+ } else {
154
+ throw new Error(`unexpected target ${target} for ${specifier}`);
155
+ }
156
+
157
+ return {
158
+ url,
159
+ format,
160
+ shortCircuit: true,
161
+ };
162
+ }
163
+
164
+ return protectedNextResolve(nextResolve, specifier, context);
165
+ };
166
+
167
+ // readFile is a live binding. we need to capture it so it won't be
168
+ // altered by any patching later.
169
+ const readFile = rf;
170
+
171
+ const load = async function(url, context, nextLoad) {
172
+ (log & logLoad) && console.log(`ESM HOOK(${W.threadId}) -> LOAD ${url}`);
173
+
174
+ const urlObject = new URL(url);
175
+
176
+ if (urlObject.searchParams.has('csi-flag')) {
177
+ // target type will, i think, be used to determine if this will require extra
178
+ // processing of some sort for esm-native modules.
179
+ // eslint-disable-next-line no-unused-vars
180
+ const targetType = urlObject.searchParams.get('csi-flag');
181
+ const { pathname } = urlObject;
182
+
183
+ (log & logLoad) && console.log(`ESM HOOK(${W.threadId}) -> LOAD -> CSI FLAG ${pathname}`);
184
+
185
+ const source = await readFile(fixPath(pathname), 'utf8');
186
+ return {
187
+ source,
188
+ format: 'module',
189
+ shortCircuit: true,
190
+ };
191
+ }
192
+
193
+ if (urlObject.pathname.endsWith('.node')) {
194
+ const metaUrl = JSON.stringify(url);
195
+ const addon = urlObject.pathname;
196
+ return {
197
+ source: `require("node:module").createRequire(${metaUrl})("${addon}")`,
198
+ format: 'commonjs',
199
+ shortCircuit: true,
200
+ };
201
+ }
202
+
203
+ // if it's not a builtin, a .node addon, or a flagged file, it needs to be
204
+ // rewritten if it's a module.
205
+ if (urlObject.pathname.match(/(.js|.mjs|.cjs)$/)) {
206
+ // if it's not a module it will be rewritten by the require hooks.
207
+ const type = getFileType(urlObject);
208
+ if (type !== 'module') {
209
+ return nextLoad(url, context);
210
+ }
211
+
212
+ const filename = fixPath(urlObject.pathname);
213
+ const source = await readFile(filename, 'utf8');
214
+
215
+ const rewriteOptions = {
216
+ filename,
217
+ isModule: type === 'module',
218
+ inject: true,
219
+ wrap: type !== 'module', // cannot wrap modules
220
+ };
221
+
222
+ core.threadInfo.post('rewrite', rewriteOptions);
223
+
224
+ const result = core.rewriter.rewrite(source, rewriteOptions);
225
+
226
+ return {
227
+ source: result.code,
228
+ format: type || 'commonjs', // don't know what else to do here. log?
229
+ shortCircuit: true,
230
+ };
231
+ }
232
+ // this never gets called, so hmmm.
233
+ return nextLoad(url, context);
234
+ };
235
+
236
+ // from esmock: git@github.com:iambumblehead/esmock.git
237
+ //
238
+ // new versions of node: when multiple loaders are used and context
239
+ // is passed to nextResolve, the process crashes in a recursive call
240
+ // see: /esmock/issues/#48
241
+ //
242
+ // old versions of node: if context.parentURL is defined, and context
243
+ // is not passed to nextResolve, the tests fail
244
+ //
245
+ // later versions of node v16 include 'node-addons'
246
+ async function protectedNextResolve(nextResolve, specifier, context) {
247
+ if (context.parentURL) {
248
+ if (context.conditions.at(-1) === 'node-addons' || context.importAssertions || isLT16_12) {
249
+ return nextResolve(specifier, context);
250
+ }
251
+ }
252
+
253
+ return nextResolve(specifier);
254
+ }
255
+
256
+ export {
257
+ load,
258
+ resolve,
259
+ initialize,
260
+ // TODO: add for testing/verification
261
+ //loaderIsVerified as default
262
+ };
@@ -0,0 +1,149 @@
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
+ import Module from 'node:module';
17
+ import W from 'node:worker_threads';
18
+ import EventEmitter from 'node:events';
19
+
20
+ import checkImportVsLoaderVsNodeVersion from './check-flag-vs-node-version.mjs';
21
+
22
+ // might need to get exclude function here as opposed to deep within initialize
23
+ // execution.
24
+ // 1) core should be a singleton so it's always shared between modules
25
+ // 2) core should include the redirect map.
26
+ // 3) is the exclude function needed here as we move to async loading? if so,
27
+ // this will need to add the root directory(ies) to an exclude list.
28
+
29
+ const logLoad = 1;
30
+ // eslint-disable-next-line no-unused-vars
31
+ const logResolve = 2;
32
+ const logRequireAll = 4;
33
+ let log = 0;
34
+ if (process.env.CSI_HOOKS_LOG) {
35
+ log = +process.env.CSI_HOOKS_LOG;
36
+ }
37
+
38
+ // verify that we're running with the correct flag for the version of node.
39
+ const { flag, msg } = checkImportVsLoaderVsNodeVersion();
40
+ if (msg) {
41
+ console.error(msg);
42
+ throw new Error(msg);
43
+ // belt and suspenders
44
+ // eslint-disable-next-line no-unreachable
45
+ process.exit(1); // eslint-disable-line no-process-exit
46
+ }
47
+
48
+ //
49
+ // if CJS hooks are not already setup, setup CJS hooks. each thread must patch
50
+ // Module. I don't think this will be called more than once, but the flag is
51
+ // in place just in case.
52
+ //
53
+ let core;
54
+
55
+ if (!core) {
56
+ core = (await import('./initialize.mjs')).default;
57
+
58
+ // most of this can be removed; it's here for debugging. but considering the relatively
59
+ // low cost compared with loading a module, i'm leaving it in.
60
+ (log & logRequireAll) && console.log('ESM-LOADER executing CJS Module patching');
61
+ const originalRequire = Module.prototype.require;
62
+ const originalCompile = Module.prototype._compile;
63
+ const originalExtensions = Module._extensions['.js'];
64
+ const originalLoad = Module._load;
65
+ if (originalLoad?.name !== '__loadOverride') {
66
+ // debugger; // this is a good place to make a quick check if things aren't working
67
+ }
68
+
69
+ Module.prototype.require = function(moduleId) {
70
+ (log & logRequireAll) && console.log('CJS -> require() called for', moduleId);
71
+ return originalRequire.call(this, moduleId);
72
+ };
73
+
74
+ Module.prototype._compile = function(code, filename) {
75
+ (log & logRequireAll) && console.log('CJS -> _compile() called for', filename);
76
+ return originalCompile.call(this, code, filename);
77
+ };
78
+
79
+ Module._extensions['.js'] = function(module, filename) {
80
+ (log & logRequireAll) && console.log('CJS -> _extensions[".js"] called for', filename);
81
+ return originalExtensions.call(this, module, filename);
82
+ };
83
+
84
+ Module._load = function(request, parent, isMain) {
85
+ (log & logLoad) && console.log(`CJS(${W.threadId}) -> _load() ${request}`);
86
+ return originalLoad.call(this, request, parent, isMain);
87
+ };
88
+ }
89
+
90
+ core.threadInfo.syncEmitter = new EventEmitter();
91
+ // abstract how notifications are posted so that the non-loader
92
+ // code is not coupled to the implementation. the loader-thread
93
+ // complement of this is in esm-hooks.
94
+ //
95
+ // specifically, this is the main thread and post() directly emits
96
+ // the event while in the loader thread post() uses post.postMessage()
97
+ // to communicate back to this (the main) thread.
98
+ core.threadInfo.post = (type, data) => {
99
+ core.threadInfo.syncEmitter.emit(type, data);
100
+ };
101
+
102
+ //
103
+ // setup ESM hooks
104
+ //
105
+ // if register exists this is 20.6.0 or later. we do not support
106
+ // node 20 prior to 20.6.0 (or 20.9.0 when 20 became LTS).
107
+ // news flash: backported register to node 18.19.0, so checking register
108
+ // is no longer enough.
109
+ //
110
+ if (Module.register && flag === '--import') {
111
+ // this file should never be executed in the loader thread; this file creates
112
+ // the loader thread by calling register(). the loader thread must create its
113
+ // own copy of threadInfo and insert it into core.
114
+ if (!core.threadInfo.isMainThread) {
115
+ // only get an error in CI on node v18.
116
+ throw new Error('esm-loader.mjs should not be executed in the loader thread');
117
+ }
118
+ const { MessageChannel } = await import('node:worker_threads');
119
+ const { port1, port2 } = new MessageChannel();
120
+
121
+
122
+ core.threadInfo.port = port1;
123
+ // messages received on the port are re-emitted as standard node events. the
124
+ // body must contain the type; data can be undefined.
125
+ core.threadInfo.missingTypeCount = 0;
126
+ core.threadInfo.port.on('message', (body) => {
127
+ const { type, data } = body;
128
+ if (type) {
129
+ core.threadInfo.syncEmitter.emit(type, data);
130
+ } else {
131
+ core.threadInfo.missingTypeCount += 1;
132
+ }
133
+ });
134
+ core.threadInfo.port.unref();
135
+
136
+ // record the URL of the entry point.
137
+ core.threadInfo.url = import.meta.url;
138
+
139
+ // get relative URL
140
+ const url = new URL('./esm-hooks.mjs', import.meta.url);
141
+ await Module.register(url.href, import.meta.url, { data: { port: port2 }, transferList: [port2] });
142
+ }
143
+
144
+ // it's not possible to conditionally export, but exporting undefined
145
+ // values is close.
146
+ import * as hooks from './esm-hooks.mjs';
147
+ const finalHooks = (Module.register && flag === '--import') ? {} : hooks;
148
+ const { load, resolve } = finalHooks;
149
+ export { load, resolve };
package/lib/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*
2
- * Copyright: 2023 Contrast Security, Inc
2
+ * Copyright: 2024 Contrast Security, Inc
3
3
  * Contact: support@contrastsecurity.com
4
4
  * License: Commercial
5
5
 
@@ -53,15 +53,22 @@ agentify((core) => {
53
53
  };
54
54
 
55
55
  core.npmVersionRange = npmEngine;
56
+ require('@contrast/telemetry')(core);
56
57
  require('@contrast/assess')(core);
57
58
  require('@contrast/architecture-components')(core);
58
59
  require('@contrast/library-analysis')(core);
59
60
  require('@contrast/route-coverage')(core);
60
61
  require('@contrast/protect')(core); // protect loads lastly, being the only non-passive feature
62
+
63
+ if (process.env.CSI_EXPOSE_CORE) {
64
+ const coreKey = Symbol.for('contrast:core');
65
+ global[coreKey] = core;
66
+ }
61
67
  }, {
62
68
  installOrder: [
63
69
  'reporter',
64
70
  'startupValidation',
71
+ 'telemetry',
65
72
  'contrastMethods',
66
73
  'deadzones',
67
74
  'scopes',
@@ -0,0 +1,132 @@
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
+ // this file is the bootstrap for starting the agent's loader. it needs to be executed
17
+ // in the main thread and the loader thread. it should not try to load all the modules
18
+ // required to fully instantiate the agent, but only those required to make the loader
19
+ // work.
20
+
21
+ import { default as semver } from 'semver';
22
+ import { default as Module } from 'node:module';
23
+ import { isMainThread, threadId } from 'node:worker_threads';
24
+
25
+ const require = Module.createRequire(import.meta.url);
26
+
27
+ const {
28
+ name: agentName,
29
+ version: agentVersion,
30
+ engines: {
31
+ node: nodeEngine,
32
+ npm: npmEngine
33
+ }
34
+ } = require('../package.json');
35
+
36
+ const installOrder = [
37
+ 'reporter',
38
+ 'startupValidation',
39
+ 'contrastMethods',
40
+ 'deadzones',
41
+ 'scopes',
42
+ 'sources',
43
+ 'architectureComponents',
44
+ 'assess',
45
+ 'protect',
46
+ 'depHooks',
47
+ 'esmHooks',
48
+ 'routeCoverage',
49
+ 'libraryAnalysis',
50
+ 'heapSnapshots',
51
+ 'rewriteHooks',
52
+ 'functionHooks',
53
+ 'metrics',
54
+ ];
55
+
56
+ //
57
+ // In order to avoid breaking every test and function in the agent, agentify
58
+ // isn't changed at all; initialize() replaces it with new behavior.
59
+ //
60
+ import { loadModules, startAgent } from '@contrast/agentify/lib/initialize.mjs';
61
+
62
+ let core;
63
+
64
+ if (!core) {
65
+ // self-identification
66
+ const me = import.meta.url;
67
+ // we can determine when the app is being run by examining this
68
+ const argv = process.argv.slice();
69
+ // we can tell how the agent was started by examining this. --loader/--import must
70
+ // be correct for the node version.
71
+ const execArgv = process.execArgv.slice();
72
+
73
+ core = { agentName, agentVersion, me, argv, execArgv };
74
+
75
+ core = await loadModules({ core, options: {} });
76
+
77
+ core = await startAgent({ core, options: { executor, installOrder } });
78
+
79
+ // self identification
80
+ // for communications between the main and loader thread (if present)
81
+ core.threadInfo = {
82
+ isMainThread,
83
+ threadId,
84
+ port: undefined, // filled in if there's a loader thread
85
+ post: () => {}, // replaced by appropriate function
86
+ };
87
+
88
+ if (process.env.CSI_EXPOSE_CORE) {
89
+ const coreKey = Symbol.for('contrast:core');
90
+ global[coreKey] = core;
91
+ }
92
+
93
+ }
94
+
95
+
96
+ export default core;
97
+
98
+ async function executor(core) {
99
+ if (!semver.satisfies(process.version, nodeEngine)) {
100
+ let validRanges = '';
101
+ nodeEngine.split('||').forEach((range) => {
102
+ const minVersion = semver.minVersion(range).toString();
103
+ const maxVersion = range.split('<').pop()
104
+ .trim();
105
+ validRanges += `${minVersion} and ${maxVersion}, `;
106
+ });
107
+ throw new Error(`Contrast supports Node LTS versions between ${validRanges}but detected ${process.version}.`);
108
+ }
109
+
110
+ core.startupValidation = {
111
+ /**
112
+ * This will run after we onboard to the UI and pick up any remote settings.
113
+ */
114
+ install() {
115
+ if (
116
+ !core.config.getEffectiveValue('assess.enable') &&
117
+ !core.config.getEffectiveValue('protect.enable')
118
+ ) {
119
+ throw new Error('Neither Assess nor Protect are enabled. Check local configuration and UI settings');
120
+ }
121
+ }
122
+ };
123
+
124
+ core.npmVersionRange = npmEngine;
125
+ require('@contrast/assess')(core);
126
+ require('@contrast/architecture-components')(core);
127
+ require('@contrast/library-analysis')(core);
128
+ require('@contrast/route-coverage')(core);
129
+ require('@contrast/protect')(core); // protect loads last; the other features are all passive
130
+
131
+ return core; // should this return core or just null/undefined?
132
+ }
package/package.json CHANGED
@@ -1,28 +1,33 @@
1
1
  {
2
2
  "name": "@contrast/agent",
3
- "version": "5.0.0-beta.9",
3
+ "version": "5.0.1",
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)",
7
7
  "files": [
8
8
  "lib/"
9
9
  ],
10
+ "exports": {
11
+ "import": "./lib/esm-loader.mjs",
12
+ "require": "./lib/index.js"
13
+ },
10
14
  "main": "lib/index.js",
11
15
  "types": "lib/index.d.ts",
12
16
  "engines": {
13
17
  "npm": ">=6.13.7 <7 || >= 8.3.1",
14
- "node": ">=14.15.0 <15 || >=16.9.1 <17 || >=18.7.0 <19 || >=20.5.0 <21"
18
+ "node": ">=14.15.0 <15 || >=16.9.1 <17 || >=18.7.0 <19 || >=20.6.0 <21"
15
19
  },
16
20
  "scripts": {
17
21
  "test": "../scripts/test.sh"
18
22
  },
19
23
  "dependencies": {
20
- "@contrast/agentify": "1.16.0",
21
- "@contrast/architecture-components": "1.13.1",
22
- "@contrast/assess": "1.17.0",
23
- "@contrast/library-analysis": "1.14.0",
24
- "@contrast/protect": "1.28.1",
25
- "@contrast/route-coverage": "1.12.0",
24
+ "@contrast/agentify": "1.18.1",
25
+ "@contrast/architecture-components": "1.14.0",
26
+ "@contrast/assess": "1.20.0",
27
+ "@contrast/library-analysis": "1.15.1",
28
+ "@contrast/protect": "1.30.1",
29
+ "@contrast/route-coverage": "1.14.0",
30
+ "@contrast/telemetry": "1.2.0",
26
31
  "semver": "^7.3.7"
27
32
  }
28
33
  }