@contrast/agent 5.2.2 → 5.4.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.
package/README.md CHANGED
@@ -47,40 +47,49 @@ $ npm install @contrast/agent
47
47
 
48
48
  ## Usage
49
49
 
50
- ### [CommonJS (CJS) Applications](https://nodejs.org/docs/latest-v12.x/api/modules.html)
50
+ ### With LTS (Long Term Support) Node.js Versions
51
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.
52
+ [Node.js policy](https://nodejs.org/en/about/previous-releases/) is that production applications
53
+ should use only Active LTS or Maintenance LTS releases. All current LTS versions of Node.js support
54
+ ECMAScript modules (ESM) and CommonJS modules (CJS) with the `--import` flag. To ensure that the
55
+ agent can instrument your application, use:
57
56
 
58
57
  ```sh
59
- node -r @contrast/agent app-main.js [app arguments]
58
+ node --import @contrast/agent app-main [app arguments]
60
59
  ```
61
60
 
62
- ### [ECMAScript (ESM) Applications](https://nodejs.org/docs/latest-v12.x/api/esm.html#esm_ecmascript_modules)
61
+ Notes:
62
+ - `--import` should be used for Node.js LTS (Active and Maintenance) versions `>=18.19.0`
63
+ - Node.js versions `>=20.0.0` and `<20.6.0` are not supported
64
+
65
+ ### With end-of-life Node.js Versions
66
+
67
+ When using the agent with end-of-life Node.js versions, use either the `--loader` or
68
+ `--require` flag, depending on the version of Node.js and the module system used.
63
69
 
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.
70
+ Use the `--loader` flag for Node.js versions `>=16.17.0` and `<18.19.0`.
71
+
72
+ ```sh
73
+ node --loader @contrast/agent app-main.mjs [app arguments]
74
+ ```
67
75
 
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.
76
+ Use the `--require` (`-r`) flag for Node.js versions `<16.17.0`.
71
77
 
72
- - Node versions `>=16.17.0` and `<18.19.0`:
78
+ ```sh
79
+ node -r @contrast/agent app-main [app arguments]
80
+ ```
73
81
 
74
- ```sh
75
- node --loader @contrast/agent app-main.mjs [app arguments]
76
- ```
82
+ Note:
83
+ - `-r` will still work for Node.js versions that have no ESM modules or dependencies.
77
84
 
78
- - Node versions `>=18.19.0` and `<20.0.0`, or versions `>=20.6.0`:
85
+ ### With @contrast/agent v4
79
86
 
80
- ```sh
81
- node --import @contrast/agent app-main.mjs [app arguments]
82
- ```
87
+ The Contrast Node.js agent v4 is still available for use, but does not support ESM
88
+ modules. To use the v4 agent, use the `--require` (`-r`) flag.
83
89
 
90
+ ```sh
91
+ node -r @contrast/agent app-main [app arguments]
92
+ ```
84
93
 
85
94
  ### Configuration
86
95
 
@@ -14,148 +14,13 @@
14
14
  */
15
15
 
16
16
  import Module from 'node:module';
17
- import W from 'node:worker_threads';
18
- import EventEmitter from 'node:events';
19
- import * as hooks from './esm-hooks.mjs';
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
-
41
- if (msg) {
42
- console.error(msg);
43
- throw new Error(msg);
44
- // belt and suspenders
45
- // eslint-disable-next-line no-unreachable
46
- process.exit(1); // eslint-disable-line no-process-exit
47
- }
48
-
49
- //
50
- // if CJS hooks are not already setup, setup CJS hooks. each thread must patch
51
- // Module. I don't think this will be called more than once, but the flag is
52
- // in place just in case.
53
- //
54
-
55
- const core = (await import('./initialize.mjs')).default;
17
+ const require = Module.createRequire(import.meta.url);
18
+ const { startAgent } = require('./start-agent');
56
19
  let load, resolve;
57
20
 
58
- if (core) {
59
- // most of this can be removed; it's here for debugging. but considering the relatively
60
- // low cost compared with loading a module, i'm leaving it in.
61
- (log & logRequireAll) && console.log('ESM-LOADER executing CJS Module patching');
62
- const originalRequire = Module.prototype.require;
63
- const originalCompile = Module.prototype._compile;
64
- const originalExtensions = Module._extensions['.js'];
65
- const originalLoad = Module._load;
66
- if (originalLoad?.name !== '__loadOverride') {
67
- // debugger; // this is a good place to make a quick check if things aren't working
68
- }
69
-
70
- Module.prototype.require = function(moduleId) {
71
- (log & logRequireAll) && console.log('CJS -> require() called for', moduleId);
72
- return originalRequire.call(this, moduleId);
73
- };
74
-
75
- Module.prototype._compile = function(code, filename) {
76
- (log & logRequireAll) && console.log('CJS -> _compile() called for', filename);
77
- return originalCompile.call(this, code, filename);
78
- };
79
-
80
- Module._extensions['.js'] = function(module, filename) {
81
- (log & logRequireAll) && console.log('CJS -> _extensions[".js"] called for', filename);
82
- return originalExtensions.call(this, module, filename);
83
- };
84
-
85
- Module._load = function(request, parent, isMain) {
86
- (log & logLoad) && console.log(`CJS(${W.threadId}) -> _load() ${request}`);
87
- return originalLoad.call(this, request, parent, isMain);
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
- // we only need to do this if there is a background thread.
144
- // The esmHooks component of the main agent will send TS settings update to the loader agent via the port.
145
- // To get the loader agent components to update we just need to forward the settings using `.messages` emitter.
146
- core.messages.on(Event.SERVER_SETTINGS_UPDATE, (msg) => {
147
- core.threadInfo.port.postMessage({
148
- type: Event.SERVER_SETTINGS_UPDATE,
149
- ...msg,
150
- });
151
- });
152
- }
153
- // it's not possible to conditionally export, but exporting undefined
154
- // values is close.
155
-
156
-
157
- const finalHooks = (Module.register && flag === '--import') ? {} : hooks;
158
- ({ load, resolve } = finalHooks);
159
- }
21
+ try {
22
+ const core = await startAgent({ type: 'esm' });
23
+ ({ load, resolve } = core.esmHooks.hooks); // remove when all LTS versions support register
24
+ } catch (err) { } // eslint-disable-line no-empty
160
25
 
161
- export { load, resolve };
26
+ export { load, resolve }; // remove when all LTS versions support register
package/lib/index.js CHANGED
@@ -15,73 +15,8 @@
15
15
 
16
16
  'use strict';
17
17
 
18
- const semver = require('semver');
19
- const {
20
- name: agentName,
21
- version: agentVersion,
22
- engines: {
23
- node: nodeEngine,
24
- npm: npmEngine
25
- }
26
- } = require('../package.json');
27
- const agentify = require('@contrast/agentify')({ agentName, agentVersion });
18
+ const { startAgent } = require('./start-agent');
28
19
 
29
- agentify((core) => {
30
- if (!semver.satisfies(process.version, nodeEngine)) {
31
- let validRanges = '';
32
- nodeEngine.split('||').forEach((range) => {
33
- const minVersion = semver.minVersion(range).toString();
34
- const maxVersion = range.split('<').pop()
35
- .trim();
36
- validRanges += `${minVersion} and ${maxVersion}, `;
37
- });
38
- throw new Error(`Contrast only officially supports Node LTS versions between ${validRanges}but detected ${process.version}.`);
39
- }
40
-
41
- core.startupValidation = {
42
- /**
43
- * This will run after we onboard to the UI and pick up any remote settings.
44
- */
45
- install() {
46
- if (
47
- !core.config.getEffectiveValue('assess.enable') &&
48
- !core.config.getEffectiveValue('protect.enable')
49
- ) {
50
- throw new Error('Neither Assess nor Protect are enabled. Check local configuration and UI settings');
51
- }
52
- }
53
- };
54
-
55
- core.npmVersionRange = npmEngine;
56
- require('@contrast/telemetry')(core);
57
- require('@contrast/assess')(core);
58
- require('@contrast/architecture-components')(core);
59
- require('@contrast/library-analysis')(core);
60
- require('@contrast/route-coverage')(core);
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
- }
67
- }, {
68
- installOrder: [
69
- 'reporter',
70
- 'startupValidation',
71
- 'telemetry',
72
- 'contrastMethods',
73
- 'deadzones',
74
- 'scopes',
75
- 'sources',
76
- 'architectureComponents',
77
- 'assess',
78
- 'protect',
79
- 'depHooks',
80
- 'routeCoverage',
81
- 'libraryAnalysis',
82
- 'heapSnapshots',
83
- 'rewriteHooks',
84
- 'functionHooks',
85
- 'metrics',
86
- ],
87
- });
20
+ try {
21
+ startAgent({ type: 'cjs' });
22
+ } catch (err) {} // eslint-disable-line no-empty
@@ -0,0 +1,146 @@
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 process = require('process');
19
+ const semver = require('semver');
20
+ const _agentify = require('@contrast/agentify');
21
+ const {
22
+ name: agentName,
23
+ version: agentVersion,
24
+ engines: {
25
+ node: nodeEngine,
26
+ npm: npmEngine
27
+ }
28
+ } = require('../package.json');
29
+
30
+ function initCore() {
31
+ if (!semver.satisfies(process.version, nodeEngine)) {
32
+ let validRanges = '';
33
+ nodeEngine.split('||').forEach((range) => {
34
+ const minVersion = semver.minVersion(range).toString();
35
+ const maxVersion = range.split('<').pop()
36
+ .trim();
37
+ validRanges += `${minVersion} and ${maxVersion}, `;
38
+ });
39
+ throw new Error(`Contrast only officially supports Node LTS versions between ${validRanges}but detected ${process.version}.`);
40
+ }
41
+
42
+ checkImportVsLoaderVsNodeVersion();
43
+
44
+ const core = { agentName, agentVersion };
45
+
46
+ core.startupValidation = {
47
+ /**
48
+ * This will run after we onboard to the UI and pick up any remote settings.
49
+ */
50
+ install() {
51
+ if (
52
+ !core.config.getEffectiveValue('assess.enable') &&
53
+ !core.config.getEffectiveValue('protect.enable')
54
+ ) {
55
+ throw new Error('Neither Assess nor Protect are enabled. Check local configuration and UI settings');
56
+ }
57
+ }
58
+ };
59
+
60
+ core.npmVersionRange = npmEngine;
61
+
62
+ if (process.env.CSI_EXPOSE_CORE) {
63
+ global[Symbol.for('contrast:core')] = core;
64
+ }
65
+
66
+ return core;
67
+ }
68
+
69
+ function loadFeatures(core) {
70
+ require('@contrast/assess')(core);
71
+ require('@contrast/architecture-components')(core);
72
+ require('@contrast/library-analysis')(core);
73
+ require('@contrast/route-coverage')(core);
74
+ require('@contrast/protect')(core);
75
+ }
76
+
77
+ /**
78
+ * check to see if we're running with the correct flag (--loader or --import)
79
+ * for the right version of node. we do not function correctly when --loader
80
+ * is used for node >= 18 or when --import is used for node < 18.
81
+ */
82
+ function checkImportVsLoaderVsNodeVersion() {
83
+ // allow testing to ignore these restrictions
84
+ const noValidate = process.env.CSI_EXPOSE_CORE === 'no-validate';
85
+ let msg;
86
+
87
+ const { execArgv, version } = process;
88
+ // eslint-disable-next-line newline-per-chained-call
89
+ const [major, minor] = version.slice(1).split('.').map(Number);
90
+ for (let i = 0; i < execArgv.length; i++) {
91
+ const loader = execArgv[i];
92
+ const agent = execArgv[i + 1];
93
+ if (['--import', '--loader', '--experimental-loader'].includes(loader) && agent?.startsWith('@contrast/agent')) {
94
+ if (noValidate) {
95
+ return;
96
+ }
97
+ if (loader === '--import') {
98
+ // loader is --import
99
+ if (major === 18 && minor >= 19 || major >= 20) {
100
+ return;
101
+ }
102
+ if (major <= 18 && minor < 19) {
103
+ msg = 'Contrast requires node versions less than 18.19.0 use the --loader flag for ESM support';
104
+ }
105
+ } else {
106
+ // loader is either --loader or --experimental-loader (same thing)
107
+ if (major >= 20 || major === 18 && minor >= 19) {
108
+ msg = 'Contrast requires node versions >= 18.19.0 use the --import flag for ESM support';
109
+ }
110
+ }
111
+ break;
112
+ }
113
+ }
114
+
115
+ if (msg) throw new Error(msg);
116
+ }
117
+
118
+ async function startAgent({ type = 'cjs' } = {}) {
119
+ const core = initCore();
120
+ const agentify = _agentify(core);
121
+
122
+ return agentify(loadFeatures, {
123
+ installOrder: [
124
+ 'reporter',
125
+ 'startupValidation',
126
+ 'contrastMethods',
127
+ 'deadzones',
128
+ 'scopes',
129
+ 'sources',
130
+ 'architectureComponents',
131
+ 'assess',
132
+ 'protect',
133
+ 'depHooks',
134
+ 'routeCoverage',
135
+ 'libraryAnalysis',
136
+ 'heapSnapshots',
137
+ 'metrics',
138
+ 'rewriteHooks',
139
+ 'functionHooks',
140
+ 'esmHooks',
141
+ ],
142
+ type
143
+ });
144
+ }
145
+
146
+ module.exports = { startAgent };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contrast/agent",
3
- "version": "5.2.2",
3
+ "version": "5.4.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)",
@@ -21,13 +21,13 @@
21
21
  "test": "../scripts/test.sh"
22
22
  },
23
23
  "dependencies": {
24
- "@contrast/agentify": "1.20.1",
25
- "@contrast/architecture-components": "1.16.0",
26
- "@contrast/assess": "1.24.2",
27
- "@contrast/library-analysis": "1.17.0",
28
- "@contrast/protect": "1.32.1",
29
- "@contrast/route-coverage": "1.16.0",
30
- "@contrast/telemetry": "1.4.1",
24
+ "@contrast/agentify": "1.22.0",
25
+ "@contrast/architecture-components": "1.17.0",
26
+ "@contrast/assess": "1.26.0",
27
+ "@contrast/library-analysis": "1.18.0",
28
+ "@contrast/protect": "1.34.0",
29
+ "@contrast/route-coverage": "1.17.0",
30
+ "@contrast/telemetry": "1.5.1",
31
31
  "semver": "^7.3.7"
32
32
  }
33
33
  }
@@ -1,61 +0,0 @@
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
- const { execArgv, version, env: { NODE_OPTIONS } } = process;
25
-
26
- // eslint-disable-next-line newline-per-chained-call
27
- const [major, minor] = version.slice(1).split('.').map(Number);
28
-
29
- const _execArgs = [
30
- ...(NODE_OPTIONS?.split?.(/\s+/).map((v) => v.trim()) || []),
31
- ...execArgv,
32
- ]
33
-
34
- for (let i = 0; i < _execArgs.length; i++) {
35
- const loader = _execArgs[i];
36
- const agent = _execArgs[i + 1];
37
- if (['--import', '--loader', '--experimental-loader'].includes(loader) && agent?.startsWith('@contrast/agent')) {
38
- flag = loader;
39
- if (noValidate) {
40
- return { flag, msg: '' };
41
- }
42
- if (loader === '--import') {
43
- // loader is --import
44
- if (major === 18 && minor >= 19 || major >= 20) {
45
- return { flag, msg: '' };
46
- }
47
- if (major <= 18 && minor < 19) {
48
- return { flag, msg: 'Contrast requires node versions less than 18.19.0 use the --loader flag for ESM support' };
49
- }
50
- } else {
51
- // loader is either --loader or --experimental-loader (same thing)
52
- if (major >= 20 || major === 18 && minor >= 19) {
53
- return { flag, msg: 'Contrast requires node versions >= 18.19.0 use the --import flag for ESM support' };
54
- }
55
- }
56
- break;
57
- }
58
- }
59
- // i think we only get here if flag === '--loader' and node < 20
60
- return { flag, msg: '' };
61
- }
package/lib/esm-hooks.mjs DELETED
@@ -1,266 +0,0 @@
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
-
32
- // from esmock: git@github.com:iambumblehead/esmock.git
33
- //
34
- // new versions of node: when multiple loaders are used and context
35
- // is passed to nextResolve, the process crashes in a recursive call
36
- // see: /esmock/issues/#48
37
- //
38
- // old versions of node: if context.parentURL is defined, and context
39
- // is not passed to nextResolve, the tests fail
40
- //
41
- // later versions of node v16 include 'node-addons'
42
- async function protectedNextResolve(nextResolve, specifier, context) {
43
- if (context.parentURL) {
44
- if (context.conditions.at(-1) === 'node-addons' || context.importAssertions || isLT16_12) {
45
- return nextResolve(specifier, context);
46
- }
47
- }
48
-
49
- return nextResolve(specifier);
50
- }
51
-
52
- // import.meta.resolve('node-fetch') v20 became synchronous. not all that useful for now because
53
- // of the significant break in behavior. but the function is great - it resolves a specifier to the
54
- // file that would be loaded.
55
- // file:///home/bruce/github/csi/rasp-v3/node_modules/node-fetch/src/index.js
56
-
57
- const { default: core } = await import('./initialize.mjs');
58
- let load, resolve, initialize;
59
-
60
- if (core) {
61
- const { esmHooks: { mappings, fixPath, getFileType } } = core;
62
- initialize = Module.register && async function(data) {
63
- if (data?.port) {
64
- data.port.on('message', _msg => {
65
- // we don't currently send messages to the loader thread but when we do,
66
- // this is where they will show up.
67
- //console.log('ESM HOOK -> INIT -> MESSAGE', _msg);
68
- });
69
- data.port.postMessage({ type: 'keep-alive', data: 'hello from esm-hooks' });
70
- data.port.unref();
71
- // this is running in the loader thread. save thread info because it can't
72
- // be set from the main thread.
73
- if (W.isMainThread) {
74
- throw new Error('initialize() called from main thread.');
75
- }
76
- core.threadInfo.isMainThread = false;
77
- core.threadInfo.threadId = W.threadId;
78
- core.threadInfo.port = data.port;
79
- // the loader thread's post() sends via the port.
80
- core.threadInfo.post = (type, data) => {
81
- core.threadInfo.port.postMessage({ type, data });
82
- };
83
- }
84
-
85
- log && console.log('ESM HOOK -> INIT');
86
- // it's not clear to me why Module is present when initialize() is being
87
- // executed in the loader thread. but it is. (code here originally loaded
88
- // Module via createRequire(), but that is not necessary.)
89
- const originalRequire = Module.prototype.require;
90
- const originalCompile = Module.prototype._compile;
91
- const originalExtensions = Module._extensions['.js'];
92
- const originalLoad = Module._load;
93
-
94
- // Module needs to be patched in the loader thread too. initialize() runs in
95
- // that context.
96
- Module.prototype.require = function(moduleId) {
97
- (log & logRequireAll) && console.log(`CJS(init ${W.threadId}) -> require(${moduleId})`);
98
- return originalRequire.call(this, moduleId);
99
- };
100
-
101
- Module.prototype._compile = function(code, filename) {
102
- (log & logRequireAll) && console.log(`CJS(init ${W.threadId}) -> _compile(${filename})`);
103
- return originalCompile.call(this, code, filename);
104
- };
105
-
106
- Module._extensions['.js'] = function(module, filename) {
107
- (log & logRequireAll) && console.log(`CJS(init ${W.threadId}) -> _extensions[".js"]: ${filename}`);
108
- return originalExtensions.call(this, module, filename);
109
- };
110
-
111
- Module._load = function(request, parent, isMain) {
112
- (log & logLoad) && console.log(`CJS(init ${W.threadId}) -> _load(${request})`);
113
- return originalLoad.call(this, request, parent, isMain);
114
- };
115
- };
116
-
117
- resolve = async function(specifier, context, nextResolve) {
118
- (log & logResolve) && console.log(`ESM HOOK(${W.threadId}) -> RESOLVE -> ${specifier}`);
119
- let isFlaggedToPatch = false;
120
- if (context.parentURL) {
121
- isFlaggedToPatch = context.parentURL.endsWith('csi-flag=', context.parentURL.length - 1);
122
- }
123
-
124
- // this needs to be generalized so it uses the execArgv value. the idea is to
125
- // capture when the application actually starts. is this needed? i don't think so
126
- // because the loaders aren't instantiated until esm-loader.mjs returns/register()s
127
- // the hooks. i am leaving the comment here as a breadcrumb in case we need to
128
- // capture the application start time. but we could 1) look at process.argv, 2)
129
- // find the first argument, and 3) notice when that's loaded and capture that
130
- // "now we are in the user's application".
131
- //
132
- //if (false) debugger;
133
- //if (log & logApplication) {
134
- // const RE = /test-integration-servers\/express.m?js(\?.+)?$/;
135
- // if (specifier.match(RE) || context.parentURL?.match(RE)) {
136
- // console.log(`ESM HOOK(${W.threadId}) -> RESOLVE -> ${specifier} from ${context.parentURL}`);
137
- // }
138
- //}
139
-
140
- // this works for most of what we need to patch because they are not esm-native
141
- // modules. but for esm-native modules, e.g., node-fetch, this won't work. they
142
- // will need to be patched in the loader thread.
143
- //
144
- // We could walk up the parent chain to see if we should interpret this specifier
145
- // as a module or commonjs, but it seems more straightforward to just always
146
- // redirect. walking up the parent chain would let us avoid redirecting commonjs
147
- // files. we could also call nextResolve() and let node tell us type of the file
148
- // is but that would potentially create problems when other hooks, e.g., datadog,
149
- // are in place.
150
- //
151
- // also we could check to see if this module's already been patched and skip this.
152
- // not sure of that though; it might get loaded as a module, not a commonjs file,
153
- // and that would be a problem. so when we start patching esm-native modules, we'll
154
- // need a second "already patched" weakmap.
155
- if (!isFlaggedToPatch && specifier in mappings) {
156
- // eslint-disable-next-line prefer-const
157
- let { url, format, target } = mappings[specifier];
158
- // set flag to module or commonjs. i'm not sure this is needed but am keeping it
159
- // in place until we have to implement esm-native module rewrites/wrapping. this
160
- // is the point at which resolve() communicates to load().
161
- //
162
- // builtin's are probably the most likely to be loaded, so they're first.
163
- // some tweaks might be needed when we start to patch esm-native modules
164
- // in esm-native-code:
165
- // https://nodejs.org/docs/latest-v20.x/api/esm.html#builtin-modules
166
- // https://nodejs.org/docs/latest-v20.x/api/module.html#modulesyncbuiltinesmexports
167
- if (target === 'builtin') {
168
- url = `${url.href}?csi-flag=m`;
169
- format = 'module';
170
- } else if (target === 'commonjs') {
171
- url = `${url.href}?csi-flag=c`;
172
- format = getFileType(url) || format || 'commonjs';
173
- } else if (target === 'module') {
174
- url = `${url.href}?csi-flag=m`;
175
- format = getFileType(url) || format || 'module';
176
- } else {
177
- throw new Error(`unexpected target ${target} for ${specifier}`);
178
- }
179
-
180
- return {
181
- url,
182
- format,
183
- shortCircuit: true,
184
- };
185
- }
186
-
187
- return protectedNextResolve(nextResolve, specifier, context);
188
- };
189
-
190
- // readFile is a live binding. we need to capture it so it won't be
191
- // altered by any patching later.
192
- const readFile = rf;
193
-
194
- load = async function(url, context, nextLoad) {
195
- (log & logLoad) && console.log(`ESM HOOK(${W.threadId}) -> LOAD ${url}`);
196
-
197
- const urlObject = new URL(url);
198
-
199
- if (urlObject.searchParams.has('csi-flag')) {
200
- // target type will, i think, be used to determine if this will require extra
201
- // processing of some sort for esm-native modules.
202
- // eslint-disable-next-line no-unused-vars
203
- const targetType = urlObject.searchParams.get('csi-flag');
204
- const { pathname } = urlObject;
205
-
206
- (log & logLoad) && console.log(`ESM HOOK(${W.threadId}) -> LOAD -> CSI FLAG ${pathname}`);
207
-
208
- const source = await readFile(fixPath(pathname), 'utf8');
209
- return {
210
- source,
211
- format: 'module',
212
- shortCircuit: true,
213
- };
214
- }
215
-
216
- if (urlObject.pathname.endsWith('.node')) {
217
- const metaUrl = JSON.stringify(url);
218
- const addon = urlObject.pathname;
219
- return {
220
- source: `require("node:module").createRequire(${metaUrl})("${addon}")`,
221
- format: 'commonjs',
222
- shortCircuit: true,
223
- };
224
- }
225
-
226
- // if it's not a builtin, a .node addon, or a flagged file, it needs to be
227
- // rewritten if it's a module.
228
- if (urlObject.pathname.match(/(.js|.mjs|.cjs)$/)) {
229
- // if it's not a module it will be rewritten by the require hooks.
230
- const type = getFileType(urlObject);
231
- if (type !== 'module') {
232
- return nextLoad(url, context);
233
- }
234
-
235
- const filename = fixPath(urlObject.pathname);
236
- const source = await readFile(filename, 'utf8');
237
-
238
- const rewriteOptions = {
239
- filename,
240
- isModule: type === 'module',
241
- inject: true,
242
- wrap: type !== 'module', // cannot wrap modules
243
- };
244
-
245
- core.threadInfo.post('rewrite', rewriteOptions);
246
-
247
- const result = core.rewriter.rewrite(source, rewriteOptions);
248
-
249
- return {
250
- source: result.code,
251
- format: type || 'commonjs', // don't know what else to do here. log?
252
- shortCircuit: true,
253
- };
254
- }
255
- // this never gets called, so hmmm.
256
- return nextLoad(url, context);
257
- };
258
- }
259
-
260
- export {
261
- load,
262
- resolve,
263
- initialize,
264
- // TODO: add for testing/verification
265
- //loaderIsVerified as default
266
- };
@@ -1,133 +0,0 @@
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
- if (core) core = await startAgent({ core, options: { executor, installOrder } });
78
-
79
- // self identification
80
- // for communications between the main and loader thread (if present)
81
- if (core) 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
- export default core;
95
-
96
- async function executor(core) {
97
- if (!semver.satisfies(process.version, nodeEngine)) {
98
- let validRanges = '';
99
- nodeEngine.split('||').forEach((range) => {
100
- const minVersion = semver.minVersion(range).toString();
101
- const maxVersion = range.split('<').pop()
102
- .trim();
103
- validRanges += `${minVersion} and ${maxVersion}, `;
104
- });
105
- throw new Error(`Contrast supports Node LTS versions between ${validRanges}but detected ${process.version}.`);
106
- }
107
-
108
- core.startupValidation = {
109
- /**
110
- * This will run after we onboard to the UI and pick up any remote settings.
111
- */
112
- install() {
113
- if (
114
- isMainThread &&
115
- !core.config.getEffectiveValue('assess.enable') &&
116
- !core.config.getEffectiveValue('protect.enable')
117
- ) {
118
- throw new Error('Neither Assess nor Protect are enabled. Check local configuration and UI settings');
119
- }
120
- }
121
- };
122
-
123
- core.npmVersionRange = npmEngine;
124
- if (isMainThread) {
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
-
132
- return core; // should this return core or just null/undefined?
133
- }